Skip to content

Keep this undefined for call targets and template tags that a rewrite turned into a property access - #40829

Open
robobun wants to merge 8 commits into
mainfrom
farm/9140bd75/preserve-this-for-call-targets-and-template-tags
Open

robobun wants to merge 8 commits into
mainfrom
farm/9140bd75/preserve-this-for-call-targets-and-template-tags

Conversation

@robobun

@robobun robobun commented Aug 28, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • A call target or template tag that is a property access binds this to the object. When a fold turns another expression into a property access in that position, the printer emitted it bare and this changed: (0, o.f)\x`and(true && o.f)`x`both printed aso.f`x`. bun runhits the same folds, sobun file.js` disagreed with Node.
  • Two more faces: import { fn } from "./cjs"; fn() bundled to import_cjs.fn() (the wrap check in src/js_printer/lib.rs matched EIdentifier, not EImportIdentifier). const t = a?.b; t\x`printeda?.b`x``, a SyntaxError.

Fix

  • Mirror esbuild: E::Call gets target_was_originally_property_access and E::Template gets tag_was_originally_property_access, set in parse_suffix.rs. The printer wraps the target in (0, ...) when it is a property access now but was not one originally.
  • Correct because the printer sees the final AST. No fold needs its own guard, so a new fold cannot reopen the bug. The EImportIdentifier arm wraps through the same flag.
  • The parser tracks template_tag next to call_target so the {f}.f and [o.f][0] inlining paths and dotted --define substitution treat a tag like a call target. The paren check for an optional chain inspects the tag.
  • Verified: test/bundler/transpiler/transpiler.test.js, test/bundler/transpiler/runtime-transpiler.test.ts (output equals Node), test/bundler/bundler_cjs.test.ts. Also test/bundler/**, test/bake/dev, test/js/bun/resolve.

Background

  • a.b() passes a as this. (0, a.b)() and fn() where fn holds a.b pass undefined. Tagged templates follow the same rule.
  • The bundler rewrites a named import of a CommonJS module into a namespace member (import_cjs.fn). Node calls such an export with this === undefined.
  • Seven synthesized calls have a property-access target and set the flag (hmr.require, import.meta.require, Temporal .from, the React compiler MethodCall and CallExpression, classic JSX factory, decorator .call). The others target identifiers.
Notes
  • esbuild reference: CallKind::TargetWasOriginallyPropertyAccess and ETemplate.TagWasOriginallyPropertyAccess in js_ast.go, the (0, ...) paths in js_printer.go (printExpr for ECall and ETemplate), and p.templateTag in js_parser.go. esbuild 0.28.2 output for the repros matches this PR except for import items used as template tags (import_lib.who\``), where this PR also wraps.
  • bun run output for the fixtures in runtime-transpiler.test.ts was compared with Node 26.3.0.
  • The runtime transpiler cache version is bumped to 28 because the printed output of cached files changes.
  • test/bundler/bundler_npm.test.ts (ReactSSR): five jsxDEV calls are now (0,Ha.jsxDEV)(...), which moves two source map columns by 16 and grows the file by 20 bytes. react-compiler.test.ts.snap: three import_react.* calls are now wrapped. bundler_bytecode_portable.test.ts: the corpus bundles now print (0, import_net.isIP)(...) style calls, so the snapshot hashes moved.
  • PR js_parser: track template_tag so wrapper folds don't rebind this #36735 fixed the template tag case with a guard in each fold. It is closed in favor of this PR. The folds it guarded one by one are covered by the printer here, and its test cases are folded into the tests: a ternary whose test is typeof x or a dropped || (the CouldHaveSideEffects path in e_if), a call or tag nested in the left operand of a comma (the late call_target capture in e_binary), the {}.x ??= v fold, and ({ f: o.f })["f"].
  • React compiler: optimize_props_method_calls turns props.render(1) into a CallExpression whose callee is the props.render property load. An audit of every synthesized E::Call found that this one printed as (0, props.render)(1) with the new printer rule, while Babel prints props.render(1). Codegen now sets the flag from the callee it emits (5d98550, test react-compiler/PropsMethodCallKeepsReceiver).
  • TypeScript namespaces: a bare f() inside namespace NS { export const f = ... } becomes NS.f() in the output. It now prints (0, NS.f)(), which keeps this undefined as the source call did. esbuild 0.21 prints the same. tsc prints NS.f() and leaks NS as this. Locked in by the TypeScript namespace exports case.
  • Pre-existing and unchanged: [f][0]() folds to f() (since js_parser: stop the [x][0] / {f:x}.f folds from changing optional-chain / this / assignment semantics #36730). Node passes the array as this there, bun passes undefined. [o.f][0]() gets the (0, o.f) wrap, so this is undefined in both.
  • Found while testing, handed off separately: bun build --format=cjs drops "use strict" inside __commonJS wrappers (the cjs and iife bundler tests here use a probe that reports "unbound" for both undefined and globalThis); --format=iife --target=node with externals emits __require("node:module") before __require exists; JavaScriptCore evaluates (y?.z)\`withthis === undefinedwhere V8 usesy(the runtime test avoids athis`-dependent tag there).
  • compile/HelloWorldWithProcessVersionsBun fails on a debug build independent of this change (process.versions.bun keeps the -debug suffix).

no test proof · iteration 4 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/bundler/bundler_npm.test.ts, test/bundler/bundler_bytecode_portable.test.ts

…te turned into a property access

A call target or template tag that is a property access binds `this` to
the object. When a fold, a single-use inlining, a dotted define, or the
bundler's namespace rewrite turned some other expression into a property
access in that position, the printer emitted it bare and `this` changed:
`import { fn } from "./cjs"; fn()` bundled to `import_cjs.fn()`,
`(0, o.f)\`x\`` printed as `o.f\`x\``, and `const t = a?.b; t\`x\`` printed
as `a?.b\`x\``, which is a SyntaxError.

Mirror esbuild: `E::Call` gets `target_was_originally_property_access`
and `E::Template` gets `tag_was_originally_property_access`, set by the
parser when the callee or tag is an `EDot` or `EIndex`. The printer wraps
the target in `(0, ...)` when it is a property access now but was not one
originally. An import item in that position is wrapped by the
`EImportIdentifier` arm through the same flag. The parser tracks
`template_tag` next to `call_target` so the `{f}.f` and `[o.f][0]`
inlining and dotted defines treat a tag like a call target. The
optional-chain check for tags now inspects the tag node.

Synthesized calls with a property-access target set the flag. The runtime
transpiler cache version is bumped because printed output changes.
@coderabbitai

coderabbitai Bot commented Aug 28, 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: Pro

Run ID: f9ffb636-0110-434f-ad77-f2558560274c

📥 Commits

Reviewing files that changed from the base of the PR and between d578a8c and c7fe42e.

⛔ Files ignored due to path filters (1)
  • test/bundler/transpiler/__snapshots__/react-compiler.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (25)
  • src/ast/e.rs
  • src/ast/expr.rs
  • src/ast/fold_string_addition.rs
  • src/bundler/ParseTask.rs
  • src/bundler/linker_context/convertStmtsForChunkForDevServer.rs
  • src/bundler/linker_context/generateCodeForLazyExport.rs
  • src/js_parser/fold.rs
  • src/js_parser/lower/lower_decorators.rs
  • src/js_parser/lower/lower_esm_exports_hmr.rs
  • src/js_parser/p.rs
  • src/js_parser/parse/parse_entry.rs
  • src/js_parser/parse/parse_prefix.rs
  • src/js_parser/parse/parse_suffix.rs
  • src/js_parser/parse/parse_typescript.rs
  • src/js_parser/parser.rs
  • src/js_parser/visit/visit_expr.rs
  • src/js_printer/lib.rs
  • src/jsc/RuntimeTranspilerCache.rs
  • src/react_compiler/codegen.rs
  • test/bundler/bundler_bytecode_portable.test.ts
  • test/bundler/bundler_cjs.test.ts
  • test/bundler/bundler_npm.test.ts
  • test/bundler/transpiler/react-compiler.test.ts
  • test/bundler/transpiler/runtime-transpiler.test.ts
  • test/bundler/transpiler/transpiler.test.js

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


Walkthrough

Changes

The parser and AST now record original property access for call targets and template tags. Visitors, lowerers, and generated expressions preserve this metadata. The printer emits (0, target) when needed, with regression coverage across bundling and transpilation.

Receiver semantics

Layer / File(s) Summary
AST metadata and parse-time tracking
src/ast/*, src/js_parser/parse/*, src/js_parser/parser.rs, src/react_compiler/codegen.rs
Call and template nodes record whether their original targets used property access.
Visitor, folding, and generated-expression propagation
src/js_parser/visit/*, src/js_parser/fold.rs, src/js_parser/lower/*, src/bundler/*
Expression rewrites and generated calls preserve receiver metadata.
Unbound call and template printing
src/js_printer/lib.rs, src/jsc/RuntimeTranspilerCache.rs
Printing tracks unbound targets, emits (0, target) when required, and updates the cache format.
Receiver-preservation regression coverage
test/bundler/*, test/bundler/transpiler/*
Tests cover calls, template tags, imports, folding, substitutions, optional chains, namespaces, React Compiler output, and bytecode snapshots.

Suggested reviewers: jarred-sumner, dylan-conway

Merge Risk: ⚪ Minimal · up to c7fe4

The PR preserves undefined this semantics for rewritten call targets and template tags and updates affected output and cache expectations; no actionable merge-blocking risk remains beyond 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.
Title check ✅ Passed The title clearly and concisely summarizes the primary change: preserving undefined this binding when rewrites create property-access call targets or template tags.
Description check ✅ Passed The description is detailed and covers the problem, fix, verification, regression tests, cache changes, and known CI limitations. It provides the information required by the template, although it uses…
Full details: Description check

Explanation

The description is detailed and covers the problem, fix, verification, regression tests, cache changes, and known CI limitations. It provides the information required by the template, although it uses Problem and Fix headings instead of the template headings.


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

@robobun

robobun commented Aug 28, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 4:39 PM PT - Aug 28th, 2026

❌ @robobun, your commit c7fe42e has 1 failures in Build #108032 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 40829

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

bun-40829 --bun

…raps

The happy-dom and libraries.js corpus bundles call named imports of node
builtins (net.isIP, fs.readFileSync, url.fileURLToPath). In CommonJS
output those are namespace members, so they now print as
(0, import_net.isIP)(...) and the JS and bytecode hashes move on every
platform at once.
Comment thread src/ast/e.rs Resolved
Comment thread src/ast/e.rs Resolved
Comment thread src/js_parser/parser.rs Resolved
Comment thread src/js_parser/visit/visit_expr.rs Resolved
Comment thread src/js_parser/visit/visit_expr.rs Resolved
Comment thread src/js_printer/lib.rs Resolved
Comment thread src/js_printer/lib.rs Resolved
Comment thread src/js_printer/lib.rs Resolved
Comment thread src/js_printer/lib.rs Resolved
Comment thread src/jsc/RuntimeTranspilerCache.rs Resolved
Comment thread src/ast/e.rs Resolved
Comment thread src/js_printer/lib.rs Resolved
Comment thread src/js_printer/lib.rs Resolved
Comment thread src/js_printer/lib.rs Resolved
Comment thread src/js_printer/lib.rs Resolved
@robobun

robobun commented Aug 28, 2026 •

Copy link
Copy Markdown
Collaborator Author

Reproduced on main (65362b5) with var obj = { m() { return this === obj } } and the tag expressions (0, obj.m), (1 ? obj.m : 0), (null ?? obj.m), (true && obj.m), (false || obj.m), [obj.m][0], ({m: obj.m}).m, (typeof x ? obj.m : 0), ({}.x ??= obj.m): every tag call printed true under bun run and bun build --minify-syntax, Node prints false. On this branch they print false, and the bundle shows (0, obj.m) for each.

CI on f61cb42: 180 of 181 jobs pass. The one red lane is darwin x64, where test/js/web/url/url.test.ts ("special-scheme hosts use the Unicode 16 IDNA table") fails with TypeError: Invalid URL. That test depends on the system ICU version and fails on main on that lane too. It is reported to main-break triage. The first run also moved the bytecode portability snapshot, since the happy-dom and libraries.js corpus bundles now print (0, import_net.isIP)(...) style calls; b086423 updates it and the hashes match on every lane.

b3549b6 fixes the cargo clippy failure on f61cb42 (trivially_copy_pass_by_ref on is_unbound_call_target in src/js_printer/lib.rs) and adds the test cases from #36735 that were missing here: the typeof ternary, a call or tag nested in the left operand of a comma, the {}.x ??= v fold, and ({ f: o.f })["f"]. #36735 is closed in favor of this PR.

5d98550 fixes a regression found in a self-review of the printer rule: with --react-compiler, props.render(1) printed as (0, props.render)(1) because optimize_props_method_calls emits a CallExpression whose callee is the props.render property load. Codegen now sets the flag from the callee it emits, so the output is props.render(1) as on main and in Babel. Test: react-compiler/PropsMethodCallKeepsReceiver. The same commit locks in (0, NS.f)() for a bare f() inside a TypeScript namespace, which matches esbuild.

CI on c7fe42e (build 108032): cargo clippy is green again. The transpiler, runtime transpiler, bundler and React compiler test files pass on every lane. The red lanes are the same darwin x64 url.test.ts failure (pre-existing on main, already in triage) and tests marked flaky that passed on retry (bun-install-offline, bun-lock, no-orphans, compression, tcp-server, serve-http2-lifecycle, fetch/body, bun-patch, two napi tests, node-http-pinned-write, spawnsync-isolated-event-loop, fetch.tls.wildcard). None of them touch code this PR changes.

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

Code review found no issues

No high-confidence issues detected in this change.

#36735

The clippy lint trivially_copy_pass_by_ref rejects a reference to the
8-byte E::ImportIdentifier in is_unbound_call_target.

The new test cases cover the folds that PR #36735 guarded one by one:
a ternary whose test is typeof or a dropped ||, a call or template tag
nested in the left operand of a comma, the {}.x ??= v fold, and an
index into an object literal. The printer wraps every one of them.

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

Code review found no issues

No high-confidence issues detected in this change.

optimize_props_method_calls turns props.foo(1) into a CallExpression
whose callee is the props.foo property load. The printer now wraps a
property access call target in (0, ...) unless the call was originally
a property access, so the compiled output became (0, props.foo)(1) and
this changed from props to undefined. Babel prints the callee as
written. Set the flag from the callee that codegen emits.

Also lock in the output for a bare call of a namespace export: f()
inside namespace NS prints (0, NS.f)(), as esbuild does.
@robobun

robobun commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator Author

Landing order note: once the printer keeps the parentheses of (a?.b)\x`, that expression reaches JavaScriptCore, which currently calls the tag with this=undefined(engine bug, fix in oven-sh/WebKit#534, pinned by #40848). Today the printer's SyntaxError hides it. Landing #40848 before this PR keeps the loud error from turning into a silent wrongthis`.

Verified on bun 1.4.1 with new Function("y", "return (y?.z)\x`")(y): the tag sees undefined, node sees y`.

@robobun

robobun commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator Author

Landing order note for #42402.

#42402 fixes a regression from #41186 that is in 1.4.1 and 1.4.2. An import item made of ns.f, where ns is a require() / import() local, lost its (0, ns.f)() wrapper, and an unused ns.a read was removed. It adds P::has_value_for_this_in_call(&expr), which also returns true for such an item, and it removes Expr::has_value_for_this_in_call.

When this PR is rebased after #42402:

  • This PR calls Expr::has_value_for_this_in_call at parse time to set target_was_originally_property_access. That is a syntactic check, and those calls no longer compile. Use matches!(x.data, EDot(_) | EIndex(_)) there, or add the method again under a syntactic name such as is_property_access.
  • The printer rule here covers the this half of bundler: keep a property read's this and getter for an unbound item of a require() / import() local #42402. Drop the is_namespace_local_read arm of P::has_value_for_this_in_call. A bound item then prints f() again and not (0, f)().
  • Keep the guard arm in expr_can_be_removed_if_unused. That is the getter half, which this PR does not cover.
  • UnboundItemCallWithoutThis* and UnboundItemReadRunsGetter* in test/bundler/bundler_dynamic_import_dce.test.ts must stay green.

#42452 lists the shapes of this kind that are wrong on 1.4.0 too. This PR covers all of them.

Jarred-Sumner pushed a commit that referenced this pull request Sep 26, 2026
…#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 -->
@robobun

robobun commented Oct 31, 2026

Copy link
Copy Markdown
Collaborator Author

Update to my landing order note above, the one that asks for #40848 to land first: that constraint no longer applies.

Upstream WebKit fixed the engine side in WebKit/WebKit@7fbe85cfbfdc, and main has it since #44615 (in Bun v1.4.3). On a debug build of main at 34cbc70, new Function("y", "return (y?.z)\x`")(y)calls the tag withthis=y`, as node does. #40848 is closed.

The printer side is still open on main: a plain file with (y?.z)\x`fails withSyntaxError: Cannot use tagged templates in an optional chain, because the parentheses are dropped. So this PR no longer has to wait for a WebKit bump, and once the printer keeps the parentheses the tag gets the right this. A test can now assert that thisfor(y?.z)`x`` in a plain file.

This branch has not been deployed

No deployments
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