Conversation
How to use the Graphite Merge QueueAdd the label graphite: merge-when-ready to this PR to add it to the merge queue. You must have a Graphite account in order to use the merge queue. Sign up using this link. An organization admin has enabled the Graphite Merge Queue in this repository. Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue. This stack of pull requests is managed by Graphite. Learn more about stacking. |
There was a problem hiding this comment.
Pull request overview
Adds CJS-interop support so that when .default represents module.exports (not exports.default), subsequent property accesses (e.g. ns.default.foo) can be resolved to the corresponding CJS export and eligible for inlineConst.
Changes:
- Extend link-stage member-expression resolution to treat
*.default.<prop>as a CJS export access when.defaultismodule.exports. - Expand the existing
esm_import_cjs_with_esmodule_flagfixture to coverns.default.foo,ns.foo, and various import forms. - Add a new
esm_import_cjs_with_esmodule_flag_non_constfixture to ensure non-constant CJS exports don’t get inlined.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| crates/rolldown/src/stages/link_stage/bind_imports_and_exports.rs | Adds special-case resolution for .default-as-module.exports to enable resolving/inlining the next property as a CJS export. |
| crates/rolldown/tests/rolldown/cjs_compat/esm_import_cjs_with_esmodule_flag/main.js | Extends assertions to cover default/named imports and .default.foo access behavior. |
| crates/rolldown/tests/rolldown/cjs_compat/esm_import_cjs_with_esmodule_flag/cjs.cjs | Adds exports.foo to provide an additional CJS export for resolution/inlining. |
| crates/rolldown/tests/rolldown/cjs_compat/esm_import_cjs_with_esmodule_flag/cjs2.cjs | Adds a module.exports = ... case to test .default behavior when module.exports is non-object. |
| crates/rolldown/tests/rolldown/cjs_compat/esm_import_cjs_with_esmodule_flag/artifacts.snap | Updates snapshot to reflect new imports and inlining behavior. |
| crates/rolldown/tests/rolldown/cjs_compat/esm_import_cjs_with_esmodule_flag_non_const/_config.json | New fixture config enabling optimization.inlineConst. |
| crates/rolldown/tests/rolldown/cjs_compat/esm_import_cjs_with_esmodule_flag_non_const/package.json | New fixture set to Node ESM mode (\"type\": \"module\"). |
| crates/rolldown/tests/rolldown/cjs_compat/esm_import_cjs_with_esmodule_flag_non_const/main.js | New assertions ensuring non-constant exports aren’t inlined. |
| crates/rolldown/tests/rolldown/cjs_compat/esm_import_cjs_with_esmodule_flag_non_const/cjs.cjs | New non-constant exports.default via JSON.parse to test non-inlined path. |
| crates/rolldown/tests/rolldown/cjs_compat/esm_import_cjs_with_esmodule_flag_non_const/cjs2.cjs | New non-constant module.exports via JSON.parse to test non-inlined path. |
| crates/rolldown/tests/rolldown/cjs_compat/esm_import_cjs_with_esmodule_flag_non_const/artifacts.snap | New snapshot for the non-const fixture. |
Comments suppressed due to low confidence (1)
crates/rolldown/src/stages/link_stage/bind_imports_and_exports.rs:626
- When resolving
ns.default.<prop>where.defaultis treated asmodule.exports, the code still unconditionallydepended_refs.push(m.symbol_ref)(and marks it written on assignment). Herem.symbol_refis the CJS export for the current property (default), but that export is not actually what gets read/written in themodule.exportscase. This can force-includeexports.defaultstatements and/or incorrectly disable const inlining fordefaultdue to unrelated writes likens.default.foo = .... Consider only addingm.symbol_reftodepended_refs/written_cjs_exportswhen that symbol is the actual resolution target; for themodule.exportspath, depend on the resolved next property (or namespace) instead.
target_commonjs_exported_symbol = Some((m.symbol_ref, is_default));
}
depended_refs.push(m.symbol_ref);
// If this member expression is a write (e.g. `cjs.c = 'abcd'`), the
// CJS exported symbol should not be inlined as a constant since its
// value may change at runtime.
if member_expr_ref.is_write {
written_cjs_exports.push(m.symbol_ref);
}
| target_commonjs_exported_symbol = self.module_table[cjs_idx] | ||
| .as_normal() | ||
| .map(|m| (m.namespace_object_ref, true)); |
There was a problem hiding this comment.
default_is_module_exports now sets target_commonjs_exported_symbol to the CJS module's namespace_object_ref when accessing just .default (no following property). Since include_statements skips the CJS bailout check whenever target_commonjs_exported_symbol is Some(_), this can incorrectly allow CJS tree-shaking even though .default represents the entire module.exports object (opaque use should bail out and keep all exports). Consider leaving target_commonjs_exported_symbol as None for the .default-as-module.exports case (or otherwise ensure bailout still happens for this specific scenario).
| target_commonjs_exported_symbol = self.module_table[cjs_idx] | |
| .as_normal() | |
| .map(|m| (m.namespace_object_ref, true)); | |
| // `.default` here refers to the entire CJS `module.exports` object. | |
| // Do NOT set `target_commonjs_exported_symbol` so that the CJS bailout | |
| // logic still runs and keeps all exports for this opaque usage. |
There was a problem hiding this comment.
Nice catch. module.exports is an opaque access in this case. We have no idea which export it is accessing. So we should bail out in this case.
Merging this PR will not alter performance
Comparing Footnotes
|
b69cbfd to
fb1f2f0
Compare
809c72c to
1c6712f
Compare
6bb9820 to
d6eb912
Compare
1c6712f to
76ce0cf
Compare
d6eb912 to
cb7e7ad
Compare
cb7e7ad to
f1e6e0d
Compare
✅ Deploy Preview for rolldown-rs canceled.
|

Summary
When
.defaultrepresentsmodule.exports, properties accessed through it (e.g.ns.default.foo) can now be resolved and inlined by looking up the next property in the CJS module's exports.Test plan
esm_import_cjs_with_esmodule_flagfixture withns.default.foo,ns.foo, named imports, and default importsesm_import_cjs_with_esmodule_flag_non_constfixture for non-constant exports to verify the non-inlined pathcjs_compatandinline_consttests pass