Problem
I found this while bundling Storybook. The reproduction below is reduced, but the original failure was a generated Storybook bundle where a lazy __esmMin wrapper still referenced external bindings after the corresponding external import had been removed.
https://github.com/storybookjs/storybook/blob/d0ef37ec3f69e9ceff6328e9772cc506dd58f995/code/builders/builder-vite/src/utils/vite-features.ts#L27
When strictExecutionOrder is enabled, Rolldown can generate a shared chunk that still references bindings from an external module, but the corresponding external import statement is missing from the output.
The generated code can look like this:
var init_leaf = __esmMin(() => {
value = first(second);
});
In that chunk, first and second came from an external import:
import { first, second } from "dep";
but the import declaration is removed, so the emitted chunk contains free identifiers and fails at runtime.
Expected behavior
If generated code still references bindings from an external import, the ImportDeclaration that defines those bindings should remain in the same output chunk.
Why this seems tied to strictExecutionOrder
With strictExecutionOrder, ESM modules may be wrapped in lazy __esmMin initializers to preserve evaluation order across entries. That wrapper can keep top-level module code alive even after tree-shaking. In this case, the wrapper body still uses the external bindings, but finalization removes the external import statement because the import record does not resolve to an internal module.
Minimal reproduction
import { rolldown } from 'rolldown';
const modules = {
'entry-a': `import { value } from 'first'; console.log(value); export { read } from 'first';`,
'entry-b': `import { value } from 'second'; console.log(value); export { read } from 'second';`,
first: `export { value, read } from 'shared';`,
second: `export { value, read } from 'shared';`,
shared: `export { value, read } from 'leaf';`,
leaf: `import { first, second } from 'dep'; export const value = first(second); export function read() { return value; }`,
};
const plugin = {
name: 'virtual',
resolveId(id) {
if (id in modules) return id;
},
load(id) {
return modules[id];
},
};
const bundle = await rolldown({
input: {
a: 'entry-a',
b: 'entry-b',
},
external: ['dep'],
platform: 'browser',
plugins: [plugin],
treeshake: {
moduleSideEffects: false,
},
});
const { output } = await bundle.generate({
format: 'es',
strictExecutionOrder: true,
minify: false,
});
for (const chunk of output) {
if (chunk.type === 'chunk' && chunk.code.includes('first(second)')) {
console.log(chunk.code);
}
}
Related PR: #10009
Problem
I found this while bundling Storybook. The reproduction below is reduced, but the original failure was a generated Storybook bundle where a lazy
__esmMinwrapper still referenced external bindings after the corresponding external import had been removed.https://github.com/storybookjs/storybook/blob/d0ef37ec3f69e9ceff6328e9772cc506dd58f995/code/builders/builder-vite/src/utils/vite-features.ts#L27
When
strictExecutionOrderis enabled, Rolldown can generate a shared chunk that still references bindings from an external module, but the corresponding externalimportstatement is missing from the output.The generated code can look like this:
In that chunk,
firstandsecondcame from an external import:but the import declaration is removed, so the emitted chunk contains free identifiers and fails at runtime.
Expected behavior
If generated code still references bindings from an external import, the
ImportDeclarationthat defines those bindings should remain in the same output chunk.Why this seems tied to
strictExecutionOrderWith
strictExecutionOrder, ESM modules may be wrapped in lazy__esmMininitializers to preserve evaluation order across entries. That wrapper can keep top-level module code alive even after tree-shaking. In this case, the wrapper body still uses the external bindings, but finalization removes the external import statement because the import record does not resolve to an internal module.Minimal reproduction
Related PR: #10009