Skip to content

[Feature]: tsrx-prettier-plugin: Upgrade Prettier to 3.9.9 and re-import its format tests #833

Closed

Description

@leonidaz

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.js
  • js/identifier/for-of/let.js
  • js/identifier/parentheses/const.js, and its {"semi":false} variant
  • js/reserved-word/let.js
  • js/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)

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-estree 8.65 → 8.70, which isn't in the changelog. It changes only the typescript parser'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, which src/jsx.js copies;
    • src/language-js/parse/postprocess/, which the adapter in src/parse.js mirrors;
    • src/language-js/comments/, src/main/comments/, needs-parens.js, and isNextLineEmpty, which printOwnComments, maybeWrapJsxElementInParens and isNextLineEmpty in src/printer.js mirror.

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.js and jsx/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 let cases above. A re-import at 3.9.6 already gives 2,046. Core now collects The keyword 'let' is reserved instead 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

  1. Decide the open question above first. It decides whether the 6 let cases stay imported.

  2. 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 devDependencies take from the default catalog ("prettier": "catalog:default", catalog entry pnpm-workspace.yaml#L121). The entry points at the prettier_common anchor, pnpm-workspace.yaml#L33. Before and after:

      prettier_common: &prettier_common ^3.9.6
      prettier_common: &prettier_common ^3.9.9
  3. 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
  4. The catalog entry is shared, so @tsrx/prettier-plugin and tsrx-mcp move 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-plugin uses the catalog only for its devDependencies. Its peer range comes from the peer catalog (>=2.0.0) and doesn't change.
    • tsrx-mcp (@tsrx/mcp, published) lists Prettier under dependencies (packages/tsrx-mcp/package.json#L43), so its published range becomes ^3.9.9, which needs a patch changeset.

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

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions