Introduction
A .tsrx file is TypeScript with HTML-like markup in it, plus blocks like
@if and @for for showing something only sometimes, or once per item in a
list. yuku-tsrx reads those files.
It is a parser, an analyzer, and a code generator for .tsrx, written in Zig,
with a JavaScript API on top of a native addon. You call it from Node, you get
back a real TSRX syntax tree, and you can hand a tree back to it and get source
out again.
import { parseModule, walk } from "yuku-tsrx";
const program = parseModule(source, "Cart.tsrx");
walk(program, {
JSXCodeBlock(node) {
// every TSRX code block in the file
},
});What a .tsrx file looks like#
export function Cart({ items }): unknown @{
const total = items.length;
<section className="cart">
@if (total > 0) {
@for (const item of items; index i; key item.id) {
<span>{i}:{item.id}</span>
} @empty {
<span>empty</span>
}
} @else {
<span>no cart</span>
}
<style>.cart { display: grid; }</style>
</section>
}Three things in that file are not TypeScript and not JSX: the @{ } code block
that lets statements and markup sit next to each other, the @if / @for
directives with their ; index and ; key clauses and @empty branch, and the
<style> element holding raw CSS. TSRX Syntax Support has
each construct with a real example.
A dialect on Yuku, not a fork#
yuku-tsrx is built as a compile-time dialect on Yuku (opens in new tab), a JavaScript and TypeScript toolchain written in Zig. It is not a fork of Yuku and not a second parser engine beside it.
Yuku does all the JavaScript and TypeScript work. yuku-tsrx owns only the rules
that are specific to TSRX, and reaches Yuku through the hook declarations in
src/dialect/parser_extension.zig. A dialect is a plain struct of optional hook
declarations resolved at compile time, so a Yuku built without a dialect
compiles to exactly what it did before.
The other way to get here is to write a parallel engine, which is what the sibling project oxc-tsrx (opens in new tab) had to do: OXC (opens in new tab)'s parser has no extension point, so its TSRX support carries a 17,057-line parser engine that is mostly plain TypeScript parsing done a second time. The dialect design exists to avoid owning that copy. Zig/Yuku Dialect Core has the mechanism.
Five steps take a .tsrx file to the JavaScript API, and the interesting part
of each one is who owns it:
The file you wrote. Nothing owns it yet, and nothing on disk changes at any point in what follows.
Yuku owns the JavaScript and TypeScript grammar. yuku-tsrx owns only the 20 answers below, declared in src/dialect/parser_extension.zig and resolved at compile time.
Yuku owns the ordinary nodes. yuku-tsrx owns these records, declared in src/dialect/schema.zig, and the parser produces those exact names rather than lowering TSRX to TSX.
The tree crosses into JavaScript as a single buffer, decoded on the JavaScript side rather than built node by node in the addon.
Three calls on the JavaScript side, each with its own guide.
export function Cart({ items }): unknown @{
const total = items.length;
<section className="cart">
@if (total > 0) {
@for (const item of items; index i; key item.id) {
<span>{i}:{item.id}</span>
} @empty {
<span>empty</span>
}
} @else {
<span>no cart</span>
}
<style>.cart { display: grid; }</style>
</section>
}Statement
statement_at_code_block statement_at_control_flow
Expression
expression_at_code_block expression_at_control_flow
Pattern
lazy_assignment_pattern binding_pattern can_start_binding
Function
function_body function_body_starts
For-of
for_of_tail
Module
module_specifier
JSX
jsx_child_at_code_block jsx_child_at_control_flow jsx_element_name jsx_element_after_open jsx_fragment_after_open validate_jsx_element_name jsx_names_match
Text
jsx_text_boundary jsx_text_value
JSXCodeBlock JSXIfExpression JSXForExpression JSXSwitchExpression JSXTryExpression TSRXExpression JSXStyleElement StyleSheet
The layout, and what the decoder does with it, is written up in The wire format underneath. The Zig side of it is src/dialect/transfer.zig and src/dialect/semantic_transfer.zig.
parseandparseModule: source in, a TSRX tree and its diagnostics out.analyze: scopes, symbols and references over that tree.generate: a tree back to source.
The same module compiled to WebAssembly is what runs in the playground and in the figures on the guide pages.
Who it is for#
This is a library for people building tooling that has to understand .tsrx
source: a framework's TSRX plugin, a compiler, a bundler plugin, a codemod, an
analysis tool.
The output is a real TSRX AST. Consumers pattern-match on the node type names
directly, JSXCodeBlock, TSRXExpression, JSXStyleElement,
JSXForExpression, and so on, so the parser produces those exact names rather
than lowering TSRX to TSX. In
Markless (opens in new tab), a function is a component
if and only if its body is a JSXCodeBlock, so a lowering that erased the name
would erase the answer.
parseModule(source, filename, options) is shaped as a drop-in for
@tsrx/core's parseModule, which is the interface Markless already calls, so
a consumer on that interface can swap engines without changing its call sites.
Status#
Nothing has been published to npm. The package name is yuku-tsrx and the
version in npm/yuku-tsrx/package.json is 0.0.0. The extension points it
builds against live in yuku-toolchain/yuku#164 (opens in new tab),
which is open, so building today means a Yuku checkout of that branch in a
sibling directory. Getting Started has the build, and
Limitations has the full list of what does not exist.