Skip to content

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

Merged
leonidaz merged 9 commits into
mainfrom
feat/tsrx-prettier-plugin-plan
Sep 27, 2026
Merged

leonidaz merged 9 commits into
mainfrom
feat/tsrx-prettier-plugin-plan

Conversation

@leonidaz

@leonidaz leonidaz commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

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.

  • Work on the plugin lands here as commits.
  • Parser work lands as separate core PRs, tracked here.
  • This description is updated as items are done.

It started with an empty commit, because a PR needs one. Commits so far:

Closes #630, closes #639.

Background

@tsrx/prettier-plugin prints 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.

Current state (main at 1af65cc6, after #511)

Prettier cases imported (JS, JSX, TS from Prettier 3.9.6 that apply to TSRX) 2040, out of 5,114 in these areas; the rest are error, cursor or range tests, or not TSX
Pass 2023: every imported case except the deliberate skips
Known failures (tests/prettier-known-failures.json) 0. From here on, a new one means a regression or a new Prettier release.
Skipped by override (tests/prettier-overrides.js) 17: 8 private-field destructuring (#424), 6 inputs only TypeScript's error recovery parses (#415), 2 CommonJS await(…), 1 line-starting < after a callee (decision 18)
Repository .tsrx files all 69 format without errors, and a second format changes nothing
prettier/standalone works with only this plugin (a test covers it)

Decisions so far

The user made these decisions during #409, #511 and the parser PRs. Keep to them unless the user says otherwise.

  1. Prettier parity. Output is exactly what Prettier prints with its typescript parser for the same TSX. TSRX differs only where its syntax does. Differences from Prettier are bugs, even where @tsrx/prettier-plugin did something else on purpose.

  2. 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.

  3. 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-plugin on main. 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.

  4. Comments between JSX children are comments (// and /* */). TSX reads them as text. They keep their line.

  5. A .tsrx file 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.

  6. JSX spread children ({...items}) parse, but are unsupported on every target. Core's analyzer reports tsrx-jsx-spread-child: fatal in a compile, collected in editor mode (fix(core): parse JSX spread children and report them as unsupported (#417) #495).

  7. 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).

  8. 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).

  9. @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.

  10. <script> bodies are formatted only when they hold JavaScript or TypeScript (no type, module, or a JS/TS type), with this plugin's own parser. Anything else is kept as written.

  11. 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 typescript parser 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_MISTAKES in src/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).

  12. No switch until the user is absolutely sure. Whether this plugin replaces @tsrx/prettier-plugin, or ships separately as tsrx-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.

  13. Upstream parser bugs are worked around locally. When a parser bug's cause is in acorn or @sveltejs/acorn-typescript, override that one method in TSRXPlugin (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.

  14. 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, abstract outside an abstract class, an ambient initializer, or optional-chaining assignment. In collect and loose mode 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.

  15. A missing } is reported as '}' expected, like TypeScript, not as acorn's generic Unexpected token. This applies to every block, template and JavaScript alike.

  16. A // comment between < and a tag name goes on its own indented line after <, as @tsrx/prettier-plugin prints 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 (<// note with div on 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, so a < b works in text), and the layout would turn the element into text.

  17. 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.

  18. A < that starts a line is an element, even after a callee (foo followed by <string>(1) on the next line). This is what makes semicolon-less templates work (const label = "hi" followed by <string>{label}</string>). Prettier's typescript/call/callee-comments.ts is skipped as a TSRX difference.

  19. 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:

    const y = // lead
      (
        @if (a) {
          <b />
        }
      );
    

    A comment on its own line after the = goes inside the parentheses:

    const y = (
      // lead
      @if (a) {
        <b />
      }
    );
    
  20. Comments between a directive's branches follow Prettier's if and try ([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, @if and @try present themselves as IfStatement and TryStatement:

    • a comment before @else stays after the }, and a line comment puts @else on the next line;
    • a block comment before @catch stays before the @, and a line comment moves into the @catch body;
    • a comment before @empty is treated like one before else.
  21. 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).

  22. 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 />.foo is a syntax error. (<b />).foo keeps working. This changes what some code that parses today means (it followed Babel before).

  23. 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.

  24. 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.

  25. 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.

  26. A keyword after export that can't start the declaration that follows is rejected, not dropped ([Bug]: tsrx-core: abstract, type, namespace or module after export is dropped before a declaration it does not start #651). Examples: export abstract function f() {}.

  27. abstract or declare followed by a line break after export follows TypeScript ([Bug]: tsrx-core: abstract or declare before a line break after export is read as a modifier, unlike TypeScript #608): it isn't a modifier there.

  28. 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.

  29. 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.

  30. JSXText follows the JSX AST standard ([Bug]: tsrx-core: JSXText value keeps character references undecoded, unlike the JSX AST standard #710): value has character references decoded, raw keeps the text as written, and printers print from raw. TSRX is a superset of TSX, and the website's specification already says so.

  31. The next @tsrx/core release 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, empty for head, and let name mistakes that TypeScript reports from its checker, and reject decorators on object literal members (#644, #645, #646) #666, J2, B5). [Bug]: tsrx-core: JSXText value keeps character references undecoded, unlike the JSX AST standard #710's fix gets a minor changeset too, so that ^0.5.0 ranges don't pick it up before Ripple and Octane are ready.

  32. 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: boundaryToken is 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.

  33. <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 is content, as written, with no comments, no decoding and no {…} expressions, so <script>{code}</script> is the text {code}. Core's targets print content in each target's exact form (React a string child, Preact and Hono dangerouslySetInnerHTML, Solid and Vue innerHTML), the mirrored JSXText child goes, and the body ends where HTML ends it. Whether a script runs is each target's decision.

  34. Comments between JSX children are a TSRX feature for all TSRX markup, not only templates (decision 4 stands). [Bug]: tsrx-core: JSXText value keeps character references undecoded, unlike the JSX AST standard #710 and fix(core): write a > in text as &gt;, read it as text in containers, and print JSX text from raw (#694, #693) #711 don't affect it: the scanner decides comments before value and raw are built. Comment-like text is written as &#47;* … */ 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.

  35. @tsrx/oxc follows whatever is decided for @tsrx/core, and Ripple, Octane and oxc issues are filed directly when something is broken there.

  36. 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.

  37. 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.

  38. 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.

  39. JSDoc casts print like Prettier's typescript parser ([Bug]: tsrx-prettier-plugin: A JSDoc type cast keeps its parentheses, where Prettier's typescript parser drops them #844): the parentheses are dropped (/** @type {T} */ z), because TSRX is a TSX extension. The plugin sets preserveParens: false.

  40. 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.

  41. 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.

  42. 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 @switch body stays inside the braces, and neither the formatter nor the compiler removes an empty @switch or other empty block, with or without comments. A bundler's dead-code elimination can remove it.

  43. 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 a TSRX code for TSRX's own errors, by area (TSRX1xxx markup syntax, 2xxx template rules, 3xxx <style>/CSS, 4xxx other). That includes DIAGNOSTIC_CODES (the constant names stay, their values change) and the analyzer's errors in analyze/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.

  44. 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 JSXText is 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, no innerComments), and whitespace-only text is kept. Compilers join JSXText siblings 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.

Group Issues Area Status
A1 #194, #533, #564 export-default classes and decorators merged (#614)
B #587, #588, #550, #557 mistakes collect mode should record, not throw (the #415 family); #550 is sveltejs/acorn-typescript#123 upstream merged (#618)
B2 #616, #617, #610, #611 follow-ups to B: ? 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 modifiers merged (#647)
C #225, #253, #342, #554 valid code rejected by upstream checks merged (#602)
D #442, #444, #540 JSX and template text whitespace merged (#615)
A2 #523, #545, #578, #607, #609, #631 (and #608 once decided) type arguments after classes, superclasses and function expressions; export abstract before a line break crashing; decorators accepted and dropped in array binding patterns; m<const T>() in an object literal merged (#650)
E #477, #547, #584, #586, #426 tokenizer: comments after a directive keyword, TSRX values after yield, a regular expression starting with >, </ after an operand, and no subscripts after an element (decision 22) merged (#635)
F #458, #488, #489 core comment attachment, for @tsrx/prettier-plugin merged (#633)
G #612, #613 template text in an element inside a {…} container or an @switch case: lost spaces, a non-breaking space that fails to parse, a stack overflow merged (#641)
H #636, #638 JSX text in an element in a @{ … } setup statement is read as code (const a = <span>Hello<b /></span>; fails with Not enough stack space, broken since before Phase 1); an internal invariant message for a syntax error after a closing tag merged (#657)
I #653, #654, #655 x = </>; crashes with RangeError (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 text merged (#695)
B3 #644, #645, #646 collect mode throws for a parameter property with a binding pattern (TS1187) or on a function's parameter (TS2369); decorators on object literal members are accepted and dropped merged (#666)
B4 #664, #663, #665 collect mode throws for a parameter-property modifier in a function type, a signature, or an arrow's parameter; an arrow's optional rest or destructured parameter isn't reported; constructor(public [a] = [1]) compiles to output esbuild rejects (decision 23) merged (#701)
B5 #702, #703, #704, #705, #706 arrow and signature follow-ups: an async arrow's optional rest parameter fails to parse; a generic arrow's syntax error is reported at its type parameters; async(a)(b) => 1 parses as an async arrow (acornjs/acorn#1460); collect mode throws for a signature's parameter initializer (TS2371); (x as T) => x parses and keeps the assertion merged (#728)
B6 #723, #724, #725, #726, #727 for ((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) #777 ready (CI and Bugbot green, no threads); new issues #769, #770, #771 and acorn-typescript#157
J #608, #651 keywords after export: follow TypeScript for a line break after abstract/declare (decision 27); reject a keyword that can't start the declaration that follows (decision 26) merged (#696)
J2 #697, #698, #699, #700, #709 abstract before a non-class declaration, abstract interface, abstract declare class; export default interface with the name on the next line; a type alias named as; export global {}; escaped TypeScript keywords (TS1260) merged (#721)
J3 #715, #716, #717, #718, #719, #720 a parenthesized word read as a keyword ((abstract) class A {}); intrinsic and interface as type names; import type options accept any object; an escaped type in an import or export clause; modifiers before a declaration that only TypeScript's checker reports; export default @dec declare class #788 ready (CI and Bugbot green, no threads); new issues #783, #784, #785, #786 and acorn-typescript#151–#156
K #656 elements in a spread attribute's argument, and in an unbraced attribute value, are TSRX template markup (decision 24) merged (#713)
L #693, #694 text that compiles to wrong or invalid output: a > in template text is printed bare (esbuild rejects it), and character references in plain-JSX elements are decoded and printed bare merged (#711)
M #710 the parser decodes value for template text too (decision 30); every reader of JSXText.value is checked. Ripple and Octane decode again, so both need lockstep changes (Ripple-TS/ripple#1520, octanejs/octane#1330), and @tsrx/oxc must decode too (tsrx-org/oxc#111); minor changeset (decision 31) paused (user, 2026-09-26): starts after S when work resumes; its PR merges only after 0.5.0 is published
S #708 <script> as raw text (decision 33): print content per target, drop the mirrored JSXText child, end the body where HTML does; minor changeset #790 ready (CI and Bugbot green, no threads); Vue prints v-html; downstream tsrx-org/oxc#116; new issues #792, #793
T #737 the dynamic tag rule (decision 38), reported in collect mode instead of failing the file; minor changeset; Ripple test and Octane #1329 follow #782 ready (CI and Bugbot green, no threads); downstream Ripple-TS/ripple#1522 and tsrx-org/oxc#115
U #735 modifier error positions in a normal compile (decision 37) paused (user, 2026-09-26): starts after J3 when work resumes

Downstream issues for 0.5.0 (filed 2026-09-26, after checking Ripple's and Octane's test suites and compiled output against main, #711 and a simulated #710):

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:

Override in plugin.js tsrx issue Upstream issue Removed when
tsParseModifier, tsParseModifiers (#551) #439 sveltejs/acorn-typescript#119 a release fixes #119
tsParseInterfaceBody (#551) #420 sveltejs/acorn-typescript#120 a release fixes #120
parseClassId (#551) #421 sveltejs/acorn-typescript#110 (open PR) a release includes #110
parseMaybeImportAttributes (#551) #423 sveltejs/acorn-typescript#121 a release fixes #121
tsParseImportType (#555) #422 sveltejs/acorn-typescript#110 (open PR) a release includes #110
esrap TSImportType options printed in tsx_with_ts_locations (#555) #422 sveltejs/esrap#231 an esrap release prints options
CHECKER_LEVEL_ERRORS allow-list through raise/raiseRecoverable, and canAwait in namespaces (#558) #415 sveltejs/acorn-typescript#89 upstream decides #89 and a release follows it
optional binding-pattern entry in the allow-list (#558) #415 sveltejs/acorn-typescript#110 (open PR) a release includes #110
modifier errors recorded at the modifier's offset (#558) #415 sveltejs/acorn-typescript#122 a release fixes #122
parsePropertyValue: an object method's function starts at its type parameters (#633) #458 sveltejs/acorn-typescript#127 a release fixes #127
parseYield: a yield followed by @ reads its argument (#635) #547 sveltejs/acorn-typescript#128 a release fixes #128
parseSubscript: async(a)(b) => 1 is an error (#728, pending) #704 acornjs/acorn#1460 a release fixes #1460
parseSpread, parseExprList, #parseAsyncArguments: an async arrow's optional rest parameter (#728, pending) #702 sveltejs/acorn-typescript#140 a release fixes #140
#parseGenericArrowFunction and related: errors after a generic arrow's => (#728, pending) #703 sveltejs/acorn-typescript#141 a release fixes #141
tsParseBindingListForSignature: TS2371 for a default in a signature (#728, pending) #705 sveltejs/acorn-typescript#89, #136 upstream decides #89
checkLValPattern: no type assertion in an arrow's parameters (#728, pending) #706 sveltejs/acorn-typescript#142 a release fixes #142
tsParseDeclaration, tsTryParseDeclare, hasFollowingLineBreak, tsParseModifier and the other keyword overrides (#721, pending) #697, #698, #699, #700, #709 sveltejs/acorn-typescript#143, #144, #145, #146, #147 a release fixes them
isAbstractClass, isDeclareClass, #checkExportDeclarationStart: abstract/declare before a line break (#696) #608, #651 sveltejs/acorn-typescript#137, #132 a release fixes them
parseBindingList and raise: a parameter-property modifier on a function's parameter (#666) #645 sveltejs/acorn-typescript#136 a release fixes #136
parseProperty: no decorators on object literal members (#666) #646 sveltejs/acorn-typescript#135 a release fixes #135
updateContext: a / after a tag start is a closing tag's only while the tag's context is on top (#695) #653 sveltejs/acorn-typescript#134 a release fixes #134
raise: repeated-modifier errors at the modifier (#647) #617 sveltejs/acorn-typescript#129 a release fixes #129
parseClassSuper: superclass type arguments before a line break (#650) #523 sveltejs/acorn-typescript#131 a release fixes #131
parseExportDeclaration: export abstract before a line break (#650) #607 sveltejs/acorn-typescript#132 a release fixes #132
parseAssignableListItem: no decorators in array patterns (#650) #609 sveltejs/acorn-typescript#133 a release fixes #133
parseBindingListItem: no ? after an array pattern element (#647) #616 sveltejs/acorn-typescript#130 a release fixes #130
parseClassId (extended), parseExportDefaultDeclaration (#614) #194, #533 sveltejs/acorn-typescript#113, #124 (and #110) a release fixes them
parseDecorators: decorators before export (#614) #533 sveltejs/acorn-typescript#125 a release fixes #125
parseAssignableListItem: a decorator on a rest parameter (#614) #564 sveltejs/acorn-typescript#126 a release fixes #126
parseWithEntries: quoted attribute keys compared by value (#602) #342 sveltejs/acorn-typescript#116 (the user's PR; #110 has the same fix) a release includes either
raise calls acorn-typescript's modifier error templates and reports at the modifier (#618) #550 sveltejs/acorn-typescript#123 a release fixes #123
parseBindingListItem accepts ? on an object or array pattern parameter (#618) #557 sveltejs/acorn-typescript#110 (open PR) a release includes #110
parseDynamicImport: acorn's own import() parsing (#602) #554 sveltejs/acorn-typescript#110 (open PR) a release includes #110; drop the arguments copy #110 adds, which would make esrap print the options twice

Where an open upstream issue or PR already covers a cause, the override references it instead of a new issue:

The adapter in this plugin has one workaround of its own: it wraps export import in an ExportNamedDeclaration for 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 same parseClassId, so coordinate), and #116 (tsrx #342).

Open follow-up issues from Phase 1

Filed, and not part of any group above:

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 main c70964d (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 in tests/prettier-overrides.js. The 82 cases the TSRX parser rejected at import time are imported and now pass. The target is Prettier's typescript parser; 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:

Bugs in the new plugin:

Less plugin code:

Decisions: #844, #840 and the Prettier minimum are decided (39–41). Still open: #833's let cases (7 cases, not 6, would be left out under decision 5; yield as 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:

  • (a) TSRX behavior this plugin should have. The old test captures something TSRX-specific that is right, and this plugin gets it wrong. Fix it here, and add a test to tests/tsrx.test.js.
  • (b) The old plugin deviates from Prettier. Keep this plugin's output (decision 1), and note it in the report.
  • (c) Needs a decision. It's TSRX-specific and neither output is clearly right. For example, the old plugin hugs a @{ … } that is an element's only child (<div>@{ … }</div>). List these for the user.

How:

  • Run the file with its plugin import pointed at 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.
  • Collect each failing test's input, old expected output, and new output.
  • Group the failures by cause, and post the classified report in this PR. For (c), ask the user.
  • The old tests are in flux, so rerun this before acting on it.

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:

Ongoing: keeping up with Prettier

  • On a Prettier upgrade, run pnpm --filter tsrx-prettier-plugin import-prettier-tests, then refresh the known failures.
  • Keep src/jsx.js (copied from Prettier's printJsxElementInternal and printJsxChildren) in step with Prettier.
  • Check the adapter against Prettier's 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:

  • Replacing @tsrx/prettier-plugin: publish this plugin under that name.
  • Releasing it separately as 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:

  • Settle the name, options, and version (a changeset, not major).
  • Move whichever consumers should use it:
    • the root package.json and .prettierrc
    • .changeset/config.json (the fixed group)
    • packages/language-server/src/server.js
    • packages/tsrx-mcp (src/format.js, src/server.js, src/inspect.js, tests)
    • website-tsrx (package.json, src/routes.ts, src/pages/getting-started.tsrx)
    • READMEs
  • Format the repository with it and review the diff.
  • Hook it into the playground (it already works in prettier/standalone).
  • If it replaces the old plugin, retire the old plugin's issue backlog: many "Formatter …" issues become moot.

Runbook

# Set up a worktree from main (never work on main directly)
git fetch origin && git worktree add -b <branch> ../<dir> origin/main
cd ../<dir> && pnpm install --frozen-lockfile --ignore-scripts

# Test (about 10 s), typecheck, format
pnpm vitest run --project tsrx-prettier-plugin
npx tsgo --noEmit -p packages/tsrx-prettier-plugin/tsconfig.typecheck.json
npx prettier --check packages/tsrx-prettier-plugin

# After a parser fix lands on main: merge main in (don't rebase a pushed branch),
# re-import (clones Prettier at the installed version, or pass --source), and
# refresh the known failures
git merge origin/main && pnpm install --frozen-lockfile --ignore-scripts
node packages/tsrx-prettier-plugin/scripts/import-prettier-tests.js [--source <prettier checkout>]
UPDATE_KNOWN_FAILURES=1 pnpm vitest run --project tsrx-prettier-plugin
pnpm vitest run --project tsrx-prettier-plugin

To check the repository's .tsrx files (no errors, and a second format changes nothing), run this from packages/tsrx-prettier-plugin:

node --input-type=module <<'JS'
import * as prettier from 'prettier';
import fs from 'node:fs';
import { execSync } from 'node:child_process';
import plugin from './src/index.js';

const files = execSync('git ls-files "*.tsrx"', { cwd: '../..', encoding: 'utf8' })
	.trim()
	.split('\n');
const options = {
	parser: 'tsrx',
	plugins: [plugin],
	useTabs: true,
	singleQuote: true,
	printWidth: 100,
	bracketSameLine: false,
};

for (const file of files) {
	const source = fs.readFileSync('../../' + file, 'utf8');
	try {
		const once = await prettier.format(source, options);
		const twice = await prettier.format(once, options);
		if (twice !== once) console.log('unstable', file);
	} catch (error) {
		console.log('error', file, error.message.split('\n')[0]);
	}
}
JS

Conventions

  • Findings: file an issue for every finding with a distinct root cause, using the bug template's sections (Issue Category, Bug Description, Reproduction Steps with real output, Cause with permalinks, Fix Direction, Environment, Additional Context). Verify on current main and check for duplicates first. A finding with the same root cause as the current PR goes in its description instead.
  • Parser fixes are separate PRs, one per root cause or small group, from their own worktree. They're complete when opened (not drafts), with a patch changeset for @tsrx/core, and verified by comparing ASTs before and after over the repository's .tsrx files, the test-file sources, and the Prettier snapshot inputs.
  • No git stash: refs/stash is shared by every worktree and other sessions run in parallel. Use patch files or WIP commits.
  • Anything that can hang (parser loops) runs under a hard kill: spawnSync(…, { timeout, killSignal: 'SIGKILL' }) or a Worker with terminate(), since timeout isn'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. Check ps afterwards.
  • Cursor Bugbot findings: reproduce them and push your own fix. Ignore Bugbot's autofix. A PR is ready only when every review thread has a reply and is resolved, and CI is green.
  • Merging: the user merges; main needs a review, and agents can't merge. Bring main into an open PR with git merge origin/main: never rebase or force-push a pushed branch.
  • Local validation: run only what a change touches (this plugin's project, plus prettier-plugin and the touched core tests for a parser change), then pnpm format and the typecheck. CI runs the rest.
  • Clean-up: after a merge, remove that PR's worktree once git status is clean and the local branch head matches the PR's last commit.
  • Changesets: changes limited to this private package need no changeset. Core or @tsrx/prettier-plugin changes need a patch changeset (major is 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-plugin so formatted output matches what JSX would render without the comment in the middle.

#639 — comment whitespace runs: The parser now calls describeCommentRuns when an element has TSRXJSXComment children. 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 a run object on each comment. printCommentChild in jsx.js uses that metadata to emit Prettier-style {" "} / line / hardline / breakParent instead of naïvely printing spaces, and skips gap nodes the comment run already owns. isEmptyDoc is 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 moves typeParameters onto value and sets value.start to 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.

leonidaz and others added 6 commits September 25, 2026 14:59


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>

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Create PR

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.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Create PR

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.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Fix All in Cursor

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.

Create PR

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.

This was referenced Sep 26, 2026
…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>
@leonidaz leonidaz changed the title tsrx-prettier-plugin: plan and next steps (continued from #511) 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) Sep 27, 2026
@leonidaz
leonidaz marked this pull request as ready for review September 27, 2026 03:20
@leonidaz
leonidaz merged commit 591053b into main Sep 27, 2026
8 checks passed
@leonidaz
leonidaz deleted the feat/tsrx-prettier-plugin-plan branch September 27, 2026 03:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment