Reproduction link or steps
// main.ts
DEBUG: {
import('./debug.ts').then(({ init }) => init());
}
// debug.ts
import { SomeClass } from 'some-uninstalled-package';
export function init() { ... }
// rolldown.config.ts
import { rolldown } from 'rolldown';
const build = await rolldown({
input: 'main.ts',
transform: { dropLabels: ['DEBUG'] },
});
await build.write({ file: 'out.js', format: 'esm' });
some-uninstalled-package is not installed. Expected: build succeeds because the DEBUG: block is dropped. Actual: build fails with a resolution error for some-uninstalled-package.
What is expected?
transform.dropLabels removes the labeled block from the source before the module is scanned for imports. The dynamic import('./debug.ts') inside the DEBUG: block is therefore never seen by the module graph traversal, and debug.ts (along with its transitive dependencies) is never resolved or bundled.
This matches Rollup's behavior: transform hooks run on a module's source before that source is parsed for its imports. If a transform removes an import() call, Rollup does not follow it.
What is actually happening?
Rolldown resolves dynamic imports before applying transform.dropLabels. The DEBUG: block is dropped from the output, but the import('./debug.ts') call has already been discovered and added to the module graph. Rolldown then attempts to resolve debug.ts and all its transitive dependencies, including some-uninstalled-package, causing a build failure.
This also means that even when the package IS resolvable, its code ends up in the module graph and must be explicitly excluded via a virtual stub plugin- defeating the purpose of label-based dead code elimination for dynamic imports.
System Info
Any additional comments?
Discovered while implementing a debug/release build split for a Minecraft scripting addon. The intent was to use DEBUG: labels to gate debug-only imports so the release build has zero trace of the debug module chain. Static imports were already known to require special handling, but dynamic imports inside dropped labels were expected to be transparently excluded.
The workaround is a virtual stub plugin that intercepts the problematic package's resolution and returns empty exports:
const stub = {
name: 'stub',
resolveId: (id) => id.includes('some-uninstalled-package') ? '\0stub' : undefined,
load: (id) => id === '\0stub' ? 'export const SomeClass = undefined;' : undefined,
};
This works but is not maintainable- it requires manually tracking every export of every debug-only dependency. The root fix would be to apply dropLabels (and other built-in OXC transforms in InputOptions.transform) before the module is scanned for imports, consistent with how Rollup processes plugin transform hooks.
Reproduction link or steps
some-uninstalled-packageis not installed. Expected: build succeeds because theDEBUG:block is dropped. Actual: build fails with a resolution error forsome-uninstalled-package.What is expected?
transform.dropLabelsremoves the labeled block from the source before the module is scanned for imports. The dynamicimport('./debug.ts')inside theDEBUG:block is therefore never seen by the module graph traversal, anddebug.ts(along with its transitive dependencies) is never resolved or bundled.This matches Rollup's behavior: transform hooks run on a module's source before that source is parsed for its imports. If a transform removes an
import()call, Rollup does not follow it.What is actually happening?
Rolldown resolves dynamic imports before applying
transform.dropLabels. TheDEBUG:block is dropped from the output, but theimport('./debug.ts')call has already been discovered and added to the module graph. Rolldown then attempts to resolvedebug.tsand all its transitive dependencies, includingsome-uninstalled-package, causing a build failure.This also means that even when the package IS resolvable, its code ends up in the module graph and must be explicitly excluded via a virtual stub plugin- defeating the purpose of label-based dead code elimination for dynamic imports.
System Info
Any additional comments?
Discovered while implementing a debug/release build split for a Minecraft scripting addon. The intent was to use
DEBUG:labels to gate debug-only imports so the release build has zero trace of the debug module chain. Static imports were already known to require special handling, but dynamic imports inside dropped labels were expected to be transparently excluded.The workaround is a virtual stub plugin that intercepts the problematic package's resolution and returns empty exports:
This works but is not maintainable- it requires manually tracking every export of every debug-only dependency. The root fix would be to apply
dropLabels(and other built-in OXC transforms inInputOptions.transform) before the module is scanned for imports, consistent with how Rollup processes plugintransformhooks.