Repository navigation
fix(tsrx-prettier-plugin): keep a comment in a method's type parameters, and what an element renders around comments between its children (#630, #639) - #596
Conversation
The plan, decisions, and state carried over from #511 are in the PR description. Work on the plugin lands here as commits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rameters (#630) The adapter moves a class method's type parameters onto its function, as typescript-estree has them, and now also starts the function at them, so a comment inside them belongs to the function instead of the method's name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t is between its children (#639) A comment adds nothing to the text around it: the whitespace on its two sides is one run (#615), which renders a space without a line break, and nothing with one beside an element or the start or end of the children. The adapter now describes each comment group's run (`describeCommentRuns`), including a `{" "}` beside it, and `printCommentChild` prints a run that renders the same: - a significant space is JSX whitespace, which prints `{" "}` where a line breaks, at the start or end of the children when the element breaks, and written out before a line comment, which ends its line; - a line break at the start or end of the children breaks the element when a space in the run would otherwise render; - whitespace and `{" "}`s in the run are printed by the comments; - a line comment after another comment starts its own line, since the parser reads `/* c */ // c` as text. Also fixes `isEmptyDoc`, which judged any doc starting with an empty string empty, and lays out children with such a comment with `fill`, like text. Checked on every sequence of up to three of text, an element, an expression, `{" "}`, a block comment, and a line comment, with no separator, a space, or a line break between them (12,546 cases): each renders the same after formatting, and a second format changes nothing. Before, 137 of the 558 two-part cases didn't. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Comment runs can double spaces
- A comment run that ends the children no longer keeps the source space before the comment when it also prints that run's one space as {" "} after it.
Or push these changes by commenting:
@cursor push bebfc9c9cf
Preview (bebfc9c9cf)
diff --git a/packages/tsrx-prettier-plugin/src/jsx.js b/packages/tsrx-prettier-plugin/src/jsx.js
--- a/packages/tsrx-prettier-plugin/src/jsx.js
+++ b/packages/tsrx-prettier-plugin/src/jsx.js
@@ -502,7 +502,10 @@
// breaks, so the space goes there, where it prints as `{" "}`.
if (run.after === 'boundary') {
if (outerAfter) after = whitespace;
- if (outerBefore && run.before === 'boundary') before = '';
+ // That `{" "}` is the run's one space. A literal space before
+ // the comment would stay on its line and render again once
+ // the comment is gone.
+ if (outerBefore && (run.before === 'boundary' || before === ' ')) before = '';
} else if (outerBefore) {
before = whitespace;
}
diff --git a/packages/tsrx-prettier-plugin/tests/tsrx.test.js b/packages/tsrx-prettier-plugin/tests/tsrx.test.js
--- a/packages/tsrx-prettier-plugin/tests/tsrx.test.js
+++ b/packages/tsrx-prettier-plugin/tests/tsrx.test.js
@@ -611,7 +611,7 @@
`export function App() @{
<>
<div>
- <b>t</b> /* c */{" "}
+ <b>t</b>/* c */{" "}
</div>
<div>
{" "}You can send follow-ups to the cloud agent here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
There are 3 total unresolved issues (including 1 from previous review).
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Comment run drops explicit space
- A {" "} in a comment run is now recorded on the side it sits and printed by the comment, so neighbors keep the space that expression rendered.
- ✅ Fixed: Skipped space expression drops comments
- A {" "} that still has a comment is no longer treated as a skippable gap, so that expression and its comment are printed.
Or push these changes by commenting:
@cursor push 62876ae23c
Preview (62876ae23c)
diff --git a/packages/tsrx-prettier-plugin/src/jsx.js b/packages/tsrx-prettier-plugin/src/jsx.js
--- a/packages/tsrx-prettier-plugin/src/jsx.js
+++ b/packages/tsrx-prettier-plugin/src/jsx.js
@@ -364,9 +364,11 @@
return;
}
// TSRX: the comment beside a `{" "}` prints it, and whitespace in a
- // comment's run keeps what the comment printed after itself.
+ // comment's run keeps what the comment printed after itself. An
+ // expression that still has comments (`{" " /* note */}`) prints itself:
+ // skipping it would drop those comments, and its space with them.
if (
- node.tsrxCommentSpace ||
+ (node.tsrxCommentSpace && !node.expression?.comments?.length && !node.comments?.length) ||
(node.tsrxInCommentRun && node.type === 'JSXText' && !/\n/u.test(getRaw(node)))
) {
return;
@@ -482,21 +484,21 @@
? ' '
: '';
}
- if (
- run.space &&
- !run.lineBreak &&
- run.lineComment &&
- !(run.before === 'text' && run.after === 'text')
- ) {
+ // A `{" "}` renders a space without being a space or tab next to the
+ // comment, so `spaceBefore` / `spaceAfter` miss it.
+ const explicitBefore = outerBefore && run.explicitBefore;
+ const explicitAfter = outerAfter && run.explicitAfter;
+ if (run.space && !run.lineBreak && run.lineComment) {
// A line comment puts a line break in the run, so the space it renders
- // is written out, before the comments.
+ // is written out, before the comments, between two words too. A plain
+ // space there would make `//` text (`hello// c`).
if (outerBefore) before = rawWhitespace;
if (outerAfter && !lineAfter) after = comment.spaceAfter ? ' ' : '';
} else if (run.space && !run.lineBreak) {
if (run.before === 'text' && run.after === 'text') {
// Between two words, a line break renders the space too.
- if (outerBefore && comment.spaceBefore) before = line;
- if (outerAfter && comment.spaceAfter) after = line;
+ if (outerBefore && (comment.spaceBefore || explicitBefore)) before = line;
+ if (outerAfter && (comment.spaceAfter || explicitAfter)) after = line;
} else if (run.after === 'boundary' || run.before === 'boundary') {
// The start or end of the children has a line break when the element
// breaks, so the space goes there, where it prints as `{" "}`.
@@ -508,9 +510,13 @@
}
} else {
// The space stays on its side; a line break there prints `{" "}`.
- const side = outerAfter && comment.spaceAfter ? 'after' : 'before';
+ const side = outerAfter && (comment.spaceAfter || explicitAfter) ? 'after' : 'before';
if (side === 'after') after = whitespace;
- else if (outerBefore && comment.spaceBefore) before = whitespace;
+ else if (outerBefore && (comment.spaceBefore || explicitBefore)) before = whitespace;
+ // The other side's `{" "}` is its own space.
+ if (side === 'after' && explicitBefore && !comment.spaceBefore) before = whitespace;
+ if (side === 'before' && explicitAfter && !comment.spaceAfter && !lineAfter)
+ after = whitespace;
}
} else if (run.lineBreak) {
// A space beside the start or end of the children renders nothing here.
diff --git a/packages/tsrx-prettier-plugin/src/parse.js b/packages/tsrx-prettier-plugin/src/parse.js
--- a/packages/tsrx-prettier-plugin/src/parse.js
+++ b/packages/tsrx-prettier-plugin/src/parse.js
@@ -349,11 +349,18 @@
describeCommentRuns(children, start, end) {
const isBlank = (/** @type {Node | undefined} */ child) =>
child?.type === 'JSXText' && /^[ \t\r\n]*$/u.test(child.value);
+ // A comment inside `{" " /* note */}` belongs to that expression. It is not
+ // a gap the comment run reprints: printing the expression keeps the space
+ // and the comment (`isJsxWhitespaceExpression` leaves it as an expression).
+ const hasInnerComment = (/** @type {Node} */ child) =>
+ this.comments.some((comment) => comment.start > child.start && comment.end < child.end);
// `{" "}` renders a space whatever is around it.
const isSpace = (/** @type {Node | undefined} */ child) =>
- child?.type === 'JSXExpressionContainer' &&
+ !!child &&
+ child.type === 'JSXExpressionContainer' &&
child.expression.type === 'Literal' &&
- child.expression.value === ' ';
+ child.expression.value === ' ' &&
+ !hasInnerComment(child);
const isGap = (/** @type {Node | undefined} */ child) => isBlank(child) || isSpace(child);
const kind = (/** @type {Node | undefined} */ child) =>
!child ? 'boundary' : child.type === 'JSXText' ? 'text' : 'node';
@@ -375,13 +382,19 @@
const before = children[previous];
const after = children[following];
// The whitespace and `{" "}`s beside the comments are the run, which the
- // comments print.
+ // comments print. A `{" "}` is not whitespace beside the comment, so
+ // record which outer side it is on: `spaceBefore` / `spaceAfter` only
+ // see spaces and tabs touching the comment.
let explicitSpace = false;
+ let explicitBefore = false;
+ let explicitAfter = false;
for (let index = previous + 1; index < following; index++) {
children[index].tsrxInCommentRun = true;
if (isSpace(children[index])) {
children[index].tsrxCommentSpace = true;
explicitSpace = true;
+ if (index < first) explicitBefore = true;
+ else if (index > last) explicitAfter = true;
}
}
const runStart = !before
@@ -415,6 +428,9 @@
lineBreak: lineBreak && !explicitSpace,
// Whether a line comment in the group puts a line break in the run.
lineComment: group.some((comment) => comment.commentType === 'Line'),
+ // A `{" "}` before the first comment or after the last one.
+ explicitBefore,
+ explicitAfter,
};
for (const [index, comment] of group.entries()) {
// Between two comments, whatever the source had there (whitespace or
diff --git a/packages/tsrx-prettier-plugin/tests/tsrx.test.js b/packages/tsrx-prettier-plugin/tests/tsrx.test.js
--- a/packages/tsrx-prettier-plugin/tests/tsrx.test.js
+++ b/packages/tsrx-prettier-plugin/tests/tsrx.test.js
@@ -707,6 +707,46 @@
);
});
+ // A `{" "}` is the space in the run, but it is not a space or tab touching
+ // the comment, and a comment inside it is that expression's.
+ test('a {" "} beside a comment keeps its space, and a comment inside it', async () => {
+ await expectFormat(
+ `export function App() @{
+ <>
+ <div>hello{" "}/* c */world</div>
+ <div>hello/* c */{" "}world</div>
+ <div><b />{" "}/* c */<i /></div>
+ <div><b />/* c */{" "}<i /></div>
+ <div>hello{" "}// c
+ world</div>
+ <div>hello{" " /* note */}/* c */world</div>
+ <div>hello/* c */{" " /* note */}world</div>
+ <div>{/* note */ " "}/* c */x</div>
+ </>
+}`,
+ `export function App() @{
+ <>
+ <div>hello /* c */world</div>
+ <div>hello/* c */ world</div>
+ <div>
+ <b /> /* c */<i />
+ </div>
+ <div>
+ <b />/* c */ <i />
+ </div>
+ <div>
+ hello{" "}// c
+ world
+ </div>
+ <div>hello{" " /* note */}/* c */world</div>
+ <div>hello/* c */{" " /* note */}world</div>
+ <div>{/* note */ " "}/* c */x</div>
+ </>
+}
+`,
+ );
+ });
+
test('in a fragment, and a JSDoc-style block comment is re-indented', async () => {
await expectFormat(
`const a = <>You can send follow-ups to the cloud agent here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
There are 4 total unresolved issues (including 3 from previous reviews).
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Comments inside space expressions vanish
- Space expressions that contain comments are no longer treated as comment-run whitespace, so those comments are printed with the expression.
Or push these changes by commenting:
@cursor push 6fc77c0238
Preview (6fc77c0238)
diff --git a/.changeset/prettier-space-expression-comments.md b/.changeset/prettier-space-expression-comments.md
new file mode 100644
--- /dev/null
+++ b/.changeset/prettier-space-expression-comments.md
@@ -1,0 +1,7 @@
+---
+'@tsrx/prettier-plugin': patch
+---
+
+A comment inside `{" "}` stays when that expression is beside a comment
+between JSX children. It was treated as the comment run's space, so the
+comment was dropped.
diff --git a/packages/tsrx-prettier-plugin/src/parse.js b/packages/tsrx-prettier-plugin/src/parse.js
--- a/packages/tsrx-prettier-plugin/src/parse.js
+++ b/packages/tsrx-prettier-plugin/src/parse.js
@@ -363,11 +363,21 @@
describeCommentRuns(children, start, end) {
const isBlank = (/** @type {Node | undefined} */ child) =>
child?.type === 'JSXText' && /^[ \t\r\n]*$/u.test(child.value);
- // `{" "}` renders a space whatever is around it.
- const isSpace = (/** @type {Node | undefined} */ child) =>
- child?.type === 'JSXExpressionContainer' &&
- child.expression.type === 'Literal' &&
- child.expression.value === ' ';
+ // `{" "}` renders a space whatever is around it. A comment inside the
+ // braces belongs to that expression: `isJsxWhitespaceExpression` keeps
+ // the node once Prettier reattaches the comment, so it is a child, not
+ // whitespace in the run. (The parser has already cleared `comments`.)
+ const isSpace = (/** @type {Node | undefined} */ child) => {
+ if (
+ child?.type !== 'JSXExpressionContainer' ||
+ child.expression.type !== 'Literal' ||
+ child.expression.value !== ' '
+ ) {
+ return false;
+ }
+ const { start, end } = child;
+ return !this.comments.some((comment) => comment.start > start && comment.end < end);
+ };
const isGap = (/** @type {Node | undefined} */ child) => isBlank(child) || isSpace(child);
const kind = (/** @type {Node | undefined} */ child) =>
!child ? 'boundary' : child.type === 'JSXText' ? 'text' : 'node';
diff --git a/packages/tsrx-prettier-plugin/tests/tsrx.test.js b/packages/tsrx-prettier-plugin/tests/tsrx.test.js
--- a/packages/tsrx-prettier-plugin/tests/tsrx.test.js
+++ b/packages/tsrx-prettier-plugin/tests/tsrx.test.js
@@ -725,6 +725,33 @@
);
});
+ // `{" "}` with a comment inside the braces is an expression, not the space a
+ // comment run renders. The comment has to stay with the expression.
+ test('a comment inside {" "} stays when the expression is beside a child comment', async () => {
+ await expectFormat(
+ `const a = <div>/* c */ {/* inner */ " "}</div>;
+const b = <div>/* c */ {" " /* inner */}</div>;
+const c = <div>{/* inner */ " "} /* c */</div>;
+const d = (
+ <div>
+ /* c */
+ {/* keep */ " "}
+ </div>
+);
+`,
+ `const a = <div> /* c */ {/* inner */ " "}</div>;
+const b = <div> /* c */ {" " /* inner */}</div>;
+const c = <div>{/* inner */ " "} /* c */ </div>;
+const d = (
+ <div>
+ /* c */
+ {/* keep */ " "}
+ </div>
+);
+`,
+ );
+ });
+
test('in a fragment, and a JSDoc-style block comment is re-indented', async () => {
await expectFormat(
`const a = <>You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 5bd12c4. Configure here.
…nders, and a {" "} with its own comment (#639)
- A `{" "}` in a comment's run counts as a space on its side of the comments,
so it prints as the run's space instead of being dropped.
- Where the comments keep a line break inside the run, the run's space is
written out as `{" "}`, since a plain space next to the line break is trimmed.
- A line comment after text starts its own line (on the text's line it is
text) and always ends its line.
- The space around a group of comments goes on one side, chosen once, so a
narrow line breaks in one place and the next format is stable.
- A `{" "}` with a comment inside is an ordinary child that prints its comment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>


Note
The plan moved to #851 (2026-09-26). This PR now only carries the #630 and #639 fixes, plus the fixes for Bugbot's review of #639 (de0450d). Keep #851's description current, not this one.
What this PR is
This is the working PR for
tsrx-prettier-plugin, the candidate successor to@tsrx/prettier-plugin, continued from #511. The previous working PR, #511, merged Phase 1. This description carries its plan over: the decisions, the state, and everything needed to pick the work up, including by another agent.It started with an empty commit, because a PR needs one. Commits so far:
describeCommentRuns), andprintCommentChildprints a run that renders the same: JSX whitespace ({" "}where a line breaks) for a significant space, and a forced break where the start or end of the children needs its line break.isEmptyDocinsrc/jsx.js, which judged any doc starting with""empty.{" "}, a block comment and a line comment, with no separator, a space, or a line break between them (12,546 cases): each renders the same after formatting, and a second format changes nothing. Before, 137 of the 558 two-part cases didn't.{" "}beside a comment is part of the comment's run, and its space printed nowhere:<p>one{" "}/* c */two</p>became<p>one/* c */two</p>. It now counts as a space on its side of the comments. Where the comments keep a line break inside the run, the space is written out as{" "}, because JSX trims a plain space next to a line break.{" "}with a comment inside ({/* d */ " "}) was skipped as part of the run, so its comment crashed the formatter (Comment "d" was not printed). It's now an ordinary child.<p>a b</p>prints<p>a b</p>).{" "}, block and line comments,{" "}with a comment) at widths 80 and 12, 231,000 formats. No case that passed before fails. What's left, [Bug]: tsrx-prettier-plugin: Text starting with // is moved to the start of a line, where it becomes a comment #849 (//text moved to the start of a line becomes a comment) and [Bug]: tsrx-prettier-plugin: A block comment after text never moves to the next line, so the next format breaks the braces before it #850 (a block comment after text never moves to the next line),mainhas too.Closes #630, closes #639.
Background
@tsrx/prettier-pluginprints every JavaScript and TypeScript node itself (over 13,000 lines), and it kept drifting from Prettier: dozens of open "Formatter … like Prettier" issues.tsrx-prettier-plugin(packages/tsrx-prettier-plugin, private, not published) instead reuses Prettier's own printer (prettier/plugins/estree) and prints only the TSRX syntax itself. It is tested against Prettier's own format tests. The package README has the design.usingdeclarations, and new regex syntax because it parses as ES2022 #414), fix(core): continue an expression after an element that starts a statement, and read type parameters on the line after a declaration name (#416, #419) #471 ([Bug]: tsrx-core: Parser rejects or splits off an operator after an element that starts a statement #416, [Bug]: tsrx-core: Parser rejects type parameters that start on the line after a declaration name #419), fix(core): parse JSX spread children and report them as unsupported (#417) #495 ([Bug]: tsrx-core: Parser rejects JSX spread children (<div>{...children}</div>) #417), fix(core): stop an @switch arm body at the end of the input (#492) #497 ([Bug]: tsrx-core: Parser hangs when the input ends inside an @switch case body #492), fix(core): start an element at its<, and continue an element statement across a line break (#467, #496) #500 ([Bug]: tsrx-core: An element or fragment with whitespace or a comment after<starts inside the gap #467, [Bug]: tsrx-core: Parser ends an element statement before an operator on the next line, where TSX continues the expression #496);staticbefore a line break as a modifier, a generic call signature first in an interface, a class named after a contextual keyword, andassertafter an import (#439, #420, #421, #423) #551 ([Bug]: tsrx-core: Parser readsstaticbefore a line break as a field name, making static members instance members #439, [Bug]: tsrx-core: Parser rejects a generic call signature as the first member of an interface #420, [Bug]: tsrx-core: Parser rejects classes named after TypeScript contextual keywords (class global {},class abstract {}) #421, [Bug]: tsrx-core: Parser readsassert (…)on the line after an import as import assertions #423), fix(core): parse import attributes in import types (#422) #555 ([Bug]: tsrx-core: Parser rejects import attributes in import types (import("foo", { with: { type: "json" } })) #422), fix(core): collect type-check-level errors in collect mode instead of throwing (#415) #558 ([Bug]: tsrx-core: Parser throws on mistakes that TypeScript reports only as type-check diagnostics, even withcollect: true#415), fix(core): report '}' expected for a missing closing brace (#583) #590 ([Bug]: tsrx-core: A missing}is reported asUnexpected tokeninstead of'}' expected, and an unclosed directive body at the end of the input parses #583).Current state (
mainat1af65cc6, after #511)tests/prettier-known-failures.json)tests/prettier-overrides.js)await(…), 1 line-starting<after a callee (decision 18).tsrxfilesprettier/standaloneDecisions so far
The user made these decisions during #409, #511 and the parser PRs. Keep to them unless the user says otherwise.
Prettier parity. Output is exactly what Prettier prints with its
typescriptparser for the same TSX. TSRX differs only where its syntax does. Differences from Prettier are bugs, even where@tsrx/prettier-plugindid something else on purpose.JSX layout follows Prettier: parentheses around multi-line JSX, and a forced break for an element with more than one attribute plus children. Templates format like the same JSX in a regular function.
TSRX values lay out like JSX. A directive (
@if/@for/@switch/@try), or a@{ … }block used as a value, is wrapped in(…)where it's assigned, returned, thrown, or an arrow body, with its comments inside the parentheses. A@{ … }function or arrow body stays on its line. This matches@tsrx/prettier-pluginonmain. The one deliberate difference: a leading line comment before a directive in an assignment (const y = // c) prints the way Prettier prints a JSX element there.Comments between JSX children are comments (
//and/* */). TSX reads them as text. They keep their line.A
.tsrxfile is a strict-mode ES module and TSX-like. A lone arrow type parameter is written<T,>. Namespaced tags aren't supported. Prettier cases that aren't valid TSX, or not valid in a strict module, are left out at import.JSX spread children (
{...items}) parse, but are unsupported on every target. Core's analyzer reportstsrx-jsx-spread-child: fatal in a compile, collected in editor mode (fix(core): parse JSX spread children and report them as unsupported (#417) #495).An operator on the next line continues an element statement, as in TSX ([Bug]: tsrx-core: Parser ends an element statement before an operator on the next line, where TSX continues the expression #496). A
<that starts a line is the next element (the TSRX rule), except where only type parameters can follow a name ([Bug]: tsrx-core: Parser rejects type parameters that start on the line after a declaration name #419).Proposal-only syntax isn't supported until it becomes JavaScript. Its Prettier cases are skipped with a reason, and the issue is closed as not planned ([Bug]: tsrx-core: Parser rejects destructuring of private fields (
const { #x: x } = this) #424).@tsrx/prettier-plugin's tests aren't the expected output for this plugin. They are still being changed. For now this plugin isn't tested against them at all (Phase 2 is on hold). When it resumes, differences are review items, not failures.<script>bodies are formatted only when they hold JavaScript or TypeScript (notype,module, or a JS/TS type), with this plugin's own parser. Anything else is kept as written.Markup the parser had to recover (an unclosed or mismatched tag) is a formatting error, not guessed output. The same goes for a collected mistake that Prettier's
typescriptparser rejects and that would leave code out of the tree: a disallowed modifier, which the tree drops, and a declaration list with no declarator (REJECTED_MISTAKESinsrc/parse.js, fix(core): collect more checker-level mistakes, report a disallowed modifier's message, and accept an optional binding pattern in an overload (#587, #588, #550, #557) #618). Mistakes that only TypeScript's checker reports still format (decision 14).No switch until the user is absolutely sure. Whether this plugin replaces
@tsrx/prettier-plugin, or ships separately astsrx-prettier-plugin, is one of the last decisions. Until then, nothing else in the repository moves to it, and the old plugin stays the one that's used and published.Upstream parser bugs are worked around locally. When a parser bug's cause is in acorn or
@sveltejs/acorn-typescript, override that one method inTSRXPlugin(packages/tsrx/src/plugin.js) and file the issue upstream. Mark the override with// UPSTREAM(<repo>#<issue>): remove once a release includes the fix, and list it in the "Upstream workarounds" table below, so it can be found and removed later.Type-check-level errors are collected in collect mode ([Bug]: tsrx-core: Parser throws on mistakes that TypeScript reports only as type-check diagnostics, even with
collect: true#415). These are mistakes TypeScript only reports as diagnostics, such as a redeclared type,abstractoutside an abstract class, an ambient initializer, or optional-chaining assignment. Incollectandloosemode the parser records them and keeps parsing; a normal compile still throws. Inputs that only TypeScript's error recovery accepts (6 Prettier cases) are skipped with a reason, like proposal syntax.A missing
}is reported as'}' expected, like TypeScript, not as acorn's genericUnexpected token. This applies to every block, template and JavaScript alike.A
//comment between<and a tag name goes on its own indented line after<, as@tsrx/prettier-pluginprints it since fix(core): start an element at its<, and continue an element statement across a line break (#467, #496) #500. Prettier's own output (<// notewithdivon the next line) isn't valid TSX, and Prettier can't re-parse it. The exception is an element's direct child, which keeps<// note: there, a<followed by a line break is text (core's literal-<rule, soa < bworks in text), and the layout would turn the element into text.A block comment on its own line between
<and a tag name prints straight away in the form Prettier's second pass settles on,</* note */ div />. That's Prettier parity, and it's stable. The same goes for a closing tag (</ /* note */ div>), where Prettier's first format is unstable too.A
<that starts a line is an element, even after a callee (foofollowed by<string>(1)on the next line). This is what makes semicolon-less templates work (const label = "hi"followed by<string>{label}</string>). Prettier'stypescript/call/callee-comments.tsis skipped as a TSRX difference.A comment before a directive or
@{ … }value follows Prettier's JSX layout, which is already the case. A comment on the line of the=stays there:A comment on its own line after the
=goes inside the parentheses:Comments between a directive's branches follow Prettier's
ifandtry([Bug]: tsrx-prettier-plugin moves a comment inside @catch and breaks compilation #505, fixed in feat(tsrx-prettier-plugin): Phase 1 formatting decisions, Prettier-style parse errors, and fixes (#503, #504, #505, #509) #511). While Prettier attaches comments,@ifand@trypresent themselves asIfStatementandTryStatement:@elsestays after the}, and a line comment puts@elseon the next line;@catchstays before the@, and a line comment moves into the@catchbody;@emptyis treated like one beforeelse.With
embeddedLanguageFormatting: "off",<style>and<script>bodies are kept as written ([Bug]: tsrx-prettier-plugin deletes style contents when embedded formatting is off #503, fixed in feat(tsrx-prettier-plugin): Phase 1 formatting decisions, Prettier-style parse errors, and fixes (#503, #504, #505, #509) #511).An element or fragment isn't a left-hand-side expression, as in TypeScript's TSX parser ([Bug]: core: A call, index, or template literal after an element continues the element, unlike TypeScript #426). A
(,[or template literal on the line after an element starts a new statement, and<b />.foois a syntax error.(<b />).fookeeps working. This changes what some code that parses today means (it followed Babel before).Mistakes that compile today are reported when their output is wrong or TypeScript reports them ([Bug]: tsrx-core: Collect mode throws for a parameter property modifier on an arrow function's parameter, which TypeScript reports from its checker #663, [Bug]: tsrx-core: A parameter property with a binding pattern and a default parses with no error, and its output doesn't build #665). An arrow's optional rest or destructured parameter and
constructor(public [a] = [1])are recorded in collect mode and throw in a strict compile, although they compiled before.Elements in a spread attribute's argument are TSRX template markup, like everywhere else ([Bug]: tsrx-core: An element in a spread attribute's argument is parsed as plain JSX: comments and template syntax read as text #656). This changes what that markup means; it mostly affects components passed in a spread.
A missing tag
>is reported as'>' expected., like TypeScript (fix(core): read the text of elements in setup statements as template text, and report syntax errors in element values (#636, #638) #657), in line with decision 15.A keyword after
exportthat can't start the declaration that follows is rejected, not dropped ([Bug]: tsrx-core:abstract,type,namespaceormoduleafterexportis dropped before a declaration it does not start #651). Examples:export abstract function f() {}.abstractordeclarefollowed by a line break afterexportfollows TypeScript ([Bug]: tsrx-core:abstractordeclarebefore a line break afterexportis read as a modifier, unlike TypeScript #608): it isn't a modifier there.A non-breaking space at the start or end of a line of JSX text is left to each target's compiler (fix(core): start the text after a closing tag at the tag, and read a non-breaking space as text (#442, #444, #540) #615). The parser keeps it, and each target renders what its own compiler renders for the same TSX.
catch ({ e }) { var e; }stays an error (fix(core): accept catch var redeclarations, type and namespace exports, quoted attribute keys, and import() trailing commas (#225, #253, #342, #554) #602), as ECMAScript and JavaScript engines have it, although TypeScript accepts it.JSXTextfollows the JSX AST standard ([Bug]: tsrx-core: JSXTextvaluekeeps character references undecoded, unlike the JSX AST standard #710):valuehas character references decoded,rawkeeps the text as written, and printers print fromraw. TSRX is a superset of TSX, and the website's specification already says so.The next
@tsrx/corerelease is a minor,0.5.0(chore(release): release the next @tsrx/core as a minor (0.5.0) #732): it changes the language (fix(core): read an element in a spread attribute's argument or an attribute value without braces as template markup (#656) #713) and rejects code that used to parse (fix(core): read tokens by their context like TSX: comments after directive keywords,yield @…,/>/,</after an operand, and subscripts after elements (#477, #547, #584, #586, #426) #635, fix(core): read type arguments after superclasses and class and function expressions, and reject what follows export or decorates an array pattern (#523, #545, #578, #607, #609, #631) #650, fix(core): collect parameter property, emptyforhead, andletname mistakes that TypeScript reports from its checker, and reject decorators on object literal members (#644, #645, #646) #666, J2, B5). [Bug]: tsrx-core: JSXTextvaluekeeps character references undecoded, unlike the JSX AST standard #710's fix gets aminorchangeset too, so that^0.5.0ranges don't pick it up before Ripple and Octane are ready.esrap stays at 2.3.2 until feat: use tokens for more accurate sourcemaps (alternative approach) sveltejs/esrap#235 merges and is released. The upgrade is breaking:
boundaryTokenis gone, and core will pass a tokens array so esrap can map what the parser gives no location, such as brackets. [Bug]: tsrx-core: A generic arrow function with one type parameter compiles to<T>(…) => …, which isn't valid TSX #707, [Bug]: tsrx-core: Compiled and editor output drop parameter decorators #606 and [Bug]: tsrx-core: An object literal getter or setter with type parameters loses them in compiled output and breaks editor mappings #632 wait for that upgrade.<script>is a raw-text element, like<style>([Bug]: tsrx-core: A<script>body compiles to JSX text, so its line breaks collapse and its character references decode #708), in templates and plain JSX: its body iscontent, as written, with no comments, no decoding and no{…}expressions, so<script>{code}</script>is the text{code}. Core's targets printcontentin each target's exact form (React a string child, Preact and HonodangerouslySetInnerHTML, Solid and VueinnerHTML), the mirroredJSXTextchild goes, and the body ends where HTML ends it. Whether a script runs is each target's decision.Comments between JSX children are a TSRX feature for all TSRX markup, not only templates (decision 4 stands). [Bug]: tsrx-core: JSXText
valuekeeps character references undecoded, unlike the JSX AST standard #710 and fix(core): write a>in text as>, read it as text in containers, and print JSX text fromraw(#694, #693) #711 don't affect it: the scanner decides comments beforevalueandraware built. Comment-like text is written as/* … */or{'/* … */'}. The docs and spec say so in [Bug]: tsrx-website: document that JS comments apply to all TSRX markup, differ from TSX, and how to write comment-like text #733.@tsrx/oxcfollows whatever is decided for@tsrx/core, and Ripple, Octane and oxc issues are filed directly when something is broken there.export declare module "m" {}stays as it is. TypeScript emits it and reports TS2668 only as a type error, which TSRX already shows through TypeScript; the declaration is type-only and the bundler strips it. Following TypeScript means TSRX adds no error of its own.Modifier order and compatibility errors point at the modifier in a normal compile too ([Bug]: tsrx-core: A modifier order or compatibility error points at the wrong line in a normal compile #735), with an
UPSTREAM(sveltejs/acorn-typescript#122)override, as fix(core): collect decorator, modifier, and rest parameter mistakes that TypeScript reports from its checker (#610, #611, #616, #617) #647 did for repeated modifiers.A dynamic tag expression is an identifier, a member access or a string ([Bug]: tsrx-core: A dynamic tag accepts conditionals, arrow functions, elements and null, where only an identifier, member access or string should be allowed, and a disallowed form fails the whole file in collect mode #737). A computed key must itself be an identifier, a literal or a member access. Conditionals,
||,??, parentheses and type wrappers are reported too, since the closing tag repeats the expression; anything more is computed above the element (const Tag = …;).null,&&,a?.b, calls, arrow functions and elements are reported. Any expression parses; a disallowed one is recorded in collect mode and thrown in a normal compile. The spec narrows to match. This supersedes the question of elements in dynamic tag names.JSDoc casts print like Prettier's
typescriptparser ([Bug]: tsrx-prettier-plugin: A JSDoc type cast keeps its parentheses, where Prettier'stypescriptparser drops them #844): the parentheses are dropped (/** @type {T} */ z), because TSRX is a TSX extension. The plugin setspreserveParens: false.Prettier
^3.9.9([Feature]: tsrx-prettier-plugin: Upgrade Prettier to 3.9.9 and re-import its format tests #833): 3.9.9 is the minimum, and 4.0 isn't allowed until the plugin is tested with it. The tests are re-imported from 3.9.9.A comment between an enum's name and its
{stays outside the body ([Bug]: tsrx-prettier-plugin: A comment between an enum's name and its{moves into the enum body #840), for line and block comments alike. Moving it inside changes what it refers to; for a line comment this deliberately differs from Prettier, which the user considers a Prettier bug.Empty bodies are kept as written ([Bug]: tsrx-prettier-plugin: A comment in an empty @switch body moves into the parentheses #835): a comment in an empty
@switchbody stays inside the braces, and neither the formatter nor the compiler removes an empty@switchor other empty block, with or without comments. A bundler's dead-code elimination can remove it.Every error gets a code ([Bug]: tsrx-core, tsrx-prettier-plugin: Most errors have no code: give TypeScript's mistakes their TS code and TSRX's own errors a TSRX code #843): TypeScript's own code (
TS1029) for a mistake TypeScript also reports, and aTSRXcode for TSRX's own errors, by area (TSRX1xxxmarkup syntax,2xxxtemplate rules,3xxx<style>/CSS,4xxxother). That includesDIAGNOSTIC_CODES(the constant names stay, their values change) and the analyzer's errors inanalyze/validation.js. The specification lists every code. Downstream: [Bug]: Style diagnostic tests compare TSRX error codes as literal strings, which change in the next @tsrx/core Ripple-TS/ripple#1524, Tests compare TSRX error codes as literal strings, which change in the next @tsrx/core octanejs/octane#1337, Errors carry no code; report the same TS and TSRX codes as @tsrx/core oxc#117.Core's JSX children take the standard shape ([Bug]: tsrx-core, tsrx-prettier-plugin: Give JSX children the standard shape: text is exactly its source, comments stay in the comment list, whitespace-only runs are text #841, breaking): a
JSXTextis exactly the source it covers, a comment between children splits the text and lives only in the file's comment list (no new node type, noinnerComments), and whitespace-only text is kept. Compilers joinJSXTextsiblings that only comments separate before applying JSX's whitespace rules, which keeps today's rendering (fix(core): start the text after a closing tag at the tag, and read a non-breaking space as text (#442, #444, #540) #615). The old formatter is fixed to match. Upgrade issues: [Bug]: Render template comments like {/* */} and read // like JavaScript, for the next @tsrx/core Ripple-TS/ripple#1525, Render template comments like {/* */} and read // like JavaScript, for the next @tsrx/core octanejs/octane#1338, Read // like @tsrx/core and give an element's children TSX's shape, with comments as {} children oxc#118.Remaining work
The order the user set: first the open parser issues (below), then Phase 3, with Phase 2 on hold and the release decision last.
Now: the open parser issues
The user asked for every open parser issue to be fixed before moving on. They are grouped by the code they touch, and each group is one core PR from its own worktree. The groups follow decision 13 and the conventions below.
?after an array pattern element, an optional rest parameter's message,Accessibility modifier already seen.in collect mode, repeated-modifier positions, and collect mode throwing for TS1206 decorators and TS1317 rest-parameter modifiersexport abstractbefore a line break crashing; decorators accepted and dropped in array binding patterns;m<const T>()in an object literalyield, a regular expression starting with>,</after an operand, and no subscripts after an element (decision 22)@tsrx/prettier-plugin{…}container or an@switchcase: lost spaces, a non-breaking space that fails to parse, a stack overflow@{ … }setup statement is read as code (const a = <span>Hello<b /></span>;fails withNot enough stack space, broken since before Phase 1); an internal invariant message for a syntax error after a closing tagx = </>;crashes withRangeError(upstream sveltejs/acorn-typescript#134); an element as an unbraced attribute value (<div attr=<b>/* c */</b> />) never finishes parsing and runs out of memory; a/in a template element's opening tag is read as textconstructor(public [a] = [1])compiles to output esbuild rejects (decision 23)async(a)(b) => 1parses as an async arrow (acornjs/acorn#1460); collect mode throws for a signature's parameter initializer (TS2371);(x as T) => xparses and keeps the assertionfor ((a as T) of x)rejected;(x: number),f(x?),f(...x: number[])accepted with no=>; an async arrow's rest parameter ends before its type annotation, so both formatters move a comment; collect mode throws for a rest parameter with a default (TS1048) and for(...a, b) => 1(TS1014)export: follow TypeScript for a line break afterabstract/declare(decision 27); reject a keyword that can't start the declaration that follows (decision 26)abstractbefore a non-class declaration,abstract interface,abstract declare class;export default interfacewith the name on the next line; a type alias namedas;export global {}; escaped TypeScript keywords (TS1260)(abstract) class A {});intrinsicandinterfaceas type names; import type options accept any object; an escapedtypein an import or export clause; modifiers before a declaration that only TypeScript's checker reports;export default @dec declare class>in template text is printed bare (esbuild rejects it), and character references in plain-JSX elements are decoded and printed barevaluefor template text too (decision 30); every reader ofJSXText.valueis checked. Ripple and Octane decode again, so both need lockstep changes (Ripple-TS/ripple#1520, octanejs/octane#1330), and@tsrx/oxcmust decode too (tsrx-org/oxc#111);minorchangeset (decision 31)<script>as raw text (decision 33): printcontentper target, drop the mirroredJSXTextchild, end the body where HTML does;minorchangesetv-html; downstream tsrx-org/oxc#116; new issues #792, #793minorchangeset; Ripple test and Octane #1329 followDownstream issues for
0.5.0(filed 2026-09-26, after checking Ripple's and Octane's test suites and compiled output againstmain, #711 and a simulated #710):JSXText.value(tsrx-org/tsrx#710) Ripple-TS/ripple#1520 (the one break is fix(core): read an element in a spread attribute's argument or an attribute value without braces as template markup (#656) #713's server spread argument, with a fix), and [Bug]: TS view prints an unused template element statement without its;, so TypeScript adds TS2657 or TS1005 Ripple-TS/ripple#1517, #1518, #1519.raw,>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, #1328, #1329.@tsrx/oxcparity with core, which Octane parses with on Node: Comments in JSX text stay in the text'svalueandraw, where@tsrx/coreleaves them out oxc#110 (comments in text), chore(release): allow minor bumps, upgrade changesets to 3.x, sync Zed grammar rev #111 (references), perf(runtime): skip the cleanups array for two-ref mergeRefs mounts #112 (a non-breaking space next to a line break), refactor: remove array_slice, iterable_array_from, and buildFallback #113 (scope errors).;, so TypeScript adds TS2657 or TS1005 #730 (an unused element statement loses its;in editor output), [Bug]: tsrx-core: Aconsttype parameter in a function type, constructor type, or signature fails to parse #731 (<const T>in a function type or signature fails to parse).Waiting for the user:
Not included, since they aren't parser issues: #259 and #501 (compiled output), #454 (the ESLint parser), #455 (the tree-sitter grammar).
Upstream workarounds (decision 13). Add a row when an override lands, and remove it when the upstream fix is released and the override is gone:
plugin.jstsParseModifier,tsParseModifiers(#551)tsParseInterfaceBody(#551)parseClassId(#551)parseMaybeImportAttributes(#551)tsParseImportType(#555)TSImportTypeoptions printed intsx_with_ts_locations(#555)optionsCHECKER_LEVEL_ERRORSallow-list throughraise/raiseRecoverable, andcanAwaitin namespaces (#558)parsePropertyValue: an object method's function starts at its type parameters (#633)parseYield: ayieldfollowed by@reads its argument (#635)parseSubscript:async(a)(b) => 1is an error (#728, pending)parseSpread,parseExprList,#parseAsyncArguments: an async arrow's optional rest parameter (#728, pending)#parseGenericArrowFunctionand related: errors after a generic arrow's=>(#728, pending)tsParseBindingListForSignature: TS2371 for a default in a signature (#728, pending)checkLValPattern: no type assertion in an arrow's parameters (#728, pending)tsParseDeclaration,tsTryParseDeclare,hasFollowingLineBreak,tsParseModifierand the other keyword overrides (#721, pending)isAbstractClass,isDeclareClass,#checkExportDeclarationStart:abstract/declarebefore a line break (#696)parseBindingListandraise: a parameter-property modifier on a function's parameter (#666)parseProperty: no decorators on object literal members (#666)updateContext: a/after a tag start is a closing tag's only while the tag's context is on top (#695)raise: repeated-modifier errors at the modifier (#647)parseClassSuper: superclass type arguments before a line break (#650)parseExportDeclaration:export abstractbefore a line break (#650)parseAssignableListItem: no decorators in array patterns (#650)parseBindingListItem: no?after an array pattern element (#647)parseClassId(extended),parseExportDefaultDeclaration(#614)parseDecorators: decorators beforeexport(#614)parseAssignableListItem: a decorator on a rest parameter (#614)parseWithEntries: quoted attribute keys compared by value (#602)raisecalls acorn-typescript's modifier error templates and reports at the modifier (#618)parseBindingListItemaccepts?on an object or array pattern parameter (#618)parseDynamicImport: acorn's ownimport()parsing (#602)argumentscopy #110 adds, which would make esrap print the options twiceWhere an open upstream issue or PR already covers a cause, the override references it instead of a new issue:
class global {},class abstract {}) #421 (contextual-keyword class names), [Bug]: tsrx-core: Parser rejects import attributes in import types (import("foo", { with: { type: "json" } })) #422 (import attributes in import types), and the optional binding-pattern parameter from [Bug]: tsrx-core: Parser throws on mistakes that TypeScript reports only as type-check diagnostics, even withcollect: true#415. Overrides for these mirror feat: remove lazy destructuring (RFC #106) #110's AST, so removing them later doesn't change the AST.collect: true#415's question for acorn-typescript's errors. acorn's errors in [Bug]: tsrx-core: Parser throws on mistakes that TypeScript reports only as type-check diagnostics, even withcollect: true#415 are ECMAScript early errors, where acorn is right, so nothing is filed with acorn.The adapter in this plugin has one workaround of its own: it wraps
export importin anExportNamedDeclarationfor sveltejs/acorn-typescript#114 (src/parse.js,TSImportEqualsDeclaration). The user's other open acorn-typescript items are #111 (tsrx #169), #112 with its fix #113 (tsrx #194; #421's override touches the sameparseClassId, so coordinate), and #116 (tsrx #342).Open follow-up issues from Phase 1
Filed, and not part of any group above:
import()andimport.defer()get no editor mappings, so tsrx-tsc drops their errors #599: the options ofimport()andimport.defer()get no editor mappings.declare namespaceordeclare modulebody rejects a function declared there #600: an export list in adeclare namespace/declare modulebody rejects a function declared there.declare globaldeclaration fails with "Export is not defined" #601: a named export of adeclare globaldeclaration is rejected (fixed by test: run the parser over the TypeScript compiler corpus sveltejs/acorn-typescript#110 once it's released).?in compiled and editor output #556: the printed TypeScript drops the?from an optional binding-pattern parameter.<T>(…) => …, which isn't valid TSX #707:<T,>(x: T) => xcompiles to<T>(x: T) => x, which isn't valid TSX, in the build output and the editor's virtual code. Every single-type-parameter generic arrow is affected. The user's esrap PR fix: disambiguate generic arrows in the TSX printer sveltejs/esrap#188 fixed it in 2.3.8; the catalog has 2.3.2. Upgrading esrap (latest 2.3.13) also brings [Bug]: tsrx-core: Compiled and editor output drop parameter decorators #606's and likely [Bug]: tsrx-core: An object literal getter or setter with type parameters loses them in compiled output and breaks editor mappings #632's fixes, and source map changes that Adopt esrap 2.3.11+ sourcemaps: pass tokens, drop boundaryTokens and the bracket guess once esrap #212/#213 land #235 planned to adopt once feat: map delimiters and keywords inside nodes from the parser's tokens sveltejs/esrap#213 lands. Waits for feat: use tokens for more accurate sourcemaps (alternative approach) sveltejs/esrap#235 (decision 32).found '</null>'instead offound '</>'#712: an element closed by a fragment's closing tag reportsfound '</null>'instead offound '</>'(cosmetic).@misses the@when a comment comes before it #714: the editor's completion mapping for text that starts with@misses the@when a comment comes before it in the same text.@tsrx/prettier-plugin: [Bug]: tsrx-core, @tsrx/prettier-plugin: A block comment between children of an element in a {…} container is attached to the element's body, so the formatter moves it before the closing tag #637 (a block comment between the children of an element in a{…}container moves before the closing tag), [Bug]: @tsrx/prettier-plugin: A comment on its own line before a multi-line JSX child keeps the text after the child on its closing line, and needs a second format pass #640 (a comment on its own line before a multi-line child), [Bug]: @tsrx/prettier-plugin: A line comment between a superclass and type arguments on the next line moves into the class body, unlike Prettier #652 (a line comment between a superclass and type arguments on the next line moves into the class body).//starts a comment only when just whitespace comes before it on its line (sohttps://…stays text), and a block comment doesn't count as whitespace. So/* c */ // creads// cas text, in template and expression JSX alike. This plugin never prints a line comment after another comment on the same line. Whether a block comment should count as whitespace there is a language question for the user.@tsrx/prettier-plugin: [Bug]: @tsrx/prettier-plugin: Formatter never breaks between the arguments ofimport(source, options)like Prettier #553 (the arguments ofimport(source, options)never break the way Prettier breaks them).async(a)(b) => 1);parseDynamicImportcopiesimport()options ontoarguments, which esrap prints twice;options) sveltejs/esrap#231 (TSImportTypeoptions).Status and findings (2026-09-26)
Focus (user, 2026-09-26): only completing
tsrx-prettier-plugin. Parser work only when it fixes the plugin's Prettier parity or lets plugin code be removed.Status on
mainc70964d (this PR at 5bd12c4): 2,105 tests pass, 17 skipped, known failures[]. All 2,040 imported Prettier 3.9.6 cases pass except the 17 deliberate skips intests/prettier-overrides.js. The 82 cases the TSRX parser rejected at import time are imported and now pass. The target is Prettier'stypescriptparser; Babel and babel-ts aren't targeted (29 snapshots where Babel formats differently weren't imported). No open parser issue adds a Prettier case or removes plugin code.More Prettier cases:
letcases, decision 5 or 14.requirePragma/insertPragmawork).Bugs in the new plugin:
<script>, or a<style>misses Prettier's parentheses, and its own comments print outside them or crash #836: elements the plugin prints itself (comment children,<script>,<style>) miss Prettier's parentheses (unparseable output), and their comments print outside them or crash.const,in, oroutand its name crashes with "Comment location overlaps with node location" #839: a comment after a type parameter'sconst/in/outcrashes.{moves into the enum body #840: a comment between an enum's name and{moves into the body.@switchbody moves into the parentheses.@casearm that ends with a comment is deleted #837: a blank line after an@casearm ending with a comment is deleted.for awaithead fails to parse, although TypeScript accepts it #769, [Bug]: tsrx-core: In an element in a {…} container, a // after text is read as a line comment and kept as text, so the formatter prints it twice and changes the text #668.Less plugin code:
@casearm that ends with a comment is deleted #837).@caseor@defaultbody's{ … }isn't a node, so the formatter rebuilds it by searching the source for the brace #842@caseblock body (about −17; [Bug]: tsrx-core: The comments in an empty template @switch, @case, or @default body move out of it, and one before its { moves before the ) or : #774's root cause), [Bug]: tsrx-core, tsrx-prettier-plugin: Most errors have no code: give TypeScript's mistakes their TS code and TSRX's own errors a TSRX code #843 TypeScript codes on collected errors (about −25).Decisions: #844, #840 and the Prettier minimum are decided (39–41). Still open: #833's
letcases (7 cases, not 6, would be left out under decision 5;yieldas a name is thrown, not collected); #840's scope (the same rule for a line comment before{in a class, interface, function and namespace).Phase 2: compare with
@tsrx/prettier-plugin's tests (next)On hold by the user's decision: don't test this plugin against the old plugin's tests for now. When it resumes: Run the old plugin's test suite (
packages/prettier-plugin/src/index.test.js: 826 tests, input →format()→toBeWithNewline(expected)) against this plugin, and sort every difference into:tests/tsrx.test.js.@{ … }that is an element's only child (<div>@{…}</div>). List these for the user.How:
packages/tsrx-prettier-plugin/src/index.js, from a scratch copy or with a vitest alias. Don't change the old plugin or its tests.Phase 3: more TSRX tests
Write them from the language rules and Prettier's behavior, not from the old plugin's tests. Areas that aren't covered yet:
@forheads that break, with destructuring,indexandkey@{ … }inside JSX attributes and expression containers<style apply>variants and assigned themessemi: false,singleQuote,bracketSameLine,singleAttributePerLinefor TSRX syntaxOngoing: keeping up with Prettier
pnpm --filter tsrx-prettier-plugin import-prettier-tests, then refresh the known failures.src/jsx.js(copied from Prettier'sprintJsxElementInternalandprintJsxChildren) in step with Prettier.src/language-js/parse/postprocess: node shapes the printer expects can change between releases.Last: the release decision (the user decides)
Nothing moves to this plugin until the user is absolutely sure (decision 12). Phases 1–3 are what build that confidence. When the time comes, the user chooses between:
@tsrx/prettier-plugin: publish this plugin under that name.tsrx-prettier-plugin, next to or instead of the old one.Either way, these would need doing, so check each one with the user before touching it:
major).package.jsonand.prettierrc.changeset/config.json(the fixed group)packages/language-server/src/server.jspackages/tsrx-mcp(src/format.js,src/server.js,src/inspect.js, tests)website-tsrx(package.json,src/routes.ts,src/pages/getting-started.tsrx)prettier/standalone).Runbook
To check the repository's
.tsrxfiles (no errors, and a second format changes nothing), run this frompackages/tsrx-prettier-plugin:Conventions
mainand check for duplicates first. A finding with the same root cause as the current PR goes in its description instead.patchchangeset for@tsrx/core, and verified by comparing ASTs before and after over the repository's.tsrxfiles, the test-file sources, and the Prettier snapshot inputs.git stash:refs/stashis shared by every worktree and other sessions run in parallel. Use patch files or WIP commits.spawnSync(…, { timeout, killSignal: 'SIGKILL' })or aWorkerwithterminate(), sincetimeoutisn't installed. Use at most 4 workers, and isolate tests in a Worker or child process, because a vitest timeout can't stop a synchronous loop. Checkpsafterwards.mainneeds a review, and agents can't merge. Bringmaininto an open PR withgit merge origin/main: never rebase or force-push a pushed branch.prettier-pluginand the touched core tests for a parser change), thenpnpm formatand the typecheck. CI runs the rest.git statusis clean and the local branch head matches the PR's last commit.@tsrx/prettier-pluginchanges need apatchchangeset (majoris rejected).Open questions for the user
🤖 Generated with Claude Code
Note
Medium Risk
Changes JSX child printing and parser metadata for every element with inter-child comments; wrong run logic would alter rendered whitespace or break layout, but behavior is heavily covered by new and existing format tests.
Overview
Fixes #639 and #630 in
tsrx-prettier-pluginso formatted output matches what JSX would render without the comment in the middle.#639 — comment whitespace runs: The parser now calls
describeCommentRunswhen an element hasTSRXJSXCommentchildren. It groups adjacent comments (with only blank text or{" "}between them), computes what the surrounding whitespace would render as (space vs line break vs boundary), and attaches arunobject on each comment.printCommentChildinjsx.jsuses that metadata to emit Prettier-style{" "}/line/hardline/breakParentinstead of naïvely printing spaces, and skips gap nodes the comment run already owns.isEmptyDocis aligned with Prettier so empty strings don’t count as content. Regression tests cover comments beside elements, expressions,{" "}, and multi-comment groups.#630 — comments in method type parameters: In
parse.js, class/abstract method reshaping movestypeParametersontovalueand setsvalue.startto the type-parameter list start (typescript-estree shape), so comments inside<T>stay attached to the method, not the name.Reviewed by Cursor Bugbot for commit de0450d. Bugbot is set up for automated code reviews on this repo. Configure here.