Skip to content

minify-syntax: keep this for ns["tag"]x on a lifted CommonJS export - #42549

Merged
Jarred-Sumner merged 2 commits into
mainfrom
robobun/db567482/minify-syntax-template-tag-keeps-this
Sep 26, 2026
Merged

Jarred-Sumner merged 2 commits into
mainfrom
robobun/db567482/minify-syntax-template-tag-keeps-this

Conversation

@robobun

@robobun robobun commented Sep 13, 2026 •

Copy link
Copy Markdown
Collaborator

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 bundler: keep this for a method call on a lifted CommonJS export #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. bundler: keep this for a method call on a lifted CommonJS export #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.
Notes

Repro (fails on 1.4.3-canary.1+6a92015fc and on main b993710). git tag --contains f31440d21d lists bun-v1.4.1 and bun-v1.4.2, so both releases have the same code:

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:

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.


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

fails on main (without fix)
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 (6a92015fc)

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)
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.3 (6a92015fc)

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)
diff hotspot
src/js_parser/visit/visit_expr.rs    |  3 ++
 test/bundler/bundler_cjs2esm.test.ts | 62 ++++++++++++++++++++++++++++++++++++
 2 files changed, 65 insertions(+)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                  reads  edits  tests
src/js_parser/visit/visit_expr.rs         3      1     12
test/bundler/bundler_cjs2esm.test.ts      3      4     12

…["b"] to a.b

The rewrite moved call_target and delete_target to the new E::Dot, but not
template_tag. So ns["tag"]`x` on a lifted CommonJS export printed as
$tag`x`, and the tag function ran with this === undefined.
@robobun

robobun commented Sep 13, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 7:57 PM PT - Sep 12th, 2026

✅ @robobun, your commit c4fb19213088f57c3638d409e47f857d9ced4077 passed in Build #114980! 🎉


🧪   To try this PR locally:

bunx bun-pr 42549

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

bun-42549 --bun

@coderabbitai

coderabbitai Bot commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview 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: 4ecdabfd-732c-4306-a5e6-328108686d7b

📥 Commits

Reviewing files that changed from the base of the PR and between f04caca and c4fb192.

📒 Files selected for processing (2)
  • src/js_parser/visit/visit_expr.rs
  • test/bundler/bundler_cjs2esm.test.ts

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


Walkthrough

The visitor now preserves template-tag tracking when it converts computed properties to dot access. Bundler tests cover imported methods, template tags, and self-referential exports calls under syntax minification.

Changes

Computed member minification

Layer / File(s) Summary
Preserve template-tag tracking
src/js_parser/visit/visit_expr.rs
The computed-to-dot rewrite updates template_tag when the original index expression is the tracked template-tag target.
Validate minified member calls
test/bundler/bundler_cjs2esm.test.ts
Regression tests cover default and namespace imports, receiver preservation, emitted syntax, runtime results, and self-referential exports calls.

Suggested reviewers: jarred-sumner

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to c4fb1

The fix preserves receiver binding for minified computed template tags, with regression coverage for import and CommonJS export paths. No actionable merge risk remains.

🚥 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 identifies the minify-syntax fix and the affected computed template-tag access on a lifted CommonJS export.
Description check ✅ Passed The description explains the problem, fix, affected code path, regression coverage, and verification results. It does not use the exact template headings, but it provides the required content and is c…

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

@robobun

robobun commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator Author

Status

  • Reproduced on 1.4.3-canary.1+6a92015fc and on main b993710. The repro is in the notes of the PR body. bun build entry.mjs --minify-syntax --target bun prints console.log($tag`z`), and the output throws TypeError: undefined is not an object (evaluating 'this._helper').
  • The 3 new tests in test/bundler/bundler_cjs2esm.test.ts fail with the released binary (USE_SYSTEM_BUN=1 bun test) and pass with the debug build (bun bd test).
  • PR: minify-syntax: keep this for ns["tag"]x on a lifted CommonJS export #42549

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

LGTM — the is_template_tag local was already computed at the top of e_index and this just propagates it to the rewritten dot node, exactly mirroring the neighboring is_call_target/is_delete_target arms and esbuild's p.templateTag = dot.

What was reviewed:

  • Confirmed is_template_tag is computed at line 854 alongside the other two markers, so the added block is the missing sibling, not a new code path.
  • Checked the second a["x"+"y"] → a.b rewrite further down in e_index — it returns without re-visiting, so no marker propagation is needed there (matches the PR's claim).
  • Tests reuse the existing thisReadingLib fixture, cover all three consumers (default/star/self), and assert both emitted bundle text and runtime stdout; the star-import case correctly avoids pinning the namespace variable name.
Extended reasoning...

Overview

This PR is a three-line fix in src/js_parser/visit/visit_expr.rs (e_index), plus three new itBundled test cases appended to test/bundler/bundler_cjs2esm.test.ts. When --minify-syntax rewrites X["name"] to X.name, the visitor already re-tags the replacement E::Dot node as p.call_target and p.delete_target; the fix adds the identical treatment for p.template_tag. The is_template_tag boolean was already being computed at the top of the function (line 854) — it just wasn't being used at this rewrite site. Without the fix, ns["tag"]`z` on a lifted CommonJS export unwraps to a bare identifier and loses its this receiver.

Security risks

None. This is a bundler AST-marker propagation fix with no bearing on auth, crypto, permissions, network, filesystem, or untrusted input parsing. The change only affects which node address is stored in a parser-internal pointer field during expression visiting.

Level of scrutiny

Low. The fix is mechanical and byte-for-byte parallel to the two adjacent if blocks for call_target and delete_target. The PR description notes esbuild does the same (p.templateTag = dot), and REVIEW.md treats esbuild as the reference implementation for ported bundler logic. I verified the second index→dot rewrite in e_index (constant-folded string keys) does not re-visit the node and so needs no marker — the PR author's rationale checks out against the code. No CODEOWNERS entry covers src/js_parser/ or test/bundler/.

Other factors

The three new tests are placed in the existing bundler_cjs2esm.test.ts file (per CLAUDE.md guidance), reuse the file's shared thisReadingLib fixture, and assert both the emitted bundle text (toContain/toMatch on the exact console.log(...) line) and runtime stdout — so each test can fail on the code shape and on behavior independently. Each fixture uses only computed member forms (X["tag"]) so the test isolates the rewrite path rather than being satisfied by a sibling X.tag marking the symbol. The star-import test uses \w+ for the namespace name to avoid coupling to an unrelated open PR that renames it. The bug-hunting run exited on dry_streak with no findings and no ruled-out candidates. The PR timeline has no outstanding reviewer objections.

@Jarred-Sumner
Jarred-Sumner merged commit a2b4233 into main Sep 26, 2026
11 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the robobun/db567482/minify-syntax-template-tag-keeps-this branch September 26, 2026 02:41
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