Walkthrough (Vite+)
Your framework's TSRX plugin runs your app, so vp build and vp dev use that,
not this package. oxc-tsrx does the other job, checking your code rather than
running it, so vp lint, vp fmt, and vp check --fix read .tsrx instead of
skipping past it.
Five steps, one command each.
1. Get the vp command#
vp is the Vite+ command line, and it ships in the vite-plus package. Install
it in an empty folder, since step 2 creates the project inside it. Do not reach
for npx vp: vp on npm is somebody else's package.
mkdir tsrx-vp-demo && cd tsrx-vp-demo
npm install vite-plus
export PATH="$PWD/node_modules/.bin:$PATH"mkdir tsrx-vp-demo && cd tsrx-vp-demo
pnpm add vite-plus
export PATH="$PWD/node_modules/.bin:$PATH"mkdir tsrx-vp-demo && cd tsrx-vp-demo
yarn add vite-plus
export PATH="$PWD/node_modules/.bin:$PATH"mkdir tsrx-vp-demo && cd tsrx-vp-demo
bun add vite-plus
export PATH="$PWD/node_modules/.bin:$PATH"Keep the export. vp create calls vp again by bare name, so without it you
get a scaffolded app and no node_modules.
2. Example React app#
vp create vite --no-git --no-agent --no-editor --no-interactive --approve-builds \
-- my-app --template react-ts --no-eslint
cd my-appWhat you get is an ordinary React and TypeScript app that knows nothing about
.tsrx yet.
3. Add the linter and the editor toolchain#
vp install -D oxc-tsrx@0.2.3 oxlint-tsgolint@0.24.0 \
@tsrx/typescript-plugin @tsrx/reactvp install, not npm install: inside a Vite+ project your own package manager
often refuses to run at all.
EBADDEVENGINES is why.
Four packages. The first two are what lints .tsrx; the last two
belong to the TSRX toolchain rather than to oxc-tsrx, and without them
vp lint still works while your editor stays blank.
oxc-tsrx(opens in new tab) is this package, and it is what teachesoxlintandoxfmtto read.tsrxat all.oxlint-tsgolint(opens in new tab) is the type-aware lint engine avp createReact project turns on. It is pinned becauseoxc-tsrxspeaks one protocol version, and a mismatch stops.tsrxlinting.@tsrx/typescript-plugin(opens in new tab) is what gives your editor types inside.tsrx. Step 4 is where you declare it.@tsrx/react(opens in new tab) is this template's framework binding. Swap it for@tsrx/vue,@tsrx/solid,@tsrx/preact,@tsrx/ripple, oroctaneif you scaffolded something else.
One command, not four: step 4 writes inside node_modules, and anything
installed after it quietly undoes what it did.
If the install stops over peer dependencies
@tsrx/typescript-plugin declares peers a current scaffold does not satisfy,
starting with typescript (opens in new tab)
^5.9.3 against the TypeScript 6 the scaffold just gave you. What happens next
depends on which package manager vp install forwards to — it uses your
project's own manager, not a fixed one:
pnpm (and Bun) record the mismatch as a warning and finish the install. Measured with pnpm 10: all four packages land, exit 0, nothing else to do.
npm stops the whole install with
ERESOLVE. Re-run with the flag npm's own error message names, forwarded through the bare--sovp installhands it to npm instead of reading it itself:vp install -D oxc-tsrx@0.2.3 oxlint-tsgolint@0.24.0 \ @tsrx/typescript-plugin @tsrx/react -- --legacy-peer-deps--legacy-peer-depsis npm's, not pnpm's: under pnpm it fails as an unknown option, which is why the command above does not carry it.
Keeping TypeScript 6 is the point either way. Every step here was run on 6.0.3,
and vp lint reports both files exactly as step 5 shows. setup does mention
that 6 is outside the plugin's declared range; that note is expected rather
than something to fix.
4. Run setup, and let it declare the plugin#
One command does two things:
vp exec oxc-tsrx setup --write-tsconfigVite+ finds its linter and formatter by package name, and setup puts
oxc-tsrx in those two slots. --write-tsconfig adds the
TypeScript (opens in new tab) plugin your
editor needs to the tsconfig that owns your source. It prints what it did:
- oxc-parseractive
- oxlintactive
- oxfmtactive
- oxc.path.oxlintunnecessary (editor)
- tsconfig.app.jsonwritten (tsconfig)
Run this after every install, never before one. If an install does land on top of it, see when something goes wrong.
What --write-tsconfig writes, and where
It adds this under compilerOptions:
"plugins": [{ "name": "@tsrx/typescript-plugin" }],The line above says tsconfig.app.json rather than tsconfig.json because a
scaffold's root config only points at other configs and owns no files itself, so
a plugin declared there does nothing. --write-tsconfig follows the reference to
the project that actually holds your source.
It splices that one entry in and leaves every other byte alone, comments
included. Drop the flag and setup goes back to only telling you the entry is
missing. If your compilerOptions already has a plugins list, it says so
instead of appending to a list it did not write.
5. Add a .tsrx file and lint it#
Five files: two components, a lint rule of your own that catches both, and the
two config files that point at it. tar only writes them, nothing is executed,
and every one of them is listed below:
curl -sL https://github.com/markless-dev/oxc-tsrx/archive/refs/heads/main.tar.gz \
| tar -xz --strip-components=4 oxc-tsrx-main/examples/custom-js-plugins/vite-plus
vp lint5 files. Open any of them to read what lands on disk, before you run anything.
.oxlintrc.jsonturns the rule on. This is the file your editor reads
{ "jsPlugins": ["./house-rules.mjs"], "rules": { "house-rules/no-inline-style-object": "warn" } }house-rules.mjsthe rule itself, an ordinary Oxlint JavaScript plugin
// A house rule: an ordinary Oxlint JavaScript plugin. The default export is // `{ meta, rules }`, and each rule's `create(context)` returns a visitor keyed // by AST node type. Nothing here is TSRX-specific. const noInlineStyleObject = { meta: { type: "suggestion", docs: { description: "Prefer a class over an inline style object" }, messages: { inline: "Inline `style={{ ... }}` object. Use a class instead." }, schema: [], }, create(context) { return { JSXAttribute(node) { if (node.name?.name !== "style") return; if (node.value?.type !== "JSXExpressionContainer") return; if (node.value.expression?.type !== "ObjectExpression") return; context.report({ node, messageId: "inline" }); }, }; }, }; export default { meta: { name: "house-rules", version: "1.0.0" }, rules: { "no-inline-style-object": noInlineStyleObject }, };vite.config.tswhat
vp lintreads, replacing the scaffold'simport { defineConfig, lazyPlugins } from "vite-plus"; import react from "@vitejs/plugin-react"; // https://vite.dev/config/ export default defineConfig({ staged: { "*": "vp check --fix", }, fmt: {}, lint: { plugins: ["react", "typescript", "oxc"], rules: { "react/rules-of-hooks": "error", "react/only-export-components": [ "warn", { allowConstantExport: true, }, ], "vite-plus/prefer-vite-plus-imports": "error", "house-rules/no-inline-style-object": "warn", }, options: { typeAware: true, typeCheck: true, }, jsPlugins: [ { name: "vite-plus", specifier: "vite-plus/oxlint-plugin", }, { name: "house-rules", specifier: "./house-rules.mjs", }, ], }, plugins: lazyPlugins(() => [react()]), });- src/
Greeting.tsrxthe
.tsrxcomponenttype Guest = { id: string; name: string }; export function Greeting({ guests, ready }: { guests: Guest[]; ready: boolean }) @{ @if (ready) { <ul style={{ padding: 0 }}> @for (const guest of guests; key guest.id) { <li>{guest.name}</li>; } </ul>; } @else { <p>Loading</p>; } }Panel.tsxan ordinary
.tsxcomponent, so you see both flaggedexport function Panel({ label }: { label: string }) { return <section style={{ margin: 0 }}>{label}</section>; }
A working setup reports your own rule twice, once from each component:
# One command, one top-level jsPlugins, both file types node_modules/oxc-tsrx/bin/oxlint src src/Greeting.tsrx:5:9: warning house-rules(no-inline-style-object): Inline `style={{ ... }}` object. Use a class instead. src/Panel.tsx:2:19: warning house-rules(no-inline-style-object): Inline `style={{ ... }}` object. Use a class instead. Found 2 warnings and 0 errors. Finished in 70ms on 2 files with 96 rules using 18 threads. oxlint (oxc-tsrx): running JS plugins on 1 .tsrx file(s) by linting the TSX projection; this parses each of those files once more. Disable with "settings": { "oxcTsrx": { "jsPluginsOnTsrx": false } }.
The .tsrx line is the one that proves it: without oxc-tsrx that file is not
linted at all. If only the .tsx line appears, see vp lint reports .tsx and
skips .tsrx.
The five files live in
examples/custom-js-plugins/vite-plus (opens in new tab),
where CI runs four of them on every change. Custom JavaScript
plugins builds them up one
at a time.
Why the recording runs oxlint rather than vp lint
Recordings on this site are captured when the site is built, and that build has
no Vite+ in it, so the run above calls oxc-tsrx's own oxlint on the same
files instead. vp lint reaches the same linter through Vite+ and prints the
same two diagnostics at the same positions. That was measured on a real
scaffold; it is just not something the build can record for you.
6. Make the editor agree#
Steps 3 and 4 install and declare what the editor needs. Two things are left,
and both are outside oxc-tsrx. They are the same two extensions Getting
Started asks for, repeated here so you
do not have to go back. If you installed them there, you are already done.
Install the official OXC extension. That is what gives .tsrx diagnostics,
formatting, and quick fixes:
oxc.oxc-vscode
Install (opens in new tab)
One catch: it does not start on a .tsrx file. Open any JavaScript, TypeScript,
or JSON file once, and .tsrx works for the rest of the session.
Syntax highlighting and types are a different job, owned by the TSRX toolchain
rather than by oxc-tsrx. Its extension is what provides them:
ripple-ts.ripple-ts-vscode-plugin
Install (opens in new tab)
That is the whole setup. The TSRX extension brings its own language server and finds a TypeScript to run it against on its own, so there is nothing to point at and no setting to add.
If the editor still shows nothing, oxc-tsrx status lists every prerequisite it
can see. An empty list means the gap is on the editor's side, not your
project's. The editor page covers both extensions in
full.
The setup report back in step 4 says unnecessary (editor) because this
walkthrough's tree needs no setting at all. setup checks that rather than
assuming it: it replays the extension's own lookup from my-app and from every
folder above it that looks like a workspace root, and if one of them would run a
different oxlint the report says inert (editor) and names it. When setup
does write oxc.path.oxlint, open the folder holding that
.vscode/settings.json, meaning my-app itself: VS Code reads the file only
from the folder you open, so opening a folder above it makes the key inert and
status says so. When the key does not take
effect has the rest.
When typescript.tsdk actually matters
Not for .tsrx files. Version 2.0.69 of the TSRX extension ships its own
language server and picks a TypeScript through Volar's usual lookup, falling
back to the copy bundled with VS Code when your workspace has no
typescript.tsdk set. It registers no TypeScript server plugin, so nothing
about .tsrx editing depends on which TypeScript your editor chose.
The setting matters for the other direction: an ordinary .ts or .tsx file
importing a .tsrx module is typed by VS Code's own TypeScript service, and a
plugin declared in tsconfig.json loads there only when the editor runs your
project's TypeScript rather than its bundled copy. If those imports come back
untyped, set "typescript.tsdk" to "node_modules/typescript/lib" in
.vscode/settings.json and run TypeScript: Select TypeScript Version.
setup merges only its own key into that file, so the two coexist.
Adding this to a project you already have#
If your project is not on Vite+ yet, these two lines add Vite+ and this package, and the walkthrough above is the rest:
npm install --save-dev vite-plus oxc-tsrx@0.2.3
npx oxc-tsrx setuppnpm add -D vite-plus oxc-tsrx@0.2.3
pnpm exec oxc-tsrx setupyarn add -D vite-plus oxc-tsrx@0.2.3
yarn oxc-tsrx setupbun add -d vite-plus oxc-tsrx@0.2.3
bunx oxc-tsrx setupIf it is already a Vite+ project, use vp install -D and vp exec instead.
When something goes wrong#
Three things go wrong often enough to name. Pick what you saw rather than reading all three.
EBADDEVENGINES
A vp create scaffold pins the exact version of whichever package manager made it, in devEngines, and a switcher like fnm or nvm makes it easy to be on a different one. Installs and npx in that directory then stop before doing anything. onFail: "download" reads like it should fetch the right version; it does not. Use vp install and vp exec rather than your own manager: they run against the manager Vite+ manages, so the pin is always satisfied.
vp lint reports .tsx and skips .tsrx
This package speaks one oxlint-tsgolint protocol version and refuses the rest, so your ordinary files keep linting while your .tsrx files quietly stop. The last line of the run names both versions: unsupported tsgolint version <theirs>; OXC for TSRX requires oxlint-tsgolint <ours> for protocol v2. Install the version it names, in the same vp install as oxc-tsrx and before setup.
refusing to replace unowned package slot(s)
An install landed after setup and rewrote node_modules, taking the slots with it. setup will not silently reclaim a slot it no longer owns, and installing on top of the old tree does not free it. Rebuild the tree with rm -rf node_modules, then vp install, then vp exec oxc-tsrx setup.