Skip to content

feat: support inlineConst for CJS exports accessed through module.exports#8976

Open
h-a-n-a wants to merge 1 commit intomainfrom
04-01-feat_support_inlineconst_for_cjs_exports_accessed_through_module.exports
Open

feat: support inlineConst for CJS exports accessed through module.exports#8976
h-a-n-a wants to merge 1 commit intomainfrom
04-01-feat_support_inlineconst_for_cjs_exports_accessed_through_module.exports

Conversation

@h-a-n-a
Copy link
Copy Markdown
Member

@h-a-n-a h-a-n-a commented Apr 1, 2026

Summary

When .default represents module.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

  • Extended esm_import_cjs_with_esmodule_flag fixture with ns.default.foo, ns.foo, named imports, and default imports
  • Added esm_import_cjs_with_esmodule_flag_non_const fixture for non-constant exports to verify the non-inlined path
  • All existing cjs_compat and inline_const tests pass

Copy link
Copy Markdown
Member Author

h-a-n-a commented Apr 1, 2026


How to use the Graphite Merge Queue

Add 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.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 .default is module.exports.
  • Expand the existing esm_import_cjs_with_esmodule_flag fixture to cover ns.default.foo, ns.foo, and various import forms.
  • Add a new esm_import_cjs_with_esmodule_flag_non_const fixture 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 .default is treated as module.exports, the code still unconditionally depended_refs.push(m.symbol_ref) (and marks it written on assignment). Here m.symbol_ref is the CJS export for the current property (default), but that export is not actually what gets read/written in the module.exports case. This can force-include exports.default statements and/or incorrectly disable const inlining for default due to unrelated writes like ns.default.foo = .... Consider only adding m.symbol_ref to depended_refs/written_cjs_exports when that symbol is the actual resolution target; for the module.exports path, 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);
                    }

Comment on lines +614 to +616
target_commonjs_exported_symbol = self.module_table[cjs_idx]
.as_normal()
.map(|m| (m.namespace_object_ref, true));
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Suggested change
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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

@h-a-n-a h-a-n-a Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Apr 1, 2026

Merging this PR will not alter performance

✅ 4 untouched benchmarks
⏩ 10 skipped benchmarks1


Comparing 04-01-feat_support_inlineconst_for_cjs_exports_accessed_through_module.exports (f1e6e0d) with main (76ce0cf)

Open in CodSpeed

Footnotes

  1. 10 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@h-a-n-a h-a-n-a force-pushed the 04-01-feat_support_inlineconst_for_cjs_exports_accessed_through_module.exports branch from b69cbfd to fb1f2f0 Compare April 1, 2026 04:13
@h-a-n-a h-a-n-a force-pushed the 04-01-fix_correct_inlining_based_on_module_s_def_format_and_esmodule_flag branch from 809c72c to 1c6712f Compare April 1, 2026 04:13
@h-a-n-a h-a-n-a force-pushed the 04-01-feat_support_inlineconst_for_cjs_exports_accessed_through_module.exports branch 2 times, most recently from 6bb9820 to d6eb912 Compare April 1, 2026 04:27
@graphite-app graphite-app bot changed the base branch from 04-01-fix_correct_inlining_based_on_module_s_def_format_and_esmodule_flag to graphite-base/8976 April 1, 2026 05:35
@graphite-app graphite-app bot force-pushed the graphite-base/8976 branch from 1c6712f to 76ce0cf Compare April 1, 2026 05:40
@graphite-app graphite-app bot force-pushed the 04-01-feat_support_inlineconst_for_cjs_exports_accessed_through_module.exports branch from d6eb912 to cb7e7ad Compare April 1, 2026 05:40
@graphite-app graphite-app bot changed the base branch from graphite-base/8976 to main April 1, 2026 05:40
@graphite-app graphite-app bot force-pushed the 04-01-feat_support_inlineconst_for_cjs_exports_accessed_through_module.exports branch from cb7e7ad to f1e6e0d Compare April 1, 2026 05:40
@netlify
Copy link
Copy Markdown

netlify bot commented Apr 1, 2026

Deploy Preview for rolldown-rs canceled.

Name Link
🔨 Latest commit f1e6e0d
🔍 Latest deploy log https://app.netlify.com/projects/rolldown-rs/deploys/69ccafede93faf0008ea3273

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants