Skip to content

fix(oxc-runtime): route require() to CJS helper variant (#9263)#9526

Merged
graphite-app[bot] merged 1 commit into
mainfrom
05-09-fix_9263
May 25, 2026
Merged

fix(oxc-runtime): route require() to CJS helper variant (#9263)#9526
graphite-app[bot] merged 1 commit into
mainfrom
05-09-fix_9263

Conversation

@IWANABETHATGUY

@IWANABETHATGUY IWANABETHATGUY commented May 23, 2026

Copy link
Copy Markdown
Member

The on-disk @oxc-project/runtime package ships two variants of every helper: a CJS form at src/helpers/<name>.js that sets module.exports = fn, and an ESM form at src/helpers/esm/<name>.js that does export default fn. The plugin previously embedded only the ESM variant and reused it for every caller, which meant require("@oxc-project/runtime/helpers/X") returned the ESM namespace { default: fn } rather than the function itself.

This PR embeds both variants and dispatches resolution by ImportKind: require() gets the CJS variant (callable directly), import / dynamic import() get the ESM variant. Relative imports between virtual helpers preserve the importer's directory so an ESM helper's ./typeof.js stays in esm/. Fixes #9263, where _defineProperty resolved to the ESM namespace inside CJS-classified modules and threw TypeError: _defineProperty is not a function at init.

Copy link
Copy Markdown
Member Author

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.

@netlify

netlify Bot commented May 23, 2026

Copy link
Copy Markdown

Deploy Preview for rolldown-rs canceled.

Name Link
🔨 Latest commit f06c2a0
🔍 Latest deploy log https://app.netlify.com/projects/rolldown-rs/deploys/6a13a7b421074300089b47cb

@IWANABETHATGUY IWANABETHATGUY force-pushed the 05-09-fix_9263 branch 2 times, most recently from 36ef49e to 8d4e742 Compare May 23, 2026 15:27
@IWANABETHATGUY IWANABETHATGUY marked this pull request as ready for review May 23, 2026 16:48
@codspeed-hq

codspeed-hq Bot commented May 23, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 4 untouched benchmarks
⏩ 10 skipped benchmarks1


Comparing 05-09-fix_9263 (8d4e742) with main (d82d2ff)

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.

@hyf0

hyf0 commented May 24, 2026

Copy link
Copy Markdown
Member

I'm not sure this is a good direction. I'm worred that this might cause the runtime code have dual package hazard.

What will happen if the runtime code got both import and require?

This PR embeds both variants and dispatches resolution by ImportKind: require() gets the CJS variant (callable directly), import / dynamic import() get the ESM variant. Relative imports between virtual helpers preserve the importer's directory so an ESM helper's ./typeof.js stays in esm/.

From the PR description, I assume the anwser is that rolldown will ship both esm and cjs version code.


I think we need to refer to other bundlers and see how does they deal with the situation. @sapphi-red cc

@IWANABETHATGUY

Copy link
Copy Markdown
Member Author

I'm not sure this is a good direction. I'm worred that this might cause the runtime code have dual package hazard.

What will happen if the runtime code got both import and require?

This PR embeds both variants and dispatches resolution by ImportKind: require() gets the CJS variant (callable directly), import / dynamic import() get the ESM variant. Relative imports between virtual helpers preserve the importer's directory so an ESM helper's ./typeof.js stays in esm/.

From the PR description, I assume the anwser is that rolldown will ship both esm and cjs version code.

I think we need to refer to other bundlers and see how does they deal with the situation. @sapphi-red cc

1. Almost all helpers are pure stateless functions

I scanned all 116 ESM helpers. The overwhelming majority are pure functions like:

function _defineProperty(e, r, t) { /* …pure… */ }
function _typeof(o) { /* …pure… */ }

For those, "two copies in one bundle" just means two equivalent function objects — no helper is compared by identity, only called, so duplication is at worst a few wasted bytes.

The exceptions worth naming explicitly:

Helper Why it isn't strictly pure Risk if duplicated
OverloadYield Constructor used as a brand. wrapAsyncGenerator does o instanceof OverloadYield to detect values produced by awaitAsyncGenerator / asyncGeneratorDelegate. The instanceof check could miss values if wrapAsyncGenerator and awaitAsyncGenerator resolve to different variants and an async generator from one classification delegates to one from the other.
AwaitValue Constructor (this.wrapped = t). No other helper checks instanceof AwaitValue, but user-emitted regenerator code might. Same shape as OverloadYield, but only matters if user code does cross-variant identity checks on the wrapped value.
wrapNativeSuper Holds a closure-local Map cache mapping native ctor → Wrapper. Two variants → two separate caches → wrapping the same native twice yields two Wrappers, so obj instanceof Wrapper across variants would fail. Almost never reached in practice — each transformed extends NativeCtor site goes through one specific call.
classPrivateFieldLooseKey Module-level var id = 0 counter producing __private_<id>_<name> keys. Each duplicate copy restarts at 0, so the IDs are no longer globally unique. Harmless in practice because keys are scoped to the class instance they're written on.

Within a single user module, the helper and its transitive ./Foo.js imports go through the same ImportKind (the PR's relative-import resolver preserves the importer's directory — esm/wrapAsyncGenerator.js resolves ./OverloadYield.js to esm/OverloadYield.js, and the CJS sibling stays in CJS). So the only way to trip the OverloadYield brand check is to pass a value produced by helpers from one variant into helpers from the other variant — i.e. async-generator delegation across the CJS/ESM module boundary. Niche, but worth flagging.

2. The on-disk package already does exactly this dispatch

Look at @oxc-project/runtime's own package.json exports:
image

"./helpers/defineProperty": [
  {
    "node":    "./src/helpers/defineProperty.js",       // CJS variant
    "import":  "./src/helpers/esm/defineProperty.js",   // ESM variant
    "default": "./src/helpers/defineProperty.js"
  },
  "./src/helpers/defineProperty.js"
]

So if oxc-resolver (or Node, or webpack) were resolving @oxc-project/runtime/helpers/defineProperty against the real package, require() would already give the CJS file and import would already give the ESM file. The PR isn't inventing a new dual-shipping policy — it's making the embedded plugin honor the policy the upstream package already encodes via exports conditions. Until now we were always returning the ESM variant regardless of caller, which is what causes #9263.

3. The CJS variant is interop-safe by construction

The on-disk CJS helper ends with:

module.exports = _defineProperty;
module.exports.__esModule = true;
module.exports["default"] = module.exports;

So require("…"), require("…").default, and import X from "…" all yield the function. Two copies of this in one bundle still both behave as fn. There's no shape divergence to expose.

4. Other bundlers in this niche behave the same way

@babel/runtime ships the identical dual-export shape (it's where this package's structure comes from), and webpack/Rollup pick the variant via exports conditions on a per-caller basis — same outcome as this PR. @swc/helpers does the same. So even if a real app reaches one helper from both import and require() callers, it'd already end up with two copies under those bundlers.

5. The alternatives are worse

The other ways to fix #9263 are:

  • (a) Keep ESM-only and fix the CJS classification adapter so var X = (init_X(), __toCommonJS(X_exports)) binds to the function instead of the namespace. This is doable but tangled — the helper is a virtual \0… module, the CJS-emitter path needs to special-case it, and we'd be growing logic in the linking/codegen stages rather than where the helper lives.
  • (b) Always emit the CJS variant. Then every ESM helper import in user code becomes a __toESM(require_…) shape, which the rest of tree-shaking and chunking treats as a "CJS classified" module. Helpers would no longer be statically-inlinable function decls. That regresses ESM output noticeably.
  • (c) This PR. Match what the package's own exports map says. Matches Node and matches what every other bundler does when resolving this package off disk.

@IWANABETHATGUY

Copy link
Copy Markdown
Member Author

Another reason I don't prefer the way Keep ESM-only and fix the CJS classification adapter is written in https://github.com/IWANABETHATGUY-reproduction/9263-comparison/blob/main/README.md

@hyf0 hyf0 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Webpack + babel runtime has the same behavior as this PR. LGTM.

IWANABETHATGUY commented May 25, 2026

Copy link
Copy Markdown
Member Author

Merge activity

  • May 25, 1:33 AM UTC: The merge label 'graphite: merge-when-ready' was detected. This PR will be added to the Graphite merge queue once it meets the requirements.
  • May 25, 1:36 AM UTC: IWANABETHATGUY added this pull request to the Graphite merge queue.
  • May 25, 1:41 AM UTC: Merged by the Graphite merge queue.

@IWANABETHATGUY

Copy link
Copy Markdown
Member Author

I have fixed the issues(refactor) that @shulaoda mentioned, so I merged.

The on-disk `@oxc-project/runtime` package ships two variants of every helper: a CJS form at `src/helpers/<name>.js` that sets `module.exports = fn`, and an ESM form at `src/helpers/esm/<name>.js` that does `export default fn`. The plugin previously embedded only the ESM variant and reused it for every caller, which meant `require("@oxc-project/runtime/helpers/X")` returned the ESM namespace `{ default: fn }` rather than the function itself.

This PR embeds both variants and dispatches resolution by `ImportKind`: `require()` gets the CJS variant (callable directly), `import` / dynamic `import()` get the ESM variant. Relative imports between virtual helpers preserve the importer's directory so an ESM helper's `./typeof.js` stays in `esm/`. Fixes #9263, where `_defineProperty` resolved to the ESM namespace inside CJS-classified modules and threw `TypeError: _defineProperty is not a function` at init.
@graphite-app graphite-app Bot merged commit f06c2a0 into main May 25, 2026
32 of 33 checks passed
@graphite-app graphite-app Bot deleted the 05-09-fix_9263 branch May 25, 2026 01:41
@sapphi-red

Copy link
Copy Markdown
Member

I guess we can add module condition to the runtime package to reduce the output size. I think it's not urgent though.

V1OL3TF0X pushed a commit to V1OL3TF0X/rolldown that referenced this pull request May 25, 2026
… (rolldown#9526)

The on-disk `@oxc-project/runtime` package ships two variants of every helper: a CJS form at `src/helpers/<name>.js` that sets `module.exports = fn`, and an ESM form at `src/helpers/esm/<name>.js` that does `export default fn`. The plugin previously embedded only the ESM variant and reused it for every caller, which meant `require("@oxc-project/runtime/helpers/X")` returned the ESM namespace `{ default: fn }` rather than the function itself.

This PR embeds both variants and dispatches resolution by `ImportKind`: `require()` gets the CJS variant (callable directly), `import` / dynamic `import()` get the ESM variant. Relative imports between virtual helpers preserve the importer's directory so an ESM helper's `./typeof.js` stays in `esm/`. Fixes rolldown#9263, where `_defineProperty` resolved to the ESM namespace inside CJS-classified modules and threw `TypeError: _defineProperty is not a function` at init.
@shulaoda shulaoda mentioned this pull request May 27, 2026
shulaoda added a commit that referenced this pull request May 27, 2026
## [1.0.3] - 2026-05-27

### 🚀 Features

- transform: respect decorator strictNullChecks option (#9580) by @kylecannon
- drop `defer` keyword (#9503) by @TheAlexLichter

### 🐛 Bug Fixes

- ci: create target dir before cargo release-oxc update (#9584) by @shulaoda
- ci: reorder prepare-release steps to avoid dirty git check failure (#9583) by @shulaoda
- testing: canonicalize temp dir early and use platform-specific separator in test262 (#9582) by @shulaoda
- testing: resolve symlinked temp dir in test262 snapshot normalization (#9581) by @shulaoda
- testing: canonicalize temp dir path in test262 snapshot normalization (#9579) by @shulaoda
- dev: `onOutput` called twice when initial build fails (#9552) by @hyf0
- dev: make `ensureCurrentBuildFinish` not returning error when engine closes (#9564) by @h-a-n-a
- oxc-runtime: route require() to CJS helper variant (#9263) (#9526) by @IWANABETHATGUY
- generator: use exporter chunk's export mode for CJS default re-exports (#9299) (#9529) by @IWANABETHATGUY
- rolldown: always run reduced-atom static cycle check (#9441) (#9514) by @IWANABETHATGUY
- apply transform.dropLabels before scanning (#9521) (#9522) by @IWANABETHATGUY
- rolldown_watcher: take `rolldown` dep through the workspace (#9510) by @Boshen
- cache: keep the scan-stage cache consistent when a build fails (#9495) by @h-a-n-a
- skip JSON default-import namespace optimization for write targets (#9484) (#9489) by @IWANABETHATGUY
- deps: skip pnpm frozen-lockfile on Netlify to dodge catalog mismatch bug (#9471) by @Boshen

### 🚜 Refactor

- oxc-runtime: use Cow for helper path construction (#9538) by @IWANABETHATGUY
- fold import defer phase drop into PreProcessor (#9524) by @IWANABETHATGUY
- distinguish `map: null` vs `map: undefined` in transform hook output (#9497) by @sapphi-red

### 📚 Documentation

- explain the policy for Rust crates (#9547) by @sapphi-red
- cache: add design doc for cache (#9544) by @h-a-n-a
- guide/troubleshooting: add TDZ error section (#9537) by @sapphi-red
- dev-engine: add design doc for dev-engine (#9479) by @h-a-n-a
- lazy-barrel: tweak some words (#9483) by @shulaoda
- lazy-barrel: expand reasoning behind LARGE_BARREL_MODULES advice (#9477) by @shulaoda

### ⚡ Performance

- generate: thread ast_table by value into codegen consumer (#9555) by @Boshen
- finalizers: replace `_reExport` construction with a direct call to avoid calling `clone_in` (#9501) by @Dunqing
- reorder hot-path boolean checks to short-circuit on cheap predicates first (#9523) by @Boshen

### 🧪 Testing

- rolldown: regression fixture for #9401 (#9418) by @IWANABETHATGUY
- failing test for #9441 (#9504) by @TheAlexLichter

### ⚙️ Miscellaneous Tasks

- deps: upgrade oxc to 0.133.0 (#9563) by @Dunqing
- deps: update crate-ci/typos action to v1.46.3 (#9576) by @renovate[bot]
- deps: update mimalloc-safe to 0.1.62 (#9577) by @shulaoda
- mimalloc-safe: update to a bug-fix branch for verification (#9569) by @shulaoda
- deps: update test262 submodule for tests (#9551) by @rolldown-guard[bot]
- point published crates' readme to root README.md (#9553) by @Boshen
- replace actions-cool/issues-helper with gh CLI (#9543) by @Boshen
- deps: update cargo-shear to 1.12.4 (#9541) by @Boshen
- deps: update taiki-e/install-action action to v2.79.4 (#9535) by @renovate[bot]
- deps: update github actions (#9532) by @renovate[bot]
- deps: update rust crates (#9534) by @renovate[bot]
- deps: update npm packages (#9533) by @renovate[bot]
- gate experimental/testing-only items to silence dead_code in publish builds (#9517) by @Boshen
- docs: deploy to Void (#9509) by @Boshen
- release: set up cargo-release-oxc for publishing crates (#9476) by @Boshen
- rolldown_plugin_lazy_compilation: add missing description (#9507) by @Boshen
- mimalloc-safe: update to a bug-fix branch for verification (#9506) by @shulaoda
- deps: update crate-ci/typos action to v1.46.2 (#9468) by @renovate[bot]

### ❤️ New Contributors

* @kylecannon made their first contribution in [#9580](#9580)
shulaoda pushed a commit that referenced this pull request May 27, 2026
## [1.0.3] - 2026-05-27

### 🚀 Features

- transform: respect decorator strictNullChecks option (#9580) by @kylecannon
- drop `defer` keyword (#9503) by @TheAlexLichter

### 🐛 Bug Fixes

- ci: create target dir before cargo release-oxc update (#9584) by @shulaoda
- ci: reorder prepare-release steps to avoid dirty git check failure (#9583) by @shulaoda
- testing: canonicalize temp dir early and use platform-specific separator in test262 (#9582) by @shulaoda
- testing: resolve symlinked temp dir in test262 snapshot normalization (#9581) by @shulaoda
- testing: canonicalize temp dir path in test262 snapshot normalization (#9579) by @shulaoda
- dev: `onOutput` called twice when initial build fails (#9552) by @hyf0
- dev: make `ensureCurrentBuildFinish` not returning error when engine closes (#9564) by @h-a-n-a
- oxc-runtime: route require() to CJS helper variant (#9263) (#9526) by @IWANABETHATGUY
- generator: use exporter chunk's export mode for CJS default re-exports (#9299) (#9529) by @IWANABETHATGUY
- rolldown: always run reduced-atom static cycle check (#9441) (#9514) by @IWANABETHATGUY
- apply transform.dropLabels before scanning (#9521) (#9522) by @IWANABETHATGUY
- rolldown_watcher: take `rolldown` dep through the workspace (#9510) by @Boshen
- cache: keep the scan-stage cache consistent when a build fails (#9495) by @h-a-n-a
- skip JSON default-import namespace optimization for write targets (#9484) (#9489) by @IWANABETHATGUY
- deps: skip pnpm frozen-lockfile on Netlify to dodge catalog mismatch bug (#9471) by @Boshen

### 🚜 Refactor

- oxc-runtime: use Cow for helper path construction (#9538) by @IWANABETHATGUY
- fold import defer phase drop into PreProcessor (#9524) by @IWANABETHATGUY
- distinguish `map: null` vs `map: undefined` in transform hook output (#9497) by @sapphi-red

### 📚 Documentation

- explain the policy for Rust crates (#9547) by @sapphi-red
- cache: add design doc for cache (#9544) by @h-a-n-a
- guide/troubleshooting: add TDZ error section (#9537) by @sapphi-red
- dev-engine: add design doc for dev-engine (#9479) by @h-a-n-a
- lazy-barrel: tweak some words (#9483) by @shulaoda
- lazy-barrel: expand reasoning behind LARGE_BARREL_MODULES advice (#9477) by @shulaoda

### ⚡ Performance

- generate: thread ast_table by value into codegen consumer (#9555) by @Boshen
- finalizers: replace `_reExport` construction with a direct call to avoid calling `clone_in` (#9501) by @Dunqing
- reorder hot-path boolean checks to short-circuit on cheap predicates first (#9523) by @Boshen

### 🧪 Testing

- rolldown: regression fixture for #9401 (#9418) by @IWANABETHATGUY
- failing test for #9441 (#9504) by @TheAlexLichter

### ⚙️ Miscellaneous Tasks

- deps: upgrade oxc to 0.133.0 (#9563) by @Dunqing
- deps: update crate-ci/typos action to v1.46.3 (#9576) by @renovate[bot]
- deps: update mimalloc-safe to 0.1.62 (#9577) by @shulaoda
- mimalloc-safe: update to a bug-fix branch for verification (#9569) by @shulaoda
- deps: update test262 submodule for tests (#9551) by @rolldown-guard[bot]
- point published crates' readme to root README.md (#9553) by @Boshen
- replace actions-cool/issues-helper with gh CLI (#9543) by @Boshen
- deps: update cargo-shear to 1.12.4 (#9541) by @Boshen
- deps: update taiki-e/install-action action to v2.79.4 (#9535) by @renovate[bot]
- deps: update github actions (#9532) by @renovate[bot]
- deps: update rust crates (#9534) by @renovate[bot]
- deps: update npm packages (#9533) by @renovate[bot]
- gate experimental/testing-only items to silence dead_code in publish builds (#9517) by @Boshen
- docs: deploy to Void (#9509) by @Boshen
- release: set up cargo-release-oxc for publishing crates (#9476) by @Boshen
- rolldown_plugin_lazy_compilation: add missing description (#9507) by @Boshen
- mimalloc-safe: update to a bug-fix branch for verification (#9506) by @shulaoda
- deps: update crate-ci/typos action to v1.46.2 (#9468) by @renovate[bot]

### ❤️ New Contributors

* @kylecannon made their first contribution in [#9580](#9580)
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.

[Bug]: Class fields in CJS-classified modules bind _defineProperty to namespace via __toCommonJS, throwing at init

5 participants