@fuzdev/tsv

中文文档
0.8.1发布于 2026/11/070BSD AND BSD-3-ClauseTypeScriptES Module零依赖构建来源已验证近 30 天下载 2422

native formatter and parser for Svelte, TypeScript, and CSS, a TypeScript type stripper, and an import-records parser (N-API)

原始使用文档
文档目录12 节
  1. Install
  2. CLI
  3. Usage
  4. Formatting
  5. Parsing
  6. Stripping types
  7. Listing imports
  8. Options
  9. File scoping
  10. Errors
  11. Status
  12. License

@fuzdev/tsv

native formatter and parser for Svelte, TypeScript, and CSS, a TypeScript type stripper, and an import-records parser (N-API)

Rust-based formatter + parser + type stripper + import lister as a prebuilt native addon for Node.js and Bun. A near-Prettier formatter that tracks Prettier + prettier-plugin-svelte closely (documented divergences), and a parser whose AST is compatible with Svelte's parser + acorn + acorn-typescript (and parseCss).

The API mirrors @fuzdev/tsv-wasm export for export — same names, same options, same errors — minus only what a WASM engine needs and this one doesn't (init, init_sync, wasm_module, reinstantiate, the ./worker subpath, and the WASM IgnoreStack's free() / [Symbol.dispose](), which a GC-managed native class doesn't need), so the two are drop-in swaps: this package is the fast native path; the WASM one runs everywhere (browsers included) and is the fallback for platforms without a prebuilt binding.

Docs and benchmarks: tsv.fuz.dev. Source and conformance notes: github.com/fuzdev/tsv.

Install

npm i @fuzdev/tsv

Requires Node.js 22+ or Bun.

The right platform binary installs automatically (per-platform optionalDependencies). Prebuilt platforms:

  • linux-x64-gnu, linux-arm64-gnu, linux-x64-musl (Alpine)
  • darwin-arm64, darwin-x64
  • win32-x64

On any other platform the import throws with a pointer at @fuzdev/tsv-wasm.

The same native tsv CLI binary is also attached to each GitHub Release (one per platform above, with a SHA512SUMS for sha512sum -c; every asset carries a build provenance attestation: gh attestation verify <file> -R fuzdev/tsv) for use without npm.

CLI

npx @fuzdev/tsv format src        # format .ts/.mts/.cts/.js/.mjs/.cjs/.svelte/.css in place, recursively
npx @fuzdev/tsv format --check .  # CI: exit 1 if anything would change
npx @fuzdev/tsv format --list .   # list the in-scope files, format nothing
npx @fuzdev/tsv parse file.svelte # JSON AST to stdout (--pretty to indent)
npx @fuzdev/tsv strip file.ts     # TypeScript to JS on stdout, every position kept

Installed (npm i -D @fuzdev/tsv), the bin is tsv — and here it is the real native CLI: the platform package ships tsv's production Rust binary beside the addon, and the bin execs it directly (the esbuild/biome shape), so you get the native CLI's exact contract — multi-file parallelism (--jobs), parallel discovery, native error paths. (Both binaries ship because neither can play the other's role: an addon can't be exec'd as a process, and an executable can't be loaded as an in-process module.)

Directories recurse over the JS/TS family (.ts/.mts/.cts/.js/.mjs/.cjs), .svelte, and .css with gitignore-aware discovery:

  • Inside a git repo it honors .gitignore, then .formatignore — both hierarchical, like git — plus .prettierignore as a drop-in fallback, read in any directory that has no .formatignore of its own (a sibling .formatignore shadows it, with a warning); all scoped to the repo so results are reproducible.
  • Outside a repo it honors only .formatignore.
  • With no .gitignore in scope (in or out of a repo), discovery falls back to skipping hidden directories and dist/build/target. node_modules and VCS directories are always skipped by a walk.
  • A path you name — file or directory — is bounded by the ignore files alone: one they exclude is skipped (with a warning, unless it is a file your .formatignore or .prettierignore excludes), while the built-in skips never apply to it; a named file's extension must still be one tsv formats.

format --list prints the discovered in-scope files without formatting — a read-only view of what format would touch. --content <source> / --stdin (with --parser svelte|typescript|css) format or parse strings to stdout. For TypeScript, --source-type script|module (parse defaults to module; format takes an unset flag as none named — module, retried as a script, except a .mjs/.mts path, a module by its own name, which takes no retry — and accepts it on --content/--stdin only) selects the parse goal — at script, await is an ordinary identifier, top-level import/export, for await and import.meta are errors, and the code is sloppy unless its own "use strict" prologue makes it strict (so with and legacy octal literals/escapes parse). parse emits the span-only wire (no per-node loc; Svelte also no name_loc); parse --locations adds them. Exit codes — format: 0 clean, 1 would-change (--check), 2 errors; parse: 0 parsed, 1 the input does not parse, 2 a usage or read error.

strip takes one TypeScript input — a .ts/.mts/.cts path (a .svelte.ts rune module included; a JavaScript path is refused rather than read as TypeScript), --content, or --stdin — and prints it as JS with every type blanked to whitespace (Stripping types); --check writes nothing to stdout and only sets the exit code (a refusal or parse error still says why on stderr). The parse goal is format's: a module, retried as a script if that fails (a .mts path takes no retry), or the one --source-type script|module names for --content/--stdin. Exit codes — strip: 0 stripped, 1 refused (TypeScript that type blanking cannot erase, such as an enum), 2 a parse error, an unreadable file or a usage error.

Files format in parallel; --jobs N overrides the worker count. (If the platform package's CLI binary is missing or unrunnable, the bin degrades to a JS mirror of the same contract over the addon — which parallelizes too, on worker threads; on a platform with no prebuilt package at all, use @fuzdev/tsv-wasm instead.) Both packages claim the tsv bin name, so install one or the other in a project.

Usage

The package is ESM, and there is no initialization step (a CommonJS host loads it with await import('@fuzdev/tsv')):

import {format_svelte, format_typescript, parse_typescript} from '@fuzdev/tsv';

const formatted = format_svelte('<script>\nconst   x=1\n</script>'); // {ok: true, code}
const parsed = parse_typescript('const x = 1;'); // {ok: true, ast}: acorn-typescript-shaped, span-only
if (!parsed.ok) throw new Error(parsed.error.message);

Formatting

format_svelte / format_typescript / format_css take a source string and return {ok: true, code} with the formatted string, or {ok: false, error} for a source they cannot format (Errors). Formatting itself is non-configurable; the only option is TypeScript's parse goal, format_typescript(source, {sourceType: 'script' | 'module'}):

  • 'module' — the source is a module, always strict.
  • 'script' — await is an ordinary identifier, top-level import/export, for await and import.meta are syntax errors, and the code is sloppy unless its own "use strict" prologue makes it strict (so with and legacy octal literals/escapes parse).
  • omitted — not the same as 'module': the source is formatted as a module and, only if that parse fails, retried as a script. So a legacy sloppy script formats with no options at all, while anything the module grammar accepts is never reinterpreted.

A set value is exact. With none named and both grammars rejecting the source, the error returned is the one that best explains the failure (the rule is in the repo's docs/cli.md). parse_typescript has no such fallback; there an omitted sourceType means 'module'.

Parsing

parse_svelte / parse_typescript / parse_css return {ok: true, ast} with the language's public JSON AST as an object — span-only by default, start/end offsets on every node and no per-node loc; the parse_*_json siblings return that wire as a JSON string, {ok: true, json}, for consumers that forward it without paying JSON.parse. A source that doesn't parse returns {ok: false, error} (Errors). The object parsers take an optional {locations?, sourceType?} bag, the _json ones {sourceType?}, and sourceType is TypeScript-only. TypeScript types for the AST are bundled in tsv_ast.d.ts and re-exported from the package (import type {...} from '@fuzdev/tsv').

locations: true adds loc (and Svelte's name_loc), computed in JS after the parse by reconstruct_locations. It is exported too, with create_locator(source, {language}) for sparse lookups (language required), whose position_at(offset) / loc_of(node) throw a RangeError for an offset or span the source doesn't hold (a TypeError for an offset that isn't a number, or a node whose start/end getter throws); the whole-tree forms (reconstruct_locations, the locator's reconstruct(ast)) check nothing per node, and take an acyclic tree — the parse's own, or a clone of it. Both are also the @fuzdev/tsv/locations entry point, pure JS that loads no native addon.

Stripping types

strip_typescript(source) turns TypeScript into JS by blanking every type to whitespace — no transform, no sourcemap: every surviving character keeps its line and UTF-16 column, line terminators are kept verbatim, and a type-free source comes back unchanged. Where blanking alone would let the next line continue the statement, one ;, ( or ) takes a blanked character's place.

It returns a result rather than throwing for what it finds in the source: {ok: true, code}, or {ok: false, error} with error.kind 'parse' (the error a parse_typescript of the source returns at the goal the error belongs to) or 'unsupported' (TypeScript that type blanking cannot erase — an enum, a value namespace, a parameter property, import =, export =, <T>x, or an as / satisfies whose removal would regroup its expression — named by error.construct, with its span as start/end and loc, a one-line message, and a frame with the line and a caret).

import {strip_typescript} from '@fuzdev/tsv';

const result = strip_typescript('let id: string = "a";\nenum Color { Red }\n');
if (!result.ok && result.error.kind === 'unsupported') {
    result.error.construct; // 'enum'
    result.error.loc.start; // {line: 2, column: 0}
}

The one option is sourceType, read as format_typescript reads it: omitted, the source is a module retried as a script if that fails, so a legacy sloppy script strips as the sloppy script it is; 'script' or 'module' is exact.

Listing imports

parse_imports_typescript(source) and parse_imports_svelte(source) list a source's module requests — every static import, re-export, dynamic import(…) and import x = require(…), in source order — without handing you an AST: {ok: true, hasModuleSyntax, imports}, or {ok: false, error} for a source that does not parse. Each record is {kind, start, end, specifier, typeOnly, phase, attributes} — kind 'static' / 're_export' / 'dynamic' / 'import_equals', specifier {value, start, end} (the decoded string, its span quotes excluded) or null for a computed import(x), typeOnly 'statement' / 'specifiers' / null, phase 'source' / 'defer' / null, attributes a {start, end} span or null — in UTF-16 offsets, the same records @fuzdev/tsv-wasm returns.

import {parse_imports_svelte} from '@fuzdev/tsv';

const result = parse_imports_svelte('<script>\n\timport Button from "./Button.svelte";\n</script>\n');
if (result.ok) {
    result.imports[0]?.specifier?.value; // './Button.svelte'
    result.hasModuleSyntax; // true
}

parse_imports_typescript takes sourceType, read as strip_typescript reads it; parse_imports_svelte takes none.

Options

Options behave as in @fuzdev/tsv-wasm: a supported key set to undefined reads as its default, and a sourceType-only bag forwards to any function (sourceType: undefined included on Svelte and CSS; a locations key, whatever its value, throws anywhere but an object parser). Argument errors are thrown TypeErrors:

  • an unknown key, whatever its value;
  • a wrong-typed or invalid value, or a source that is not a string or not well-formed UTF-16 (it holds a lone surrogate);
  • a non-object second argument, arrays included — write sources.map((s) => format_typescript(s)), not sources.map(format_typescript), which passes the index.

File scoping

IgnoreStack is tsv's own hierarchical, git-faithful matcher plus its discovery policy (classify_dir, should_format_file, is_path_pruned, path_shadow_warning, excluded_argument_warning, unsupported_extension_error, and the warning templates), exported so tooling can reproduce exactly which files tsv format would touch — the same class @fuzdev/tsv-wasm exports; @fuzdev/tsv-format-wasm documents it with examples. The two answer every call its declared types admit alike; unlike the facade's functions, the class checks its arguments in each engine's own binding layer, so a wrong-typed argument is refused or coerced differently by each.

Errors

A source that doesn't parse returns {ok: false, error} — from a parser, a formatter and the strip alike, the same result @fuzdev/tsv-wasm returns — with error a plain object (no stack; postMessage-able): {kind: 'parse', message, frame, start, end, loc}, typed TsvParseError (exported), in the same coordinates as the AST's own positions:

  • message says what went wrong, on one line; frame is the excerpt under it — a line:column header and the source line, then a caret line — so `${message}\n${frame}` is the text the CLI prints.
  • start and end are the error's UTF-16 offsets — a parse error is a point, so end === start — and loc is their {start: {line, column}, end: {line, column}} (1-based line, 0-based UTF-16 column), shaped like a node's: create_locator(source, {language}).loc_of(result.error) equals it.
  • TypeScript counts ECMAScript line terminators (LF, CR, CRLF, U+2028, U+2029) and a leading BOM; Svelte (<script>, <style> and template expressions included) and CSS count LF alone and leave a leading BOM out.
  • The frame's header is loc.start as line:column + 1.
  • A formatter reports positions in your own source, CRLF line endings included — the error a parser reports (its message can differ where it quotes source text, which a formatter reads with line endings folded to LF), except that with no sourceType, format_typescript may report its script retry's error instead.
import {format_typescript} from '@fuzdev/tsv';

const result = format_typescript('let a;\nconst = ;');
if (!result.ok) {
    result.error.kind; // 'parse'
    result.error.message; // "Expected identifier or destructuring pattern, found '='"
    result.error.frame; // '2:7 const = ;\n          ^ here'
    [result.error.start, result.error.loc.start]; // [13, {line: 2, column: 6}]
}

format_svelte refuses one source that does parse: a lone CR (not part of a CRLF) with comment text after it inside a // comment between a tag's attributes, which Svelte ends at LF alone — the formatter's CR-to-LF fold would end that comment early and turn the rest of its line into markup. The refusal is {kind: 'refused', message, frame, start, end, loc} (typed FormatRefusedError), its span the CR.

What still throws is what no result can report: an argument error (a TypeError, above), a source over the 4 GiB size cap, and the engine failures below.

A Rust panic — always a tsv bug, please report it — is also thrown rather than aborting the process; stack overflow is the one crash that could still abort, as a bare SIGSEGV, and tsv's depth limits keep it out of reach: a source nested or chained past them is refused as an ordinary parse error ({ok: false, error}) — counting the parens the formatter would add, so the output of any source it accepts parses again — and every input under them fits a 2 MiB thread — graded with every shape the limits count at its deepest accepted depth (the limits and the stack contract are in the repo's docs/cli.md). A main thread (the process stack limit, commonly 8 MiB) and a worker_threads worker (Node's 4 MiB default) both hold it; only a worker you start with resourceLimits.stackSizeMb below 2 is under the contract. The bundled tsv CLI sizes its own stack. Reconstructing loc — {locations: true}, or reconstruct_locations — runs in JS without recursion, so it reaches as deep as the parse does.

Status

The formatters are near production-ready. A long tail of rare bugs remains and APIs may still change; reports are appreciated.

License

0BSD AND BSD-3-Clause: tsv's own code is 0BSD, and the WHATWG named character reference table compiled in is BSD-3-Clause — see NOTICE, which ships in this package.

版本与发布记录

@fuzdev/tsv 共发布 9 个版本,首次发布于 2026/09/15;过去 12 个月发布了 9 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.8.1latest2026/11/078 天前
0.8.02026/11/0213 天前
0.7.12026/10/2224 天前
0.7.02026/10/1828 天前
0.6.02026/10/071 个月前
0.5.02026/10/031 个月前

关于 @fuzdev/tsv 的常见问题

@fuzdev/tsv 是什么?

native formatter and parser for Svelte, TypeScript, and CSS, a TypeScript type stripper, and an import-records parser (N-API)

如何安装 @fuzdev/tsv?

在项目目录运行 npm install @fuzdev/tsv,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 tsv,可以用 npx tsv 直接运行。

@fuzdev/tsv 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@fuzdev/tsv 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@fuzdev/tsv 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@fuzdev/tsv 可以商用吗?

@fuzdev/tsv 使用 0BSD AND BSD-3-Clause 许可。声明的许可为 0BSD AND BSD-3-Clause,请阅读许可原文确认使用条件。以上为概括,不构成法律意见,请以许可原文为准。

@fuzdev/tsv 的下载量有多少?

根据 npm 官方下载统计,@fuzdev/tsv 在 2026/10/16 – 2026/11/14 这 30 天内被下载 2,422 次,日均约 81 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@fuzdev/tsv 还在维护吗?

最新版本 0.8.1 发布于 2026/11/07(8 天前);至今共发布 9 个版本,其中过去 12 个月发布了 9 个;首次发布于 2026/09/15。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。