Reproduction link or steps
This came from investigating vitejs/vite#22435, but the behavior reproduces with Rolldown directly and without Backbone.
A minimal fixture added under crates/rolldown/tests/rolldown/function/experimental/strict_execution_order/cjs_namespace_late_mutation:
// main.js
import * as target from './target.cjs';
import './augment.cjs';
export const result = {
existing: target.existing,
addedType: typeof target.added,
defaultAddedType: typeof target.default?.added,
};
// target.cjs
module.exports = {
existing: 'existing',
};
// augment.cjs
const target = require('./target.cjs');
target.added = function added() {
return 'added';
};
// _test.mjs
import assert from 'node:assert';
const { result } = await import('./dist/main.js');
assert.deepStrictEqual(result, {
existing: 'existing',
addedType: 'function',
defaultAddedType: 'function',
});
// _config.json
{
config: {
input: [
{
name: main,
import: ./main.js
}
]
},
expectExecuted: false,
snapshot: false
}
Run:
NEEDS_EXTENDED=false cargo run-fixture crates/rolldown/tests/rolldown/function/experimental/strict_execution_order/cjs_namespace_late_mutation/_config.json
What is expected?
The namespace object should observe target.added after the side-effect CommonJS module has executed, matching ESM static import execution semantics and the behavior of Rollup + @rollup/plugin-commonjs for the same fixture.
In other words, result should be:
{
existing: 'existing',
addedType: 'function',
defaultAddedType: 'function',
}
What is actually happening?
The test fails with:
AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
+ actual - expected
{
+ addedType: 'undefined',
- addedType: 'function',
defaultAddedType: 'function',
existing: 'existing'
}
The generated output initializes the CJS namespace wrapper before the later side-effect require mutates the same CommonJS export object:
var import_target = __toESM(require_target(), 1);
require_augment();
__toESM snapshots/copies the current own properties from module.exports, so target.added is missing on the namespace object. target.default.added works because default still points to the mutable original CommonJS export object.
A named import variant works because the output keeps a direct reference to the CJS export object and reads the property after the side-effect module runs:
var import_target = require_target();
require_augment();
typeof import_target.added;
System Info
OS: Windows 11
Node: v24.14.0
pnpm: 11.1.2
rustc: 1.95.0 (59807616e 2026-04-14)
cargo: 1.95.0 (f2d3ce0bd 2026-03-21)
Rolldown repo commit tested: bf468a4dc
Any additional comments?
This affects Vite 8 production builds for legacy CommonJS packages that augment another CommonJS package via side effects. One concrete downstream case is backbone-relational augmenting backbone: import * as Backbone from backbone; import backbone-relational works in Vite dev and Vite 7 build, but fails in Vite 8 build because the namespace wrapper is created before the augmentation runs.
Reproduction link or steps
This came from investigating vitejs/vite#22435, but the behavior reproduces with Rolldown directly and without Backbone.
A minimal fixture added under
crates/rolldown/tests/rolldown/function/experimental/strict_execution_order/cjs_namespace_late_mutation:Run:
What is expected?
The namespace object should observe
target.addedafter the side-effect CommonJS module has executed, matching ESM static import execution semantics and the behavior of Rollup +@rollup/plugin-commonjsfor the same fixture.In other words,
resultshould be:What is actually happening?
The test fails with:
The generated output initializes the CJS namespace wrapper before the later side-effect
requiremutates the same CommonJS export object:__toESMsnapshots/copies the current own properties frommodule.exports, sotarget.addedis missing on the namespace object.target.default.addedworks becausedefaultstill points to the mutable original CommonJS export object.A named import variant works because the output keeps a direct reference to the CJS export object and reads the property after the side-effect module runs:
System Info
Any additional comments?
This affects Vite 8 production builds for legacy CommonJS packages that augment another CommonJS package via side effects. One concrete downstream case is
backbone-relationalaugmentingbackbone:import * as Backbone from backbone; import backbone-relationalworks in Vite dev and Vite 7 build, but fails in Vite 8 build because the namespace wrapper is created before the augmentation runs.