Repository navigation
[Feature]: tsrx-prettier-plugin: Upgrade Prettier to 3.9.9 and re-import its format tests #833
Description
Important
Decided on 2026-09-26: tsrx-prettier-plugin supports Prettier 3.9.9 at minimum: its peer dependency becomes ^3.9.9 (3.9.9 up to, but not including, 4.0, until the plugin is tested with Prettier 4), and its tests are re-imported from Prettier 3.9.9. That's the version the re-imported tests check the plugin against, and it has the embedded-template fix (prettier/prettier#19725) that 3.9.6 lacks.
Feature Category
Testing/Quality
Problem Statement
tsrx-prettier-plugin is tested against Prettier 3.9.6, and Prettier 3.9.9 is out (released on 2026-09-23). Moving to 3.9.9 gives users one visible fix and brings the imported tests up to date. One question is still open: whether 6 test cases that use let as a variable name stay in the tests (below).
The fix users get: an embedded template, such as a css template, is formatted right the first time (prettier/prettier#19725, "Fix embedded template literal idempotency", released in 3.9.7). The plugin passes embed() through to Prettier, so the fix needs no plugin change.
Input:
css`
${() => {
return 1;
}}
`;Output today (Prettier 3.9.6), first format:
css`
${() => {
return 1;
}}
`;Output today, second format, which indents the body:
css`
${() => {
return 1;
}}
`;Expected (Prettier 3.9.9): the first format is already the stable output.
css`
${() => {
return 1;
}}
`;Open question: should the tests keep the 6 cases that use let as a name?
A .tsrx file is a strict-mode module, where let can't be a name: a compile stops with The keyword 'let' is reserved. Prettier has 6 test cases that use it as one anyway, mostly to check where Prettier keeps parentheses around let:
js/identifier/for-of/await.jsjs/identifier/for-of/let.jsjs/identifier/parentheses/const.js, and its{"semi":false}variantjs/reserved-word/let.jsjs/optional-chaining-assignment/valid-complex-case.js(let?.()[a] = 1;)
Core used to throw that error, so the import script left these cases out. Core now records it and keeps parsing (collect mode, which the plugin uses), so a re-import brings them in. The plugin formats all 6 exactly as Prettier's typescript parser does. Two of them:
js/identifier/parentheses/const.js. Input:
const [a = ((let)[0] = 1)] = 2;The plugin today, the same as Prettier:
const [a = (let[0] = 1)] = 2;js/reserved-word/let.js. Input, which the plugin and Prettier both leave unchanged:
foo.let;
let.foo;
new let();
({ let: "foo" });
(let, "foo");
void let;
var let = "foo";Neither option changes what the plugin prints. A file with let as a name formats as above either way. The question is only whether the tests check it:
| Option | Plugin output for const [a = ((let)[0] = 1)] = 2; |
Checked by the tests? | Imported cases |
|---|---|---|---|
| Decision 14: keep them | const [a = (let[0] = 1)] = 2; |
Yes, by the 6 cases | 2,047 |
| Decision 5: leave them out | const [a = (let[0] = 1)] = 2; |
No | 2,040 (7 fewer, see below) |
- Decision 14 in 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: mistakes that TypeScript reports only as diagnostics are collected, not thrown, and they still format.
letas a name is one: TypeScript's parser accepts it, and TypeScript reports it only as a diagnostic (TS1214,Identifier expected. 'let' is a reserved word in strict mode. Modules are automatically in strict mode.). So the 6 cases stay imported, and they pass. Nothing else changes. - Decision 5 in 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: a
.tsrxfile is a strict-mode ES module, and Prettier cases that aren't valid in one are left out at import. The 6 cases would join the 23 other cases that a re-import leaves out as "not valid in a strict-mode module" (29 in today's manifest, which still counts the 6), and the plugin's output for them would go untested. The import script then has to check the errors the parser collects, not only a thrown one. For example, it could parse withcollect: trueand test each collected message againstSTRICT_MODULE_ERROR. Today it calls the plugin'sparse(), which throws only for the mistakes the plugin rejects. That check also leaves outjs/non-strict/argument-name-clash.js(function f(a,a){return a}), which is imported and passing today, because core collectsArgument name clashtoo. So the import drops 7 cases, not 6.
Either way, code that core still throws for stays a formatting error. For example, var yield = 1; gives The keyword 'yield' is reserved (1:5), although TypeScript reports yield as a name the same way it reports let (TS1214).
What else the upgrade changes. The plugin is tested against Prettier 3.9.6's own format tests: 2,040 imported cases, all passing except 17 deliberate skips (tests/prettier/manifest.json). Two things are out of date:
- Prettier. The catalog has
^3.9.6(pnpm-workspace.yaml#L33) and the lockfile resolves 3.9.6. - The imported cases. They were imported before recent core changes, so they're no longer what the import script picks today.
What changed in Prettier 3.9.7–3.9.9 for JavaScript, JSX and TypeScript:
- One printing change: Fix embedded template literal idempotency prettier/prettier#19725 above. Everything else in the three releases is Markdown or Angular.
@typescript-eslint/typescript-estree8.65 → 8.70, which isn't in the changelog. It changes only thetypescriptparser's output, which the import script uses to pick cases. The estree printer's visitor keys are the same for all 297 node types.- Nothing the plugin copies or mirrors changed between the tags:
src/language-js/print/jsx.js, whichsrc/jsx.jscopies;src/language-js/parse/postprocess/, which the adapter insrc/parse.jsmirrors;src/language-js/comments/,src/main/comments/,needs-parens.js, andisNextLineEmpty, whichprintOwnComments,maybeWrapJsxElementInParensandisNextLineEmptyinsrc/printer.jsmirror.
Snapshot entries in tests/format/{js,jsx,typescript}, 3.9.6 → 3.9.9:
| Area | Added | Changed | Removed |
|---|---|---|---|
| js | 4: embeded/indention/{19518,indention,indention-2}.js, and the error entry dynamic-import/import-phase.js format[typescript] |
1: multiparser-comments/comment-inside.js |
0 |
| jsx | 0 | 1: template/styled-components.js |
0 |
| typescript | 0 | 0 | 0 |
A re-import with Prettier 3.9.9 on today's core (main at c70964d7) imports 2,047 cases. All pass: 2,030 pass and the same 17 are skipped. Compared with today's 2,040:
- +4 new cases:
js/embeded/indention/19518.js,indention.js,indention-2.js;js/import/long-module-name/import-defer.js, which typescript-estree 8.70 now prints as the snapshot does.
- 2 cases with new expected output:
js/multiparser-comments/comment-inside.jsandjsx/template/styled-components.js. The expressions inside embedded templates now indent one more level (Fix embedded template literal idempotency prettier/prettier#19725). These two fail if Prettier is bumped without re-importing. - −3 cases that typescript-estree 8.70 now rejects with
'source' is not a valid meta-property for keyword 'import'.:js/dynamic-import/import-phase.js;js/source-phase-imports/import-source-attributes-expression.js;js/source-phase-imports/import-source-dynamic-import.js.
- +6 cases from today's core, unrelated to Prettier: the
letcases above. A re-import at 3.9.6 already gives 2,046. Core now collectsThe keyword 'let' is reservedinstead of throwing it. The import script's strict-mode check (STRICT_MODULE_ERROR) only looks at a thrown error (#L136-L151), so it no longer leaves these out. All 6 pass.
Proposed Solution
-
Decide the open question above first. It decides whether the 6
letcases stay imported. -
Require Prettier 3.9.9 (decided), and refresh the lockfile:
-
The plugin's peer dependency,
packages/tsrx-prettier-plugin/package.json#L25-L27. Before and after:"peerDependencies": { "prettier": "^3.9.6" },
"peerDependencies": { "prettier": "^3.9.9" },
-
The installed version, which the plugin's
devDependenciestake from thedefaultcatalog ("prettier": "catalog:default", catalog entrypnpm-workspace.yaml#L121). The entry points at theprettier_commonanchor,pnpm-workspace.yaml#L33. Before and after:prettier_common: &prettier_common ^3.9.6
prettier_common: &prettier_common ^3.9.9
-
-
Re-import and refresh the known failures, as in 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's runbook. The known failures should stay empty.
node packages/tsrx-prettier-plugin/scripts/import-prettier-tests.js UPDATE_KNOWN_FAILURES=1 pnpm vitest run --project tsrx-prettier-plugin pnpm vitest run --project tsrx-prettier-plugin
-
The catalog entry is shared, so
@tsrx/prettier-pluginandtsrx-mcpmove to 3.9.9 as well. Run their tests too. [Bug]: prettier-plugin: A breaking ${…} expression in a CSS, GraphQL, HTML, or Markdown template needs a second pass to indent #516 describes the same embedded-template instability in@tsrx/prettier-plugin.@tsrx/prettier-pluginuses the catalog only for itsdevDependencies. Its peer range comes from thepeercatalog (>=2.0.0) and doesn't change.tsrx-mcp(@tsrx/mcp, published) lists Prettier underdependencies(packages/tsrx-mcp/package.json#L43), so its published range becomes^3.9.9, which needs apatchchangeset.
Not chosen: keeping the plugin's peer range at ^3.9.6, since the plugin works on both versions.
Alternatives Considered
Staying on 3.9.6 breaks nothing, but users don't get prettier/prettier#19725's fix, and the imported cases stay out of step with what the import script picks.
Examples & References
- Changelog: 3.9.7, 3.9.8 and 3.9.9 (Markdown only). Tags: 3.9.6 is
8f0c9505, 3.9.9 iscdd17f22. - Decisions 5 and 14 are in 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's description.
- Checked with scratch copies of the import script and the test runner, run on:
mainatc70964d7,@tsrx/core0.4.0 (workspace),tsrx-prettier-plugin0.0.1 (private);- Prettier 3.9.6 and 3.9.9;
- Node 24.18.0, macOS 26.6.2.
- The cases the import script leaves out as cursor or end-of-line tests don't change between the two versions, and neither do the range cases (see [Feature]: tsrx-prettier-plugin: Run Prettier's cursor and end-of-line format tests #832 and [Bug]: tsrx-prettier-plugin, @tsrx/prettier-plugin: Range formatting (rangeStart/rangeEnd) formats nothing, because Prettier only finds range nodes for its own parsers #831).
Script for the let examples
At c70964d7 (main), run pnpm install --frozen-lockfile, then from the repository root:
node --input-type=module <<'JS'
import prettier from 'prettier';
import plugin from './packages/tsrx-prettier-plugin/src/index.js';
import { parseModule } from './packages/tsrx/src/index.js';
const sources = [
`const [a = ((let)[0] = 1)] = 2;
`,
`foo.let;
let.foo;
new let();
({ let: "foo" });
(let, "foo");
void let;
var let = "foo";
`,
`var yield = 1;
`,
];
for (const source of sources) {
console.log('=== input');
process.stdout.write(source);
for (const [name, options] of [
['tsrx-prettier-plugin', { parser: 'tsrx', plugins: [plugin] }],
['Prettier typescript', { parser: 'typescript' }],
]) {
try {
const output = await prettier.format(source, options);
console.log(`--- ${name}${output === source ? ' (unchanged)' : ''}`);
if (output !== source) process.stdout.write(output);
} catch (error) {
console.log(`--- ${name}: error: ${error.message.split('\n')[0]}`);
}
}
try {
parseModule(source, 'App.tsrx');
console.log('--- normal parse, as in a compile: no error');
} catch (error) {
console.log(`--- normal parse, as in a compile: ${error.message}`);
}
}
JS=== input
const [a = ((let)[0] = 1)] = 2;
--- tsrx-prettier-plugin
const [a = (let[0] = 1)] = 2;
--- Prettier typescript
const [a = (let[0] = 1)] = 2;
--- normal parse, as in a compile: The keyword 'let' is reserved (1:13)
=== input
foo.let;
let.foo;
new let();
({ let: "foo" });
(let, "foo");
void let;
var let = "foo";
--- tsrx-prettier-plugin (unchanged)
--- Prettier typescript (unchanged)
--- normal parse, as in a compile: The keyword 'let' is reserved (2:0)
=== input
var yield = 1;
--- tsrx-prettier-plugin: error: The keyword 'yield' is reserved (1:5)
--- Prettier typescript (unchanged)
--- normal parse, as in a compile: The keyword 'yield' is reserved (1:4)
Checklist
- I've searched for existing feature requests
Activity
chore(tsrx-prettier-plugin): require Prettier 3.9.9 and re-import its…