From 78ede51fcf6aa90ff5a156372fd69582a6ee5336 Mon Sep 17 00:00:00 2001 From: Dmitrii Troitskii Date: Sat, 7 Mar 2026 18:34:29 +0000 Subject: [PATCH] [compiler] preserve JSXText HTML entity references during whitespace normalization HTML entity references such as are decoded to their character value by Babel before the compiler sees the JSXText node. trimJsxText() normalises whitespace by checking for non-space/tab characters, so a node whose raw source was purely entity-encoded whitespace (e.g. ' ') would be trimmed to null and silently dropped from the output. Fix: when trimJsxText returns null, fall back to trimming the raw source text (exprPath.node.extra.raw). Because the raw form contains non-whitespace characters ('&', '#', digits, ';'), trimJsxText(' ') returns ' ' which is then emitted as a JSX string expression {" "} and decoded correctly by the standard JSX transform at runtime. Fixes #35971 --- .../src/HIR/BuildHIR.ts | 16 +++++++ .../jsx-html-entity-preserved.expect.md | 43 +++++++++++++++++++ .../compiler/jsx-html-entity-preserved.js | 10 +++++ 3 files changed, 69 insertions(+) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-html-entity-preserved.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-html-entity-preserved.js 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 8f44594c0031..04dab9bd64f2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts @@ -3516,6 +3516,22 @@ function lowerJsxElement( text = exprPath.node.value; } else { text = trimJsxText(exprPath.node.value); + /* + * If the decoded value trimmed to null, the text may still contain + * meaningful content that Babel decoded from HTML entity references + * (e.g. ` ` → space). In that case, trimming the decoded value + * discards the intentional whitespace. Preserve it by trimming the + * raw source instead so that the entity reference is retained in the + * output JSX and decoded correctly by the standard JSX transform. + */ + if (text === null) { + const rawValue = ( + exprPath.node.extra as {raw?: string} | undefined + )?.raw; + if (rawValue != null) { + text = trimJsxText(rawValue); + } + } } if (text === null) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-html-entity-preserved.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-html-entity-preserved.expect.md new file mode 100644 index 000000000000..c85df4b726d8 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-html-entity-preserved.expect.md @@ -0,0 +1,43 @@ + +## Input + +```javascript +// Ensure HTML entity references in JSX text (e.g. ) are preserved after +// compilation and not stripped as whitespace-only nodes. +function MyApp() { + return ( +
+ + hello world +
+ ); +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // Ensure HTML entity references in JSX text (e.g. ) are preserved after +// compilation and not stripped as whitespace-only nodes. +function MyApp() { + const $ = _c(1); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = ( +
+ {" "} + hello world +
+ ); + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-html-entity-preserved.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-html-entity-preserved.js new file mode 100644 index 000000000000..ba4f7d05ebe4 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-html-entity-preserved.js @@ -0,0 +1,10 @@ +// Ensure HTML entity references in JSX text (e.g. ) are preserved after +// compilation and not stripped as whitespace-only nodes. +function MyApp() { + return ( +
+ + hello world +
+ ); +}