Repository navigation
bundler: keep a property read's this and getter for an unbound item of a require() / import() local - #42402
bundler: keep a property read's this and getter for an unbound item of a require() / import() local#42402robobun wants to merge 4 commits into
this and getter for an unbound item of a require() / import() local#42402Conversation
… of a require() / import() local `ns.f` off a local that holds a require() / import() result is an import item since #41186. The linker binds it only to an export of a bundled ES module. For a CommonJS or external importee, or an escaped namespace, it prints `ns.f`, a property read on an object. The parser treated the item as a bound ES import: - It dropped the wrapper of `(0, ns.f)()`, `(true ? ns.f : 0)()`, `(1 && ns.f)()`, `(0 || ns.f)()`, `(null ?? ns.f)()`, so the call got `ns` as `this`. With --minify-syntax it also moved `const f = ns.f` into `f()`. - It removed an unused `ns.a` read, `void ns.a`, and `if (ns.a) {}`, and moved one read past another, so a getter did not run or ran in a different order. Now the parser keeps both behaviours of a property read for such an item.
|
Updated 8:46 AM PT - Oct 16th, 2026
✅ @robobun, your commit 7d3da10c3d2a730dee7927c753953639abe5e8e9 passed in 🧪 To try this PR locally: bunx bun-pr 42402That installs a local version of the PR into your bun-42402 --bun |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
WalkthroughThe parser detects namespace-local property reads from ChangesNamespace read preservation
Suggested reviewers: Priority: ➖ Normal Merge Risk: ⚪ Minimal · up to
|
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. Because it changes parser-side minification/DCE semantics that every bundled output flows through, and the description notes an overlapping printer-side approach in #40829, a human familiar with the esbuild port should confirm this is the right layer for the fix.
What was reviewed:
- Grepped
has_value_for_this_in_callacrosssrc/— all 7js_parsercall sites now go through the parser-aware wrapper; none remain onExpr::directly. - The
ECallsubstitution guard refactor is behavior-preserving:Expr::has_value_for_this_in_callis exactlyEDot | EIndex, so the new predicate only widens the guard. self.symbols[ref.inner_index() as usize]matches the indexing pattern used throughoutsrc/js_parser/(visit/mod.rs, scan_symbols.rs, p.rs).- Tests exercise comma /
?:/&&/||/??/ local-substitution /?.()forrequire,await import,.then, escaped ESM, and external, with and withoutminifySyntax, plus getter retention and read ordering.
Extended reasoning...
Overview
The PR fixes a bundler regression where EImportIdentifier nodes synthesized from ns.a (where ns is a require()/import() result local) were treated as side-effect-free bound imports during minification. Two new helpers on P — is_namespace_local_read and a parser-aware has_value_for_this_in_call — are threaded through the seven wrapper-fold sites in visit_binary.rs / visit_expr.rs and the single-use-substitution call-target guard in p.rs. expr_can_be_removed_if_unused_without_dce_check now returns false for these reads. Four itBundled cases (two bugs × minifySyntax on/off) are added to the existing test/bundler/bundler_dynamic_import_dce.test.ts.
Security risks
None. This is bundler output-shape logic with no auth, crypto, filesystem, or network surface. The change is strictly more conservative — it only ever keeps a wrapper or a read that was previously dropped, never the reverse — so it cannot introduce new code into user bundles that wasn't there before.
Level of scrutiny
Moderate-to-high. The code change itself is small, mechanical, and verifiably complete (grep confirms no Expr::has_value_for_this_in_call() callers remain in src/js_parser; the EDot | EIndex → predicate refactor is exact because that's the full body of the Expr method; the symbol-table indexing follows the file's ubiquitous pattern). The tests assert runtime-observable this values and getter execution order rather than output snapshots, so they prove the behavior end-to-end. However, this is parser-side minification semantics that interacts with a later linker binding decision, REVIEW.md calls out esbuild as the spec for ported code, and the PR itself flags #40829 as a competing printer-side approach for the this half — a maintainer who owns the port should weigh which layer the fix belongs at.
Other factors
No CODEOWNERS entry covers src/js_parser/ or test/bundler/. The bug hunt exited on dry_streak with no findings and no ruled-out candidates. The PR description's claim that the extensive bundler suites (minify, dce, importstar, cjs, cjs2esm, bytecode snapshots) still pass is consistent with the change being purely widening of an existing conservative check, but I did not independently run them.
|
Status: the branch is merged with main again (7d3da10), so the conflict is gone. The one conflict was in the test file. The tests of this PR are now at the end of that file. The PR body has the measured costs. How to reproduce (1.4.1, 1.4.2, 1.4.3 and main. 1.4.0 is correct): // util.cjs
"use strict";
exports.tag = "EXPORTS";
exports.who = function () { return this === undefined ? "unbound" : this.tag; };
// entry.js
const util_1 = require("./util.cjs");
console.log(util_1.who(), (0, util_1.who)());
console.log((true ? util_1.who : 0)(), (1 && util_1.who)());bun build ./entry.js --target=bun --minify-syntax --outdir out && bun out/entry.js
The getter case: a module that defines Both cases are in |
ImportScanner::scan also sets was_originally_property_access, for the items of `import * as ns`. It runs in to_ast, after every caller of this check. Comment only.
…pr::has_value_for_this_in_call - is_namespace_local_read looks the namespace_ref of the item's alias up in dynamic_import_namespace_locals. It no longer reads was_originally_property_access, which ImportScanner also sets later for the items of `import * as ns`. - Expr::has_value_for_this_in_call is gone. It cannot see the symbol table, so a fold site that calls it misses these items. Each fold site now has to call P::has_value_for_this_in_call. - expr_can_be_removed_if_unused gets a guard arm for such an item. The esbuild comment on the import-item arm is as before. - Tests: `(false ? 0 : ns.f)()`, `[ns.f][0]()` and `ns["f"]` for the call, `await import()`, `.then` and a plain local that must not move past a read for the getter.
…#42549) ### Problem - With `bun build --minify-syntax`, ``ns["tag"]`z` `` prints as ``$tag`z` `` when `tag` is a lifted CommonJS export. The tag then runs with `this === undefined`: `TypeError: undefined is not an object (evaluating 'this._helper')`. ``st["tag"]`z` `` on a default import and ``exports["tag"]`z` `` inside the CommonJS file break too. - The cause is the `a["b"]` to `a.b` rewrite in `e_index` (`src/js_parser/visit/visit_expr.rs:858`). It moves `p.call_target` and `p.delete_target` to the new `E::Dot`, then visits it. It does not move `p.template_tag` (added in #41251). So `e_dot` sees no tag, and the linker binds the export directly. ### Fix - The rewrite also points `p.template_tag` at the new `E::Dot`. esbuild does the same (`p.templateTag = dot`). - The second rewrite in `e_index` (`a["x" + "y"]`) is unchanged. It returns the new `E::Dot` without a visit, so nothing reads the marker. That form already works. - Verified: `test/bundler/bundler_cjs2esm.test.ts` (3 new tests, all fail on main). Also 11 other bundler suites (see the notes). - Self-reviewed: 2 concerns raised, 2 addressed. ### Background - Lifting turns `exports.foo = x` in a CommonJS file into `var $foo = x` plus an ES export. The namespace object (`exports_lib`) stands in for `module.exports`. - ``X.name`z` `` passes `X` as `this`, like a method call. ``$name`z` `` passes `undefined`. #41251 makes the linker keep `X.name` as written when the file calls it or tags a template with it. - `e_template` stores the tag node in `p.template_tag`. `e_dot` and `e_index` compare their own node with it by address. A rewrite that replaces the node must move the marker. <details><summary>Notes</summary> Repro (fails on 1.4.3-canary.1+6a92015fc and on main b993710). `git tag --contains f31440d` lists bun-v1.4.1 and bun-v1.4.2, so both releases have the same code: ```sh cat > lib.cjs <<'EOF' exports._helper = function (s) { return "helped:" + s[0]; }; exports.tag = function (s) { return this._helper(s); }; EOF cat > entry.mjs <<'EOF' import * as ns from "./lib.cjs"; console.log(ns["tag"]`z`); EOF bun build entry.mjs --minify-syntax --outfile out/min.js --target bun && bun out/min.js # TypeError: undefined is not an object (evaluating 'this._helper') at $tag ``` `out/min.js` on main holds ``console.log($tag`z`)``. With this change it holds ``console.log(exports_lib.tag`z`)`` and prints `helped:z`. The marker reaches three consumers, all through `IdentifierOpts::is_template_tag` set in `e_dot`: - `maybe_rewrite_property_access` (`src/js_parser/fold.rs:271`) for `import * as ns`. - `record_import_property_use` (`src/js_parser/p.rs:6712`) for a default import. - `note_commonjs_export_use` (`src/js_parser/p.rs:5955`) for `exports.name` in the lifted file itself. Each new test covers one consumer. Each test also has the computed method call (`X["parse"]("y")`), which already worked, so both markers are covered under `minifySyntax`. The name `tag` appears only in computed form in each entry, because one marked use of a name keeps every `X.name` of that file as written and would hide the fault. The second rewrite (`ns["ta" + "g"]`, or an inlined TypeScript enum key) does not go through `maybe_rewrite_property_access` at all. It prints ``exports_lib.tag`z` `` on main and with this change. In esbuild that rewrite happens after `maybeRewritePropertyAccess` and carries no marker. Checked by hand with `--minify-syntax` and `--minify`, all print `helped:z`: `const ns = await import("./lib.cjs")`, `require("./lib.cjs")["tag"]`, `const ns = require("./lib.cjs")`, `import * as ns` through an `export *` barrel, and `import { default as st }`. Sites this change leaves alone: - Other folds check only for a call target, not for a template tag: the comma fold in `e_binary`, the ternary fold in `e_if`, and the `[x][0]` fold in `e_index`. They turn ``(0, o.f)`x` `` into ``o.f`x` ``. That is the opposite fault (the tag gains a receiver). It also reaches plain `bun run`: ``(0, o.f)`x` `` sees `this === o` there, and `undefined` in node. #42452 (section 4) tracks them. - #40829 (open, conflicts with main) is the candidate fix for those folds. Its diff has the same three lines as this change, as one hunk of 26 files. It predates #41251. If this change lands first, #40829 drops that hunk on its rebase. - #42402 (open) changes `has_value_for_this_in_call` at the fold sites. It does not touch this rewrite or `p.template_tag`. The star import test matches the receiver with `\w+` and does not pin `exports_lib`, because #41820 (open) gives `import *` of a lifted module a namespace object of its own, with another name. The default import test and the self test pin the exact output. Suites run on the debug build: `bundler_cjs2esm`, `bundler_minify`, `bundler_cjs`, `bundler_edgecase`, `bundler_dynamic_import_dce`, `esbuild/default`, `esbuild/dce`, `esbuild/importstar`, `esbuild/importstar_ts`, `esbuild/ts`, `transpiler/transpiler`, `transpiler/macro-test`. </details> <!-- robobun:evidence:begin --> --- **[human-review]** gate passed · iteration 0 · 2 files touched <details><summary>fails on main (without fix)</summary> ```console ASAN without fix: BUILD FAILED (no junit output) $ 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" ninja: Entering directory `/workspace/bun/build/debug' [1/66] cc obj/src/jsc/bindings/sqlite/sqlite3.c.o [2/66] gen ZigGeneratedClasses.{cpp,h,rs} Found 2 classes from /workspace/bun/src/jsc/resolve_message.classes.ts - ResolveMessage (15 fields) - BuildMessage (10 fields) Found 1 classes from /workspace/bun/src/runtime/api/Archive.classes.ts - Archive (4 fields, 1 class fields) Found 2 classes from /workspace/bun/src/runtime/api/BunObject.classes.ts - ResourceUsage (8 fields) - Subprocess (20 fields) Found 1 classes from /workspace/bun/src/runtime/api/cron.classes.ts - CronJob (5 fields) Found 3 classes from /workspace/bun/src/runtime/api/filesystem_router.classes.ts - FileSystemRouter (5 fields) - FrameworkFileSystemRouter (2 fields) - MatchedRoute (8 fields) Found 1 classes from /workspace/bun/src/runtime/api/Glob.classes.ts - Glob (5 fields) Found 1 classes from /workspace/bun/src/runtime/api/h2.classes.ts - H2FrameParser (32 fields) Found 9 classes from /workspace/bun/src ... (truncated) release without fix: 3 FAILED bun test v1.4.3-canary.1 (6a92015) test/bundler/bundler_cjs2esm.test.ts: (pass) bundler > cjs2esm/ModuleExportsFunction [29.67ms] (pass) bundler > cjs2esm/ImportNamedFromExportStarCJSModuleRef [13.37ms] (pass) bundler > cjs2esm/ImportNamedFromExportStarCJS [14.05ms] (pass) bundler > cjs2esm/BadNamedImportNamedReExportedFromCommonJS [12.90ms] (pass) bundler > cjs2esm/ExportsFunction [12.86ms] (pass) bundler > cjs2esm/ModuleExportsFunctionTreeShaking [14.55ms] (pass) bundler > cjs2esm/ModuleExportsEqualsRequire [14.10ms] (pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPoint [14.87ms] (pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPointImportedByEntryPoint [12.71ms] (pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPointImportedByEntryPointSplitting [11.37ms] (pass) bundler > cjs2esm/ModuleExportsEqualsRequireTwoEntryPoints [12.24ms] (pass) bundler > cjs2esm/ModuleExportsBasedOnNodeEnvProduction [14.95ms] (pass) bundler > cjs2esm/ModuleExportsBasedOnNodeEnvDevelopment [15.54ms] (pass) bundler > cjs2esm/ModuleExportsEqualsRuntimeCondition [11.08ms] (pass) bundler > cjs2esm/UnwrappedModuleRequireAssigned [14.33ms] (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.3 (6a92015) test/bundler/bundler_cjs2esm.test.ts: (pass) bundler > cjs2esm/ModuleExportsFunction [956.60ms] (pass) bundler > cjs2esm/ImportNamedFromExportStarCJSModuleRef [435.19ms] (pass) bundler > cjs2esm/ImportNamedFromExportStarCJS [376.00ms] (pass) bundler > cjs2esm/BadNamedImportNamedReExportedFromCommonJS [410.55ms] (pass) bundler > cjs2esm/ExportsFunction [350.96ms] (pass) bundler > cjs2esm/ModuleExportsFunctionTreeShaking [570.76ms] (pass) bundler > cjs2esm/ModuleExportsEqualsRequire [366.45ms] (pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPoint [483.54ms] (pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPointImportedByEntryPoint [421.50ms] (pass) bundler > cjs2esm/ModuleExportsEqualsRequireEntryPointImportedByEntryPointSplitting [435.10ms] (pass) bundler > cjs2esm/ModuleExportsEqualsRequireTwoEntryPoints [423.59ms] (pass) bundler > cjs2esm/ModuleExportsBasedOnNodeEnvProduction [613.50ms] (pass) bundler > cjs2esm/ModuleExportsBasedOnNodeEnvDevelopment [603 ... (truncated) release with fix: all passed $ bun scripts/build.ts --profile=release [configured] bun-profile → bun (stripped) in 739ms (unchanged) ninja: Entering directory `/workspace/bun/build/release' [1/61] cc obj/src/jsc/bindings/sqlite/sqlite3.c.o [2/61] gen generated_host_exports.rs generated_host_exports.rs: 122 exports (host=5, lazy=10, generic=107, rust=0); 243 extern-C blocks audited [3/61] gen ZigGeneratedClasses.{cpp,h,rs} Found 2 classes from /workspace/bun/src/jsc/resolve_message.classes.ts - ResolveMessage (15 fields) - BuildMessage (10 fields) Found 1 classes from /workspace/bun/src/runtime/api/Archive.classes.ts - Archive (4 fields, 1 class fields) Found 2 classes from /workspace/bun/src/runtime/api/BunObject.classes.ts - ResourceUsage (8 fields) - Subprocess (20 fields) Found 1 classes from /workspace/bun/src/runtime/api/cron.classes.ts - CronJob (5 fields) Found 3 classes from /workspace/bun/src/runtime/api/filesystem_router.classes.ts - FileSystemRouter (5 fields) - FrameworkFileSystemRouter (2 fields) - MatchedRoute (8 fields) Found 1 classes from /workspace/bun/src/runtime/api/Glob.classes.ts - Glob (5 fields) Found 1 classes from /workspace/bun/src/runtime/api/h2 ... (truncated) ``` </details> <details><summary>diff hotspot</summary> ``` src/js_parser/visit/visit_expr.rs | 3 ++ test/bundler/bundler_cjs2esm.test.ts | 62 ++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) ``` </details> **gate history** · 1 passed · 0 rejected · iteration 0 <details><summary>evidence per changed file</summary> ``` file reads edits tests src/js_parser/visit/visit_expr.rs 3 1 12 test/bundler/bundler_cjs2esm.test.ts 3 4 12 ``` </details> <!-- robobun:evidence:end -->
…nd-getters Conflict: test/bundler/bundler_dynamic_import_dce.test.ts. Main added ExternalReExportDestructureIsSnapshot (#42471) at the place where this branch added its tests. Both are kept. The tests of this branch now follow the KeepNamespace cases at the end of the file. Those cases (CommonJS importee, external importee, escaped namespace) are the conditions under which an item stays unbound.
Problem
this.(0, x_1.fn)(), which TypeScript emits, printsx_1.fn()with--minify-syntax. An unusedns.a;is removed, so a getter does not run.maybe_rewrite_property_access(src/js_parser/fold.rs:175) makesns.fan import item. The folds andexpr_can_be_removed_if_unusedthen treat it as a bound ES import.Fix
P::has_value_for_this_in_callreplaces theExprmethod. It also accepts an item whosenamespace_aliasnames a namespace local (P::is_namespace_local_read).expr_can_be_removed_if_unusedreturns false for such an item.test/bundler/bundler_dynamic_import_dce.test.ts(4 new cases, all fail on main), 23 other bundler suites. Self-reviewed (Notes).Background
require()orimport():const ns = require(x).ns.f.thisundefined for call targets and template tags that a rewrite turned into a property access #40829) and the linker. It removes the first cost below, but it is a larger change.Downsides
--minify). An unused top-level read keeps its export:const unused = THREE.WebGLRenderergrows a three bundle from 343,632 to 919,596 bytes..text: +768 bytes. Instructions: +0.04% on typeorm, inside the noise floor on three and zod.Notes
Reach. No user issue reports this. It still changes real output today.
bun build --minifyofimport "<pkg>"was run for each of the 110 packages intest/node_modules. 96 build, and in 17 of them main drops 2,440(0, …)wrappers that this branch keeps: typeorm 661, mongodb 520, verdaccio 367, fastify 254,@grpc/grpc-js188. Each one is a call that gets the required module asthis.Repro with a real package.
cross-fetch@4.1.0(dist/browser-ponyfill.js) ends withexports = ctx.fetch; exports.fetch = ctx.fetch; module.exports = exports, wherectx.fetchis the platformfetchwhen there is one. TypeScript 6.0.2 (module: commonjs) compilesto
const cross_fetch_1 = require("cross-fetch"); await (0, cross_fetch_1.fetch)(url);. Withbun build --minify --target=browserthe bundle callscross_fetch_1.fetch(url). That calls the platformfetchwith the function object asthis. Browsers reject that receiver (Illegal invocation). Checked under node 26.3.0 with aglobalThis.fetchthat makes the same receiver check, with and without--format=cjs:--minifyok 200ok 200ok 200TypeError: … Illegal invocationok 200ok 200Released binaries (each bundle is built with that binary and run under node 26.3.0. "main" is a release build of 5866b5f. The entries are the ones in the new tests, reduced):
--minify-syntax(true ? ns.f : 0)(),(1 && ns.f)(),(0 || ns.f)(),(null ?? ns.f)()undefinedx4exportsx4exportsx4undefinedx4(0, ns.f)(),(0, ns.f)?.(),const f = ns.f; f()undefinedx3undefinedx3exportsx3undefinedx3ns.boot; void ns.boot; const unused = ns.boot; if (ns.boot) {}const first = ns.first; return ns.second + firstfirst,secondfirst,secondsecond,firstfirst,second1.4.0 gives node's result in each cell, with and without
--minify-syntax. For animport()local of a CommonJS module, node itself reads a snapshot and runs no getter, so there the reference is 1.4.0 (3 getter runs for 3 reads. 1.4.1 to main: 0).Measurements. Two release builds of the merged tree (main at 5866b5f): one with the
src/of main, one with thesrc/of this branch.size):.text106,331,459 to 106,332,227 (+768).dataandbssdo not change. The stripped file has 106,501,896 bytes in both builds.perfandvalgrindare not installed in the container and its package mirrors do not resolve, so a small C program readPERF_COUNT_HW_INSTRUCTIONSthroughperf_event_open.--minifyimport(),--minifyrequire(),--minifylibraries.jsof the bytecode corpus)Only typeorm is above its noise floor. It is TypeScript CommonJS output, so it has the most items and gets 661 wrappers back.
--minify-syntaxand--minify:const { z } = await import("zod"),const zod = await import("zod")withzod.zreadsconst { pipe } = await import("effect"),const effect = require("effect")(effect 4.0.5 is ESM only, so the items are bound)const THREE = await import("three"),import * as THREE from "three"(0, effect_1.pipe)(…), a wrapped call of a bound item--minify-syntax), +4 (--minify)const unused = THREE.WebGLRenderer;added to the three entry, no flags--minifyconst unused = THREE.Quaternion;(an export the bundle needs anyway)--minifybundler_bytecode_portable.test.tssnapshots (lodash, acorn, react-dom, svelte, undici, happy-dom, immutable, 50 libraries in one file).jsand.jsc, 23,703,231 bytes of JavaScripttest/node_modules,import "<pkg>"andrequire("<pkg>"), no flags--minifyThe minified growth on the 110 packages is the fix itself, on CommonJS items: restored
(0, x.f)()wrappers, and a local that no longer moves past an item read (let s={…};return Dt.f(s)in tsyringe, as 1.4.0 and esbuild print it). webpack is left out of the minified count: main gives two different outputs for it in four runs.The cost for a bound item, and how to remove it. The parser decides before the linker knows the binding, so it has to assume the item is a property read. 1.4.1 to 1.4.3 assume the opposite, which is the bug. A bound item therefore keeps a wrapper and an unused top-level read, as on 1.4.0. To give both back, the decision has to move to where the binding is known. For
this, that is the printer rule of #40829. For an unused read, the parser can record the items that a part reads at top level, and the linker can keep the part only if one of them is unbound. Neither is in this PR.How the item is found.
fold.rsgives the item anamespace_aliaswhosenamespace_refis the local.is_namespace_local_readlooks that ref up indynamic_import_namespace_locals. The other writers ofnamespace_alias(the import scanner, the hot-reload paths inp.rs) use the namespace of an import statement, never a namespace local.Why the
Exprmethod is gone.Expr::has_value_for_this_in_callcannot see the symbol table, so it cannot see these items. With it removed, a new fold site has to call the parser method. #41159 (open) adds a fold site and rewrites both ternary arms with theExprmethod. After a rebase it does not compile until it callsp.has_value_for_this_in_call.esbuild (0.18.6 from
test/node_modules,--bundle --minify-syntax): forconst ns = require("./x.cjs")it keepsns.boot;and(0, ns.who)(). Forimport * as nsof the same file it drops the read and printsns.who().Scope. Only
const/letlocals and.thenparameters are tracked, sovar x = require(...)(Babel, ES5 TypeScript output) was never affected.const x = __importDefault(require(...))/__importStar(...)is not a directrequire()initializer and is not tracked either. With--format=esm,require()of a package inDEFAULT_UNWRAP_COMMONJS_PACKAGES(react,react-dom,react-is,scheduler,react-client,react-server,react-refresh) outsidetrybecomes an import statement. Its items areimport * as nsitems, so they behave as on 1.4.0 and this PR does not change them.Related.
thisundefined for call targets and template tags that a rewrite turned into a property access #40829 (open, conflicts with main) ports esbuild'starget_was_originally_property_access, so the printer adds(0, ...)for a call target that became a property access. It covers thethispart without a check in each fold and without the(0, f)()cost for a bound item. It does not cover the getter part. When it is rebased, drop theis_namespace_local_readarm ofP::has_value_for_this_in_call. The guard arm inexpr_can_be_removed_if_unusedstays.Cases that keep the namespace objectheader of the same test file. The tests of this PR are at the end of that file, after those cases, so the two PRs do not conflict. A trial merge of bundler: do not bind a redeclared import() / require() local, or a pattern off a {...rest} copy #45016 on top of this branch is clean.Not changed.
ns.f()written as a direct call on a bound item still callsf()withthis === undefined(the behaviour change that #41186 lists). These shapes are wrong on 1.4.0 too, so they are not part of this regression. They print the same on 1.4.0, on main and on this branch (#42452, closed, has the repros):(0, o.f)`x`folds for each plainEDotwith--minify-syntax.(0, ns.f)()and(1 && ns.f)()for an item ofimport * as ns, or of a named import, of a CommonJS module that stays wrapped lose the wrapper.(0, exports.f)()and(1 && exports.f)()lose the wrapper (ECommonjsExportIdentifier).([] ? o.m : 0)()folds too.m()for each property access (the ternary path for a test that is known but not free of side effects).#40829 covers the
thispart of all four in the printer.Self-review. A review of the diff raised these points. Addressed: the test for the item no longer depends on the order of the import scanner (map lookup), a new fold site cannot call a check that misses these items (the
Exprmethod is gone), each changed fold site has a test shape ((false ? 0 : ns.f)(),[ns.f][0](),ns["f"]), the getter test coversawait import(),.thenand a plain local that must not move past a read, and the notes above name #40829, #41159, the unwrap list and the siblings. Not taken: a fix for the sibling shapes, because they are not part of the regression.Suites run on the debug build of the merged tree, all pass:
bundler_dynamic_import_dce(337),bundler_minify,bundler_cjs2esm,bundler_cjs,bundler_promiseall_deadcode,bundler_barrel,bundler_splitting,bundler_edgecase,bundler_regressions,bundler_bun,bundler_jsx,bundler_feature_flag,bundler_drop,bundler_npm,bundler_browser,bundler_plugin,esbuild/dce,esbuild/importstar,esbuild/importstar_ts,esbuild/splitting,esbuild/extra,esbuild/default,esbuild/ts,transpiler/transpiler.test.js. The 4 new cases also fail 4 of 4 on the release build of main and pass 4 of 4 on the release build of this branch.bundler_bytecode_portabletimes out on a debug build (5 s per test), so its 16 bundles were built with both release builds and compared (see Measurements).[human-review] gate passed · iteration 0 · 5 files touched
fails on main (without fix)
passes on PR (with fix)
diff hotspot
gate history · 2 passed · 0 rejected · iteration 0
evidence per changed file