Skip to content

Show an anonymous default export as default, not starDefault - #47502

Draft
robobun wants to merge 3 commits into
mainfrom
robobun/2cdb9aa5/default-export-display-name
Draft

robobun wants to merge 3 commits into
mainfrom
robobun/2cdb9aa5/default-export-display-name

Conversation

@robobun

@robobun robobun commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • Stack frames, Bun.inspect, snapshots and the debugger show an anonymous default export as starDefault. Examples: at new starDefault, [class starDefault]. The name property and Node say default.
  • The text is JavaScriptCore's private name for the binding. The engine stored it as the name of the function.
  • No user reported this (Notes). Since bun test: Vitest compatibility #44833, main gives it as a mock name too: jest.fn(fn).name.

Fix

  • Use the WebKit build of [JSC] Store the name default for an anonymous default export WebKit#950, which stores the name default. Bun changes no name itself.
  • JSC__JSValue__getNameProperty loses a dead fallback (src/jsc/bindings/bindings.cpp:5406). The ahead-of-time compiler keeps HAS_EXPLICIT_NAME for export default class {}, so compiled code names the class as plain Bun does.
  • Verified: the 14 new tests fail with main's engine and pass with this one (13 files, Notes).
  • Self-reviewed: 8 concerns raised, 8 addressed. Left out: a test of the new toStrictEqual result, which Jest does not give.

Background

Downsides

  • Recorded text changes for export default class {}: snapshot values, describe(Class) titles and jest.fn(Class).name. Notes has the list.
  • toStrictEqual compares class names. An instance of an anonymous default class now equals a like instance of another class named default.
  • Not reached: export default function () {} from a file is still <file>_default (js_parser: attach the generated default name to an anonymous default function only in a bundle #34932). An async arrow with an await has an unnamed frame.
Notes

This is a draft for CI. WEBKIT_VERSION is the preview build of oven-sh/WebKit#950 (autobuild-preview-pr-950-c5dabb29: Bun's pin 4d45755fed plus that one commit). It must not merge like this. Before a merge the pin has to be a merged WebKit commit with builds for every platform. Fork main is ahead of Bun's pin (six commits on 1 November), so the commit reaches Bun main with the first pin bump after it merges, or with a bump to a merged commit of this one alone. The tests and the two source changes here land with that bump or right after it.

Where the rule is. oven-sh/WebKit#950 has the cause, the design, the measurements and four questions for a maintainer: is the change of text wanted, is JavaScriptCore the place, patch or minor release, and one compare per new function. #38517 and #38530 are the other place for the rule, a filter in Bun's bindings. They stay open until that is answered. If WebKit#950 merges, this pull request takes their place. #34932 lands after this one.

Text that changes

  • Snapshot values, re-recorded with bun test -u: [class starDefault] to [class default], starDefault {} to default {}, <starDefault /> to <default />, Any<starDefault> to Any<default>. GitHub code search finds no public Bun Snapshot v1 file with starDefault in it, of about 5,400.
  • Titles from describe(Class), test(fn) and .each: starDefault > title to default > title. A snapshot key, a JUnit name and a -t pattern with starDefault in it change with the title. Such a -t pattern matches nothing any more.
  • jest.fn(fn).name and mock(fn).getMockName(): "default" for every form in the table below. Bun 1.4.3 gives "". Main gives "starDefault" since bun test: Vitest compatibility #44833, which reads the raw name of the function (copyNameAndLength in src/jsc/bindings/JSMockFunction.cpp). jest.fn(fn).getMockName() is "jest.fn()" on main, whatever fn is.
  • Stack frames: at new default, was at new starDefault. A tool that groups errors by frame text sees a new group once.
  • The two errors that name a class declaration: Cannot call a class constructor default without |new|, Cannot return a non-object type in the constructor of a derived class default.
  • In the debugger, the frame of the constructor that JavaScriptCore makes for export default class {} has a scope with a variable default. A class with a written name shows its name there in the same way.

What gets the name, by form

form after this change
export default class {} default in frames, Bun.inspect, snapshots, both class errors, mock names, heap snapshots, the debugger, --cpu-prof
export default () => {}, (function () {}), (class {}) default in frames, Bun.inspect and mock names. The name is inferred, so a snapshot prints [Function] or [class], and the class errors have no name
export default function () {}, also function* and async function from a file the transpiler names them <file>_default first, as before (#34932). They get default where the text reaches JavaScriptCore as written: node:vm SourceTextModule, and a file that starts with // @bun
export default async () => { await x } an unnamed frame, before and after. With oven-sh/WebKit#820 the body gets the name

Not changed

  • A frame of a getter or setter, and a frame of a function with a displayName, still change text once .name is read (functionName in src/jsc/bindings/ErrorStackTrace.cpp). That is not about default exports.
  • The variable edge of the module binding in a heap snapshot is still starDefault, and so are three SyntaxErrors for a second export default declaration in one module. They show the binding, not the function.
  • Error.captureStackTrace(err, fn) matches frames by name as before. A check of the source range emptied the stack for return super.method(), so it is not here (see Error.captureStackTrace: keep frames when the callee frame was elided by a tail call #32136).
  • A bundle names the function <file>_default, as before.

Source changes

  • JSC__JSValue__getNameProperty: the deleted arm returned the raw name of the executable when JSFunction::name() was empty. With the new engine the two are the same string. bun:test: print a function's own name instead of its AsyncFunction / GeneratorFunction tag #38959 changes the same function, and the second of the two to land rebases.
  • finish_constructor_header in the ahead-of-time compiler: a class declaration with no name is export default class {}. It keeps HAS_EXPLICIT_NAME when the name is its own, as src/aot/lower/function/header.rs does for export default function () {}. Compiled code then gives the same mock names, class errors and snapshot text as plain Bun. Before, plain Bun said starDefault there and compiled code said nothing.
  • Neither adds work at run time. The cost of the engine change is in [JSC] Store the name default for an anonymous default export WebKit#950: +2 instructions for each function executable that is made, and name reads get cheaper.
  • Measured on release builds of main (8971677) and of this branch, linux x64, same toolchain. .text of bun: 108,951,913 to 108,951,337 bytes (-576). .rodata, .data and .bss do not change. require("typescript"): +56,613 user instructions of 814,854,823 (+0.007%, minimum of 9 interleaved rounds, control +7,940). After that load, no count in heapStats().objectTypeCounts differs (70 object types).
  • CI's size test (test/bundler/compile-aot-size.test.ts, red on main for its budget) reports the bytes that a trimmed --aot hello world maps: 25,953,252 here and 25,961,444 on main at the same commit (builds 136363 and 136333), 8,192 fewer.

Tests

  • test/js/bun/test/stack.test.ts: error.stack, CallSite#getFunctionName, the two class errors and Bun's error printer, for nine forms, before and after .name is read.
  • test/js/bun/util/inspect.test.js, test/js/bun/test/snapshot-tests/snapshots/moremore.test.ts: Bun.inspect and snapshot text.
  • test/js/bun/test/mock-fn.test.js, test/js/bun/test/describe.test.ts: mock names and describe(Class) titles.
  • test/js/node/v8/capture-stack-trace.test.js: Error.captureStackTrace with a resumed anonymous default function.
  • test/js/bun/util/v8-heap-snapshot.test.ts, test/cli/inspect/inspect.test.ts, test/cli/run/cpu-prof.test.ts, test/v8/v8.test.ts: both heap snapshot formats, the debugger, an inlined frame of the profiler, v8::Function::GetName.
  • test/cli/test/test-aot.test.ts, test/aot/ir/lower/classes/anonymous-class-names.ts, test/aot/parallel/errors/names-of-functions-behind-export-default: compiled code against plain Bun.
  • On a debug ASAN build of this branch (main cee9dd9) with the preview engine: the 14 new tests pass. In ten of the eleven bun test files 1,178 tests pass and 1 fails. In test/v8/v8.test.ts I ran the new test alone, and it passes.
  • The same objects linked against the engine that main pins (4d45755fed): the 14 new tests fail, and no other result changes.
  • The 1 failure is a matcher after .resolves or .rejects waits for a pending promise in test/cli/test/test-aot.test.ts, a test that main has. On a debug build it fails with both engines: plain bun test prints one more <anonymous> frame than the compiled run.
  • cargo test -p bun_aot --test ir_tests: the changed file passes. The programs under test/aot/parallel need a canary build, so CI runs them.
  • Two public traces with the old text: treetrum/amazon-kindle-bulk-downloader issue 148 and extractus/article-extractor issue 412 have at starDefault frames. Neither asks for another name.

no test proof · iteration 3 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/v8/v8.test.ts, test/js/bun/util/inspect.test.js, test/js/bun/test/describe.test.ts, test/cli/run/cpu-prof.test.ts

@robobun

robobun commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 3:18 AM PT - Nov 1st, 2026

❌ @robobun, your commit 9926993 has 41 failures in Build #136363 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 47502

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

bun-47502 --bun

@robobun

robobun commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status

JavaScriptCore binds an anonymous default export to the private name
`*default*`, whose text is `starDefault`. The tests hold each reader of a
function name to `default`: stack frames and call sites, `Bun.inspect`, mock
names, `describe(Class)` titles, the two heap snapshot formats, the debugger,
`--cpu-prof` and `v8::Function::GetName`.

The tests pass with the JavaScriptCore change that stores `default` in the
executable. `JSC__JSValue__getNameProperty` no longer reads the raw name of
the executable when `JSFunction::name()` is empty: the two are the same
string now.
JavaScriptCore now has the name `default` as the declared name of an
anonymous default class. The ahead-of-time compiler kept that class as one
with an inferred name, so compiled code left the name out of the two class
errors, of mock names and of snapshot text. `finish_constructor_header`
keeps `HAS_EXPLICIT_NAME` for the class, as `header.rs` does for the
function of `export default function () {}`.

More tests: snapshot text, the scope that holds the built-in constructor
under its name in the debugger, a source that is already transpiled, and
`bun test --aot` against `bun test`. The `--cpu-prof` test runs again
when the profile has no sample in the function it looks for.
@robobun
robobun force-pushed the robobun/2cdb9aa5/default-export-display-name branch from f18aaf0 to 9926993 Compare November 1, 2026 05:52

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