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-reactimport 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 `)` expectedThat 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. .editorconfigand formatter options that take callbacks are rejected.- Oxfmt options that only affect other languages (JSON, prose) do not change
.tsrxoutput.
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
.tsrxsee the projection, not your authored tree. YourjsPluginsrules do run on.tsrx, from theoxlintcommand 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@tryreach your rule as the ordinaryif,for, andswitchstatements they project to. A rule keyed onJSXForExpressionnever 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 onIfStatementfires inside Oxlint and produces nothing on a file whoseifcame from@if.context.filenameis the mirror path, ending in.tsrx.tsx, not your authored path.- Each linted
.tsrxfile is parsed once more. The cost is announced on stderr by the CLI and in the server log by the editor, andsettings.oxcTsrx.jsPluginsOnTsrx: falseturns 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=jsoncarriesoxcTsrx.jsPluginProjection.unmapped, the CLI notes the count on stderr, and the editor publishes ajs-plugins-unmappedwarning. 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
--lspsessions, and a parser addon load (see platform support), but the suites that drivejsPluginson.tsrxare 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
SourceCodetoken rules cannot be correct, and there is no full framework scope contract. The upstream custom-parser draft is broader (it does provideSourceCodeand 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-awareor--type-checkopt-in and the exact supportedoxlint-tsgolintexecutable. 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-tsgolintversion has to match, and Vite+ 0.2.6 ships a different one. This package runs the type-aware lane againstoxlint-tsgolint0.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 ownoxlintuses happily and this package will not. Vite+ picks the runner withrequire.resolve("oxlint-tsgolint/bin/tsgolint", { paths: [process.cwd(), …] })and passes the result to whicheveroxlintit launches, so the outcome is a property of your install layout rather than of Vite+ 0.2.6 on its own:- Stock
vp createscaffold on 0.2.6. Nothing at the project root resolvesoxlint-tsgolint, so Vite+'s own 7.0.2001 wins. Withlint.options.typeAwareortypeCheckset,vp lintover a batch containing a.tsrxfile reports the ordinary half, then stops withunsupported tsgolint version 7.0.2001and exits 2. - Any layout where the project root resolves 0.24.0. Adding
oxlint-tsgolint@0.24.0as a direct dev dependency does it, and so can a workspace root, a hoisting install layout, or an ancestornode_modulesthat happens to contain it. Vite+ then hands 0.24.0 to both linters and the type-aware lane runs on.tsrxand 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
.tsrxfile in it is unaffected either way, because it never reaches this package's type-aware lane.
Measured on a
vp createReact scaffold with publishedoxc-tsrx0.1.4 and Vite+ 0.2.6, in a directory with no ancestornode_modules, on darwin-arm64; all three states are printed on the type-aware template default.- Stock
vp lintand your editor read different config files. Vite+ owns lint configuration in thelintblock ofvite.config.tsand folds any scaffolded.oxlintrc.jsoninto it, while the language server reads.oxlintrc.json. Measured on a fresh scaffold: ajsPluginsentry present only in.oxlintrc.jsonproduced nothing fromvp lint, and the same entry present only invite.config.tsproduced 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, orjson. Any other Oxlint format is refused on a run that includes.tsrx.--disable-nested-configis not supported for.tsrxformatting.
CLI and configuration#
A bare
npx oxlintlintsnode_modulestoo, 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 withnpm init -ythat is thousands of warnings, nearly all of them fromnode_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
.gitignorecontainingnode_modulesremoves 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 --fixwill rewrite files insidenode_modulesif they are in scope. Measured in a project with no source files of its own: files changed undernode_modules, exit code 0, no warning. Official Oxlint does the same thing in the same folder, so again this is upstream parity. Point--fixat a path you own, or make surenode_modulesis ignored first.oxfmtis not affected; it skipsnode_modulesunless you pass--with-node-modules.npx oxc-tsrx statusreportsmissingthree times in a healthy project. The first three of its four slots are the Vite+ compatibility facades, sooxc-parser: missing,oxlint: missing, andoxfmt: missingare the correct state for every command-line and editor user, and the fourth readsoxc.path.oxlint: unnecessary (editor)because the ordinary lookup already reaches this package. It exits 0. Usenpx oxc-tsrx providersto check that TSRX support is wired up.setupreports the TSRX editor prerequisites and never acts on them. It does not install@tsrx/typescript-pluginor a framework binding, does not editpackage.json, and does not edit anytsconfig.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 thingsetupdoes write outsidenode_modulesis theoxc.path.oxlintkey in your.vscode/settings.json, and only when the ordinary lookup cannot reach this package.A seventh command,
tsgolint, appears innode_modules/.bin. It is not part of this project. It comes from theoxlint-tsgolintdependency, which is the official type-aware runner behind--type-awareand--type-check. You never invoke it yourself.The native binaries take explicit file paths only. Directory walking and globs come from the
oxc-tsrxnpm 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 extractedlint/fmtsettings. 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
oxlintandoxfmt, and it pins its ownoxlint@=1.72.0. A bin name cannot answer a package resolution, andoxc-tsrxcannot legitimately publish a package under either name, sooxc-tsrx setupwrites those project-local slots instead.Because
setupworks insidenode_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/oxfmtcommand 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-tsrxdeclares 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-tsrxalso declares a staticoxc.providerblock in its ownpackage.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-vscodebuild readsoxc.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.provideris a recorded proposal, and the command names plussetupare 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 --lspmultiplexer insideoxc-tsrx. The parser has no host at all: it is public, but you reach it by importingoxc-tsrx/parseryourself, never through discovery.A project that pins official
oxlintoroxfmtkeeps official behavior for those command names. That is deliberate: breaking a pinned setup would be worse..tsrxis then reachable throughoxc-tsrx-lintandoxc-tsrx-fmt, which are always installed.An earlier research design that would have had
setupwrite 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 --lspandoxfmt --lspstdio 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--lspsessions, and a parser addon load on every pull request and on every commit that lands onmain. 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
vpcommand matrix runs on npm only; pnpm, Yarn, and Bun are not claimed for those commands. The package-manager facade proof and the npmvpcommand 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
.tsrxitself. The released official OXC extension does not: every one of its activation events is anonLanguage:entry and none of them is.tsrx's language, so a.tsrxfile 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.