Skip to content

bundler: hoist object/array --define values to a shared var - #35958

Open
robobun wants to merge 11 commits into
mainfrom
farm/6f67858c/hoist-object-define
Open

bundler: hoist object/array --define values to a shared var#35958
robobun wants to merge 11 commits into
mainfrom
farm/6f67858c/hoist-object-define

Conversation

@robobun

@robobun robobun commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

What

When --define is given an object or array literal, emit it once as var define_KEY_default = {...} at module scope and substitute every reference with that identifier, instead of inlining a fresh {...} at each use site.

Repro

// entry.mjs, built with: --define 'CFG={"k":1,"nested":{"x":2}}'
const a = CFG, b = CFG;
console.log(a === b, a.nested === b.nested);   // real global: true true
a.k = 99; console.log(b.k);                    // real global: 99

Before:

var a = { k: 1, nested: { x: 2 } };
var b = { k: 1, nested: { x: 2 } };            // fresh copy per site
// prints: false false / 1

After (matches esbuild):

var define_CFG_default = { k: 1, nested: { x: 2 } };
var a = define_CFG_default;
var b = define_CFG_default;
// prints: true true / 99

Cause

value_for_define in p.rs returned Expr { data: define_data.value, loc } for anything that wasn't an identifier or string. For EObject/EArray that meant the same AST node was printed inline at every substitution site, so the output contained N independent literals. esbuild instead routes compound define values through its --inject path as a synthetic <define:KEY> module and substitutes a reference to its default export.

Fix

  • Define::insert assigns an injected_define_index to any DefineData whose value is EObject/EArray and records it on Define.injected.
  • prepare_for_visit_pass pre-declares one module-scope symbol per injected define (define_<key>_default, hash-suffixed when the renamer isn't in use).
  • value_for_define returns EIdentifier(ref) for injected defines and records the usage.
  • After the visit, parse_entry.rs emits var define_X_default = {...} as its own before part (marked can_be_removed_if_unused) for each injected define that was actually referenced.

This also fixes a latent bug where assigning to an object-valued define printed {"k":1} = x;, which is a runtime SyntaxError.

Cross-module identity (a single object shared across every bundled file, esbuild's <define:X> virtual module) is not included; it needs the --inject infrastructure which bun has not ported. Each module now materializes the literal once instead of once per reference.

Verification

USE_SYSTEM_BUN=1 bun test bundler/esbuild/extra.test.ts -t DefineObjectIdentity   # 3 fail
bun bd test bundler/esbuild/extra.test.ts -t DefineObjectIdentity                 # 3 pass

Also ran the full extra.test.ts (223 pass), transpiler.test.js (182 pass), bundler_env.test.ts, bundler_jsx.test.ts, and the bun-build-api.test.ts define cases.

Fixes #21319
Fixes #21312


[review] gate passed · iteration 1 · 7 files touched

fails on main (without fix)
ASAN without fix: 3 failed, 12 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bundler/esbuild/extra.test.ts
bun test v1.4.0 (9c4f9f976)

test/bundler/esbuild/extra.test.ts:
(pass) bundler > extra/FileAsDirectoryBreak [499.88ms]
(todo) bundler > extra/PathWithQuestionMark
(pass) bundler > extra/JSXEscaping1 [131.11ms]
(pass) bundler > extra/JSXEscaping2 [152.24ms]
(pass) bundler > extra/ArbitraryModuleNamespaceIdentifiers1 [431.19ms]
(pass) bundler > extra/ArbitraryModuleNamespaceIdentifiers2 [396.61ms]
(pass) bundler > extra/ArbitraryModuleNamespaceIdentifiers3 [417.30ms]
(pass) bundler > extra/ArbitraryModuleNamespaceIdentifiers4 [423.55ms]
(pass) bundler > extra/ArbitraryModuleNamespaceIdentifiers5 [374.85ms]
(pass) bundler > extra/ArbitraryModuleNamespaceIdentifiers6 [466.56ms]
(pass) bundler > extra/RemoveASMDirective [448.24ms]
(pass) bundler > extra/ImportOrder1 [386.23ms]
(pass) bundler > extra/ImportOrder2 [441.48ms]
(pass) bundler > extra/CyclicImport1 [383.81ms]
(pass) bundler > extra/TypeofRequireESM [1433.45ms]
(pass) bundler > extra/CJSExport1 [919.59ms]
(pass) bundler > extra/CJSExport2 [428.0
... (truncated)

release without fix: 12 skipped
bun test v1.4.0-canary.1 (9c4f9f976)

test/bundler/esbuild/extra.test.ts:
(pass) bundler > extra/FileAsDirectoryBreak [17.96ms]
(todo) bundler > extra/PathWithQuestionMark
(pass) bundler > extra/JSXEscaping1 [3.67ms]
(pass) bundler > extra/JSXEscaping2 [3.85ms]
(pass) bundler > extra/ArbitraryModuleNamespaceIdentifiers1 [19.66ms]
(pass) bundler > extra/ArbitraryModuleNamespaceIdentifiers2 [18.14ms]
(pass) bundler > extra/ArbitraryModuleNamespaceIdentifiers3 [17.73ms]
(pass) bundler > extra/ArbitraryModuleNamespaceIdentifiers4 [23.71ms]
(pass) bundler > extra/ArbitraryModuleNamespaceIdentifiers5 [18.77ms]
(pass) bundler > extra/ArbitraryModuleNamespaceIdentifiers6 [24.21ms]
(pass) bundler > extra/RemoveASMDirective [19.47ms]
(pass) bundler > extra/ImportOrder1 [18.95ms]
(pass) bundler > extra/ImportOrder2 [28.79ms]
(pass) bundler > extra/CyclicImport1 [18.59ms]
(pass) bundler > extra/TypeofRequireESM [42.13ms]
(pass) bundler > extra/CJSExport1 [22.73ms]
(pass) bundler > extra/CJSExport2 [18.42ms]
(pass) bundler > extra/CJSExport3 [20.11ms]
(pass) bundler > extra/CJSExport4 [23.32ms]
(pass) bundler > extra/CJSExport5 [24.08ms]
(pass) bundler > extra/CJSExport6 [21.66m
... (truncated)
passes on PR (with fix)
ASAN with fix: 12 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bundler/esbuild/extra.test.ts
bun test v1.4.0 (9c4f9f976)

test/bundler/esbuild/extra.test.ts:
(pass) bundler > extra/FileAsDirectoryBreak [471.72ms]
(todo) bundler > extra/PathWithQuestionMark
(pass) bundler > extra/JSXEscaping1 [103.74ms]
(pass) bundler > extra/JSXEscaping2 [80.14ms]
(pass) bundler > extra/ArbitraryModuleNamespaceIdentifiers1 [373.93ms]
(pass) bundler > extra/ArbitraryModuleNamespaceIdentifiers2 [411.65ms]
(pass) bundler > extra/ArbitraryModuleNamespaceIdentifiers3 [404.21ms]
(pass) bundler > extra/ArbitraryModuleNamespaceIdentifiers4 [399.03ms]
(pass) bundler > extra/ArbitraryModuleNamespaceIdentifiers5 [362.78ms]
(pass) bundler > extra/ArbitraryModuleNamespaceIdentifiers6 [394.97ms]
(pass) bundler > extra/RemoveASMDirective [408.94ms]
(pass) bundler > extra/ImportOrder1 [384.28ms]
(pass) bundler > extra/ImportOrder2 [408.29ms]
(pass) bundler > extra/CyclicImport1 [403.53ms]
(pass) bundler > extra/TypeofRequireESM [1339.66ms]
(pass) bundler > extra/CJSExport1 [383.73ms]
(pass) bundler > extra/CJSExport2 [701.54
... (truncated)

release with fix: 12 skipped
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 739ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[0/4] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

�[1m�[92m   Compiling�[0m bun_js_parser v0.0.0 (/workspace/bun/src/js_parser)
�[1m�[92m   Compiling�[0m bun_resolver v0.0.0 (/workspace/bun/src/resolver)
�[1m�[92m   Compiling�[0m bun_ini v0.0.0 (/workspace/bun/src/ini)
�[1m�[92m   Compiling�[0m bun_router v0.0.0 (/workspace/bun/src/router)
�[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_ast_jsc v0.0.0 (/workspace/bun/src/ast_jsc)
�[1m�[92m   Compili
... (truncated)
diff hotspot
src/bundler/defines.rs             |  4 ++
 src/bundler/options.rs             |  2 +
 src/js_parser/lib.rs               | 35 ++++++++++++++-
 src/js_parser/p.rs                 | 45 +++++++++++++++++++
 src/js_parser/parse/parse_entry.rs | 40 +++++++++++++++++
 src/js_parser/visit/visit_expr.rs  | 17 +++++++
 test/bundler/esbuild/extra.test.ts | 92 ++++++++++++++++++++++++++++++++++++++
 7 files changed, 234 insertions(+), 1 deletion(-)

gate history · 2 passed · 0 rejected · iteration 1

evidence per changed file
file                                reads  edits  tests
src/bundler/defines.rs                  2      4      0
src/bundler/options.rs                  1      2      0
src/js_parser/lib.rs                    5     10      0
src/js_parser/p.rs                     11     10      0
src/js_parser/parse/parse_entry.rs      4      4      0
src/js_parser/visit/visit_expr.rs       7      5      0
test/bundler/esbuild/extra.test.ts      3      5      0

When --define is given an object or array literal, emit it once as
`var define_KEY_default = {...}` at module scope and substitute every
reference with that identifier, instead of inlining a fresh literal at
each use site.

This matches esbuild's output and makes two references to the same
define compare equal (a === b), lets mutations be visible across
references, and stops a define referenced in a hot loop from allocating
per iteration. It also fixes the case where assigning to an
object-valued define previously printed an invalid `{...} = x`
expression.
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Hoisted define identity

Layer / File(s) Summary
Define storage and propagation
src/js_parser/lib.rs, src/bundler/defines.rs, src/bundler/options.rs
Define data tracks injected values, object and array literals are stored once, and bundle options initialize and copy them.
Injected symbol resolution
src/js_parser/p.rs
The parser predeclares generated symbols and resolves injected define references to identifiers.
Removable define emission and validation
src/js_parser/parse/parse_entry.rs, test/bundler/esbuild/extra.test.ts
Used injected defines are emitted as removable variable parts, with tests covering bundled and non-bundled identity.
Delete substitution handling
src/js_parser/visit/visit_expr.rs, test/bundler/esbuild/extra.test.ts
Renamed identifier operands in delete expressions are wrapped to preserve strict-mode behavior, with corresponding output tests.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The implementation matches #21319 and #21312 by hoisting JSON define values, sharing references, and avoiding duplicated full-object output.
Out of Scope Changes check ✅ Passed The added delete-handling and tests are consistent with the stated fix and do not appear unrelated to the linked objectives.
Title check ✅ Passed The title clearly summarizes the main change: hoisting object/array --define values into a shared variable.
Description check ✅ Passed The description covers what changed and how it was verified, even though it uses different headings than the template.

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

@robobun

robobun commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator Author

Status: tests pass locally (fail-before/pass-after). Diff is green.

USE_SYSTEM_BUN=1 bun test bundler/esbuild/extra.test.ts -t DefineObjectIdentity   # 3 fail
bun bd test bundler/esbuild/extra.test.ts -t DefineObjectIdentity                 # 3 pass

Linked #21319 and #21312 (output now matches esbuild). Not linking #21210: that issue is about primitive-valued defines on the LHS of an assignment (2 = x), which this PR doesn't touch; only the object/array case becomes a valid assignable identifier as a side effect.

Addressed review in bea1e6e + bc1585c: the Op::UnDelete visitor now wraps the operand as (0, x) whenever define substitution yields an identifier the source didn't name (dot-define and bare-identifier cases alike), comparing the symbol name before vs after the visit so source-written delete x / delete unbound stay bare. This also fixes the pre-existing case where --define process.env.X=someGlobal + delete process.env.X emitted delete someGlobal (a strict-mode SyntaxError). Covered by extra/DefineObjectDeleteTarget and extra/DefineObjectDeleteBareIdentifier.

CI build 82612: none of the failures touch this diff.

Ready for review.

@robobun

robobun commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 2:19 PM PT - Jul 26th, 2026

@robobun, your commit 9c4f9f9 has 2 failures in Build #82612 (All Failures):

  • test/cli/run/no-orphans.test.ts - code 1 on 🍎 14 x64
  • 📦 Binary size — 12 over 0.50 MB
  • targetthis build canary: main #79916
    sizeΔ
    bun-darwin-aarch6458.13 MB57.58 MB+564.9 KB
    bun-darwin-x6463.48 MB62.95 MB+544.5 KB
    bun-linux-aarch6470.98 MB70.42 MB+576.0 KB
    bun-linux-x6472.48 MB71.95 MB+544.0 KB
    bun-linux-aarch64-musl64.88 MB64.32 MB+576.0 KB
    bun-linux-x64-musl66.98 MB66.45 MB+544.0 KB
    bun-linux-aarch64-android78.47 MB77.97 MB+512.0 KB
    bun-linux-x64-android80.62 MB80.10 MB+529.2 KB
    bun-freebsd-x6483.09 MB82.56 MB+544.0 KB
    bun-freebsd-aarch6484.84 MB84.31 MB+544.0 KB
    bun-windows-x6480.26 MB79.70 MB+577.0 KB
    bun-windows-aarch6470.86 MB70.34 MB+539.0 KB

    Add [skip size check] to the commit message if this increase is intentional.


🧪   To try this PR locally:

bunx bun-pr 35958

That installs a local version of the PR into your bun-35958 executable, so you can run:

bun-35958 --bun

Comment thread src/js_parser/lib.rs Outdated
Comment thread src/js_parser/lib.rs Outdated
Comment thread src/js_parser/lib.rs Outdated
Comment thread src/js_parser/p.rs Outdated
Comment thread src/js_parser/p.rs Outdated
Comment thread src/js_parser/p.rs Outdated
Comment thread src/js_parser/p.rs Outdated
Comment thread src/js_parser/parse/parse_entry.rs Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Found 3 issues this PR may fix:

  1. bun --define with JSON data performs inefficient substitution ( duplicates values on every usage ) #21319 - Directly reports the inefficient duplication of JSON/object define values at every usage site instead of hoisting to a shared var
  2. bun.build --define with JSON values prints full objects in output #21312 - Reports that --define with JSON values inlines full object literals in output instead of resolving them properly
  3. bun.build --define should not substitution a variable that is assigned to with a substitution that is not assignable #21210 - Reports that assigning to an object-valued define produces invalid JS like {"k":1} = x;; hoisting to a var makes the LHS an assignable identifier

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #21319
Fixes #21312
Fixes #21210

🤖 Generated with Claude Code

@coderabbitai coderabbitai 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.

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/lib.rs`:
- Around line 514-517: Add a dedicated SAFETY comment immediately before the
unsafe impl Sync for InjectedDefine, documenting why synchronized access is
sound for InjectedDefine::value; keep the existing Send comment attached only to
the unsafe impl Send.
🪄 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: af89bf53-ff7a-4aa3-80ae-13263def4110

📥 Commits

Reviewing files that changed from the base of the PR and between 44f6469 and b810537.

📒 Files selected for processing (6)
  • src/bundler/defines.rs
  • src/bundler/options.rs
  • src/js_parser/lib.rs
  • src/js_parser/p.rs
  • src/js_parser/parse/parse_entry.rs
  • test/bundler/esbuild/extra.test.ts

Comment thread src/js_parser/lib.rs Outdated

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

Additional findings (outside current diff — PR may have been updated during review):

  • 🔴 src/js_parser/p.rs:6350-6359 — The new injected-define path returns EIdentifier(ref_) without accounting for delete targets. With --define 'process.env.CFG={"x":1}', source delete process.env.CFG now emits delete define_process_env_CFG_default, which is an early SyntaxError in strict mode (all ESM output) — before this PR the same input emitted the valid delete { x: 1 }. Either skip the injected path for delete targets and inline the literal, or add a post-visit (0, x) wrap in the Op::UnDelete arm mirroring UnTypeof.

    Extended reasoning...

    What the bug is

    When a --define key is given an object/array value and the source contains delete <that key>, the bundler now emits delete <bare-identifier>. In strict mode — which every ES module is, and Format::Esm is the default output format — delete <IdentifierReference> is an early SyntaxError per ES §13.5.1.1. The bundled module fails to parse at load time.

    Code path

    Walk delete process.env.CFG with --define 'process.env.CFG={"x":1}':

    1. Parseparse_prefix.rs:408-414: the delete operand is EDot, so UnaryFlags::WAS_ORIGINALLY_DELETE_OF_IDENTIFIER_OR_PROPERTY_ACCESS is set on the unary node.
    2. Visit the unaryvisit_expr.rs:1221-1223: Op::UnDelete visits its operand with ExprIn::default(), so in_.assign_target == None. Unlike the UnTypeof arm just above (lines 1179-1196), there is no post-visit fixup that wraps a newly-appeared identifier in (0, x).
    3. Visit the e_dot operandvisit_expr.rs:1347: the dot-define substitution gates only on in_.assign_target == None (which it is), so it calls value_for_define.
    4. This PR's new codep.rs:6350-6359: injected_define_index is Some, so it returns EIdentifier(ref_) unconditionally. The is_delete_target parameter is ignored on this path.
    5. Printjs_printer/lib.rs:4102: the printer's protective (0, x) wrap fires only when !e.flags.contains(WAS_ORIGINALLY_DELETE_OF_IDENTIFIER_OR_PROPERTY_ACCESS). That flag is set (step 1), so the guard is false and the identifier prints bare.

    Output: delete define_process_env_CFG_default → strict-mode SyntaxError.

    Why existing code doesn't prevent it

    The printer guard at js_printer/lib.rs:4102 was designed for the reverse case: preventing delete (0, x) from being simplified to delete x. It trusts the WAS_ORIGINALLY_... flag to mean "the source already had a valid delete target here, leave it alone". That assumption breaks when the visitor swaps an EDot for an EIdentifier while the parse-time flag remains set. The UnTypeof arm has an explicit post-visit id_before/id_after check for exactly this class of substitution; UnDelete does not.

    Regression

    Before this PR, value_for_define fell through to the tail Expr { data: value, loc } and returned the EObject inline, producing delete { x: 1 }. delete on a non-Reference is valid strict-mode JS (evaluates to true). So this is a valid → syntactically-invalid output regression: any bundle that contains delete <object-valued-dot-define> now fails to load. (The identifier-key case — --define 'CFG={..}' + delete CFG — was already broken pre-PR because the source operand was an identifier and the flag was set anyway; but object/array dot defines were previously safe and are now broken.)

    Fix

    Two options:

    • In value_for_define (p.rs:6350), skip the injected path when the expression is a delete target and fall through to inline the literal — restoring the pre-PR delete { x: 1 } behaviour. Caveat: the is_delete_target argument is computed from p.delete_target, which is not currently set in the Op::UnDelete visitor arm (grep shows it's only assigned at visit_expr.rs ~911/1026 during e_index→e_dot conversion), so this alone won't fire on the direct delete a.b.c path — you'd also need to set p.delete_target before visiting the operand in the UnDelete arm.
    • Or, more robustly, mirror the UnTypeof post-visit fixup (visit_expr.rs:1179-1196) in the Op::UnDelete arm: if the operand was not an identifier before visiting but is one after, wrap it in (0, x). That produces delete (0, define_process_env_CFG_default), which is valid strict-mode JS and preserves the identity/hoisting benefit of this PR.

    Repro

    echo 'export {}; delete process.env.CFG;' > a.mjs
    bun build --define 'process.env.CFG={"x":1}' a.mjs
    # output contains: delete define_process_env_CFG_default
    # → SyntaxError: Delete of an unqualified identifier in strict mode

Comment thread src/js_parser/p.rs Outdated
Also propagate AllocError from ensure_valid_identifier instead of
catching it.
Comment thread src/js_parser/visit/visit_expr.rs Outdated
Comment thread src/js_parser/visit/visit_expr.rs
Compare the operand's symbol name before vs after the visit instead of
only checking whether the tag became EIdentifier. This covers
`delete CFG` (sloppy source) where the pre-visit operand is already an
identifier that gets replaced by the hoisted define's identifier, while
leaving source-written `delete x` / `delete unbound` untouched.
Comment thread src/js_parser/visit/visit_expr.rs Outdated

@coderabbitai coderabbitai 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.

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/bundler/esbuild/extra.test.ts`:
- Around line 742-761: Update the DefineObjectDeleteBareIdentifier test
configuration to execute the bundled output and assert the expected stdout: true
for delete CFG, true for delete ARR, and false for delete keep. Preserve the
existing onAfterBundle regex checks, using the sibling DefineObjectDeleteTarget
test’s run configuration as the model.
🪄 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: 80107e7c-8e5e-4931-a252-a629b21181fc

📥 Commits

Reviewing files that changed from the base of the PR and between b810537 and 72bf8a9.

📒 Files selected for processing (5)
  • src/js_parser/lib.rs
  • src/js_parser/p.rs
  • src/js_parser/parse/parse_entry.rs
  • src/js_parser/visit/visit_expr.rs
  • test/bundler/esbuild/extra.test.ts

Comment thread test/bundler/esbuild/extra.test.ts

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

Both earlier findings (OOM catch-default in prepare_for_visit_pass, and the bare-identifier delete CFG sibling case) are addressed and covered by tests. No further issues found, but this touches the core define-substitution path across the parser/visitor/part-emission and adds new unsafe impl Send/Sync, so it's worth a human look — particularly the per-module (not cross-module) hoisting tradeoff called out in the description.

What was reviewed:

  • Define::insert / InjectedDefine — index assignment happens for both identifier and dot defines; for_worker clones injected.
  • value_for_define — falls through to the old inline path when injected_define_refs is empty (e.g. no visit pass), so no regression there.
  • Op::UnDelete name-compare — verified source-written delete x stays unwrapped (name matches after find_symbol); the wrap now also fires for pre-existing identifier→identifier defines, which is the intended strict-mode fix.
  • parse_entry.rs — parts are gated on use_count_estimate > 0 and marked can_be_removed_if_unused, so unused defines don't bloat output.
Extended reasoning...

Overview

This PR changes how --define handles object/array values in the bundler: instead of inlining a fresh literal at every substitution site, it declares one var define_KEY_default = {...} per module and substitutes an identifier reference. Changes span src/js_parser/lib.rs (new InjectedDefine type, Define.injected field, DefineData.injected_define_index), src/js_parser/p.rs (symbol pre-declaration in prepare_for_visit_pass, new branch in value_for_define), src/js_parser/parse/parse_entry.rs (emit the var part per referenced define), src/js_parser/visit/visit_expr.rs (wrap delete operands that become identifiers via substitution), and src/bundler/{defines,options}.rs (field plumbing). Five new itBundled tests exercise identity, dot-defines, no-bundle mode, and both delete variants with runtime execution.

Security risks

None identified. Define values are parsed once at startup by the existing parse_env_json path; this PR only changes where the resulting AST node is emitted. The new unsafe impl Send/Sync for InjectedDefine mirrors the existing DefineData impls with the same rationale (immutable process-lifetime AST-store pointers, read-only after init) and now carries per-impl SAFETY comments.

Level of scrutiny

Medium-high. This is the bundler's define-substitution path — it runs on every parsed file when --define is set, and the Op::UnDelete change affects every delete <expr> regardless of defines. The change is well-scoped and well-tested, but it's not mechanical: it introduces new module-scope symbol declaration, a new Part-emission site (which interacts with tree-shaking via can_be_removed_if_unused / declared_symbols), and a name-comparison heuristic in the delete visitor. The PR also explicitly ships a partial esbuild-parity design (per-module hoisting rather than a cross-module <define:X> virtual module) — a maintainer should confirm that intermediate step is acceptable.

Other factors

Two prior review rounds from this bot were addressed in bea1e6e/bc1585c/9c4f9f9 (OOM propagation, the bare-identifier delete sibling, and running the delete test under sloppy-mode iife/node). The clippy SAFETY-comment failure was fixed. Test coverage is solid: all five new tests run: the bundle and assert stdout, and the identity test additionally counts literal occurrences in the output. The Op::UnDelete wrap now also applies to identifier→identifier defines (--define X=someGlobal + delete X), which is a behavior change beyond the linked issues but is correctly framed as a strict-mode-SyntaxError fix. CI's only hard failure is the binary-size gate against a stale baseline (per the author's note), unrelated to this diff.

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

Labels

Projects

None yet

2 participants