Parsing
oxc-tsrx/parser is the parser behind the lint and format tools, exposed
as a library you can call yourself. It has the same API shape as
oxc-parser (opens in new tab), OXC's official npm
parser, and it handles ordinary JavaScript and TypeScript (js, jsx, ts,
tsx, dts) plus .tsrx files.
If you are building something that needs to understand .tsrx source (a
bundler plugin, a codemod, an editor feature, a static analysis tool), this
is the package for you.
Install#
You need Node.js 20.19 or newer:
npm install oxc-tsrx@latestpnpm add oxc-tsrx@latestyarn add oxc-tsrx@latestbun add oxc-tsrx@latestLike the CLI tools, the parser is native Rust code. Your package manager downloads a ready-made binary for your platform during this normal install. There are no install scripts, and nothing is downloaded later.
Parse your first file#
Save this as src/View.tsrx. The "Try in playground" button lets you explore
the file in your browser first:
import { Row } from "./Row";
export function View({ items }: { items: Item[] }) @{
<ul class="list">
@for (const item of items; key item.id) {
<Row item={item} />
} @empty {
<li>No items yet</li>
}
</ul>
}Then save this parse script as parse.mjs. It parses the file and walks the
tree looking for the @for node:
import { readFileSync } from "node:fs";
import { parseSync } from "oxc-tsrx/parser";
const source = readFileSync("src/View.tsrx", "utf8");
const result = parseSync("src/View.tsrx", source);
console.log("errors:", result.errors.length);
console.log("imports:", result.module.staticImports.map((s) => s.moduleRequest.value));
console.log("top level:", result.program.body.map((node) => node.type));
function* walk(node) {
if (Array.isArray(node)) {
for (const item of node) yield* walk(item);
} else if (node && typeof node === "object") {
if (typeof node.type === "string") yield node;
for (const value of Object.values(node)) yield* walk(value);
}
}
const forNode = [...walk(result.program)].find((node) => node.type === "JSXForExpression");
console.log("found:", forNode.type);
console.log("its first line, straight from your file:");
console.log(source.slice(forNode.start, forNode.end).split("\n")[0]);# Run the parse script from above against the good file node parse.mjs errors: 0 imports: [ './Row' ] top level: [ 'ImportDeclaration', 'ExportNamedDeclaration' ] found: JSXForExpression its first line, straight from your file: @for (const item of items; key item.id) { # Parse errors land in result.errors and point at your file node parse-broken.mjs Error: unterminated JSX closing tag starting at byte 30 x unterminated JSX closing tag starting at byte 30 ,-[src/Broken.tsrx:2:3] 1 | export function Broken() @{ 2 | <p>hello</p : ^ 3 | } `----
Three things worth noticing:
top level:lists ordinary ESTree node types. This is a normal AST, and code that walks ASTs can walk it with no special cases.- The
@forcame back as aJSXForExpressionnode, a real node type, not a comment or a placeholder. - Slicing the source with the node's
startandendreturns exactly what you typed. Positions always point into the string you passed in, never into an internal copy.
How a parse works#
Step by step, for a .tsrx file:
- The file is scanned once to find the TSRX-only syntax.
- A valid-TSX copy is built in memory (the projection). Your code is copied into it unchanged; only the TSRX control syntax is replaced with TSX placeholders.
- Canonical OXC parses that copy, once. There is no second parser and no TSRX fork of OXC.
- The parsed tree is rebuilt into the tree you authored: the placeholders become TSRX nodes again, and every position is mapped back to your original source.
Ordinary .js, .jsx, .ts, and .tsx files skip steps 1, 2, and 4
entirely: the file goes straight to OXC, exactly like calling oxc-parser
yourself.
The tree is the code you wrote#
Every TSRX control comes back as its own node type, shaped like the JSX nodes you already know:
| You write | You get back |
|---|---|
@{ } statement containers |
JSXCodeBlock |
@if / @else |
JSXIfExpression |
@for / @empty |
JSXForExpression |
@switch / @case / @default |
JSXSwitchExpression |
@try / @pending / @catch |
JSXTryExpression |
<{expr}> dynamic tags |
JSXElement whose tag name is a JSXExpressionContainer |
Everything else (elements, attributes, statements, expressions, TypeScript
types) uses the same node shapes as oxc-parser, and the package re-exports
the @oxc-project/types type definitions so your editor can autocomplete
them.
Positions are plain JavaScript string indexes, so source.slice(node.start, node.end) is always the exact text of a node, even in files with emoji or
other multi-byte characters.
Four results, each loaded lazily#
parseSync returns a result with four independent getters:
result.programis the AST.result.moduleis a fast summary of imports and exports:staticImports,staticExports,dynamicImports, andimportMetas, each with positions.result.commentslists every line and block comment.result.errorslists everything wrong with the parsed code.
Each field is converted from the native side only the first time you read it. A tool that only wants the import list never pays for building the full JavaScript AST.
There is also an async entry point with the same signature:
import { parse } from "oxc-tsrx/parser";
const result = await parse("src/View.tsrx", source);Options#
The third argument tunes the parse:
| Option | What it does |
|---|---|
lang |
Forces a language: "js", "jsx", "ts", "tsx", "dts", or "tsrx". Without it, the file extension of the first argument decides. |
sourceType |
"module", "script", "commonjs", or "unambiguous". |
astType |
"js" or "ts": which AST flavor to produce when the extension alone does not decide it. |
range |
Adds a range: [start, end] array to every node, for tools that expect ESLint-style ranges. |
preserveParens |
On by default: parenthesized expressions appear as ParenthesizedExpression nodes. Set false to see through them. |
showSemanticErrors |
Also reports semantic problems, like declaring the same let twice. |
recovery |
"none" (default) or "editor". Editor recovery is a capability flag; see below. |
Two kinds of errors#
Problems in the code you parse never throw. They land in result.errors,
each with a severity, a message, positions, and a ready-to-print codeframe,
as you saw in the Broken.tsrx run above.
Problems running the parser itself do throw, as a ParserOperationalError
with a stable code you can match on. Examples: the native binary for your
platform is not installed (ERR_TSRX_NATIVE_NOT_INSTALLED), it failed its
integrity check (ERR_TSRX_NATIVE_INTEGRITY), or you asked for a feature
this build does not support (ERR_TSRX_CAPABILITY_RECOVERY). Before loading
the native binary, the loader verifies its package role, target, version,
and SHA-256 digest, and a failed check is an error, never a silent fallback.
Feature detection with capabilities#
The package exports a capabilities object describing exactly what the
installed native build can do, including the pinned OXC revision it was
built from:
import { capabilities } from "oxc-tsrx/parser";
console.log(capabilities.languages); // ["js", "jsx", "ts", "tsx", "dts", "tsrx"]
console.log(capabilities.oxcRevision); // the exact OXC commit
console.log(capabilities.editorRecovery);Capability-gated options like recovery: "editor" follow the project's
fail-closed rule: if the installed build does not support one, the call
throws a clear error instead of quietly parsing with less than you asked
for.
What is tested#
The retained tests/parser-api suite proves the program crosses the native
boundary as one versioned payload (including BigInt and RegExp values),
that the loader's integrity checks reject tampered binaries, that the
package stays safe to bundle with tools like Rolldown, and that lazy fields
convert correctly in files with multi-byte characters. The recorded terminal
output above is captured by really running the scripts at build time.
Next steps#
- See exactly which TSRX syntax the parser recognizes in TSRX Syntax.
- Lint and format the same files with the CLI tools in Getting Started.
- Curious how one parse serves lint, format, and this API? Read Architecture.