Skip to content

[Bug]: transform.dropLabels does not prevent dynamic imports inside dropped blocks from being resolved #9521

@theaddonn

Description

@theaddonn

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

rolldown: 1.0.2

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.

Metadata

Metadata

Type

Priority

None yet

Effort

None yet

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions