Skip to content

js_parser: keep anonymous export default function anonymous so .name is "default" - #34932

Open
robobun wants to merge 5 commits into
mainfrom
farm/14e68797/export-default-fn-name
Open

robobun wants to merge 5 commits into
mainfrom
farm/14e68797/export-default-fn-name

Conversation

@robobun

@robobun robobun commented Jul 21, 2026 •

Copy link
Copy Markdown
Collaborator

What

export default function () {} (and its async/generator variants) produced a function whose observable .name was "<basename>_default" instead of the spec-required "default". Node prints "default".

// mod.mjs
export default function () {}
import self from './mod.mjs';
console.log(JSON.stringify(self.name));
// node mod.mjs -> "default"
// bun  mod.mjs -> "mod_default"   (before)

export default class {} and export default () => 1 were already correct at runtime.

Why

The visit pass wrote the generated default-export binding symbol (<basename>_default, created by create_default_name) into func.func.name when the source had no name, so the printer emitted export default function mod_default() {}. Per ES2015 15.2.3.11, an anonymous default-exported function declaration receives its name via SetFunctionName(F, "default") at evaluation time, which only happens when the emitted declaration stays anonymous.

The class path only injects data.default_name into class_name when decorator lowering needs a concrete binding; this change brings the function path in line. The assignment is kept under react_fast_refresh because that dev-only transform emits a $RefreshReg$(default_name_ref, ...) call that needs a concrete binding, and ConvertESMExportsForHmr relies on func.name to produce one. react_fast_refresh is only set on bundler/bake paths, never on the runtime transpiler, so bun run / --no-bundle output stays anonymous. data.default_name is still recorded via record_on_exit!(), and the bundler's convertStmtsForChunk assigns the name itself when stripping exports.

This is observable via .name-keyed dispatch/telemetry and leaked the source file's basename.

Verification

New tests in test/bundler/transpiler/export-default-name.test.ts cover function, async function, function*, async function*, class, arrow, and a named function (regression guard). The four function cases fail on main with "mod_default" and pass with this change. test/bake/dev/react-spa.test.ts (React Fast Refresh with anonymous default components) passes. test/regression/issue/31401.test.ts was updated to check the _1_default identifier in bundled output, where convertStmtsForChunk still emits it.


[review] gate passed · iteration 4 · 3 files touched

fails on main (without fix)
ASAN without fix: 4 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bundler/transpiler/export-default-name.test.ts "test/regression/issue/31401.test.ts"
bun test v1.4.0 (259c30df9)

test/regression/issue/31401.test.ts:
(pass) issue 31401: anonymous default export from digit-named module > bundled output uses a valid identifier for the generated default name [277.90ms]
(pass) issue 31401: anonymous default export from digit-named module > run a digit-named module with an anonymous default function [452.77ms]
(pass) issue 31401: anonymous default export from digit-named module > import a digit-named module with an anonymous default function [445.81ms]

test/bundler/transpiler/export-default-name.test.ts:
18 |       env: bunEnv,
19 |       cwd: String(dir),
20 |       stderr: "pipe",
21 |     });
22 |     const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
23 |     expect({ stdout, stderr, exitCode }).toEqual({ stdout: '"default"\n', stderr: "", exitCode: 0 });
                                              ^
error: expect(received).toEqual(expe
... (truncated)

release without fix: all passed
bun test v1.4.0-canary.1 (88e8b7a84)

test/regression/issue/31401.test.ts:
(pass) issue 31401: anonymous default export from digit-named module > bundled output uses a valid identifier for the generated default name [5.23ms]
(pass) issue 31401: anonymous default export from digit-named module > run a digit-named module with an anonymous default function [13.05ms]
(pass) issue 31401: anonymous default export from digit-named module > import a digit-named module with an anonymous default function [12.24ms]

test/bundler/transpiler/export-default-name.test.ts:
(pass) anonymous export default .name > function: .name is 'default' [14.26ms]
(pass) anonymous export default .name > function*: .name is 'default' [12.98ms]
(pass) anonymous export default .name > async function: .name is 'default' [13.52ms]
(pass) anonymous export default .name > async function*: .name is 'default' [12.47ms]
(pass) anonymous export default .name > class: .name is 'default' [11.96ms]
(pass) anonymous export default .name > arrow: .name is 'default' [11.46ms]
(pass) anonymous export default .name > named function keeps its own name [11.50ms]

 10 pass
 0 fail
 2 snapshots, 16 expect() calls
Ran 
... (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/mechgate.xml" test/bundler/transpiler/export-default-name.test.ts "test/regression/issue/31401.test.ts"
bun test v1.4.0 (259c30df9)

test/regression/issue/31401.test.ts:
(pass) issue 31401: anonymous default export from digit-named module > bundled output uses a valid identifier for the generated default name [377.54ms]
(pass) issue 31401: anonymous default export from digit-named module > run a digit-named module with an anonymous default function [477.34ms]
(pass) issue 31401: anonymous default export from digit-named module > import a digit-named module with an anonymous default function [607.24ms]

test/bundler/transpiler/export-default-name.test.ts:
(pass) anonymous export default .name > function*: .name is 'default' [512.83ms]
(pass) anonymous export default .name > function: .name is 'default' [747.25ms]
(pass) anonymous export default .name > class: .name is 'default' [864.17ms]
(pass) anonymous export default .name > async function: .name is 'default' [921.52ms]
(pass) anonymous export default .name > async function*: .name is 'default' [1
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 727ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[0/5] 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_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m   Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
�[1m�[92m   Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
�[1m�[92m   Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
�[1m�[92m   Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety)
�[1m�[92m   Compiling�[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys)
�[1m�[92m   Compiling�[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys)
�[1m�[92m   Compiling�[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd)
�[1m�[92m   Compiling�[0m bun_paths v0.0.0 (/workspace/bun/src/paths)
�[1m�[92m   Compiling�[0m bun_collections v0.0.0 (/workspace/bun/src/collections)
�[1m�[92m   Compiling�[0m bun_picohttp v0.0.0 (/workspace/bun/src/picohttp)
�[1m�[92m   Compiling�[0m b
... (truncated)
diff hotspot
src/js_parser/visit/visit_stmt.rs                  |  6 +++-
 .../bundler/transpiler/export-default-name.test.ts | 39 ++++++++++++++++++++++
 test/regression/issue/31401.test.ts                |  9 ++---
 3 files changed, 49 insertions(+), 5 deletions(-)

gate history · 3 passed · 1 rejected · iteration 4

evidence per changed file
file                                                 reads  edits  tests
src/js_parser/visit/visit_stmt.rs                        4      4      0
test/bundler/transpiler/export-default-name.test.ts      0      1      0
test/regression/issue/31401.test.ts                      1      4      0

The visit pass was writing the generated default-export binding symbol
(`<basename>_default`) into `func.func.name` for `export default function () {}`
(and its async/generator variants), so the printer emitted
`export default function <basename>_default() {}` and the function's
observable `.name` became `"<basename>_default"` instead of `"default"`.

Per ES2015 15.2.3.11, an anonymous `export default` function declaration
gets its name via SetFunctionName(F, "default"); node prints "default".
The class path already only injects a name when decorator lowering
requires it; this brings the function path in line.

`data.default_name` is still recorded via `record_on_exit!()`, and the
bundler's own conversion in convertStmtsForChunk assigns the name
separately, so nothing else relies on this assignment.
@robobun

robobun commented Jul 21, 2026 •

Copy link
Copy Markdown
Collaborator Author

Reproduced with:

$ printf 'export default function () {}\nimport self from "./mod.mjs";\nconsole.log(JSON.stringify(self.name));\n' > /tmp/mod.mjs
$ bun /tmp/mod.mjs
"mod_default"
$ node /tmp/mod.mjs
"default"

PR: #34932

The name injection is kept for the React Fast Refresh dev path (which needs a binding for $RefreshReg$); the runtime transpiler leaves the function anonymous.

The diff is green on every lane that runs it (export-default-name.test.ts, 31401.test.ts, bake/dev/react-spa.test.ts all pass). The remaining CI red across builds 76886 and 76924 is unrelated: the darwin 14 x64 lane failed at checkout on one run (git clone: destination not empty on macOS-13-x64-1) and at the test-runner level on the other (uv_os_get_passwd ENOENT, bun-profile ENOENT); bun-server.test.ts on darwin aarch64 is a CPU-usage threshold flake. Ready for review.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Default export naming

Layer / File(s) Summary
Conditional default export naming
src/js_parser/visit/visit_stmt.rs
Anonymous default-exported functions retain their anonymous status unless React Fast Refresh is enabled, where generated stable names remain assigned.
Runtime and bundling regression tests
test/bundler/transpiler/export-default-name.test.ts, test/regression/issue/31401.test.ts
Tests cover runtime .name values for default exports and bundled output for digit-named modules.

Possibly related PRs

  • oven-sh/bun#34795: Refactors the same s_export_default path while this change adjusts generated default function name assignment.
🚥 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 summarizes the main fix: anonymous default exports keep the default name.
Description check ✅ Passed The description covers what changed, why, and how it was verified, matching the template's intent.

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

@robobun

robobun commented Jul 21, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 9:07 AM PT - Jul 21st, 2026

❌ @robobun, your commit 259c30d has some failures in Build #76924 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 34932

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

bun-34932 --bun

Comment thread src/js_parser/visit/visit_stmt.rs
Comment thread src/js_parser/visit/visit_stmt.rs Outdated
Comment thread test/bundler/transpiler/runtime-transpiler.test.ts Outdated
…ests

The React Fast Refresh transform emits $RefreshReg$(default_name_ref, ...)
for an anonymous default-exported function, and the HMR export lowering
relies on func.func.name being set to produce a binding for that ref.
Keep the name injection on that dev-only path so bake/dev react-spa
works, while the runtime transpiler (bun run / --no-bundle) still leaves
the function anonymous.

Also:
- 31401.test.ts: the third case asserted the --no-bundle identifier;
  switch it to a bundled build, which still surfaces _1_default via
  convertStmtsForChunk and preserves the original leading-digit check.
- runtime-transpiler.test.ts: assert {stdout, stderr, exitCode} as one
  object instead of expect(stderr).toBe("").

@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: 2

🤖 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/visit/visit_stmt.rs`:
- Around line 584-587: Shorten the comment near the generated function-name
handling to no more than three lines while preserving the essential points:
leave the name unset for default assignment, and inject it only for the React
Fast Refresh binding path.

In `@test/regression/issue/31401.test.ts`:
- Around line 48-49: Remove the prose comments in the regression test, including
the comment describing --no-bundle and convertStmtsForChunk; retain only the
issue URL comment required by the project’s regression-test convention.
🪄 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: 0525b3dc-1123-4c95-a29b-faf7034d3681

📥 Commits

Reviewing files that changed from the base of the PR and between e550f2c and e4da3ef.

📒 Files selected for processing (3)
  • src/js_parser/visit/visit_stmt.rs
  • test/bundler/transpiler/runtime-transpiler.test.ts
  • test/regression/issue/31401.test.ts

Comment thread src/js_parser/visit/visit_stmt.rs Outdated
Comment thread test/regression/issue/31401.test.ts Outdated
Comment thread src/js_parser/visit/visit_stmt.rs Outdated
runtime-transpiler.test.ts has an in-process dynamic .hbs import that is
sensitive to the release transpiler cache; keep the new subprocess-based
tests in a separate file so the gate's release lane stays deterministic.

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

Beyond the inline nit, I re-checked the downstream consumers after the react_fast_refresh gating: convertStmtsForChunk.rs:525 still force-assigns s.default_name to func.name when stripping exports (so bundled output and the updated 31401 test are covered), and opts.features.react_fast_refresh is only ever set true on bundler/bake paths (ParseTask.rs:2475, bake/*) — never on the runtime transpiler — so the .name === "default" fix holds for bun run and --no-bundle while HMR keeps its binding.

Extended reasoning...

The only finding this run is a test-organization nit; the substantive concerns from my earlier passes (31401 breakage, HMR ReferenceError) were addressed in e4da3ef and I verified the current head still satisfies them: the bundler linker unconditionally names the function when lowering SExportDefault, and the feature flag the new gate reads is exclusively a bundler/dev-server flag. Deferring rather than approving because this is a parser visit-pass change that fans out to four output modes; the gating choice (keep name-injection under react_fast_refresh vs. teaching ConvertESMExportsForHmr to handle the anonymous case) is a small design call worth a maintainer glance.

Comment thread test/bundler/transpiler/export-default-name.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.

No issues found in this pass; all earlier feedback (HMR/$RefreshReg$ binding, 31401 test, stderr assertion, comment length, test-file placement) has been addressed. Deferring to a human because this is a user-observable parser output change whose first revision broke the Fast Refresh / HMR export-lowering path — someone familiar with bake/HMR should confirm the react_fast_refresh gate is the right seam.

Checked: the else { data.default_name.ref_ } arm in the fast-refresh block is now only reachable when func.func.name was set (gate matches), so ConvertESMExportsForHmr still gets a binding.
Checked: convertStmtsForChunk assigns default_name itself when stripping exports, and the updated 31401 test exercises that.
Checked: the server_components.wraps_exports() path without fast-refresh now wraps an anonymous function expr — the reference uses the "default" string arg, not .name, so no behavior loss.
The one CI failure (bun-server.test.ts on macOS aarch64) is unrelated to the parser.

Extended reasoning...

Overview

The PR gates a single assignment in src/js_parser/visit/visit_stmt.rs (s_export_default → SFunction arm): func.func.name = Some(data.default_name) now only fires when p.options.features.react_fast_refresh is set. This keeps export default function () {} anonymous in runtime-transpiler and --no-bundle output so the engine's SetFunctionName assigns .name = "default", matching Node and the ES2015 spec. A new test file covers function/async/generator/async-generator/class/arrow/named variants, and test/regression/issue/31401.test.ts is re-expressed against bundled output where convertStmtsForChunk still emits _1_default.

Security risks

None. This changes emitted identifier presence for anonymous default-exported functions; if anything it stops leaking the source file's basename via .name. No untrusted-input parsing, no allocation, no FFI.

Level of scrutiny

Moderate-to-high. The production diff is ~6 lines, but it sits in the parser visit pass and the removed assignment was doing double duty — it was both the source of the .name bug and the thing ConvertESMExportsForHmr relied on to produce a binding for $RefreshReg$. The first revision of this PR unconditionally removed the assignment and would have caused a runtime ReferenceError under bake HMR for anonymous default components in .tsx files; the current revision gates it correctly and test/bake/dev/react-spa.test.ts was verified locally. That history is exactly why a maintainer who owns bake/HMR should confirm react_fast_refresh is the right flag to gate on (vs. e.g. hot_module_reloading or checking both).

Other factors

  • All four inline findings from earlier passes are resolved and marked as such in the thread.
  • Test coverage is good: the variant matrix (all four function forms, class, arrow, named regression guard) is covered via subprocess spawns with combined {stdout, stderr, exitCode} assertions; the bundler path's name-injection is still exercised by the updated 31401 test.
  • The separate export-default-name.test.ts file was justified by the author (a pre-existing cache-poisoning bug in runtime-transpiler.test.ts).
  • The one CI failure (test/js/bun/http/bun-server.test.ts, macOS aarch64) is an HTTP test with no plausible connection to a parser identifier-emission change.

@robobun

robobun commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

Findings from work on the same hunk, while I looked at #44721. I ran each item on release builds of main bd599f5 and of the branch below, except where it says "read".

Gate. This PR sets the name only under react_fast_refresh. The React Compiler also reads it in a bundle. visit_func passes func.name to enter_react_compiler_candidate (src/js_parser/react_compiler_host.rs:189), and may_compile infers a component from that name. So bun build --react-compiler without Fast Refresh loses the name of an anonymous default function in App.tsx (read, not run). The gate if p.options.bundle keeps every bundler parse as it is. Branch robobun/da68edbd/anonymous-default-export-name has the hunk with that gate and tests (33ef9bd).

Labels. Without a name, JSC binds the function to its private name *default*. A reader that does not go through .name then shows starDefault or nothing, where main shows <file>_default:

  • Stack frames, Bun.inspect, describe() titles and bun:test matcher output. The mapping of Name anonymous export default functions and classes "default" in stack traces and inspect #38517 fixes these, so it must land first or together with this PR.
  • mock(fn).name and getMockName() give "". JSMockFunction.cpp:296 reads JSFunction::name.
  • --cpu-prof shows starDefault when the function is inlined into its caller (1,108 of 1,121 samples in one run, mid_default on main).
  • Heap snapshot class names (generateHeapSnapshotForDebugging) show starDefault, anonfn_default on main.

JavaScriptCore composes the last two itself (SamplingProfiler.cpp, HeapSnapshotBuilder.cpp). A mapping in Bun's bindings does not reach them. The rule that *default* shows as default has to be in the display name fall-throughs of the WebKit fork before this hunk lands.

Cache version. The printed text of a module changes, so EXPECTED_VERSION in src/jsc/RuntimeTranspilerCache.rs needs a bump (the rule at the top of src/js_parser/parser.rs).

One more effect. The runtime transpiler cache keys an entry on the content, not on the path. On main, a 73 KB utils.mjs copied to helpers.mjs reports utils_default as the .name of the default function of helpers.mjs. With this hunk both report default.

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.

1 participant