From 2b2acd9b3fa8930f037779fc9ccb7c91cfb3448b Mon Sep 17 00:00:00 2001 From: Ousama Ben Younes Date: Sat, 6 Jun 2026 15:48:18 +0000 Subject: [PATCH 1/2] fix(compiler): treat underscore-prefixed JSX tags as components The React Compiler was using a regex check `/^[A-Z]/` to determine whether a JSX tag is a component or a builtin (host) element. This meant that tags prefixed with underscore (e.g., `<_Bar>`) were incorrectly treated as builtin elements, even though the JSX specification states that only tags starting with a lowercase letter should be treated as host elements. This change flips the condition to check for lowercase first letters (`/^[a-z]/`) for builtin tags, treating everything else (uppercase, underscore-prefixed, dollar-prefixed, etc.) as component references, which matches the React JSX transform behavior. Fixes #36601 --- .../src/HIR/BuildHIR.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts index 452aa0ce329d..75e2db0266f2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts @@ -3409,19 +3409,19 @@ function lowerJsxElementName( const exprLoc = exprNode.loc ?? GeneratedSource; if (exprPath.isJSXIdentifier()) { const tag: string = exprPath.node.name; - if (tag.match(/^[A-Z]/)) { + if (tag.match(/^[a-z]/)) { + return { + kind: 'BuiltinTag', + name: tag, + loc: exprLoc, + }; + } else { const kind = getLoadKind(builder, exprPath); return lowerValueToTemporary(builder, { kind: kind, place: lowerIdentifier(builder, exprPath), loc: exprLoc, }); - } else { - return { - kind: 'BuiltinTag', - name: tag, - loc: exprLoc, - }; } } else if (exprPath.isJSXMemberExpression()) { return lowerJsxMemberExpression(builder, exprPath); From f3a7ec8c634aab198b81f30f9a7d0952d1964347 Mon Sep 17 00:00:00 2001 From: Ousama Ben Younes Date: Sat, 6 Jun 2026 22:34:35 +0000 Subject: [PATCH 2/2] [compiler] Add fixture for underscore-prefixed JSX tags as components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fix shipped without a fixture. Adds a snapshot test exercising the #36601 repro: a `<_Bar>` JSX tag must be lowered as a component reference, not a BuiltinTag. RED (fix reverted to `/^[A-Z]/`): the fixture fails — Forget output throws "_Bar is not defined" vs the non-Forget `
{"children":"ok"}
`, because `_Bar` is emitted as a host string tag. GREEN (fix `/^[a-z]/`): `_Bar` is lowered as a reactive component dependency; snap passes (1 Tests, 1 Passed). Co-Authored-By: Claude Opus 4.8 (1M context) --- ...rscore-prefixed-tag-is-component.expect.md | 58 +++++++++++++++++++ ...x-underscore-prefixed-tag-is-component.tsx | 16 +++++ 2 files changed, 74 insertions(+) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-underscore-prefixed-tag-is-component.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-underscore-prefixed-tag-is-component.tsx diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-underscore-prefixed-tag-is-component.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-underscore-prefixed-tag-is-component.expect.md new file mode 100644 index 000000000000..b4e7b84bacdf --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-underscore-prefixed-tag-is-component.expect.md @@ -0,0 +1,58 @@ + +## Input + +```javascript +import {Stringify} from 'shared-runtime'; + +// Repro for #36601: an underscore-prefixed JSX tag (`<_Bar>`) is a component +// reference, not a host/builtin element. Before the fix, `lowerJsxElementName` +// used `/^[A-Z]/` to detect components, so `_Bar` (which is not A-Z) was lowered +// as a BuiltinTag — emitting the literal string tag "_Bar" instead of loading +// the `_Bar` binding. The compiled output below must reference the `_Bar` +// identifier (component), not a string tag. +function Foo({_Bar}: {_Bar: React.ComponentType<{children: React.ReactNode}>}) { + return <_Bar>ok; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{_Bar: Stringify}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { Stringify } from "shared-runtime"; + +// Repro for #36601: an underscore-prefixed JSX tag (`<_Bar>`) is a component +// reference, not a host/builtin element. Before the fix, `lowerJsxElementName` +// used `/^[A-Z]/` to detect components, so `_Bar` (which is not A-Z) was lowered +// as a BuiltinTag — emitting the literal string tag "_Bar" instead of loading +// the `_Bar` binding. The compiled output below must reference the `_Bar` +// identifier (component), not a string tag. +function Foo(t0) { + const $ = _c(2); + const { _Bar } = t0; + let t1; + if ($[0] !== _Bar) { + t1 = <_Bar>ok; + $[0] = _Bar; + $[1] = t1; + } else { + t1 = $[1]; + } + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{ _Bar: Stringify }], +}; + +``` + +### Eval output +(kind: ok)
{"children":"ok"}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-underscore-prefixed-tag-is-component.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-underscore-prefixed-tag-is-component.tsx new file mode 100644 index 000000000000..9a81d99d1838 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-underscore-prefixed-tag-is-component.tsx @@ -0,0 +1,16 @@ +import {Stringify} from 'shared-runtime'; + +// Repro for #36601: an underscore-prefixed JSX tag (`<_Bar>`) is a component +// reference, not a host/builtin element. Before the fix, `lowerJsxElementName` +// used `/^[A-Z]/` to detect components, so `_Bar` (which is not A-Z) was lowered +// as a BuiltinTag — emitting the literal string tag "_Bar" instead of loading +// the `_Bar` binding. The compiled output below must reference the `_Bar` +// identifier (component), not a string tag. +function Foo({_Bar}: {_Bar: React.ComponentType<{children: React.ReactNode}>}) { + return <_Bar>ok; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{_Bar: Stringify}], +};