Vite and Vite+ integration
Install#
You need Node.js 20.19 or newer. Install Vite+ plus the one public TSRX toolchain:
npm install --save-dev vite-plus oxc-tsrx@latestpnpm add -D vite-plus oxc-tsrx@latestyarn add -D vite-plus oxc-tsrx@latestbun add -d vite-plus oxc-tsrx@latestoxc-tsrx names a version on purpose. A resolver that holds new releases
back for their first day can otherwise install an older one, report success, and
leave you with a build whose parser export throws.
Getting the vp command in the first place#
Nothing above gives you vp, because vp is what you use to create the project
that will contain the install. There is no separate installer for it: run it
straight from the registry, naming the Vite+ version you want.
npx --yes -p vite-plus@0.2.6 vp create vite -- my-app --template react-ts
cd my-appInside the project, vp is on node_modules/.bin, so your package manager
reaches it: pnpm exec vp lint, yarn vp lint, bunx vp lint, npx vp lint.
The examples on this page write plain vp for readability.
That install is everything a host needs in order to discover TSRX.
oxc-tsrx declares a static oxc.provider block in its own package.json, and
a host that performs provider discovery reads that JSON to find which files the
package owns (.tsrx) plus the parser, linter, formatter, and language server
to use for them. No alias, no overrides block, no activation command, and
nothing written into node_modules afterwards. Run
npx oxc-tsrx providers --json to see the index; it writes nothing.
Vite+ is not one of those hosts yet, so on this page the install is not the whole story. Keep reading.
The one extra step Vite+ needs#
Vite+ finds its lint and format tools through project-local
packages named literally oxlint and oxfmt
(join(dirname(dirname(resolve("oxlint"))), "bin", "oxlint")). It reads no
oxc.provider metadata. Activate oxc-tsrx's qualified, reversible
project-local facades after installation:
pnpm exec oxc-tsrx setup # pnpm
yarn oxc-tsrx setup # yarn
bunx oxc-tsrx setup # bun
npx oxc-tsrx setup # npmRun it with your own package manager, not with npx, unless npm is your
package manager. vp create writes a devEngines.packageManager block into
package.json, and npm refuses to run in a project that declares a different
manager:
npm error code EBADDEVENGINES
npm error Invalid name "pnpm" does not match "npm"That is npm enforcing your own project's declaration, not an oxc-tsrx
failure. Any modern version of your package manager works; nothing here needs
Corepack.
One related case worth knowing, because it is also not an oxc-tsrx failure:
vp create populates node_modules with a pnpm it downloads itself. If the
pnpm on your own path is a different major version, the first command you run
in the project stops with ERR_PNPM_UNEXPECTED_STORE. Run pnpm install once,
which is the fix pnpm itself prints, and your pnpm takes the store over. You
still do not need a specific pnpm version, and you still do not need Corepack.
The command never edits package.json, never runs as an install lifecycle
script, refuses direct or unrecognized package collisions, and is idempotent.
Use oxc-tsrx status to inspect it and oxc-tsrx remove to restore transitive
official packages. Run setup again after a clean dependency install.
setup writes one file in your tree, and it says so#
The three facades above go into node_modules. The editor needs a fourth slot,
and that one is not in node_modules.
The official OXC extension finds its linter through
node_modules/.bin/oxlint. In a Vite+ project that shim is Vite+'s own wrapper,
which knows nothing about .tsrx, so fixing package resolution does nothing for
the editor: you get no .tsrx diagnostics and no error saying why. The only
thing that reaches the extension is a setting. setup writes it:
{
"oxc.path.oxlint": "node_modules/oxc-tsrx/bin/oxlint"
}That file is .vscode/settings.json, and it is yours, not node_modules. This
is the one place setup writes outside node_modules, which is why every
setup, status, and remove run names the file on its own line rather than
doing it quietly. Four rules bound it:
- Only when needed. If
node_modules/.bin/oxlintalready resolves into this package, which is every project that does not use Vite+, nothing is written and the slot reportsunnecessary. - Merge, never clobber. An existing
.vscode/settings.jsonkeeps every other key, every comment, and its own formatting. The key is spliced in by byte offset, so the rest of the file comes back byte for byte. - Your value wins. If
oxc.path.oxlintis already set to something else,setupreports the conflict and leaves it, exactly the way it refuses to replace a package slot you own. - Reversible.
oxc-tsrx removetakes back that one key, and deletes the file or the.vscodedirectory only whensetupcreated it and nothing else is left in it.
package.json and tsconfig.json are never edited by any of this.
What setup checks but will not touch#
.tsrx as a language in the editor belongs to the TSRX toolchain, not to this
package, so setup configures none of it. It does check, because a green bridge
plus a dead editor otherwise gives you no way to tell which half is missing. Each
missing item gets one line telling you what to do:
@tsrx/typescript-pluginresolvable from the project;- a framework binding:
@tsrx/react,@tsrx/vue,@tsrx/solid,@tsrx/preact,@tsrx/ripple, oroctane; - the tsconfig that owns your source declaring
"plugins": [{ "name": "@tsrx/typescript-plugin" }]; and - TypeScript inside
@tsrx/typescript-plugin's declared peer range,>=5.9 <6.
Nothing on that list is installed, edited, or upgraded for you.
Two of those four are worth stating precisely, because guessing at them is expensive.
The tsconfig is the referenced project, not the root. A vp create scaffold
writes a solution-style root, { "files": [], "references": [...] }, which owns
no files, so a plugin declared there applies to nothing and fails by doing
nothing. In a React scaffold the one that owns src is tsconfig.app.json, and
setup prints that path rather than saying "your tsconfig" for exactly this
reason.
The TypeScript line is a declared range, not a known failure.
@tsrx/typescript-plugin declares peerDependencies.typescript: ^5.9.3 and
vp create scaffolds TypeScript 6, so a stock project sits outside the declared
range and your package manager will say so. A stock scaffold on TypeScript 6.0.3
was measured working, so treat it as unsupported rather than broken; if the
language service does misbehave, pinning TypeScript into the declared range is
the first thing to try.
Custom JavaScript plugins
walks that whole sequence, from vp create to your own rule reporting on a
.tsrx file.
See the editor page for what the lookup actually resolves to.
So Vite+ is two steps: the install, then setup. Every other host is one step,
the install on its own. The table of all three is in
Getting Started.
The type-aware template default#
A vp create React scaffold turns type-aware lint on, and on Vite+ 0.2.6 that
default needs one more dependency before vp lint can read a .tsrx file. The
scaffold writes this lint block into vite.config.ts:
lint: {
plugins: ["react", "typescript", "oxc"],
rules: {
"react/rules-of-hooks": "error",
"vite-plus/prefer-vite-plus-imports": "error",
},
options: { typeAware: true, typeCheck: true },
jsPlugins: [
{ name: "vite-plus", specifier: "vite-plus/oxlint-plugin" },
],
}Every line of it can stay. jsPlugins works on both halves of the project:
ordinary files reach canonical Oxlint directly, and .tsrx files are linted
through their TSX projection, which costs one extra parse per file and is
announced on stderr each time. Custom JavaScript
plugins explains that route and the
settings.oxcTsrx.jsPluginsOnTsrx key that switches it off.
What the block needs is a matching type-aware runner. Vite+ 0.2.6 depends on
oxlint-tsgolint 7.0.2001; this package speaks protocol v2 and runs the lane
only against 0.24.0, refusing any other version rather than guessing. On a stock
scaffold Vite+ hands over its own 7.0.2001 and the .tsrx half stops.
The three runs below share one project: a src/Counter.tsrx with a debugger
and a const label: string = start, and a src/Bad.tsx with a
const n: number = label. Each is the real output, stdout then stderr.
$ vp lint src
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 } }.
src/Bad.tsx:2:9: error typescript(TS2322): Type 'string' is not assignable to type 'number'.
Found 1 error(s) and 0 warning(s).
oxlint (oxc-tsrx): unsupported tsgolint version 7.0.2001; OXC for TSRX requires oxlint-tsgolint 0.24.0 for protocol v2Exit code 2. The ordinary half of the batch reported normally; the .tsrx half
reported nothing, including the debugger a syntax-only rule would have caught.
A batch with no .tsrx file in it is unaffected, which is why a stock scaffold
lints cleanly until you add your first TSRX component.
The fix that keeps type-aware#
Add the version this package runs as a direct dev dependency of your project:
npm install --save-dev oxlint-tsgolint@0.24.0pnpm add -D oxlint-tsgolint@0.24.0yarn add -D oxlint-tsgolint@0.24.0bun add -d oxlint-tsgolint@0.24.0Vite+ picks the type-aware runner with
require.resolve("oxlint-tsgolint/bin/tsgolint", { paths: [process.cwd(), …] })
and passes the result to whichever oxlint it launches, so your project's own
copy is the first one it finds and the one both linters then use. Same project,
options left exactly as vp create wrote it:
$ vp lint src
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 } }.
src/Counter.tsrx:2:3: warning eslint(no-debugger): `debugger` statement is not allowed
src/Counter.tsrx:3:9: error typescript(TS2322): Type 'number' is not assignable to type 'string'.
src/Bad.tsx:2:9: error typescript(TS2322): Type 'string' is not assignable to type 'number'.
Found 2 error(s) and 1 warning(s).Exit code 1: type-aware diagnostics on both halves, at the positions you wrote.
Vite+'s own lane accepted 0.24.0 for the .tsx file in that run. It is an older
runner than the one Vite+ ships, so if you depend on a type-aware rule only
7.0.2001 has, check that rule after making the change.
The alternative, if you would rather not add a dependency#
Delete options from the lint block and leave everything else. Same project,
no oxlint-tsgolint dependency:
$ vp lint src
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 } }.
src/Counter.tsrx:2:3: warning eslint(no-debugger): `debugger` statement is not allowed
Found 0 error(s) and 1 warning(s).Exit code 0. You keep the plugins list, every rules entry, and your
jsPlugins, on both file types, and the .tsrx half is linted again. What you
give up is type-aware lint for the whole project, ordinary files included: both
TS2322 errors are gone from that run. That is why it is the second choice
rather than the first.
Which layout you are in#
This is a resolution result, not a fixed property of Vite+ 0.2.6, so it is worth knowing which side you are on before you change anything:
Can your project root resolve oxlint-tsgolint 0.24.0? |
vp lint on a batch containing .tsrx |
|---|---|
No — a stock vp create scaffold on Vite+ 0.2.6 |
exits 2, unsupported tsgolint version 7.0.2001 |
| Yes — you added it, or a workspace root or hoisting install layout supplies it | works, type-aware included |
| Vite+ 0.2.4 | works: it depends on 0.24.0 itself |
The check is one line, from your project root:
node -e "console.log(require.resolve('oxlint-tsgolint/bin/tsgolint',{paths:[process.cwd()]}))"If that prints a path under a 0.24.0 directory, vp lint already works and
you need neither edit. This is also why the same scaffold can behave differently
on two machines: a project nested under a directory that happens to have
oxlint-tsgolint 0.24.0 in an ancestor node_modules inherits it, and an
earlier version of this page reported that layout's result as everyone's.
vp lint reads vite.config.ts, and the editor reads .oxlintrc.json#
These are two different config files, and a plugin declared in one is invisible
to the other. vp create folds any scaffolded .oxlintrc.json into the lint
block of vite.config.ts on the way out, and from then on vp lint reads that
block and nothing else. The language server the official OXC extension talks to
reads .oxlintrc.json, because that is what Oxlint reads everywhere else.
Measured on a fresh scaffold: a jsPlugins entry present only in
.oxlintrc.json produced no diagnostics from vp lint, and the same entry
present only in vite.config.ts produced none in the editor. If you want your
own rule on both surfaces, declare it in both files. The vite.config.ts form
is a { name, specifier } object in lint.jsPlugins; the .oxlintrc.json form
is a path string in jsPlugins.
Custom JavaScript plugins
shows both, filled in.
oxlint and oxfmt on the command line belong to Vite+ here#
In a Vite+ project, node_modules/.bin/oxlint is Vite+'s own wrapper, not this
package's, and it refuses to lint:
$ pnpm exec oxlint src/Counter.tsrx
This oxlint wrapper is for IDE extension use only (--lsp mode).
To lint your code, run: vp lintThat is Vite+ telling you to go through vp, and it is correct. Use vp lint
and vp fmt in a Vite+ project. The direct oxlint and oxfmt commands
described elsewhere in these docs are for projects that do not use Vite+.
status is about these four slots and nothing else, which matters if you read it
before running setup or in a project that does not use Vite+ at all:
$ pnpm exec oxc-tsrx status
oxc-tsrx 0.1.4 compatibility (pnpm)
- oxc-parser: missing
- oxlint: missing
- oxfmt: missing
- oxc.path.oxlint: missing (editor)
…/node_modules/.bin/oxlint does not resolve into this package, so the official
OXC extension would find no .tsrx support and say nothing about it. …missing with exit code 0 means the slot is not installed. On this page that is
the state setup is about to change. Outside Vite+ the first three lines stay
missing and the fourth reads unnecessary, and that is the correct, healthy
state with nothing to fix. To confirm that TSRX support itself is wired up, run
oxc-tsrx providers and look for routed extensions: .tsrx -> oxc-tsrx.
This step is permanent. It is not a shim waiting to be deleted, and it is
not something a future oxc-tsrx release removes. Two facts about Vite+ make it
structural:
- Vite+ resolves a package named
oxlint, the same wayimport "oxlint"resolves. A bin name cannot answer a package resolution, andoxc-tsrxcannot legitimately publish a package under that name. - Vite+ pins its own
oxlintdependency exactly (=1.72.0on 0.2.4,=1.75.0on 0.2.6), so that package slot is already filled. Only a project-local package sitting in that slot changes what Vite+ resolves, and writing that slot is precisely whatsetupdoes.
oxc.provider does not change this either. It is a protocol proposed to OXC:
nothing has been submitted upstream, nothing has been accepted, and upstream
patching is not part of this project's plan. So no released Vite+ reads it, and
none is going to.
Everywhere else, an install is the whole story. Vite+ is the single integration where it is not.
Quick start#
With the install and that setup step done, the ordinary Vite+ commands handle
.tsrx files natively:
npx vp lint # lint every file, .tsrx included
npx vp fmt --check # list files the formatter would change
npx vp check --fix # lint and format together, applying safe fixesvp lint and vp fmt route ordinary .js/.jsx/.ts/.tsx files to
canonical Oxlint and Oxfmt, and .tsrx files to the native TSRX support.
Diagnostics point at the lines you wrote, and vp build / vp dev are
untouched because linting and formatting sit outside the build pipeline.
Architecture#
OXC for TSRX does not compile application modules for Vite. Your framework's
official TSRX plugin (for example @tsrx/vite-plugin-react, installed per
tsrx.dev/getting-started (opens in new tab), or Markless's
plugin) still owns runtime semantics, CSS extraction, source maps, and HMR, so
vp build and vp dev flow through that plugin into Vite/Rolldown exactly as
before.
The integration uses Vite+'s public project-local tool resolution. The explicit
setup command provides exact oxlint and oxfmt facades backed by
oxc-tsrx; those facades keep the canonical root APIs and expected
dist/index.js plus bin/* layout. Vite+ resolves them without a fork or
private imports.
Selection by literal name has real downsides. It depends on install layout and
name ownership rather than on a declared capability, and two tools cannot own
one name. oxc.provider is the shape this project would prefer instead, and it
is recorded as a proposal for that reason.
It is a proposal only. No released host reads it, upstream patching is not part
of this project's plan, and so the two mechanisms never overlap in practice: the
facades are how Vite+ reaches TSRX, and the provider block is read only by the
hosts inside this repository. Neither the facades nor the oxlint/oxfmt names
are ever provider capability targets.
Consumer shape#
The consumer manifest is:
{
"devDependencies": {
"vite-plus": "0.2.6",
"oxc-tsrx": "0.1.4",
"oxlint-tsgolint": "0.24.0"
}
}The third entry is the type-aware runner from the type-aware template default, and it is needed only on Vite+ 0.2.6 with type-aware lint left on.
The setup command does not add dependencies to this manifest. oxc-tsrx lists
the eight @oxc-tsrx/native-* platform packages as optionalDependencies and
resolves the matching one itself, so you never name a platform package. During
source development, OXC_TSRX_LINT_BIN and
OXC_TSRX_FORMAT_BIN select release binaries explicitly. A missing native
artifact is an error; .tsrx is never silently delegated to stock tools.
Use the framework plugin exactly as its framework documents. No OXC for TSRX Vite plugin is required for compilation:
import { tsrxReact } from '@tsrx/vite-plugin-react';
import { defineConfig } from 'vite-plus';
export default defineConfig({
plugins: [tsrxReact()],
lint: {
plugins: ['typescript'],
rules: {
'no-debugger': 'error',
'typescript/no-floating-promises': 'error',
},
options: {
typeAware: true,
},
},
fmt: {
semi: true,
singleQuote: true,
},
});Optional parser-aware Vite plugins (source-local example)#
This is separate from everything above, and it is separate from the Vite+
lint config too. It has nothing to do with vp lint or jsPlugins. It is
just a pattern for letting one of your own Vite plugins read the authored
.tsrx AST.
Vite does not let a plugin replace Rolldown's parser, but a pre-transform
plugin can inspect raw .tsrx before the framework compiler runs. The
in-repo example examples/custom-js-plugins/tsrx-parser-service.mjs composes
around the framework plugin, parses each raw file once, caches it, and passes
that AST to parser-aware consumers through a closure:
plugins: [
withTsrxParser(tsrxReact(), (parser) => tsrxDemoLint(parser)),
]Two things to be clear about, because it is easy to assume this is more finished than it is:
- It is a source-local proof, not an installable feature. In the example,
the service imports the parser with a relative path
(
../../packages/toolchain/dist/parser.js) rather than fromoxc-tsrx/parser. That file is the same module an install reaches through theoxc-tsrx/parsersubpath, so the parser itself is public; what is not public is the glue.withTsrxParseris not exported by theoxc-tsrxpackage. - The
parserargument comes from a closure the helper creates, not from any Vite parser lifecycle. Vite does not hand plugins a parser.
The framework plugin still owns compilation, CSS, maps, and HMR, and Rolldown still parses only the generated JavaScript. A real Vite build proves the ordering and the authored custom-node observation. See Custom JavaScript plugins for the full example, the publish gap, and the separate Oxlint host boundary.
Configuration boundary#
When Vite+ passes vite.config.* as the tool config:
- The toolchain's Node boundary resolves it once through Vite+'s public
resolveConfig, extracts only thelintorfmtfield, and hands the native process a disposable JSON file. The file is removed after the batch; nothing is added to your project. - Relative paths in object
extends, override globs, andignorePatternsresolve from where you wrote them. - Non-serializable values (callback functions, and any
jsPluginsentry that is not a specifier string or a{ name, specifier }pair) fail with an error instead of being dropped. An explicit JSON/JSONC--configkeeps the direct native configuration path.
For type-aware lint, the toolchain reads the resolved
lint.options.typeAware and lint.options.typeCheck values and adds
--type-aware or --type-check to the native .tsrx batch automatically.
Running the Rust binary directly still requires one of those flags, so
config alone can never start a type process. typeCheck implies the
type-aware lane and adds TypeScript syntactic and semantic diagnostics.
All discovered .tsrx files for a command share one oxlint-tsgolint
process; canonical Oxlint still handles ordinary files in its own lane. A
missing or mismatched executable fails the command instead of silently
falling back to syntax-only lint; see
type-aware linting.
Proven compatibility#
Read the scope carefully, because it is narrower than it might sound. The
end-to-end Vite+ command matrix runs on npm only. It does not claim pnpm,
Yarn, or Bun for these vp commands. Retained tests exercise:
- a real Vite 8.1.5 production build and dev server (filesystem watcher,
framework recompilation, module invalidation, and emitted HMR payloads)
with the published
@tsrx/vite-plugin-react0.0.72; - literal
vp build,vp dev, mixedvp lint,vp fmt --check, and convergentvp check --fix, run with npm, on the tested minimum Vite+ 0.1.24 and the pinned Vite+ 0.2.4; and - imported object
extends, TSRX-specific overrides, rooted ignores, and theoptions.typeAwareauto-opt-in with a real mappedtypescript/no-floating-promisesdiagnostic.
An older Vite+ version is retained only as a disposable legacy control for
the read-only Markless oracle and is absent from the root and release
dependency graphs. The supported clean-install report is
tests/packaging/vite-plus-matrix-report.json.
Performance#
The aggregate-selected report is
benchmarks/vite/results-1784321678410.json on Apple M5 Pro. The ordinary
lane imports the exact manifest-declared Oxfmt launcher in the same Node
process; trace evidence records zero TSRX dispatch:
| Boundary | p95 | Canonical ratio | Budget |
|---|---|---|---|
| Ordinary toolchain format-check | 113.44 ms | 1.101× | ≤150 ms and ≤1.25× |
| Mixed toolchain lint | 57.91 ms | 1.813× | ≤150 ms and ≤2.5× |
| Mixed toolchain format-check | 127.11 ms | 1.234× | ≤220 ms and ≤2.0× |
| Vite+ 0.2.4 mixed lint | 237.08 ms | n/a | ≤750 ms |
These are fresh-process ecosystem boundaries. Native throughput, allocation, RSS, and cold-start gates remain independently enforced by the native lint and format benchmarks. Vite runtime compilation has zero OXC for TSRX transforms or parses, so framework build and HMR performance is not taxed by this package.
Still pending#
Everything above is proven locally. Hosted production of all eight release
candidates remains a post-push release gate, and registry and Marketplace
publication remain separate approval-gated actions. Your JavaScript Oxlint
plugins run on .tsrx today, through the TSX projection; what still waits on a
released custom-parser or language-plugin host API is a rule that visits
authored TSRX node types such as JSXForExpression inside Oxlint itself.
Provider discovery is not pending for this page, though, because it was never
going to serve it. No released OXC, Oxlint, Oxfmt, Vite+, or oxc.oxc-vscode
build reads oxc.provider metadata, and nothing is being submitted upstream to
change that. The reference implementation in this repository discovers
providers; the protocol only records what a host could do.
So the setup step above is not waiting on anything. It is the permanent shape of the Vite+ integration.