diff --git a/CLAUDE.md b/CLAUDE.md index 46c2c85..e05c7f2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,18 +23,20 @@ IMPORTANT for AI agents: Do NOT run `gro dev` - the developer will manage the de - Svelte 5 - component framework with runes - SvelteKit - application framework with static adapter -- fuz_css (`@fuzdev/fuz_css`) - CSS framework and design system +- fuz_css (`@fuzdev/fuz_css`) - semantic-first CSS framework and design system - fuz_ui (`@fuzdev/fuz_ui`) - UI components, theming, docs system - fuz_util (`@fuzdev/fuz_util`) - utility functions - fuz_code (`@fuzdev/fuz_code`) - syntax highlighting - Gro (`@fuzdev/gro`) - build system and task runner -- prettier + prettier-plugin-svelte - code formatting +- tsv (via Gro) - code formatting - mdz (`@fuzdev/mdz`) - markdown preprocessor wired into `svelte.config.js` -- `@fuzdev/tsv_wasm` - tsv's formatter + parser as WASM; powers the playground, loaded lazily in the browser +- `@fuzdev/tsv-wasm` - tsv's formatter + parser as WASM; powers the playground, loaded lazily in the browser -Note: `@fuzdev/tsv_wasm` is loaded only on `/docs/playground` via a browser-only dynamic `import()`, so the ~900KB WASM never weighs down `/docs` or the prerendered pages. +Note: `@fuzdev/tsv-wasm` is loaded only on `/docs/playground` via a browser-only dynamic `import()`, so the ~1MB-gzipped WASM (~2.8MB decoded) never weighs down `/docs` or the prerendered pages. -Note: several devDependencies — `@webref/css` (CSS spec data), `zimmerframe` (AST traversal), `@sveltejs/acorn-typescript`, `zod`, and `@fuzdev/blake3_wasm` — are *optional peer dependencies* of `@fuzdev/fuz_css`'s `vite_plugin_fuz_css`, declared here so its build-time CSS generation resolves them (e.g. `css_literal.ts` imports `@webref/css`, `css_class_extractor.ts` walks ASTs with `zimmerframe`). They aren't imported by this app's own source, so don't mistake them for dead deps. +Note: several devDependencies — `@webref/css` (CSS spec data), `zimmerframe` (AST traversal), `@sveltejs/acorn-typescript`, `zod`, and `@fuzdev/blake3-wasm` — are *optional peer dependencies* of `@fuzdev/fuz_css`'s `vite_plugin_fuz_css`, declared here so its build-time CSS generation resolves them (e.g. `css_literal.ts` imports `@webref/css`, `css_class_extractor.ts` walks ASTs with `zimmerframe`). They aren't imported by this app's own source, so don't mistake them for dead deps. Likewise `esm-env`, `@types/estree`, and `@types/node` are optional peers of `@fuzdev/fuz_util`, `@fuzdev/mdz`, and `@fuzdev/fuz_ui`, and `tslib` backs `tsconfig.json`'s `importHelpers` — none is imported here directly either. + +Note: `vite` is deliberately held at 7.x (with `@sveltejs/vite-plugin-svelte` 6.x) — vite 8 + plugin-svelte 7 was buggy with this app or SvelteKit's integration. Don't upgrade to vite 8 without deliberately re-verifying the site works. ## Scope @@ -43,37 +45,38 @@ tsv.fuz.dev is the public face of the tsv tool: - Landing page (home) with links to benchmarks and docs - Benchmarks page with bar charts and summary tables - Docs section (introduction, playground, benchmarks) -- Interactive playground (`/docs/playground`) — edit a deliberately-unformatted Svelte example in a syntax-highlighted editor (fuz_code's `CodeTextarea`); the formatted output and parsed AST update live alongside it; runs `@fuzdev/tsv_wasm` as lazily-loaded WASM +- Interactive playground (`/docs/playground`) — edit a deliberately-unformatted Svelte example in a syntax-highlighted editor (fuz_code's `CodeTextarea`); the formatted output and parsed AST update live alongside it; runs `@fuzdev/tsv-wasm` as lazily-loaded WASM - Theme controls via fuz_ui's `ThemeRoot` in the root layout (no separate about/settings page) -- Shows install instructions for `@fuzdev/tsv_wasm` (full tool + `tsv` CLI; format/parse subsets mentioned in the intro) +- Shows install instructions led by the native `@fuzdev/tsv` (prebuilt N-API addon for Node/Bun, ships the `tsv` CLI), then `@fuzdev/tsv-wasm` (universal, same `tsv` CLI) and the format/parse subsets ### What tsv.fuz.dev does NOT include (yet) - Authentication or backend -- Native-binary downloads (tsv ships WASM-only at v0.1) +- Standalone native-binary downloads (the native path ships as the `@fuzdev/tsv` npm addon, not as a downloadable executable) ## Routes ``` src/ -├── lib/ -│ └── sample.ts # placeholder constant (not yet wired up) ├── routes/ │ ├── +page.svelte # Home: hero, links to docs and benchmarks │ ├── +layout.svelte # Root layout: fuz_css/fuz_code CSS, ThemeRoot, SiteState │ ├── +layout.ts # prerender: true, ssr: true │ ├── style.css # global styles │ ├── library.ts # builds library_json from virtual:svelte-docinfo + pkg.json -│ ├── example.test.ts # placeholder test │ └── docs/ │ ├── +layout.svelte # Docs layout (Docs wrapper; sets library_context) │ ├── +page.svelte # Docs index (DocsContent) │ ├── tomes.ts # Docs structure (introduction, playground, benchmarks) │ ├── introduction/ # Introduction page (install + usage) -│ ├── playground/ # Interactive playground (Playground.svelte + playground_example.ts; lazy @fuzdev/tsv_wasm) -│ └── benchmarks/ # Benchmark visualizations (BenchmarksBar, BenchmarksGroup, etc.) +│ ├── playground/ # Interactive playground (Playground.svelte + playground_example.ts; lazy @fuzdev/tsv-wasm) +│ └── benchmarks/ # Benchmarks page: the four JSON reports, benchmark_data.ts / benchmarks_cli.ts / formatter_benchmark_data.ts, the .gen.json.ts for the CLI harness, and the Benchmarks*.svelte visualizations (see Benchmarks below) └── test/ - └── benchmark_data.test.ts # tests for benchmark-data derivations + ├── benchmark_data.test.ts # unit tests for the pure derivations and formatters + ├── benchmark_data.shape.test.ts # shape gates over the committed JSON reports + ├── benchmark_data.prose.test.ts # gates every ratio the page's prose quotes + ├── benchmarks_cli.test.ts # the CLI-harness data as the page consumes it + └── formatter_benchmark_data.test.ts # the harness README parser ``` ## Benchmarks @@ -81,32 +84,85 @@ src/ Benchmark data comes from `tsv`. Full workflow to update: ```bash -# 1. In ~/dev/tsv — run benchmarks (builds ffi + wasm automatically) +# 1. In ~/dev/tsv — run benchmarks across deno/node/bun (builds artifacts automatically) deno task bench -# 2. In ~/dev/tsv.fuz.dev — copy the latest result +# 2. In ~/dev/tsv.fuz.dev — copy the latest results npm run update-benchmarks ``` -Step 1 writes `benches/deno/results/report.json` (committed to tsv). -Step 2 copies it to `src/routes/docs/benchmarks/benchmarks.json`. -The JSON format matches `BenchmarkBaseline` in `benchmark_data.ts`. +Step 1 writes the per-runtime `benches/js/results/report..{json,md}` +siblings, the composed cross-runtime `report.{json,md}`, and the conformance +coverage report `report.conformance.node.json` (committed to tsv). +Step 2 copies three of them — `report.node.json` → `benchmarks.json`, the +composed `report.json` → `benchmarks_cross_runtime.json`, and +`report.conformance.node.json` → `benchmarks_conformance.json` — verbatim; +tsv already writes them tab-indented, and Gro's formatter leaves JSON untouched, +so the committed copies are byte-identical to tsv's reports and the tests +gate their shape. Note the script's source paths are hardcoded to +`../tsv` — if the reports were generated in a different worktree, copy them +into `~/dev/tsv` (or copy manually) first. +The JSON formats match the types in `benchmark_data.ts`. + +The end-to-end CLI comparison against Prettier, Biome, and Oxfmt comes from a +separate harness, a fork of Oxc's `bench-formatter` that adds tsv +(../oxc-bench-formatter). It publishes no JSON — its numbers live only as the +hyperfine console dump in its README, between the +`` / `END` markers, plus a `## Versions` list +and a `_Measured on: …_` machine line. To update: + +```bash +# 1. In ~/dev/oxc-bench-formatter — re-run and rewrite its README (needs a built ../tsv) +pnpm run update-readme + +# 2. In ~/dev/tsv.fuz.dev — reparse the README into JSON +gro gen +``` + +`benchmarks_formatters.gen.json.ts` parses that README and writes +`benchmarks_formatters.json`, keeping only the scenarios tsv participates in +(it has no JSX/TSX parser, so the harness runs it on the JSX-free corpora +only). That includes the harness's Svelte scenario, which benches tsv against +rsvelte-fmt (`@rsvelte/fmt`), and its tsv-only delivery scenario (the native +binary vs `@fuzdev/tsv`'s Node dispatcher vs `@fuzdev/tsv-wasm`, flagged +`tsv_only` so "every other tool" claims skip it); a scenario renders on the +page only once it has an entry in `SCENARIO_COPY` (`benchmarks_cli.ts`), and +prose claims about the Svelte head-to-head are conditional on its data being +present, so the site stays correct whether or not the harness README has been +regenerated with it. + +A **missing** sibling checkout is the one tolerated case — generation is +skipped, the committed JSON stands, and `gro gen --check` passes on any machine +or CI that has only this repo (CI never checks out the harness, so it always +takes this path; no `--no-gen` needed). A README that **is** present but has +drifted fails the task loudly, naming the scenario and the section that stopped +parsing, rather than publishing stale or scenario-stripped numbers. A drift that +still parses but renames a scenario is caught on the site side instead: every +key in `benchmarks_cli.ts`'s `SCENARIO_COPY` must resolve to generated data, and +a test asserts it. Key files in `src/routes/docs/benchmarks/`: -- `benchmarks.json` — raw benchmark data (copied from tsv) +- `benchmarks.json` — per-runtime Node report (copied from tsv) +- `benchmarks_cross_runtime.json` — composed cross-runtime report (copied from tsv) +- `benchmarks_conformance.json` — conformance parse-coverage report (copied from tsv) +- `benchmarks_formatters.json` — formatter CLI comparison, generated from the sibling harness's README - `benchmark_data.ts` — TypeScript types matching the JSON format -- `benchmarks.ts` — re-exports the JSON with types +- `formatter_benchmark_data.ts` — types plus `parse_formatter_benchmarks`, the README parser +- `benchmarks_cli.ts` — shapes `benchmarks_formatters.json` for `BenchmarksCli.svelte` and owns the per-scenario prose; the numbers are all generated +- `benchmarks.ts`, `benchmarks_cross_runtime.ts`, `benchmarks_conformance.ts`, `benchmarks_formatters.ts` — re-export the JSON with types - `BenchmarksBar.svelte`, `BenchmarksGroup.svelte`, etc. — visualization components +- `BenchmarksBaselineGroup.svelte` — shared interactive column behind the format, parse, and binary-size groups: hovering a row re-baselines that group's ratios (each of the three groups per section is independent), restoring the default anchor (the canonical reference — Prettier for format, the JS baseline for parse — and the smallest build for size) on mouseleave. `benchmark_data.ts`'s `compute_baseline_ratio`/`format_baseline_ratio`/`baseline_ratio_color` carry the per-`BaselineDirection` (`speed`/`size`) formulas it and the derivations share ## Architecture - Static SvelteKit app (`adapter-static`), deploys to GitHub Pages - Uses fuz_ui tome system for docs navigation - `docs/tomes.ts` defines the doc sections: introduction, playground, benchmarks -- Benchmark data lives in `src/routes/docs/benchmarks/benchmarks.json` (see [Benchmarks](#benchmarks)) +- Benchmark data lives in `src/routes/docs/benchmarks/` as four JSON reports — three copied from tsv, one generated from the sibling CLI harness (see [Benchmarks](#benchmarks)) +- The benchmarks page quotes no hand-written ratios — its prose computes them from the same reports the charts render, via `benchmark_data.ts`'s `benchmark_speedup`/`format_ratio_approx`/`format_ratio_range`/`format_ratio_range_approx` and `benchmarks_cli.ts`'s `cli_speedup_vs_tsv`/`cli_memory_ratio_range`. A test gates that every pair the copy names still resolves - `library.ts` builds component metadata at runtime from the `virtual:svelte-docinfo` module (provided by the `svelte-docinfo` Vite plugin); the docs index passes it to `DocsContent` -- The playground (`/docs/playground`) loads `@fuzdev/tsv_wasm` via a browser-only dynamic `import()` inside `Playground.svelte`, so the WASM code-splits into its own chunk fetched only on that route — the same lazy discipline `library.ts` uses for the heavy svelte-docinfo data, keeping `/docs` and the prerendered pages WASM-free. `@fuzdev/tsv_wasm` is in `vite.config.ts` `optimizeDeps.exclude` (like `blake3_wasm`) +- The playground (`/docs/playground`) loads `@fuzdev/tsv-wasm` via a browser-only dynamic `import()` inside `Playground.svelte`, so the WASM code-splits into its own chunk fetched only on that route — the same lazy discipline `library.ts` uses for the heavy svelte-docinfo data, keeping `/docs` and the prerendered pages WASM-free. `@fuzdev/tsv-wasm` is in `vite.config.ts` `optimizeDeps.exclude` (like `@fuzdev/blake3-wasm`) - The playground's editor is fuz_code's `CodeTextarea` (live syntax highlighting via the experimental CSS Custom Highlight API). It needs `@fuzdev/fuz_code/theme_highlight.css`, imported inside `Playground.svelte` rather than the root layout so it stays on this route only; `supports_css_highlight_api()` drives a graceful-degradation note where the API is unavailable (the editor still works, unstyled) ## Deployment @@ -117,7 +173,7 @@ Deploys to `https://tsv.fuz.dev/` via `gro deploy` (builds and pushes to deploy - TypeScript strict mode - Svelte 5 with runes API -- Prettier with tabs, 100 char width +- tsv with tabs, 100 char width - Node >= 24.14 - Private package (not published to npm) diff --git a/eslint.config.js b/eslint.config.js index 100b57f..d903ae8 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,3 +1,3 @@ -import {configs} from '@ryanatkn/eslint-config'; +import { configs } from '@ryanatkn/eslint-config'; export default configs; diff --git a/package-lock.json b/package-lock.json index ee9152a..773b020 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,37 +9,34 @@ "version": "0.0.1", "license": "MIT", "devDependencies": { - "@fuzdev/blake3_wasm": "^0.1.1", - "@fuzdev/fuz_code": "^0.46.3", - "@fuzdev/fuz_css": "^0.63.2", - "@fuzdev/fuz_ui": "^0.206.5", - "@fuzdev/fuz_util": "^0.65.2", - "@fuzdev/gro": "^0.205.1", - "@fuzdev/mdz": "^0.2.0", - "@fuzdev/tsv_wasm": "^0.1.0", - "@ryanatkn/eslint-config": "^0.12.1", + "@fuzdev/blake3-wasm": "^0.2.0", + "@fuzdev/fuz_code": "^0.48.0", + "@fuzdev/fuz_css": "^0.63.3", + "@fuzdev/fuz_ui": "^0.208.0", + "@fuzdev/fuz_util": "^0.69.0", + "@fuzdev/gro": "^0.212.0", + "@fuzdev/mdz": "^0.3.0", + "@fuzdev/tsv-wasm": "^0.4.0", + "@ryanatkn/eslint-config": "^0.12.2", "@sveltejs/acorn-typescript": "^1.0.9", "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.61.1", - "@sveltejs/vite-plugin-svelte": "^7.1.2", + "@sveltejs/kit": "^2.69.1", + "@sveltejs/vite-plugin-svelte": "^6.2.4", "@types/estree": "^1.0.8", "@types/node": "^24.12.4", "@webref/css": "^8.2.0", "eslint": "^9.39.1", "eslint-plugin-svelte": "^3.13.1", "esm-env": "^1.2.2", - "magic-string": "^0.30.21", - "prettier": "^3.7.4", - "prettier-plugin-svelte": "^3.4.1", - "svelte": "^5.56.0", - "svelte-check": "^4.3.6", - "svelte-docinfo": "^0.5.4", + "svelte": "^5.56.4", + "svelte-check": "^4.7.2", + "svelte-docinfo": "^0.7.0", "svelte2tsx": "^0.7.47", "tslib": "^2.8.1", "typescript": "^5.9.3", "typescript-eslint": "^8.48.1", - "vite": "^8.0.16", - "vitest": "^4.1.9", + "vite": "^7.3.6", + "vitest": "^5.0.0", "zimmerframe": "^1.1.4", "zod": "^4.3.6" }, @@ -54,6 +51,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" @@ -66,6 +64,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -77,6 +76,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -94,7 +94,6 @@ "os": [ "aix" ], - "peer": true, "engines": { "node": ">=18" } @@ -112,7 +111,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -130,7 +128,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -148,7 +145,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -166,7 +162,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=18" } @@ -184,7 +179,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=18" } @@ -202,7 +196,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -220,7 +213,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -238,7 +230,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -256,7 +247,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -274,7 +264,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -292,7 +281,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -310,7 +298,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -328,7 +315,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -346,7 +332,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -364,7 +349,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -382,7 +366,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -400,7 +383,6 @@ "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -418,7 +400,6 @@ "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -436,7 +417,6 @@ "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -454,7 +434,6 @@ "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -472,7 +451,6 @@ "os": [ "openharmony" ], - "peer": true, "engines": { "node": ">=18" } @@ -490,7 +468,6 @@ "os": [ "sunos" ], - "peer": true, "engines": { "node": ">=18" } @@ -508,7 +485,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -526,7 +502,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -544,7 +519,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -706,10 +680,10 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@fuzdev/blake3_wasm": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@fuzdev/blake3_wasm/-/blake3_wasm-0.1.1.tgz", - "integrity": "sha512-JikFOouJEVLKJvsEQ7+fRdo3GElL4nmu2sV8rg+xu2bv+BAMk+GvoO3TOSPYX9fdHeXJ7U4N0IdIP/mNh7WNfw==", + "node_modules/@fuzdev/blake3-wasm": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@fuzdev/blake3-wasm/-/blake3-wasm-0.2.0.tgz", + "integrity": "sha512-rDZsN3Pk4k0pxuCfa+Mp6dTbS6RUL4o9awdteEA3EPjMls7agOpKP3fKLH4+XbboCOHeb2JvB1ouJ6v1mFL/bA==", "dev": true, "license": "MIT", "engines": { @@ -720,9 +694,9 @@ } }, "node_modules/@fuzdev/fuz_code": { - "version": "0.46.3", - "resolved": "https://registry.npmjs.org/@fuzdev/fuz_code/-/fuz_code-0.46.3.tgz", - "integrity": "sha512-vN+h5Z9t0dBBEMTlngWQas/wJqK5L6xsroItj17iGuDvmm8V4kKrFbUEFltnSSwLrICypXyywrCTwqVb/QkFqA==", + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@fuzdev/fuz_code/-/fuz_code-0.48.0.tgz", + "integrity": "sha512-uzcAkElwu73UULfd2Vij0U9oCL0ySZEYHd8CxYGSglxLZc4JzTIxDpFkY67glPBmAVR36CgyXpdWGGCTW3bF7A==", "dev": true, "license": "MIT", "dependencies": { @@ -751,9 +725,9 @@ } }, "node_modules/@fuzdev/fuz_css": { - "version": "0.63.2", - "resolved": "https://registry.npmjs.org/@fuzdev/fuz_css/-/fuz_css-0.63.2.tgz", - "integrity": "sha512-df2j970oRPHPJVGoMIhkHltRI6ZMTrhVoBEPfek8hY8RiFMnJMW8cFq+X6oTSGpdfo1COQpjHbeLZemd74z+mg==", + "version": "0.63.3", + "resolved": "https://registry.npmjs.org/@fuzdev/fuz_css/-/fuz_css-0.63.3.tgz", + "integrity": "sha512-nZB2nG2AdwNDVfZ1Qmu0Rt905vzeS8sfEkGZZRjzl+tFbAndCbvnOUfh9B+MDtffXm8V1CRfE6+mJoGXOBOhSw==", "dev": true, "license": "MIT", "engines": { @@ -764,7 +738,7 @@ }, "peerDependencies": { "@fuzdev/blake3_wasm": "^0.1.0", - "@fuzdev/fuz_util": ">=0.65.1", + "@fuzdev/fuz_util": ">=0.65.2", "@fuzdev/gro": ">=0.203.0", "@sveltejs/acorn-typescript": "^1", "@webref/css": "^8", @@ -801,9 +775,9 @@ } }, "node_modules/@fuzdev/fuz_ui": { - "version": "0.206.5", - "resolved": "https://registry.npmjs.org/@fuzdev/fuz_ui/-/fuz_ui-0.206.5.tgz", - "integrity": "sha512-P+9U7xuGs1O1NrusiItcCQF/Sx7HPs/Tukg6cyWqDOatdRtro/nIVlzASZvk7/34fzGrwXYiHobRmY1jBchxHA==", + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@fuzdev/fuz_ui/-/fuz_ui-0.208.0.tgz", + "integrity": "sha512-WBKa9ZiMXxkcCrqMaRxTZMxx8xvB0P67uKPIJsRjPSzOXT+at/wTBUr1htasPy9RiL/Rfbd3wAZdUWSRG5hlMQ==", "dev": true, "license": "MIT", "engines": { @@ -842,9 +816,9 @@ } }, "node_modules/@fuzdev/fuz_util": { - "version": "0.65.2", - "resolved": "https://registry.npmjs.org/@fuzdev/fuz_util/-/fuz_util-0.65.2.tgz", - "integrity": "sha512-VE20HhfZTPOY3SMx4kBELaAZFSRppbskQsIsqy4SvMmGknc0LrxN6jTrnzhp0blooPt3vPparf/yKdzwPUVUCg==", + "version": "0.69.0", + "resolved": "https://registry.npmjs.org/@fuzdev/fuz_util/-/fuz_util-0.69.0.tgz", + "integrity": "sha512-oXOftvPL0bQ4mQ0qxfssqsaBIFVT0F11G+bkDPmB3XZB3Q6TK3OpLMsAR2axczt9ijrFog4bas3j89SZO1mJdA==", "dev": true, "license": "MIT", "engines": { @@ -854,7 +828,7 @@ "url": "https://www.ryanatkn.com/funding" }, "peerDependencies": { - "@fuzdev/blake3_wasm": "^0.1.0", + "@fuzdev/blake3-wasm": "^0.2.0", "@types/estree": "^1", "@types/node": "^24", "esm-env": "^1.2.2", @@ -863,7 +837,7 @@ "zod": "^4.0.14" }, "peerDependenciesMeta": { - "@fuzdev/blake3_wasm": { + "@fuzdev/blake3-wasm": { "optional": true }, "@types/estree": { @@ -884,18 +858,17 @@ } }, "node_modules/@fuzdev/gro": { - "version": "0.205.1", - "resolved": "https://registry.npmjs.org/@fuzdev/gro/-/gro-0.205.1.tgz", - "integrity": "sha512-o5WBXWSH/UlN1BsDnpBNIi0WHR7lj7UFGltH/JctSP7oSB2ojvmoXD5OLCEeVdRVcDaSQKl48xcXKUwHKqVSmQ==", + "version": "0.212.0", + "resolved": "https://registry.npmjs.org/@fuzdev/gro/-/gro-0.212.0.tgz", + "integrity": "sha512-AUnwZssUoBITdXBGm6UkIZgiGhNk+7igNNHMcu17A7CBpGgqrwWR9m9CJvwspEX9CguVFVO/rp5JwGKGZNGSJA==", "dev": true, "license": "MIT", "dependencies": { + "@fuzdev/tsv": "^0.4.0", "chokidar": "^5.0.0", "dotenv": "^17.2.3", "esm-env": "^1.2.2", "oxc-parser": "^0.99.0", - "prettier": "^3.7.4", - "prettier-plugin-svelte": "^3.5.1", "ts-blank-space": "^0.6.2", "tslib": "^2.8.1" }, @@ -909,17 +882,18 @@ "url": "https://www.ryanatkn.com/funding" }, "optionalDependencies": { - "vitest": "^3 || ^4" + "vitest": "^3 || ^4 || ^5" }, "peerDependencies": { - "@fuzdev/blake3_wasm": "^0.1.0", - "@fuzdev/fuz_util": ">=0.65.2", + "@fuzdev/blake3-wasm": "^0.2.0", + "@fuzdev/fuz_util": ">=0.69.0", "@sveltejs/kit": "^2", "esbuild": "^0.28.0", "svelte": "^5", - "svelte-docinfo": ">=0.4.1", + "svelte-docinfo": ">=0.6.0", "typescript": "^5", - "vitest": "^3 || ^4", + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vitest": "^3 || ^4 || ^5", "zod": "^4" }, "peerDependenciesMeta": { @@ -929,15 +903,18 @@ "svelte-docinfo": { "optional": true }, + "vite": { + "optional": true + }, "vitest": { "optional": true } } }, "node_modules/@fuzdev/mdz": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@fuzdev/mdz/-/mdz-0.2.0.tgz", - "integrity": "sha512-ZdAcPtGAETgMg4kGx9xZ+K6FJ76CMoQ0MoxC0+r/2iCgXvo70VkQSxOswmfojzX335WT+Oe0PzsiXy1DWDth9Q==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@fuzdev/mdz/-/mdz-0.3.0.tgz", + "integrity": "sha512-hd6hEWYY/CUqAHc277pYKySkjHbwABy6vY+AOd0GArOcsi6QM6FZ3nTiB3Lzw7NECx0ODPjPuHCFPYkFOhKm8w==", "dev": true, "license": "MIT", "dependencies": { @@ -966,17 +943,170 @@ } } }, - "node_modules/@fuzdev/tsv_wasm": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@fuzdev/tsv_wasm/-/tsv_wasm-0.1.0.tgz", - "integrity": "sha512-hlZxdL1c5jiMyLnMv3NWONxNR64JChNekxlsKEJxXvgr/bmcRsgF1XuyabOTtppW9C+9HUvCpklJgJeU4aZbKw==", + "node_modules/@fuzdev/tsv": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@fuzdev/tsv/-/tsv-0.4.0.tgz", + "integrity": "sha512-dGh7w6fG0gUdRE36i0RCj1AH7SXe/SFW2G9KQJNxA0fk5onKDiRuc+JpEluI5zuutgUWrrsVN7IHagBJi/VOQA==", + "dev": true, + "license": "MIT", + "bin": { + "tsv": "bin.js" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://www.ryanatkn.com/funding" + }, + "optionalDependencies": { + "@fuzdev/tsv-darwin-arm64": "0.4.0", + "@fuzdev/tsv-darwin-x64": "0.4.0", + "@fuzdev/tsv-linux-arm64-gnu": "0.4.0", + "@fuzdev/tsv-linux-x64-gnu": "0.4.0", + "@fuzdev/tsv-linux-x64-musl": "0.4.0", + "@fuzdev/tsv-win32-x64": "0.4.0" + } + }, + "node_modules/@fuzdev/tsv-darwin-arm64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@fuzdev/tsv-darwin-arm64/-/tsv-darwin-arm64-0.4.0.tgz", + "integrity": "sha512-2K1x1+9/IZ5lMtCsiHRBAJ6r2BD6dGUz5slr8FYtNBpYRdqFy4QtXoE89pRq9GLJ96XJyS31tjC/YrpMNCxwWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://www.ryanatkn.com/funding" + } + }, + "node_modules/@fuzdev/tsv-darwin-x64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@fuzdev/tsv-darwin-x64/-/tsv-darwin-x64-0.4.0.tgz", + "integrity": "sha512-Y6vG/OdAj8RNpYBOuLeInyl1OLhn7nIG6g1jBQWLCfbMDRgKgkb5pcBjc4dCqlfzv/EXpvc3rSRJYVOc7cTE1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://www.ryanatkn.com/funding" + } + }, + "node_modules/@fuzdev/tsv-linux-arm64-gnu": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@fuzdev/tsv-linux-arm64-gnu/-/tsv-linux-arm64-gnu-0.4.0.tgz", + "integrity": "sha512-HvVnMraSt8mHKhjhAmHUFdSvzmAmkMXgWp33ubOnGsntOoKzfUpw0518nkHY0I8aP1+x1+bXVe+5/aB8NfJQZw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://www.ryanatkn.com/funding" + } + }, + "node_modules/@fuzdev/tsv-linux-x64-gnu": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@fuzdev/tsv-linux-x64-gnu/-/tsv-linux-x64-gnu-0.4.0.tgz", + "integrity": "sha512-x+w/LpaJdXo/b01QAad7Gsho4J5526BEMJxP5r8tuvlYNsBcYNFKDrCZV+NRxZN0QmmgxPJnQI8efa29lcZTUA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://www.ryanatkn.com/funding" + } + }, + "node_modules/@fuzdev/tsv-linux-x64-musl": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@fuzdev/tsv-linux-x64-musl/-/tsv-linux-x64-musl-0.4.0.tgz", + "integrity": "sha512-NT1tDx7w/ZvzDlmL0p+q5V9Ou0PAp9/vH4BjsdE0QCbS81xxVuUHZqNTMo4Z1I7ojfA4yomvFnFwsf5jb27rtw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://www.ryanatkn.com/funding" + } + }, + "node_modules/@fuzdev/tsv-wasm": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@fuzdev/tsv-wasm/-/tsv-wasm-0.4.0.tgz", + "integrity": "sha512-FxPju91PXkbDRe3kPG6drwuU37PNDlm66+MS2+zpEPklCeSDoLAt7d6lMq9rR87v+8GpppWnaW+jJ5P/UnwhEw==", "dev": true, "license": "MIT", "bin": { "tsv": "cli.js" }, "engines": { - "node": ">=20" + "node": ">=22" + }, + "funding": { + "url": "https://www.ryanatkn.com/funding" + } + }, + "node_modules/@fuzdev/tsv-win32-x64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@fuzdev/tsv-win32-x64/-/tsv-win32-x64-0.4.0.tgz", + "integrity": "sha512-aJNRqt8ceOMdyx84nvwPiuEZp+bzXGgTjqBGX6xcg4L9LyGB3fEJNvO92sBqEp96bSv5EGFUMXiaSlYVr/y12A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=22" }, "funding": { "url": "https://www.ryanatkn.com/funding" @@ -1067,9 +1197,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", "dev": true, "license": "MIT" }, @@ -1393,27 +1523,38 @@ "dev": true, "license": "MIT" }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", - "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", "cpu": [ - "arm64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", - "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", "cpu": [ "arm64" ], @@ -1422,15 +1563,12 @@ "optional": true, "os": [ "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", - "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", "cpu": [ "x64" ], @@ -1439,15 +1577,26 @@ "optional": true, "os": [ "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", - "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", "cpu": [ "x64" ], @@ -1456,32 +1605,46 @@ "optional": true, "os": [ "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", - "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", - "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", "cpu": [ "arm64" ], @@ -1493,15 +1656,12 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", - "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", "cpu": [ "arm64" ], @@ -1513,15 +1673,46 @@ "optional": true, "os": [ "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", - "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", "cpu": [ "ppc64" ], @@ -1533,17 +1724,31 @@ "optional": true, "os": [ "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", - "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", "cpu": [ - "s390x" + "riscv64" ], "dev": true, "libc": [ @@ -1553,17 +1758,31 @@ "optional": true, "os": [ "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", - "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", "cpu": [ - "x64" + "s390x" ], "dev": true, "libc": [ @@ -1573,15 +1792,29 @@ "optional": true, "os": [ "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", - "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", "cpu": [ "x64" ], @@ -1593,15 +1826,26 @@ "optional": true, "os": [ "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", - "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", "cpu": [ "arm64" ], @@ -1610,51 +1854,40 @@ "optional": true, "os": [ "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", - "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", "cpu": [ - "wasm32" + "arm64" ], "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "os": [ + "win32" + ] }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", - "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", "cpu": [ - "arm64" + "ia32" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", - "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", "cpu": [ "x64" ], @@ -1663,22 +1896,26 @@ "optional": true, "os": [ "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, "node_modules/@ryanatkn/eslint-config": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/@ryanatkn/eslint-config/-/eslint-config-0.12.1.tgz", - "integrity": "sha512-rur/pPhaaGyBlFjsPs1wIjyHnlPI5UT617TibNSAq6B3fialaI/YqsaDx/DMU5yRXlyhU6esYm+aOmXnckOZOA==", + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@ryanatkn/eslint-config/-/eslint-config-0.12.2.tgz", + "integrity": "sha512-p5yIpW0ka5Gev3VM2a7X8kEnVqORaF8JccU79D7BxEL+erTMlsn0aFWtMaxPXaIxBwL3yqJ+42uFc4AKDIn/aA==", "dev": true, "license": "Unlicense", "dependencies": { @@ -1724,9 +1961,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.61.1", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.61.1.tgz", - "integrity": "sha512-Ny8s1SR1TyQS2hD2Rvw0XKzU2Nw1eUF52dTb6T2bdcgz7wSC+Nyb5IwjWYlR4b2dvbbR5NJDiQwHg3rnNseghg==", + "version": "2.69.1", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.69.1.tgz", + "integrity": "sha512-+nz8Fx/cElzb2ZPHC+6Ll3y3NEVIe4Na75PeplLlyTmd1dBXAjz2KR14y1ZgNjb2ThfAYzulu+PFy1UE3RCUzA==", "dev": true, "license": "MIT", "dependencies": { @@ -1765,24 +2002,53 @@ } } }, + "node_modules/@sveltejs/load-config": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@sveltejs/load-config/-/load-config-0.2.0.tgz", + "integrity": "sha512-1LgZ/qUqSoq+QorD83lk2hka79Px0wXNW2q5V1nZlxGhQgw1jrsIbVz5YiCeucVLo4XvFLjXukUaQjIiqowkcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + } + }, "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.1.2.tgz", - "integrity": "sha512-DrUBA2UXRfDmUX/ZTiEopd3X40yavsJF1FX2RygcuIScHL7o5YX1fMvoYnDhjeJQC4weCOklirpNWlcb2NiSeA==", + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.4.tgz", + "integrity": "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==", "dev": true, "license": "MIT", "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", - "vitefu": "^1.1.2" + "vitefu": "^1.1.1" }, "engines": { "node": "^20.19 || ^22.12 || >=24" }, "peerDependencies": { - "svelte": "^5.46.4", - "vite": "^8.0.0-beta.7 || ^8.0.0" + "svelte": "^5.0.0", + "vite": "^6.3.0 || ^7.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.2.tgz", + "integrity": "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "obug": "^2.1.0" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", + "svelte": "^5.0.0", + "vite": "^6.3.0 || ^7.0.0" } }, "node_modules/@tybys/wasm-util": { @@ -1822,9 +2088,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, @@ -2109,34 +2375,17 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@vitest/expect": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", - "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.1.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.9", - "@vitest/utils": "4.1.9", - "chai": "^6.2.2", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/@vitest/mocker": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", - "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.0.tgz", + "integrity": "sha512-66PGTMIiVJP3t4a5yxU9qPtf7MdTBs8jmToMvy+HVflB3Yy13WJZTtPePdvU+wjRV02SKK5doLbSA6o9pwOmiA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.9", + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.0", "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" + "magic-string": "^1.2.3" }, "funding": { "url": "https://opencollective.com/vitest" @@ -2154,70 +2403,22 @@ } } }, - "node_modules/@vitest/pretty-format": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", - "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", - "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "node_modules/@vitest/mocker/node_modules/magic-string": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.3.1.tgz", + "integrity": "sha512-rm91zr2Ou+XueDTohjQQjdQEcYM6zVi8KVUCG8Ec3vHwUEKrhSdCNyfuIywkA6hcCAteIn0ZOtAHA6eGpiX+Pg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.9", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", - "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.9", - "@vitest/utils": "4.1.9", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "@jridgewell/sourcemap-codec": "^1.6.0" } }, "node_modules/@vitest/spy": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", - "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", - "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.0.tgz", + "integrity": "sha512-uy+luWBAPw9XfthoHi5AkfHUnuPYEESjl0p/r+meoBnU8bxg5GDQ3Ey8MjcJ6sqahkL4PFyrvfMJJBw7LbU06g==", "dev": true, "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.9", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.1.0" - }, "funding": { "url": "https://opencollective.com/vitest" } @@ -2438,15 +2639,8 @@ }, "node_modules/concat-map": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, "license": "MIT" }, @@ -2545,16 +2739,6 @@ "node": ">=0.10.0" } }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, "node_modules/devalue": { "version": "5.8.1", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", @@ -2576,9 +2760,9 @@ } }, "node_modules/es-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", "dev": true, "license": "MIT" }, @@ -2589,7 +2773,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -2801,9 +2984,9 @@ } }, "node_modules/esrap": { - "version": "2.2.9", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.9.tgz", - "integrity": "sha512-4KijP+NxCWthMCUC3qHbE6n4vCjqgJS1uAYKhuT/GWfFTf1Qyive2TgOjep+gzbSzRfnNyaN/UU9YmdOt8Eg0A==", + "version": "2.2.13", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.13.tgz", + "integrity": "sha512-m8jH5hZgJE2RRUK/jjkGPcJEDAV+dYnZYFkosQaPTcE+Yw4xynXHOo6FUdwaWBtdR3b1MMa7wEDTSHeR2VWsGA==", "dev": true, "license": "MIT", "dependencies": { @@ -2862,9 +3045,9 @@ } }, "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3181,279 +3364,6 @@ "node": ">= 0.8.0" } }, - "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -3579,15 +3489,18 @@ "license": "MIT" }, "node_modules/obug": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", "https://opencollective.com/debug" ], - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } }, "node_modules/optionator": { "version": "0.9.4", @@ -3705,13 +3618,6 @@ "node": ">=8" } }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3720,9 +3626,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", "engines": { @@ -3879,33 +3785,6 @@ "node": ">= 0.8.0" } }, - "node_modules/prettier": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", - "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prettier-plugin-svelte": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.5.1.tgz", - "integrity": "sha512-65+fr5+cgIKWKiqM1Doum4uX6bY8iFCdztvvp2RcF+AJoieaw9kJOFMNcJo/bkmKYsxFaM9OsVZK/gWauG/5mg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "prettier": "^3.0.0", - "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3940,48 +3819,49 @@ "node": ">=4" } }, - "node_modules/rolldown": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", - "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.133.0", - "@rolldown/pluginutils": "^1.0.0" + "@types/estree": "1.0.9" }, "bin": { - "rolldown": "bin/cli.mjs" + "rollup": "dist/bin/rollup" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18.0.0", + "npm": ">=8.0.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.3", - "@rolldown/binding-darwin-arm64": "1.0.3", - "@rolldown/binding-darwin-x64": "1.0.3", - "@rolldown/binding-freebsd-x64": "1.0.3", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", - "@rolldown/binding-linux-arm64-gnu": "1.0.3", - "@rolldown/binding-linux-arm64-musl": "1.0.3", - "@rolldown/binding-linux-ppc64-gnu": "1.0.3", - "@rolldown/binding-linux-s390x-gnu": "1.0.3", - "@rolldown/binding-linux-x64-gnu": "1.0.3", - "@rolldown/binding-linux-x64-musl": "1.0.3", - "@rolldown/binding-openharmony-arm64": "1.0.3", - "@rolldown/binding-wasm32-wasi": "1.0.3", - "@rolldown/binding-win32-arm64-msvc": "1.0.3", - "@rolldown/binding-win32-x64-msvc": "1.0.3" - } - }, - "node_modules/rolldown/node_modules/@oxc-project/types": { - "version": "0.133.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", - "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" } }, "node_modules/sade": { @@ -4087,9 +3967,9 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", - "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", "dev": true, "license": "MIT" }, @@ -4120,9 +4000,9 @@ } }, "node_modules/svelte": { - "version": "5.56.0", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.0.tgz", - "integrity": "sha512-kTXr26t1bchFp28ROrb957LtbujpBmBDibmqMGziVpUs7awBi96TGgX6SovrA8BNoEUDVRK2Fb9FkeYlGspoVg==", + "version": "5.56.4", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.4.tgz", + "integrity": "sha512-/d0QHehmRuJW8gVz395MTkPcPozxzdjBMBE8oEYGz8O3b9KTMzzQ9ZHJQLuFKOHOPQbU6kx/X4iid/EBBzH7iw==", "dev": true, "license": "MIT", "dependencies": { @@ -4137,7 +4017,7 @@ "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", - "esrap": "^2.2.9", + "esrap": "^2.2.12", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", @@ -4148,13 +4028,14 @@ } }, "node_modules/svelte-check": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.6.tgz", - "integrity": "sha512-uBkz96ElE3G4pt9E1Tw0xvBfIUQkeH794kDQZdAUk795UVMr+NJZpuFSS62vcmO/DuSalK83LyOwhgWq8YGU1Q==", + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.7.2.tgz", + "integrity": "sha512-GoS4XJdGswlq0rIT1vtFLzJY1bvHtY37McY9H9Gkm1Ggw/ICdZYn8J/Z8Yi0BEL0i3R4+jtaWVePjyppMlij/A==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", + "@sveltejs/load-config": "^0.2.0", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", @@ -4202,9 +4083,9 @@ } }, "node_modules/svelte-docinfo": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/svelte-docinfo/-/svelte-docinfo-0.5.4.tgz", - "integrity": "sha512-9XONWxu//kRmThbJBpDuNu3jSP6lSPgMy4T9G2GYHWWrHDiowJFn8ov5acvhBvAogBinBATylXk1TsvyQlI0iQ==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/svelte-docinfo/-/svelte-docinfo-0.7.0.tgz", + "integrity": "sha512-N+uoJf5UWgsT1E3KPc4iqeRtlkkn2e+c7k5HV97YUvnY0VoN0qNL5FjX34pbtvYDf+tJ7xjhQTP0+v13rrlZFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4212,8 +4093,7 @@ "commander": "^14.0.3", "es-module-lexer": "^2.0.0", "picomatch": "^4.0.4", - "tinyglobby": "^0.2.15", - "typescript": "^5.9.3" + "tinyglobby": "^0.2.15" }, "bin": { "svelte-docinfo": "dist/main.js" @@ -4221,6 +4101,7 @@ "peerDependencies": { "svelte": "^5.0.0", "svelte2tsx": "^0.7.30", + "typescript": "^5.9.3", "zod": "^4" } }, @@ -4270,16 +4151,19 @@ } }, "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.1.4.tgz", + "integrity": "sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } }, "node_modules/tinyexec": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", "dev": true, "license": "MIT", "engines": { @@ -4303,16 +4187,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -4432,17 +4306,18 @@ "license": "MIT" }, "node_modules/vite": { - "version": "8.0.16", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", - "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", "dev": true, "license": "MIT", "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.15", - "rolldown": "1.0.3", - "tinyglobby": "^0.2.17" + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" @@ -4458,10 +4333,9 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.18", - "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", + "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", @@ -4474,18 +4348,15 @@ "@types/node": { "optional": true }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, "jiti": { "optional": true }, "less": { "optional": true }, + "lightningcss": { + "optional": true + }, "sass": { "optional": true }, @@ -4530,38 +4401,31 @@ } }, "node_modules/vitest": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", - "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.0.tgz", + "integrity": "sha512-gpsMNoRhMjMktVxPtstOH4/PJuPyovVaMDr4oDilXaGH1EcqM2OE96SoHT2VIQ6fTGtTjqmHDrEu2X9RQiXf8Q==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.9", - "@vitest/mocker": "4.1.9", - "@vitest/pretty-format": "4.1.9", - "@vitest/runner": "4.1.9", - "@vitest/snapshot": "4.1.9", - "@vitest/spy": "4.1.9", - "@vitest/utils": "4.1.9", - "es-module-lexer": "^2.0.0", - "expect-type": "^1.3.0", - "magic-string": "^0.30.21", - "obug": "^2.1.1", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^4.0.0-rc.1", - "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.1.0", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.0.0", + "chai": "^6.2.2", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.2.3", + "obug": "^2.1.4", + "picomatch": "^4.0.7", + "std-env": "^4.2.0", + "tinybench": "6.1.4", + "tinyexec": "1.3.0", + "tinyglobby": "^0.2.17", "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -4569,16 +4433,16 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", - "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.9", - "@vitest/browser-preview": "4.1.9", - "@vitest/browser-webdriverio": "4.1.9", - "@vitest/coverage-istanbul": "4.1.9", - "@vitest/coverage-v8": "4.1.9", - "@vitest/ui": "4.1.9", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.0", + "@vitest/browser-preview": "5.0.0", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.0", + "@vitest/coverage-v8": "5.0.0", + "@vitest/ui": "5.0.0", "happy-dom": "*", "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { @@ -4619,6 +4483,16 @@ } } }, + "node_modules/vitest/node_modules/magic-string": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.3.1.tgz", + "integrity": "sha512-rm91zr2Ou+XueDTohjQQjdQEcYM6zVi8KVUCG8Ec3vHwUEKrhSdCNyfuIywkA6hcCAteIn0ZOtAHA6eGpiX+Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index e1c6a86..5bb16b3 100644 --- a/package.json +++ b/package.json @@ -16,64 +16,44 @@ "test": "gro test", "preview": "vite preview", "deploy": "gro deploy", - "update-benchmarks": "cp ../tsv/benches/deno/results/report.json src/routes/docs/benchmarks/benchmarks.json" + "update-benchmarks": "cp ../tsv/benches/js/results/report.node.json src/routes/docs/benchmarks/benchmarks.json && cp ../tsv/benches/js/results/report.json src/routes/docs/benchmarks/benchmarks_cross_runtime.json && cp ../tsv/benches/js/results/report.conformance.node.json src/routes/docs/benchmarks/benchmarks_conformance.json" }, "type": "module", "engines": { "node": ">=24.14" }, "devDependencies": { - "@fuzdev/blake3_wasm": "^0.1.1", - "@fuzdev/fuz_code": "^0.46.3", - "@fuzdev/fuz_css": "^0.63.2", - "@fuzdev/fuz_ui": "^0.206.5", - "@fuzdev/fuz_util": "^0.65.2", - "@fuzdev/gro": "^0.205.1", - "@fuzdev/mdz": "^0.2.0", - "@fuzdev/tsv_wasm": "^0.1.0", - "@ryanatkn/eslint-config": "^0.12.1", + "@fuzdev/blake3-wasm": "^0.2.0", + "@fuzdev/fuz_code": "^0.48.0", + "@fuzdev/fuz_css": "^0.63.3", + "@fuzdev/fuz_ui": "^0.208.0", + "@fuzdev/fuz_util": "^0.69.0", + "@fuzdev/gro": "^0.212.0", + "@fuzdev/mdz": "^0.3.0", + "@fuzdev/tsv-wasm": "^0.4.0", + "@ryanatkn/eslint-config": "^0.12.2", "@sveltejs/acorn-typescript": "^1.0.9", "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.61.1", - "@sveltejs/vite-plugin-svelte": "^7.1.2", + "@sveltejs/kit": "^2.69.1", + "@sveltejs/vite-plugin-svelte": "^6.2.4", "@types/estree": "^1.0.8", "@types/node": "^24.12.4", "@webref/css": "^8.2.0", "eslint": "^9.39.1", "eslint-plugin-svelte": "^3.13.1", "esm-env": "^1.2.2", - "magic-string": "^0.30.21", - "prettier": "^3.7.4", - "prettier-plugin-svelte": "^3.4.1", - "svelte": "^5.56.0", - "svelte-check": "^4.3.6", - "svelte-docinfo": "^0.5.4", + "svelte": "^5.56.4", + "svelte-check": "^4.7.2", + "svelte-docinfo": "^0.7.0", "svelte2tsx": "^0.7.47", "tslib": "^2.8.1", "typescript": "^5.9.3", "typescript-eslint": "^8.48.1", - "vite": "^8.0.16", - "vitest": "^4.1.9", + "vite": "^7.3.6", + "vitest": "^5.0.0", "zimmerframe": "^1.1.4", "zod": "^4.3.6" }, - "prettier": { - "plugins": [ - "prettier-plugin-svelte" - ], - "useTabs": true, - "printWidth": 100, - "singleQuote": true, - "bracketSpacing": false, - "overrides": [ - { - "files": "package.json", - "options": { - "useTabs": false - } - } - ] - }, "sideEffects": [ "**/*.css" ], diff --git a/src/app.d.ts b/src/app.d.ts index 8364d73..b9e1d38 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -6,7 +6,7 @@ declare module 'virtual:fuz.css' { export default css; } declare module 'virtual:pkg.json' { - import type {PkgJson} from '@fuzdev/fuz_util/pkg_json.ts'; + import type { PkgJson } from '@fuzdev/fuz_util/pkg_json.ts'; const pkg_json: PkgJson; export default pkg_json; } diff --git a/src/lib/sample.ts b/src/lib/sample.ts deleted file mode 100644 index 429c922..0000000 --- a/src/lib/sample.ts +++ /dev/null @@ -1 +0,0 @@ -export const sample = 'TODO - publish tsv!'; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 1e580bb..6411db4 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -4,13 +4,13 @@ import './style.css'; import ThemeRoot from '@fuzdev/fuz_ui/ThemeRoot.svelte'; - import {SiteState, site_context} from '@fuzdev/fuz_ui/site.svelte.ts'; - import {logo_tsv} from '@fuzdev/fuz_ui/logos.ts'; - import type {Snippet} from 'svelte'; + import { SiteState, site_context } from '@fuzdev/fuz_ui/site.svelte.ts'; + import { logo_tsv } from '@fuzdev/fuz_ui/logos.ts'; + import type { Snippet } from 'svelte'; import pkg_json from 'virtual:pkg.json'; const { - children, + children }: { children: Snippet; } = $props(); @@ -19,7 +19,7 @@ // and `repo_url` points to the tsv tool repo rather than this website's repo // (`pkg_json.repository`). site_context.set( - new SiteState({icon: logo_tsv, pkg_json, repo_url: 'https://github.com/fuzdev/tsv'}), + new SiteState({ icon: logo_tsv, pkg_json, repo_url: 'https://github.com/fuzdev/tsv' }) ); diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index e98aa2e..c29ee24 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,10 +1,10 @@ @@ -24,9 +24,9 @@
diff --git a/src/routes/docs/+layout.svelte b/src/routes/docs/+layout.svelte index a161ea3..28a2d09 100644 --- a/src/routes/docs/+layout.svelte +++ b/src/routes/docs/+layout.svelte @@ -1,13 +1,13 @@

- tsv is a toolchain for TypeScript/JS, CSS, and Svelte in Rust. Performance is its priority - after correctness, and the numbers compare favorably to Oxc and Biome for its supported - languages. + tsv is a toolchain for TypeScript/JS, CSS, and Svelte in Rust. After correctness, performance + is tsv's next priority. This page shows how it measures up against Prettier, which tsv closely + follows, and against Oxc and + Biome, which are similar tools with wider language support + (tsv doesn't support JSX/TSX/SCSS/etc). Also included for comparison: + rsvelte (Svelte parser/formatter), + Yuku (TypeScript/JS parser), swc + (TypeScript/JS parser), dprint-typescript (TypeScript/JS + formatter), malva (CSS formatter, dprint's CSS + plugin), and PostCSS (CSS parser).

-
-
- tsv native - tsv native json - tsv wasm - tsv wasm json - biome - oxc - canonical -
-
- - +

- Rather than supporting many languages, tsv focuses on Svelte/HTML, TypeScript/JS, and CSS. - This lets it be smaller when it's all you need, a quality that's more relevant when used in - the browser via wasm: + Compared to Oxc and Biome, tsv is smaller and faster at formatting its supported languages, + and faster than Oxc's parser on TypeScript/JS, the one language they share, when both emit the + same AST payload (Biome exposes no parser) — and lacks their features and language breadth. + Prose summary here; charts below.

- - + {/if} + +

+ Each section's notes contextualize its numbers. The charts are the {node_display} run; a + cross-runtime comparison closes the + page. +

- - + + + {#if unstable_entries.length} + + {/if} - +

- tsv's formatter is similar to Oxfmt - and Biome. Today it can format Svelte, TypeScript, and CSS, - plus HTML and JS (as strict-mode TypeScript): + tsv's formatter is similar to + Oxfmt, + Biome, and + dprint. It formats Svelte, TypeScript, + and CSS, plus JS through the same TypeScript parser:

{#each format_groups as group (group.language)} {/each}
- + +

+ tsv and oxc-parser share a mechanism: both serialize the AST to JSON in Rust and deserialize + it in JS, native and wasm alike. The deliverables differ — tsv's default wire + (tsv-json / tsv-wasm-json) carries a per-node loc + (line/column) object that oxc-parser's span-only AST omits, at ~{parse_ts_loc_cost} the + hand-off time on the TypeScript corpus. The tsv-json-no-locations / + tsv-wasm-json-no-locations entries drop it and emit oxc's span-only shape, so + they are the payload-matched comparison with oxc-parser (line/column stays derivable from the + offsets plus source, so nothing is lost). The tsv-internal and tsv-wasm-internal entries build + the native AST but skip JS-side materialization entirely, showing raw engine speed and tsv's + own serialization overhead rather than a cross-tool comparison. +

- The parse rows that build a full JS AST are directly comparable: tsv and oxc-parser both - serialize the AST to JSON in Rust and deserialize it in JS, native and wasm alike. The - tsv-internal and tsv_wasm-internal rows are tsv's parse-only numbers - they build the native - AST but skip JS-side materialization, so they show raw in-engine speed rather than a - cross-tool comparison. + yuku-parser, a JS/TS parser written in Zig, emits the same span-only AST as oxc, so it too + compares against the no-locs entries. It gets there differently: instead of JSON + it returns a compact binary buffer that its JS side decodes into objects lazily, so the bench + forces the whole tree and times that. The deliverable is the same fully materialized tree, so + the entries stay comparable — though every cross-tool entry times parsing and the hand-off + into JS together; this benchmark doesn't split the two.

{#each parse_groups as group (group.language)} @@ -141,9 +364,294 @@
  • JS parsers skip the Rust-to-JS serialization step that tsv and oxc pay for, which keeps - them competitive. + them competitive — and on CSS it decides the order: the JSON hand-off is most of tsv's + time there (compare the internal rows), and CSS is a simple enough grammar that Svelte's + parseCss and PostCSS both finish ahead of tsv-json. +
  • +
  • + Biome is grayed out across all three parse groups: @biomejs/js-api exposes + formatting and linting only, so the AST it parses internally never crosses the JS boundary + and can't be measured. +
  • +
  • + oxc-parser parses TypeScript and JS (and JSX, not measured here) only — no CSS, no Svelte. + Its AST has no line/column option to turn on (only a range flag that repeats + the offsets as a pair), so the span-only no-locs entries are the one + payload-matched pairing available. Its experimental raw-transfer mode is not a row: tsv's + harness measured it setup-dominated and slower than the eager JSON path it does time. +
  • +
  • + yuku-parser and swc both parse TypeScript and JS (and JSX, not measured here) and nothing + else — no CSS, no Svelte, no formatter — so they appear in the TypeScript parse group + only. +
  • +
  • + rsvelte's parser is the only other Svelte parser here, and it matches tsv's default wire + in mechanism: a JSON string carrying per-node loc throughout, which the + caller JSON.parses, exactly as tsv-json does — so + rsvelte-parse compares against tsv-json, not the + no-locs entries. Its second entry passes rsvelte's own + skipExpressionLoc, which drops loc only on embedded JS + expressions and keeps the top-level offsets, a different trade than tsv's span-only wire, + hence the entry is named for the option. +
  • +
  • + swc parses into its own AST shape — a Module root carrying span + offsets rather than ESTree's loc/range — so it isn't + payload-matched to either tsv wire. +
  • +
  • + PostCSS is the only other CSS parser here; none of the Rust CSS tools considered offers a + parse call: Lightning CSS's transform can hand JS the stylesheet tree through + a visitor hook, but only as a side channel of a transform run, Biome surfaces no parser, + and malva is a formatter. PostCSS is also the parser behind Prettier's CSS printer, so + it's the parse-side counterpart of the Prettier format entry. +
  • +
+ +
+ + + +

+ Where the speed numbers above use real-world code, this section measures parse + coverage: how much of a much larger, deliberately hard corpus each parser accepts — + Prettier's format-test suites, Svelte's compiler test suite, CSS extracted from + web-platform-tests, + test262's expected-valid tests, parsed at the + goal each declares, and the single-file cases from the + TypeScript compiler's own test suite + that tsc itself parses cleanly. +

+ {#if benchmarks_cross_runtime_json.conformance_vintage?.stale} + + {/if} + + +
+ + + +

+ tsv covers only Svelte/HTML, TypeScript/JS, and CSS, so it can be smaller when that's all you + need — which matters most in the browser via wasm. +

+ + +
+ + + +

+ The numbers above time tsv's engine in-process, one file at a time. This section is a fork of + Oxc's official + + bench-formatter + + that adds tsv, + timing the whole CLI end-to-end — process spawn, file discovery, I/O, each tool's default + multi-file parallelism — plus peak memory: what you experience typing the command, on real + code. tsv appears only in the JSX-free scenarios (it has no JSX/TSX parser). The tsv binary is + the native CLI @fuzdev/tsv ships in its platform packages and execs from + npx tsv, installed from npm and pinned by the fork's lockfile like every other + formatter here. The last table compares tsv with itself: what the npx path and + the WASM fallback each add over the bare binary. +

+ +
@@ -151,40 +659,66 @@

- All numbers are single-threaded: every library formats or parses one file at a time, measured - sequentially with no cross-file parallelism. These are per-file, single-core latency and - throughput numbers - not the multi-core batch throughput a CLI gets when it formats many files - at once, which most of these tools (tsv included) can do. + Every section except the CLI benchmark is single-threaded and in-process: each library formats + or parses one file at a time, measured sequentially with no cross-file parallelism (oxfmt's + programmatic format is an async N-API call whose native work may run off the JS + thread, but each call is awaited before the next, so it is still one thread of compute per + file). Each row is the total time to process the whole corpus once — not the multi-core batch + throughput a CLI gets when it formats many files at once, which most of these tools (tsv + included) can do. +

+

+ Within a group every tool is timed on the same file set — the intersection of what all of them + accepted, so a file one tool rejects drops out of the group for everyone (the count above each + chart is that intersection, a few files short of the corpus total where a tool rejects one). + These are warm numbers: tsv's staging buffers grow once and are reused across sweeps, so a + cold one-shot call pays more, and each native call also pays a UTF-8 encode and decode across + the boundary that the JS tools skip. The tsv-wasm rows run the full parse+format + build, not the smaller format-only or parse-only packages. +

+

+ What's measured: {corpus_file_count.toLocaleString('en-US')} files of .svelte, + .ts/.js, and .css — real-world code only, from two + sources: the author's libraries, apps, and sites (the fuz.dev ecosystem plus personal + SvelteKit sites), and upstream framework source (Svelte, SvelteKit, and the svelte.dev site). + The CSS set also includes real-authored CSS extracted from those components' + <style> blocks, concatenated per repo — standalone CSS files are rare in + this ecosystem, and the same bytes appear in the Svelte rows (rows are never summed). Test + files count as real code and stay in; fixture files (formatter test suites, and fixture + subtrees inside the measured repos) are excluded — deliberately tricky edge cases measure + conformance, not typical throughput, and are covered by the parse-conformance section above.

- What's measured: around 5,500 files (~15 MB) of - .svelte/.html, .ts/.js, and - .css, from three sources: the fuz.dev libraries and apps, upstream framework - source (Svelte, SvelteKit, and the svelte.dev site), and formatter conformance fixtures - (Prettier's and prettier-plugin-svelte's own test suites - deliberately tricky edge cases, not - typical code, so they skew the corpus toward hard cases). + Two caveats on that corpus. It is dominated by the author's own code plus Svelte's, the same + code tsv is developed and tested against and mostly tsv-formatted already, so every ratio here + is "on this corpus", not a universal figure; the CLI section's Svelte corpus shares only its + kit and svelte.dev trees with this one, and its five third-party component libraries are + deliberately kept out of this view. And CSS is the weakest sample: a few dozen standalone + files plus the per-repo <style> concatenations, which keep the one level of + indent they carried inside their tags — so every tool re-indents them (identically), and the + dominant CSS input measures a full re-indent rather than the already-formatted steady state. + CSS ratios are the noisiest on the page for it.

-
- + + +

+ The same benchmark harness runs under three JS runtimes — Node, Deno, and Bun. The headline + numbers above are the Node run. The native entry differs by runtime: Node and Bun load tsv's + N-API addon, while Deno loads its C-FFI library. They share code but cross a different binding + boundary, so a per-runtime delta on the same row is a runtime effect — the JS engine or the + binding boundary — not a difference in tsv's own engine, which is identical across all three. +

+ + +
+ diff --git a/src/routes/docs/benchmarks/BenchmarksBar.svelte b/src/routes/docs/benchmarks/BenchmarksBar.svelte index 128c7b5..beae5e7 100644 --- a/src/routes/docs/benchmarks/BenchmarksBar.svelte +++ b/src/routes/docs/benchmarks/BenchmarksBar.svelte @@ -3,7 +3,7 @@ category_color, format_label, type FormattedUnit, - type ImplementationCategory, + type ImplementationCategory } from './benchmark_data.ts'; const { @@ -14,6 +14,10 @@ ratio_text, ratio_color, annotation, + disabled = false, + coverage_only = false, + on_enter, + on_leave }: { label: string; bar_fraction: number; @@ -24,48 +28,102 @@ // optional extra context shown between value and ratio (corpus coverage in // benchmark groups, gzipped size in binary-size groups); omitted when absent annotation?: string | undefined; + // a grayed-out, inert placeholder (a tool that doesn't run in this group) — + // no bar, no value, no ratio, just the label held in its shared slot + disabled?: boolean; + // inert for a different reason: the tool DID run over the whole corpus but + // was deliberately never timed (no in-process API, so a per-file row would + // have measured process spawn). Reads `not timed` rather than `n/a`, since + // "we chose not to measure this" and "this tool wasn't here" are different + // claims and the gray alone can't tell them apart + coverage_only?: boolean; + // hover-to-rebaseline: fired when the pointer enters/leaves the row so the + // group can adopt this row as its ratio anchor; omitted on inert placeholders + on_enter?: (() => void) | undefined; + on_leave?: (() => void) | undefined; } = $props(); + + // the parenthesized binding suffix (`(wasm)`/`(node napi)`) describes how the + // tool actually ran in this (Node) report - meaningless for the disabled + // placeholders of single-variant tools (biome and dprint each ship only a wasm + // build here, so there's no ambiguity to lose). oxc-parser keeps its suffix even + // when disabled since it has two placeholder rows (napi and wasm) that would + // otherwise become indistinguishable. + const display_label = $derived( + disabled && (category === 'biome' || category === 'dprint') + ? format_label(label).replace(/ \([^)]*\)$/, '') + : format_label(label) + ); -
- {format_label(label)} + + +
+ {display_label}
-
+ {#if !disabled} +
+ {/if}
- {value.value} {value.unit} + + {#if disabled} + {coverage_only ? 'not timed' : 'n/a'} + {:else} + {value.value} {value.unit} + {/if} + {#if annotation != null} {annotation} {/if} - {ratio_text} + + {#if !disabled}{ratio_text}{/if} +
diff --git a/src/routes/docs/benchmarks/BenchmarksConformance.svelte b/src/routes/docs/benchmarks/BenchmarksConformance.svelte new file mode 100644 index 0000000..bf96e1f --- /dev/null +++ b/src/routes/docs/benchmarks/BenchmarksConformance.svelte @@ -0,0 +1,98 @@ + + + +{#each groups as group (group.language)} +
+

+ Parsing {group.files_total.toLocaleString('en-US')} + {LANGUAGE_LABELS[group.language] ?? group.language} files +

+ + + + + + + + + + {#each group.rows as row (row.name)} + + + + + + {/each} + +
files acceptedcoverage
+ {row.name}{#if row.note} + ({row.note}) + {/if} + + {row.files_processed.toLocaleString('en-US')} / + {row.files_total.toLocaleString('en-US')} + {format_coverage_percent(row.coverage_fraction)}
+
+{/each} + + diff --git a/src/routes/docs/benchmarks/BenchmarksCrossRuntime.svelte b/src/routes/docs/benchmarks/BenchmarksCrossRuntime.svelte new file mode 100644 index 0000000..4518391 --- /dev/null +++ b/src/routes/docs/benchmarks/BenchmarksCrossRuntime.svelte @@ -0,0 +1,224 @@ + + +
+ {#if report.mixed_vintage} + + {/if} + {#if report.mixed_machine} + + {/if} + {#if unavailable.length} + + {/if} + {#if unstable.length} + + {/if} + {#if runtime_versions.length} +
    + {#each runtime_versions as { runtime, version } (runtime)} +
  • {runtime} {version}
  • + {/each} +
+ {/if} + {#each groups as group (group.group)} +
+

{group_label(group.operation, group.language)}

+ + + + + {#each runtimes as runtime (runtime)} + + {/each} + {#each others as runtime (runtime)} + + {/each} + + + + {#each group.rows as row (row.name)} + + + {#each runtimes as runtime (runtime)} + {@const ops = row.ops_per_second[runtime]} + {@const cell_unstable = is_unstable(group.group + '/' + row.name, runtime)} + + {/each} + {#each others as runtime (runtime)} + {@const ratio = row.ratio_vs_base[runtime]} + + {/each} + + {/each} + +
{runtime}{runtime}/{base}
+ + {format_cross_runtime_label(row.name)} + {#if row.files_iterated_mismatch} + + ⚠ files {files_mismatch_label(row.files_iterated_mismatch)} + + {/if} + + {format_ops(ops)}{cell_unstable ? ' ⚠' : ''} + + {ratio != null ? format_speedup(ratio) : 'fail'} +
+
+ {/each} +

+ sweeps/sec — one sweep is a full pass over the group's timed file set (higher is faster); ratios + are vs {base} (> 1 = faster than {base}). A fail is a row that + runtime contributed no number for — an implementation it can't load (listed above when the + report records it), or one its report doesn't carry. The native rows load each + runtime's idiomatic binding of the same engine — the N-API addon under node and + bun, the C-FFI library under deno — so the deno column is + a first-class FFI-vs-N-API comparison, not a re-run of the same binding. +

+
+ + diff --git a/src/routes/docs/benchmarks/BenchmarksGroup.svelte b/src/routes/docs/benchmarks/BenchmarksGroup.svelte index d3e2cb8..c26fd26 100644 --- a/src/routes/docs/benchmarks/BenchmarksGroup.svelte +++ b/src/routes/docs/benchmarks/BenchmarksGroup.svelte @@ -1,30 +1,43 @@ @@ -33,27 +46,9 @@ {group.operation === 'format' ? 'Formatting' : 'Parsing'} {count_label} - {group.language} files - {#if has_coverage}files handled / total  ·  speed{/if} + {group.language} files + + total time  ·  speed

-
- {#each group.entries as entry (entry.name)} - - {/each} -
+
diff --git a/src/routes/docs/benchmarks/BenchmarksMeta.svelte b/src/routes/docs/benchmarks/BenchmarksMeta.svelte index 596ee77..a61e338 100644 --- a/src/routes/docs/benchmarks/BenchmarksMeta.svelte +++ b/src/routes/docs/benchmarks/BenchmarksMeta.svelte @@ -1,22 +1,55 @@ -{#if wasm_display.length > 0} -
-

wasm

-
- {#each wasm_display as s (s.label)} - - {/each} -
+{#each size_groups as group (group.capability)} +
+

{group.heading}

+
-{/if} - -{#if native_display.length > 0} -
-

native

-
- {#each native_display as s (s.label)} - - {/each} -
-
-{/if} - - +{/each} diff --git a/src/routes/docs/benchmarks/BenchmarksSummary.svelte b/src/routes/docs/benchmarks/BenchmarksSummary.svelte index af3b40e..3504419 100644 --- a/src/routes/docs/benchmarks/BenchmarksSummary.svelte +++ b/src/routes/docs/benchmarks/BenchmarksSummary.svelte @@ -1,40 +1,26 @@

- tsv is heavily inspired by and borrows architectural patterns from Prettier. We're very grateful for the hard work of its - contributors. tsv offers a - speedup over Prettier: + tsv's formatter is inspired by and borrows architectural patterns from + Prettier, and we're grateful for the hard work of its + contributors. + Single-threaded on the benchmarked corpus, tsv is this much faster than Prettier:

- - - + + + @@ -56,11 +42,6 @@ } thead th { padding: var(--space_xs) var(--space_sm); - font-size: var(--font_size_sm); - } - thead th small { - font-weight: 400; - opacity: 0.7; } tbody td { padding: var(--space_xs) var(--space_sm); @@ -72,6 +53,5 @@ .speedup { font-size: var(--font_size_xl); font-weight: 700; - font-variant-numeric: tabular-nums; } diff --git a/src/routes/docs/benchmarks/benchmark_data.ts b/src/routes/docs/benchmarks/benchmark_data.ts index 9bf0958..a013b8a 100644 --- a/src/routes/docs/benchmarks/benchmark_data.ts +++ b/src/routes/docs/benchmarks/benchmark_data.ts @@ -1,7 +1,12 @@ -// Raw types matching the tsv bench.ts `Baseline` format +// Raw types matching the tsv bench's per-runtime report format +// (`benches/js/results/report..json` — the site's flagship detailed +// view is the Node report, the N-API native path) export interface BenchmarkBaseline { version: number; + // The runtime that produced this report (`node` for the flagship view). + // Present from report `version` 5 on. + runtime?: string; timestamp: string; git_commit: string; corpus: Record; @@ -11,23 +16,192 @@ export interface BenchmarkBaseline { // Counts of silenced third-party stderr noise, keyed by message pattern. // Present from baseline `version` 4 on; not rendered, kept for parity. suppressed_noise?: Record; + // Same-engine native/wasm pairs whose pre-flight accept sets or output bytes + // disagreed — `[]` when healthy. A non-empty list is a binding-boundary bug in + // the producing bench (see tsv's `check_variant_parity`), caught at review time + // in the copied report's diff; not rendered, kept for parity. Absent on + // older reports — treat as optional. + variant_parity?: Array; + // Which corpus/surface produced the report: `perf` (real-world corpus, + // format + parse) or `conformance` (the deliberately-hard fixture suites, + // disjoint from the perf corpus, parse only). Present from `version` 6 on. + corpus_kind?: 'perf' | 'conformance'; + // Per-entry corpus composition (path + loaded file count) — discloses which + // sources were present on the machine that produced the report. Present + // from `version` 6 on. + corpus_sources?: Array; + // The real-code snapshot every `real`/`framework` source was read from — the + // `fuzdev/corpora` checkout at its commit (`subpath` empty), one roll-up commit + // for the whole real-code corpus. Present from `version` 14 on; absent on + // conformance-only reports (no real code). + corpus_snapshot?: CorpusRepoRef; + // The machine that produced the report — CPU model, OS/arch, runtime version. + // The throughput numbers are machine-relative, so this is the environment the + // meta panel discloses. Present from `version` 7 on (absent on older reports). + machine?: Machine; + // Per-corpus-source coverage — `group → source → impl → {processed, total}`, + // the machine-readable half of the per-source tables in tsv's own markdown + // report. Conformance reports only (the perf surface is 100% by construction). + // Present from `version` 8 on; not rendered — this page shows each group's + // aggregate, which blends corpora answering different questions, so these rows + // are the sharper view if it ever grows one. + coverage_by_source?: Record>>; + // Artifacts the size table reached for and didn't find. That table's + // COMPOSITION varies by the producing machine — a row exists only for a built + // artifact — so this is what tells a missing row apart from an artifact that + // stopped being produced. Present from `version` 11 on; not rendered, kept for + // parity. + binary_sizes_absent?: Array; + // Implementations that failed to initialize on the producing machine, as + // `{impl, reason, rows}`. An impl that doesn't load contributes NO row, so + // without this a tool that broke upstream is indistinguishable from one that was + // never measured (the Node report should be `[]`; Bun's carries its known + // `biome-wasm` load failure). Present from `version` 10 on, `rows` from `version` 12; not + // rendered here, kept for parity. + unavailable?: Array; + // Files a byte-graded row ACCEPTED whose output the producing bench's + // byte-parity check could not digest, as `{"/": count}` — `{}` when + // every accepted output was gradeable, which is the healthy state. The one known + // cause is a pathologically deep AST overflowing V8's recursive `JSON.stringify`. + // Unlike every other field here it records a measurement the run could NOT make, + // so a growing count means that check is quietly covering less. Present from + // `version` 13 on; not rendered, kept for parity. + output_digest_ungraded?: Record; } +// Per-impl coverage for one corpus source (see `coverage_by_source`). +export interface SourceCoverageCell { + processed: number; + total: number; +} + +// One implementation that failed to load on the machine that produced a report +// (see `BenchmarkBaseline.unavailable`). Mirrors the bench's `UnavailableImpl`. +// +// `impl` is the bench's init-line LABEL (`Biome`, `OXC WASM`) and matches no row +// name — `rows` is the joinable identity, and the only one to look a row up by. +export interface UnavailableImpl { + impl: string; + reason: string; + // The row names this failure removed from the tables. Absent on `version` 11 + // and older reports. + rows?: Array; +} + +// A same-engine pair that disagreed — one engine behind two bindings +// (native/wasm), or one binding under two options. Mirrors the bench's +// `VariantParityFinding` (see `BenchmarkBaseline.variant_parity`); the +// neutral keys arrived with report `version` 9, which is also when the check +// started pairing options (older reports spell them `native`/`wasm`). +export interface VariantParityFinding { + group: string; + impl: string; + sibling: string; + // files only `impl` accepted + impl_only: number; + // files only `sibling` accepted + sibling_only: number; + // files BOTH accepted whose outputs differ byte-for-byte, and up to three of + // their paths. Only tsv's own native/wasm pair is graded on bytes, and a + // non-zero count fails the producing bench outright, so these are absent + // here in practice — kept for parity with the report shape. Absent on + // reports older than the byte check; treat as optional. + output_mismatch?: number; + output_mismatch_examples?: Array; +} + +// The hardware/runtime a report was measured on. Excludes hostname (the reports +// are published) and volatile fields (free memory, load) that would churn. +export interface Machine { + cpu_model: string; + os: string; + arch: string; + // The producing runtime's own version (`node`/`deno`/`bun` version string). + runtime_version: string; +} + +export interface CorpusSource { + path: string; + files: number; + // Per-language split of `files` (svelte/typescript/css counts summing to + // `files`). Present on reports whose loader emitted it; older reports carry + // only the `files` total, so treat it as optional. + by_language?: Partial>; + // The source's GitHub origin, detected by the bench at report-build time (URL + + // commit + subpath): for a `fuzdev/corpora` collection the UPSTREAM its manifest + // names, read from that manifest; for any other checkout, git. Absent on older reports and on sources with no + // GitHub remote — presence keys on the field, not the report `version` (it + // arrived without one and is not tied to any), so treat it as optional. + repo?: CorpusRepoRef; +} + +// A corpus source's GitHub origin — see `CorpusSource.repo` and `corpus_source_url`. +export interface CorpusRepoRef { + // Canonical https GitHub URL, e.g. `https://github.com/sveltejs/svelte`. + url: string; + // `owner/name` (e.g. `sveltejs/svelte`) — a compact label. + slug: string; + // The commit the corpus was loaded at (full SHA); `''` for a harvested cache + // linked at its canonical upstream root (no pin). + commit: string; + // Path within the repo to this source (`''` = repo root). + subpath: string; +} + +/** + * The GitHub URL for a repo ref, pinned to its commit + subpath + * (`…/tree//`), or the repo root when the ref carries no commit + * (a canonical-upstream cache). + */ +export const corpus_repo_ref_url = (repo: CorpusRepoRef): string => { + if (!repo.commit) return repo.url; + return repo.subpath + ? `${repo.url}/tree/${repo.commit}/${repo.subpath}` + : `${repo.url}/tree/${repo.commit}`; +}; + +/** + * The GitHub URL for a corpus source, pinned to the measured commit + subpath when + * detected. `undefined` when the source has no detected origin (the local + * `svelte_styles` cache). + */ +export const corpus_source_url = (source: CorpusSource): string | undefined => + source.repo ? corpus_repo_ref_url(source.repo) : undefined; + export interface BaselineEntry { name: string; group: string; - mean_ns: number; - p50_ns: number; - p75_ns: number; - p90_ns: number; - p95_ns: number; - p99_ns: number; - min_ns: number; - max_ns: number; - std_dev_ns: number; - cv: number; - ops_per_second: number; - sample_size: number; + // Timing stats are `null` on a coverage-only report (the conformance surface + // the site refreshes from — parse coverage measured, timed phase skipped). A + // perf report always carries real numbers, and only the perf path + // (`derive_benchmark_groups`) reads these, so the nulls are unreachable there + // but must be expressed for the shared cast to stay sound. + mean_ns: number | null; + p50_ns: number | null; + p75_ns: number | null; + p90_ns: number | null; + p95_ns: number | null; + p99_ns: number | null; + min_ns: number | null; + max_ns: number | null; + std_dev_ns: number | null; + cv: number | null; + ops_per_second: number | null; + sample_size: number | null; + // Stability read from the RAW timings (report `version` 15 on): the cv before + // outlier removal and the second-half-over-first-half `drift`, which see a cost + // that moved WHILE the row was measured — the cleaned `cv` above cannot, since + // the bench's outlier cleaner deletes or blends a second mode rather than + // reporting it. With them: the timing count before cleaning, the share removed, + // the protocol the row ran under, and a hash of the timed path set. Absent on + // older reports. + cv_raw?: number | null; + drift?: number | null; + raw_sample_size?: number | null; + outlier_ratio?: number | null; + warmup_iterations?: number | null; + min_iterations?: number | null; + files_iterated_digest?: string | null; // Per-implementation preflight coverage: files this impl processed / the // language's total discovered files. Present from baseline `version` 3 on; // absent (or `null`) in older baselines. @@ -36,6 +210,9 @@ export interface BaselineEntry { // Files this impl was actually timed on (the per-group intersection in // default mode). Present from baseline `version` 4 on. files_iterated?: number | null; + // Present from report `version` 5 on (matches the report's top-level); + // not rendered, kept for parity. + runtime?: string; } export interface BaselineVersions { @@ -46,8 +223,39 @@ export interface BaselineVersions { prettier: string; prettier_svelte: string; oxc_parser?: string; + // `@oxc-parser/binding-wasm32-wasi` — the binding behind the `oxc-parser-wasm` + // row, pinned apart from `oxc-parser` (its newer versions fail to load), so the + // two can differ. Absent on reports produced before the split. + oxc_parser_wasm?: string; oxfmt?: string; + // `yuku-parser` (N-API) and `@yuku-parser/wasm` — one Zig engine behind two + // bindings, versioned in lockstep upstream. Absent on reports produced before + // the yuku rows. + yuku_parser?: string; + yuku_parser_wasm?: string; biome?: string; + // `@dprint/typescript` — the plugin version (the host `@dprint/formatter` is + // just the Wasm loader). Absent on reports produced before the dprint row. + dprint?: string; + // `@rsvelte/fmt` — the coverage-only Svelte formatter row. Absent on reports + // produced before the rsvelte-fmt row. + rsvelte_fmt?: string; + // `dprint-plugin-malva` — dprint's CSS formatter plugin, over the same Wasm + // host as `dprint` above. Absent on reports produced before the malva row. + malva?: string; + // `postcss` — the CSS parser row. Absent on reports produced before it. + postcss?: string; + // `@rsvelte/vite-plugin-svelte-native` — the N-API addon behind the Svelte + // PARSE rows, a different package from `@rsvelte/fmt` above and versioned + // independently. Absent on reports produced before those rows. + rsvelte_parse?: string; + // The upstream Svelte version that addon targets (its own `VERSION` export), + // which is not its package version — a drift from the `svelte` pin means those + // rows parse to a different Svelte than the svelte/compiler row beside them. + rsvelte_parse_svelte_target?: string; + // `@swc/core` — the TypeScript/JS parser row. Absent on reports produced + // before it. + swc?: string; } export interface BinarySize { @@ -68,7 +276,12 @@ export type ImplementationCategory = | 'tsv_wasm' | 'tsv_wasm_json' | 'biome' - | 'oxc'; + | 'dprint' + | 'oxc' + | 'postcss' + | 'rsvelte' + | 'swc' + | 'yuku'; export interface BenchmarkGroup { operation: string; @@ -84,10 +297,23 @@ export interface BenchmarkDisplayEntry { name: string; mean_ns: number; bar_fraction: number; - speedup_vs_canonical: number | undefined; category: ImplementationCategory; files_processed: number | null; files_total: number | null; + // A placeholder entry mirrored from another language's group for a tool that + // doesn't run in this one (e.g. `oxc-parser` under svelte/css parse) — rendered + // grayed-out and inert so the parse groups share one entry order. Absent on real, + // measured entries. Also set on a `coverage_only` row, which shares the inert + // rendering for a different reason (see below). + disabled?: boolean; + // The harness measured this tool's COVERAGE but deliberately never timed it — + // it ships no in-process API, so a per-file row would have measured process + // spawn rather than format work (`rsvelte-fmt`; see the tsv harness's + // §Coverage-only rows). Distinct from a plain `disabled` placeholder: that one + // never ran here at all, while this one ran over the whole corpus and has real + // `files_processed`/`files_total` to show. Both render inert; only this one + // carries a coverage annotation and needs the page to explain itself. + coverage_only?: boolean; } export interface SpeedupRow { @@ -105,43 +331,115 @@ const CATEGORY_BY_NAME: Record = { 'acorn-typescript': 'canonical', tsv: 'tsv_native', 'tsv-json': 'tsv_native_json', + 'tsv-json-no-locations': 'tsv_native_json', 'tsv-internal': 'tsv_native', - tsv_wasm: 'tsv_wasm', - 'tsv_wasm-json': 'tsv_wasm_json', - 'tsv_wasm-internal': 'tsv_wasm', + 'tsv-wasm': 'tsv_wasm', + 'tsv-wasm-json': 'tsv_wasm_json', + 'tsv-wasm-json-no-locations': 'tsv_wasm_json', + 'tsv-wasm-internal': 'tsv_wasm', 'biome-wasm': 'biome', + 'dprint-wasm': 'dprint', + // malva is dprint's own CSS plugin, loaded through the same Wasm host, so it + // shares dprint's category rather than claiming a hue of its own — the palette + // has ten and all ten are spoken for. + 'malva-wasm': 'dprint', 'oxc-parser': 'oxc', 'oxc-parser-wasm': 'oxc', oxfmt: 'oxc', + postcss: 'postcss', + 'rsvelte-fmt': 'rsvelte', + // The parse rows come from a different package than `rsvelte-fmt`, but the same + // tool, so one category covers all three. + 'rsvelte-parse': 'rsvelte', + 'rsvelte-parse-skip-expr-loc': 'rsvelte', + swc: 'swc', + 'yuku-parser': 'yuku', + 'yuku-parser-wasm': 'yuku' }; -const categorize_name = (name: string): ImplementationCategory => CATEGORY_BY_NAME[name] ?? 'oxc'; +export const categorize_name = (name: string): ImplementationCategory => + CATEGORY_BY_NAME[name] ?? 'oxc'; export const categorize_size = (label: string): ImplementationCategory => { - // covers `tsv_wasm` plus the `tsv_format_wasm`/`tsv_parse_wasm` subsets + // covers `tsv-wasm` plus the `tsv-format-wasm`/`tsv-parse-wasm` subsets if (label.startsWith('tsv') && label.includes('wasm')) return 'tsv_wasm'; if (label.startsWith('tsv')) return 'tsv_native'; if (label.startsWith('biome')) return 'biome'; - if (label.includes('oxc') || label.includes('oxfmt')) return 'oxc'; - return 'oxc'; -}; - -// Canonical entry names per group -const CANONICAL_BY_GROUP: Record = { - 'parse/svelte': 'svelte/compiler', - 'parse/typescript': 'acorn-typescript', - 'parse/css': 'svelte/compiler', - 'format/svelte': 'prettier', - 'format/typescript': 'prettier', - 'format/css': 'prettier', + if (label.startsWith('dprint')) return 'dprint'; + if (label.startsWith('malva')) return 'dprint'; // dprint's CSS plugin — see CATEGORY_BY_NAME + // Covers both `rsvelte-fmt (binary)` and `rsvelte compiler (napi)`. + if (label.startsWith('rsvelte')) return 'rsvelte'; + if (label.startsWith('swc')) return 'swc'; + if (label.startsWith('yuku')) return 'yuku'; + return 'oxc'; // oxc-parser / oxfmt, and any unrecognized label }; // Primary tsv entry names for speedup summary (fair comparisons) const PRIMARY_NATIVE_FORMAT = 'tsv'; -const PRIMARY_WASM_FORMAT = 'tsv_wasm'; +const PRIMARY_WASM_FORMAT = 'tsv-wasm'; // Derivation functions +// Shared display order for benchmark groups: format before parse, then by +// language — used by the detailed, conformance, and cross-runtime views alike. +const OPERATION_ORDER: Record = { format: 0, parse: 1 }; +const LANGUAGE_ORDER: Record = { + svelte: 0, + typescript: 1, + css: 2 +}; + +/** + * Fixed slot for a format/parse row, applied in place of a size-ordered sort so the + * rows read in a stable, meaningful sequence across every group: the canonical + * reference first (the default 1.0x anchor), then the cross-tool comparisons + * (alphabetically: biome, dprint — whose category malva shares — oxc, postcss, + * rsvelte, swc, yuku), then tsv's JSON-materializing + * wires (the span-only `no-locations` wire before the default `loc`-carrying one), + * then tsv's own engine rows — `tsv`/`tsv-wasm` in the format groups, the + * `-internal` rows in the parse groups. + */ +const speed_entry_rank = (entry: BenchmarkDisplayEntry): number => { + if (entry.category === 'canonical') return 0; + if (entry.category === 'biome') return 1; + if (entry.category === 'dprint') return 2; + if (entry.category === 'oxc') return 3; + if (entry.category === 'postcss') return 4; + if (entry.category === 'rsvelte') return 5; + if (entry.category === 'swc') return 6; + if (entry.category === 'yuku') return 7; + if (entry.name.endsWith('-no-locations')) return 8; // tsv json, span-only wire + if (entry.name.endsWith('-json')) return 9; // tsv json, loc-carrying wire + return 10; // tsv's engine rows: `tsv`/`tsv-wasm` (format), `-internal` (parse, no JS materialization) +}; + +/** + * Orders format/parse rows by their fixed `speed_entry_rank` slot, then wasm before + * native within a tier (the browser-relevant build leads each pairing), then by name + * — shared by the initial sort and the re-sort after disabled placeholders are mixed + * in, so every group scans identically. + */ +const compare_speed_entries = (a: BenchmarkDisplayEntry, b: BenchmarkDisplayEntry): number => { + const rank = speed_entry_rank(a) - speed_entry_rank(b); + if (rank !== 0) return rank; + // wasm before native within a tier + const kind = (a.name.includes('wasm') ? 0 : 1) - (b.name.includes('wasm') ? 0 : 1); + if (kind !== 0) return kind; + return a.name.localeCompare(b.name); +}; + +/** + * A grayed-out, inert copy of a measured entry (or a bare template) for a group the + * tool doesn't run in — no bar, no coverage, never an anchor. + */ +const to_placeholder = (entry: BenchmarkDisplayEntry): BenchmarkDisplayEntry => ({ + ...entry, + bar_fraction: 0, + files_processed: null, + files_total: null, + disabled: true +}); + export const derive_benchmark_groups = (baseline: BenchmarkBaseline): Array => { const grouped: Map> = new Map(); for (const entry of baseline.entries) { @@ -159,30 +457,40 @@ export const derive_benchmark_groups = (baseline: BenchmarkBaseline): Array e.name === canonical_name); - const slowest = Math.max(...entries.map((e) => e.mean_ns)); - - const display_entries: Array = entries.map((e) => ({ - name: e.name, - mean_ns: e.mean_ns, - bar_fraction: slowest > 0 ? e.mean_ns / slowest : 0, - speedup_vs_canonical: - canonical_entry_raw && e.name !== canonical_name - ? canonical_entry_raw.mean_ns / e.mean_ns - : undefined, - category: categorize_name(e.name), - files_processed: e.files_processed ?? null, - files_total: e.files_total ?? null, - })); + // The sort below leads each group with its canonical reference (Prettier for + // format, the JS baseline for parse), so the first row is the default 1.0x + // anchor; the shared component reads that default off the first row and + // recomputes every ratio, re-baselining onto whichever row is hovered. (Size + // groups lead with their smallest build; see `derive_size_groups`.) + // `?? 0` coerces a null timing so the display entry's `mean_ns` stays a + // number. On a perf report the nulls are exactly the COVERAGE-ONLY rows — + // a tool measured for what it accepts but never timed (see + // `BenchmarkDisplayEntry.coverage_only`) — and a 0 there is inert: those + // rows render without a bar, value, or ratio, so the coerced number is + // never displayed or divided by. It does keep them out of `slowest`, which + // is what we want — a row with no timing must not set the bar scale. + const slowest = Math.max(...entries.map((e) => e.mean_ns ?? 0)); - // Sort: canonical first, then by mean_ns descending (slowest first for visual) - display_entries.sort((a, b) => { - if (a.category === 'canonical' && b.category !== 'canonical') return -1; - if (b.category === 'canonical' && a.category !== 'canonical') return 1; - return b.mean_ns - a.mean_ns; + const display_entries: Array = entries.map((e) => { + // `mean_ns` is the timing the bars and ratios are built from, so its + // absence — not the tool's identity — is what marks a row untimed. That + // keeps this independent of which tools happen to be coverage-only. + const untimed = e.mean_ns == null; + return { + name: e.name, + mean_ns: e.mean_ns ?? 0, + bar_fraction: untimed || slowest <= 0 ? 0 : (e.mean_ns ?? 0) / slowest, + category: categorize_name(e.name), + files_processed: e.files_processed ?? null, + files_total: e.files_total ?? null, + ...(untimed ? { disabled: true, coverage_only: true } : null) + }; }); + // Fixed order (see `compare_speed_entries`): canonical leads as the default 1.0x + // anchor, then the cross-tool rows, tsv's json wires, then tsv's internal engine + display_entries.sort(compare_speed_entries); + const iterated_counts = entries .map((e) => e.files_iterated) .filter((v): v is number => v != null); @@ -191,27 +499,206 @@ export const derive_benchmark_groups = (baseline: BenchmarkBaseline): Array e.category === 'canonical'), - files_iterated: iterated_counts.length > 0 ? Math.max(...iterated_counts) : null, + files_iterated: iterated_counts.length > 0 ? Math.max(...iterated_counts) : null }); } - // Sort groups: format before parse, then by language - const LANG_ORDER: Record = {svelte: 0, typescript: 1, css: 2}; - const OP_ORDER: Record = {format: 0, parse: 1}; result.sort( (a, b) => - (OP_ORDER[a.operation] ?? 9) - (OP_ORDER[b.operation] ?? 9) || - (LANG_ORDER[a.language] ?? 9) - (LANG_ORDER[b.language] ?? 9), + (OPERATION_ORDER[a.operation] ?? 9) - (OPERATION_ORDER[b.operation] ?? 9) || + (LANGUAGE_ORDER[a.language] ?? 9) - (LANGUAGE_ORDER[b.language] ?? 9) ); + // Neither `biome` nor (for svelte/css) `oxc-parser` has a real entry in every + // parse group. `biome`'s `@biomejs/js-api` never exposes a parser to JS at all + // (only formatting and linting), so no parse group has a real biome entry; + // `oxc-parser` only parses TypeScript/JS, so the svelte and css parse groups + // lack it. Mirror both in as disabled placeholders — biome always, oxc-parser + // only where it's missing — then re-sort so they fall into their fixed slots + // (biome then oxc, right after the canonical row), giving all three parse groups + // one shared entry order. + // + // `yuku-parser` is deliberately NOT mirrored, though it is TypeScript/JS-only too. + // A grayed-out slot states a SCOPE gap: biome and oxc are broad web toolchains, so + // a missing Svelte or CSS parser is worth showing. yuku claims nothing wider, so + // an empty slot would invent a shortfall against a promise it never made. + const ts_parse = result.find((g) => g.operation === 'parse' && g.language === 'typescript'); + const oxc_templates = ts_parse?.entries.filter((e) => e.category === 'oxc') ?? []; + for (const group of result) { + if (group.operation !== 'parse') continue; + // guarded like the others, so a report that grows a real biome parse row + // can't produce a second `biome-wasm` entry (the rows are keyed by name) + const biome_placeholders: Array = group.entries.some( + (e) => e.category === 'biome' + ) + ? [] + : [ + to_placeholder({ + name: 'biome-wasm', + mean_ns: 0, + bar_fraction: 0, + category: 'biome', + files_processed: null, + files_total: null + }) + ]; + const needs_oxc = + group.language !== 'typescript' && !group.entries.some((e) => e.category === 'oxc'); + const oxc_placeholders = needs_oxc ? oxc_templates.map(to_placeholder) : []; + group.entries.push(...biome_placeholders, ...oxc_placeholders); + group.entries.sort(compare_speed_entries); + } + + // The format-side analogue: `@dprint/typescript` formats TypeScript/JS only and + // rejects Svelte outright, so the svelte FORMAT group has no real dprint entry. + // Mirror it in as a disabled placeholder so the format groups share one entry + // order, exactly as oxc-parser is mirrored into the svelte/css parse groups + // above. css is NOT filled: the bench runs dprint's own CSS plugin, malva, + // through the same Wasm host, and that row shares dprint's category + // (`CATEGORY_BY_NAME`), so the "no dprint-category entry" guard below already + // leaves it alone. Guarded on the template existing, so a report predating the + // dprint row renders unchanged. + const ts_format = result.find((g) => g.operation === 'format' && g.language === 'typescript'); + const dprint_templates = ts_format?.entries.filter((e) => e.category === 'dprint') ?? []; + if (dprint_templates.length > 0) { + for (const group of result) { + if (group.operation !== 'format') continue; + if (group.entries.some((e) => e.category === 'dprint')) continue; + group.entries.push(...dprint_templates.map(to_placeholder)); + group.entries.sort(compare_speed_entries); + } + } + return result; }; +// Parse-conformance coverage (the conformance report's headline metric) + +export interface ConformanceRow { + name: string; + // dimmed qualifier rendered beside the name, for the one case where WHICH + // binding produced the row is worth saying — see `CONFORMANCE_ROW_NOTES` + note?: string; + files_processed: number; + files_total: number; + // files_processed / files_total, rendered as the coverage percentage + coverage_fraction: number; +} + +export interface ConformanceGroup { + language: string; + // the language's total discovered files (every row shares it) + files_total: number; + rows: Array; +} + +/** + * One coverage row per ENGINE, not per binding: the conformance headline is + * "which files does this parser accept," which is identical across a tool's + * native/wasm/internal variants — so the `-wasm` and `-internal` duplicates + * are dropped and `tsv-json` stands in for tsv (relabeled plainly, since the + * JSON-materialization qualifier is a speed concern, not a coverage one). + * + * Which binding stands in is therefore arbitrary — except for yuku, where the + * conformance report carries the wasm row alone: its native binding crashes the + * host process on this corpus's escaped-identifier tests, so tsv's harness omits + * that row there. Keying on `yuku-parser` would silently drop the engine. + */ +const CONFORMANCE_ENGINE_NAMES: Record = { + 'svelte/compiler': 'svelte/compiler', + 'acorn-typescript': 'acorn-typescript', + 'tsv-json': 'tsv', + 'oxc-parser': 'oxc-parser', + 'yuku-parser-wasm': 'yuku-parser', + // rsvelte's two parse rows are one engine under two options, so only the + // default-wire one stands in — the option changes the payload, never the + // verdict. + 'rsvelte-parse': 'rsvelte', + swc: 'swc', + // The TypeScript compiler's own parser, which appears on this surface alone — + // a verdict rather than a speed, so it carries no throughput row. Its reading + // changes by corpus source, which the note below spells out. + tsc: 'tsc', + postcss: 'PostCSS' +}; + +/** + * Qualifiers rendered beside a coverage row, keyed by report entry name. The + * table is per engine and says nothing about bindings — so a note here is for + * when the row's reading needs a caveat the coverage number can't carry, whether + * that's the binding or the grammar being measured. + */ +const CONFORMANCE_ROW_NOTES: Record = { + 'yuku-parser-wasm': 'wasm — native segfaults', + // tsc selected part of this corpus, where it therefore scores 100% by + // construction — the same shape as svelte/compiler on the Svelte set, but + // invisible here because it holds for only a SLICE of the TypeScript group + // rather than the whole of it. Without the note the blended number reads as + // one achieved result, and its shortfall reads as a defect in the corpus + // rather than as the corpora tsc never selected. Spelled out below the tables. + tsc: 'oracle for part of this corpus', + // The CSS group's reference row is svelte/compiler's `parseCss`, which is not a + // validity oracle in either direction — so PostCSS sitting slightly above it is + // a grammar difference, not a conformance verdict. Spelled out in the notes + // below the tables. + postcss: 'a different CSS grammar' +}; + +/** + * Derives per-language parse-coverage groups from a conformance report + * (`corpus_kind: 'conformance'` — parse groups only). Rows are ordered by coverage, + * highest first; entries without coverage data are dropped. + */ +export const derive_conformance_groups = (baseline: BenchmarkBaseline): Array => { + const by_language: Map> = new Map(); + for (const entry of baseline.entries) { + const [operation, language] = entry.group.split('/'); + if (operation !== 'parse' || !language) continue; + const display_name = CONFORMANCE_ENGINE_NAMES[entry.name]; + if (!display_name) continue; + if (entry.files_processed == null || entry.files_total == null) continue; + let rows = by_language.get(language); + if (!rows) { + rows = []; + by_language.set(language, rows); + } + rows.push({ + name: display_name, + note: CONFORMANCE_ROW_NOTES[entry.name], + files_processed: entry.files_processed, + files_total: entry.files_total, + coverage_fraction: entry.files_total > 0 ? entry.files_processed / entry.files_total : 0 + }); + } + + const result: Array = []; + for (const [language, rows] of by_language) { + // coverage descending (highest acceptance first), name as a stable tiebreak + rows.sort((a, b) => b.coverage_fraction - a.coverage_fraction || a.name.localeCompare(b.name)); + result.push({ + language, + files_total: Math.max(0, ...rows.map((r) => r.files_total)), + rows + }); + } + result.sort((a, b) => (LANGUAGE_ORDER[a.language] ?? 9) - (LANGUAGE_ORDER[b.language] ?? 9)); + return result; +}; + +/** + * Formats a coverage fraction as a percentage with two decimals (`99.85%`), + * FLOORED rather than rounded — rounding would render e.g. 44219/44220 as + * `100.00%` next to a visibly non-total count. Only exact totality reads 100% + * (matching the harness's own `coverage_pct` convention in tsv's report.ts). + */ +export const format_coverage_percent = (fraction: number): string => + `${(Math.floor(fraction * 10_000) / 100).toFixed(2)}%`; + export const derive_speedup_summary = (groups: Array): Array => { const find_speedup = ( operation: string, language: string, - primary_name: string, + primary_name: string ): number | undefined => { const group = groups.find((g) => g.operation === operation && g.language === language); if (!group?.canonical_entry) return undefined; @@ -225,17 +712,589 @@ export const derive_speedup_summary = (groups: Array): Array = { + 'dprint (wasm)': 'formatter', + 'malva (wasm)': 'formatter', + 'swc (napi)': 'parser', + 'rsvelte compiler (napi)': 'parser' +}; + +/** + * Buckets a binary-size label by capability so each build lands next to its + * closest competitor: `parser` (tsv's parse-only build, `oxc-parser`), + * `formatter` (tsv's format-only build, `oxfmt`), else `full` (the flagship + * parse+format builds, `biome`, and the combined `oxc-parser + oxfmt` entry). + * + * A label naming neither job is looked up in `SIZE_CAPABILITY_BY_LABEL` first — + * the keyword read below can only answer for tools named after what they do. + */ +export const categorize_size_capability = (label: string): SizeCapability => { + const stated = SIZE_CAPABILITY_BY_LABEL[label]; + if (stated) return stated; + const has_parse = label.includes('parse'); + const has_format = label.includes('format') || label.includes('fmt'); + // a build that does both is a full toolchain (e.g. the combined oxc-parser + oxfmt entry) + if (has_parse && has_format) return 'full'; + if (has_parse) return 'parser'; // tsv parse (ffi), tsv-parse-wasm, oxc-parser + if (has_format) return 'formatter'; // tsv format, oxfmt + // Reaching here must be a DECISION, not a name the heuristic couldn't read: + // every label that lands in `full` this way ships both operations (biome's whole + // toolchain included). Anything else belongs in the table above. + return 'full'; // tsv (napi/ffi), tsv-wasm, biome (wasm) +}; + +export interface SizeDisplayEntry extends BinarySize { + bar_fraction: number; + category: ImplementationCategory; + // a grayed-out, inert placeholder for a build that doesn't exist (e.g. oxfmt's + // absent wasm build) — no bar, no size, just the label held in its slot. Absent + // on real, measured entries. + disabled?: boolean; +} + +export interface SizeCapabilityGroup { + capability: SizeCapability; + heading: string; + // sorted smallest-first, so the leading entry is the default ratio anchor (1.0x) + entries: Array; +} + +const SIZE_CAPABILITY_ORDER: ReadonlyArray<{ + capability: SizeCapability; + heading: string; +}> = [ + { capability: 'full', heading: 'Full toolchain (parse + format)' }, + { capability: 'formatter', heading: 'Formatter' }, + { capability: 'parser', heading: 'Parser' } +]; + +/** Display label for the synthesized combined oxc full-toolchain build. */ +export const OXC_FULL_LABEL = 'oxc-parser + oxfmt (napi)'; + +/** Display label for oxfmt's absent wasm build placeholder — see `derive_size_groups`. */ +export const OXFMT_WASM_LABEL = 'oxfmt (wasm)'; + +/** + * Synthesizes oxc's full-toolchain native build by summing its separately-shipped + * parser (`oxc-parser (napi)`) and formatter (`oxfmt (napi)`) packages — together + * they're the closest equivalent to tsv's single parse+format build, so the entry + * stands beside `tsv (napi)` in the full-toolchain group. Returns `undefined` when + * either half is missing (older baselines), and sums gzip only when both carry it. + */ +const synthesize_oxc_full = (sizes: Array): BinarySize | undefined => { + const parser = sizes.find((s) => s.label === 'oxc-parser (napi)'); + const formatter = sizes.find((s) => s.label === 'oxfmt (napi)'); + if (!parser || !formatter) return undefined; + return { + label: OXC_FULL_LABEL, + bytes: parser.bytes + formatter.bytes, + kind: 'native', + gzip_bytes: + parser.gzip_bytes != null && formatter.gzip_bytes != null + ? parser.gzip_bytes + formatter.gzip_bytes + : null + }; +}; + +/** The rsvelte-fmt binary on its own — the label the tsv harness emits. */ +export const RSVELTE_LABEL = 'rsvelte-fmt (binary)'; + +/** Display label for the rsvelte-fmt + oxfmt install — see `synthesize_rsvelte_install`. */ +export const RSVELTE_INSTALL_LABEL = 'rsvelte-fmt + oxfmt (binary)'; + +/** + * Synthesizes what installing rsvelte-fmt to format a project actually costs, by + * summing its own binary with the `oxfmt` it declares as a peer dependency. + * + * Both figures are shown because which one is honest depends on how you invoke + * it, and the difference is verifiable: pointed at a **single `.svelte` file** (or + * stdin) rsvelte-fmt needs no oxfmt at all — it formats the embedded + * ` @@ -23,15 +23,16 @@ {:else}

- This loads @fuzdev/tsv_wasm in your browser to format and parse Svelte with TypeScript and CSS. + This loads + @fuzdev/tsv-wasm + in your browser to format and parse Svelte with TypeScript and CSS.

See the for setup and API usage. The textarea above uses fuz_code's - CodeTextarea component. + CodeTextarea + component.

{/if} diff --git a/src/routes/docs/playground/Playground.svelte b/src/routes/docs/playground/Playground.svelte index ba20a09..3fa1e4a 100644 --- a/src/routes/docs/playground/Playground.svelte +++ b/src/routes/docs/playground/Playground.svelte @@ -5,15 +5,16 @@ import Code from '@fuzdev/fuz_code/Code.svelte'; import CodeTextarea from '@fuzdev/fuz_code/CodeTextarea.svelte'; - import {supports_css_highlight_api} from '@fuzdev/fuz_code/highlight_manager.ts'; - import {to_error_message} from '@fuzdev/fuz_util/error.ts'; + import { supports_css_highlight_api } from '@fuzdev/fuz_code/highlight_manager.ts'; + import CopyToClipboard from '@fuzdev/fuz_ui/CopyToClipboard.svelte'; + import { to_error_message } from '@fuzdev/fuz_util/error.ts'; - import {playground_example} from './playground_example.ts'; + import { playground_example } from './playground_example.ts'; - // `@fuzdev/tsv_wasm` is loaded lazily, in the browser only — a dynamic import - // so the ~900KB WASM lands in its own chunk, fetched the first time this + // `@fuzdev/tsv-wasm` is loaded lazily, in the browser only — a dynamic import + // so the ~1MB-gzipped WASM lands in its own chunk, fetched the first time this // component mounts and never pulled into `/docs` or the prerendered HTML. - let tsv: typeof import('@fuzdev/tsv_wasm') | null = $state(null); + let tsv: typeof import('@fuzdev/tsv-wasm') | null = $state(null); let load_error: string | null = $state(null); // the editable source — starts as the deliberately-unformatted example so the @@ -23,7 +24,7 @@ const ready = $derived(tsv !== null); // A tsv call's outcome: its string result, or the thrown error's message. - type Outcome = {value: string; error: null} | {value: null; error: string}; + type Outcome = { value: string; error: null } | { value: null; error: string }; // Run a tsv call, capturing a thrown error as a message; `null` until the WASM // loads. Lets `formatted` and `ast` share one shape and recompute as `source` changes @@ -31,9 +32,9 @@ const run = (fn: (t: NonNullable) => string): Outcome | null => { if (!tsv) return null; try { - return {value: fn(tsv), error: null}; + return { value: fn(tsv), error: null }; } catch (err) { - return {value: null, error: to_error_message(err)}; + return { value: null, error: to_error_message(err) }; } }; @@ -59,7 +60,7 @@ let cancelled = false; void (async () => { try { - const mod = await import('@fuzdev/tsv_wasm'); + const mod = await import('@fuzdev/tsv-wasm'); await mod.init(); if (cancelled) return; tsv = mod; @@ -72,6 +73,8 @@ }; }); + const is_base = $derived(source === playground_example); + const reset = (): void => { source = playground_example; }; @@ -84,8 +87,8 @@ }; -
- +
+ @@ -107,12 +110,15 @@ {#if !ready}

loading the formatter…

{:else if error} -

{error}

+

{error}

{:else}

formatted

AST

- +
+ + +
{/if} @@ -132,11 +138,31 @@ section :global(.ast) { max-height: 500px; } + /* float a copy button over the AST pane's top-right corner; it stays pinned as + the pane scrolls since it's absolute to this wrapper, not inside the scroller */ + .ast-output { + position: relative; + } + .ast-output :global(.ast-copy) { + --font_size: var(--font_size_lg); + position: absolute; + top: var(--space_xs); + /* clear the pane's scrollbar */ + right: var(--space_md); + z-index: 1; + background: var(--shade_00); + } + /* TODO hacky: fuz_ui's CopyToClipboard sets an inline `style:width="100%"` on + its icon wrapper div, which only `!important` can beat from here — drop this + once that wrapper stops hardcoding its width */ + .ast-output :global(.ast-copy > div) { + width: auto !important; + } .error { color: var(--color_e_40); white-space: pre-wrap; } - .parse_error { + .parse-error { color: var(--color_c_50); white-space: pre-wrap; } diff --git a/src/routes/docs/tomes.ts b/src/routes/docs/tomes.ts index 733ca43..c53039a 100644 --- a/src/routes/docs/tomes.ts +++ b/src/routes/docs/tomes.ts @@ -1,4 +1,4 @@ -import type {Tome} from '@fuzdev/fuz_ui/tome.ts'; +import type { Tome } from '@fuzdev/fuz_ui/tome.ts'; import IntroductionPage from './introduction/+page.svelte'; import PlaygroundPage from './playground/+page.svelte'; import BenchmarksPage from './benchmarks/+page.svelte'; @@ -10,7 +10,7 @@ export const tomes: Array = [ Component: IntroductionPage, related_tomes: ['playground', 'benchmarks'], related_modules: [], - related_declarations: [], + related_declarations: [] }, { slug: 'playground', @@ -18,7 +18,7 @@ export const tomes: Array = [ Component: PlaygroundPage, related_tomes: ['introduction', 'benchmarks'], related_modules: [], - related_declarations: [], + related_declarations: [] }, { slug: 'benchmarks', @@ -26,6 +26,6 @@ export const tomes: Array = [ Component: BenchmarksPage, related_tomes: ['introduction', 'playground'], related_modules: [], - related_declarations: [], - }, + related_declarations: [] + } ]; diff --git a/src/routes/example.test.ts b/src/routes/example.test.ts deleted file mode 100644 index 57684dd..0000000 --- a/src/routes/example.test.ts +++ /dev/null @@ -1,5 +0,0 @@ -import {assert, test} from 'vitest'; - -test('tests work', () => { - assert.strictEqual('ok', 'OK'.toLowerCase()); -}); diff --git a/src/routes/library.ts b/src/routes/library.ts index b2aa8df..b47f208 100644 --- a/src/routes/library.ts +++ b/src/routes/library.ts @@ -1,5 +1,5 @@ -import {library_json_from_modules} from '@fuzdev/fuz_util/library_json.ts'; -import {modules} from 'virtual:svelte-docinfo'; +import { library_json_from_modules } from '@fuzdev/fuz_util/library_json.ts'; +import { modules } from 'virtual:svelte-docinfo'; import pkg_json from 'virtual:pkg.json'; export const library_json = library_json_from_modules(pkg_json, modules); diff --git a/src/routes/style.css b/src/routes/style.css index 19df833..037d1b7 100644 --- a/src/routes/style.css +++ b/src/routes/style.css @@ -1,9 +1,7 @@ -/* global stylesheet imported by `src/routes/+layout.svelte */ +/* global stylesheet imported by `src/routes/+layout.svelte` */ -/* - -:root { - --some_stylish_property: 100%; +/* a warning aside: the red tint over fuz_css's base aside styling, shared by the + benchmarks page and its cross-runtime tables */ +.mixed-vintage { + border-left-color: var(--color_c_50); } - -*/ diff --git a/src/test/benchmark_data.prose.test.ts b/src/test/benchmark_data.prose.test.ts new file mode 100644 index 0000000..2aa2f6c --- /dev/null +++ b/src/test/benchmark_data.prose.test.ts @@ -0,0 +1,262 @@ +import { assert, describe, test } from 'vitest'; + +import { benchmarks_json } from '$routes/docs/benchmarks/benchmarks.ts'; +import { benchmarks_cross_runtime_json } from '$routes/docs/benchmarks/benchmarks_cross_runtime.ts'; +import { benchmarks_conformance_json } from '$routes/docs/benchmarks/benchmarks_conformance.ts'; +import { + benchmarks_cli, + cli_memory_ratio_range, + cli_speedup_vs_tsv, + CLI_DELIVERY_KEY, + CLI_TS_REPO_KEY, + CLI_TSV_NPM_LABEL, + CLI_TSV_WASM_LABEL +} from '$routes/docs/benchmarks/benchmarks_cli.ts'; +import { + benchmark_speedup, + derive_unstable_cells, + derive_unstable_entries, + is_entry_unstable, + format_ratio_approx, + format_ratio_range, + format_ratio_range_approx +} from '$routes/docs/benchmarks/benchmark_data.ts'; + +// The page's prose quotes ratios computed from the reports rather than +// hand-written numbers, so a renamed entry or a dropped scenario would render +// `—` mid-sentence instead of failing. These gate every pair the copy names. +describe('prose ratios resolve', () => { + // `[group, slower, faster]` in the direction each sentence reads — the copy says + // "X faster than" for the pairs tsv leads and "slower than" / "Y is faster than + // tsv" for the ones it trails (yuku on TypeScript, the JS parsers on CSS), so + // every ratio here must land above 1 or the sentence has flipped. + const IN_PROCESS_PAIRS: Array<[string, string, string]> = [ + ['format/typescript', 'oxfmt', 'tsv'], + ['format/typescript', 'prettier', 'tsv'], + ['format/typescript', 'biome-wasm', 'tsv-wasm'], + ['format/svelte', 'prettier', 'tsv'], + ['format/svelte', 'biome-wasm', 'tsv-wasm'], + ['format/css', 'oxfmt', 'tsv'], + ['format/css', 'biome-wasm', 'tsv-wasm'], + ['parse/typescript', 'oxc-parser', 'tsv-json-no-locations'], + ['parse/typescript', 'tsv-json-no-locations', 'yuku-parser'], + // "carrying it costs ~Nx the hand-off time" — the loc-bearing wire over the span-only one + ['parse/typescript', 'tsv-json', 'tsv-json-no-locations'], + ['parse/svelte', 'svelte/compiler', 'tsv-json'], + ['parse/svelte', 'rsvelte-parse', 'tsv-json'], + ['parse/css', 'tsv-json', 'svelte/compiler'], + ['parse/css', 'tsv-json', 'postcss'] + ]; + + test('every in-process pair the TLDR quotes is present and runs the way the sentence reads', () => { + for (const [group, slower, faster] of IN_PROCESS_PAIRS) { + const ratio = benchmark_speedup(benchmarks_json, group, slower, faster); + assert.isDefined(ratio, `${group}: ${slower} vs ${faster}`); + assert.isAbove(ratio, 1, `${group}: ${slower} vs ${faster}`); + } + }); + + test('oxfmt formats Svelte at Prettier speed, as the note says it delegates', () => { + // "for Svelte it delegates to Prettier internally" — if a future oxfmt grows its + // own Svelte path the two rows will part ways and the note is stale + const ratio = benchmark_speedup(benchmarks_json, 'format/svelte', 'oxfmt', 'prettier'); + assert.isDefined(ratio); + assert.closeTo(ratio, 1, 0.1, 'oxfmt and prettier should be within 10% on format/svelte'); + }); + + test('the CSS parse note reads the internal rows it points at', () => { + // "the JSON hand-off is most of tsv's time there (compare the internal rows)" + // — the wire must cost more than the engine, or the explanation is wrong + const wire_share = benchmark_speedup(benchmarks_json, 'parse/css', 'tsv-json', 'tsv-internal'); + assert.isDefined(wire_share); + assert.isAbove(wire_share, 2, 'tsv-json should cost at least twice tsv-internal on CSS'); + }); + + test('every in-process pair the TLDR quotes was measured stably', () => { + // A headline ratio divides two means; a row the bench flagged as unstable (a + // cv past 10%, cleaned or raw, or a drift past 5% — a cost that moved while + // the row was measured) publishes a mean that may sit between two modes. The + // bench discloses it in its report; nothing on the page read that field, and + // a "~10x" once shipped off a row that was 8x by median. Re-run the runtime + // (`deno task bench:node:run && deno task bench:compose`) and re-publish. + for (const [group, slower, faster] of IN_PROCESS_PAIRS) { + for (const name of [slower, faster]) { + const entry = benchmarks_json.entries.find((e) => e.group === group && e.name === name); + assert(entry, `${group}/${name} is missing`); + assert.isFalse( + is_entry_unstable(entry), + `${group}/${name} was not measured stably (cv ${entry.cv}, raw cv ${entry.cv_raw}, ` + + `drift ${entry.drift}, n=${entry.sample_size}) — re-run before publishing` + ); + } + } + }); + + test('the unstable disclosures read the report fields they claim to', () => { + // `derive_unstable_entries` must agree with the per-row predicate over the + // whole report, and the combined report's `unstable_cells` must name only rows + // the tables carry — the disclosure can't point at a row a reader can't find. + const flagged = derive_unstable_entries(benchmarks_json); + for (const entry of benchmarks_json.entries) { + assert.strictEqual( + flagged.includes(entry), + is_entry_unstable(entry), + `${entry.group}/${entry.name}` + ); + } + const row_keys = new Set(benchmarks_cross_runtime_json.rows.map((r) => `${r.group}/${r.name}`)); + for (const cell of derive_unstable_cells(benchmarks_cross_runtime_json)) { + assert(row_keys.has(`${cell.group}/${cell.name}`), `${cell.group}/${cell.name}`); + assert( + benchmarks_cross_runtime_json.runtimes.includes(cell.runtime), + `${cell.runtime} is not a runtime the report carries` + ); + } + }); + + test('every CLI ratio the prose quotes is present', () => { + for (const [label, metric] of [ + ['oxfmt', 'wall_ms'], + ['oxfmt', 'cpu_ms'], + ['biome', 'wall_ms'], + ['biome', 'cpu_ms'] + ] as const) { + assert.isDefined( + cli_speedup_vs_tsv(CLI_TS_REPO_KEY, label, metric), + `${CLI_TS_REPO_KEY}: ${label} ${metric}` + ); + } + // the TLDR's "less memory than either" range, scoped to the tools it names, and + // the CLI note's "less than every other tool in every scenario" — both read as + // "less", so the LOW end must clear 1 or the floored range would print "0–Nx" + for (const range of [ + cli_memory_ratio_range(CLI_TS_REPO_KEY, ['oxfmt', 'biome']), + cli_memory_ratio_range() + ]) { + assert.isDefined(range); + assert.isAbove(range.min, 1); + } + // the delivery note's three ratios, tsv against its own distributions + for (const [label, metric] of [ + [CLI_TSV_NPM_LABEL, 'wall_ms'], + [CLI_TSV_WASM_LABEL, 'wall_ms'], + [CLI_TSV_WASM_LABEL, 'memory_mb'] + ] as const) { + const ratio = cli_speedup_vs_tsv(CLI_DELIVERY_KEY, label, metric); + assert.isDefined(ratio, `${CLI_DELIVERY_KEY}: ${label} ${metric}`); + // the copy reads "takes ~Nx as long" / "~Nx the memory", so each must exceed 1 + assert.isAbove(ratio, 1, `${CLI_DELIVERY_KEY}: ${label} ${metric}`); + } + }); + + test('the CLI CPU-work note reads the way the numbers run', () => { + // "tsv is ~Nx faster than Oxfmt in wall-clock but ~Mx in CPU work, because tsv + // spreads its work across more cores than Oxfmt" — that holds only while the + // wall ratio EXCEEDS the CPU ratio; "against Biome ... Biome spreads wider than + // tsv but burns more CPU doing it" only while the wall ratio TRAILS it. Either + // inequality flipping on a refresh leaves the sentence explaining the opposite + // of what the table shows, so both are pinned in the direction the copy reads. + const ratio = (label: string, metric: 'wall_ms' | 'cpu_ms') => { + const value = cli_speedup_vs_tsv(CLI_TS_REPO_KEY, label, metric); + assert.isDefined(value, `${CLI_TS_REPO_KEY}: ${label} ${metric}`); + return value; + }; + assert.isAbove( + ratio('oxfmt', 'wall_ms'), + ratio('oxfmt', 'cpu_ms'), + 'oxfmt: wall lead > CPU lead' + ); + assert.isBelow( + ratio('biome', 'wall_ms'), + ratio('biome', 'cpu_ms'), + 'biome: wall lead < CPU lead' + ); + }); + + test('the unscoped memory range spans only the scenarios that face other tools', () => { + // "less than every other tool in every scenario it faces them" — the tsv-only + // delivery rows are tsv's own distributions and must not widen or narrow it + const delivery = benchmarks_cli.scenarios.find((s) => s.key === CLI_DELIVERY_KEY); + assert(delivery, `no generated scenario has id "${CLI_DELIVERY_KEY}"`); + assert.isTrue(delivery.tsv_only); + for (const r of delivery.results) { + assert.ok( + r.label === 'tsv' || r.label.startsWith('tsv'), + `${CLI_DELIVERY_KEY} carries a non-tsv row: ${r.label}` + ); + } + const competitor_keys = benchmarks_cli.scenarios.filter((s) => !s.tsv_only).map((s) => s.key); + assert.isNotEmpty(competitor_keys); + const ranges = competitor_keys.map((key) => cli_memory_ratio_range(key)).filter((r) => r); + const expected = { + min: Math.min(...ranges.map((r) => r!.min)), + max: Math.max(...ranges.map((r) => r!.max)) + }; + assert.deepStrictEqual(cli_memory_ratio_range(), expected); + // and the delivery scenario, named explicitly, still spans on its own + assert.isDefined(cli_memory_ratio_range(CLI_DELIVERY_KEY)); + }); + + test('the WASM delivery row is "still ahead of the JS formatters above"', () => { + // the delivery and large-single-file scenarios time the same parser.ts, so + // the note's ordering claim is checkable across them + const delivery = benchmarks_cli.scenarios.find((s) => s.key === CLI_DELIVERY_KEY); + const single = benchmarks_cli.scenarios.find((s) => s.key === 'large-single-file'); + assert(delivery && single); + assert.strictEqual(delivery.target.split(',')[0], single.target, 'same corpus file'); + const wasm = delivery.results.find((r) => r.label === CLI_TSV_WASM_LABEL); + assert(wasm, 'delivery scenario has no tsv-wasm row'); + for (const label of ['prettier', 'prettier + oxc-parser']) { + const js = single.results.find((r) => r.label === label); + assert(js, `large-single-file has no ${label} row`); + assert.isBelow(wasm.wall_ms, js.wall_ms, `tsv-wasm vs ${label}`); + } + }); + + test("the conformance note on oxc-parser's two bindings reads the report", () => { + // "its wasm binding is pinned to an older release ... and the two accept sets + // differ by a couple of files" — both halves are facts about the copied report, + // and either can go stale on a refresh: the bindings re-aligning makes the note + // a fiction, a wider gap makes "a couple" an understatement. + const { versions, entries } = benchmarks_conformance_json; + assert.isDefined(versions.oxc_parser_wasm); + assert.notStrictEqual(versions.oxc_parser_wasm, versions.oxc_parser, 'bindings re-aligned'); + const processed = (name: string) => { + const entry = entries.find((e) => e.group === 'parse/typescript' && e.name === name); + assert(entry?.files_processed != null, `${name} coverage`); + return entry.files_processed; + }; + const gap = Math.abs(processed('oxc-parser') - processed('oxc-parser-wasm')); + assert.isAtLeast(gap, 1, 'the accept sets agree — the note claims they differ'); + assert.isAtMost(gap, 5, 'the accept sets differ by more than "a couple of files"'); + }); + + test('the corpus repos the TLDR names are present', () => { + // "including Svelte's official repos (svelte, kit, svelte.dev) and the + // fuz.dev repos" — gate the names so the sentence can't outlive the corpus + const slugs = new Set( + (benchmarks_json.corpus_sources ?? []).map((s) => s.repo?.slug).filter(Boolean) + ); + for (const slug of ['sveltejs/svelte', 'sveltejs/kit', 'sveltejs/svelte.dev']) { + assert.ok(slugs.has(slug), `TLDR names ${slug} but the corpus lacks it`); + } + assert.ok( + [...slugs].some((slug) => slug?.startsWith('fuzdev/')), + 'TLDR names the fuz.dev repos but the corpus has none' + ); + }); + + test('approximate formatting drops digits as the ratio grows', () => { + assert.strictEqual(format_ratio_approx(1.66), '1.7x'); + assert.strictEqual(format_ratio_approx(26.241), '26x'); + assert.strictEqual(format_ratio_approx(undefined), '—'); + // floored at both ends so the claim never overstates either bound + assert.strictEqual(format_ratio_range(3.001, 9.89), '3–9x'); + // the approx variant brackets instead (floor min, ceil max), for ranges + // flooring both would collapse — the stated range always contains the truth + assert.strictEqual(format_ratio_range_approx(4.48, 4.86), '4–5x'); + // floor keeps the "at least" end honest where nearest-integer would inflate it + assert.strictEqual(format_ratio_range_approx(4.6, 4.86), '4–5x'); + assert.strictEqual(format_ratio_range_approx(3.001, 9.89), '3–10x'); + }); +}); diff --git a/src/test/benchmark_data.shape.test.ts b/src/test/benchmark_data.shape.test.ts new file mode 100644 index 0000000..a649160 --- /dev/null +++ b/src/test/benchmark_data.shape.test.ts @@ -0,0 +1,603 @@ +import { assert, describe, test } from 'vitest'; + +import { benchmarks_json } from '$routes/docs/benchmarks/benchmarks.ts'; +import { benchmarks_conformance_json } from '$routes/docs/benchmarks/benchmarks_conformance.ts'; +import { benchmarks_cross_runtime_json } from '$routes/docs/benchmarks/benchmarks_cross_runtime.ts'; +import { + categorize_name, + categorize_size, + categorize_size_capability, + derive_benchmark_groups, + derive_conformance_groups, + derive_cross_runtime_groups, + derive_size_groups, + derive_speedup_summary, + format_coverage_percent, + OXC_FULL_LABEL, + RSVELTE_INSTALL_LABEL, + RSVELTE_LABEL, + OXFMT_WASM_LABEL +} from '$routes/docs/benchmarks/benchmark_data.ts'; + +// The report shape versions the committed copies are pinned to — tsv's +// `REPORT_SCHEMA_VERSION` (per-runtime and conformance reports) and its composer's +// `COMBINED_SCHEMA_VERSION`. Exact rather than floors, so `npm run update-benchmarks` +// pulling a newer shape fails here until `benchmark_data.ts` mirrors the new fields +// and these are re-pinned together. +const REPORT_VERSION = 15; +const COMBINED_VERSION = 15; + +// Shape gate for the committed benchmarks.json: the bench report format drifts +// (it once went 3 months stale across a key rename that rendered as `undefined`), +// and `benchmarks.ts` casts the JSON, so typechecking alone won't catch it. +// When `npm run update-benchmarks` pulls in a new shape, these fail loudly. +describe('benchmarks.json shape', () => { + test('baseline version is current', () => { + // pinned exactly: a bump in tsv's `REPORT_SCHEMA_VERSION` must be a deliberate + // re-pin here, after `benchmark_data.ts` gains the new fields' version-notes + assert.strictEqual(benchmarks_json.version, REPORT_VERSION); + }); + + test('binary sizes include the flagship tsv builds', () => { + const labels = benchmarks_json.binary_sizes.map((s) => s.label); + assert.include(labels, 'tsv (napi)'); // flagship N-API build (perf report anchor) + assert.include(labels, 'tsv-wasm'); // the full wasm build — smallest full-toolchain, size baseline + }); + + test('versions carries the keys the meta component renders', () => { + const { versions } = benchmarks_json; + assert.isString(versions.svelte); + assert.isString(versions.acorn_ts); + assert.isString(versions.prettier); + assert.isString(versions.prettier_svelte); + }); + + test('corpus covers every benchmarked language', () => { + for (const language of ['svelte', 'typescript', 'css']) { + assert.isAbove(benchmarks_json.corpus[language] ?? 0, 0, language); + } + }); + + test('every group leads with its canonical entry (the default anchor) and a timed-set count', () => { + const groups = derive_benchmark_groups(benchmarks_json); + assert.isAtLeast(groups.length, 6); // format+parse × svelte/typescript/css + for (const group of groups) { + const key = `${group.operation}/${group.language}`; + assert.ok(group.canonical_entry, `${key} has no canonical entry`); + // the canonical reference sorts first, so it's the default 1.0x anchor the + // shared component reads off the leading row + assert.strictEqual(group.entries[0]?.category, 'canonical', `${key} canonical leads`); + assert.isNotNull(group.files_iterated, `${key} has no files_iterated`); + for (const entry of group.entries) { + if (entry.disabled) continue; + // measured entries render the whole-sweep mean (total corpus time); a + // zero would misread as an instantaneous run + assert.isAbove(entry.mean_ns, 0, `${key}/${entry.name} has no mean`); + } + } + }); + + test('svelte/css parse groups get disabled oxc placeholders, typescript keeps real ones', () => { + const groups = derive_benchmark_groups(benchmarks_json); + const parse = (language: string) => + groups.find((g) => g.operation === 'parse' && g.language === language); + + // typescript actually runs oxc-parser — its oxc entries are real, not placeholders + const ts = parse('typescript'); + const ts_oxc = ts?.entries.filter((e) => e.category === 'oxc') ?? []; + assert.isNotEmpty(ts_oxc); + for (const e of ts_oxc) assert.isNotOk(e.disabled, `${e.name} should be a real entry`); + + // svelte and css mirror those oxc entries in, disabled, in the same fixed slot: + // directly after the biome placeholder (both lead the cross-tool comparisons, + // right after the canonical row) and before tsv's json wires + for (const language of ['svelte', 'css']) { + const group = parse(language); + assert.ok(group, `${language} parse group missing`); + const oxc = group.entries.filter((e) => e.category === 'oxc'); + assert.strictEqual(oxc.length, ts_oxc.length, `${language} oxc placeholder count`); + for (const e of oxc) { + assert.ok(e.disabled, `${language} ${e.name} should be disabled`); + assert.strictEqual(e.bar_fraction, 0, `${language} ${e.name} bar`); + } + const names = group.entries.map((e) => e.name); + const first_biome = names.findIndex((n) => n.includes('biome')); + const first_oxc = names.findIndex((n) => n.includes('oxc')); + const first_json = names.findIndex((n) => n.endsWith('-json') || n.endsWith('-no-locations')); + assert.strictEqual(first_oxc, first_biome + 1, `${language} oxc directly after biome`); + assert.isBelow(first_oxc, first_json, `${language} oxc before the tsv json entries`); + } + }); + + test('yuku appears in the typescript parse group only, and is never mirrored elsewhere', () => { + const groups = derive_benchmark_groups(benchmarks_json); + + // The page's yuku prose is unconditional, so the copied report must carry the + // rows it describes — this failing means the report needs a refresh. + const ts = groups.find((g) => g.operation === 'parse' && g.language === 'typescript'); + const ts_yuku = ts?.entries.filter((e) => e.category === 'yuku') ?? []; + assert.isNotEmpty(ts_yuku, 'typescript parse must carry yuku rows'); + for (const e of ts_yuku) assert.isNotOk(e.disabled, `${e.name} should be a real entry`); + + // A grayed-out slot says a tool with WIDER scope is missing this language — + // true of biome and oxc, not of a parser that claims nothing wider. + for (const group of groups) { + if (group.operation !== 'parse' || group.language === 'typescript') continue; + assert.isEmpty( + group.entries.filter((e) => e.category === 'yuku'), + `${group.language} parse must hold no yuku slot` + ); + } + }); + + test('every parse group gets a disabled biome placeholder, since biome never exposes a parser to JS', () => { + const groups = derive_benchmark_groups(benchmarks_json); + for (const language of ['svelte', 'typescript', 'css']) { + const group = groups.find((g) => g.operation === 'parse' && g.language === language); + assert.ok(group, `${language} parse group missing`); + const biome = group.entries.filter((e) => e.category === 'biome'); + assert.strictEqual(biome.length, 1, `${language} biome placeholder count`); + const [entry] = biome; + assert.ok(entry, `${language} biome entry missing`); + assert.ok(entry.disabled, `${language} biome entry should be disabled`); + assert.strictEqual(entry.bar_fraction, 0, `${language} biome bar`); + + // biome leads the cross-tool comparisons: directly after the single canonical + // row (index 0), before tsv's json wires + const names = group.entries.map((e) => e.name); + const first_biome = names.findIndex((n) => n.includes('biome')); + const first_json = names.findIndex((n) => n.endsWith('-json') || n.endsWith('-no-locations')); + assert.strictEqual(first_biome, 1, `${language} biome directly after the canonical row`); + assert.isBelow(first_biome, first_json, `${language} biome before the tsv json entries`); + } + }); + + test('svelte gets a disabled dprint placeholder; typescript keeps dprint and css keeps malva real', () => { + const groups = derive_benchmark_groups(benchmarks_json); + const format = (language: string) => + groups.find((g) => g.operation === 'format' && g.language === language); + const dprint_rows = (language: string) => + format(language)?.entries.filter((e) => e.category === 'dprint') ?? []; + + // typescript actually runs dprint — its entry is real, not a placeholder + const ts_dprint = dprint_rows('typescript'); + + // The dprint row postdates older reports. With no measured entry there is + // nothing to mirror, and the derivation deliberately leaves every group + // untouched — so assert exactly that (no invented rows) and stop. Refreshing + // the report is what promotes this to the full assertion below. + if (ts_dprint.length === 0) { + assert.isEmpty( + dprint_rows('svelte'), + 'svelte must not invent a dprint row when the report carries none' + ); + return; + } + for (const e of ts_dprint) assert.isNotOk(e.disabled, `${e.name} should be a real entry`); + + // css runs dprint's own CSS plugin, malva, through the same Wasm host — a REAL + // row that shares dprint's category (see CATEGORY_BY_NAME), so css gets no + // placeholder: the mirror only fills a group with no dprint-category entry + const css_dprint = dprint_rows('css'); + assert.deepEqual( + css_dprint.map((e) => e.name), + ['malva-wasm'], + 'css carries malva-wasm and no dprint placeholder' + ); + for (const e of css_dprint) assert.isNotOk(e.disabled, `${e.name} should be a real entry`); + + // svelte mirrors the typescript entry in, disabled: `@dprint/typescript` + // rejects Svelte outright and the bench loads no Svelte plugin for the host + const svelte_dprint = dprint_rows('svelte'); + assert.strictEqual(svelte_dprint.length, ts_dprint.length, 'svelte dprint placeholder count'); + for (const e of svelte_dprint) { + assert.ok(e.disabled, `svelte ${e.name} should be disabled`); + assert.strictEqual(e.bar_fraction, 0, `svelte ${e.name} bar`); + } + + // in every group the dprint-category row sits directly after biome in the + // shared cross-tool ordering, real or placeholder alike + for (const language of ['svelte', 'css']) { + const names = format(language)!.entries.map((e) => e.name); + const first_biome = names.findIndex((n) => n.includes('biome')); + const first_dprint = names.findIndex((n) => n.includes('dprint') || n.includes('malva')); + assert.strictEqual( + first_dprint, + first_biome + 1, + `${language} dprint-category row directly after biome` + ); + } + }); + + test('the coverage-only rsvelte-fmt row is inert but keeps its real coverage', () => { + const groups = derive_benchmark_groups(benchmarks_json); + const svelte_format = groups.find((g) => g.operation === 'format' && g.language === 'svelte'); + assert.ok(svelte_format, 'svelte format group missing'); + const rsvelte = svelte_format.entries.find((e) => e.name === 'rsvelte-fmt'); + + // The row postdates older reports; refreshing the report promotes this to + // the full assertion, exactly as the dprint test above works. + if (!rsvelte) return; + + // Inert: no bar, and never the ratio anchor — it has no timing to anchor on. + assert.ok(rsvelte.disabled, 'rsvelte-fmt should render inert'); + assert.ok(rsvelte.coverage_only, 'rsvelte-fmt should be flagged coverage-only'); + assert.strictEqual(rsvelte.bar_fraction, 0, 'rsvelte-fmt bar'); + + // ...but unlike a plain placeholder it DID run, so its coverage is real. + // This is the distinction the page renders as `not timed` rather than `n/a`. + assert.isNumber(rsvelte.files_processed, 'rsvelte-fmt coverage should be present'); + assert.isNumber(rsvelte.files_total, 'rsvelte-fmt total should be present'); + + // It must not have become the group's bar scale: an untimed row coerces to + // mean_ns 0, so a regression that let it into `slowest` would silently + // rescale every real bar in the group. + const timed = svelte_format.entries.filter((e) => !e.disabled); + assert.ok( + timed.some((e) => e.bar_fraction === 1), + 'some timed row must still be the full-width bar' + ); + + // Only format/svelte carries it — it formats nothing else here. + for (const group of groups) { + if (group === svelte_format) continue; + assert.isEmpty( + group.entries.filter((e) => e.name === 'rsvelte-fmt'), + `${group.operation}/${group.language} should carry no rsvelte-fmt row` + ); + } + }); + + test('format/parse rows follow the fixed canonical → biome → dprint → oxc → postcss → rsvelte → swc → yuku → json → internal order', () => { + // independently mirrors the requested row order (`speed_entry_rank`) so a + // regression in the derivation's comparator fails here + const tier = (name: string, category: string): number => { + if (category === 'canonical') return 0; + if (category === 'biome') return 1; + if (category === 'dprint') return 2; + if (category === 'oxc') return 3; + if (category === 'postcss') return 4; + if (category === 'rsvelte') return 5; + if (category === 'swc') return 6; + if (category === 'yuku') return 7; + if (name.endsWith('-no-locations')) return 8; // tsv json, span-only wire + if (name.endsWith('-json')) return 9; // tsv json, loc-carrying wire + return 10; // tsv's engine rows (`tsv`/`tsv-wasm`, `-internal`) + }; + for (const group of derive_benchmark_groups(benchmarks_json)) { + const key = `${group.operation}/${group.language}`; + const tiers = group.entries.map((e) => tier(e.name, e.category)); + assert.deepEqual( + tiers, + [...tiers].toSorted((a, b) => a - b), + `${key} tier order` + ); + // wasm precedes native within a tier + for (let i = 1; i < group.entries.length; i++) { + if (tiers[i] !== tiers[i - 1]) continue; + const prev_wasm = group.entries[i - 1]!.name.includes('wasm'); + const curr_wasm = group.entries[i]!.name.includes('wasm'); + assert.isFalse(!prev_wasm && curr_wasm, `${key} native precedes wasm within a tier`); + } + } + }); + + test('speedup summary is fully populated', () => { + const rows = derive_speedup_summary(derive_benchmark_groups(benchmarks_json)); + assert.strictEqual(rows.length, 2); // native + wasm + for (const row of rows) { + assert.isDefined(row.format_svelte, row.variant); + assert.isDefined(row.format_typescript, row.variant); + assert.isDefined(row.format_css, row.variant); + } + }); + + test('binary sizes group by capability with one smallest-build ratio anchor', () => { + const groups = derive_size_groups(benchmarks_json.binary_sizes); + // full / formatter / parser, in that order, all present in the current data + assert.deepStrictEqual( + groups.map((g) => g.capability), + ['full', 'formatter', 'parser'] + ); + for (const group of groups) { + assert.isNotEmpty(group.entries); + // every entry lands in the group its capability names + for (const e of group.entries) { + assert.strictEqual(categorize_size_capability(e.label), group.capability); + } + // the group's default ratio anchor (its 1.0x row) is the single smallest real + // build, and it leads the group — the shared component reads every ratio off + // the first row. Disabled placeholders (e.g. oxfmt's absent wasm build) never + // sort first. + const real = group.entries.filter((e) => !e.disabled); + const smallest = real.reduce((a, b) => (a.bytes <= b.bytes ? a : b)); + assert.strictEqual( + group.entries[0]?.label, + smallest.label, + `${group.capability} smallest leads` + ); + } + // the parser group pits tsv against oxc-parser in both kinds + const parser = groups.find((g) => g.capability === 'parser'); + const parser_labels = parser?.entries.map((e) => e.label) ?? []; + assert.include(parser_labels, 'oxc-parser (wasm)'); + assert.include(parser_labels, 'oxc-parser (napi)'); + }); + + test('full toolchain group carries the combined oxc-parser + oxfmt entry', () => { + const sizes = benchmarks_json.binary_sizes; + const groups = derive_size_groups(sizes); + const full = groups.find((g) => g.capability === 'full'); + const combined = full?.entries.find((e) => e.label === OXC_FULL_LABEL); + assert.ok(combined, 'combined oxc entry missing from full toolchain group'); + + // its bytes and gzip are the sum of oxc's separate parser and formatter builds + const oxc_parser = sizes.find((s) => s.label === 'oxc-parser (napi)'); + const oxfmt = sizes.find((s) => s.label === 'oxfmt (napi)'); + assert.ok(oxc_parser && oxfmt, 'source oxc builds missing'); + assert.strictEqual(combined.bytes, oxc_parser.bytes + oxfmt.bytes); + assert.strictEqual(combined.gzip_bytes, oxc_parser.gzip_bytes! + oxfmt.gzip_bytes!); + + // native build, colored as oxc + assert.strictEqual(combined.kind, 'native'); + assert.strictEqual(combined.category, 'oxc'); + }); + + test('formatter group carries rsvelte-fmt both bare and summed with the oxfmt it needs for a project', () => { + const sizes = benchmarks_json.binary_sizes; + const groups = derive_size_groups(sizes); + const formatter = groups.find((g) => g.capability === 'formatter'); + assert.ok(formatter, 'formatter group missing'); + + const bare = formatter.entries.find((e) => e.label === RSVELTE_LABEL); + // The rsvelte rows postdate older reports; refreshing the report promotes + // this to the full assertion, as the dprint test above works. + if (!bare) { + assert.isEmpty( + formatter.entries.filter((e) => e.label === RSVELTE_INSTALL_LABEL), + 'must not synthesize the rsvelte pair when the report carries no rsvelte build' + ); + return; + } + + const combined = formatter.entries.find((e) => e.label === RSVELTE_INSTALL_LABEL); + assert.ok(combined, 'rsvelte-fmt + oxfmt entry missing from formatter group'); + + const oxfmt = sizes.find((s) => s.label === 'oxfmt (napi)'); + assert.ok(oxfmt, 'source oxfmt build missing'); + assert.strictEqual(combined.bytes, bare.bytes + oxfmt.bytes); + assert.strictEqual(combined.gzip_bytes, bare.gzip_bytes! + oxfmt.gzip_bytes!); + + // both are real measured builds, not placeholders — the pair is the project + // figure, the bare binary the single-file one + assert.isNotOk(bare.disabled, 'bare rsvelte-fmt should be a real entry'); + assert.isNotOk(combined.disabled, 'rsvelte pair should be a real entry'); + assert.strictEqual(combined.kind, 'native'); + for (const e of [bare, combined]) { + assert.strictEqual(e.category, 'rsvelte', `${e.label} category`); + } + + // the sum must sort after its own bare half — a group ordered smallest-first + // would otherwise be reporting a sum smaller than one of its addends + const labels = formatter.entries.map((e) => e.label); + assert.isAbove( + labels.indexOf(RSVELTE_INSTALL_LABEL), + labels.indexOf(RSVELTE_LABEL), + 'the rsvelte pair should sort after the bare binary' + ); + }); + + test('formatter group gets a disabled oxfmt (wasm) placeholder just above oxfmt (napi), since oxfmt has no wasm build', () => { + const groups = derive_size_groups(benchmarks_json.binary_sizes); + const formatter = groups.find((g) => g.capability === 'formatter'); + assert.ok(formatter, 'formatter group missing'); + + const placeholder = formatter.entries.find((e) => e.label === OXFMT_WASM_LABEL); + assert.ok(placeholder, 'oxfmt (wasm) placeholder missing'); + assert.ok(placeholder.disabled, 'oxfmt (wasm) should be disabled'); + assert.strictEqual(placeholder.kind, 'wasm'); + assert.strictEqual(placeholder.category, 'oxc'); + assert.strictEqual(placeholder.bar_fraction, 0); + + const labels = formatter.entries.map((e) => e.label); + const placeholder_index = labels.indexOf(OXFMT_WASM_LABEL); + const native_index = labels.indexOf('oxfmt (napi)'); + assert.isAbove(native_index, -1, 'oxfmt (napi) missing from formatter group'); + assert.strictEqual( + placeholder_index, + native_index - 1, + 'placeholder should sit just above oxfmt (napi)' + ); + }); + + test('flagship report is the node runtime', () => { + // the headline detailed view switched to N-API under Node; guards against an + // `update-benchmarks` that pulls the wrong runtime's sibling report + assert.strictEqual(benchmarks_json.runtime, 'node'); + }); + + test('every report row and size label maps to an explicit category', () => { + // `categorize_name` and `categorize_size` fall back to `'oxc'` for anything they + // don't recognize, so a new upstream row would silently render in oxc's hue + // rather than fail. Only the genuinely-oxc names may land on that fallback. + for (const entry of benchmarks_json.entries) { + assert.ok( + categorize_name(entry.name) !== 'oxc' || + entry.name.includes('oxc') || + entry.name === 'oxfmt', + `${entry.group}/${entry.name} falls back to the oxc hue` + ); + } + for (const row of benchmarks_cross_runtime_json.rows) { + assert.ok( + categorize_name(row.name) !== 'oxc' || row.name.includes('oxc') || row.name === 'oxfmt', + `${row.group}/${row.name} falls back to the oxc hue` + ); + } + for (const { label } of benchmarks_json.binary_sizes) { + assert.ok( + categorize_size(label) !== 'oxc' || label.startsWith('oxc') || label.startsWith('oxfmt'), + `${label} falls back to the oxc hue` + ); + } + }); +}); + +// Shape gate for the committed conformance report `benchmarks_conformance.json` +// (tsv's `report.conformance.node.json` — the parse-coverage surface over the +// deliberately-hard fixture suites, disjoint from the perf corpus, Svelte set minus +// canonical-rejects), consumed by the Parse conformance section. +describe('benchmarks_conformance.json shape', () => { + test('report is the conformance surface at the current version', () => { + assert.strictEqual(benchmarks_conformance_json.version, REPORT_VERSION); + assert.strictEqual(benchmarks_conformance_json.corpus_kind, 'conformance'); + assert.strictEqual(benchmarks_conformance_json.runtime, 'node'); + }); + + test('conformance report is parse-only', () => { + for (const entry of benchmarks_conformance_json.entries) { + assert.match(entry.group, /^parse\//, `${entry.group}/${entry.name}`); + } + }); + + test('corpus sources disclose the composition', () => { + assert.isNotEmpty(benchmarks_conformance_json.corpus_sources ?? []); + }); + + test('derives one coverage group per language, each with a tsv row and full coverage data', () => { + const groups = derive_conformance_groups(benchmarks_conformance_json); + assert.strictEqual(groups.length, 3); // svelte / typescript / css + assert.deepEqual( + groups.map((g) => g.language), + ['svelte', 'typescript', 'css'] + ); + for (const group of groups) { + assert.ok( + group.rows.some((r) => r.name === 'tsv'), + `${group.language} has a tsv row` + ); + // rows are ordered by coverage, highest first + const fractions = group.rows.map((r) => r.coverage_fraction); + assert.deepEqual( + fractions, + [...fractions].toSorted((a, b) => b - a), + `${group.language} rows descend by coverage` + ); + for (const row of group.rows) { + assert.isAbove(row.files_total, 0, `${group.language}/${row.name} total`); + assert.isAtLeast(row.coverage_fraction, 0); + assert.isAtMost(row.coverage_fraction, 1); + // engine-level rows only — binding/materialization variants are folded + assert.notMatch(row.name, /-internal|wasm-|-wasm/, `${group.language}/${row.name}`); + } + } + }); + + test('yuku reaches the typescript coverage group through its wasm binding', () => { + // The conformance report carries no yuku native row — that binding crashes the + // host process on this corpus, so tsv's harness omits it — and the page's note + // about it is unconditional. If the row name ever changes, the engine would + // silently vanish from the table instead of failing here. + const groups = derive_conformance_groups(benchmarks_conformance_json); + const ts = groups.find((g) => g.language === 'typescript'); + const yuku = ts?.rows.find((r) => r.name === 'yuku-parser'); + assert.ok(yuku, 'typescript coverage must carry a yuku-parser row'); + // the row's own qualifier, so the table says which binding without the + // reader having to reach the note below it + assert.strictEqual(yuku.note, 'wasm — native segfaults'); + }); + + test('tsc reaches the typescript coverage group, and only it, with its oracle note', () => { + // tsc parses TypeScript/JS alone and rides this surface only (it is a verdict, + // not a speed). It also SELECTED one slice of this corpus, where it scores 100% + // by construction — the note is what keeps the blended aggregate from reading as + // one achieved number, so a silent note rename must fail here rather than on the + // published page. + const groups = derive_conformance_groups(benchmarks_conformance_json); + const row_named = (language: string) => + groups.find((g) => g.language === language)?.rows.find((r) => r.name === 'tsc'); + const tsc = row_named('typescript'); + assert.ok(tsc, 'typescript coverage must carry a tsc row'); + assert.strictEqual(tsc.note, 'oracle for part of this corpus'); + assert.isUndefined(row_named('svelte'), 'tsc parses no Svelte'); + assert.isUndefined(row_named('css'), 'tsc parses no CSS'); + }); + + test('coverage percent floors — only exact totality reads 100%', () => { + // 44219/44220 rounds to 100.00% but must not display as it: floor, so a + // visibly non-total count never sits beside a "100.00%" label. + assert.strictEqual(format_coverage_percent(44_219 / 44_220), '99.99%'); + assert.strictEqual(format_coverage_percent(1), '100.00%'); + assert.strictEqual(format_coverage_percent(0.998549), '99.85%'); + assert.strictEqual(format_coverage_percent(0), '0.00%'); + }); +}); + +// Shape gate for the committed cross-runtime `benchmarks_cross_runtime.json` (the +// bench composer's combined `report.json`) — a different, slimmer shape than the +// per-runtime baseline, consumed by the Cross-runtime section. +describe('benchmarks_cross_runtime.json shape', () => { + test('combined report carries the current version and kind', () => { + assert.strictEqual(benchmarks_cross_runtime_json.version, COMBINED_VERSION); + assert.strictEqual(benchmarks_cross_runtime_json.kind, 'combined'); + // the committed fixture must be same-vintage — if this trips, re-run every + // runtime and recompose rather than committing a mixed set (the site would + // show the unreliable-ratios warning banner) + assert.notStrictEqual(benchmarks_cross_runtime_json.mixed_vintage, true); + }); + + test('the conformance report is same-vintage with the perf siblings', () => { + // combined `version` 13 records it; a stale one means `update-benchmarks` + // copied a conformance report from a different refresh than the perf trio + // (the site would show the parse-conformance section's warning banner) + const vintage = benchmarks_cross_runtime_json.conformance_vintage; + assert.isOk(vintage, 'conformance_vintage recorded'); + assert.notStrictEqual(vintage.stale, true); + assert.strictEqual(vintage.git_commit, benchmarks_conformance_json.git_commit); + }); + + test('the flagship report is the node sibling the combined report was composed from', () => { + // `update-benchmarks` copies three files from one `deno task bench`; if a copy + // is skipped or taken from another worktree, the detailed view and the + // cross-runtime tables silently describe different builds. The composer + // can't see the copied files, so this is the site-side vintage gate + const node = benchmarks_cross_runtime_json.sources.find((s) => s.runtime === 'node'); + assert.isOk(node, 'combined report carries a node source'); + assert.strictEqual(node.git_commit, benchmarks_json.git_commit); + assert.strictEqual(node.timestamp, benchmarks_json.timestamp); + assert.strictEqual(node.tsv, benchmarks_json.versions.tsv); + }); + + test('runtimes include the flagship and its cross-runtime peers', () => { + const { runtimes } = benchmarks_cross_runtime_json; + assert.include(runtimes, 'node'); // the flagship the headline view leads with + assert.include(runtimes, 'deno'); + assert.include(runtimes, 'bun'); + }); + + test('every group derives rows with the flagship runtime populated', () => { + const groups = derive_cross_runtime_groups(benchmarks_cross_runtime_json); + assert.isAtLeast(groups.length, 6); // format+parse × svelte/typescript/css + for (const group of groups) { + assert.isAbove(group.rows.length, 0, group.group); + for (const row of group.rows) { + assert.isNumber(row.ops_per_second.node, `${group.group}/${row.name} missing node ops`); + // ratios anchor on node (the display-order base, not the report's + // deno-first storage order), so node's own ratio is exactly 1 + assert.strictEqual(row.ratio_vs_base.node, 1, `${group.group}/${row.name} node anchor`); + } + } + }); + + test('the committed reports timed identical file sets across runtimes', () => { + // a mismatch means part of a published ratio is corpus composition, not + // runtime — recompose from same-box, same-commit siblings rather than + // committing a report that needs the ⚠ files annotation + const groups = derive_cross_runtime_groups(benchmarks_cross_runtime_json); + for (const group of groups) { + for (const row of group.rows) { + assert.isNull(row.files_iterated_mismatch, `${group.group}/${row.name} file-set mismatch`); + } + } + }); +}); diff --git a/src/test/benchmark_data.test.ts b/src/test/benchmark_data.test.ts index 0f6e8d0..341df2b 100644 --- a/src/test/benchmark_data.test.ts +++ b/src/test/benchmark_data.test.ts @@ -1,57 +1,241 @@ -import {assert, describe, test} from 'vitest'; +import { assert, describe, test } from 'vitest'; -import {benchmarks_json} from '$routes/docs/benchmarks/benchmarks.ts'; +import { benchmarks_json } from '$routes/docs/benchmarks/benchmarks.ts'; import { - derive_benchmark_groups, - derive_speedup_summary, + categorize_size_capability, + compute_baseline_ratio, + derive_corpus_repos, + derive_cross_runtime_groups, + derive_unavailable_by_runtime, + is_impl_unavailable, + format_speedup_signed, + order_cross_runtime_runtimes, + OXC_FULL_LABEL, + type CrossRuntimeReport } from '$routes/docs/benchmarks/benchmark_data.ts'; -// Shape gate for the committed benchmarks.json: the bench report format drifts -// (it once went 3 months stale across a key rename that rendered as `undefined`), -// and `benchmarks.ts` casts the JSON, so typechecking alone won't catch it. -// When `npm run update-benchmarks` pulls in a new shape, these fail loudly. -describe('benchmarks.json shape', () => { - test('baseline version is current', () => { - assert.isAtLeast(benchmarks_json.version, 4); +describe('derive_corpus_repos', () => { + test('maps the committed corpus sources to deduped org/name repo links', () => { + const repos = derive_corpus_repos(benchmarks_json.corpus_sources); + assert.isNotEmpty(repos); + // one entry per URL — no repo appears twice even though svelte.dev contributes + // several source subpaths + const urls = repos.map((r) => r.url); + assert.strictEqual(new Set(urls).size, urls.length, 'urls are distinct'); + const svelte_dev = repos.filter((r) => r.url === 'https://github.com/sveltejs/svelte.dev'); + assert.strictEqual(svelte_dev.length, 1, 'svelte.dev collapses to one entry'); + // each label is the linkified `org/name`, derived from (and ending) its URL + for (const repo of repos) { + assert.match(repo.label, /^[^/]+\/[^/]+$/, repo.url); + assert.isTrue(repo.url.endsWith(repo.label), `${repo.url} ends with ${repo.label}`); + } }); - test('binary sizes include the ratio anchors', () => { - const labels = benchmarks_json.binary_sizes.map((s) => s.label); - assert.include(labels, 'tsv (native)'); // native anchor - assert.include(labels, 'tsv_wasm'); // wasm anchor (the full build) + test('collapses shared repos and drops sources with no detected repo', () => { + const ref = (slug: string, subpath: string) => ({ + url: `https://github.com/${slug}`, + slug, + commit: '0123456789abcdef0123456789abcdef01234567', + subpath + }); + const repos = derive_corpus_repos([ + { + path: '../corpora/collections/zzz/src', + files: 1, + repo: ref('fuzdev/zzz', 'src') + }, + { + path: '../corpora/collections/svelte.dev/apps/svelte.dev/src', + files: 1, + repo: ref('sveltejs/svelte.dev', 'apps/svelte.dev/src') + }, + { + path: '../corpora/collections/svelte.dev/packages/repl/src', + files: 1, + repo: ref('sveltejs/svelte.dev', 'packages/repl/src') // same repo → collapsed + }, + { path: 'benches/js/.cache/svelte_styles', files: 1 } // no detected repo → dropped + ]); + assert.deepStrictEqual(repos, [ + { url: 'https://github.com/fuzdev/zzz', label: 'fuzdev/zzz' }, + { + url: 'https://github.com/sveltejs/svelte.dev', + label: 'sveltejs/svelte.dev' + } + ]); }); - test('versions carries the keys the meta component renders', () => { - const {versions} = benchmarks_json; - assert.isString(versions.svelte); - assert.isString(versions.acorn_ts); - assert.isString(versions.prettier); - assert.isString(versions.prettier_svelte); + test('handles a missing corpus_sources field', () => { + assert.deepStrictEqual(derive_corpus_repos(undefined), []); }); +}); - test('corpus covers every benchmarked language', () => { - for (const language of ['svelte', 'typescript', 'css']) { - assert.isAbove(benchmarks_json.corpus[language] ?? 0, 0, language); - } +describe('format_speedup_signed', () => { + test('anchor-and-faster entries read as a plain multiple', () => { + assert.strictEqual(format_speedup_signed(1), '1.00x'); + assert.strictEqual(format_speedup_signed(2.5), '2.50x'); + assert.strictEqual(format_speedup_signed(12.3), '12.3x'); // >= 10 drops to one decimal }); - test('every group derives a canonical entry and a timed-set count', () => { - const groups = derive_benchmark_groups(benchmarks_json); - assert.isAtLeast(groups.length, 6); // format+parse × svelte/typescript/css - for (const group of groups) { - const key = `${group.operation}/${group.language}`; - assert.ok(group.canonical_entry, `${key} has no canonical entry`); - assert.isNotNull(group.files_iterated, `${key} has no files_iterated`); - } + test('slower entries negate the reciprocal so the factor is directly legible', () => { + assert.strictEqual(format_speedup_signed(0.15), '-6.67x'); + assert.strictEqual(format_speedup_signed(0.05), '-20.0x'); // >= 10 magnitude → one decimal + assert.strictEqual(format_speedup_signed(0.98), '-1.02x'); // near-parity sign flip }); +}); - test('speedup summary is fully populated', () => { - const rows = derive_speedup_summary(derive_benchmark_groups(benchmarks_json)); - assert.strictEqual(rows.length, 2); // native + wasm - for (const row of rows) { - assert.isDefined(row.format_svelte, row.variant); - assert.isDefined(row.format_typescript, row.variant); - assert.isDefined(row.format_css, row.variant); - } +describe('compute_baseline_ratio', () => { + test('speed reads the anchor as the reference — faster entries exceed 1', () => { + // anchor 100ns; a 50ns entry is 2x faster, a 200ns entry is half the speed + assert.strictEqual(compute_baseline_ratio('speed', 50, 100), 2); + assert.strictEqual(compute_baseline_ratio('speed', 200, 100), 0.5); + }); + + test('size reads the anchor as the reference — bigger entries exceed 1', () => { + // anchor 100 bytes; a 300-byte build is 3x bigger, a 50-byte build is half + assert.strictEqual(compute_baseline_ratio('size', 300, 100), 3); + assert.strictEqual(compute_baseline_ratio('size', 50, 100), 0.5); + }); +}); + +describe('order_cross_runtime_runtimes', () => { + test('reorders the report storage order to node-first display order', () => { + assert.deepStrictEqual(order_cross_runtime_runtimes(['deno', 'node', 'bun']), [ + 'node', + 'deno', + 'bun' + ]); + }); + + test('anchors on the next runtime in display order when node is absent', () => { + assert.deepStrictEqual(order_cross_runtime_runtimes(['bun', 'deno']), ['deno', 'bun']); + assert.deepStrictEqual(order_cross_runtime_runtimes([]), []); + }); +}); + +// Per-runtime load failures (the composer's `unavailable_by_runtime`, combined +// `version` 9+ — it carried init-line labels under `impls` at 8, which matched no +// row name). Synthetic reports: the committed one predates the field, and the +// point of these is the DISTINCTION the field draws — a runtime that couldn't load +// the impl behind a row versus a report that simply has no such row. Both render +// as `fail`. +describe('derive_unavailable_by_runtime', () => { + const report = ( + unavailable_by_runtime?: CrossRuntimeReport['unavailable_by_runtime'] + ): CrossRuntimeReport => ({ + version: 9, + kind: 'combined', + generated: '2026-01-01T00:00:00.000Z', + runtimes: ['deno', 'node', 'bun'], + unavailable_by_runtime, + sources: [], + rows: [] + }); + + test('lists each runtime in the site column order, not the report storage order', () => { + // the report stores deno-first; the tables read node-first, and a disclosure + // listing runtimes in a different order than the columns invites misreading + const derived = derive_unavailable_by_runtime( + report([ + { runtime: 'bun', rows: ['biome-wasm', 'oxc-parser-wasm'] }, + { runtime: 'node', rows: ['biome-wasm'] } + ]) + ); + assert.deepStrictEqual( + derived.map((entry) => entry.runtime), + ['node', 'bun'] + ); + }); + + test('an empty row list is not a disclosure', () => { + assert.isEmpty(derive_unavailable_by_runtime(report([{ runtime: 'bun', rows: [] }]))); + }); + + test('a report predating the field discloses nothing — silence, not an all-clear', () => { + assert.isEmpty(derive_unavailable_by_runtime(report(undefined))); + }); + + test('is_impl_unavailable answers per runtime, and never guesses on an older report', () => { + // keyed by ROW name (`biome-wasm`), which is what the tables render — the + // bench's init label (`Biome`) would match no cell + const recorded = report([{ runtime: 'bun', rows: ['biome-wasm'] }]); + assert.isTrue(is_impl_unavailable(recorded, 'bun', 'biome-wasm')); + assert.isFalse(is_impl_unavailable(recorded, 'node', 'biome-wasm')); + assert.isFalse(is_impl_unavailable(recorded, 'bun', 'oxfmt')); + // absent field → every cell reads as "not measured here", which is the only + // claim the data supports + assert.isFalse(is_impl_unavailable(report(undefined), 'bun', 'biome-wasm')); + }); +}); + +// Binary-size capability grouping. The heuristic reads the tool's NAME, so the +// tools not named after their job are the ones that can silently land under a +// heading that misdescribes them (a formatter filed as "parse + format"). +describe('categorize_size_capability', () => { + test('reads the job out of a label that names it', () => { + assert.strictEqual(categorize_size_capability('tsv parse (ffi)'), 'parser'); + assert.strictEqual(categorize_size_capability('tsv-format-wasm'), 'formatter'); + assert.strictEqual(categorize_size_capability('oxfmt (napi)'), 'formatter'); + assert.strictEqual(categorize_size_capability(OXC_FULL_LABEL), 'full'); + assert.strictEqual(categorize_size_capability('tsv (napi)'), 'full'); + }); + + test('the tools whose names say nothing are stated, not guessed', () => { + // each of these would otherwise fall through to `full` — a "parse + format" + // heading over four builds, two shipping only a formatter and two no formatter + assert.strictEqual(categorize_size_capability('dprint (wasm)'), 'formatter'); + assert.strictEqual(categorize_size_capability('malva (wasm)'), 'formatter'); + assert.strictEqual(categorize_size_capability('swc (napi)'), 'parser'); + assert.strictEqual(categorize_size_capability('rsvelte compiler (napi)'), 'parser'); + }); + + test('the two dprint plugins share a bucket', () => { + // `dprint (wasm)` and `malva (wasm)` are the same kind of artifact — plugins + // over the one @dprint/formatter host, neither exposing a parser — so they + // must never be filed apart. Only `malva` reads as a formatter to a human + // eye, which is exactly how `dprint` sat under "parse + format" alone. + assert.strictEqual( + categorize_size_capability('dprint (wasm)'), + categorize_size_capability('malva (wasm)') + ); + }); +}); + +// The per-row file-set-mismatch annotation (the site rendering of the composer's +// `⚠ files a/b/c`), on a synthetic report since the committed one is healthy. +describe('derive_cross_runtime_groups files_iterated_mismatch', () => { + const report = ( + files_iterated: CrossRuntimeReport['rows'][number]['files_iterated'] + ): CrossRuntimeReport => ({ + version: 7, + kind: 'combined', + generated: '2026-01-01T00:00:00.000Z', + runtimes: ['deno', 'node', 'bun'], + sources: [], + rows: [ + { + group: 'parse/typescript', + name: 'tsv-json', + ops_per_second: { deno: 1, node: 2, bun: 3 }, + mean_ns: { deno: 3, node: 2, bun: 1 }, + files_iterated + } + ] + }); + + const derive_row = (files_iterated: CrossRuntimeReport['rows'][number]['files_iterated']) => + derive_cross_runtime_groups(report(files_iterated))[0]!.rows[0]!; + + test('equal counts across runtimes derive null', () => { + assert.isNull(derive_row({ deno: 767, node: 767, bun: 767 }).files_iterated_mismatch); + }); + + test('unequal counts surface the raw per-runtime counts', () => { + const mismatch = { deno: 765, node: 767, bun: 767 }; + assert.deepStrictEqual(derive_row(mismatch).files_iterated_mismatch, mismatch); + }); + + test('a null count (untimed runtime) is not a mismatch by itself', () => { + assert.isNull(derive_row({ deno: null, node: 767, bun: 767 }).files_iterated_mismatch); }); }); diff --git a/src/test/benchmarks_cli.test.ts b/src/test/benchmarks_cli.test.ts new file mode 100644 index 0000000..04af809 --- /dev/null +++ b/src/test/benchmarks_cli.test.ts @@ -0,0 +1,106 @@ +import { assert, describe, test } from 'vitest'; + +import { benchmarks_formatters_json } from '$routes/docs/benchmarks/benchmarks_formatters.ts'; +import { + benchmarks_cli, + CLI_SCENARIO_KEYS, + CLI_TS_REPO_KEY +} from '$routes/docs/benchmarks/benchmarks_cli.ts'; + +// Shape gate for the CLI report `benchmarks_cli.ts` derives from the generated +// `benchmarks_formatters.json`. The generator parses prose, so a drifted heading +// or scenario id in the harness's README would silently drop a scenario or its +// `tsv` reference row and render an empty table rather than fail to typecheck. +describe('benchmarks_cli shape', () => { + test('every scenario the page has prose for resolved to generated data, in prose order', () => { + // a copy entry with no matching scenario id drops silently from the table, so + // every entry must resolve — an aborted scenario resolves too, it just + // renders its abort note instead of a table + assert.deepEqual( + benchmarks_cli.scenarios.map((s) => s.key), + CLI_SCENARIO_KEYS + ); + }); + + test('the TypeScript-repo scenario the prose quotes resolved to generated data', () => { + assert.ok( + benchmarks_cli.scenarios.some((s) => s.key === CLI_TS_REPO_KEY), + `no generated scenario has id "${CLI_TS_REPO_KEY}" — did the harness rename it?` + ); + }); + + test('every timed scenario carries a tsv reference row with positive metrics', () => { + assert.isAtLeast(benchmarks_cli.scenarios.length, 1); + for (const scenario of benchmarks_cli.scenarios) { + // an aborted scenario carries its note; one aborted before timing has no + // rows to anchor, one aborted in its memory pass keeps its timed rows + if (scenario.aborted !== undefined) { + assert.isNotEmpty(scenario.aborted, `${scenario.key} aborted without a note`); + if (scenario.results.length === 0) continue; + } + // BenchmarksCli anchors every ratio on the tsv row; without it the table is empty + assert.ok( + scenario.results.some((r) => r.label === 'tsv'), + `${scenario.key} is missing its tsv row` + ); + for (const r of scenario.results) { + assert.isAbove(r.wall_ms, 0, `${scenario.key}/${r.label} wall_ms`); + assert.isAbove(r.cpu_ms, 0, `${scenario.key}/${r.label} cpu_ms`); + // null is legal (a harness run without GNU time measures no memory), 0 is not + assert.ok(r.memory_mb === null || r.memory_mb > 0, `${scenario.key}/${r.label} memory_mb`); + } + } + }); + + test('the derived wall-clock ratios agree with hyperfine own summary', () => { + // The generated report carries the harness's own `Summary` ratios beside the + // raw timings. Recomputing them from the timings and comparing catches a + // misparse that would otherwise render plausible-but-wrong numbers. + for (const scenario of benchmarks_formatters_json.scenarios) { + if (scenario.timings.length === 0) { + // aborted before timing: no baseline and nothing to cross-check + assert.isDefined(scenario.aborted, `${scenario.id} has no timings and no abort`); + assert.isEmpty(scenario.speedups, `${scenario.id} aborted but carries speedups`); + continue; + } + assert.strictEqual(scenario.baseline, 'tsv', `${scenario.id} baseline`); + // from the raw timings, not the rendered report: the generated data also + // carries tsv scenarios the page has no copy for, and their numbers must + // parse just as soundly + const tsv = scenario.timings.find((t) => t.name === 'tsv'); + assert(tsv, `${scenario.id} has no tsv timing row`); + for (const speedup of scenario.speedups) { + const other = scenario.timings.find((t) => t.name === speedup.name); + assert(other, `${scenario.id}/${speedup.name} has no timing row`); + const derived = other.mean_ms / tsv.mean_ms; + // hyperfine derives its summary from full-precision means but prints the + // timings rounded, so recomputing from the printed numbers lands within a + // fraction of a percent — wide enough for that, far too tight to hide a + // misparse (a wrong unit would be off by 1000x) + assert.closeTo( + derived, + speedup.ratio, + speedup.ratio * 0.01, + `${scenario.id}/${speedup.name}` + ); + } + } + }); + + test('every formatter accepts the whole corpus in every timed scenario, as the page claims', () => { + // The page's notes say the preflight parse check found nothing rejected, so + // no formatter is credited for skipping files. That's a claim about the data — + // for the scenarios that were timed. An aborted one is the harness saying the + // opposite, and the page shows it as an abort, so its rows are exempt. + for (const scenario of benchmarks_formatters_json.scenarios) { + assert.isNotEmpty(scenario.preflight, `${scenario.id} ran no preflight`); + // aborted before timing — its rows are the fault, shown as such + if (scenario.timings.length === 0) continue; + for (const entry of scenario.preflight) { + assert.isFalse(entry.crashed, `${scenario.id}/${entry.name} crashed during its check`); + assert.strictEqual(entry.rejected, 0, `${scenario.id}/${entry.name} rejected files`); + assert.isFalse(entry.unavailable, `${scenario.id}/${entry.name} never launched`); + } + } + }); +}); diff --git a/src/test/formatter_benchmark_data.test.ts b/src/test/formatter_benchmark_data.test.ts new file mode 100644 index 0000000..e4e37cf --- /dev/null +++ b/src/test/formatter_benchmark_data.test.ts @@ -0,0 +1,284 @@ +import { assert, describe, test } from 'vitest'; + +import { parse_formatter_benchmarks } from '$routes/docs/benchmarks/formatter_benchmark_data.ts'; + +// A trimmed stand-in for the bench harness's README — one scenario tsv runs in, +// one it sits out, plus the versions and machine lines below the results block. +const readme = `# Benchmark + + + +\`\`\` +========================================= +Benchmarking Large Single File +========================================= + +Target: TypeScript compiler parser.ts (~540KB) +- 2 warmup runs, 5 benchmark runs +- Copy original before each run + + +Preflight (per-formatter parse check): + biome: clean + oxfmt: 3 rejected + tsv: unavailable (command failed to launch) +Benchmark 1: biome + Time (mean ± σ): 1.597 s ± 0.015 s [User: 1.583 s, System: 0.846 s] + Range (min … max): 1.581 s … 1.614 s 5 runs + +Benchmark 2: tsv + Time (mean ± σ): 28.8 ms ± 0.7 ms [User: 15.2 ms, System: 13.5 ms] + Range (min … max): 27.8 ms … 29.5 ms 5 runs + +Summary + tsv ran + 55.41 ± 1.36 times faster than biome + +Memory Usage: + biome: 303.2 MB (min: 291.5 MB, max: 319.4 MB, 13.12 ± 0.62 times more than tsv) + tsv: 23.1 MB (min: 22.5 MB, max: 23.9 MB) + +Large single file benchmark complete! + + +========================================= +Benchmarking Mixed (embedded) +========================================= + +Target: Storybook repository (mixed with embedded languages) +- 1 warmup runs, 3 benchmark runs +- Git reset before each run + +Benchmark 1: oxfmt + Time (mean ± σ): 7.832 s ± 0.209 s [User: 82.370 s, System: 5.801 s] + Range (min … max): 7.622 s … 8.040 s 3 runs + +Mixed (embedded) benchmark complete! + + +========================================= +Benchmarking Svelte (tsv vs rsvelte-fmt) +========================================= + +Target: third-party .svelte corpus (kit, svelte.dev, layerchart) +- 2 warmup runs, 5 benchmark runs +- Git reset before each run +- .svelte only: the two Svelte-native formatters head-to-head + + +Preflight (per-formatter parse check): + tsv: clean (2226 files, 2041 would change) + rsvelte-fmt: clean (2226 files, 2023 would change) + → all formatters accept the whole corpus; nothing excluded +Benchmark 1: tsv + Time (mean ± σ): 52.3 ms ± 1.1 ms [User: 255.0 ms, System: 194.2 ms] + Range (min … max): 50.9 ms … 53.6 ms 5 runs + +Benchmark 2: rsvelte-fmt + Time (mean ± σ): 272.9 ms ± 3.9 ms [User: 1016.7 ms, System: 408.5 ms] + Range (min … max): 268.1 ms … 277.6 ms 5 runs + +Summary + tsv ran + 5.22 ± 0.13 times faster than rsvelte-fmt + + → aborting: rsvelte-fmt crashed (killed by a signal) in 2 of 5 memory runs — a crash must fail the scenario, not thin its row + + +========================================= +Benchmarking CSS (tsv vs oxfmt) +========================================= + +Target: third-party .css corpus +Corpus: 1a2b3c4 2026-09-01 +- 2 warmup runs, 5 benchmark runs +- Git reset before each run + + +Preflight (per-formatter parse check): + tsv: clean (312 files, 300 would change) + oxfmt: CRASHED during check (exit 134) + → oxfmt crashed partway through its check — its coverage is unknown and its timed runs may crash too + → aborting: this scenario would not measure every formatter on the same work + + +========================================= +All benchmarks complete! +========================================= +\`\`\` + + + +## Versions + +- **Prettier**: 3.9.1 +- **Biome**: 2.5.1 +- **rsvelte-fmt**: 0.7.4 +- **tsv**: 0.2.0 + +_Measured on: Some CPU · 12 threads · linux x64 — the ratios below depend on the core count._ +`; + +describe('parse_formatter_benchmarks', () => { + test('keeps only the scenarios tsv runs in', () => { + const parsed = parse_formatter_benchmarks(readme); + assert.deepEqual( + parsed.scenarios.map((s) => s.id), + ['large-single-file', 'svelte-tsv-vs-rsvelte-fmt', 'css-tsv-vs-oxfmt'] + ); + }); + + test('keeps a scenario the harness aborted, with its reason and no numbers', () => { + const scenario = parse_formatter_benchmarks(readme).scenarios[2]!; + assert.equal(scenario.name, 'CSS (tsv vs oxfmt)'); + assert.equal(scenario.target, 'third-party .css corpus'); + assert.equal(scenario.warmup_runs, 2); + assert.equal(scenario.benchmark_runs, 5); + assert.equal( + scenario.aborted, + 'this scenario would not measure every formatter on the same work' + ); + // the preflight rows are what say which formatter caused the abort + assert.deepEqual(scenario.preflight, [ + { name: 'tsv', rejected: 0, unavailable: false, crashed: false }, + { name: 'oxfmt', rejected: 0, unavailable: false, crashed: true } + ]); + assert.deepEqual(scenario.timings, []); + assert.equal(scenario.baseline, ''); + assert.deepEqual(scenario.speedups, []); + assert.deepEqual(scenario.memory, []); + }); + + test('drops an aborted scenario tsv was not in', () => { + // with no timing rows, the preflight is the only sign tsv was part of it + const without_tsv = readme.replace( + /^ {2}tsv: clean \(312 files, 300 would change\)$/m, + ' biome: clean (312 files, 300 would change)' + ); + assert.deepEqual( + parse_formatter_benchmarks(without_tsv).scenarios.map((s) => s.id), + ['large-single-file', 'svelte-tsv-vs-rsvelte-fmt'] + ); + }); + + test('parses the machine and versions from below the results block', () => { + const parsed = parse_formatter_benchmarks(readme); + assert.equal(parsed.machine, 'Some CPU · 12 threads · linux x64'); + assert.deepEqual(parsed.versions, { + prettier: '3.9.1', + biome: '2.5.1', + 'rsvelte-fmt': '0.7.4', + tsv: '0.2.0' + }); + }); + + test('normalizes timings to milliseconds across units', () => { + const parsed = parse_formatter_benchmarks(readme); + const [biome, tsv] = parsed.scenarios[0]!.timings; + assert.deepEqual(biome, { + name: 'biome', + mean_ms: 1597, + stddev_ms: 15, + user_ms: 1583, + system_ms: 846, + min_ms: 1581, + max_ms: 1614 + }); + assert.equal(tsv!.mean_ms, 28.8); + }); + + test('parses the scenario header, preflight, speedups, and memory', () => { + const scenario = parse_formatter_benchmarks(readme).scenarios[0]!; + assert.equal(scenario.name, 'Large Single File'); + assert.equal(scenario.target, 'TypeScript compiler parser.ts (~540KB)'); + assert.equal(scenario.warmup_runs, 2); + assert.equal(scenario.benchmark_runs, 5); + assert.deepEqual(scenario.preflight, [ + { name: 'biome', rejected: 0, unavailable: false, crashed: false }, + { name: 'oxfmt', rejected: 3, unavailable: false, crashed: false }, + { name: 'tsv', rejected: 0, unavailable: true, crashed: false } + ]); + assert.equal(scenario.baseline, 'tsv'); + assert.deepEqual(scenario.speedups, [{ name: 'biome', ratio: 55.41, ratio_stddev: 1.36 }]); + // the lowest-memory formatter is the ratio baseline, so it carries no ratio + assert.deepEqual(scenario.memory, [ + { + name: 'biome', + mean_mb: 303.2, + min_mb: 291.5, + max_mb: 319.4, + ratio: 13.12, + ratio_stddev: 0.62 + }, + { name: 'tsv', mean_mb: 23.1, min_mb: 22.5, max_mb: 23.9 } + ]); + }); + + test('parses a two-formatter scenario aborted in its memory pass, keeping its timings', () => { + const scenario = parse_formatter_benchmarks(readme).scenarios[1]!; + assert.equal(scenario.name, 'Svelte (tsv vs rsvelte-fmt)'); + assert.deepEqual(scenario.preflight, [ + { name: 'tsv', rejected: 0, unavailable: false, crashed: false }, + { name: 'rsvelte-fmt', rejected: 0, unavailable: false, crashed: false } + ]); + assert.deepEqual( + scenario.timings.map((t) => t.name), + ['tsv', 'rsvelte-fmt'] + ); + assert.equal(scenario.baseline, 'tsv'); + assert.deepEqual(scenario.speedups, [{ name: 'rsvelte-fmt', ratio: 5.22, ratio_stddev: 0.13 }]); + // the harness stopped before its memory table, and said so + assert.equal( + scenario.aborted, + 'rsvelte-fmt crashed (killed by a signal) in 2 of 5 memory runs — a crash must fail the scenario, not thin its row' + ); + assert.deepEqual(scenario.memory, []); + }); + + // A README that's present but drifted must fail the gen task rather than quietly + // publishing stale or scenario-stripped numbers — only a MISSING readme is benign, + // and that's the caller's call, not the parser's. + test('throws when the results markers are missing', () => { + assert.throws( + () => parse_formatter_benchmarks('# Benchmark\n\nno results here\n'), + /no
Svelte
{count_label('svelte')} files
TypeScript
{count_label('typescript')} files
CSS
{count_label('css')} files
SvelteTypeScriptCSS