Formatting
oxfmt formats your .tsrx files with OXC's real formatter, then converts the
result back into TSRX. Ordinary JS/TS files go straight to Oxfmt, and the output
is byte-for-byte identical to running oxfmt yourself.
Input and output#
Here is one of the files this project tests against, exactly as it is committed before formatting:
type Item={id:string;label:string};export function Rows({items}:{items:Item[]})@{<ul>@for(item of items;index i;key item.id){<li data-index={i}>{item.label}</li>}@empty{<li>Empty</li>}</ul>}becomes:
type Item = { id: string; label: string };
export function Rows({ items }: { items: Item[] }) @{
<ul>
@for (item of items; index i; key item.id) {
<li data-index={i}>{item.label}</li>;
} @empty {
<li>Empty</li>;
}
</ul>;
}Both files are committed fixtures, and the test suite formats the first one and compares it against the second on every run, so this is real output rather than a hand-written example. You can read them in tests/fixtures/control (opens in new tab).
How a format run works#
The file you wrote, byte for byte. Nothing is changed on disk at any point.
export function Cart({ items }: Props) @{
var total = 0;
debugger;
<section class="cart">
@if (items.length > 0) {
@for (const item of items; key item.id) {
<Row item={item} />
}
} @else {
<Empty />
}
</section>
}One byte-oriented pass finds every TSRX control token and records its exact position. This is the real overlay for the file above:
| Token | Bytes |
|---|---|
FunctionBody | 39–40 |
If | 101–102 |
For | 132–133 |
Else | 216–217 |
The TSRX syntax becomes formatting-safe markers in an in-memory copy; your code is copied verbatim. This is the actual projection:
export function Cart({ items }: Props) /*_t0_0*/{
var total = 0;
debugger;
<section class="cart">
{_t0_W0_({async *_t0_M0_(){/*_t0_N0S__*//*_t0_1*/if (items.length > 0) {
/*_t0_2*/for (const item of _t0_H0_(/*_t0_R0S__*/items/*_t0_R0E__*/,_t0_KH0_(/*_t0_K0S__*/item.id/*_t0_K0E__*/),_t0_HE0_)) {
<Row item={item} />
}
} /*_t0_3*/else {
<Empty />
}/*_t0_N0E__*/}},_t0_E0_)}
</section>
}Canonical Oxfmt parses and lays out that copy exactly once. The markers are designed to survive formatting so nothing about your control flow is lost.
A checked single pass converts the formatted copy back into TSRX: markers become @-controls again, raw <style> bytes are restored from your original, and the result must re-scan to the same structure before anything is written.
Four steps:
- Copy. Build a valid TSX copy, the same idea as linting, except the placeholders here are markers designed to survive formatting.
- Format. Oxfmt parses and formats that copy, once.
- Convert back. Markers become
@if,@for,@switch, and@tryagain, and your code keeps its new formatting. - Check. The result is re-read and compared against the original. If the structure does not match, the tool errors out instead of writing a broken file.
Two things are carried over rather than reformatted: dynamic closing tags are
rebuilt from their opening expression, and whatever is inside a raw <style>
block is copied from your file untouched.
Usage#
# Check without modifying files; exits 1 and lists files that differ oxc-tsrx-fmt --check src/Counter.tsrx Checking formatting... src/Counter.tsrx (0ms) Format issues found in above 1 files. Run without `--check` to fix. Finished in 0ms on 1 files using 18 threads. # Format and write files; success prints only the summary line oxc-tsrx-fmt --write src/Counter.tsrx src/View.tsx Finished in 0ms on 2 files using 18 threads. # Editor/stdin mode: formatted source goes to stdout oxc-tsrx-fmt --stdin-filepath=src/Counter.tsrx < src/Counter.tsrx export function Counter({ start }: { start: number }) @{ var count = start; console.log("mounted"); debugger; <div class="counter"> <span>{count}</span> </div>; } # Explicit config and worker count oxc-tsrx-fmt --write --config config/format.json --threads=4 src/Counter.tsrx Finished in 0ms on 1 files using 4 threads. # The explicit config switched the file to single quotes, no semicolons cat src/Counter.tsrx export function Counter({ start }: { start: number }) @{ var count = start console.log('mounted') debugger ;<div class="counter"> <span>{count}</span> </div> }
Writes are transactional: every file in the batch must format successfully before the first one is replaced on disk, so a crash or bad file never leaves your project half-formatted. Symbolic links are rejected.
Configuration#
Your .oxfmtrc.json works as usual. oxfmt searches upward from the current
directory to find it, or takes a --config path, and the layout options you
already use all apply: printWidth, singleQuote, semi, tabWidth,
trailingComma, and the rest, plus overrides and ignorePatterns.
Configuration has the full list.
A few options are refused with a clear error before anything is written,
because they would change .tsrx output in ways this project cannot yet
guarantee: sortImports, jsdoc, embedded-language formatting, experimental
flags, .editorconfig, and JS or TS config files.
CSS inside <style> is preserved, not formatted#
Bytes inside a raw <style> element are copied through exactly as you wrote
them. The upstream OXC CSS formatter currently can't be used without patching
OXC's dependency graph, and this project's core rule is no patches, so CSS
formatting waits until upstream exposes a clean package boundary.
How we know it is safe#
This is tested against a real TSRX codebase, not just fixtures. All 179 valid
files format, re-parse, and settle, meaning formatting an already-formatted file
changes nothing. All 12 broken files are rejected rather than mangled, and every
raw <style> block comes out byte for byte identical.