Upstreaming TSRX to OXC

This page is for OXC maintainers and contributors evaluating whether .tsrx support belongs upstream, and for anyone who wants to attempt it. It maps what would transfer, what would have to be rewritten, and what upstream would have to open up first, against pinned revisions.

Nothing here has been submitted to OXC and nothing is scheduled. OXC for TSRX is an independent community project, not affiliated with or endorsed by VoidZero or the OXC team, and no maintainer interest is claimed. What ships today is one npm package owning the oxlint and oxfmt command names, which does not depend on any of this.

Four facts set up the rest:

  • TSRX works by making a copy: a scan builds a temporary in-memory TSX version of a file, OXC parses that copy exactly once, and results map back onto the authored bytes. It is the shape of a Vite plugin transform, except the copy never reaches disk.
  • The reusable core is one crate, crates/tsrx_syntax. Its one direct dependency is unicode-id-start = "1", a standalone Unicode table, not an OXC API.
  • There is no fork. All twelve OXC dependencies sit behind crates/oxc_adapter at one exact commit pin.
  • At the audited revision, OXC has no merged whole-file language hook that could load this front end. Audited 2026-07-16 against the pin, 8e0ed2e (opens in new tab), and then-current OXC main, 6fe866a (opens in new tab). Boundaries move, so a real proposal would repeat that audit.

Select a node to read what it is, or step the buttons through the order:

Select a diagram node to read its explanation.
tsrx_syntax coreunicode-id-startTests, fixtures, and benchmarksoxc_adapter commit pinCLI, npm, and editor companionOXC maintainer design reviewExperimental TSRX front-end crateProjected-source Oxlint contractOxfmt strategy around checked liftApp routing, SourceType, and LSP
Upstream review map. What this repository offers for review, the order a credible upstream landing would take, and what stays downstream on purpose.

The reusable core#

crates/tsrx_syntax keeps its modules private and its public API small. Two ideas run through it: the copy, a temporary legal-TSX version OXC reads while your file is never touched, and mapping back, the return trip that puts OXC's output into TSRX and checks itself before anything is written.

The module tree, and what each part owns
Module Owns
scanner/mod.rs the borrowed source, scanner state, and the main loop
scanner/lexical.rs strings, comments, regular expressions, templates, numbers, and identifiers, as plain methods with no dynamic dispatch
scanner/control.rs, header.rs, jsx.rs, overlay.rs, stack.rs the TSRX grammar. scanner/overlay.rs records scanner state and can undo it
projection/mapping.rs, builder.rs, marker.rs which byte of the copy came from which byte of your file, building the copy, and naming its placeholders
projection/lint.rs, types.rs, format.rs, lift/* one path per job. projection/lift/scaffold.rs is the formatter's return trip: it maps output back into TSRX and verifies it
diagnostics.rs, model.rs the error shapes, and the compact records

What could move upstream#

The classification is the point, not the directory names. "Direct reuse" means the code and its tests could move with little conceptual change, not that a patch would compile as-is.

Local responsibility Classification Where it could land
Scanner records, checkpoints, spans, shape fingerprint Direct reuse An oxc_tsrx-style crate
The mapping between your file and the copy Direct reuse An Oxlint route that accepts a copy and tracks which ranges are yours
The formatting copy and its checked return trip Direct reuse A TSRX crate beside the language-agnostic formatter
TSRX control and header grammar Adapt or replace oxc_parser grammar modules. Test cases transfer, the rest follows upstream
Lexical shielding and Unicode boundaries Adapt or replace The parser lexer, or oxc_lexer (opens in new tab) once OXC adopts it
JSX, dynamic tags, raw <style> Adapt or replace JSX parser and lexer, once maintainers decide who owns the AST
Error construction Adapt or replace oxc_diagnostics, if errors keep pointing at your file
The test suites Direct reuse as evidence OXC's fixture and snapshot systems
oxc_adapter, config discovery, type-aware process protocol, CLI Standalone product glue Usually nowhere
Vite+, npm platform packages, the LSP multiplexer Standalone product glue Separate ecosystem packages
Native TSRX AST nodes, visitors, semantics, formatter Upstream-only redesign oxc_ast and friends. The alternative to the copy, and not something this repository can build in advance
.tsrx dispatch, linter loading, Oxfmt routing, editor selection Upstream-only redesign OXC application and editor layers

What OXC does not expose today#

Every route a third-party language would need is closed in the audited source:

Upstream boundary What it actually is
SourceType (opens in new tab) JavaScript and TypeScript syntax choices plus known extensions, not a registry a third-party grammar can join
Oxlint's PartialLoader (opens in new tab) Hard-coded framework containers returning contiguous borrowed script regions at fixed offsets
Oxfmt's FileKind classification (opens in new tab) and Oxfmt LSP routing (opens in new tab) Closed application routes. Neither loads a native TSRX formatter from project configuration
ParserConfig (opens in new tab) Tuning for the existing parser, not a way to replace its grammar
The language server's ToolBuilder (opens in new tab) A compile-time Rust embedding seam, not a runtime language loader

The PartialLoader row matters most. It hands the linter one contiguous slice of a file, which suits a <script> block inside HTML. TSRX is not like that: the whole file becomes a copy in which some ranges are yours and some are placeholders the tool wrote, and the two have to stay told apart. Treating it as a partial-loader case would quietly lose accuracy.

Unmerged research threads upstream

Three exist. They are research, not runtime dependencies: Language Plugins RFC #21936 (opens in new tab), custom-template issue #19918 (opens in new tab), and draft custom-parser PR #24262 (opens in new tab), which as of 2026-07-24 reaches past a bare AST hook into editor routing but is still Oxlint-only.

Provider discovery patches, built locally#

A smaller question stands on its own: how would an OXC tool find out that a project installed a third-party language provider at all? The proposed answer is a static oxc.provider block, and three adoption patches exist as source. "Built, verified locally" means the patch was applied to a clone at a pinned revision, compiled, and run against tests.

Target Pinned revision Size Diff Status
oxc-project/oxc (opens in new tab) Oxlint npm wrapper a065946 (opens in new tab) +1463 / -10, of which 43 lines touch existing code docs/architecture/patches/oxlint-provider-dispatch.patch built, verified locally
oxc-project/oxc-vscode (opens in new tab) document selector beaffb9 (opens in new tab) +106 / -1 docs/architecture/patches/oxc-vscode-provider-selector.patch built, verified locally
voidzero-dev/vite-plus (opens in new tab) a24eede (opens in new tab) zero lines none needed verified, with a version pin caveat below

The diffs live under docs/architecture/patches/, with the pinned revisions and the exact git apply commands. No source is vendored here.

The Oxlint one is the substantive patch, and it stays in JavaScript: it sits directly above the single lint(args, ...callbacks) call at the end of apps/oxlint/src-js/cli.ts, so nothing crosses into Rust. With no provider installed the wrapper is unchanged. With one, it sends the paths that provider claims to its binary in pass-through mode and takes the worst exit code, and in --lsp mode composes both language servers behind the editor's one stdio connection. See the calling convention. With node_modules/.bin deleted and every tool name shadowed on PATH, the released official OXC extension still found that patched wrapper by ordinary Node resolution and started the provider's language server, which proves the protocol implementable and nothing more.

Vite+ needs no patch of its own, because it resolves the oxlint package and runs bin/oxlint from it without reading file extensions, so patching the resolved wrapper reaches Vite+ users. That holds only while Vite+ pins one exact oxlint version, and the pinned number moves between Vite+ releases. tests/packaging/vite-plus-provider.test.mjs records which version, the rest of the measurements, and the limit of a JavaScript-only patch: a directory argument goes to the native walker and never enumerates provider extensions.

Landing order#

The order this proposal would suggest, if a maintainer first accepted reading a TSX copy as a starting design. None of it is scheduled or agreed to.

  1. Prototype a private TSRX front end. Scanner, copy builder, mapping, and errors in an experimental crate, with ordinary JS and TS skipping all of it, measured for speed and memory before anything else.
  2. Give Oxlint a real way to accept a copy. It needs your file path, the TSX copy, the full mapping, and the rules for when a fix is safe. PartialLoader cannot carry that.
  3. Integrate one tool at a time. Oxlint diagnostics and fixes, then formatting. Only then should SourceType, FileKind, and the language server advertise .tsrx.
  4. Decide about native AST nodes separately. First-class TSRX redesigns node types, visitors, semantic analysis, and the formatter. Hiding that inside a "parser support" patch would be dishonest.

Budgets any experiment has to keep#

Ordinary JS, JSX, TS, and TSX files skip TSRX work entirely. Beyond that, four budgets:

  • a normal TSRX lint does one scan, one TSX copy, and one OXC parse;
  • TSRX format does two structural scans, the authored one plus a cheap check that the result came back the same shape, and still one OXC parse;
  • the scanner borrows your text and stores numbers rather than copying strings per node, and fixes and formatting check themselves before writing;
  • moving a module upstream adds no new copies, parses, allocations, or dynamic dispatch without measurements and an agreed new budget.

Tests freeze the size of sixteen hot data records, and release benchmarks gate every stage. See the core performance contract and the acceptance matrix.

Reproducible evidence#

Every command, to rerun it from a clean checkout
cargo test --locked -p tsrx_syntax --test architecture
cargo test --locked -p tsrx_syntax --all-targets
pnpm run benchmark:native-lint
pnpm run benchmark:native-format

The architecture test checks the private module tree, the public API, and the dependency boundary. The two benchmarks are the ones the budgets above are measured against.