Summary
With experimental.lazyBarrel (on by default since 1.1.0), rolldown prunes an import that is still referenced by retained code. A sideEffects:false module's import is dropped, but statement-level DCE keeps the now-dead derived-initializer statements that use it inside an __esm-wrapped consumer, so the emitted bundle references a binding that is never declared/imported → ReferenceError: eg is not defined at module evaluation.
experimental.lazyBarrel: false fixes it.
Versions
- Broken: rolldown 1.1.2 and 1.1.3 (latest).
- Works: rolldown 1.0.3 (lazyBarrel was off by default before 1.1.0), and any version with
experimental.lazyBarrel: false.
Reproduction
Repro repo: https://github.com/hyf0/rolldown-lazybarrel-reexport-repro
Pure JS, only dependency is rolldown. Five source modules (inlined below).
package.json
{
"type": "module",
"private": true,
"sideEffects": false,
"dependencies": { "rolldown": "1.1.3" }
}
src/lib/eg.js — defines eg (the import that gets pruned)
const primitives = { string: { tag: 's' }, number: { tag: 'n' } };
const higher = { object: (shape) => ({ tag: 'o', shape, parse: () => true }) };
export const eg = { ...primitives, ...higher };
src/lib/account.js — sideEffects:false; imports eg, defines derived initializers
import { eg } from './eg.js';
export const AccountSettings = eg.object({ a: eg.string, n: eg.number });
export const AccountMeta = eg.object({ b: eg.string });
src/lib/index.js — re-exports the schema module via export *, plus an unrelated util
export * from './account.js';
export const objectKeys = (o) => Object.keys(o);
src/story.js — ESM consumer; imports only the util, never the schemas
import { objectKeys } from './lib/index.js';
export const run = () => (objectKeys ? 'ok' : 'no');
src/entry.cjs — CommonJS entry that requires the ESM consumer → wraps it in __esm
const { run } = require('./story.js');
console.log('result:', run(), 'REPRO_OK');
build.mjs
import { rolldown } from 'rolldown';
const disable = process.env.LAZY_BARREL === 'off';
const bundle = await rolldown({
input: { entry: './src/entry.cjs' },
...(disable ? { experimental: { lazyBarrel: false } } : {}),
});
await bundle.write({ dir: disable ? './dist-fixed' : './dist', format: 'es' });
await bundle.close();
npm install
node build.mjs && node dist/entry.js # ReferenceError: eg is not defined
LAZY_BARREL=off node build.mjs && node dist-fixed/entry.js # result: ok REPRO_OK
What the emitted code looks like
In dist/entry.js (lazyBarrel on) the whole eg.js defining region (primitives, higher, eg, the init_eg initializer) is absent, yet the __esm-wrapped module still contains the retained eg.object(...) initializers referencing eg. Diffing against the lazyBarrel:false bundle, the only difference is the missing eg.js region and its init_eg() calls — i.e. lazyBarrel pruned exactly the still-needed defining code.
Minimal trigger (each verified necessary by deletion)
- an
export * re-export of a separate sideEffects:false module that imports eg, distinct from the module providing the export the consumer actually uses (collapsing the schema into the consumed module, or using a named re-export instead of export *, hides it);
- a CommonJS → ESM
require that wraps the consumer in the __esm helper (a pure-ESM import chain does not reproduce — DCE then drops the dead initializers too);
experimental.lazyBarrel enabled (the default).
Not required: nested barrels, code-splitting, minification, legacy.inconsistentCjsInterop, Vite/Storybook.
Relation to prior issues
Same family as #9806 (flat barrel, no wrap) and #9691 / #9709 (codeSplitting:false dynamic-import wrap). The closest prior fix is #9757 ("load locally-used imports on a re-exported record", closing #9713), which is included in 1.1.2/1.1.3 — but this variant still reproduces on 1.1.3, so it is not covered by that fix.
Workaround
experimental: { lazyBarrel: false }.
Summary
With
experimental.lazyBarrel(on by default since 1.1.0), rolldown prunes an import that is still referenced by retained code. AsideEffects:falsemodule'simportis dropped, but statement-level DCE keeps the now-dead derived-initializer statements that use it inside an__esm-wrapped consumer, so the emitted bundle references a binding that is never declared/imported →ReferenceError: eg is not definedat module evaluation.experimental.lazyBarrel: falsefixes it.Versions
experimental.lazyBarrel: false.Reproduction
Repro repo: https://github.com/hyf0/rolldown-lazybarrel-reexport-repro
Pure JS, only dependency is
rolldown. Five source modules (inlined below).package.json{ "type": "module", "private": true, "sideEffects": false, "dependencies": { "rolldown": "1.1.3" } }src/lib/eg.js— defineseg(the import that gets pruned)src/lib/account.js—sideEffects:false; importseg, defines derived initializerssrc/lib/index.js— re-exports the schema module viaexport *, plus an unrelated utilsrc/story.js— ESM consumer; imports only the util, never the schemassrc/entry.cjs— CommonJS entry thatrequires the ESM consumer → wraps it in__esmbuild.mjsWhat the emitted code looks like
In
dist/entry.js(lazyBarrel on) the wholeeg.jsdefining region (primitives,higher,eg, theinit_eginitializer) is absent, yet the__esm-wrapped module still contains the retainedeg.object(...)initializers referencingeg. Diffing against thelazyBarrel:falsebundle, the only difference is the missingeg.jsregion and itsinit_eg()calls — i.e. lazyBarrel pruned exactly the still-needed defining code.Minimal trigger (each verified necessary by deletion)
export *re-export of a separatesideEffects:falsemodule that importseg, distinct from the module providing the export the consumer actually uses (collapsing the schema into the consumed module, or using a named re-export instead ofexport *, hides it);requirethat wraps the consumer in the__esmhelper (a pure-ESM import chain does not reproduce — DCE then drops the dead initializers too);experimental.lazyBarrelenabled (the default).Not required: nested barrels, code-splitting, minification,
legacy.inconsistentCjsInterop, Vite/Storybook.Relation to prior issues
Same family as #9806 (flat barrel, no wrap) and #9691 / #9709 (
codeSplitting:falsedynamic-import wrap). The closest prior fix is #9757 ("load locally-used imports on a re-exported record", closing #9713), which is included in 1.1.2/1.1.3 — but this variant still reproduces on 1.1.3, so it is not covered by that fix.Workaround
experimental: { lazyBarrel: false }.