Print JSX component tags with the component's inferred name or displayName
RO
3mo ago+111+111+012345678901234567890123456789-58-58-01234567890123456789
Problem
Bun.inspect/console.logof a JSX element prints<NoName />for the most common kinds of components:const Bar = () => nullandconst Bar = function () {}(name inferred from the binding),const Bar = class {}, and anything named throughdisplayName.console.log(Bar)prints[Function: Bar]for the same values, so the two disagree. (Same symptom as #4104; thefunction App() {}case reported there has since been fixed, the cases above were not.)- An async component (
async function Page() {}) prints<AsyncFunction />. - The test runner's formatter has the same bug, so
expect(<Bar />).toMatchSnapshot()stores<NoName />and failure diffs show<NoName />. - Cause: both JSX printers (
print_jsxinsrc/jsc/ConsoleObject.rs, theTag::JSXarm insrc/runtime/test_runner/pretty_format.rs) look the tag name up withJSValue::get_name_property(JSC__JSValue__getNameProperty,src/jsc/bindings/bindings.cpp). That binding readsSymbol.toStringTagfirst (which is howAsyncFunction.prototype[Symbol.toStringTag]wins over the function's own name) and then only the explicit name from the function's executable; it never consults the inferred name ordisplayName. The function and class printers were moved toJSValue::get_name(getCalculatedDisplayName) in #6612; the JSX printers were not.
Fix
- Adds
jsx_component_tag_namenext to the console formatter and uses it from both JSX printers. Lookup order: a non-empty stringdisplayNameproperty, elseget_name(the lookup[Function: x]/[class x]use, which falls back toSymbol.toStringTagwhen there is no name), elseNoNameas before. - Why this is correct:
displayName || nameis the rule React itself uses to name a component in its warnings and devtools, and the rule jest's React element serializer uses for function components. Taking thenamehalf fromget_namemeans that for a function or class component the tag is the same stringconsole.logprints for the component value ([Function: x]/[class x]), which is the consistency the report asks for. displayNameis read as a property rather than left togetCalculatedDisplayName(which already honors it for functions) becauseReact.memo()/React.forwardRef()results are plain objects, andButton.displayName = "Button"is the only way they get a name;get_namereturns nothing for a non-function.- Each tag-name branch now yields the
ZigStringSlicedirectly instead of going through aZigStringfirst; the string / symbol / primitive branches produce the same bytes as before (fragments still print as<>...</>). - Behavior that changes besides the cases above, for the record: a component that has both a name and a
Symbol.toStringTagnow prints its name (before, the tag won; async and generator functions are the practical instance). A function whosedisplayNameis the empty string prints<NoName />(before: its function name), becausegetCalculatedDisplayNamereturns the emptydisplayNameas the name; this is the same thingconsole.logalready prints for such a function ([Function]), so the two printers still agree, and it is not a pattern React code uses. - Behavior deliberately unchanged: a
memo/forwardRefobject without adisplayNamestill prints<NoName />(naming it after the wrapped function is a separate feature), an objecttypewith aSymbol.toStringTagstill prints the tag, and a component with no name at all still prints<NoName />. - Not changed:
get_name_propertyitself.pretty_format.rsstill uses it to print[Function]for inferred-name functions in snapshots, which #6612 deliberately kept so existing.snapfiles do not change; this PR only changes the JSX tag, which wasNoNamebefore and therefore not something a snapshot could usefully depend on. - Verified:
test/js/bun/util/inspect.test.js,describe("jsx component tag name"): arrow function, anonymous function expression, function declaration,displayNameover an explicit function name, class, anonymous class expression, async function,displayNameon a forwardRef-style object, a memo-style object withoutdisplayName(stillNoName), a function with no name at all (stillNoName), the closing tag of an element with children, and the[Function: x]/[class x]output for the same values. Each element case assertsBun.inspectand, throughtoMatchInlineSnapshot, the test runner's printer. 7 of the 12 tests fail on the released bun 1.4.0 (<NoName />/<AsyncFunction />/<NoName>child</NoName>); all pass with this change. The previous"jsx with anon component"test, which asserted<NoName />forconst Foo = () => ..., is replaced by the arrow function case.- The rest of
inspect.test.js,bundler_jsx.test.ts,run-eval.test.ts(string tags and fragments through the restructured branches),snapshot-tests/snapshots/snapshot.test.tsandconsole-table.test.tspass with the debug build. - The test runner formatter's output for function values themselves (
[Function],[class],[Function: AsyncFunction]) is byte-identical before and after, per the note above.
Background
- React element: the object JSX compiles to,
{ $$typeof: Symbol.for("react.element"), type, props, key }.typeis a string for host elements ("div") and a function, class, or (formemo/forwardRef) an object for components. Both formatters detect it by$$typeofand print it back as JSX, usingtype's name as the tag. - Inferred function name:
const Bar = () => nullgives the arrowBar.name === "Bar", but JSC stores that as the executable'secmaName, separate from the explicit name offunction Bar() {}.JSFunction::name()returns only the explicit one;getCalculatedDisplayName(whatJSValue::get_namewraps) checksdisplayName, then the explicit name, then the inferred one. displayName: a plain property React reads to name a component in warnings and devtools. React honors it on functions and onmemo/forwardRefobjects alike.JSValue::get_namereturns abun_core::Stringholding a +1 ref;OwnedStringreleases it on scope exit andto_utf8()takes its own ref (or makes a copy), which is why the returned slice can outlive theOwnedString.
Before / after on the cases from the report
$ bun -e '
const Bar = () => null;
console.log(Bar);
console.log(Bun.inspect({ $$typeof: Symbol.for("react.element"), type: Bar, props: {} }));
const W = function () {}; W.displayName = "Pretty";
console.log(W);
console.log(Bun.inspect({ $$typeof: Symbol.for("react.element"), type: W, props: {} }));
async function Page() {}
console.log(Bun.inspect({ $$typeof: Symbol.for("react.element"), type: Page, props: {} }));
'
# bun 1.4.0 # this branch
[Function: Bar] [Function: Bar]
<NoName /> <Bar />
[Function: Pretty] [Function: Pretty]
<NoName /> <Pretty />
<AsyncFunction /> <Page />