introduction #

tsv is a toolchain for TypeScript/JS, CSS, and Svelte in Rust. The first release has a formatter that closely follows Prettier + prettier-plugin-svelte, and a drop-in replacement for Svelte's parser + acorn + acorn-typescript.

Compared to Oxc, Biome, and SWC, tsv is a set of focused tools, not a generic language platform, so the focus is web standards and there's no support for JSX/SCSS/etc, beyond Svelte as the only JS framework. The extensibility story is currently limited to using its Rust crates as libraries; bridging to JS or WASM plugins is an open question, but may not be supported.

tsv prioritizes, in order:

  1. correctness (Svelte and TypeScript conformance, spec adherence for HTML/CSS/JS)
  2. speed
  3. binary size and memory usage
  4. extensibility (valued but deprioritized)

See the benchmarks for stats. Compared to Oxc and Biome, tsv is significantly faster, smaller, and uses less memory to parse and format its supported languages

This is an early release, and reports and feedback are appreciated - see the issues and discussions.

AI disclosure: this codebase is mostly LLM-generated, and the usual caveats apply. The first release took 7 months and ~1800 manual commits. It's a high-effort project that prioritizes quality.

These docs are a work in progress. For design details see the readme.

Install
#

tsv ships as WASM packages on npm, a CLI with a formatter and parser:

npm i -D @fuzdev/tsv_wasm npx tsv format src npx tsv parse src/foo.svelte

For smaller builds, the formatter and parser also ship solo:

npm i -D @fuzdev/tsv_format_wasm npm i -D @fuzdev/tsv_parse_wasm

See the benchmarks for size and performance details.

Native builds are not yet available but are coming in v0.2, see issue 139.

Usage
#

All three packages share the same API. The full package exports both halves:

import {format_svelte, parse_svelte, type Root} from '@fuzdev/tsv_wasm'; const formatted = format_svelte('<script>\nconst x=1\n<\/script>'); const ast: Root = parse_svelte('<script>const x = 1;<\/script>');

The formatter alone:

import {format_svelte} from '@fuzdev/tsv_format_wasm'; const formatted = format_svelte('<script>\nconst x=1\n<\/script>');

The parser alone:

import {parse_svelte, type Root} from '@fuzdev/tsv_parse_wasm'; const ast: Root = parse_svelte('<script>const x = 1;<\/script>');

format_typescript, format_css, parse_typescript, and parse_css work the same way, and the parsers return Svelte-compatible JSON ASTs with bundled TS types. Everything works zero-config in Node.js, Bun, and Deno (sync auto-init); browsers and bundlers call await init() once first.

Span-only parsing
#

For TypeScript and Svelte, the parsers also emit a span-only AST that drops the per-node loc (line/column) object — Svelte also drops name_loc — for a ~46% smaller, faster-to-materialize result, mirroring acorn's locations: false. Line and column stay derivable from the start/end offsets plus your source, so nothing is lost when you have the source.

import {parse_typescript_no_locations, reconstruct_locations} from '@fuzdev/tsv_parse_wasm'; // span-only AST: start/end offsets, no per-node loc (~46% smaller) const ast = parse_typescript_no_locations('const x = 1;'); // derive line/column back when you need it, no re-parse reconstruct_locations(ast, 'const x = 1;');

reconstruct_locations(ast, source) walks the tree and adds loc back, mutating in place — exact for TypeScript, approximate for Svelte (it skips the parser's own name_loc and a couple of position quirks). For sparse lookups, create_locator(source) reuses one line table across calls. CSS has no loc, so there's no span-only variant for it.

Source code
#