@tsrx/prettier-plugin
v0.6.3
Published
TSRX plugin for Prettier
Readme
@tsrx/prettier-plugin
A Prettier plugin for formatting TSRX files.
The goal is Prettier's output wherever TSRX is TSX, so TSRX only differs where its
syntax does: @{ … } blocks, @if/@for/@switch/@try, {value} shorthand
props, <style>/<script> bodies, and comments between JSX children.
Usage
Install Prettier and the plugin as dev dependencies:
npm install -D prettier @tsrx/prettier-plugin # or pnpm add -D prettier @tsrx/prettier-pluginAdd the plugin to your Prettier config, for example
.prettierrc:{ "plugins": ["@tsrx/prettier-plugin"] }Format with Prettier:
npx prettier --write "src/**/*.tsrx" # or pnpm exec prettier --write "src/**/*.tsrx"
In your editor
The
TSRX language server
formats .tsrx files with your project's Prettier and this plugin. The
TSRX Syntax for VS Code
extension uses it, and so does any editor that formats through the language
server. You do not need the Prettier extension for VS Code.
The language server adds the plugin itself, so a .tsrx file formats in the
editor even when your Prettier config does not list the plugin. The prettier
command does not do this: for the command line, keep the plugin in your Prettier
config (step 2).
How it works
- Parser (
src/parse.js) parses with@tsrx/coreincollectmode, so mistakes TypeScript only reports as diagnostics (a redeclared variable) still format. An unclosed or mismatched tag is still an error: the parser would guess the markup's structure. So is a dynamic tag whose expression isn't an identifier, a member access or a string literal (<{getTag()} />). Then it reshapes the acorn-typescript AST into the typescript-estree shape that Prettier's printer expects:- renames, such as
superTypeParameters→superTypeArguments,accessorfields →AccessorProperty,namespace A.B→TSQualifiedName, andexport import→ExportNamedDeclaration; - the parts of Prettier's parser postprocess that its printer relies on:
__contentEnd,locEndfor statements, rebalanced logical expressions, merged touching JSDoc comments, dropped single-type unions; - a comment between JSX children, which renders like
{/* … */}in TSX, is the{…}child that{/* … */}is in the parser's tree (aJSXExpressionContainerwith aJSXEmptyExpression, and no braces in the source), so Prettier lays it out; it becomes the plugin's comment child, which prints without the braces, and aprettier-ignoreone keeps the next child as written; - comments go to Prettier as a flat list with their source text, and Prettier attaches them itself.
- renames, such as
- Printer (
src/printer.js) is Prettier'sestreeprinter fromprettier/plugins/estree. It sees the options as its owntypescriptparser's, because it checks the parser's name for TypeScript-only rules. The plugin prints the TSRX syntax:@{ … }is a block statement with an@, and@casebodies are blocks, so blank lines and comments follow Prettier's statement rules.- Directives are presented to Prettier as
do { … }expressions. A directive's head is printed while the node presents itself as its statement (IfStatement,ForOfStatement), so@if (…)breaks likeif (…). - A directive, or a
@{ … }block used as a value, lays out like a JSX element: wrapped in(…)where it's assigned, returned, thrown, or an arrow's body, with its comments inside the parentheses. While its parent prints, it presents itself as aJSXElement(withTsrxValuesAsJsx), so Prettier's JSX-specific layout applies to it. A@{ … }function or arrow body stays on its line. - An element that starts a statement keeps its parentheses where TSRX would read
it as output without them, as Prettier keeps them before a statement that
would start with
{,functionorclass: in a template body wherever it starts the statement ((<hr />) + 1;), and in any other block when it is the whole statement ((<div />);). - A
@{ … }block that is an element's or fragment's only child hugs its tags (<div>@{, the statements,}</div>), as a function's@{ … }body hugs its), and one in an expression container hugs the braces ({@{,}}), as Prettier hugs a function there. - A comment before a tag name: after a line comment, Prettier would print
<// notewith the name below it, which TSX can't parse, so the comment and the name go on their own indented lines after<, as in Prettier's closing tags. A block comment prints straight after<(</* note */ div />). <style>bodies are formatted as CSS. A<script>body is formatted as Prettier's HTML formatter formats it: itstypeorlangpicks the parser (JavaScript without either; JSON for JSON, import maps, and speculation rules; Markdown; HTML), JavaScript and TypeScript bodies are formatted with this plugin's own parser, and a body without a parser (src, an unknown type), or that its parser can't read, is kept as written on its own lines, as Prettier keeps it. WithembeddedLanguageFormatting: "off",<style>bodies are kept as written and<script>bodies are kept on their own lines.- Comments between a directive's branches are placed by Prettier's own comment
handling for
ifandtry, while@ifand@trypresent themselves as those statements. A comment before@emptyis handled like one beforeelse.
- JSX children (
src/jsx.js): a comment between children prints without the braces, as written. An element with such comments is laid out by a copy of Prettier'sprintJsxElementInternalandprintJsxChildrenwith the two rules a bare comment needs: a line comment ends its line, and it keeps whitespace before it after text or a comment (touching them,//is text); text that starts with//right after a block comment stays on its line. Every other element is printed by Prettier itself. Keep the copy in step with Prettier when upgrading.
The plugin needs no other Prettier plugin, including in prettier/standalone,
except to format JSON, HTML, or Markdown <script> bodies there.
Selection formatting
The plugin supports selection formatting (also called range formatting). Use your
editor's Format Selection command to format part of a .tsrx file, or pass
Prettier's rangeStart and rangeEnd options when formatting through its API.
In the browser
prettier/standalone loads no parser by itself, and this plugin brings only its
own and the CSS one for <style>. JavaScript and TypeScript <script> bodies are
formatted with the plugin's parser, but a JSON, HTML, or Markdown body is kept as
written unless you pass the Prettier plugins that parse and print it: Prettier
finds a body's parser and printer among all the plugins in plugins.
import * as prettier from 'prettier/standalone';
import tsrx from '@tsrx/prettier-plugin';
// JSON, import maps, and speculation rules: babel parses JSON, estree prints it
import * as babel from 'prettier/plugins/babel';
import * as estree from 'prettier/plugins/estree';
// <script type="text/html">
import * as html from 'prettier/plugins/html';
// <script type="text/markdown">
import * as markdown from 'prettier/plugins/markdown';
const formatted = await prettier.format(code, {
parser: 'tsrx',
plugins: [tsrx, babel, estree, html, markdown],
});In Node, prettier loads its own plugins, so these bodies are formatted without
passing them.
Prettier versions
The Prettier peer dependency is >=3.6.0, with no upper limit. We recommend
Prettier 3.9.9 or newer for more correct formatting. Older versions have
formatting differences and known compatibility gaps, including prettier-ignore
on template outputs.
The full formatting suite uses 3.9.9; focused import-type compatibility tests also run with 3.6.0.
Tests
tests/import-types.test.jschecks import types, comments, standalone output, cursors, and selections with both the workspace Prettier and 3.6.0. Run the older-version checks withpnpm test --project prettier-plugin-3.6.tests/tsrx.test.jscovers TSRX syntax, which Prettier's own tests don't. Each formatting test also checks that a second format changes nothing.tests/tsrx-migrated.test.jsholds every test of the previous implementation that exercises TSRX syntax (#852, Phase 2), under onedescribewith their old names. Each expects this plugin's output and that formatting it again changes nothing, and, where Prettier'stypescriptparser gives the same output, checks that too. The old tests of plain JavaScript and TypeScript aren't migrated: Prettier prints those, the imported Prettier tests run them through the TSRX parser, and core's parser tests cover the parser.tests/prettier/holds Prettier's own format tests for JavaScript, JSX, and TypeScript, imported byscripts/import-prettier-tests.js. A case is imported when Prettier'stypescriptparser prints the snapshot's output and the input is TSRX: valid TSX, in a strict-mode module.tests/prettier.test.jsruns every case through this plugin the way Prettier's harness does (with its cursor, range and line endings) and expects exactly Prettier's output.tests/prettier/manifest.jsonrecords the Prettier version, the cases that were left out and why, and the imported cases that the TSRX parser rejects.tests/prettier-known-failures.jsonlists the cases that don't pass yet. They run as expected failures, and a listed case that starts passing fails the run, so the list only shrinks. Refresh it after a change withUPDATE_KNOWN_FAILURES=1 pnpm test --project prettier-plugin.tests/prettier-overrides.jsholds the cases where TSRX deliberately differs from Prettier, each with its reason. An override skips the case (for example, syntax that is only a proposal), expects Prettier's output for the input as a.tsxfile, or replaces the input or the expected output.
The files in tests/prettier/ come from Prettier and are under
Prettier's MIT license.
Ongoing: keeping up with Prettier
The maintenance checklist from #852 applies to every Prettier upgrade:
Re-import the installed release's format tests, then review and refresh the known failures:
pnpm --filter @tsrx/prettier-plugin import-prettier-tests UPDATE_KNOWN_FAILURES=1 pnpm test --project prettier-plugin pnpm test --project prettier-plugin --project prettier-plugin-3.6The importer clones the installed release's tests, or accepts
--source <prettier checkout>. Review changes to the imported cases, overrides, and known failures: a new failure can mean a regression or a change in Prettier's output.Keep
src/jsx.jsin step with Prettier'sprintJsxElementInternalandprintJsxChildren, which it copies to handle comments between TSRX children.Check
src/parse.jsagainst Prettier'ssrc/language-js/parse/postprocess. The node shapes and metadata the printer expects can change between releases.Check range formatting in
src/range.jsagainst Prettier'sformatRangeinsrc/main/core.jsandcalculateRangeinsrc/main/range.js. It relies on Prettier passing the options object from the file's parse to the range's format, and on how Prettier grows a selection to AST nodes. For example, prettier/prettier#19880 changes how a range grows after 3.9. Verify the range and cursor tests on each upgrade, even when the release is already allowed by the peer dependency.
