# Vite and Vite+ integration

## Install

You need Node.js 20.19 or newer. Install Vite+ plus the one public TSRX
toolchain:

```sh
npm install --save-dev vite-plus oxc-tsrx@latest
```

`oxc-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.

```sh
npx --yes -p vite-plus@0.2.6 vp create vite -- my-app --template react-ts
cd my-app
```

Inside 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:

```sh
pnpm exec oxc-tsrx setup   # pnpm
yarn oxc-tsrx setup        # yarn
bunx oxc-tsrx setup        # bun
npx oxc-tsrx setup         # npm
```

Run 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:

```text
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:

```json
{
  "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/oxlint` already resolves into this
  package, which is every project that does not use Vite+, nothing is written
  and the slot reports `unnecessary`.
- **Merge, never clobber.** An existing `.vscode/settings.json` keeps 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.oxlint` is already set to something else,
  `setup` reports the conflict and leaves it, exactly the way it refuses to
  replace a package slot you own.
- **Reversible.** `oxc-tsrx remove` takes back that one key, and deletes the
  file or the `.vscode` directory only when `setup` created 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-plugin` resolvable from the project;
- a framework binding: `@tsrx/react`, `@tsrx/vue`, `@tsrx/solid`,
  `@tsrx/preact`, `@tsrx/ripple`, or `octane`;
- 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](/integrations/custom-js-plugins#the-whole-path-on-a-fresh-vite-project)
walks that whole sequence, from `vp create` to your own rule reporting on a
`.tsrx` file.

See [the editor page](/integrations/editor#in-a-vite-project-setup-writes-oxcpathoxlint)
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](/guide/getting-started#the-minimum-steps-per-host).

### 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`:

```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](/integrations/custom-js-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.

```text
$ 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 v2
```

Exit 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:

```sh
npm install --save-dev oxlint-tsgolint@0.24.0
```

Vite+ 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:

```text
$ 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:

```text
$ 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:

```sh
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](/integrations/custom-js-plugins#the-whole-path-on-a-fresh-vite-project)
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:

```text
$ pnpm exec oxlint src/Counter.tsrx
This oxlint wrapper is for IDE extension use only (--lsp mode).
To lint your code, run: vp lint
```

That 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:

```text
$ 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 way `import "oxlint"`
  resolves. A bin name cannot answer a package resolution, and `oxc-tsrx`
  cannot legitimately publish a package under that name.
- Vite+ pins its own `oxlint` dependency exactly (`=1.72.0` on 0.2.4, `=1.75.0`
  on 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 what `setup` does.

`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:

```sh
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 fixes
```

`vp 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](https://tsrx.dev/getting-started), 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:

```json
{
  "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](#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:

```js
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:

```js
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 from
  `oxc-tsrx/parser`. That file is the same module an install reaches through the
  `oxc-tsrx/parser` subpath, so the parser itself is public; what is not public
  is the glue. `withTsrxParser` is not exported by the `oxc-tsrx` package.
- **The `parser` argument 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](/integrations/custom-js-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 the `lint` or `fmt` field, 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, and `ignorePatterns`
  resolve from where you wrote them.
- Non-serializable values (callback functions, and any `jsPlugins` entry that
  is not a specifier string or a `{ name, specifier }` pair) fail with an
  error instead of being dropped. An explicit JSON/JSONC `--config` keeps
  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](/integrations/configuration#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-react` 0.0.72;
- literal `vp build`, `vp dev`, mixed `vp lint`, `vp fmt --check`, and
  convergent `vp 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
  the `options.typeAware` auto-opt-in with a real mapped
  `typescript/no-floating-promises` diagnostic.

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.
