Conversation
|
Updated 6:38 PM PT - Jul 11th, 2026
❌ @robobun, your commit 23c6fcf has 2 failures in
🧪 To try this PR locally: bunx bun-pr 33807That installs a local version of the PR into your bun-33807 --bun |
|
Found 1 issue this PR may fix:
🤖 Generated with Claude Code |
|
Checked #28864: the pyodide failure there is caused by |
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
WalkthroughThis PR makes explicit module type authoritative across loader resolution, runtime transpilation, parser ESM handling, cache hashing, and tests covering extension and Changesmodule_type authoritative propagation
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/cli/run/run-cjs.test.ts`:
- Around line 129-152: The last CJS sniff tests use inconsistent failure
handling, so align them with the earlier tests by capturing stderr and guarding
JSON parsing. Update the "ambiguous .js with typeof module still runs as CJS"
and ".cjs with typeof module still runs as CJS" cases in run-cjs.test.ts to
destructure stderr from run() and parse stdout with a safe fallback like the
other tests, so failures report exitCode/stderr instead of an opaque
SyntaxError. Use the existing tempDir, run, and JSON.parse(stdout || "null")
pattern as the reference for consistency.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: cd246ccb-c320-4bdb-9021-be93f8d1e8ad
📒 Files selected for processing (6)
src/js_parser/p.rssrc/js_parser/parse/parse_entry.rssrc/jsc/RuntimeTranspilerCache.rssrc/jsc/RuntimeTranspilerStore.rssrc/runtime/jsc_hooks.rstest/cli/run/run-cjs.test.ts
|
On the dedup suggestions:
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/js_parser/p.rs`:
- Around line 5613-5615: In `value_for_this`, explicit ESM should take
precedence over `commonjs_named_exports`, since that set can still be populated
in `.mjs`/`.mts` or `"type":"module"` files when `unwrap_commonjs_to_esm` is
enabled. Update the conditional so `self.options.module_type ==
options::ModuleType::Esm` bypasses the `commonjs_named_exports.count() == 0`
gate, and only use that count check for inferred ESM cases based on
`self.has_es_module_syntax`. Keep the top-level `this` rewrite to `exports` only
for non-explicit ESM scenarios.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: c35b7c15-2b53-48f5-9ce8-53ab8dbb19d3
📒 Files selected for processing (2)
src/js_parser/p.rstest/cli/run/run-cjs.test.ts
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/cli/run/run-cjs.test.ts`:
- Around line 38-46: The parsing in the run-cjs test helper can throw on
malformed non-empty stdout, preventing crash diagnostics from being surfaced.
Update the stdout handling around the `out` निर्माण in the helper used by
`run-cjs.test.ts` so that JSON parsing failures are caught and fall back to the
same `{ crashed: stderr }` shape used for empty stdout. Keep the fix localized
to the helper that builds `out`/`exitCode`, ensuring partial or invalid lines do
not leak a raw `SyntaxError`.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: cef22b14-f0ca-4f93-a26a-1ba3c06970cd
📒 Files selected for processing (6)
src/js_parser/parse/parse_entry.rssrc/resolver/resolver.rssrc/runtime/jsc_hooks.rstest/bundler/bundler_edgecase.test.tstest/cli/run/run-cjs.test.tstest/js/third_party/es-module-lexer/package.json
💤 Files with no reviewable changes (1)
- test/js/third_party/es-module-lexer/package.json
There was a problem hiding this comment.
Thanks for addressing all three rounds of feedback — the !bundle gates and the bundler regression tests look right. I'm not auto-approving because this changes core CJS/ESM classification semantics across parser/resolver/runtime and has a stated design disagreement with #32177, so a maintainer should sign off on the direction.
Extended reasoning...
Overview
This PR makes .mjs/.mts/"type":"module" authoritative over the parser's CJS content sniff (bare module/exports identifier references). It touches 6 source files across four subsystems: the parser's exports_kind decision and value_for_this rewrite (parse_entry.rs, p.rs), the runtime transpiler store's module-type threading (RuntimeTranspilerStore.rs), the runtime loader's nearest-package.json walk (jsc_hooks.rs), the resolver's built-in-module classification (resolver.rs), and the transpiler cache's version bump + hash key (RuntimeTranspilerCache.rs, parse_entry.rs). Test coverage is thorough: 11 runtime cases in run-cjs.test.ts and 2 bundler regression cases in bundler_edgecase.test.ts.
Security risks
None identified. This is module-classification logic with no auth, crypto, network, or untrusted-input parsing surface. The nearest-package.json walk reuses the existing resolver's DirInfo cache and does not introduce new filesystem traversal.
Level of scrutiny
High. Module classification (CJS vs ESM) is one of the most semantically load-bearing decisions in the runtime — getting it wrong silently changes this, module, exports, strict mode, and import/require interop for user code. The PR already went through three rounds of review-driven fixes here (a real bundler regression was caught and gated on !bundle), which is evidence the interaction surface is subtle. The change is well-reasoned and well-tested, but it is not mechanical.
Other factors
- The PR description and the author's own dedup analysis note that this overlaps with #32173 (same
value_for_this+ cache-key change) and disagrees on a design point with #32177 (whether explicit ESM should override the CJS-interop fallback for files that referencemodule/exports). That's a direction call a maintainer should make, not a bot. - The
resolver.rschange (built-inmodule_type: Esm→Unknown) and thees-module-lexerfixture change are small but each shift existing behavior; they're justified in the description but warrant a human glance. - All prior inline feedback (mine and CodeRabbit's) is resolved; the bug hunter found nothing on 7408ef4.
|
CI on 7408ef4 is red on tests that are failing across multiple unrelated PR builds right now (napi.test.ts on builds 70852-70862, update_interactive_install.test.ts on 70853/70857/70858/70862, bun-install.test.ts on 70856, complex-workspace.test.ts on 70853/70858, postgres-invalid-message-length on 70852-70858, express/res.send on 70853-70858). The one bundler_compile.test.ts failure is a bytecode disk-cache hit/miss ordering diff on a single lane, unrelated to module classification. The diff itself is green: the new runtime tests in run-cjs.test.ts (7 fail before, 11 pass after) and the new bundler_edgecase tests (NamelessNestedTypeCommonjsUnderTypeModule, ExportsMapImportConditionPointsAtCjs) pass locally, as do jsdom, react-spa, es-module-lexer, bundler_cjs*, esbuild/default, and transpiler/ with no new failures. Ready for review. |
|
Ran into the mirror of this from the One case this PR doesn't yet cover: an empty (or whitespace-only) file declared as CommonJS. Pushed claude/farm/4a8127f4/fix-cjs-extension-async-transpiler with:
Feel free to cherry-pick the empty-file bits and tests if useful; happy to rebase on top of this once it lands otherwise. |
…exports refs A bare reference to the identifiers module or exports (e.g. the common feature-detection guard typeof module !== "undefined") caused the parser's CJS content sniff to classify a file as CommonJS even when the .mjs/.mts extension or package.json "type":"module" unambiguously marks it as ESM. If the file also had an import statement, it failed to load with "Cannot use import statement with CommonJS-only features"; without one it silently ran with CJS semantics (module/exports bound, top-level this = exports). Consult options.module_type before the uses_module_ref/uses_exports_ref sniff so an explicit ESM type wins, matching Node. The sniff still applies to ambiguous .js files. Also: - value_for_this: substitute the ESM value for top-level this when module_type == Esm (not just when ESM syntax is present). - RuntimeTranspilerStore: thread the caller's extension-aware module_type through to the worker instead of re-deriving it from package.json alone, so imported .mjs files see Esm like the sync path. - Hash module_type into the runtime transpiler cache features hash and bump EXPECTED_VERSION, since identical source now transpiles differently under different module types.
Guard JSON.parse with a fallback and include stderr in the combined assertion so a crashing subprocess surfaces its real output instead of an opaque parse error.
…ones
Making .mjs/"type":"module" authoritative exposed two places where the
module_type fed to the parser was wrong; the CJS content sniff had been
masking both.
- jsc_hooks: enclosing_package_json skips package.json files without a
"name" (kept for dependency resolution), so a .js file under
parse5/dist/cjs/ (which ships {"type":"commonjs"} with no name) was
seeing the outer parse5 "type":"module". Walk the DirInfo parent chain
to read "type" from the nearest package.json regardless of name, and
thread that as a separate LoaderResult field so pkg_name semantics are
unchanged.
- resolver: resolve_with_framework hard-coded ModuleType::Esm for
embedded BuiltInModule::Code, but the react-refresh fallback it
carries is CJS. Use Unknown so the parser decides from content; the
ESM .tsx built-ins still classify as ESM via their import syntax.
Also: drop the stray "type":"module" from the es-module-lexer test
fixture (its index.ts is CJS and was relying on the sniff override), and
rewrite the run-cjs assertions so stderr only appears in the failure
diff instead of being asserted empty. Adds a test for the
nested-nameless-package.json case.
The bundler's resolver can return ModuleType::Esm for files that are
actually CJS (a nameless nested {"type":"commonjs"} scope that
enclosing_package_json skips, or an exports-map "import" condition
pointing at a .js file). The content sniff masked this; making Esm
authoritative in the bundler path broke bundling such packages.
Restrict the new check to the runtime path, where jsc_hooks now derives
module_type from the nearest package.json regardless of name. The
bundler keeps falling back to the sniff until its resolver is fixed.
Adds itBundled coverage for both layouts.
Same rationale as d92882b: the bundler's resolver can return Esm for CJS files, so a misclassified CJS file using top-level this (with no exports.<name> assignment to populate commonjs_named_exports) would rewrite this -> null instead of this -> exports, even though parse_entry.rs then correctly classifies the file as CJS via the content sniff. Extend the NamelessNestedTypeCommonjsUnderTypeModule bundler test to cover this.viaThis.
7408ef4 to
a46baf7
Compare
|
CI on a46baf7 (build 71980, post-rebase): the three failing tests (30205.test.ts napi-finalizer SIGABRT, fetch-backpressure, bake/dev-and-prod) are failing on other PR builds 71972/71976/71977/71978/71979 as well; none touch module classification. Diff remains green on all new runtime and bundler tests locally. |
Node's LOOKUP_PACKAGE_SCOPE stops when it reaches a directory named node_modules and defaults the format to commonjs. Without this, a .js file at node_modules/foo/index.js with no foo/package.json would inherit an outer project's "type":"module" and fail at runtime with module undefined. Adds a test for the layout.
There was a problem hiding this comment.
No new issues after 23c6fcf — the four concerns I raised earlier are all addressed. Deferring to a human reviewer because this changes core module-classification semantics, has stated design overlap with #32173/#32177/#30367, and the two !bundle gates are an intentional workaround pending a resolver-side finalize_result fix; those are design calls worth a maintainer's sign-off.
Checked this pass:
nearest_module_typewalk now breaks atis_node_modules()before consultingpackage_json(), matching Node's LOOKUP_PACKAGE_SCOPE; new runtime test covers it.- Both
!bundlegates (parse_entry.rsexports_kind,p.rsvalue_for_this) are consistent; the two bundler-resolver misclassification layouts are locked in byitBundledtests. module_typeis threaded throughTranspilerJob, hashed into the features hash, andEXPECTED_VERSIONbumped to 23.resolve_with_framework→ModuleType::Unknownfor embedded code — the ESM.tsxbuilt-ins still classify via their import syntax.
Extended reasoning...
Overview
The PR makes .mjs/.mts/"type":"module" authoritative over the parser's CJS content sniff (bare module/exports references). It touches: parse_entry.rs (the exports_kind decision + hashing module_type), p.rs (value_for_this top-level this substitution), RuntimeTranspilerStore.rs (thread caller-derived module_type onto TranspilerJob and use it in the worker), jsc_hooks.rs (new nearest_module_type walk over DirInfo parents including nameless package.json, stopping at node_modules), resolver.rs (resolve_with_framework embedded code → Unknown), RuntimeTranspilerCache.rs (version 23), plus 12 new runtime tests, 2 new bundler tests, and dropping a stray "type":"module" from the es-module-lexer fixture.
Security risks
None identified. This is module-format classification; no auth, crypto, network, or untrusted-input parsing is introduced. The nearest_module_type walk reads already-cached DirInfo entries via read_dir_info.
Level of scrutiny
High. Module classification governs whether a file is wrapped as CJS or run as ESM — misclassification breaks real packages (the PR itself found and fixed three such regressions during review: nameless nested {"type":"commonjs"}, exports-map "import" condition, and the node_modules boundary). The runtime path now trusts module_type == Esm unconditionally, which is only correct because nearest_module_type derives it per Node's rule; any remaining divergence there is a runtime regression. The bundler path is deliberately kept on the old content-sniff via !bundle gates as a workaround.
Other factors
- Design overlap: the author's own dedup analysis notes #32177 explicitly preserves the CJS-interop fallback for forced-ESM files that reference
module/exports, while this PR removes it. #32173 makes overlapping changes tovalue_for_thisand the cache key. Whichever direction is chosen, a maintainer should reconcile these. - Workaround shape: the
!bundlegates are documented as temporary untilfinalize_resultis fixed to re-derivemodule_typefrom extension + nearest package.json. That's a reasonable scoping choice, but it leaves the parser's behavior forked between runtime and bundler on the same input — worth explicit maintainer acknowledgment. - Companion branch: the author mentioned a separate branch handling the
.cjsmirror (empty/marker-less CJS files) that isn't folded in here. - Test coverage: the new tests are thorough (extension × package.json × direct/dynamic-import × nested scopes × node_modules boundary, plus negative controls). All four issues I raised during earlier passes are addressed with tests.
Given the critical code path, the intentional runtime/bundler behavioral fork, and the open design question vs. #32177, this warrants human review rather than bot approval.
A .js file under "type":"commonjs" (or a .cjs file) containing `export` or top-level `await` was silently classified and run as ESM. In Node those are a hard SyntaxError: an explicit "type":"commonjs" pins the format of every .js in the tree, and .cjs is always CommonJS. The exports_kind decision in parse_entry.rs checked `esm_export_keyword || top_level_await_keyword` before ever consulting `options.module_type`, so the syntax out-ranked the explicit declaration. - parse_entry.rs: when `module_type == Cjs` at runtime (not bundling, not TypeScript), emit a range error pointing at the `export`/`await` with a note saying why the file is CommonJS. TypeScript is excluded because `export` in a CommonJS-typed .ts/.cts is idiomatic (tsc compiles it to `exports.x = ...`). - RuntimeTranspilerStore.rs: thread the caller's extension-aware `module_type` onto `TranspilerJob` and use it in the worker. The worker was re-deriving it from `resolved_source.tag`, which never carried the .cjs/.mjs extension signal. Also check `log.errors` after a successful parse (matching the sync path in transpile_source_code_inner); without this, parser-emitted errors on the async path were silently dropped and the output executed anyway. - jsc_hooks.rs: pass the computed `module_type` to `transpiler_store.transpile()`. - parse_entry.rs/RuntimeTranspilerCache.rs: hash `module_type` into the runtime transpiler features hash and bump EXPECTED_VERSION to 23, since byte-identical sources now parse differently under different module types. - resolve-ts.test.ts: the `type:commonjs && jsFile` matrix rows wrote ESM `export` into a .js under "type":"commonjs" and relied on the old lenient behaviour; write CJS-shaped sources for that combination. Sibling of #33807, which makes "type":"module" authoritative over bare `module`/`exports` references (the ESM direction of the same decision-order bug). The RuntimeTranspilerStore module_type threading and cache-hash change here are the same as in that PR; whichever lands second resolves a small conflict.
A .js file under "type":"commonjs" (or a .cjs file) containing `export` or top-level `await` was silently classified and run as ESM. In Node those are a hard SyntaxError: an explicit "type":"commonjs" pins the format of every .js in the tree, and .cjs is always CommonJS. The exports_kind decision in parse_entry.rs checked `esm_export_keyword || top_level_await_keyword` before ever consulting `options.module_type`, so the syntax out-ranked the explicit declaration. - parse_entry.rs: when `module_type == Cjs` at runtime (not bundling, not TypeScript), emit a range error pointing at the `export`/`await` with a note saying why the file is CommonJS. TypeScript is excluded because `export` in a CommonJS-typed .ts/.cts is idiomatic (tsc compiles it to `exports.x = ...`). - RuntimeTranspilerStore.rs: thread the caller's extension-aware `module_type` onto `TranspilerJob` and use it in the worker. The worker was re-deriving it from `resolved_source.tag`, which never carried the .cjs/.mjs extension signal. Also check `log.errors` after a successful parse (matching the sync path in transpile_source_code_inner); without this, parser-emitted errors on the async path were silently dropped and the output executed anyway. - jsc_hooks.rs: pass the computed `module_type` to `transpiler_store.transpile()`. - parse_entry.rs/RuntimeTranspilerCache.rs: hash `module_type` into the runtime transpiler features hash and bump EXPECTED_VERSION to 23, since byte-identical sources now parse differently under different module types. - resolve-ts.test.ts: the `type:commonjs && jsFile` matrix rows wrote ESM `export` into a .js under "type":"commonjs" and relied on the old lenient behaviour; write CJS-shaped sources for that combination. Sibling of #33807, which makes "type":"module" authoritative over bare `module`/`exports` references (the ESM direction of the same decision-order bug). The RuntimeTranspilerStore module_type threading and cache-hash change here are the same as in that PR; whichever lands second resolves a small conflict.
A .js file under "type":"commonjs" (or a .cjs file) containing `export` or top-level `await` was silently classified and run as ESM. In Node those are a hard SyntaxError: an explicit "type":"commonjs" pins the format of every .js in the tree, and .cjs is always CommonJS. The exports_kind decision in parse_entry.rs checked `esm_export_keyword || top_level_await_keyword` before ever consulting `options.module_type`, so the syntax out-ranked the explicit declaration. - parse_entry.rs: when `module_type == Cjs` at runtime (not bundling, not TypeScript), emit a range error pointing at the `export`/`await` with a note saying why the file is CommonJS. TypeScript is excluded because `export` in a CommonJS-typed .ts/.cts is idiomatic (tsc compiles it to `exports.x = ...`). - RuntimeTranspilerStore.rs: thread the caller's extension-aware `module_type` onto `TranspilerJob` and use it in the worker. The worker was re-deriving it from `resolved_source.tag`, which never carried the .cjs/.mjs extension signal. Also check `log.errors` after a successful parse (matching the sync path in transpile_source_code_inner); without this, parser-emitted errors on the async path were silently dropped and the output executed anyway. - jsc_hooks.rs: pass the computed `module_type` to `transpiler_store.transpile()`. - parse_entry.rs/RuntimeTranspilerCache.rs: hash `module_type` into the runtime transpiler features hash and bump EXPECTED_VERSION to 23, since byte-identical sources now parse differently under different module types. - resolve-ts.test.ts: the `type:commonjs && jsFile` matrix rows wrote ESM `export` into a .js under "type":"commonjs" and relied on the old lenient behaviour; write CJS-shaped sources for that combination. Sibling of #33807, which makes "type":"module" authoritative over bare `module`/`exports` references (the ESM direction of the same decision-order bug). The RuntimeTranspilerStore module_type threading and cache-hash change here are the same as in that PR; whichever lands second resolves a small conflict.
A .js file under "type":"commonjs" (or a .cjs file) containing `export` or top-level `await` was silently classified and run as ESM. In Node those are a hard SyntaxError: an explicit "type":"commonjs" pins the format of every .js in the tree, and .cjs is always CommonJS. The exports_kind decision in parse_entry.rs checked `esm_export_keyword || top_level_await_keyword` before ever consulting `options.module_type`, so the syntax out-ranked the explicit declaration. - parse_entry.rs: when `module_type == Cjs` at runtime (not bundling, not TypeScript), emit a range error pointing at the `export`/`await` with a note saying why the file is CommonJS. TypeScript is excluded because `export` in a CommonJS-typed .ts/.cts is idiomatic (tsc compiles it to `exports.x = ...`). - RuntimeTranspilerStore.rs: thread the caller's extension-aware `module_type` onto `TranspilerJob` and use it in the worker. The worker was re-deriving it from `resolved_source.tag`, which never carried the .cjs/.mjs extension signal. Also check `log.errors` after a successful parse (matching the sync path in transpile_source_code_inner); without this, parser-emitted errors on the async path were silently dropped and the output executed anyway. - jsc_hooks.rs: pass the computed `module_type` to `transpiler_store.transpile()`. - parse_entry.rs/RuntimeTranspilerCache.rs: hash `module_type` into the runtime transpiler features hash and bump EXPECTED_VERSION to 23, since byte-identical sources now parse differently under different module types. - resolve-ts.test.ts: the `type:commonjs && jsFile` matrix rows wrote ESM `export` into a .js under "type":"commonjs" and relied on the old lenient behaviour; write CJS-shaped sources for that combination. Sibling of #33807, which makes "type":"module" authoritative over bare `module`/`exports` references (the ESM direction of the same decision-order bug). The RuntimeTranspilerStore module_type threading and cache-hash change here are the same as in that PR; whichever lands second resolves a small conflict.
…ed or not (#41232) ### Problem - Regression on main from #41150. No release has it. `bun build` reads `"type"` from the wrong package.json, so it misses a `{ "type": "module" }` that has no `"name"`. Two common places: a project root, and a dual package's `dist/esm/package.json`. - A `.js` or `.ts` file there gets `exports.default` for the default import of a CommonJS module with `__esModule`. Node, esbuild and bun 1.4.0 give the whole `module.exports`. The bundle has `__toESM(require_tsdep())`, without `, 1`. - Cause: `finalize_result` (`src/resolver/resolver.rs:1669`) read `"type"` from the package root, or else from `enclosing_package_json`. `dir_info_uncached` (`src/resolver/resolver.rs:6357`) sets that field only for a named package.json (#229). ### Fix - `DirInfo` gets `package_json_for_module_type`: the nearest package.json in the directory or above it, named or not. `finalize_result` reads `"type"` from it for the primary path. The extension still wins. - Correct because esbuild uses this rule, and Node ignores `"name"` too. - Only the bundler reads `Result.module_type`. `enclosing_package_json` does not change. The four runtime lookups in `src/runtime/jsc_hooks.rs` are out of scope. - Verified: `test/bundler/bundler_cjs.test.ts`, 10 new cases, 9 fail on main. Self-reviewed: 3 concerns raised, 3 addressed. Other suites in Notes. ### Background - `__toESM(mod, isNodeMode)` builds the ESM view of a CommonJS module. With `, 1` (Node mode), `default` is the whole `module.exports`. Without it, `default` is `mod.default` when `__esModule` is set. - `DirInfo` is the resolver's cached record for one directory. Its "enclosing" fields come from the parent. - Open PRs in this area: #33883, #33807, #33890, #40940. This PR supersedes none. Notes cover #40940. <details><summary>Notes</summary> Found by comparing `bun build` on main with bun 1.4.0, Node 26 and esbuild 0.25. No issue is open for it. Repro for the dual-package face: ```sh D=$(mktemp -d); cd $D; mkdir -p node_modules/pkg/dist/esm node_modules/tsdep echo '{"name":"tsdep","version":"1.0.0","main":"index.js"}' > node_modules/tsdep/package.json echo 'Object.defineProperty(exports,"__esModule",{value:true}); exports.default=function styled(){}; exports.css="css";' > node_modules/tsdep/index.js echo '{"name":"pkg","version":"1.0.0","main":"./dist/esm/index.js"}' > node_modules/pkg/package.json echo '{"type":"module"}' > node_modules/pkg/dist/esm/package.json echo 'import styled from "tsdep"; export const seen = typeof styled + "/" + typeof styled.default;' > node_modules/pkg/dist/esm/index.js echo 'import { seen } from "pkg"; console.log(seen);' > app.mjs node app.mjs # object/function bun build ./app.mjs --target=node --outfile=out.mjs && node out.mjs # main: function/undefined, this PR: object/function ``` For the project-root face, put `{ "type": "module" }` (no `"name"`) in the project's package.json and bundle a `.js` file that imports `tsdep`. Faces of the bug on main. Each has a test: - A project package.json with `"type"` and no `"name"` (case 58). - The nested marker reached through `"main"`, `"module"` or a relative path (cases 53, 55, 56). Through an exports map it worked, because `handle_esm_resolution` reads the file's own directory. - A nested package.json with a `"name"`, reached through `"main"` (case 54). `result.package_json` was the package root, so the nested file was not read at all. - A file in a subdirectory of the marker (case 57). Why a new field instead of widening `enclosing_package_json`: that field also names the package for `sideEffects`, the auto-install version gate and `bun run` script discovery. #33883 widens it for every consumer and had to rework the `sideEffects` loop in `finalize_result` to keep the DCE tests passing. Four cases pin the lookup rule. Each result matches esbuild 0.25.1: - Case 59: a nameless `{ "type": "commonjs" }` below a `"type": "module"` package wins, because it is the nearest. - Case 60: a nearest package.json without `"type"` is the scope. The lookup does not continue to a typed package root, so the importer is not ESM by type. Main read the root's `"type"` here. Node prints `object/function` for this shape, but only because it detects ESM syntax in a file with no `"type"`. #41150 chose the esbuild rule for such files. - Case 61: the lookup does not stop at a `node_modules` directory. A package without a package.json of its own takes the `"type"` above it. Node prints the same result. - Case 62: only the primary path decides. With the default target, `"module"` is the primary path and `"main"` is the fallback for `require()`. The fallback's package.json does not count. esbuild has the same check. The case fails when the check is removed. Overlap with #40940: it adds a field with the same name, but its lookup stops at `node_modules`, and the runtime reads it too. If #40940 lands after this PR, it must choose one rule for the field. Case 61 pins the crossing for the bundler. Node's stop can still apply at the runtime read sites. #40940 also calls the lookup for the fallback path, which case 62 rejects. Out of scope: the runtime's four lookups (`src/runtime/jsc_hooks.rs` lines 1474, 2972, 3220 and 4071) keep `package_json().or(enclosing_package_json)`. So `bun run` still skips a nameless package.json above the file's own directory. That gap predates #41150. For this import, `bun run` 1.4.1 gives `exports.default` for every importer, even `.mjs`. Self-review, the three concerns and what changed: - Document the `node_modules` rule on the field. Done in `src/resolver/dir_info.rs`. - Add a default-target case with both `"main"` and `"module"`. That is case 62. - Say in this body that no release has the bug, lead with the project-root face, and name the runtime lookups that stay. Suites run with the fix on a debug ASAN build, after a rebase on main: `bundler_cjs` (62), `esbuild/packagejson`, `esbuild/dce`, `esbuild/default`, `bundler_cjs2esm`, `bundler_npm`, `bundler_edgecase`, `bundler_regressions`, `bundler_splitting`, `bundler_barrel`, `cli/run/run-cjs`, `test/js/bun/resolve`. All pass except the second case of `test/js/bun/resolve/load-same-js-file-a-lot.test.ts`. It times out at 5 s on this build with and without this change (back-to-back runs on the same machine). </details> <!-- robobun:evidence:begin --> --- **[human-review]** gate passed · iteration 0 · 4 files touched <details><summary>fails on main (without fix)</summary> ```console ASAN without fix: 9 FAILED $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/bundler/bundler_cjs.test.ts bun test v1.4.1 (a6c4cc2) test/bundler/bundler_cjs.test.ts: (pass) bundler > cjs/__toESM_import_syntax_with_esModule [945.38ms] (pass) bundler > cjs/__toESM_import_syntax_without_esModule [445.45ms] (pass) bundler > cjs/__toESM_import_syntax_function [371.94ms] (pass) bundler > cjs/__toESM_import_syntax_primitive [387.72ms] (pass) bundler > cjs/__toESM_import_syntax_named_and_default [361.78ms] (pass) bundler > cjs/__toESM_import_syntax_namespace [367.76ms] (pass) bundler > cjs/__toESM_target_node [455.64ms] (pass) bundler > cjs/__toESM_target_browser [413.38ms] (pass) bundler > cjs/__toESM_target_bun [455.22ms] (pass) bundler > cjs/__toESM_format_esm [462.40ms] (pass) bundler > cjs/__toESM_format_cjs_with_import [377.93ms] (pass) bundler > cjs/__toESM_mjs_reexport [431.60ms] (pass) bundler > cjs/__toESM_mjs_reexport_with_esModule [432.63ms] (pass) bundler > cjs/__toESM_deep_reexport_chain [368.70ms] (pass) bundler > cjs/__toESM_reexport_with_rename [443.84ms] (pass) bundler > cjs/__toESM_default_prop ... (truncated) release without fix: 20 FAILED bun test v1.4.1-canary.1 (a6c4cc2) test/bundler/bundler_cjs.test.ts: runtime failed file: /tmp/bun-build-tests/bun-4t1lr0/cjs/__toESM_import_syntax_with_esModule/out.js stdout output: {"__esModule":true,"default":{"value":"default export"},"named":"named export"} --- expected stdout: {"value":"default export"} --- 1913 | console.log(`---`); 1914 | console.log(`expected ${name}:`); 1915 | console.log(expected); 1916 | console.log(`---`); 1917 | } 1918 | expect(result).toBe(expected); ^ error: expect(received).toBe(expected) Expected: "{"value":"default export"}" Received: "{"__esModule":true,"default":{"value":"default export"},"named":"named export"}" at <anonymous> (/workspace/bun/test/bundler/expectBundled.ts:1918:28) (fail) bundler > cjs/__toESM_import_syntax_with_esModule [29.17ms] (pass) bundler > cjs/__toESM_import_syntax_without_esModule [11.71ms] (pass) bundler > cjs/__toESM_import_syntax_function [9.90ms] (pass) bundler > cjs/__toESM_import_syntax_primitive [9.43ms] (pass) bundler > cjs/__toESM_import_syntax_named_and_default [9.97ms] ... (truncated) ``` </details> <details><summary>passes on PR (with fix)</summary> ```console ASAN with fix: all passed $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/bundler/bundler_cjs.test.ts bun test v1.4.1 (a6c4cc2) test/bundler/bundler_cjs.test.ts: (pass) bundler > cjs/__toESM_import_syntax_with_esModule [1024.06ms] (pass) bundler > cjs/__toESM_import_syntax_without_esModule [473.45ms] (pass) bundler > cjs/__toESM_import_syntax_function [489.71ms] (pass) bundler > cjs/__toESM_import_syntax_primitive [494.25ms] (pass) bundler > cjs/__toESM_import_syntax_named_and_default [388.89ms] (pass) bundler > cjs/__toESM_import_syntax_namespace [381.97ms] (pass) bundler > cjs/__toESM_target_node [389.92ms] (pass) bundler > cjs/__toESM_target_browser [453.17ms] (pass) bundler > cjs/__toESM_target_bun [481.14ms] (pass) bundler > cjs/__toESM_format_esm [415.53ms] (pass) bundler > cjs/__toESM_format_cjs_with_import [376.77ms] (pass) bundler > cjs/__toESM_mjs_reexport [451.67ms] (pass) bundler > cjs/__toESM_mjs_reexport_with_esModule [380.06ms] (pass) bundler > cjs/__toESM_deep_reexport_chain [453.02ms] (pass) bundler > cjs/__toESM_reexport_with_rename [430.08ms] (pass) bundler > cjs/__toESM_default_pro ... (truncated) release with fix: all passed $ bun scripts/build.ts --profile=release [configured] bun-profile → bun (stripped) target linux-x64-gnu build type Release build dir ./build/release revision 822e3b2 features baseline 23 deps, 131 codegen, 1172 objects in 647ms ninja: Entering directory `/workspace/bun/build/release' [1/1244] install /workspace/bun bun install v1.4.1-canary.1 (a6c4cc2) Checked 25 installs across 62 packages (no changes) [10.00ms] [2/1244] gen bindgenv2 [3/1244] gen ErrorCode+*.h [4/1244] install /workspace/bun/packages/bun-error bun install v1.4.1-canary.1 (a6c4cc2) Checked 1 install across 2 packages (no changes) [3.00ms] [5/1244] fetch libjpeg-turbo [libjpeg-turbo] up to date [6/1217] gen ProcessBindingConstants.lut.h Generating /workspace/bun/build/release/codegen/ProcessBindingConstants.lut.h from /workspace/bun/src/jsc/bindings/ProcessBindingConstants.cpp [7/1217] gen bake.{client,server,error}.js -> bake.client.js, bake.server.js, bake.error.js [8/1217] fetch tinycc [tinycc] up to date [9/1216] install /workspace/bun/src/node-fallbacks bun install v1.4.1-canary.1 (a6c4cc2) Checked 111 installs across 104 packages (no changes) [15.00 ... (truncated) ``` </details> <details><summary>diff hotspot</summary> ``` src/resolver/dir_info.rs | 9 ++ src/resolver/resolver.rs | 20 +++-- src/resolver/result.rs | 15 +--- test/bundler/bundler_cjs.test.ts | 182 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 207 insertions(+), 19 deletions(-) ``` </details> **gate history** · 1 passed · 0 rejected · iteration 0 <details><summary>evidence per changed file</summary> ``` file reads edits tests src/resolver/dir_info.rs 3 4 33 src/resolver/resolver.rs 8 5 34 src/resolver/result.rs 1 1 33 test/bundler/bundler_cjs.test.ts 3 11 33 ``` </details> <!-- robobun:evidence:end -->
|
Closing as part of a cleanup of stale pull requests. This PR has had no new commits since 2026-07-12, it conflicts with main, and its last CI run failed. This is not a judgment on the fix itself. If the problem still reproduces on a current build, reopen this PR after a rebase or open a new one against main. |
Problem
A bare identifier reference to
moduleorexports(for example the common dual-mode guardtypeof module !== "undefined") makes Bun's parser classify a file as CommonJS even when the.mjs/.mtsextension or package.json"type":"module"already says it is ESM.Two observable faces:
importstatement (valid ESM, Node runs it), Bun refuses to load it withCannot use import statement with CommonJS-only features/note: This file is CommonJS because 'module' was used.typeof module === "object", top-levelthisis the exports object.Node prints
esm /; Bun errors.Cause
In
src/js_parser/parse/parse_entry.rs, theexports_kinddecision runs the CJS content sniff (uses_exports_ref || uses_module_ref || ...) before ever consultingoptions.module_type. Only anexportkeyword or top-levelawaitout-ranks the sniff; the extension and"type"field do not.Fix
parse_entry.rs: treatoptions.module_type == Esmthe same asexport/top-levelawaitwhen choosingexports_kind, on the runtime path only (!p.options.bundle). The bundler's resolver can returnEsmfor files that are actually CJS (a nameless nested{"type":"commonjs"}scope thatenclosing_package_jsonskips, or anexports"import"condition pointing at a.jsfile), so the content sniff is still needed there; the runtime path is fed the correct value by the change below.p.rsvalue_for_this: substitute the ESM value for top-levelthiswhenmodule_type == Esm(not only when ESM syntax is present), so a bare.mjswith top-levelthisdoes not rewrite toexports.RuntimeTranspilerStore: thread the caller's extension-awaremodule_typethroughtranspile()ontoTranspilerJoband use it in the worker, instead of re-deriving from the package.json tag. Theresolved_source.tag(used forignoreESModuleAnnotation) is left as-is.jsc_hooks.rs: derive the runtimemodule_typefor.js/.tsfrom the nearest package.json's"type"by walkingDirInfoparents, including nameless ones thatenclosing_package_jsonskips. Without this a.jsfile underparse5/dist/cjs/(which ships{"type":"commonjs"}with no name) was seeing the outerparse5"type":"module". Carried as a separateLoaderResultfield sopkg_namesemantics are unchanged.resolver.rsresolve_with_framework: stop hard-codingModuleType::Esmfor embeddedBuiltInModule::Code; the react-refresh fallback it carries is CJS. UseUnknownso the parser decides from content; the ESM.tsxbuilt-ins still classify as ESM via their import syntax.module_typeinto the runtime transpiler features hash (byte-identical source now transpiles differently under different module types) and bumpEXPECTED_VERSIONto 23 so stale entries with the old classification are invalidated.The
es-module-lexertest fixture declared"type":"module"while itsindex.tsis CJS and relied on the sniff override; the stray"type"is dropped.Tests
test/cli/run/run-cjs.test.tsgains adescribeblock covering.mjs,.mts,"type":"module".js, with and without import statements, loaded directly and via dynamicimport(), plus negative controls (.cjs, ambiguous.js,.cjsinside"type":"module",.mjsinside"type":"commonjs", and a nested nameless{"type":"commonjs"}under a"type":"module"root). 7 of the 11 new tests fail on the unfixed build and all pass with this change.test/bundler/bundler_edgecase.test.tsaddsNamelessNestedTypeCommonjsUnderTypeModuleandExportsMapImportConditionPointsAtCjsto lock in the bundler keeping its content-sniff fallback for those layouts.Also ran:
test/js/bun/resolve/,test/bundler/bundler_cjs*.test.ts,test/bundler/bundler_edgecase.test.ts,test/bundler/esbuild/default.test.ts,test/bundler/transpiler/,test/cli/run/transpiler-cache.test.ts,test/integration/jsdom/,test/bake/dev/react-spa.test.ts,test/bake/dev/esm.test.tswith no new failures.Related: #32173 independently makes the same
value_for_thischange (plus changingnulltoundefinedthere) and the same cache-key addition; whichever lands second will have a small conflict in those two spots.no test proof · iteration 5 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/bundler/bundler_edgecase.test.ts