Repository navigation
Description
Issue Category
Parser bug (@tsrx/core). A JSXText node's value differs from TSX's.
Decision
TSRX is a superset of TSX, so JSXText follows the JSX AST standard, as the specification already says. value has its character references decoded, and raw keeps the text as written. Printers print from raw. The specification's two sentences are right, and the parser changes.
Bug Description
The specification says twice that a JSXText value has its character references decoded:
- 4.3, template text: "Character references such as
"are decoded before the text value is stored." - The AST appendix: "Static text children decode JSX character references before the text value is stored."
The parser keeps them as written. For <p>a "b" & c</p>, both value and raw are a "b" & c, in a template, in a function, and at the top level.
The JSX parsers that build this AST decode value. acorn-typescript alone gives:
value: a "b" & c
raw: a "b" & c
The compilers rely on the undecoded value today: they write it into the output as JSX text, and the target's JSX compiler decodes the references there (see the compile test "preserves entities in JSX text children for JSX runtime decoding"). A tool that reads value as decoded text gets the references instead.
Reproduction Steps
At c4fa2588, run pnpm install --frozen-lockfile, then save this as repro.mjs in the repository root and run node repro.mjs:
import { parseModule } from './packages/tsrx/src/index.js';
// The first JSXText node in the tree.
function findText(node) {
if (!node || typeof node !== 'object') return;
if (node.type === 'JSXText') return node;
for (const [key, child] of Object.entries(node)) {
if (key === 'loc' || key === 'metadata') continue;
const found = findText(child);
if (found) return found;
}
}
const sources = {
template: `export function App() @{
<p>a "b" & c</p>
}`,
function: `export function App() {
return <p>a "b" & c</p>;
}`,
module: `export const p = <p>a "b" & c</p>;`,
};
for (const [name, source] of Object.entries(sources)) {
const text = findText(parseModule(source, 'App.tsrx'));
console.log(`${name}: value ${JSON.stringify(text.value)}, raw ${JSON.stringify(text.raw)}`);
}template: value "a "b" & c", raw "a "b" & c"
function: value "a "b" & c", raw "a "b" & c"
module: value "a "b" & c", raw "a "b" & c"
Expected: value "a \"b\" & c" in all three, with raw unchanged.
Cause
#parseTemplateRawTextbuildsvaluefrom the source characters, leaving out only comments. The text of plain-JSX elements gets the same undecodedvalue.- The
JSXTextprinter writesvalueback into the output as JSX text, which only works becausevalueisn't decoded.
Fix Direction
-
The parser decodes character references into
value, for template text and plain-JSX text alike, as acorn's JSX parser does.rawstays the text as written. A comment between children stays out of both, as today. -
Everything that prints JSX text prints from
rawinstead ofvalue, keeping fix(core): write a>in text as>, read it as text in containers, and print JSX text fromraw(#694, #693) #711's escaping of<,>,{and}. That covers:- core's
JSXTextprinter, for every target; - both formatters,
@tsrx/prettier-pluginandtsrx-prettier-plugin.
Printing from
rawkeeps references like as written, instead of turning them into characters. - core's
-
Find every other reader of
JSXText.valueand switch it to whichever it means:- the analyzer, the whitespace rules (
#shouldKeepTemplateTextNode), editor mappings (segments.js) and the language server; - the ESLint parser and plugin;
- the Ripple and Octane compilers, which parse with
@tsrx/core. File follow-up issues in those repositories for them.
- the analyzer, the whitespace rules (
-
PR fix(core): write a
>in text as>, read it as text in containers, and print JSX text fromraw(#694, #693) #711 ([Bug]: tsrx-core: Character references in an element in a spread argument, or in an unbraced attribute value in a container, compile decoded:{x}becomes an expression #693) currently setsvalue = rawfor plain-JSX text, which is the opposite of this decision. It should keep acorn's decodedvalueand printrawinstead, or leavevalueto this issue's fix. -
Tests:
- the parser gives decoded
valueand as-writtenrawin each place; - compiled output for every target keeps the references and renders the decoded text;
- both formatters keep the text as written.
- the parser gives decoded
-
tsrx specification on the website updated
Environment
origin/mainatc4fa2588,@tsrx/core0.4.0- Node 24.18.0, macOS 26.6.2
Additional Context
Found while fixing #693 and #694. This was first filed as a question for the website's specification. The user decided on 2026-09-25 that the specification is right, since TSRX is a superset of TSX.
Downstream
Both Ripple and Octane decode JSX text themselves, so a decoded value makes them decode it a second time: &lt; renders < instead of <. Neither repository's tests catch it. Checked on 2026-09-26 with #710's decoding simulated on top of #711:
- Ripple has to drop its own decoder in the same change that bumps to the core with this fix: Upgrading to the next @tsrx/core: template text parsing (tsrx-org/tsrx#711), spread-argument markup on the server, and a decoded
JSXText.value(tsrx-org/tsrx#710) Ripple-TS/ripple#1520 (section "Coming later"), with the TS-view part in [Bug]: TS view prints template text decoded, so>,<,>and{x}reach TypeScript bare Ripple-TS/ripple#1519. - Octane parses with
@tsrx/oxcon Node and with@tsrx/corein its browser compiler.@tsrx/oxchas to decodevaluethe same way, or the two outputs disagree: Character references in JSX attribute strings aren't decoded intovalue, and text will need the same after tsrx-org/tsrx#710 oxc#111. Octane then decodes fromrawonce: Upgrading to the next @tsrx/core (0.5.0): template textraw,>in text, and the coming decodedvalueoctanejs/octane#1330, and JSX attribute strings are decoded twice when @tsrx/core parses a file, so&lt;renders<octanejs/octane#1327 for attribute strings, which already decode twice today. - Release: make this fix's changeset a
minor, so that^0.5.0ranges don't pick it up in a lockfile refresh before Ripple and Octane are ready (chore(release): release the next @tsrx/core as a minor (0.5.0) #732 makes the next release0.5.0).
Activity
[-][Bug]: tsrx-website: The specification says JSXText values decode character references, but the parser keeps them as written[/-][+][Bug]: tsrx-core: JSXText `value` keeps character references undecoded, unlike the JSX AST standard[/+]fix(core): print JSX text from `raw`, and keep acorn's decoded `value` (
fix(core): write a `>` in text as `>`, read it as text in containe…
fix(core): decode character references in a JSXText `value`, and read…