Limitations

Everything on this page fails loudly: you get a clear error, never a silently skipped file or a wrong-but-plausible result.

This package does not compile .tsrx#

oxc-tsrx lints, formats, parses, and powers your editor. Turning .tsrx into something a browser runs is a separate job, and it belongs to your framework's TSRX plugin. That plugin already exists: the TSRX toolchain ships one for React, Preact, Solid, Vue, Ripple, and Octane, across Vite, Rspack, Turbopack, and Bun. For React on Vite it is two packages and three lines of config:

npm install @tsrx/react @tsrx/vite-plugin-react
import tsrxReact from '@tsrx/vite-plugin-react'

export default defineConfig({ plugins: [tsrxReact()] })

The TSRX getting started guide (opens in new tab) has the other framework and bundler combinations. Install one and .tsrx imports build and run like any other module.

Without a TSRX plugin, your bundler reads .tsrx as ordinary TypeScript and fails on the first @{:

$ vp build
✗ `,` or `)` expected

That error means no TSRX plugin is installed. It does not mean anything is wrong with oxc-tsrx, and installing oxc-tsrx will not fix it. The two sit on different sides of your project: the framework plugin owns compilation, CSS, source maps, and HMR, and this package owns lint, format, parse, and editor support.

oxc-tsrx cannot stand in for that plugin, and neither can its public API. oxc-tsrx/parser gives you an AST and oxc-tsrx/format gives you formatted TSRX, and neither is code a bundler can consume. The legal-TSX projection that makes linting and formatting work is lint scaffolding, not a build output: <style> payloads are replaced with null and control flow is rewritten to plain if, so running it would not run your component.

Formatting#

  • CSS inside <style> is never reformatted. The surrounding TSRX/JSX gets full Oxfmt layout, but style contents are preserved byte-for-byte. Using OXC's CSS formatter currently requires patching OXC's dependency graph, and the project's core rule is no patches, so this waits on upstream. See the embedded CSS boundary.
  • .editorconfig and formatter options that take callbacks are rejected.
  • Oxfmt options that only affect other languages (JSON, prose) do not change .tsrx output.

Syntax#

  • Dynamic tags whose name expression contains more dynamic JSX (a dynamic tag inside a dynamic tag's name) are not supported.
  • Half-typed or broken syntax is never linted or formatted approximately. The editor publishes a parse-error, returns no formatting edit, and resumes normal diagnostics once the buffer is valid again.

Linting#

  • JavaScript lint plugins on .tsrx see the projection, not your authored tree. Your jsPlugins rules do run on .tsrx, from the oxlint command and in the editor, but neither runs your module against the TSRX AST. The native path is Rust with no Node.js runtime, so the legal-TSX projection of your file is linted by the published Oxlint binary instead, and diagnostics are mapped back to your bytes. Four consequences:

    • @if, @for, @switch, and @try reach your rule as the ordinary if, for, and switch statements they project to. A rule keyed on JSXForExpression never fires on this route. Your rule does visit those projected statements, but a report on one is dropped, because its span covers text the projection wrote. A rule keyed on IfStatement fires inside Oxlint and produces nothing on a file whose if came from @if.
    • context.filename is the mirror path, ending in .tsrx.tsx, not your authored path.
    • Each linted .tsrx file is parsed once more. The cost is announced on stderr by the CLI and in the server log by the editor, and settings.oxcTsrx.jsPluginsOnTsrx: false turns the lane off.
    • A diagnostic whose position falls on projected-only text is dropped rather than reported at an invented location. Dropped reports are counted, not silent: --format=json carries oxcTsrx.jsPluginProjection.unmapped, the CLI notes the count on stderr, and the editor publishes a js-plugins-unmapped warning. The exit code is unchanged, so a CI job that wants to fail on dropped rules has to read that field itself.
    • This lane runs in CI on Linux only. Windows and macOS are exercised on every pull request now, with a real lint, a real format, live --lsp sessions, and a parser addon load (see platform support), but the suites that drive jsPlugins on .tsrx are not in that set. Nothing has spawned the Node host from the Rust process on Windows or macOS, so treat this one lane as unverified there rather than known-good, even though the platform itself is Tier 1.

    For a rule that must see authored TSRX nodes there is still no released Oxlint route. The local ESLint adapter is one escape hatch and is AST-only: the public parser v1 exposes no token stream, so SourceCode token rules cannot be correct, and there is no full framework scope contract. The upstream custom-parser draft is broader (it does provide SourceCode and forces token/range/location options), but it is unmerged and built locally, so it is not a released product path. See Custom JavaScript plugins.

  • Type-aware rules require an explicit --type-aware or --type-check opt-in and the exact supported oxlint-tsgolint executable. Missing or mismatched tooling fails instead of silently downgrading. The editor analyzes each requested document; cross-document unsaved project semantics are not claimed.

  • The oxlint-tsgolint version has to match, and Vite+ 0.2.6 ships a different one. This package runs the type-aware lane against oxlint-tsgolint 0.24.0 and refuses any other version rather than guess at the protocol. Vite+ 0.2.4 depends on exactly =0.24.0; Vite+ 0.2.6 depends on =7.0.2001, which its own oxlint uses happily and this package will not. Vite+ picks the runner with require.resolve("oxlint-tsgolint/bin/tsgolint", { paths: [process.cwd(), …] }) and passes the result to whichever oxlint it launches, so the outcome is a property of your install layout rather than of Vite+ 0.2.6 on its own:

    • Stock vp create scaffold on 0.2.6. Nothing at the project root resolves oxlint-tsgolint, so Vite+'s own 7.0.2001 wins. With lint.options.typeAware or typeCheck set, vp lint over a batch containing a .tsrx file reports the ordinary half, then stops with unsupported tsgolint version 7.0.2001 and exits 2.
    • Any layout where the project root resolves 0.24.0. Adding oxlint-tsgolint@0.24.0 as a direct dev dependency does it, and so can a workspace root, a hoisting install layout, or an ancestor node_modules that happens to contain it. Vite+ then hands 0.24.0 to both linters and the type-aware lane runs on .tsrx and on ordinary files alike. That last case is how an earlier version of this page came to report the failure and a later audit came to report the opposite: both measurements were correct about the tree they ran in.
    • A batch with no .tsrx file in it is unaffected either way, because it never reaches this package's type-aware lane.

    Measured on a vp create React scaffold with published oxc-tsrx 0.1.4 and Vite+ 0.2.6, in a directory with no ancestor node_modules, on darwin-arm64; all three states are printed on the type-aware template default.

  • vp lint and your editor read different config files. Vite+ owns lint configuration in the lint block of vite.config.ts and folds any scaffolded .oxlintrc.json into it, while the language server reads .oxlintrc.json. Measured on a fresh scaffold: a jsPlugins entry present only in .oxlintrc.json produced nothing from vp lint, and the same entry present only in vite.config.ts produced nothing in the editor. A rule you want on both surfaces has to be declared twice, in the two different shapes those files use.

  • Not every OXC rule is guaranteed to behave identically around the TSRX placeholders. The tested guarantee covers the standard rules in the test matrix; anything that would report inside placeholder code is suppressed.

  • Lint output comes out as default, agent, github, or json. Any other Oxlint format is refused on a run that includes .tsrx.

  • --disable-nested-config is not supported for .tsrx formatting.

CLI and configuration#

  • A bare npx oxlint lints node_modules too, and that is upstream behavior. With no path argument and nothing else narrowing the run, Oxlint walks the whole current directory. In a scratch project created with npm init -y that is thousands of warnings, nearly all of them from node_modules. Official Oxlint from the same install reproduces it, so this is parity with canonical Oxlint and not something the TSRX drop-in adds.

    A .gitignore containing node_modules removes it completely, and no git repository is needed for that file to count. Naming a path (npx oxlint src) avoids it as well. This project ships no ignore file, no default config, and no postinstall that would write one for you, so an empty scratch folder is the one place you will meet it.

  • npx oxlint --fix will rewrite files inside node_modules if they are in scope. Measured in a project with no source files of its own: files changed under node_modules, exit code 0, no warning. Official Oxlint does the same thing in the same folder, so again this is upstream parity. Point --fix at a path you own, or make sure node_modules is ignored first. oxfmt is not affected; it skips node_modules unless you pass --with-node-modules.

  • npx oxc-tsrx status reports missing three times in a healthy project. The first three of its four slots are the Vite+ compatibility facades, so oxc-parser: missing, oxlint: missing, and oxfmt: missing are the correct state for every command-line and editor user, and the fourth reads oxc.path.oxlint: unnecessary (editor) because the ordinary lookup already reaches this package. It exits 0. Use npx oxc-tsrx providers to check that TSRX support is wired up.

  • setup reports the TSRX editor prerequisites and never acts on them. It does not install @tsrx/typescript-plugin or a framework binding, does not edit package.json, and does not edit any tsconfig.json, so every ! line it prints is work left for you. That ceiling is deliberate: this package owns lint and format for .tsrx, and TSRX language support in the editor belongs to the TSRX toolchain. The one thing setup does write outside node_modules is the oxc.path.oxlint key in your .vscode/settings.json, and only when the ordinary lookup cannot reach this package.

  • A seventh command, tsgolint, appears in node_modules/.bin. It is not part of this project. It comes from the oxlint-tsgolint dependency, which is the official type-aware runner behind --type-aware and --type-check. You never invoke it yourself.

  • The native binaries take explicit file paths only. Directory walking and globs come from the oxc-tsrx npm commands and Vite+.

  • Config files must be JSON/JSONC. JS/TS config modules are rejected, except through the Vite+ path, where the toolchain resolves your vite.config.* once via Vite+'s public API and hands both engines the same extracted lint/fmt settings. Values that cannot be serialized (like callbacks) fail with a clear error.

Packaging and ecosystem#

  • Prepared, not published from this repository state. The eight native npm targets and the hosted release workflow are ready. Three of the eight are built and executed on every pull request, and the other five are built, packaged, and smoked only when a release candidate is built by hand. Platform support is the per-target split, and it names what the two musl packages never get. npm availability is only claimed after an explicitly approved publication.

  • Vite+ needs one command after install, permanently. Vite+ finds its lint/format tools by the literal package names oxlint and oxfmt, and it pins its own oxlint@=1.72.0. A bin name cannot answer a package resolution, and oxc-tsrx cannot legitimately publish a package under either name, so oxc-tsrx setup writes those project-local slots instead.

    Because setup works inside node_modules, a clean install wipes it and you run it again. That rerun is real and it is not scheduled to go away.

  • Everywhere except Vite+, the install is the whole step, and the oxlint / oxfmt command names are how. Counted out: one step for the command line, one step for the editor, two for Vite+, and the second Vite+ step repeats after every clean dependency install. The full table is in Getting Started.

    oxc-tsrx declares bins under those names, which is exactly what released Vite+ and the released official OXC extension select by. That is the shipped delivery mechanism, not a stopgap.

    oxc-tsrx also declares a static oxc.provider block in its own package.json, and a host that reads that block can find TSRX from the install alone. Three separate facts hold at once here, and they are easy to blur:

    • discovery is implemented and proven in CI from clean consumers on npm, pnpm, Bun, and both Yarn Berry linkers;
    • no released Oxlint, Oxfmt, Vite+, or oxc.oxc-vscode build reads oxc.provider. Nothing has been submitted upstream, nothing has been accepted, and upstream patching is not part of this project's plan, so no released host is going to start reading it;
    • so oxc.provider is a recorded proposal, and the command names plus setup are what actually deliver the product. They stay.

    Of the four capabilities that block declares, only the language server has a host, and that host is the oxlint --lsp multiplexer inside oxc-tsrx. The parser has no host at all: it is public, but you reach it by importing oxc-tsrx/parser yourself, never through discovery.

  • A project that pins official oxlint or oxfmt keeps official behavior for those command names. That is deliberate: breaking a pinned setup would be worse. .tsrx is then reachable through oxc-tsrx-lint and oxc-tsrx-fmt, which are always installed.

  • An earlier research design that would have had setup write project-owned dependency aliases, overrides, and a lockfile once is not the roadmap. It would put permanent rewrites in your own manifest to satisfy one host's resolution, which is worse than one explicit, reversible command. That design is superseded, not pending.

  • The language server has two different levels of proof. Its protocol suite runs in CI on Linux, and every pull request also starts real oxlint --lsp and oxfmt --lsp stdio sessions on Linux, Windows, and macOS. The released-official-extension integration, which drives a real extension host, is proven locally only, on darwin-arm64. The optional legacy VSIX is also proven that way, and its Marketplace availability is a separate approval-gated action that the primary editor workflow does not need.

  • OXC upgrades are manual: bump the adapter crate and lockfile, then pass the full behavior and performance suites.

What the test suites do and do not prove#

  • A published platform is not a tested platform, and the two lists differ. Eight targets are published. Three of them (linux-x64-gnu, win32-x64-msvc, darwin-arm64) run a real lint, a real format, live --lsp sessions, and a parser addon load on every pull request and on every commit that lands on main. Three more get the same work only when a release candidate is built by hand. The two musl targets have never been run on a musl system at all, and their parser addon has never been loaded anywhere, which is a limit of the runners available rather than an oversight. The whole matrix, with the job behind each row, is in platform support.
  • The pinned read-only Markless corpus proves the formatter contract on real code: 179/179 valid files format, re-parse, and converge; 12/12 known-invalid fixtures are rejected; every <style> payload survives byte-for-byte. It does not test Markless's own compiler or runtime.
  • The Vite and Vite+ build/dev/command matrices pass for the tested minimum Vite+ 0.1.24 and the pinned current Vite+ 0.2.4. The end-to-end vp command matrix runs on npm only; pnpm, Yarn, and Bun are not claimed for those commands. The package-manager facade proof and the npm vp command proof are separate axes, not one combined guarantee.
  • A disposable-copy editor walkthrough proves automatic activation, live diagnostics, real format-on-save, and one validated safe action without changing the external worktree. Automatic activation there is the optional legacy VSIX's, which declares .tsrx itself. The released official OXC extension does not: every one of its activation events is an onLanguage: entry and none of them is .tsrx's language, so a .tsrx file opened first in a session does not start it. Open an ordinary JavaScript, TypeScript, or JSON file once and the rest of the session works. See Editor integration.