Skip to content
fuzdevPublic

About

precise language tools for TypeScript/JS, CSS, and Svelte in Rust

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

28 stars

Watchers

0 watching

Forks

Repository files navigation

tsv

a pixelated orange quill drawing a precise line

precise language tools for TypeScript/JS, CSS, and Svelte in Rust - tsv.fuz.dev

tsv is a Rust toolchain for TypeScript/JS, CSS, and Svelte (and planned HTML/JSON). It ships a formatter that closely follows Prettier + prettier-plugin-svelte, and a parser whose AST is compatible with Svelte's parser + acorn + acorn-typescript.

tsv aims to simplify its covered domains and stay lean, and so it makes opinionated choices. The formatter has a single non-configurable style, using Svelte's Prettier config. Among other benefits, this means tsv doesn't depend on a JS runtime, which it would need in order to resolve configs the way Prettier does, avoiding complexity and performance costs.

Compared to Oxc, Biome, and swc, tsv is a set of focused tools, not an extensible language platform, so it targets Web standards + TS + Svelte and there's no support for JSX/SCSS/etc. tsv's extensibility story is currently limited to using its Rust crates as libraries (or forking); bridging to JS or wasm plugins is an open question (leaning against).

Compared to rsvelte, tsv has its own TS/JS/CSS parsers instead of using Oxc. rsvelte also ships a Svelte compiler and linter/typechecker integration; tsv has experimental work in that direction that may never ship.

tsv prioritizes, in order:

  1. correctness (spec conformance for HTML/CSS/JS, fidelity to Svelte and TypeScript)
  2. speed
  3. binary size and memory usage
  4. extensibility, modularity, reusability

Staying simple is an overarching goal. Feedback is welcome to help navigate these tradeoffs.

See the benchmarks for measurements. Compared to Oxc/Oxfmt and Biome, tsv is faster and smaller and uses less memory, but lacks their features, extensibility, and broad language support. One reason for tsv to exist is to help find the performance left on the table in the Web's implementations.

tsv's formatters are near production-ready, with a long tail of rare bugs. Reports and opinions are appreciated. See the issues and discussions.

The only first-party unsafe is in tsv_ffi, where the C boundary requires it; tsv_napi relaxes the lint only to deny so napi-derive's generated code compiles. Otherwise unsafe_code = "forbid".

AI disclosure: this codebase is LLM-generated using Claude Code. It's a high-effort project that prioritizes quality.

Features

status: the formatters are near production-ready - a long tail of rare bugs remains, and APIs may still change

tsv implements aspects of:

  • HTML/CSS/JS and other Web specs
  • TypeScript
  • Svelte
  • Prettier and prettier-plugin-svelte

tsv features:

  • parsers for TypeScript/JS, CSS, and Svelte, with ASTs compatible with Svelte+acorn+acorn-typescript
  • formatters following Prettier + prettier-plugin-svelte + Svelte's prettier config (with intentional divergences and no supported style config)
  • VS Code formatter extension - fuzdev.tsv-format on the Marketplace and Open VSX (WASM-only, works in desktop VS Code and the browser host)
  • type stripping: TypeScript to JS by blanking types to whitespace, preserving every position — tsv strip, strip_typescript in @fuzdev/tsv-wasm and @fuzdev/tsv (docs/strip.md)
  • import records: every module request a TypeScript, JS or Svelte source makes — static, re-export, dynamic, import x = require() — with its span, decoded specifier and type-only verdict — parse_imports_typescript / parse_imports_svelte in @fuzdev/tsv-wasm and @fuzdev/tsv (docs/imports.md)

Future features (unknown order):

  • minifier
  • JSON support
  • HTML support (formatting as Svelte isn't correct e.g. with whitespace and {, but the lift to support it is small)
  • JS parsing diagnostics (test262 negative cases)
  • CSS error recovery (recover past invalid CSS per the spec)
  • maybe later or never:
    • TypeScript 7 integration (the Go impl), unlocking what rsvelte already provides:
      • linter - type aware, initially focused on serializable data-only plugins for extensibility
      • svelte-check replacement
      • LSP
      • includes an experimental first-party typechecker tsv_check (which may never ship)
    • Svelte compiler (experimental partial implementation, may never ship - see rsvelte)
    • bundling is out of scope
    • discussion welcome

Install

tsv ships to npm as a native package for Node and Bun, plus three WASM packages for browsers, Deno, and platforms without a prebuilt binary:

npm i -D @fuzdev/tsv
npx tsv format src            # if installed locally
npx @fuzdev/tsv format src    # or without installing first
import {format_svelte} from '@fuzdev/tsv';
const result = format_svelte('<script>\nconst   x=1\n</script>');
if (result.ok) console.log(result.code); // else result.error: {kind, message, frame, start, end, loc}
import {parse_svelte, type Root} from '@fuzdev/tsv';
const result = parse_svelte('<script>const x = 1;</script>');
if (result.ok) {
	const ast: Root = result.ast;
}

@fuzdev/tsv and @fuzdev/tsv-wasm are drop-in swaps. The native package is prebuilt for Linux (x64 gnu and musl, arm64 gnu), macOS (arm64 and x64), and Windows x64. As with other wasm packages, browsers must call await init().

The same native tsv CLI binaries are attached to each GitHub Release (one per platform above, with a SHA512SUMS for sha512sum -c) for use without npm; every asset carries a build provenance attestation (gh attestation verify tsv-linux-x64-gnu -R fuzdev/tsv). The checksum file and the attestation are this channel's whole integrity story; it has no signed release log of its own.

With cargo-binstall, cargo binstall tsv_cli installs that same prebuilt binary from the GitHub Release matching the crates.io version (Linux x64 and arm64 gnu, glibc 2.28 or newer; macOS arm64 and x64; Windows x64); any other target, musl included, falls back to a source build. From crates.io, cargo install --locked tsv_cli builds the same tsv binary from source (the crate is tsv_cli; the tsv crate on crates.io is an unrelated project). That build doesn't get the release profile the prebuilt binaries ship with (LTO and panic = "abort" - a workspace's profiles don't travel with a published crate): it runs slower, and an internal panic while formatting files is reported as that file's error rather than aborting the run.

See the website docs and package READMEs for the full API and CLI flags:

Rust

The library crates are on crates.io, one per language - tsv_ts (TypeScript/JS), tsv_css, and tsv_svelte - each self-contained, with the same functions:

// format one source; a `Formatter` formats many on one thread, keeping its arenas warm
let code = tsv_ts::format("const x=42;")?; // "const x = 42;\n"
let code = tsv_css::format("a{color:red}")?; // "a {\n\tcolor: red;\n}\n"
let code = tsv_svelte::format("<p   class=\"a\">x</p>")?; // "<p class=\"a\">x</p>\n"
let code = tsv_ts::Formatter::new().format("let y=1")?; // "let y = 1;\n"

// TypeScript to JS by blanking the types, every surviving character at its line and column
let js = tsv_ts::strip("let x: number = 1;")?.code; // "let x         = 1;"

// the modules a source requests: one record each, in source order
let imports = tsv_ts::parse_imports("import type {T} from './t';")?; // 1 record, type-only

// parse to the wire JSON AST, the same JSON the npm packages return
let json = tsv_ts::parse_to_json("const x = 1;")?;

Errors carry a kind() (Syntax, Refusal, TooDeep, TooLarge), a one-line message(), a span() in the input's UTF-8 bytes, and the report the npm packages return (to_report()); {} prints one line and {:#} the CLI's frame. TypeScript takes an exact parse goal (Goal::Script / Goal::Module) through format_with_goal, strip_with_goal and JsonOptions::goal; with none named, format, strip and parse_imports try the module grammar, then the script one, and parsing reads a module (parse_imports_with_goal names one).

This release exposes no AST: parsing returns the wire JSON — the AST as JSON in the canonical parser's shape (acorn-typescript's, Svelte's, parseCss's), its start / end UTF-16 offsets, span-only by default (no loc), unlike span()'s UTF-8 byte range — whose shape the npm packages type in tsv_ast.d.ts. parse_to_json_with(source, JsonOptions) adds loc (the locations cargo feature); the default json feature can be turned off for a build without parse_to_json*.

npm (@fuzdev/tsv, @fuzdev/tsv-wasm) Rust
format_typescript(src) tsv_ts::format(src)
format_typescript(src, {sourceType: 'script'}) tsv_ts::format_with_goal(src, Goal::Script)
format_css / format_svelte tsv_css::format / tsv_svelte::format
- Formatter::new() then .format(src), for many sources on one thread
strip_typescript(src, {sourceType}) tsv_ts::strip / strip_with_goal
parse_imports_typescript(src, {sourceType}) tsv_ts::parse_imports / parse_imports_with_goal
parse_imports_svelte(src) tsv_svelte::parse_imports
- tsv_ts::strip_with_imports: the strip and the records from one parse
parse_typescript_json(src, {sourceType}) tsv_ts::parse_to_json / parse_to_json_with with JsonOptions::goal
parse_typescript(src) (an object) - (parse_to_json and a JSON reader)
parse_*(src, {locations: true}), reconstruct_locations JsonOptions::new().locations(true) (locations feature)
{ok: false, error} Err(Error) (strip: Err(StripError))
error.kind 'parse' / 'refused' / 'unsupported' ErrorKind::Syntax or TooDeep / ErrorKind::Refusal / StripError::Unsupported
error.message, error.frame message() / to_report()'s message, frame ({e:#} is both)
error.start / end, error.loc to_report()'s start / end (WirePoint: offset, line, column)
a thrown error for a source over 4 GiB ErrorKind::TooLarge
IgnoreStack (discovery) tsv_ignore::IgnoreStack (the discovery policy has no public Rust API)

The same 1-based lines and UTF-16 offsets appear on both sides; span() is the Rust-only byte range.

Also published: tsv_ignore, the gitignore-faithful matcher behind discovery, and tsv_html, the WHATWG named character references; tsv_lang and tsv_discover are support crates.

Design

tsv's goal is to be an optimal, focused toolchain for TypeScript/JS, CSS, and Svelte (with planned JSON/HTML).

  • reduce complexity within the defined scope
    • supports Web+TS+Svelte - but no JSX/SCSS/etc
    • formatter is non-configurable: formatting style is hardcoded to Prettier's defaults with Svelte's official repo config (printWidth: 100, useTabs: true, singleQuote: true, and trailingComma: 'none'), and there are no config files or CLI options for formatting style; (opinionated like gofmt, zig fmt, and Python's Black)
  • parsers compatible with Svelte/acorn/acorn-typescript
    • can generate a public JSON AST that should exactly match Svelte 5's modern AST with acorn and acorn-typescript (excluding bug fixes, see docs/conformance_svelte.md), and tsv has its own internal optimal AST
    • the AST is span-only by default (start/end offsets, like acorn's locations: false); {locations: true} adds loc — acorn's exactly for TypeScript, and for Svelte the true line/column of every node rather than a copy of Svelte's own loc quirks
    • svelte-tsv is an experimental fork of Svelte that parses with tsv (by the author of tsv), running Svelte's own compiler and test suite on tsv's ASTs for conformance and benchmarks
  • formatters following Prettier
    • formatting is similar to Prettier and prettier-plugin-svelte for the common case, and diverges more often for Svelte than TypeScript; tsv closely follows Prettier's TypeScript choices (see docs/conformance_prettier.md)
    • tsv format discovery honors .gitignore, .prettierignore, .formatignore (the latter is original to tsv, all 3 use gitignore syntax)
  • Rust-only
    • minimal dependencies
    • no C compiler needed to build tsv
    • crates are published to crates.io for reusability (Rust)
    • tsv currently has no support for JS plugins or JS/WASM runtime integration - JS bridging and WASM plugins will be evaluated to see if the tradeoffs work for tsv's goals, but the current lean is against, mainly for performance and simplicity; JS reaches tsv through the WASM and native N-API/FFI bindings
  • optimal
    • prioritizes speed, then binary size and memory usage
    • ships optimal binary artifacts: so if all you need is a formatter or parser, a minimal build is available (currently just for WASM where it matters more, and with lang-specific artifacts likely coming), and heavier future layers (incremental parsing, CST for LSP) will be feature-gated so they don't regress the focused artifacts
  • modern and Web-conformant, roughly aiming to be up-to-date with late-stage TC39 proposals

Intentional non-features:

  • no formatter style config settings
  • no markup for frameworks besides Svelte - no JSX/TSX, Vue, Astro, etc (unlike Biome+Oxc+SWC+friends) - but note that you can use tsv's crates and patterns to vibe your own thing
  • no SCSS, CSS Modules, LESS, etc - standard CSS with Svelte extensions only
  • no strict Prettier conformance - see the conformance doc and discussion #1

Each language is a self-contained Rust crate exposing the same parse_to_json/format functions over its own concrete types - no central Language trait, registry, or dynamic dispatch ("closed scope, open convention"). Each artifact includes only what it uses: the parse build excludes the printers, the format build excludes the JSON-AST conversion layer, and a future TypeScript/JS-only build would exclude Svelte and CSS (lang-specific builds aren't published yet) - see docs/architecture.md.

Docs

Developing

Dev dependencies:

  • Rust - rustc, cargo
  • Deno - see deno.json for the tasks
    • uses npm: imports from svelte, typescript, acorn, @sveltejs/acorn-typescript, prettier, prettier-plugin-svelte

Rust dependencies are kept fairly minimal. See CLAUDE.md § Rust Crates for the full list.

# Build workspace (recommended - uses deno tasks)
deno task build          # dev build
deno task dev            # watch mode (requires: cargo install cargo-watch)
deno task check          # all checks (typecheck, tests, audits, lint, fmt)

# Or build directly with cargo
cargo build --workspace
cargo check --workspace  # fast syntax check (no codegen)
cargo test --workspace   # all tests including fixture validation

# Run CLI
cargo run -p tsv_cli parse --content "const x = 1;" --parser typescript
cargo run -p tsv_cli format --content "<div>test</div>" --parser svelte

For the full reference see CLAUDE.md § Commands.

Project structure

Multi-crate workspace with clean separation of concerns:

tsv/
├── Cargo.toml         # workspace root
├── crates/
│   ├── tsv_lang/      # shared foundation (Span, Error, the doc printer, the wire writer)
│   ├── tsv_arena/     # shared binding substrate: per-thread reusable AST/doc arenas + the goal axis
│   ├── tsv_html/      # HTML spec data: named character references
│   ├── tsv_ignore/    # gitignore-aware discovery matcher (.gitignore/.formatignore/.prettierignore)
│   ├── tsv_discover/  # file-discovery policy (build-output heuristic + safety nets) over tsv_ignore
│   ├── tsv_ts/        # TypeScript parser/formatter (standalone)
│   ├── tsv_css/       # CSS parser/formatter (standalone)
│   ├── tsv_svelte/    # Svelte parser/formatter (uses tsv_ts + tsv_css)
│   ├── tsv_svelte_compile/ # experimental Svelte→JS compiler + JS canonicalizer (may never ship; not in any shipped artifact)
│   ├── tsv_check/     # experimental TypeScript binder/checker (may never ship; not in any shipped artifact)
│   ├── tsv_cli/       # unified CLI (binary: `tsv`)
│   ├── tsv_debug/     # dev utilities (binary: `tsv_debug`, uses Deno)
│   ├── tsv_ffi/       # C FFI bindings
│   ├── tsv_wasm/      # WebAssembly bindings
│   └── tsv_napi/      # N-API bindings (Node/Bun native path)
└── tests/             # workspace-level integration tests

Each language crate exports the same functions - see Rust; the AST-taking primitives the bindings and dev tools use are hidden plumbing, not public API.

For more details see CLAUDE.md.

Credits

tsv is an implementation of various software designs and specs:

Software:

Web Standards:

Claude Code was instrumental to this project, and tsv wouldn't exist without LLMs because of the high coding labor requirements. Source code of projects similar to tsv was not used by agents or consulted by the author unless listed above. The author learned Rust in 2015 but wrote only simple learning programs and some abandoned toys before tsv.

Contributing

fuz.dev/contributing

License

0BSD

The code for the following projects was sometimes read by AI agents while producing tsv, so their license information is included for completeness:

Svelte Copyright (c) 2016-2026 Svelte Contributors MIT - https://github.com/sveltejs/svelte/blob/main/LICENSE.md

Prettier Copyright © James Long and contributors MIT - https://github.com/prettier/prettier/blob/main/LICENSE

ts-blank-space Copyright 2024 Bloomberg Finance L.P. Apache-2.0 - https://github.com/bloomberg/ts-blank-space/blob/main/LICENSE

About

precise language tools for TypeScript/JS, CSS, and Svelte in Rust

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

28 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages