Skip to content

bundler: writes through a lifted CommonJS module's namespace assign the bindings - #41182

Merged
Jarred-Sumner merged 1 commit into
mainfrom
robobun/bf7d2b68/lifted-namespace-setters
Sep 2, 2026
Merged

Jarred-Sumner merged 1 commit into
mainfrom
robobun/bf7d2b68/lifted-namespace-setters

Conversation

@robobun

@robobun robobun commented Sep 2, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • Since bundler: bind the default import of a lifted CommonJS module to its namespace #41162 a default import of a lifted CommonJS module is its namespace object, and X.foo reads the lifted $foo binding. A write went the other way: import config from "./config.js"; config.debug = true; console.log(config.debug) printed false, because config.debug = true landed on the namespace object's setter (which only shadows the getter) while the read was $debug. Bun 1.4.1 and Node print true.
  • The SSR idiom React.useLayoutEffect = React.useEffect had the same gap, and with --no-deprecated-namespace-object-setters the write threw at startup.

Fix

  • The namespace object of a lifted module stands in for module.exports, so each of its local exports now gets a setter that assigns the lifted binding: __exportCjs(exports_config, { debug: () => $debug }, { debug: (value) => $debug = value }). A write through the default import, or through import * as ns reached via a re-export, is then seen by every reader, named imports included.
  • Step 4 of scan_imports_and_exports creates the setters' parameter symbol for each lifted module (step 5 runs in parallel and cannot create symbols). create_exports_for_file emits the setter arrows. Exports that are re-exports of imports keep a getter only.
  • The setters are emitted whatever deprecatedNamespaceObjectSetters says: that option is about ES module namespaces, which are read-only. A CommonJS module.exports is writable.
  • Verified: test/bundler/bundler_cjs2esm.test.ts (5 new tests, 3 fail on main and on the released bun, all pass with the change). Also bundler_cjs, esbuild/importstar, esbuild/importstar_ts, esbuild/default, esbuild/dce, bundler_splitting, bundler_jsx, bundler_npm, bundler_edgecase, bundler_minify, bundler_barrel, bundler_browser, bundler_bun, bun-build-api, bundler_promiseall_deadcode, bundler_bytecode_portable, regression/issue/cyclic-imports-async-bundler.

Background

  • Lifting turns exports.foo = x into var $foo = x; export { $foo as foo }, and bundler: bind the default import of a lifted CommonJS module to its namespace #41162 binds import X from that module to its exports_ref, the namespace object var exports_x = {}; __export(exports_x, {...}), which exists only when X escapes. X.foo reads compile to $foo.
  • __export defines a getter per export plus a setter that replaces the getter's value on the namespace object without touching the binding. __exportGetters (--no-deprecated-namespace-object-setters) defines no setter. Both are right for an ES module namespace, which is read-only in the spec.
  • The new __exportCjs helper is appended at the end of runtime.js, so the part ranges of the existing runtime statements, and with them the debugId and bun-build-api hash snapshots, do not move.
Notes

Writes are more live than in Node: Node's named imports of a CommonJS module are snapshots, so there config.debug = true is seen by config.debug but not by import { debug }. Bun 1.4.1 made every reader see it (everything went through the CommonJS object), and this change keeps that.

import * as ns from "./lifted"; ns.debug = true is a build error (Cannot assign to import), as in esbuild. A namespace reached through a re-export (export * as ns, export { ns }) takes the new setters like the default import does.

A write of a key the module does not export (config.extra = 1) creates a plain data property on the namespace object. config.extra reads stay property reads, so they see it.

The docs paragraph in docs/bundler/index.mdx no longer lists react-dom among lifted packages: its entry files have the checkDCE(); module.exports = require(...) shape, which the lifter does not convert today (handed off separately).


[human-review] gate passed · iteration 0 · 6 files touched

fails on main (without fix)
ASAN without fix: 6 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_cjs2esm.test.ts"
bun test v1.4.1 (a6c4cc276)

test/bundler/bundler_cjs2esm.test.ts:
(pass) bundler > cjs2esm/ModuleExportsFunction [809.00ms]
(pass) bundler > cjs2esm/ImportNamedFromExportStarCJSModuleRef [467.35ms]
(pass) bundler > cjs2esm/ImportNamedFromExportStarCJS [415.11ms]
(pass) bundler > cjs2esm/BadNamedImportNamedReExportedFromCommonJS [342.57ms]
(pass) bundler > cjs2esm/ExportsFunction [370.34ms]
(pass) bundler > cjs2esm/ModuleExportsFunctionTreeShaking [375.62ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequire [352.13ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPoint [430.19ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPointImportedByEntryPoint [405.92ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPointImportedByEntryPointSplitting [482.24ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireTwoEntryPoints [466.34ms]
(pass) bundler > cjs2esm/ModuleExportsBasedOnNodeEnvProduction [635.00ms]
(pass) bundler > cjs2esm/ModuleExportsBasedOnNodeEnvDevelopment [565
... (truncated)

release without fix: 21 FAILED
bun test v1.4.1-canary.1 (a6c4cc276)

test/bundler/bundler_cjs2esm.test.ts:
(pass) bundler > cjs2esm/ModuleExportsFunction [20.03ms]
(pass) bundler > cjs2esm/ImportNamedFromExportStarCJSModuleRef [9.71ms]
(pass) bundler > cjs2esm/ImportNamedFromExportStarCJS [10.44ms]
(pass) bundler > cjs2esm/BadNamedImportNamedReExportedFromCommonJS [8.94ms]
(pass) bundler > cjs2esm/ExportsFunction [9.01ms]
(pass) bundler > cjs2esm/ModuleExportsFunctionTreeShaking [9.45ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequire [8.67ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPoint [9.79ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPointImportedByEntryPoint [10.45ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPointImportedByEntryPointSplitting [10.40ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireTwoEntryPoints [10.32ms]
(pass) bundler > cjs2esm/ModuleExportsBasedOnNodeEnvProduction [11.27ms]
(pass) bundler > cjs2esm/ModuleExportsBasedOnNodeEnvDevelopment [11.63ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRuntimeCondition [10.23ms]
(pass) bundler > cjs2esm/UnwrappedModuleRequireAssigned [9.67ms]
(pass) bundler > cjs2esm/UnwrappedModu
... (truncated)
passes on PR (with fix)
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_cjs2esm.test.ts"
bun test v1.4.1 (a6c4cc276)

test/bundler/bundler_cjs2esm.test.ts:
(pass) bundler > cjs2esm/ModuleExportsFunction [803.43ms]
(pass) bundler > cjs2esm/ImportNamedFromExportStarCJSModuleRef [429.98ms]
(pass) bundler > cjs2esm/ImportNamedFromExportStarCJS [410.65ms]
(pass) bundler > cjs2esm/BadNamedImportNamedReExportedFromCommonJS [355.03ms]
(pass) bundler > cjs2esm/ExportsFunction [346.98ms]
(pass) bundler > cjs2esm/ModuleExportsFunctionTreeShaking [361.86ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequire [415.96ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPoint [387.77ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPointImportedByEntryPoint [422.52ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPointImportedByEntryPointSplitting [439.54ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireTwoEntryPoints [408.06ms]
(pass) bundler > cjs2esm/ModuleExportsBasedOnNodeEnvProduction [568.38ms]
(pass) bundler > cjs2esm/ModuleExportsBasedOnNodeEnvDevelopment [723
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 650ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[0/4] cargo bun_runtime → libbun_runtime.a
�[1m�[92m   Compiling�[0m bun_bundler v0.0.0 (/workspace/bun/src/bundler)
�[1m�[92m   Compiling�[0m bun_standalone_graph v0.0.0 (/workspace/bun/src/standalone_graph)
�[1m�[92m   Compiling�[0m bun_transpiler v0.0.0 (/workspace/bun/src/transpiler)
�[1m�[92m   Compiling�[0m bun_bunfig v0.0.0 (/workspace/bun/src/bunfig)
�[1m�[92m   Compiling�[0m bun_install v0.0.0 (/workspace/bun/src/install)
�[1m�[92m   Compiling�[0m bun_jsc v0.0.0 (/workspace/bun/src/jsc)
�[1m�[92m   Compiling�[0m bun_js_parser_jsc v0.0.0 (/workspace/bun/src/js_parser_jsc)
�[1m�[92m   Compiling�[0m bun_semver_jsc v0.0.0 (/workspace/bun/src/semver_jsc)
�[1m�[92m   Compiling�[0m bun_bundler_jsc v0.0.0 (/workspace/bun/src/bundler_jsc)
�[1m�[92m   Compiling�[0m bun_sourcemap_jsc v0.0.0 (/workspace/bun/src/sourcemap_jsc)
�[1m�[92m   Compiling�[0m bun_css_jsc v0.0.0 (/workspace/bun/src/css_jsc)
�[1m�[92m   Compiling�[0m bun_sql_jsc v0.0.0 (/workspace/bun/src/sql_jsc)
�[1m�[92m   Compiling�[0m bun_ast_js
... (truncated)
diff hotspot
docs/bundler/index.mdx                             |   4 +-
 src/bundler/LinkerGraph.rs                         |   5 +
 src/bundler/linker_context/doStep5.rs              | 122 ++++++++++++++++++---
 .../linker_context/scanImportsAndExports.rs        |  38 ++++++-
 src/runtime.js                                     |  13 +++
 test/bundler/bundler_cjs2esm.test.ts               | 111 ++++++++++++++++++-
 6 files changed, 271 insertions(+), 22 deletions(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                                 reads  edits  tests
docs/bundler/index.mdx                                   1      1     29
src/bundler/LinkerGraph.rs                               1      0     28
src/bundler/linker_context/doStep5.rs                    7      3     28
src/bundler/linker_context/scanImportsAndExports.rs     11     15     28
src/runtime.js                                           4      6     30
test/bundler/bundler_cjs2esm.test.ts                     6      7     28

…he bindings

`import config from "./config.js"; config.debug = true` landed on the
namespace object's setter while every read of `config.debug` was the
lifted `$debug` binding, so the write was lost. The namespace of a
lifted module stands in for `module.exports`, so its properties now get
setters that assign the binding (`__exportCjs`), and every importer sees
the new value.
@coderabbitai

coderabbitai Bot commented Sep 2, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 3e0cbde1-2073-4c99-97f9-0944af609f41

📥 Commits

Reviewing files that changed from the base of the PR and between d6af50f and 3d62ed4.

📒 Files selected for processing (6)
  • docs/bundler/index.mdx
  • src/bundler/LinkerGraph.rs
  • src/bundler/linker_context/doStep5.rs
  • src/bundler/linker_context/scanImportsAndExports.rs
  • src/runtime.js
  • test/bundler/bundler_cjs2esm.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 1 remains after this review.


Walkthrough

Summary

Lifted CommonJS namespaces now use __exportCjs with generated setters. Linker metadata tracks setter parameters. Tests and documentation cover write-through behavior across imports, minification, React compatibility, and code splitting.

Changes

Lifted CommonJS namespace writes

Layer / File(s) Summary
Setter metadata and symbol preparation
src/bundler/LinkerGraph.rs, src/bundler/linker_context/scanImportsAndExports.rs
Linker metadata stores lifted_setter_param. The linker prepares the setter value symbol and caches __exportCjs.
Setter-aware namespace export generation
src/bundler/linker_context/doStep5.rs, src/bundler/linker_context/scanImportsAndExports.rs, src/runtime.js
Step 5 generates setter properties for lifted CommonJS bindings. Namespace initialization uses __exportCjs when setter parameters are present. Other modules retain the existing export helper.
Write-through validation and documentation
test/bundler/bundler_cjs2esm.test.ts, docs/bundler/index.mdx
Tests cover default, named, minified, React, disabled legacy setters, and code-split cases. Documentation describes lifted CommonJS namespace assignment behavior.

Suggested reviewers: dylan-conway, jarred-sumner

Merge Risk: ⚪ Minimal · up to 3d62e

This change makes writes through lifted CommonJS namespace objects update the underlying exported bindings. No actionable merge-blocking risk remains, so the PR is merge-ready after normal checks and review.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The description clearly explains the problem, fix, background, and verification results. It does not use the exact template headings, but it provides the required information in equivalent sections.
Title check ✅ Passed The title clearly identifies the primary bundler change: namespace writes update bindings in lifted CommonJS modules. It is concise and relevant.

Comment @coderabbitai help to get the list of available commands.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I reviewed this PR and didn't find any bugs. Because it changes bundler output semantics for every lifted CommonJS module and threads new state through the parallel step-4/step-5 linker path, a human look at the linker and runtime pieces would still be worthwhile.

What was reviewed:

  • Step 4 creates lifted_setter_param before step 5's parallel run; the unsafe SoA row read in do_step5 matches the surrounding per-task-row pattern and the SAFETY comment is accurate.
  • __exportCjs with set: setters[name] — when an alias has no setter (re-export of an import), set: undefined yields a getter-only accessor, matching the stated intent.
  • The __export → __exportCjs assertion update in the existing cjs2esm test is justified: that fixture is a lifted CJS file, so it now hits the new helper.
  • Variant coverage checked: minifyIdentifiers (shared param symbol across all setter arrows), deprecatedNamespaceObjectSetters: false, code-splitting, and a non-exported extra key write.
Extended reasoning...

Overview

The PR makes writes through a lifted CommonJS module's namespace object (its default import) assign the underlying lifted bindings, so config.debug = true is observed by named imports and import * as it was before lifting. It adds a per-file lifted_setter_param: Ref column to LinkerGraph's js_meta SoA, populates it in scanImportsAndExports step 4 (serial), reads it in doStep5 (parallel per file) to emit a third { alias: (value) => $binding = value } argument to a new __exportCjs runtime helper, and wires the runtime-import edge in step 4 to point at __exportCjs instead of __export/__exportGetters for lifted files. Docs and five itBundled cases in bundler_cjs2esm.test.ts accompany the change.

Security risks

None. This is bundler codegen and a small runtime.js helper; no untrusted input parsing, auth, crypto, or filesystem paths are involved. The new __exportCjs uses the already-captured __defProp primordial and iterates the compiler-generated getters object with for..in, identical in shape to the existing __export.

Level of scrutiny

Medium-high. The change is not mechanical: it alters emitted code for an entire class of inputs (every module the CJS→ESM lifter converts), threads new state between two linker phases where step 5 is task-parallel and cannot allocate symbols, and adds an unsafe raw-pointer read. The step-4 symbol creation is placed alongside the existing needs_exports_variable/wrapper-part logic and gates on COMMONJS_LIFTED_TO_ESM with the same export_kind/wrap guards used nearby, and the step-5 raw read follows the exact row_mut!/.cast().add(id) SoA convention already in the function. Still, this is the linker — a reviewer familiar with LinkerContext should confirm the declared_symbols treatment of the shared setter-param ref (declared once, is_top_level: true, reused as the binding of every setter arrow) is correct for renaming/scope-hoisting, and that not adding lifted_setter_param to ns_export_symbol_uses is intentional.

Other factors

Test coverage is good and follows REVIEW.md's variant-matrix guidance (plain, minified with identifier mangling, deprecatedNamespaceObjectSetters: false, the React SSR patching idiom, and cross-chunk splitting). The one weakened assertion (__export → __exportCjs) is a direct consequence of the change and the test's run.stdout still asserts behavior. The __exportCjs helper is appended at the end of runtime.js per the PR's stated intent of not shifting existing part ranges. No CODEOWNERS entries cover the touched paths. Given the scope — every lifted-CJS namespace in every bundle — I'm deferring rather than approving.

@Jarred-Sumner
Jarred-Sumner merged commit 84e2bb2 into main Sep 2, 2026
12 of 13 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the robobun/bf7d2b68/lifted-namespace-setters branch September 2, 2026 23:16
Jarred-Sumner pushed a commit that referenced this pull request Sep 3, 2026
…ace as default (#41231)

### Problem
- On canary, with `--splitting`, an `import()` of a lifted CommonJS
module (`exports.x = ...`) loads a chunk whose `default` is a
getter-only object. Since #41162 the static default import is the
namespace object. So `(await import("./lib.cjs")).default !== lib`, and
`m.default.x = v` throws `TypeError: Attempted to assign to readonly
property`. Bun 1.4.1 gives one object. No release has the bug.
- The cause is the synthetic default export in
`generate_entry_point_tail_js`
(`src/bundler/linker_context/postProcessJSChunk.rs:1089`). Before
#41162, the chunk printed `export default require_lib()`.

### Fix
- The chunk of a lifted module prints `export default exports_lib`, the
namespace object. A write through `m.default` assigns the lifted binding
(#41182), so `lib` and `import { x }` see it.
- Step 1 of `scan_imports_and_exports` sets
`needs_synthetic_default_export` only when an `import()` can read
`default`. Step 6 then keeps the namespace part alive.
- `exports.default` is a property of `module.exports`, as in Node and
`bun run`. A module that also sets `__esModule` keeps `exports.default`
as the chunk's `default`, as before.
- Verified: `test/bundler/bundler_cjs2esm.test.ts` (9 new tests, 8 fail
on main), and the suites in the notes.

### Background
- Lifting turns `exports.foo = x` into `var $foo = x; export { $foo as
foo }`. `exports_ref` names the namespace object. Its part is
tree-shaken unless a live part depends on it.
- With code splitting, each `import()` target is an entry point with its
own chunk.
- The parser records the properties read on an `import()` result. An
untracked use can read any export.

<details><summary>Notes</summary>

Suites run: `bundler_cjs2esm`, `bundler_cjs`, `bundler_splitting`,
`bundler_dynamic_import_dce`, `esbuild/splitting`, `esbuild/importstar`,
`esbuild/importstar_ts`, `esbuild/default`, `esbuild/dce`,
`bundler_edgecase`, `bundler_npm`, `bundler_minify`. On the first
version of this branch, also `bundler_barrel`, `bundler_jsx`,
`bundler_browser`, `bundler_bun` and `bun-build-api` (two bytecode tests
time out at 5s in the debug build, unrelated).

Repro from the report, `bun build ./entry.mjs --splitting --target=bun
--outdir=s`:

```js
// lib.cjs
exports.createElement = function (t) { return "<" + t + ">"; };
exports.version = "19.x";
// entry.mjs
import lib from "./lib.cjs";
const m = await import("./lib.cjs");
m.default.createElement = () => "PATCHED";
console.log(m.default === lib, lib.createElement("q"));
```

Canary: `TypeError: Attempted to assign to readonly property.` With this
change: `true PATCHED`, the same as `bun entry.mjs` and Node. The chunk
is `export default exports_lib;`, with `exports_lib` imported from the
shared chunk.

With #41186 (on main), both changes read the properties that the parser
records for an `import()` result. Two tests pin how they combine. A
static default import and a named import of a lifted module bind
`$useState` directly in both. If the split `import()` only destructures,
the chunk has no `default` and no namespace object. If it reads
`default`, the chunk exports `exports_lib`, and a write through
`m.default` reaches both static importers.

When to emit the namespace default. The flag is set per `import()` site,
when both hold:
- the site can read `default`: its uses are not all tracked, it reads
`default`, or the target is a user's entry point.
- the site's `default` is `module.exports`: the module has no `default`
export, or it is lifted, is not a user's entry point, and does not set
`__esModule`.

A chunk that no importer reads `default` from now has no `default`
export and no namespace object. For `const { version } = await
import("./lib.cjs")` with `--minify`, the output goes from 237 bytes on
canary to 166.

A lifted module with no exports gets `export default exports_x` too. On
canary its chunk had no `default`.

`import()` with splitting, for a module with `exports.default = "d"` and
`exports.x = 1`, dynamic import only, `typeof m.default`:

| | `bun run` | 1.4.1 | canary | this PR |
|---|---|---|---|---|
| no `__esModule`, `.js` or `.mjs` importer | object | string | string |
object |
| `__esModule`, `.js` or `.mjs` importer | string | string | string |
string |

With a static default import in the same `.js` or `.mjs` file and no
`__esModule`, `m.default === lib` is true on 1.4.1, false on canary and
true with this change.

Known cases this PR does not change:
- A module that sets `__esModule` and `exports.default`, with a static
default import and a split `import()` in the same file. From an `.mjs`
file, the static import is the namespace (Node's rule) and the
`import()` gives `exports.default`. From a `.js` file the module keeps
its `__commonJS` wrapper, and the next case applies.
- A split `import()` of a lifted module that is in a `__commonJS`
wrapper, because of a `require()` of it or the rule above, gets no
`__toESM` on the importer side. So its named exports are `undefined`.
This is the same on 1.4.1. It is tracked separately.
- A lifted user entry point that no `import()` reaches has no `default`
export. The `default` is decided per `import()` site, not per entry
point, as before this change.
- Open PR #40914 changes the same tail code in `postProcessJSChunk.rs`.
The two need a rebase against each other.
</details>

<!-- robobun:evidence:begin -->

---

**[human-review]** gate passed · iteration 0 · 6 files touched

<details><summary>fails on main (without fix)</summary>

```console
ASAN without fix: 8 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_cjs2esm.test.ts"
bun test v1.4.1 (a6c4cc2)

test/bundler/bundler_cjs2esm.test.ts:
(pass) bundler > cjs2esm/ModuleExportsFunction [881.80ms]
(pass) bundler > cjs2esm/ImportNamedFromExportStarCJSModuleRef [415.54ms]
(pass) bundler > cjs2esm/ImportNamedFromExportStarCJS [372.29ms]
(pass) bundler > cjs2esm/BadNamedImportNamedReExportedFromCommonJS [439.71ms]
(pass) bundler > cjs2esm/ExportsFunction [362.42ms]
(pass) bundler > cjs2esm/ModuleExportsFunctionTreeShaking [447.58ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequire [379.65ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPoint [491.09ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPointImportedByEntryPoint [441.26ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPointImportedByEntryPointSplitting [420.72ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireTwoEntryPoints [418.84ms]
(pass) bundler > cjs2esm/ModuleExportsBasedOnNodeEnvProduction [745.78ms]
(pass) bundler > cjs2esm/ModuleExportsBasedOnNodeEnvDevelopment [704
... (truncated)

release without fix: 35 FAILED
bun test v1.4.1-canary.1 (a6c4cc2)

test/bundler/bundler_cjs2esm.test.ts:
(pass) bundler > cjs2esm/ModuleExportsFunction [24.40ms]
(pass) bundler > cjs2esm/ImportNamedFromExportStarCJSModuleRef [13.68ms]
(pass) bundler > cjs2esm/ImportNamedFromExportStarCJS [15.04ms]
(pass) bundler > cjs2esm/BadNamedImportNamedReExportedFromCommonJS [13.59ms]
(pass) bundler > cjs2esm/ExportsFunction [13.89ms]
(pass) bundler > cjs2esm/ModuleExportsFunctionTreeShaking [14.70ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequire [16.19ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPoint [16.52ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPointImportedByEntryPoint [16.27ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPointImportedByEntryPointSplitting [24.98ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireTwoEntryPoints [18.70ms]
(pass) bundler > cjs2esm/ModuleExportsBasedOnNodeEnvProduction [14.39ms]
(pass) bundler > cjs2esm/ModuleExportsBasedOnNodeEnvDevelopment [15.65ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRuntimeCondition [13.84ms]
(pass) bundler > cjs2esm/UnwrappedModuleRequireAssigned [16.11ms]
(pass) bundler > cjs2esm/Unwrap
... (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_cjs2esm.test.ts"
bun test v1.4.1 (a6c4cc2)

test/bundler/bundler_cjs2esm.test.ts:
(pass) bundler > cjs2esm/ModuleExportsFunction [959.21ms]
(pass) bundler > cjs2esm/ImportNamedFromExportStarCJSModuleRef [409.94ms]
(pass) bundler > cjs2esm/ImportNamedFromExportStarCJS [587.69ms]
(pass) bundler > cjs2esm/BadNamedImportNamedReExportedFromCommonJS [379.52ms]
(pass) bundler > cjs2esm/ExportsFunction [365.54ms]
(pass) bundler > cjs2esm/ModuleExportsFunctionTreeShaking [394.43ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequire [474.62ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPoint [497.44ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPointImportedByEntryPoint [547.38ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPointImportedByEntryPointSplitting [527.60ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireTwoEntryPoints [456.15ms]
(pass) bundler > cjs2esm/ModuleExportsBasedOnNodeEnvProduction [843.87ms]
(pass) bundler > cjs2esm/ModuleExportsBasedOnNodeEnvDevelopment [650
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 586ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[0/86] cargo bun_runtime → libbun_runtime.a
�[1m�[92m   Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m   Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
�[1m�[92m   Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
�[1m�[92m   Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
�[1m�[92m   Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety)
�[1m�[92m   Compiling�[0m bun_base64 v0.0.0 (/workspace/bun/src/base64)
�[1m�[92m   Compiling�[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys)
�[1m�[92m   Compiling�[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys)
�[1m�[92m   Compiling�[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd)
�[1m�[92m   Compiling�[0m bun_picohttp v0.0.0 (/workspace/bun/src/picohttp)
�[1m�[92m   Compiling�[0m bun_brotli v0.0.0 (/workspace/bun/src/brotli)
�[1m�[92m   Compiling�[0m bun_output v0.0.0 (/workspace/bun/src/output)
�[1m�[92m   Compiling�[0m bun_clap v0.0.0 (/workspace/bun/src/clap)
�[1m�[92m   Compiling�[0m b
... (truncated)
```

</details>

<details><summary>diff hotspot</summary>

```
docs/bundler/index.mdx                             |   2 +-
 src/bundler/LinkerContext.rs                       |  10 +
 .../computeCrossChunkDependencies.rs               |  11 +-
 src/bundler/linker_context/postProcessJSChunk.rs   |  41 +++-
 .../linker_context/scanImportsAndExports.rs        |  45 +++--
 test/bundler/bundler_cjs2esm.test.ts               | 215 ++++++++++++++++++++-
 6 files changed, 301 insertions(+), 23 deletions(-)
```

</details>

**gate history** · 1 passed · 0 rejected · iteration 0

<details><summary>evidence per changed file</summary>

```
file                                                      reads  edits  tests
docs/bundler/index.mdx                                        0      0     28
src/bundler/LinkerContext.rs                                  2      5     29
…bundler/linker_context/computeCrossChunkDependencies.rs      2      5     28
src/bundler/linker_context/postProcessJSChunk.rs              8      5     28
src/bundler/linker_context/scanImportsAndExports.rs           9     14     28
test/bundler/bundler_cjs2esm.test.ts                          3      8     28
```

</details>

<!-- robobun:evidence:end -->
Jarred-Sumner pushed a commit that referenced this pull request Sep 14, 2026
…default import as a value (#42569)

### Problem
- Since #41162 (1.4.1), `import lib from "./lib.cjs"` of a lifted
CommonJS module is a namespace object with a getter and a setter per
export. `Object.defineProperty(lib, "x", ...)` and `delete lib.y` change
that object, but `lib.x` reads the lifted `$x` binding. A write after
`Object.freeze(lib)` still runs the setter.
- Node, `bun run` and 1.4.0 print `defineProperty -> 65`, `delete ->
true undefined false`, `write to frozen -> 3 true`. The bundle prints
`1`, `true 2 false`, `66 true`.
- Source: a fuzz ledger that compares bundles with `bun run`. No user
issue reports this.

### Fix
- The parser sets a new symbol flag, `import_used_as_value`, on an
import that some use holds as a value. Exempt: a read, call or
assignment of a property (`X.a`, `X[k]`, `X.a = 1`) and a destructuring
declaration. `delete X.a` counts.
- Step 1 of `scan_imports_and_exports` keeps the `__commonJS` wrapper of
a lifted module when a default import of it has the flag or is
re-exported. The importer gets the real `module.exports`, as in 1.4.0.
- `React.useState()` and `React.x = y` stay lifted: react 18.3.1
`--production --minify` is 1423 bytes, same as main. With
`Object.keys(React)` it is 7219 bytes (main 7794).
- Verified: `test/bundler/bundler_cjs2esm.test.ts` (11 new cases fail on
main), plus the suites in the Notes.

### Background
- Lifting turns `exports.foo = x` into `var $foo = x` plus an ES module
export.
- The namespace object `exports_lib` stands in for `module.exports`.
`__exportCjs` (#41182) gives it a getter and a setter per export.
- `defineProperty` and `delete` only replace or remove the accessor. A
frozen accessor still calls its setter.
- Step 1 picks the CommonJS files before imports are matched, so it has
parser facts only. A re-export counts as held.

<details><summary>Notes</summary>

Output for the report's repro is now byte-identical to what 1.4.0 emits
(`var import_lib = __toESM(require_lib(), 1);
Object.defineProperty(import_lib.default, "x", ...)`).

How the flag is computed: `e_dot` and `e_index` visit their target with
`ExprIn::is_property_access_target` (false for a delete target),
`visit_decls` does the same for the initializer of an object pattern,
and `handle_identifier` sets the flag when it turns an identifier into
an `EImportIdentifier` without it. Two places build an import identifier
outside the visitor and pass the bit themselves: the classic JSX factory
(`React.createElement`) and `emitDecoratorMetadata` (`design:type` gets
the import itself, so it sets the flag). Dead control flow does not set
the flag. The flag is per file: a use in a part that tree shaking later
removes still counts.

Routes to `module.exports` of a module that stays lifted that this PR
does not close. 1.4.0 kept the wrapper for every default import, so when
the bundle also has a static default import of the module, these gave
the real object in 1.4.0 and give the namespace object here:
- `this` in a method call. `lib.self()` with `exports.self = function ()
{ return this; }` still gets the namespace object (#41162 chose that for
`this._helper()` style code).
- `(await import(m)).default`, same chunk or split.
- `ns.default` on `import * as ns` (new in 1.4.1, #41820 reworks it).

`require()` of a lifted module outside the react family already keeps
the wrapper.

A react-family file of the shape `sideEffect(); module.exports =
require("./impl")` (react-dom/index.js) is lifted to `export * from
"./impl"`. When it keeps its wrapper it prints `module.exports =
exports_impl`, the namespace object of `./impl`, as 1.4.0 did. Every
read then goes through that object, so `defineProperty` and `delete`
work (react-dom 18.3.1 `--production`: `Object.defineProperty(ReactDOM,
"version", { value: "dp" })` prints `dp`, as 1.4.0 does, and main prints
the old version). A write after `Object.freeze` still reaches the
binding there. An earlier commit of this PR also wrapped `./impl`. That
gave `module.exports = __toESM(require_impl())`, whose getters are not
configurable, so `defineProperty` threw. It is reverted. The real fix
for that shape is to print the raw `require_impl()` (handed off, #35722
had the approach).

#41820 is open and edits the same step 1 lines, the same docs paragraph
and the same tests. This PR is the smaller one and restores 1.4.0
output, so it is simpler to land it first and rebase #41820.

Test expectations that changed, all toward Node:
- `cjs/__toESM_mixed_import_styles` is back to its expectation from
before #41162 (the namespace has a `default` key).
- `ReExportDefaultAsNameFromLiftedCommonJS` and
`ExportDefaultOfLiftedCommonJSDefaultImport` now keep the wrapper (a
re-export can reach an importer that holds the object) and also check
`Object.freeze` through the barrel. A third form, `export { React as R
}`, is added.
- `DefaultImportEscapingKeepsAssignmentOrder` became
`DefaultImportPassedAsValueKeepsAssignmentOrder` and expects the
wrapper.
- Ten tests compared the default import by identity (`m.default ===
React`) or passed it to `Object.keys`. They now compare a property
(`m.default.useState === React.useState`) so that they keep testing the
lifted path.

Suites run with the debug build: `bundler_cjs2esm`, `bundler_cjs`,
`esbuild/importstar`, `esbuild/importstar_ts`, `esbuild/default`,
`esbuild/dce`, `esbuild/splitting`, `esbuild/packagejson`, `esbuild/ts`,
`bundler_splitting`, `bundler_edgecase`, `bundler_jsx`, `bundler_npm`,
`bundler_barrel`, `bundler_minify`, `bundler_browser`, `bundler_bun`,
`bundler_plugin`, `bundler_loader`, `bundler_string`,
`bundler_promiseall_deadcode`, `transpiler/transpiler`,
`regression/issue/03844`, `bun-build-api` (two bytecode stress tests
time out at 5 s under the debug build, they bundle no imports).
</details>

<!-- robobun:evidence:begin -->

---

**[human-review]** gate passed · iteration 0 · 10 files touched

<details><summary>fails on main (without fix)</summary>

```console
ASAN without fix: 12 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 "test/bundler/bundler_cjs2esm.test.ts"
bun test v1.4.3 (b993710)

test/bundler/bundler_cjs2esm.test.ts:
(pass) bundler > cjs2esm/ModuleExportsFunction [1254.52ms]
(pass) bundler > cjs2esm/ImportNamedFromExportStarCJSModuleRef [548.64ms]
(pass) bundler > cjs2esm/ImportNamedFromExportStarCJS [554.77ms]
(pass) bundler > cjs2esm/BadNamedImportNamedReExportedFromCommonJS [472.17ms]
(pass) bundler > cjs2esm/ExportsFunction [398.00ms]
(pass) bundler > cjs2esm/ModuleExportsFunctionTreeShaking [505.22ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequire [490.55ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPoint [561.72ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPointImportedByEntryPoint [617.95ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPointImportedByEntryPointSplitting [686.37ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireTwoEntryPoints [551.89ms]
(pass) bundler > cjs2esm/ModuleExportsBasedOnNodeEnvProduction [704.70ms]
(pass) bundler > cjs2esm/ModuleExp
... (truncated)

release without fix: 12 FAILED
bun test v1.4.3-canary.1 (b993710)

test/bundler/bundler_cjs2esm.test.ts:
(pass) bundler > cjs2esm/ModuleExportsFunction [29.48ms]
(pass) bundler > cjs2esm/ImportNamedFromExportStarCJSModuleRef [14.24ms]
(pass) bundler > cjs2esm/ImportNamedFromExportStarCJS [13.88ms]
(pass) bundler > cjs2esm/BadNamedImportNamedReExportedFromCommonJS [13.68ms]
(pass) bundler > cjs2esm/ExportsFunction [14.09ms]
(pass) bundler > cjs2esm/ModuleExportsFunctionTreeShaking [13.74ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequire [13.08ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPoint [16.07ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPointImportedByEntryPoint [17.11ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPointImportedByEntryPointSplitting [15.81ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireTwoEntryPoints [15.04ms]
(pass) bundler > cjs2esm/ModuleExportsBasedOnNodeEnvProduction [17.65ms]
(pass) bundler > cjs2esm/ModuleExportsBasedOnNodeEnvDevelopment [15.90ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRuntimeCondition [16.27ms]
(pass) bundler > cjs2esm/UnwrappedModuleRequireAssigned [15.69ms]
(pass) bundler > cjs2esm/Unwrap
... (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 "test/bundler/bundler_cjs2esm.test.ts"
bun test v1.4.3 (b993710)

test/bundler/bundler_cjs2esm.test.ts:
(pass) bundler > cjs2esm/ModuleExportsFunction [1115.42ms]
(pass) bundler > cjs2esm/ImportNamedFromExportStarCJSModuleRef [521.71ms]
(pass) bundler > cjs2esm/ImportNamedFromExportStarCJS [536.01ms]
(pass) bundler > cjs2esm/BadNamedImportNamedReExportedFromCommonJS [594.49ms]
(pass) bundler > cjs2esm/ExportsFunction [501.50ms]
(pass) bundler > cjs2esm/ModuleExportsFunctionTreeShaking [632.29ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequire [418.24ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPoint [456.28ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPointImportedByEntryPoint [591.53ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPointImportedByEntryPointSplitting [603.73ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireTwoEntryPoints [546.68ms]
(pass) bundler > cjs2esm/ModuleExportsBasedOnNodeEnvProduction [667.62ms]
(pass) bundler > cjs2esm/ModuleExp
... (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     b9ce20b
  features     baseline

23 deps, 131 codegen, 1176 objects in 676ms

ninja: Entering directory `/workspace/bun/build/release'
[1/1248] install /workspace/bun
bun install v1.4.3-canary.1 (b993710)

Checked 22 installs across 61 packages (no changes) [14.00ms]
[2/1248] gen ErrorCode+*.h
[3/1248] gen bindgenv2
[4/1248] install /workspace/bun/packages/bun-error
bun install v1.4.3-canary.1 (b993710)

Checked 1 install across 2 packages (no changes) [2.00ms]
[5/1248] install /workspace/bun/src/node-fallbacks
bun install v1.4.3-canary.1 (b993710)

Checked 111 installs across 104 packages (no changes) [5.00ms]
[6/1248] fetch zlib
[zlib] up to date
[7/1248] gen node-fallbacks/react-refresh.js
Bundled 1 module in 10ms

  react-refresh.js  4.81 KB  (entry point)

[8/1248] fetch libjpeg-turbo
[libjpeg-turbo] up to date
[9/1248] fetch tinycc
[tinycc] up to date
[10/1247] gen .bind.ts → GeneratedBindings.cpp
[11/1247] gen bake.{client,server,error}.js
-> bake.client.js, bake.serve
... (truncated)
```

</details>

<details><summary>diff hotspot</summary>

```
docs/bundler/index.mdx                             |   2 +-
 src/ast/symbol.rs                                  |   4 +
 .../linker_context/scanImportsAndExports.rs        |  28 +-
 src/js_parser/fold.rs                              |   3 +
 src/js_parser/p.rs                                 |  19 +-
 src/js_parser/parser.rs                            |  14 +
 src/js_parser/visit/mod.rs                         |   2 +
 src/js_parser/visit/visit_expr.rs                  |  18 +-
 test/bundler/bundler_cjs.test.ts                   |   8 +-
 test/bundler/bundler_cjs2esm.test.ts               | 318 +++++++++++++++------
 10 files changed, 318 insertions(+), 98 deletions(-)
```

</details>

**gate history** · 1 passed · 0 rejected · iteration 0

<details><summary>evidence per changed file</summary>

```
file                                                 reads  edits  tests
docs/bundler/index.mdx                                   2      2     25
src/ast/symbol.rs                                        1      2     25
src/bundler/linker_context/scanImportsAndExports.rs      6     10     25
src/js_parser/fold.rs                                    1      1     25
src/js_parser/p.rs                                       7      6     25
src/js_parser/parser.rs                                  2      4     25
src/js_parser/visit/mod.rs                               1      1     25
src/js_parser/visit/visit_expr.rs                        5      3     25
test/bundler/bundler_cjs.test.ts                         1      2      9
test/bundler/bundler_cjs2esm.test.ts                    11     18     20
```

</details>

<!-- robobun:evidence:end -->
usrbinkat pushed a commit to usrbinkat/bun that referenced this pull request Sep 15, 2026
…default import as a value (oven-sh#42569)

### Problem
- Since oven-sh#41162 (1.4.1), `import lib from "./lib.cjs"` of a lifted
CommonJS module is a namespace object with a getter and a setter per
export. `Object.defineProperty(lib, "x", ...)` and `delete lib.y` change
that object, but `lib.x` reads the lifted `$x` binding. A write after
`Object.freeze(lib)` still runs the setter.
- Node, `bun run` and 1.4.0 print `defineProperty -> 65`, `delete ->
true undefined false`, `write to frozen -> 3 true`. The bundle prints
`1`, `true 2 false`, `66 true`.
- Source: a fuzz ledger that compares bundles with `bun run`. No user
issue reports this.

### Fix
- The parser sets a new symbol flag, `import_used_as_value`, on an
import that some use holds as a value. Exempt: a read, call or
assignment of a property (`X.a`, `X[k]`, `X.a = 1`) and a destructuring
declaration. `delete X.a` counts.
- Step 1 of `scan_imports_and_exports` keeps the `__commonJS` wrapper of
a lifted module when a default import of it has the flag or is
re-exported. The importer gets the real `module.exports`, as in 1.4.0.
- `React.useState()` and `React.x = y` stay lifted: react 18.3.1
`--production --minify` is 1423 bytes, same as main. With
`Object.keys(React)` it is 7219 bytes (main 7794).
- Verified: `test/bundler/bundler_cjs2esm.test.ts` (11 new cases fail on
main), plus the suites in the Notes.

### Background
- Lifting turns `exports.foo = x` into `var $foo = x` plus an ES module
export.
- The namespace object `exports_lib` stands in for `module.exports`.
`__exportCjs` (oven-sh#41182) gives it a getter and a setter per export.
- `defineProperty` and `delete` only replace or remove the accessor. A
frozen accessor still calls its setter.
- Step 1 picks the CommonJS files before imports are matched, so it has
parser facts only. A re-export counts as held.

<details><summary>Notes</summary>

Output for the report's repro is now byte-identical to what 1.4.0 emits
(`var import_lib = __toESM(require_lib(), 1);
Object.defineProperty(import_lib.default, "x", ...)`).

How the flag is computed: `e_dot` and `e_index` visit their target with
`ExprIn::is_property_access_target` (false for a delete target),
`visit_decls` does the same for the initializer of an object pattern,
and `handle_identifier` sets the flag when it turns an identifier into
an `EImportIdentifier` without it. Two places build an import identifier
outside the visitor and pass the bit themselves: the classic JSX factory
(`React.createElement`) and `emitDecoratorMetadata` (`design:type` gets
the import itself, so it sets the flag). Dead control flow does not set
the flag. The flag is per file: a use in a part that tree shaking later
removes still counts.

Routes to `module.exports` of a module that stays lifted that this PR
does not close. 1.4.0 kept the wrapper for every default import, so when
the bundle also has a static default import of the module, these gave
the real object in 1.4.0 and give the namespace object here:
- `this` in a method call. `lib.self()` with `exports.self = function ()
{ return this; }` still gets the namespace object (oven-sh#41162 chose that for
`this._helper()` style code).
- `(await import(m)).default`, same chunk or split.
- `ns.default` on `import * as ns` (new in 1.4.1, oven-sh#41820 reworks it).

`require()` of a lifted module outside the react family already keeps
the wrapper.

A react-family file of the shape `sideEffect(); module.exports =
require("./impl")` (react-dom/index.js) is lifted to `export * from
"./impl"`. When it keeps its wrapper it prints `module.exports =
exports_impl`, the namespace object of `./impl`, as 1.4.0 did. Every
read then goes through that object, so `defineProperty` and `delete`
work (react-dom 18.3.1 `--production`: `Object.defineProperty(ReactDOM,
"version", { value: "dp" })` prints `dp`, as 1.4.0 does, and main prints
the old version). A write after `Object.freeze` still reaches the
binding there. An earlier commit of this PR also wrapped `./impl`. That
gave `module.exports = __toESM(require_impl())`, whose getters are not
configurable, so `defineProperty` threw. It is reverted. The real fix
for that shape is to print the raw `require_impl()` (handed off, oven-sh#35722
had the approach).

oven-sh#41820 is open and edits the same step 1 lines, the same docs paragraph
and the same tests. This PR is the smaller one and restores 1.4.0
output, so it is simpler to land it first and rebase oven-sh#41820.

Test expectations that changed, all toward Node:
- `cjs/__toESM_mixed_import_styles` is back to its expectation from
before oven-sh#41162 (the namespace has a `default` key).
- `ReExportDefaultAsNameFromLiftedCommonJS` and
`ExportDefaultOfLiftedCommonJSDefaultImport` now keep the wrapper (a
re-export can reach an importer that holds the object) and also check
`Object.freeze` through the barrel. A third form, `export { React as R
}`, is added.
- `DefaultImportEscapingKeepsAssignmentOrder` became
`DefaultImportPassedAsValueKeepsAssignmentOrder` and expects the
wrapper.
- Ten tests compared the default import by identity (`m.default ===
React`) or passed it to `Object.keys`. They now compare a property
(`m.default.useState === React.useState`) so that they keep testing the
lifted path.

Suites run with the debug build: `bundler_cjs2esm`, `bundler_cjs`,
`esbuild/importstar`, `esbuild/importstar_ts`, `esbuild/default`,
`esbuild/dce`, `esbuild/splitting`, `esbuild/packagejson`, `esbuild/ts`,
`bundler_splitting`, `bundler_edgecase`, `bundler_jsx`, `bundler_npm`,
`bundler_barrel`, `bundler_minify`, `bundler_browser`, `bundler_bun`,
`bundler_plugin`, `bundler_loader`, `bundler_string`,
`bundler_promiseall_deadcode`, `transpiler/transpiler`,
`regression/issue/03844`, `bun-build-api` (two bytecode stress tests
time out at 5 s under the debug build, they bundle no imports).
</details>

<!-- robobun:evidence:begin -->

---

**[human-review]** gate passed · iteration 0 · 10 files touched

<details><summary>fails on main (without fix)</summary>

```console
ASAN without fix: 12 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 "test/bundler/bundler_cjs2esm.test.ts"
bun test v1.4.3 (b993710)

test/bundler/bundler_cjs2esm.test.ts:
(pass) bundler > cjs2esm/ModuleExportsFunction [1254.52ms]
(pass) bundler > cjs2esm/ImportNamedFromExportStarCJSModuleRef [548.64ms]
(pass) bundler > cjs2esm/ImportNamedFromExportStarCJS [554.77ms]
(pass) bundler > cjs2esm/BadNamedImportNamedReExportedFromCommonJS [472.17ms]
(pass) bundler > cjs2esm/ExportsFunction [398.00ms]
(pass) bundler > cjs2esm/ModuleExportsFunctionTreeShaking [505.22ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequire [490.55ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPoint [561.72ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPointImportedByEntryPoint [617.95ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPointImportedByEntryPointSplitting [686.37ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireTwoEntryPoints [551.89ms]
(pass) bundler > cjs2esm/ModuleExportsBasedOnNodeEnvProduction [704.70ms]
(pass) bundler > cjs2esm/ModuleExp
... (truncated)

release without fix: 12 FAILED
bun test v1.4.3-canary.1 (b993710)

test/bundler/bundler_cjs2esm.test.ts:
(pass) bundler > cjs2esm/ModuleExportsFunction [29.48ms]
(pass) bundler > cjs2esm/ImportNamedFromExportStarCJSModuleRef [14.24ms]
(pass) bundler > cjs2esm/ImportNamedFromExportStarCJS [13.88ms]
(pass) bundler > cjs2esm/BadNamedImportNamedReExportedFromCommonJS [13.68ms]
(pass) bundler > cjs2esm/ExportsFunction [14.09ms]
(pass) bundler > cjs2esm/ModuleExportsFunctionTreeShaking [13.74ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequire [13.08ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPoint [16.07ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPointImportedByEntryPoint [17.11ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPointImportedByEntryPointSplitting [15.81ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireTwoEntryPoints [15.04ms]
(pass) bundler > cjs2esm/ModuleExportsBasedOnNodeEnvProduction [17.65ms]
(pass) bundler > cjs2esm/ModuleExportsBasedOnNodeEnvDevelopment [15.90ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRuntimeCondition [16.27ms]
(pass) bundler > cjs2esm/UnwrappedModuleRequireAssigned [15.69ms]
(pass) bundler > cjs2esm/Unwrap
... (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 "test/bundler/bundler_cjs2esm.test.ts"
bun test v1.4.3 (b993710)

test/bundler/bundler_cjs2esm.test.ts:
(pass) bundler > cjs2esm/ModuleExportsFunction [1115.42ms]
(pass) bundler > cjs2esm/ImportNamedFromExportStarCJSModuleRef [521.71ms]
(pass) bundler > cjs2esm/ImportNamedFromExportStarCJS [536.01ms]
(pass) bundler > cjs2esm/BadNamedImportNamedReExportedFromCommonJS [594.49ms]
(pass) bundler > cjs2esm/ExportsFunction [501.50ms]
(pass) bundler > cjs2esm/ModuleExportsFunctionTreeShaking [632.29ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequire [418.24ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPoint [456.28ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPointImportedByEntryPoint [591.53ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPointImportedByEntryPointSplitting [603.73ms]
(pass) bundler > cjs2esm/ModuleExportsEqualsRequireTwoEntryPoints [546.68ms]
(pass) bundler > cjs2esm/ModuleExportsBasedOnNodeEnvProduction [667.62ms]
(pass) bundler > cjs2esm/ModuleExp
... (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     b9ce20b
  features     baseline

23 deps, 131 codegen, 1176 objects in 676ms

ninja: Entering directory `/workspace/bun/build/release'
[1/1248] install /workspace/bun
bun install v1.4.3-canary.1 (b993710)

Checked 22 installs across 61 packages (no changes) [14.00ms]
[2/1248] gen ErrorCode+*.h
[3/1248] gen bindgenv2
[4/1248] install /workspace/bun/packages/bun-error
bun install v1.4.3-canary.1 (b993710)

Checked 1 install across 2 packages (no changes) [2.00ms]
[5/1248] install /workspace/bun/src/node-fallbacks
bun install v1.4.3-canary.1 (b993710)

Checked 111 installs across 104 packages (no changes) [5.00ms]
[6/1248] fetch zlib
[zlib] up to date
[7/1248] gen node-fallbacks/react-refresh.js
Bundled 1 module in 10ms

  react-refresh.js  4.81 KB  (entry point)

[8/1248] fetch libjpeg-turbo
[libjpeg-turbo] up to date
[9/1248] fetch tinycc
[tinycc] up to date
[10/1247] gen .bind.ts → GeneratedBindings.cpp
[11/1247] gen bake.{client,server,error}.js
-> bake.client.js, bake.serve
... (truncated)
```

</details>

<details><summary>diff hotspot</summary>

```
docs/bundler/index.mdx                             |   2 +-
 src/ast/symbol.rs                                  |   4 +
 .../linker_context/scanImportsAndExports.rs        |  28 +-
 src/js_parser/fold.rs                              |   3 +
 src/js_parser/p.rs                                 |  19 +-
 src/js_parser/parser.rs                            |  14 +
 src/js_parser/visit/mod.rs                         |   2 +
 src/js_parser/visit/visit_expr.rs                  |  18 +-
 test/bundler/bundler_cjs.test.ts                   |   8 +-
 test/bundler/bundler_cjs2esm.test.ts               | 318 +++++++++++++++------
 10 files changed, 318 insertions(+), 98 deletions(-)
```

</details>

**gate history** · 1 passed · 0 rejected · iteration 0

<details><summary>evidence per changed file</summary>

```
file                                                 reads  edits  tests
docs/bundler/index.mdx                                   2      2     25
src/ast/symbol.rs                                        1      2     25
src/bundler/linker_context/scanImportsAndExports.rs      6     10     25
src/js_parser/fold.rs                                    1      1     25
src/js_parser/p.rs                                       7      6     25
src/js_parser/parser.rs                                  2      4     25
src/js_parser/visit/mod.rs                               1      1     25
src/js_parser/visit/visit_expr.rs                        5      3     25
test/bundler/bundler_cjs.test.ts                         1      2      9
test/bundler/bundler_cjs2esm.test.ts                    11     18     20
```

</details>

<!-- robobun:evidence:end -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants