Skip to content

Comments

feat: skip __toESM helper when only named imports are used from CJS modules#6850

Merged
sapphi-red merged 8 commits intomainfrom
copilot/skip-toesm-when-default-unstated
Nov 5, 2025
Merged

feat: skip __toESM helper when only named imports are used from CJS modules#6850
sapphi-red merged 8 commits intomainfrom
copilot/skip-toesm-when-default-unstated

Conversation

Copy link
Contributor

Copilot AI commented Nov 5, 2025

  • Understand the issue: skip __toESM when only named exports (not default) are imported from CJS
  • Explore the codebase to find where __toESM wrapping is applied
  • Add helper function import_needs_interop to check if interop is needed
  • Modify CJS import wrapping logic to conditionally use __toESM
  • Update reference_needed_symbols to conditionally reference __toESM runtime helper
  • Build and manually verify the optimization works
  • Add proper test case for the optimization
  • Run existing tests to ensure no regressions
  • Fix linting issues
  • Address code review feedback - extract shared logic
  • Address PR review comments - simplify code and remove test artifacts
  • Final validation - all tests and linting pass
Original prompt

This section details on the original issue you should resolve

<issue_title>[Feature Request]: skip __toESM when the default export is not used</issue_title>
<issue_description>### What problem does this feature solve?

__toESM helper brings in many helper functions and makes the bundle size bigger.

__toESM helper exists to add the default export in some condition. While the condition is not known until runtime, we can know whether the default import is used or not. When the default import is not used, we can skip the __toESM helper.

For this input:

// index.js
import { foo } from './foo.cjs'
console.log(foo)
// foo.cjs
exports.foo = 'foo'

Rolldown currently outputs:

// omit helper implementations
//#region foo.cjs
var require_foo = __commonJS({ "foo.cjs"(exports) {
	exports.foo = "foo";
} });

//#endregion
//#region index.js
var import_foo = __toESM(require_foo());
console.log(import_foo.foo);

//#endregion

But since we know that the default import is not used, we can improve this to:

// omit helper implementations
//#region foo.cjs
var require_foo = __commonJS({ "foo.cjs"(exports) {
	exports.foo = "foo";
} });

//#endregion
//#region index.ts
var import_foo = require_foo();
console.log(import_foo.foo);

//#endregion

What does the proposed API look like?

Not an API change</issue_description>

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

@graphite-app
Copy link
Contributor

graphite-app bot commented Nov 5, 2025

How to use the Graphite Merge Queue

Add the label graphite: merge 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.

@netlify
Copy link

netlify bot commented Nov 5, 2025

Deploy Preview for rolldown-rs ready!

Name Link
🔨 Latest commit 39464c0
🔍 Latest deploy log https://app.netlify.com/projects/rolldown-rs/deploys/690b888c3e73c20007493d3f
😎 Deploy Preview https://deploy-preview-6850--rolldown-rs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copilot AI and others added 5 commits November 5, 2025 05:08
Add optimization to skip __toESM helper when only named imports
are used from CJS modules. This reduces bundle size by avoiding
unnecessary helper code for named-only imports.

Co-authored-by: sapphi-red <[email protected]>
Also update reference_needed_symbols to conditionally reference
__toESM only for namespace or default imports, not for named-only
imports. This eliminates the helper from the runtime completely
when it's not needed.

Co-authored-by: sapphi-red <[email protected]>
Move the logic for checking if an import needs interop to a shared
utility function to avoid code duplication between module_finalizers
and reference_needed_symbols.

Co-authored-by: sapphi-red <[email protected]>
Copilot AI changed the title [WIP] Add feature to skip __toESM helper when default export is unused feat: skip __toESM helper when only named imports are used from CJS modules Nov 5, 2025
Copilot AI requested a review from sapphi-red November 5, 2025 05:40
@github-actions
Copy link
Contributor

github-actions bot commented Nov 5, 2025

Benchmarks Rust

  • target: main(35fafd0)
  • pr: copilot/skip-toesm-when-default-unstated(39464c0)
group                                                        pr                                     target
-----                                                        --                                     ------
bundle/bundle@multi-duplicated-top-level-symbol              1.02     68.0±1.68ms        ? ?/sec    1.00     66.6±1.59ms        ? ?/sec
bundle/bundle@multi-duplicated-top-level-symbol-sourcemap    1.01     75.0±1.94ms        ? ?/sec    1.00     74.2±3.09ms        ? ?/sec
bundle/bundle@rome_ts                                        1.00    111.5±3.09ms        ? ?/sec    1.00    111.8±2.77ms        ? ?/sec
bundle/bundle@rome_ts-sourcemap                              1.01    124.8±2.62ms        ? ?/sec    1.00    123.8±1.70ms        ? ?/sec
bundle/bundle@threejs                                        1.00     40.5±0.89ms        ? ?/sec    1.00     40.5±2.47ms        ? ?/sec
bundle/bundle@threejs-sourcemap                              1.01     44.2±1.14ms        ? ?/sec    1.00     43.6±0.79ms        ? ?/sec
bundle/bundle@threejs10x                                     1.00    400.9±4.69ms        ? ?/sec    1.00    400.2±8.13ms        ? ?/sec
bundle/bundle@threejs10x-sourcemap                           1.00    460.6±4.04ms        ? ?/sec    1.00    459.0±3.32ms        ? ?/sec
scan/scan@rome_ts                                            1.00     87.4±1.62ms        ? ?/sec    1.01     88.7±1.79ms        ? ?/sec
scan/scan@threejs                                            1.00     29.2±0.43ms        ? ?/sec    1.02     29.9±2.11ms        ? ?/sec
scan/scan@threejs10x                                         1.01    307.0±6.15ms        ? ?/sec    1.00    303.2±4.12ms        ? ?/sec

- Inline import_record_needs_interop call directly in if condition
- Remove manually added rolldown.config.mjs files from test directories

Co-authored-by: sapphi-red <[email protected]>
@sapphi-red sapphi-red marked this pull request as ready for review November 5, 2025 08:05
@IWANABETHATGUY IWANABETHATGUY enabled auto-merge (squash) November 5, 2025 14:40
@sapphi-red sapphi-red disabled auto-merge November 5, 2025 17:24
@sapphi-red sapphi-red enabled auto-merge (squash) November 5, 2025 17:25
Copy link
Member

hyf0 commented Nov 5, 2025

Merge activity

  • Nov 5, 5:41 PM UTC: The merge label 'graphite: merge' was detected. This PR will be added to the Graphite merge queue once it meets the requirements.
  • Nov 5, 5:44 PM UTC: The merge label 'graphite: merge' was detected. This PR will be added to the Graphite merge queue once it meets the requirements.

@sapphi-red sapphi-red merged commit 64dfc87 into main Nov 5, 2025
32 checks passed
@sapphi-red sapphi-red deleted the copilot/skip-toesm-when-default-unstated branch November 5, 2025 17:42
shulaoda added a commit that referenced this pull request Nov 10, 2025
## [1.0.0-beta.48] - 2025-11-10

:boom: Breaking Changes

- `this.emitFile` now respects `chunkFileNames` for chunk type
```js
// rolldown.config.js
export default {
  output: {
    chunkFileNames: 'chunks/[name]-[hash].js'
  }
}

// In plugin
this.emitFile({
  type: 'chunk',
  id: './my-module.js'
});

// Before: Output might not follow chunkFileNames pattern
// After: Output follows 'chunks/[name]-[hash].js' pattern
```

- Deprecated top-level options removed
  - `define` → `transform.define`
  - `inject` → `transform.inject`
  - `dropLabels` → `transform.dropLabels`
  - `keepNames` → `output.keepNames`
  - `profilerNames` → `output.generatedCode.profilerNames`

- Stable plugins moved from experimental
```js
// Before
import { replacePlugin, esmExternalRequirePlugin } from 'rolldown/experimental';

// After
import { replacePlugin, esmExternalRequirePlugin } from 'rolldown/plugins';
```

- `RolldownBuild#scan` is removed, now only available from `rolldown/experimental`
```js
// Before: scan was a method on RolldownBuild
const build = await rolldown(config);
await build.scan();

// After: import scan from rolldown/experimental
import { scan } from 'rolldown/experimental';
await scan(config);
```

### 💥 BREAKING CHANGES

- `this.emitFile` does not respect `chunkFileNames` (#6868) by @Copilot
- remove deprecated top-level `dropLabels` option (#6915) by @sapphi-red
- remove deprecated top-level `keepNames` option (#6914) by @sapphi-red
- remove deprecated top-level `profilerNames` option (#6913) by @sapphi-red
- remove deprecated top-level `define` and `inject` options (#6912) by @sapphi-red
- move stable plugins from experimental to `rolldown/plugins` (#6303) by @shulaoda
- node: remove experimental `RolldownBuild#scan`, only expose it from `rolldown/experimental` (#6889) by @hyf0

### 🚀 Features

- add side-effect detection for global constructors with primitive arguments (#6898) by @IWANABETHATGUY
- rust: use `BundleMode` to handle incremental build exhaustively (#6894) by @hyf0
- detect side-effect-free global function calls (#6897) by @IWANABETHATGUY
- expose `parseSync` / `parseAsync` function (#6866) by @sapphi-red
- skip `__toESM` helper when only named imports are used from CJS modules (#6850) by @Copilot
- rolldown_binding: expose `htmlInlineProxyPlugin` (#6856) by @shulaoda
- rolldown_plugin_html_inline_proxy: align `load` hook logic (#6855) by @shulaoda
- rolldown_plugin_html_inline_proxy: align `resolveId` hook logic (#6854) by @shulaoda
- rolldown_plugin_html_inline_proxy: initialize (#6853) by @shulaoda

### 🐛 Bug Fixes

- cli: support nested options in CLI properly (#6911) by @sapphi-red
- debug: ensure injecting `hook_resolve_id_trigger` correctly (#6908) by @hyf0
- use chunk-specific exports for entry module export detection (#6904) by @IWANABETHATGUY
- debug: ensure build get injected and add tests (#6896) by @hyf0
- error: return friendly error for bundler already closed scenario (#6878) by @hyf0
- improve dynamic entry processing with iterative approach (#6869) by @IWANABETHATGUY
- handle tsconfig option resolve error (#6871) by @sapphi-red
- handle error when creating output chunk directories (#6870) by @sapphi-red
- node: `NormalizedOutputOptionsImpl` and `NormalizedInputOptionsImpl` enumerable (#6861) by @hyf0
- node: keys of `RolldownOutput` should be enumerable (#6852) by @Copilot

### 🚜 Refactor

- rust: rename `BundleContext` to `BundleHandle` (#6893) by @hyf0
- rust: rename `build_span` to `bundle_span` (#6892) by @hyf0
- rust: introduce `PluginDriverFactory` to manage creation of `PluginDriver` (#6891) by @hyf0
- crates/rolldown_binding: remove useless `BindingBundlerImpl` (#6888) by @hyf0
- crates/rolldown_binding: rename `Bundler` to `ClassicBundler` and clarify the purpose (#6887) by @hyf0
- rust: rename `BuildFactory/Build` to `BundleFactory/Bundle` (#6886) by @hyf0
- rust: tweak incremental build related methods of `Bundler` (#6884) by @hyf0
- rust: manage build via `BuildFactory` for `Bundler` (#6883) by @hyf0
- node: implement a new `Bundler` that satisfy the usage of `RolldownBuild` (#6877) by @hyf0
- node: remove useless `nonEnumerable` decorator (#6862) by @hyf0

### 📚 Documentation

- add documentation for native replace plugin (#6315) by @shulaoda
- in-depth/directives: expand description of how directives are handled (#6882) by @sapphi-red
- in-depth/bundling-cjs: clarify the condition of `default` export interop (#6875) by @sapphi-red
- add troubleshooting section for `this` in exported functions (#6865) by @sapphi-red
- update prebuilt binaries list based on Node 24 platform support tier (#6864) by @sapphi-red
- remove unsupported [ext] placeholder from entryFileNames and chunkFileNames (#6859) by @Copilot

### ⚡ Performance

- rolldown: improve sourcemap chain processing (#6858) by @Brooooooklyn

### 🧪 Testing

- add test case for issue #6881 with scientific notation (#6906) by @IWANABETHATGUY

### ⚙️ Miscellaneous Tasks

- deps: update npm packages (#6918) by @renovate[bot]
- deps: update dependency rolldown-plugin-dts to v0.17.5 (#6917) by @renovate[bot]
- deps: lock file maintenance (#6907) by @renovate[bot]
- deps: update rust crates (#6905) by @renovate[bot]
- deps: update npm packages (#6903) by @renovate[bot]
- deps: update github-actions (#6902) by @renovate[bot]
- deps: update `oxc_resolver` and `oxc_resolver_napi` (#6901) by @shulaoda
- deps: update dependency rolldown-plugin-dts to v0.17.4 (#6895) by @renovate[bot]
- deps: update dependency tsdown to v0.16.1 (#6885) by @renovate[bot]
- deps: upgrade napi to remove linker args that skip missing symbols (#6867) by @Boshen

Co-authored-by: shulaoda <[email protected]>
graphite-app bot pushed a commit that referenced this pull request Nov 17, 2025
…mports are used from a CJS module (#7094)

`canonical name not found for "__toESM"` error when only named imports are used from a CJS module and SafelyMergeCjsNs optimization is applied.

refs #7006, #6850
sapphi-red added a commit that referenced this pull request Nov 18, 2025
…mports are used from a CJS module (#7094)

`canonical name not found for "__toESM"` error when only named imports are used from a CJS module and SafelyMergeCjsNs optimization is applied.

refs #7006, #6850
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.

[Feature Request]: skip __toESM when the default export is not used

4 participants