Editor integration

Install oxc-tsrx in the project and use the released official OXC extension (oxc.oxc-vscode). It selects the project-local oxlint command supplied by oxc-tsrx by that literal name, not by reading provider metadata. That name selection is how this path works, and it is what ships.

No TSRX-specific or forked extension is required, and outside Vite+ no setup command is either. A Vite+ project owns node_modules/.bin/oxlint, which is where the extension looks, so it needs the one command described in In a Vite+ project, setup writes oxc.path.oxlint. Rust owns TSRX parsing, linting, formatting, source mapping, and fix validation, and your own Oxlint JavaScript plugin rules show up as squiggles too. See your own JavaScript rules in the editor.

One thing to get straight before the setup steps: the extension does not wake up on a .tsrx file by itself. What "a plain install" actually covers explains what that means in a workspace where a framework extension already owns .tsrx.

How the official extension gains TSRX#

The released official client starts project-local oxlint --lsp. oxc-tsrx uses that exact invocation as a narrow multiplexer:

  • canonical Oxlint continues to receive ordinary JavaScript and TypeScript;
  • the native oxc-tsrx-lsp server receives only .tsrx document traffic;
  • the multiplexer dynamically registers .tsrx full-document sync, formatting, and quickfix capabilities with the existing official client;
  • diagnostics from both servers flow back through the same client; and
  • both client-request and server-request IDs are isolated, so responses cannot cross streams.

Those dynamic registrations are written as { scheme: "file", pattern: "**/*.tsrx" }. They match on the file name, not on the language id, so it does not matter what another extension calls the language. What does matter is whether the official extension is running at all.

What "a plain install" actually covers#

The official extension exposes no public API, it hard-codes its document selectors, and its activation events do not include .tsrx. Read from oxc.oxc-vscode 1.59.0 itself, activationEvents is 21 onLanguage: entries (astro, css, graphql, handlebars, html, javascript, javascriptreact, json, json5, jsonc, less, markdown, mdx, mjml, scss, svelte, toml, typescript, typescriptreact, vue, yaml), and none of them is tsrx, ripple, or markless-tsrx.

That matters most in the workspace you are most likely to have. Ripple's own extension (ripple-ts.ripple-ts-vscode-plugin, read from 2.0.63) contributes .tsrx as the language id ripple. So opening a .tsrx file there activates Ripple's extension and not OXC's, and until something else activates OXC's extension there is no client to route .tsrx traffic to. .tsrx diagnostics therefore do not appear from installing the two packages and opening a .tsrx file. Open any JavaScript, TypeScript, or JSON file in the workspace once first. After that the dynamic registrations above apply and .tsrx is served for the rest of the session, whatever language id it was given.

OXC's Language Plugins RFC (opens in new tab) could eventually remove both this activation caveat and the multiplexer. The source-backed upstream seam audit separates that proposed runtime contract from OXC's current compile-time embedding points.

Name selection and provider discovery are not the same thing#

Two different mechanisms meet on this page, and keeping them apart is what keeps the claims honest:

  • Name selection (released behavior). The official extension looks for a command called oxlint in your project and runs it. oxc-tsrx is picked up because it declares a command with that literal name. Nothing about TSRX is declared to the extension.
  • Provider discovery (local reference implementation and proof). oxc-tsrx also declares a static oxc.provider block in its own package.json. A host that reads that block learns which file extensions the package claims and which binary serves them, without running anything.

No released Oxlint, Oxfmt, Vite+, or oxc.oxc-vscode build reads oxc.provider. Nothing has been submitted upstream, and nothing has been accepted.

What is proven locally is narrower, and worth stating exactly. A real VS Code session ran the released official extension against a workspace whose only action was npm install, with node_modules/.bin deleted, oxc-tsrx setup never run, and every tool name shadowed on PATH by a decoy proven to fire. The extension was given one pointer to the general oxlint host, and from there the discovered provider's declared language server started as a real process and answered a real quick-fix request. Supplying that pointer is the part no released build does on its own.

That harsh setup exists to isolate discovery, so do not read it as the install you get. An ordinary install, with node_modules/.bin left alone, PATH untouched, and oxc-tsrx setup never run, was measured separately in a real oxc.oxc-vscode 1.59.0 session and served .tsrx with no pointer and no extra command. Read that measurement with the activation step included: the session opened an ordinary TypeScript file first, waited for the extension to become active, and only then opened the .tsrx file. Nothing in that run made the extension activate on .tsrx itself, and nothing can, because that is fixed in the extension's manifest. That measurement is darwin-arm64 only.

Of the four declared capabilities, only the language server has a host. The parser, lint, and format targets resolve, but nothing runs them through discovery yet.

What a session looks like#

Here is the whole flow from a keystroke to a squiggle. Select any node to read what it does, or step through the buttons:

Select a diagram node to read its explanation.
Your unsaved .tsrx bufferFramework extension keeps the languageOfficial OXC VS Code extensionoxc-tsrx LSP multiplexeroxc-tsrx-lsp native serverCanonical Oxlint for JS and TSNative OXC lint and format for TSRXOpt-in TypeScript-Go processSquiggles on your authored bytesValidated formatting and quick fixes
One editor session. How a keystroke in VS Code becomes native diagnostics, quick fixes, and formatting without any extension fighting over your file type.

And here is that session as you would actually see it. Press Play, or step through the stages yourself. Hover the squiggles: those are the real diagnostics the server publishes.

One editing session, replayed

You open an unsaved buffer with two problems in it. The native server lints the in-memory text and the squiggles land on the exact bytes you typed. Hover a squiggle to read the real diagnostic.

Counter.tsrx
export function Counter({start}:{start:number}) @{
  var count = start;
  console.log("mounted");
  debugger;

  <div   class="counter">
    <span>{count}</span>
  </div>
}

Problems

  • eslint(no-console): Unexpected console statement · at your authored bytes
  • eslint(no-debugger): `debugger` statement is not allowed · at your authored bytes
✕ 1 ⚠ 1open to first diagnostics: 2.40 ms median
The diagnostics, quick-fix rules, and latency figures are the real recorded ones: 2.40 ms median open-to-diagnostics and sub-millisecond edit, format, and code-action p95 on the recorded Apple M5 Pro, with zero disk writes from code actions.

What the server provides#

One long-lived native process provides:

  • full-document synchronization for unsaved .tsrx buffers;
  • live Oxlint diagnostics mapped to original TSRX locations;
  • Oxfmt-backed whole-document formatting;
  • quickfix actions, only with an exact authored mapping and a clean reparse;
  • UTF-8 byte to editor UTF-16 position conversion, including astral Unicode;
  • one lint, one fix-enabled lint, and one format session per workspace;
  • your own Oxlint JavaScript plugin rules, when .oxlintrc.json declares jsPlugins; and
  • opt-in type-aware or type-check diagnostics through TypeScript-Go.

The server does not advertise fix-all, suggestion, or dangerous actions. Type-aware diagnostics analyze each requested document on its own and may start one TypeScript-Go process, which the native server does not embed; troubleshooting tsgolint discovery covers the supported version and discovery rules.

Everything runs on the in-memory buffer, and code actions never touch disk. Malformed input publishes an authored parse-error diagnostic instead of stale lint results, formatting malformed input returns no edit, and a later valid edit restores normal diagnostics.

Your own JavaScript rules in the editor#

If your .oxlintrc.json declares jsPlugins, those rules run on .tsrx in the editor as well as on the command line, at the same positions. You configure nothing extra:

{
  "jsPlugins": ["./oxlint-demo-plugin.mjs"],
  "rules": {
    "tsrx-demo/require-keyed-map": "error"
  }
}

Open a .tsrx file and your rule is a squiggle, next to the built-in Rust ones. The custom JavaScript plugins guide is the tutorial; this section is only what is different inside an editor. If you are starting from a vp create scaffold, its walkthrough is the complete sequence, setup included.

It costs one extra parse per linted file#

The native server is a Rust process with no Node.js runtime, so it cannot run your module itself. What it does instead is build the same legal-TSX projection of your buffer that the built-in rules already use, hand that to a small Node.js host, and run the published Oxlint binary over it. Every diagnostic comes back through the byte map that turns projection positions into positions in the file you wrote.

That means one extra parse of each .tsrx file the server lints, on open, on change, and on save. The Node.js host is started once per workspace, and only when your config declares jsPlugins. The server says so once, in its output log:

oxc-tsrx-lsp: running this project's Oxlint JS plugins on .tsrx by linting each
file's TSX projection; this parses every linted .tsrx file once more. Disable
with "settings": { "oxcTsrx": { "jsPluginsOnTsrx": false } }.

To turn the lane off, use the key that message names:

{
  "settings": {
    "oxcTsrx": {
      "jsPluginsOnTsrx": false
    }
  }
}

With that set, your plugins keep running on ordinary files, and .tsrx publishes a single lint-unavailable diagnostic explaining why there are no .tsrx results, rather than going quiet.

Two differences worth knowing#

context.filename is the mirror path, not yours. Your rule is handed the projection, which lives in a throwaway directory and is named <name>.tsrx.tsx, so a src/View.tsrx reaches the rule as <temporary directory>/src/View.tsrx.tsx. The path relative to the project is preserved, so a rule that checks for src/ still works. The squiggle itself still lands on src/View.tsrx. This is the same known difference the command line has, and what your rule sees on .tsrx covers the rest of it, including the fact that @if and @for reach your rule already compiled to ordinary if and for.

A failing plugin is reported, not hidden. If the lane cannot start, the Node.js host dies, or one of your rules throws, the built-in Rust diagnostics still publish and you additionally get a js-plugins-unavailable warning carrying the reason. Fewer rules running is never silent.

Visual Studio Code setup#

  1. Install the released extension with ID oxc.oxc-vscode.
  2. Add oxc-tsrx to the project.
  3. Open a JS, TS, or JSON file once to activate the current released client.
  4. Open .tsrx; diagnostics, formatting, and quick fixes now come through the official client.

Select the official extension as the default formatter for the language ID contributed by your framework:

{
  "[markless-tsrx]": {
    "editor.defaultFormatter": "oxc.oxc-vscode",
    "editor.formatOnSave": true
  }
}

Use normal .oxlintrc.json and .oxfmtrc.json files for TSRX settings. During source development only, OXC_TSRX_LSP_BIN points the harness at a release binary.

In a Vite+ project, setup writes oxc.path.oxlint#

The official extension finds its linter by looking for oxlint in node_modules. In a project that also installs Vite+, that lookup does not reach this package: node_modules/.bin/oxlint is Vite+'s own wrapper, which execs

node_modules/.pnpm/vite-plus@<version>/node_modules/vite-plus/bin/oxlint

and knows nothing about .tsrx. Fixing package resolution makes vp lint work, but it does not own the .bin shim the extension reads. The result used to be an editor with no .tsrx diagnostics at all and no error explaining why.

oxc-tsrx setup now handles it. It is the fourth slot the command manages, and the only one that is not a package: when node_modules/.bin/oxlint belongs to another tool, setup merges one key into your .vscode/settings.json:

{
  "oxc.path.oxlint": "node_modules/oxc-tsrx/bin/oxlint"
}

Reload the window afterwards. Everything else in that file is preserved, comments included; an oxc.path.oxlint you already set is reported rather than overwritten; and oxc-tsrx remove takes back only that key. The Vite+ page has the full rules.

Outside Vite+ the ordinary lookup does find this package, so nothing is written and status reports the slot as unnecessary.

setup also reports the TSRX editor prerequisites it deliberately does not own: @tsrx/typescript-plugin, a framework binding, the tsconfig.json plugins entry, and TypeScript inside the plugin's declared peer range. setup installs and changes none of it; those pieces are the TSRX toolchain's, not this package's.

Two details are worth stating precisely, because guessing at them costs hours.

The plugins entry must go in the project that owns your source. A Vite+ scaffold's root tsconfig.json is solution-style, { "files": [], "references": [...] }, and owns no files, so a plugin declared there never applies to anything. Measured on a stock scaffold, same file and same plugin: declared in the root, hover returns any; declared in tsconfig.app.json, it returns const legacy: number. setup names the referenced project rather than the root for this reason.

The TypeScript range is a declared peer, not a known failure. @tsrx/typescript-plugin declares ^5.9.3 while vp create scaffolds TypeScript 6, so a stock project is outside the supported range. That is worth knowing, but a stock scaffold on TypeScript 6.0.3 was measured answering correctly three times out of three, so treat the mismatch as unsupported rather than broken. If the language service does misbehave, pinning TypeScript into that range is the first thing to try.

Optional legacy client#

packages/vscode is the older thejackshelton.oxc-tsrx-vscode client. It can still provide automatic activation in a workspace that contains only .tsrx files and has no ordinary file to activate the official extension. It is not part of the primary install, and projects must not run both TSRX document clients at once.

Run the visible lint demo#

The clean Extension Host proof installs untouched local release tarballs in an empty consumer whose only TSRX dependency is oxc-tsrx. It loads the released official extension with the legacy client absent and proves canonical TypeScript diagnostics, native TSRX diagnostics, an unsaved buffer update, formatting, and the validated no-var quick fix.

Two editor stacks, and why they are different#

Both start from the same official OXC extension, but only the first is the product you install today. Keeping them straight avoids a common mix-up.

The current, native path (ships today):

Official OXC extension
  -> project oxlint --lsp multiplexer
     -> canonical Oxlint for JS/TS
     -> native oxc-tsrx-lsp for TSRX   (Rust; built-in rules)
        -> TSX projection of the buffer
           -> published Oxlint binary, in a Node host
              -> your JavaScript plugin rule

The source-only, upstream-draft path (a local proof, not a release):

Official OXC extension
  -> source-local custom-parser launcher
     -> draft Node-enabled Oxlint
        -> TSRX parseForESLint adapter
        -> your JavaScript plugin rule

The difference is what your rule is handed. The first stack ships, needs no build, and gives your rule the TSX projection, where @if and @for have already become if and for. The second gives your rule the authored TSRX tree, with JSXIfExpression and JSXForExpression intact, but it depends on an unmerged Oxlint draft built locally, so it is not a product path.

This second stack is the tsrx-demo(no-tsrx-if) experiment on an authored @if … @else block. Open examples/vscode-lints as its own workspace with the official OXC extension, open oxlint-custom-parser.json once to activate OXC, then open LintDemo.tsrx. Its configured LSP launcher dynamically registers .tsrx and forwards to a Node-enabled Oxlint build from the upstream custom-parser draft. The source-only setup and upstream status are documented under Custom JavaScript plugins.

Reproducible proof#

Build the native binary, then run the protocol and package tests, the real Extension Host walkthrough beside the actual Markless extension, the installed-VSIX gate, and the benchmark.

There is no separate language-server executable to build. crates/oxc_tsrx_cli produces one binary, oxc-tsrx, and the lsp subcommand serves an editor. pnpm run build:native runs that cargo build and then writes the oxc-tsrx-fmt alias that the format benchmark resolves by file name.

pnpm run build:native
pnpm run test:editor
pnpm run test:plugins
pnpm run test:editor:official-toolchain
pnpm run build:editor
pnpm run test:editor:vscode
pnpm run test:packaging:vscode
pnpm run benchmark:editor

test:plugins is the one that covers the JavaScript plugin lane. It drives a real user plugin over a real .tsrx file twice, once through oxlint and once through the language server, and fails if the two disagree about a position.

Retained evidence lives in tests/editor/markless-vscode-walkthrough.json and tests/packaging/installed-vsix-report.json, both recording externalWrites: false. On the recorded Apple M5 Pro, open-to-diagnostics measured 2.40 ms median, with sub-millisecond edit, format, and code-action p95 and zero memory growth over a 1,000-edit soak. Benchmarks has every frozen editor budget.