Skip to content

experimental.lazyBarrel drops a still-referenced import from a sideEffects:false export * barrel under a CJS-wrapped ESM consumer → ReferenceError (repros on 1.1.3) #9964

Description

@hyf0

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.jssideEffects: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)

  1. 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);
  2. 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);
  3. 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 }.

Metadata

Metadata

Type

No type

Fields

Priority

None yet

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions