diff --git a/CLAUDE.md b/CLAUDE.md index 63db0bc..e44b93e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ > website for tsv, precise language tools for TypeScript/JS, CSS, and Svelte in Rust -tsv.fuz.dev is the public website for the tsv tool — landing page, benchmarks, docs, and an interactive playground. Built with SvelteKit + fuz stack, statically deployed. +tsv.fuz.dev is the public website for the tsv tool — landing page, benchmarks, parse conformance, docs, and an interactive playground. Built with SvelteKit + fuz stack, statically deployed. For coding conventions, see Skill(fuz-stack). @@ -17,96 +17,176 @@ gro deploy # build, commit, and push to deploy branch gro sync # regenerate files and run svelte-kit sync ``` -IMPORTANT for AI agents: Do NOT run `gro dev` - the developer will manage the dev server. - ## Key dependencies - 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 ~1MB-gzipped wasm (~2.5MB decoded) 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 ~900KB WASM 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`). Of those only `zod` is imported by this app's own source — `formatter_benchmark_data.ts`'s schemas, read at gen and test time and as erased types by the page, so it never reaches the client bundle — so don't mistake the rest for dead deps. Likewise `esm-env`, `@types/estree`, and `@types/node` are peers of `@fuzdev/fuz_util` (the latter two optional), and `esm-env` and `@types/estree` optional peers of `@fuzdev/mdz` and `esm-env` of `@fuzdev/fuz_ui`, and `tslib` backs `tsconfig.json`'s `importHelpers` — none is imported here directly either. -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: `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 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 +- Benchmarks page with bar charts and tables, and a language-support matrix of the tools compared +- Docs section (introduction, playground, benchmarks, conformance) +- Conformance page with per-corpus-source parse-coverage tables over deliberately hard corpora, and a `Test corpus` section on how each source was chosen +- Interactive playground (`/docs/playground`) — edit a deliberately-unformatted Svelte example in a syntax-highlighted editor (fuz_code's `CodeTextarea`); the formatted output below it updates live and the parsed AST follows on a short idle; 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: the `fuzdev.tsv-format` VS Code extension, then 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 introduction links tsv's GitHub Releases, which attach the CLI binaries, rather than hosting them) ## 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 +│ ├── style.css # global styles (currently empty of rules) │ ├── 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) +│ ├── +layout.svelte # Docs layout (Docs wrapper; sets library_context and the per-page title) │ ├── +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.) +│ ├── tomes.ts # Docs structure (introduction, playground, benchmarks, conformance) +│ ├── introduction/ # Introduction page (install + usage; code samples in introduction_examples.ts) +│ ├── playground/ # Interactive playground (Playground.svelte + playground_example.ts; lazy @fuzdev/tsv-wasm) +│ ├── conformance/ # Parse-conformance page: its report (conformance.json + conformance.ts), conformance_data.ts, ConformanceTable.svelte; builds on the benchmarks modules +│ └── benchmarks/ # Benchmarks page: the three JSON reports, the benchmark_*.ts / benchmarks_*.ts modules, 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 per-runtime derivations (groups, stability, corpus, sweeps) + ├── benchmark_data.shape.test.ts # shape gates over the committed per-runtime report + ├── benchmark_data.prose.test.ts # gates the ratio, direction, and count claims the benchmarks page's prose quotes + ├── conformance_data.test.ts # unit tests for the conformance grouping and per-source matrices + ├── conformance_data.shape.test.ts # shape gates over the committed conformance report + ├── conformance_data.prose.test.ts # gates the claims the conformance page's prose quotes + ├── benchmark_sizes.test.ts # unit tests for the binary-size target and capability grouping + ├── benchmark_sizes.shape.test.ts # shape gates over the committed report's binary sizes, and the tldr's like-for-like size claim + ├── benchmark_tools.test.ts # unit tests for the language-support matrix's timed marks + ├── benchmark_tools.shape.test.ts # gates the matrix's rows against the reports: every timed row and CLI row listed, every timed capability stated, every CLI scenario mapped to a column + ├── benchmark_cross_runtime.test.ts # unit tests for the combined-report derivations + ├── benchmark_cross_runtime.shape.test.ts # shape gates over the committed combined report + ├── benchmark_display.test.ts # unit tests for the value formatters and row labels + ├── benchmark_baseline.test.ts # unit tests for the hover-to-rebaseline ratio math + ├── benchmarks_cli.test.ts # unit tests for the CLI report shaping and claim helpers + ├── benchmarks_cli.shape.test.ts # shape gates over the CLI-harness data as the page consumes it, and its machine and versions against the in-process report's + ├── benchmark_test_helpers.ts # fixture factories the benchmark tests share + └── formatter_benchmark_data.test.ts # the harness report's validation ``` ## Benchmarks -Benchmark data comes from `tsv`. Full workflow to update: +Four JSON reports, three copied verbatim from `tsv` and one generated from the +sibling CLI harness. Three live in `src/routes/docs/benchmarks/`; the conformance +report lives with its page in `src/routes/docs/conformance/`, which shares the +per-runtime report types and the display helpers. + +| committed file | source | +| --- | --- | +| `benchmarks.json` | tsv's `benches/js/results/report.node.json` (per-runtime Node report) | +| `benchmarks_cross_runtime.json` | tsv's composed `report.json` | +| `../conformance/conformance.json` | tsv's `report.conformance.node.json` (parse coverage) | +| `benchmarks_formatters.json` | `gro gen`, from ../oxc-bench-formatter's `results.json` | + +### Refreshing ```bash -# 1. In ~/dev/tsv — run benchmarks (builds ffi + wasm automatically) +# tsv reports: in ~/dev/tsv (runs deno/node/bun, builds artifacts), then here deno task bench - -# 2. In ~/dev/tsv.fuz.dev — copy the latest result 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`. - -Key files in `src/routes/docs/benchmarks/`: +# CLI harness: in ~/dev/oxc-bench-formatter (rewrites its README and results.json, +# timing the npm-installed @fuzdev/tsv its lockfile pins), then here +pnpm run update-readme +gro gen +``` -- `benchmarks.json` — raw benchmark data (copied from tsv) -- `benchmark_data.ts` — TypeScript types matching the JSON format -- `benchmarks.ts` — re-exports the JSON with types -- `BenchmarksBar.svelte`, `BenchmarksGroup.svelte`, etc. — visualization components +`update-benchmarks` is three `cp`s hardcoded to `../tsv` — if the reports were +generated in a different worktree, copy them into `~/dev/tsv` (or copy manually) +first. tsv writes them tab-indented and Gro's formatter leaves JSON untouched, so +the copies stay byte-identical and the `*.shape.test.ts` files gate their shape. +Those tests pin each report's `version` exactly, so the types in +`benchmark_data.ts` and `benchmark_cross_runtime.ts` describe the current report +only — re-pin and update the types together on a bump. + +`benchmarks_formatters.gen.json.ts` validates the harness's `results.json` +against `formatter_benchmark_data.ts`'s Zod schemas and keeps only the scenarios +tsv participates in (it has no JSX/TSX parser). A **missing** sibling checkout is +the one tolerated case — generation is skipped and the committed JSON stands, so +`gro gen --check` passes on CI, which never checks out the harness. A report +that **is** present but doesn't validate fails the task loudly, naming its path. +A drift that still validates but renames a scenario is caught on the site side: +every key in `benchmarks_cli.ts`'s `SCENARIO_COPY` must resolve to generated +data, and a test asserts it. + +The page states the machine and tool versions once, in the Benchmarking details +section, from tsv's report. `benchmarks_cli.shape.test.ts` holds the harness's +own `machine` and `versions` to it (CPU, OS/arch, Node, and every tool both +time), so refresh the two on the same setup — a one-sided refresh fails there. + +### The CLI comparison + +The harness is a fork of Oxc's `bench-formatter` that adds tsv, benching it +against Prettier, Biome, Oxfmt, and (on Svelte) rsvelte-fmt, all timed through +their npm bins. tsv gets two rows wherever it faces another tool: `tsv-npm`, the +`@fuzdev/tsv` Node dispatcher — the like-for-like row, never a competitor — and +the bare `tsv` binary. The page's headline CLI claims lead with the dispatcher +ratios and have no fallback for a report without that row; the prose test +requires them to resolve. A tsv-only delivery scenario (binary vs dispatcher vs +`@fuzdev/tsv-wasm`) is flagged `tsv_only` so "every other tool" claims skip it. +A scenario renders only once it has a `SCENARIO_COPY` entry, and prose about the +Svelte head-to-head is conditional on its data, since the harness publishes that +scenario aborted when rsvelte-fmt's preflight crashes. Field-level detail lives +in the schema TSDoc in `formatter_benchmark_data.ts`. + +### Modules + +- `benchmark_data.ts` — per-runtime report types, plus the format/parse, stability, and corpus derivations; imports none of the others +- `BenchmarksCorpus.svelte` — the per-source corpus table both pages' corpus sections render (`Corpus` on the benchmarks page, `Test corpus` on the conformance page), from `benchmark_data.ts`'s `derive_corpus_source_table`; a source no repo names needs a hand label (`CORPUS_SOURCE_LABELS`), and the conformance page hand-labels every source (`CONFORMANCE_SOURCE_LABELS`, which its matrix also reads); the shape test holds both +- `benchmark_display.ts` — value formatters, row labels, per-category colors; imports only `benchmark_data.ts`'s types +- `benchmark_tools.ts` + `BenchmarksTools.svelte` — the language-support matrix: each tool's parse and format support per language, stated by hand (`TOOL_SUPPORT`, verified against the tools' docs and installed packages), with the cells the page times derived from the in-process and CLI reports; the shape test holds the two together +- `benchmark_sizes.ts` (the binary sizes split by target — browser: wasm and JS; native — then grouped by capability), `benchmark_cross_runtime.ts` — one domain each, built on `benchmark_data.ts` (the cross-runtime one on `benchmark_display.ts` too); the conformance page's `conformance_data.ts` does the same from its own directory — a matrix per language (corpus sources × engines, the aggregate as the leading row), with hand-stated maps the report has no field for: the engine names, the row notes, the source labels, `CONFORMANCE_ENGINE_VERSIONS` (the version keys each engine column lists, so the page's meta panel skips the report's formatters), and `CONFORMANCE_SELECTORS`, the engine that selected a source and so reads 100% on it by construction; the shape test holds each to the report +- `benchmark_baseline.ts` + `BenchmarksBaselineGroup.svelte` — hover-to-rebaseline: hovering a row re-anchors that group's ratios, restoring the default anchor on leave; pointer-only by design, and a disabled row never anchors. `BenchmarksCli.svelte`'s tables behave the same way +- `formatter_benchmark_data.ts` — the harness report's Zod schemas and `parse_formatter_benchmarks` +- `benchmarks_cli.ts` — shapes the CLI report for `BenchmarksCli.svelte` and owns the per-scenario prose and the `cli_*` claim helpers +- `benchmarks_prose.ts` — `IN_PROCESS_PAIRS`, the in-process pairings the copy names; the page reads it by key and the prose test iterates it, so a pairing added to the copy is gated by construction +- `benchmarks.ts`, `benchmarks_cross_runtime.ts`, `benchmarks_formatters.ts` (and the conformance page's `conformance.ts`) — re-export the JSON with types +- `BenchmarksCliSection.svelte` — the CLI section's prose and its claims; the other `Benchmarks*.svelte` are visualizations +- `benchmarks.css` — classes the components share, imported by the benchmarks and conformance `+page.svelte`s rather than the root stylesheet so they ship with those routes only + +The page quotes no hand-written ratios or counts from the reports: its prose computes them from +the same reports the charts render, through tested helpers in the TS modules, so +the components' scripts hold no untested reductions. ## 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)) +- `docs/tomes.ts` defines the doc sections: introduction, playground, benchmarks, conformance +- Benchmark data, modules, and the refresh workflow: see [Benchmarks](#benchmarks) +- Tests import route modules through the `$routes` alias (`svelte.config.js`), not a `#routes/*` subpath import — this repo has no `package.json` `imports` map; routes import each other relatively +- The `/docs` index renders every tome's component through `DocsContent`, so the playground, benchmarks, and conformance tomes check `at_root` (`page.url.pathname === DOCS_PATH`) and render only a one-line link there, keeping the wasm, the charts, and their section ids off the index - `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, 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 formatted pane recomputes on every keystroke (formatting is ~1 ms even on a 9 KB component) while the AST pane trails a ~150 ms idle: the AST is parsed, serialized, and syntax-highlighted — around half a megabyte of it for that same component — and rebuilding that DOM per keystroke is what a large paste feels. The top-level error is the live pane's; the AST pane renders its own, since its debounced source can still be the broken text the editor has moved past - 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,9 +197,9 @@ 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) +- Not published to npm ## Related projects 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 7d8db9c..b47185a 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.64.0", + "@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.70.1", - "@sveltejs/vite-plugin-svelte": "^7.1.2", + "@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.64.0", + "resolved": "https://registry.npmjs.org/@fuzdev/fuz_css/-/fuz_css-0.64.0.tgz", + "integrity": "sha512-y6XgF3TTRr+ppn7qaB+LsXbpA7KuIreFU5Ss9K51Z14hc6GihKNUYq0QXvz2WKuiAK0HIHcwUmZEu9c3IYEr+A==", "dev": true, "license": "MIT", "engines": { @@ -763,8 +737,8 @@ "url": "https://www.ryanatkn.com/funding" }, "peerDependencies": { - "@fuzdev/blake3_wasm": "^0.1.0", - "@fuzdev/fuz_util": ">=0.65.1", + "@fuzdev/blake3-wasm": "^0.2.0", + "@fuzdev/fuz_util": ">=0.65.2", "@fuzdev/gro": ">=0.203.0", "@sveltejs/acorn-typescript": "^1", "@webref/css": "^8", @@ -774,7 +748,7 @@ "zod": "^4" }, "peerDependenciesMeta": { - "@fuzdev/blake3_wasm": { + "@fuzdev/blake3-wasm": { "optional": true }, "@fuzdev/gro": { @@ -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.1", + "resolved": "https://registry.npmjs.org/@fuzdev/tsv/-/tsv-0.4.1.tgz", + "integrity": "sha512-SX66ib7UwQZa88URZmy+OAwpJ4g4xrTBBsISlyvfnHrE5T/R3HUoEZ6CWlmxrKp/CD1haMQqVasziv7TGM+eWQ==", + "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.1", + "@fuzdev/tsv-darwin-x64": "0.4.1", + "@fuzdev/tsv-linux-arm64-gnu": "0.4.1", + "@fuzdev/tsv-linux-x64-gnu": "0.4.1", + "@fuzdev/tsv-linux-x64-musl": "0.4.1", + "@fuzdev/tsv-win32-x64": "0.4.1" + } + }, + "node_modules/@fuzdev/tsv-darwin-arm64": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@fuzdev/tsv-darwin-arm64/-/tsv-darwin-arm64-0.4.1.tgz", + "integrity": "sha512-96TWaF4yEEdM6kuWK7YpqnSSCSIBqHprkE68d7h1PNKm0MURy+zcQYA/WdB+sn4gemyxud8lPhP8aFusTHVpgA==", + "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.1", + "resolved": "https://registry.npmjs.org/@fuzdev/tsv-darwin-x64/-/tsv-darwin-x64-0.4.1.tgz", + "integrity": "sha512-/GUCgmVVehEU8lOx0nmUFkPmHoaNxc1uQQiW1wmcFXKlMZtiTg2vk0+P4n/WEYSY987MUnOTZOJzQDGl4x9f0g==", + "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.1", + "resolved": "https://registry.npmjs.org/@fuzdev/tsv-linux-arm64-gnu/-/tsv-linux-arm64-gnu-0.4.1.tgz", + "integrity": "sha512-Se/z1sLPU7bbcO0FRw86mPRhdCQz6JiWOmKP2jQylj+mQd2qitKpQ3EvagaPWDPUd9IWAQHX+nEQ5h+wFrahrQ==", + "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.1", + "resolved": "https://registry.npmjs.org/@fuzdev/tsv-linux-x64-gnu/-/tsv-linux-x64-gnu-0.4.1.tgz", + "integrity": "sha512-s0+HxoL8t7iB1jGfmhd+696LLflHCf2UJ+2QkMpWjZfHn/d3OX02c39vYCfqfOd/rRVY9SyyWlg2cs/yTzHCwg==", + "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.1", + "resolved": "https://registry.npmjs.org/@fuzdev/tsv-linux-x64-musl/-/tsv-linux-x64-musl-0.4.1.tgz", + "integrity": "sha512-TSqmgybxzYzAdeoSwGqiIJoRgLDIkb0NxepAHuEpcMIyhJJXALH6CyeLm6H9DdPgzOxoGSUqpQm31OpeqGlllQ==", + "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.1", + "resolved": "https://registry.npmjs.org/@fuzdev/tsv-wasm/-/tsv-wasm-0.4.1.tgz", + "integrity": "sha512-2zBJflaozPi5CYLWenhAtWMyAOQHsSNCtkuFysmyT4L6ApGs9XUo1txm9rHZxD5ffY4QKT3oOZgm9tYG48kI3A==", "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.1", + "resolved": "https://registry.npmjs.org/@fuzdev/tsv-win32-x64/-/tsv-win32-x64-0.4.1.tgz", + "integrity": "sha512-BuSBLzulnxFGKLnNPDuxQwLtRmhQMvIIh6/K0BrL4wHn4B3W5DqgcRh1CGEMyzHpjTwkHYiHsvG2N5DRwzxX7Q==", + "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/@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/@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-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.70.1", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.70.1.tgz", - "integrity": "sha512-nY9SPHGOZro3doud9vZXDBwl9tCZIouuJztjgSHs6PAIrv9M/z5O7eOhPV5xU7CgVHA976Jwu3BA1hIFvXztkA==", + "version": "2.70.3", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.70.3.tgz", + "integrity": "sha512-UDvEYuZqAMbfB/oXIoqKvbKcb7YczK5zYrzmsGV1zRJk03jntwp8dXiYoIJotxAndsKvcPFtx9H1GRSKFdSHgg==", "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.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": { - "svelte": "^5.46.4", - "vite": "^8.0.0-beta.7 || ^8.0.0" + "@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 d6ca35d..1371b61 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/conformance/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.64.0", + "@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.70.1", - "@sveltejs/vite-plugin-svelte": "^7.1.2", + "@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/app.html b/src/app.html index e3061bc..06d95cd 100644 --- a/src/app.html +++ b/src/app.html @@ -11,7 +11,7 @@ ((c) => c === 'dark' || (c !== 'light' && matchMedia('(prefers-color-scheme:dark)').matches) ? 'dark' - : 'light')(localStorage.getItem('fuz:color-scheme')), + : 'light')(localStorage.getItem('fuz:color-scheme')) ); 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..42f0a57 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,12 +19,16 @@ // 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' }) ); tsv.fuz.dev + 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..a309265 100644 --- a/src/routes/docs/+layout.svelte +++ b/src/routes/docs/+layout.svelte @@ -1,21 +1,33 @@ + + {title_slug} - tsv.fuz.dev + + {@render children()} diff --git a/src/routes/docs/+page.svelte b/src/routes/docs/+page.svelte index 16a118e..735443a 100644 --- a/src/routes/docs/+page.svelte +++ b/src/routes/docs/+page.svelte @@ -1,7 +1,7 @@ -
-

- 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 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: -

- - -
- - - - - - - - -

- 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): -

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

- 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. -

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

- 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. -

-

- 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). -

- -
+
+

Each source links its upstream at the commit the snapshot vendored.

+ +
+ + {/if}
- - diff --git a/src/routes/docs/benchmarks/BenchmarksBar.svelte b/src/routes/docs/benchmarks/BenchmarksBar.svelte index 128c7b5..dadbab3 100644 --- a/src/routes/docs/benchmarks/BenchmarksBar.svelte +++ b/src/routes/docs/benchmarks/BenchmarksBar.svelte @@ -1,10 +1,6 @@ -
- {format_label(label)} -
-
+ +
+ {display_label} + - {value.value} {value.unit} + + {#if disabled} + {coverage_only ? 'not timed' : 'n/a'} + {:else} + {value.value} {value.unit} + {/if} + {#if annotation != null} - {annotation} + {annotation} {/if} - {ratio_text} + + {#if !disabled}{ratio_text}{/if} +
diff --git a/src/routes/docs/benchmarks/BenchmarksBaselineGroup.svelte b/src/routes/docs/benchmarks/BenchmarksBaselineGroup.svelte new file mode 100644 index 0000000..235a72b --- /dev/null +++ b/src/routes/docs/benchmarks/BenchmarksBaselineGroup.svelte @@ -0,0 +1,86 @@ + + + +
(hovered_key = to_baseline_key(event))} + onpointerleave={() => (hovered_key = undefined)} +> + {#each rows as row (row.key)} + {@const ratio = + !row.disabled && anchor_row && row.key !== anchor_key + ? compute_baseline_ratio(row.raw, anchor_row.raw) + : undefined} + + {/each} +
+ + diff --git a/src/routes/docs/benchmarks/BenchmarksCli.svelte b/src/routes/docs/benchmarks/BenchmarksCli.svelte new file mode 100644 index 0000000..d872128 --- /dev/null +++ b/src/routes/docs/benchmarks/BenchmarksCli.svelte @@ -0,0 +1,183 @@ + + +{#each report.scenarios as scenario (scenario.key)} + {@const anchor_label = to_anchor_label(scenario)} + {@const rows = to_rows(scenario, anchor_label)} +
+

{scenario.heading}: {scenario.target}

+

{scenario.description}

+ {#if rows.length > 0} +
+ + + + + + + + + + + + + (hovered = to_hovered(event, scenario.key))} + onpointerleave={() => (hovered = undefined)} + > + {#each rows as row (row.result.label)} + + + + + + + + + {/each} + +
formattertimetime ratioCPU ratiopeak RSSRSS ratio
{row.result.label}{format_ms(row.result.wall_ms)} + {format_cell(row, row.wall_ratio, true)} + + {format_cell(row, row.cpu_ratio, true)} + {format_mib(row.result.memory_mb)} + {format_cell(row, row.memory_ratio, row.result.memory_mb != null)} +
+
+ {/if} + +

+ + Corpus: + {format_cli_corpus(scenario.corpus)}{scenario.files + ? `, ${format_count(scenario.files)} files` + : ''}. + {#if rows.length > 0 && scenario.benchmark_runs > 0} + Each time is the mean of {scenario.benchmark_runs} runs after {scenario.warmup_runs} + warmups{to_settle_note(scenario)}. + {/if} + +

+ {#if scenario.unshimmed} +

{scenario.unshimmed}

+ {/if} + {#if scenario.aborted} +

{scenario.aborted}{scenario.abort_context ? ` ${scenario.abort_context}` : ''}

+ {/if} +
+{/each} + + diff --git a/src/routes/docs/benchmarks/BenchmarksCliSection.svelte b/src/routes/docs/benchmarks/BenchmarksCliSection.svelte new file mode 100644 index 0000000..4f43e9a --- /dev/null +++ b/src/routes/docs/benchmarks/BenchmarksCliSection.svelte @@ -0,0 +1,201 @@ + + + + +

+ The numbers above time tsv's engine in-process, one file at a time. This section comes from a + fork of Oxc's own + + bench-formatter + + that adds tsv. It + times 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. + Upstream's other three scenarios are left out: their corpora include JSX, which tsv doesn't + parse, and two also sort imports and Tailwind classes or format languages beyond tsv's three. + Every formatter is installed from npm, pinned by the fork's lockfile, and the other tools are + timed through their packages' Node bins. Against them, tsv has two rows. + {CLI_TSV_NPM_LABEL} is the like-for-like one: the Node bin of + @fuzdev/tsv, what + npx tsv runs once it's installed, launching the native binary as Biome's and + rsvelte-fmt's bins do. The plain tsv row runs the binary directly from the platform + package, skipping Node: what the binary costs on its own. +

+

+ Each table's ratios are against its highlighted row — {CLI_TSV_NPM_LABEL} where tsv + faces other tools, the bare binary in the tsv-only table — and a minus means that many times + worse; hover another row to re-anchor them. +

+ + +

+ This section ran on {cli_date}, from + + + its harness at {format_commit(benchmarks_cli.git_commit)} + {benchmarks_cli.git_dirty ? ' with uncommitted changes' : ''}, on the same machine and Node + as the in-process runs ({benchmarks_cli.machine.threads} threads, which its multi-file + wall-clock scales with). The versions of the tools both sections time are listed under + {details_title}; + {#if cli_tsv_binary.source === 'package'} + its native tsv rows run the {cli_tsv_binary.package} binary, + {:else} + its native tsv rows run a local + build{cli_tsv_binary.built ? ` from ${cli_tsv_binary.built}` : ''}, + {/if} + and its wasm row @fuzdev/tsv-wasm {cli_tsv_wasm_version}. + {#if cli_plugin_oxc_version} + Its prettier + oxc-parser row runs @prettier/plugin-oxc + {cli_plugin_oxc_version}, with its own oxc-parser, not the release listed there. + {/if} +

+
diff --git a/src/routes/docs/benchmarks/BenchmarksCorpus.svelte b/src/routes/docs/benchmarks/BenchmarksCorpus.svelte new file mode 100644 index 0000000..1d26715 --- /dev/null +++ b/src/routes/docs/benchmarks/BenchmarksCorpus.svelte @@ -0,0 +1,62 @@ + + +{#snippet count_cell(count: number | undefined)} + + {count === undefined ? '—' : format_count(count)} + +{/snippet} + +
+ + + + + + + {#each table.languages as language (language)} + + {/each} + + + + + + + + {@render count_cell(table.totals.files)} + {#each table.totals.by_language as count, i (table.languages[i])} + {@render count_cell(count)} + {/each} + + {#each table.rows as row (row.path)} + + + + {@render count_cell(row.files)} + {#each row.by_language as count, i (table.languages[i])} + {@render count_cell(count)} + {/each} + + {/each} + +
sourcecommitfiles{format_language(language)}
all sources
+ {#if row.url} + + {row.label} + {:else} + {row.label} + {/if} + {#if row.subpath}{row.subpath}{/if} + + {#if row.commit}{row.commit}{/if} +
+
diff --git a/src/routes/docs/benchmarks/BenchmarksCrossRuntime.svelte b/src/routes/docs/benchmarks/BenchmarksCrossRuntime.svelte new file mode 100644 index 0000000..6fd8d76 --- /dev/null +++ b/src/routes/docs/benchmarks/BenchmarksCrossRuntime.svelte @@ -0,0 +1,225 @@ + + +{#if report.mixed_machine} + +{/if} +{#if unavailable.length} + +{/if} +{#if unstable.length} + +{/if} +

+ Time per sweep — one sweep is a full pass over the group's timed file set, as in the charts above; + ratios are vs {base}, negative when slower than it. The other tools' + native rows are their npm N-API addons under all three runtimes. + {#if has_fail} + 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. + {/if} +

+

+ A ≈ marks a delta inside the two measurements' combined noise, which reads as parity. + The slowest rows have too few timings for that check, so an unmarked delta there isn't necessarily + an effect, and the check can't see variance between whole runs: Bun's allocation-heavy JS rows + (Prettier, PostCSS) have sat at two levels 10% or more apart. +

+{#if runtime_versions.length} +
    + {#each runtime_versions as { runtime, version } (runtime)} +
  • {runtime} {version}
  • + {/each} +
+{/if} +{#each groups as group (group.group)} +
+

{format_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 mean = row.mean_ns[runtime]} + {@const cell_unstable = is_cell_unstable(report, group.group, row.name, runtime)} + + {/each} + {#each others as runtime (runtime)} + {@const ratio = row.ratio_vs_base[runtime]} + {@const within_noise = + ratio != null && + base != null && + is_ratio_within_noise(report, group.group, row.name, base, runtime)} + + {/each} + + {/each} + +
implementation{runtime}{runtime} vs {base}
+ + {format_cross_runtime_label(row.name)} + + {format_mean(mean)}{cell_unstable ? ' ⚠' : ''} + + {ratio != null ? `${within_noise ? '≈' : ''}${format_speedup(ratio)}` : 'fail'} +
+
+
+{/each} + + diff --git a/src/routes/docs/benchmarks/BenchmarksGroup.svelte b/src/routes/docs/benchmarks/BenchmarksGroup.svelte index d3e2cb8..4e4a9c1 100644 --- a/src/routes/docs/benchmarks/BenchmarksGroup.svelte +++ b/src/routes/docs/benchmarks/BenchmarksGroup.svelte @@ -1,59 +1,49 @@
-

- - {group.operation === 'format' ? 'Formatting' : 'Parsing'} - {count_label} - {group.language} files - {#if has_coverage}files handled / total  ·  speed{/if} -

-
- {#each group.entries as entry (entry.name)} - - {/each} -
+

+ {group.operation === 'format' ? 'Formatting' : 'Parsing'} + {count_label} + {format_language(group.language)} files +

+ + {#if group.omissions} +

{format_group_omissions(group.omissions)}

+ {/if}
diff --git a/src/routes/docs/benchmarks/BenchmarksMeta.svelte b/src/routes/docs/benchmarks/BenchmarksMeta.svelte index 596ee77..a54f659 100644 --- a/src/routes/docs/benchmarks/BenchmarksMeta.svelte +++ b/src/routes/docs/benchmarks/BenchmarksMeta.svelte @@ -1,76 +1,76 @@ -
-
-

corpus

-
    - {#each Object.entries(baseline.corpus) as [lang, count] (lang)} -
  • {lang}: {count} file{count !== 1 ? 's' : ''}
  • +
    +
    +

    versions

    +
      + {#each versions as [key, version] (key)} +
    • {format_version_label(key)} {version}
    • {/each}
    -
    -

    versions

    -
      -
    • svelte {baseline.versions.svelte}
    • -
    • acorn {baseline.versions.acorn}
    • -
    • acorn-typescript {baseline.versions.acorn_ts}
    • -
    • prettier {baseline.versions.prettier}
    • -
    • prettier-plugin-svelte {baseline.versions.prettier_svelte}
    • -
    • oxc-parser {baseline.versions.oxc_parser}
    • -
    • oxfmt {baseline.versions.oxfmt}
    • -
    • biome {baseline.versions.biome}
    • -
    -
    -
    -

    run

    -
      +
      +

      run

      +
      +
      +

      environment

      +
        +
      • {baseline.machine.cpu_model}
      • +
      • {baseline.machine.os}/{baseline.machine.arch}
      • +
      • {baseline.runtime} {baseline.machine.runtime_version}
      • +
      +
    diff --git a/src/routes/docs/benchmarks/BenchmarksSizes.svelte b/src/routes/docs/benchmarks/BenchmarksSizes.svelte index ad9df5f..deebc08 100644 --- a/src/routes/docs/benchmarks/BenchmarksSizes.svelte +++ b/src/routes/docs/benchmarks/BenchmarksSizes.svelte @@ -1,87 +1,42 @@ -{#if wasm_display.length > 0} -
    -

    wasm

    -
    - {#each wasm_display as s (s.label)} - - {/each} -
    +{#each 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 deleted file mode 100644 index af3b40e..0000000 --- a/src/routes/docs/benchmarks/BenchmarksSummary.svelte +++ /dev/null @@ -1,77 +0,0 @@ - - -

    - 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: -

    - - - - - - - - - - - {#each rows as row (row.variant)} - {@const cells = [row.format_svelte, row.format_typescript, row.format_css]} - - - {#each cells as value, i (i)} - - {/each} - - {/each} - -
    Svelte
    {count_label('svelte')} files
    TypeScript
    {count_label('typescript')} files
    CSS
    {count_label('css')} files
    {row.variant}{value != null ? format_speedup(value) : '-'}
    - - diff --git a/src/routes/docs/benchmarks/BenchmarksTools.svelte b/src/routes/docs/benchmarks/BenchmarksTools.svelte new file mode 100644 index 0000000..b4b8a1a --- /dev/null +++ b/src/routes/docs/benchmarks/BenchmarksTools.svelte @@ -0,0 +1,113 @@ + + +
    + + + + + {#each TOOL_LANGUAGES as language (language)} + + {/each} + + + + + {#each rows as row (row.name)} + + + {#each TOOL_LANGUAGES as language (language)} + {@const cells = row.cells[language]} + + {/each} + + + {/each} + +
    tool{TOOL_LANGUAGE_LABELS[language]}other
    + {#if row.url} + + {row.name} + {:else} + {row.name} + {/if} + + {#each cells as cell, i (cell.operation)} + + {i > 0 ? '; ' : ''}{to_label(cell)} + + {:else} + none + {/each} + {row.other ?? ''}
    +
    +

    + + P parses to an AST in JS, F formats; bold is timed on this page, faded is + supported but not timed here. + {#each footnotes as footnote, i (footnote)} + {i + 1} {TOOL_FOOTNOTES[footnote]}{i < footnotes.length - 1 ? '; ' : '.'} + {/each} + +

    + + diff --git a/src/routes/docs/benchmarks/benchmark_baseline.ts b/src/routes/docs/benchmarks/benchmark_baseline.ts new file mode 100644 index 0000000..0f8ba43 --- /dev/null +++ b/src/routes/docs/benchmarks/benchmark_baseline.ts @@ -0,0 +1,67 @@ +// The hover-to-rebaseline ratios behind `BenchmarksBaselineGroup.svelte`: the +// shared row shape, the delegated hover read, and the ratio and its color scale for +// the speed (format/parse) and size groups. + +import type { ImplementationCategory } from './benchmark_data.ts'; +import type { FormattedUnit } from './benchmark_display.ts'; + +/** + * An entry's ratio against its group's anchor. Every measurement here is + * lower-is-better — a sweep's time, a build's bytes — so one formula serves the + * speed and size groups alike: above 1 is that many times faster or smaller than + * the anchor, below 1 that many times slower or bigger (`format_speedup` prints the + * latter negated). + */ +export const compute_baseline_ratio = (entry_raw: number, anchor_raw: number): number => + anchor_raw / entry_raw; + +/** Returns a CSS color variable for a baseline ratio. */ +export const baseline_ratio_color = (ratio: number): string => { + if (ratio < 0.5) return 'var(--color_c_50)'; // red — much worse + if (ratio < 1) return 'var(--color_h_50)'; // orange — worse + if (ratio < 2) return 'var(--color_e_50)'; // yellow — modest + if (ratio < 5) return 'var(--color_b_50)'; // green — better + return 'var(--color_j_50)'; // teal — exceptional +}; + +/** + * The row under the pointer's baseline key, read off its `data-baseline-key` — how + * a group or table re-baselines from one delegated `pointerover` rather than a + * handler per row. Pointer rather than mouse events: `pointerover` bubbles to the + * container as `mouseenter` wouldn't, and it brings pen and touch along, where a + * tap re-baselines the row it lands on and the lift restores the default. + * + * @returns the key, or `undefined` over anything that publishes none (the gaps + * between rows, a disabled placeholder), which restores the default anchor + */ +export const to_baseline_key = (event: Event): string | undefined => + (event.target as Element | null)?.closest('[data-baseline-key]')?.dataset + .baselineKey; + +/** + * A normalized row for `BenchmarksBaselineGroup` — the shared hover-to-rebaseline + * column behind the format, parse, and binary-size groups. `raw` is the number the + * ratio derives from (sweep mean ns for speed, bytes for size); `value` is the + * row's displayed measurement (the whole-sweep mean — total time over the group's + * iterated corpus — for speed, bytes for size). Flattening the group-specific + * display entries to this one shape lets a single component own the anchor state + * for all three sections. + */ +export interface BaselineRow { + // stable identity for `#each` and anchor matching (the entry name / size label) + key: string; + // raw name/label; `BenchmarksBar` formats it for display + label: string; + category: ImplementationCategory; + bar_fraction: number; + value: FormattedUnit; + // the number the ratio derives from — mean ns (speed) or bytes (size) + raw: number; + annotation: string | undefined; + // grayed-out, inert placeholder — never an anchor, no hover highlight + disabled: boolean; + // inert because the tool was measured for COVERAGE but deliberately never + // timed, not because it sat the group out — the bar reads `not timed` instead + // of `n/a` so the two aren't conflated. See `BenchmarkDisplayEntry.coverage_only`. + coverage_only?: boolean; +} diff --git a/src/routes/docs/benchmarks/benchmark_cross_runtime.ts b/src/routes/docs/benchmarks/benchmark_cross_runtime.ts new file mode 100644 index 0000000..65548f2 --- /dev/null +++ b/src/routes/docs/benchmarks/benchmark_cross_runtime.ts @@ -0,0 +1,344 @@ +// The combined cross-runtime report (the bench composer's `report.json`, +// `kind: 'combined'`) — its types, derivations, and display helpers. The +// per-runtime reports it composes live in `benchmark_data.ts`. + +import { + categorize_name, + compare_group_order, + parse_group_key, + type ImplementationCategory, + type Machine, + type UnavailableImpl +} from './benchmark_data.ts'; +import { format_label } from './benchmark_display.ts'; + +export type BenchmarkRuntime = 'deno' | 'node' | 'bun'; + +export interface CrossRuntimeRow { + group: string; + name: string; + ops_per_second: Partial>; + mean_ns: Partial>; + files_iterated: Partial>; +} + +export interface CrossRuntimeReport { + version: number; + kind: 'combined'; + generated: string; + runtimes: Array; + // The sibling reports came from different commits/versions — ratios are + // unreliable until every runtime is re-run. The shape tests forbid a committed + // report from setting it, so it isn't rendered. + mixed_vintage: boolean; + // The sibling reports were produced on different hardware — ratios are not + // comparable. + mixed_machine: boolean; + // Which ROWS lost their implementation to a load failure on which runtime, + // folded from the siblings' own `unavailable` lists; `[]` when every sibling + // loaded everything. + // + // This is what tells the table's two kinds of gap apart. A row with no number + // for a runtime renders the same either way, but it means one of: that runtime + // couldn't load the impl behind it (recorded here), or that sibling's report + // simply has no such row — an older run predating it, which mixed vintages make + // likely, and which `partial_rows` below names outright. + unavailable_by_runtime: Array; + // The other kind of gap: rows one sibling MEASURED that another doesn't carry at + // all, with no load failure recorded on that side to explain it — a stale + // sibling rather than a machine shortfall. `[]` when every row is present + // everywhere it should be. A runtime whose sibling predates `unavailable` + // contributes nothing (with nothing recorded, an absent row can't be told from + // an unloadable impl). Not rendered, kept for parity. + partial_rows: Array; + // Per-runtime deltas smaller than the combined measurement noise (cv) of the two + // means they divide — the cells that are NOT runtime effects, despite this + // report's subject being exactly those deltas. `[]` when every delta exceeds its + // noise. Unlike every other field here it qualifies a number the report already + // prints rather than adding one. Rendered as a `≈` on the ratio cell (see + // `is_ratio_within_noise`). + within_noise: Array; + // Per-runtime measurements that were NOT stable — a cleaned cv past 10%, a raw cv + // past 10% on a row with fewer than 30 raw timings, or a |drift| past 5% — + // collected ahead of `within_noise`'s sample gate, so a + // row measured on five timings that disagree is named rather than silenced. + // Every ratio through such a cell is unreadable. `[]` when every measurement was + // stable. Rendered as a banner over the cross-runtime tables and a `⚠` on the cell. + unstable_cells: Array; + // The conformance report's vintage beside the perf siblings'. The composer does + // not fold `report.conformance.node.json` (a coverage report, not a timing + // sibling), but this site publishes it from the same directory, and + // `mixed_vintage` above cannot see it — `stale` is that flag's perf/conformance + // analog: the conformance commit differs from some perf sibling's. `null` when + // the composer found no conformance report. The shape tests forbid a committed + // report from being stale, so it isn't rendered. + conformance_vintage: ConformanceVintage | null; + sources: Array<{ + runtime: BenchmarkRuntime; + timestamp: string; + git_commit: string | null; + tsv: string | null; + // The `fuzdev/corpora` commit that sibling's real-code corpus was read from — + // a third axis of `mixed_vintage`, since the bench reads whatever snapshot is + // checked out. `null` when the sibling predates its own `corpus_snapshot` field. + corpus_snapshot: string | null; + // The producing box's machine block. + machine: Machine | null; + // That sibling's own load failures, with reasons; `null` when the sibling + // predates the field. `unavailable_by_runtime` above is the folded view. + unavailable: Array | null; + }>; + rows: Array; +} + +/** The conformance report's provenance (see `CrossRuntimeReport.conformance_vintage`). */ +export interface ConformanceVintage { + git_commit: string | null; + timestamp: string; + tsv: string | null; + // The conformance commit differs from some perf sibling's. + stale: boolean; +} + +/** + * One runtime's load failures, named by the ROWS they cost (`bun` → `biome-wasm`, + * `oxc-parser-wasm`) — the identity the tables are keyed by, so a lookup can match. + */ +export interface RuntimeUnavailable { + runtime: BenchmarkRuntime; + rows: Array; +} + +/** + * One row a sibling measured and the runtimes in `missing` don't carry, with no + * load failure recorded there to explain it (see `CrossRuntimeReport.partial_rows`). + */ +export interface PartialRow { + group: string; + name: string; + missing: Array; +} + +/** + * One per-runtime cell whose delta is inside the two measurements' combined + * variation (see `CrossRuntimeReport.within_noise`). `delta` and `noise` are + * fractions, not percentages. + */ +export interface WithinNoiseCell { + group: string; + name: string; + // The two runtimes the cell divides, in the composer's canonical order — always + // exactly two, typed as an array rather than a pair because the report arrives + // as a JSON import, whose arrays never infer as tuples. Not rendered. + runtimes: Array; + delta: number; + noise: number; + // The two cleaned timing counts the noise band was taken over, in `runtimes` + // order (an array, not a pair, for the same JSON-import reason). Not rendered. + samples: Array; +} + +/** + * One per-runtime measurement that was not stable (see + * `CrossRuntimeReport.unstable_cells`). `cv`, `cv_raw` and `drift` are fractions; + * the raw two are `null` on a sibling predating them. + */ +export interface UnstableCell { + group: string; + name: string; + runtime: BenchmarkRuntime; + cv: number | null; + cv_raw: number | null; + drift: number | null; + /** The cleaned timing count behind the cv (`samples` in the composer's JSON). */ + samples: number | null; +} + +export interface CrossRuntimeDisplayRow { + name: string; + category: ImplementationCategory; + ops_per_second: Partial>; + // the mean time of one sweep per runtime, the unit the tables print + mean_ns: Partial>; + // ratio of each runtime vs the base (first present) runtime; `> 1` = faster + ratio_vs_base: Partial>; +} + +export interface CrossRuntimeGroup { + group: string; + operation: string; + language: string; + rows: Array; +} + +// The combined report stores runtimes deno-first (matching the bench's +// `report.md`); the site presents them node-first (the flagship N-API runtime), +// then deno, then bun. +const CROSS_RUNTIME_DISPLAY_ORDER: Array = ['node', 'deno', 'bun']; + +/** + * The report's runtimes in the site's display order — node (the flagship, the + * ratio anchor) first, then deno, then bun. Shared by `derive_cross_runtime_groups` + * and the table headers so the anchor can't drift between them. + */ +export const order_cross_runtime_runtimes = ( + runtimes: Array +): Array => CROSS_RUNTIME_DISPLAY_ORDER.filter((r) => runtimes.includes(r)); + +/** One runtime's own version string (`node 24.14.1`), for the cross-runtime section. */ +export interface RuntimeVersion { + runtime: BenchmarkRuntime; + version: string; +} + +/** + * The per-runtime version strings from a cross-runtime report, in the site's + * display order (node first). Drops any runtime whose source carries no machine + * block. The cross-runtime section + * renders these so the three-runtime tables disclose which node/deno/bun version + * each column was measured under; the environment panel above stays scoped to the + * flagship Node baseline. The shared hardware identity (CPU/OS/arch) isn't + * repeated here — it lives in the environment panel, and a per-runtime hardware + * mismatch is the report's `mixed_machine` flag's concern. + */ +export const derive_runtime_versions = (report: CrossRuntimeReport): Array => { + const by_runtime = new Map(report.sources.map((source) => [source.runtime, source])); + const result: Array = []; + for (const runtime of order_cross_runtime_runtimes(report.runtimes)) { + const version = by_runtime.get(runtime)?.machine?.runtime_version; + if (version) result.push({ runtime, version }); + } + return result; +}; + +/** + * Groups the combined report's rows by benchmark group, in the same display + * order as `derive_benchmark_groups` (format before parse, then svelte / + * typescript / css). The ratio base is the first runtime in display order + * (node when present — the flagship N-API path), regardless of the report's + * own deno-first storage order. + */ +export const derive_cross_runtime_groups = ( + report: CrossRuntimeReport +): Array => { + const runtimes = order_cross_runtime_runtimes(report.runtimes); + const base = runtimes[0]; + const to_display_row = (row: CrossRuntimeRow): CrossRuntimeDisplayRow => { + const base_ops = base ? row.ops_per_second[base] : undefined; + const ratio_vs_base: Partial> = {}; + for (const runtime of runtimes) { + const ops = row.ops_per_second[runtime]; + if (ops != null && base_ops != null && base_ops > 0) { + ratio_vs_base[runtime] = ops / base_ops; + } + } + return { + name: row.name, + category: categorize_name(row.name), + ops_per_second: row.ops_per_second, + mean_ns: row.mean_ns, + ratio_vs_base + }; + }; + + return [...Map.groupBy(report.rows, (row) => row.group)] + .map(([group, rows]) => ({ group, ...parse_group_key(group), rows: rows.map(to_display_row) })) + .sort(compare_group_order); +}; + +/** + * The combined report's unstable cells, in the site's runtime column order. + */ +export const derive_unstable_cells = (report: CrossRuntimeReport): Array => { + const order = order_cross_runtime_runtimes(report.runtimes); + return [...report.unstable_cells].sort( + (a, b) => order.indexOf(a.runtime) - order.indexOf(b.runtime) + ); +}; + +/** + * The rows each runtime couldn't measure, in the site's column order, for the + * disclosure above the tables. Empty when nothing was recorded. + */ +export const derive_unavailable_by_runtime = ( + report: CrossRuntimeReport +): Array => { + const by_runtime = new Map(report.unavailable_by_runtime.map((entry) => [entry.runtime, entry])); + return order_cross_runtime_runtimes(report.runtimes) + .map((runtime) => by_runtime.get(runtime)) + .filter((entry): entry is RuntimeUnavailable => entry != null && entry.rows.length > 0); +}; + +/** + * Is the ratio of `runtime` over `base` for one row inside the two measurements' + * combined noise — a delta the report itself says is not a runtime effect? Reads + * the composer's pairwise `within_noise`, where each cell names both runtimes. + * Absence is silence, not a claim the delta is real. + */ +export const is_ratio_within_noise = ( + report: CrossRuntimeReport, + group: string, + name: string, + base: BenchmarkRuntime, + runtime: BenchmarkRuntime +): boolean => + report.within_noise.some( + (cell) => + cell.group === group && + cell.name === name && + cell.runtimes.includes(base) && + cell.runtimes.includes(runtime) + ); + +/** Did the composer flag `runtime`'s measurement of one row as unstable? */ +export const is_cell_unstable = ( + report: CrossRuntimeReport, + group: string, + name: string, + runtime: BenchmarkRuntime +): boolean => + report.unstable_cells.some( + (cell) => cell.group === group && cell.name === name && cell.runtime === runtime + ); + +/** + * Did `runtime` record `name` as a load failure? The table renders an absent + * number the same either way, so this is what lets a cell say WHICH gap it is — + * a binding that couldn't load, or a row that runtime's report never carried. + */ +export const is_impl_unavailable = ( + report: CrossRuntimeReport, + runtime: BenchmarkRuntime, + name: string +): boolean => + report.unavailable_by_runtime.some( + (entry) => entry.runtime === runtime && entry.rows.includes(name) + ); + +/** + * The label for a cross-runtime row. Each row spans node/deno/bun columns, and + * tsv's native build's binding is runtime-specific (N-API on node & bun, C-FFI on + * deno), so the row can't pin one binding — neutralize `format_label`'s + * node-centric `(node napi)` suffix to `(native)`. The third-party native rows + * are npm N-API addons under all three runtimes and get the same neutral suffix; + * the per-runtime binding is disclosed once in the table caption instead (see + * `BenchmarksCrossRuntime`). + */ +export const format_cross_runtime_label = (name: string): string => + format_label(name).replace(' (node napi)', ' (native)'); + +/** + * Cross-runtime ratio cell background: a stable fuz_css red (`color_c`) / green + * (`color_b`) whose ALPHA varies with distance from parity. Fully transparent at ratio + * `1.0` (parity recedes), ramping to `0.3` alpha at ratio `0.8` and below (red) or `1.2` + * and above (green). The hue stays constant — only opacity moves — so it reads + * consistently in light and dark themes while the cell's text keeps the default color. + * Deliberately its own scale — NOT the shared `baseline_ratio_color` — because cross-runtime + * deltas cluster tightly near 1.0 and this must not bleed into the other displays. + */ +export const cross_runtime_ratio_background = (ratio: number): string => { + // 0 alpha at ratio 1.0, up to 0.3 at ratio ≤ 0.8 (red) or ≥ 1.2 (green) + const alpha = Math.min(1, Math.abs(ratio - 1) / 0.2) * 0.3; + const color = ratio < 1 ? 'var(--color_c_50)' : 'var(--color_b_50)'; + return `color-mix(in srgb, ${color} ${(alpha * 100).toFixed(1)}%, transparent)`; +}; diff --git a/src/routes/docs/benchmarks/benchmark_data.ts b/src/routes/docs/benchmarks/benchmark_data.ts index 9bf0958..9ec27f6 100644 --- a/src/routes/docs/benchmarks/benchmark_data.ts +++ b/src/routes/docs/benchmarks/benchmark_data.ts @@ -1,41 +1,280 @@ -// 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), with the format/parse, +// stability, and corpus derivations the page builds on them. +// +// Sibling modules own what reads these: `benchmark_sizes.ts` the binary-size +// tables, the conformance page's `conformance_data.ts` the parse-coverage tables, +// `benchmark_cross_runtime.ts` the combined report, `benchmark_display.ts` the value +// formatters, labels, and category colors, and `benchmark_baseline.ts` the +// hover-to-rebaseline ratios. This module imports none of them. export interface BenchmarkBaseline { version: number; + // The runtime that produced this report (`node` for the flagship view). + runtime: string; timestamp: string; git_commit: string; corpus: Record; versions: BaselineVersions; binary_sizes: Array; entries: Array; - // 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; + // Counts of silenced third-party stderr noise, keyed by message pattern. 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. + 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). + corpus_kind: 'perf' | 'conformance'; + // Per-entry corpus composition (path + loaded file count) — discloses which + // sources were present on the machine that produced the report. + corpus_sources: Array; + // The exclusion caches the conformance view applied, by label, each its size — + // `null` for an absent one, which only a tolerated (`BENCH_ALLOW_MISSING=1`), + // not-comparable run publishes. Conformance reports only; not rendered, the + // shape test refuses a `null`. + exclusion_caches?: Record; + // 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. 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. + 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, rendered by `conformance_data.ts`'s per-source matrices. Conformance + // reports only (the perf surface is 100% by construction). + 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. 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 (`[]` in every committed report, which the shape tests pin for the + // perf report). 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. Not rendered, + // kept for parity. + output_digest_ungraded: Record; + // Per timed group, the files and BYTES its intersection left out and the rows + // that left them. A file any timed row fails leaves EVERY row's timed set, so one + // tool's omit moves every number in the group — and a file count understates it + // (a harvested per-collection stylesheet is one file). A group nothing failed is + // listed with zeroes. Perf surface, intersection mode, timed runs only — absent on + // a `BENCH_MODE=union` run and on the conformance surface. + omissions?: Array; } +// One timed group's omissions (see `BenchmarkBaseline.omissions`). Mirrors the +// bench's `GroupOmissions`: `omitted_*` is the UNION over rows, `by_tool` is per +// row, so its counts can sum past it. +export interface GroupOmissions { + // `operation/language`, the same key `BaselineEntry.group` carries. + group: string; + files_total: number; + bytes_total: number; + omitted_files: number; + omitted_bytes: number; + by_tool: Array; +} + +// One row's share of a group's omissions. `categories` counts its failed files by +// the bench's omit category (`tool_limit`, `unsupported_syntax`, +// `harness_path_threading`, `harvest_artifact`, `tsv_failure`), plus `unlisted` for a +// failure no omit entry claims — unreachable on a run the bench's coverage gate +// passed, and named so the counts always sum to `files`. Keys arrive in that order. +export interface ToolOmissions { + name: string; + files: number; + bytes: number; + categories: Record; +} + +// What a PARSE row hands JS (see `BaselineEntry.payload`). Mirrors the bench's +// `PayloadTier`. +export type PayloadTier = 'drop_in' | 'span_only' | 'own_shape' | 'none'; + +// Whether a ratio between two parse rows compares the same PRODUCT: their tiers are +// equal and neither is `own_shape`. `null` when either row carries no tier — a +// format row, where the question does not arise. +export const is_payload_matched = ( + a: Pick, + b: Pick +): boolean | null => + a.payload == null || b.payload == null + ? null + : a.payload === b.payload && a.payload !== 'own_shape'; + +// Per-impl coverage for one corpus source (see `coverage_by_source`). +export interface SourceCoverageCell { + processed: number; + total: number; + // of `processed`, files accepted only on the Script-goal retry a source allows + // (Prettier's JS and TypeScript suites); absent when there was none — carried, not rendered + script_only?: 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. + 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`). +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. + 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`). + 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 + // sources with no GitHub remote. + 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}`; +}; + +/** How many characters of a commit SHA the page prints. */ +export const COMMIT_LABEL_LENGTH = 7; + +/** A repo ref's commit as prose and tables print it, `undefined` for an unpinned ref. */ +export const corpus_repo_ref_commit = (repo: CorpusRepoRef): string | undefined => + repo.commit ? repo.commit.slice(0, COMMIT_LABEL_LENGTH) : undefined; + +/** + * 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: 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. `null` on an + // untimed row. + cv_raw: number | null; + drift: number | null; + raw_sample_size: number | null; + outlier_ratio: number | null; + warmup_iterations: number | null; + min_iterations: number | null; + // The JS heap the row's warmup began from, which shows whether two runs of a row + // started from the same place; not rendered. `null` on an untimed row. + settled_heap_bytes: 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. - files_processed?: number | null; - files_total?: number | null; + // language's total discovered files. + files_processed: number | null; + files_total: number | null; // Files this impl was actually timed on (the per-group intersection in - // default mode). Present from baseline `version` 4 on. - files_iterated?: number | null; + // default mode); `null` on an untimed row. + files_iterated: number | null; + // What a parse row hands JS — the canonical parser's own AST shape (`drop_in`), + // a `start`/`end`-only tree (`span_only`), the tool's own dialect or reduction + // (`own_shape`), or nothing materialized (`none`). Most of a parse row's time is + // building that product, so a ratio between two rows integrates it + // (`is_payload_matched`). `null` on format rows. + payload: PayloadTier | null; + // Matches the report's top-level `runtime`; not rendered, kept for parity. + runtime: string; } export interface BaselineVersions { @@ -46,14 +285,47 @@ 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. + oxc_parser_wasm?: string; oxfmt?: string; + // `yuku-parser` (N-API) and `@yuku-parser/wasm` — one Zig engine behind two + // bindings, versioned in lockstep upstream. + yuku_parser?: string; + yuku_parser_wasm?: string; biome?: string; + // `@dprint/typescript` — the plugin version (the host `@dprint/formatter` is + // just the wasm loader). + dprint?: string; + // `@rsvelte/fmt` — the coverage-only Svelte formatter row. + rsvelte_fmt?: string; + // `dprint-plugin-malva` — dprint's CSS formatter plugin, over the same wasm + // host as `dprint` above. + malva?: string; + // `postcss` — the CSS parser row. + 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. + 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. + swc?: string; + // `typescript` — the engine behind the `tsc` row, which runs on the conformance + // surface alone, so the perf reports never carry it. + tsc?: string; } export interface BinarySize { label: string; bytes: number; - kind: 'native' | 'wasm'; + // `js` is a minified JS bundle the tsv harness builds + // itself — the canonical toolchain's rows, which no package ships as one file. + kind: 'native' | 'wasm' | 'js'; // Gzipped on-disk size (≈ npm-tarball wire size); `null` when `gzip` was // unavailable on the machine that generated the baseline. gzip_bytes: number | null; @@ -68,7 +340,12 @@ export type ImplementationCategory = | 'tsv_wasm' | 'tsv_wasm_json' | 'biome' - | 'oxc'; + | 'dprint' + | 'oxc' + | 'postcss' + | 'rsvelte' + | 'swc' + | 'yuku'; export interface BenchmarkGroup { operation: string; @@ -76,25 +353,34 @@ export interface BenchmarkGroup { entries: Array; canonical_entry: BenchmarkDisplayEntry | undefined; // files the timed benchmark actually iterated (the per-group intersection); - // null on older baselines (< version 4) that don't carry `files_iterated` + // null when no row in the group was timed files_iterated: number | null; + // what the intersection left out, when it left anything out; null when nothing + // was omitted or the report carries no `omissions` + omissions: GroupOmissions | null; } 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; -} - -export interface SpeedupRow { - variant: string; - format_svelte: number | undefined; - format_typescript: number | undefined; - format_css: number | undefined; + // A placeholder entry mirrored from another language's group for a tool that + // doesn't run in this one (e.g. `oxc-parser` under 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; } // Implementation categorization @@ -105,84 +391,163 @@ 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'; + +// Derivation functions -export const categorize_size = (label: string): ImplementationCategory => { - // 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'; +// 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 }; -// 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', +/** Splits an `operation/language` group key (`format/svelte`) into its halves. */ +export const parse_group_key = (group: string): { operation: string; language: string } => { + const [operation = '', language = ''] = group.split('/'); + return { operation, language }; }; -// Primary tsv entry names for speedup summary (fair comparisons) -const PRIMARY_NATIVE_FORMAT = 'tsv'; -const PRIMARY_WASM_FORMAT = 'tsv_wasm'; +/** Orders groups by `OPERATION_ORDER` then `LANGUAGE_ORDER`, unknowns last. */ +export const compare_group_order = ( + a: { operation: string; language: string }, + b: { operation: string; language: string } +): number => + (OPERATION_ORDER[a.operation] ?? 9) - (OPERATION_ORDER[b.operation] ?? 9) || + (LANGUAGE_ORDER[a.language] ?? 9) - (LANGUAGE_ORDER[b.language] ?? 9); + +/** The fixed slots ahead of tsv's own rows — see `speed_entry_rank`. */ +const CROSS_TOOL_RANK: Partial> = { + canonical: 0, + biome: 1, + dprint: 2, + oxc: 3, + postcss: 4, + rsvelte: 5, + swc: 6, + yuku: 7 +}; +// tsv's own rows follow every cross-tool slot +const TSV_RANK_BASE = Object.keys(CROSS_TOOL_RANK).length; -// Derivation functions +/** + * 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.00x 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 => { + const rank = CROSS_TOOL_RANK[entry.category]; + if (rank !== undefined) return rank; + if (entry.name.endsWith('-no-locations')) return TSV_RANK_BASE; // tsv json, span-only wire + if (entry.name.endsWith('-json')) return TSV_RANK_BASE + 1; // tsv json, loc-carrying wire + return TSV_RANK_BASE + 2; // tsv's engine rows: `tsv`/`tsv-wasm` (format), `-internal` (parse, no JS materialization) +}; -export const derive_benchmark_groups = (baseline: BenchmarkBaseline): Array => { - const grouped: Map> = new Map(); - for (const entry of baseline.entries) { - let group = grouped.get(entry.group); - if (!group) { - group = []; - grouped.set(entry.group, group); - } - group.push(entry); - } +/** + * 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 row for a group the tool doesn't run in — no bar, no + * coverage, never an anchor. Built from the name and category alone, so nothing a + * template row measured (a TypeScript mean ~100x any real CSS row, a + * `coverage_only` flag) can ride into the group it is mirrored into. + */ +const to_placeholder = ( + entry: Pick +): BenchmarkDisplayEntry => ({ + name: entry.name, + category: entry.category, + mean_ns: 0, + bar_fraction: 0, + files_processed: null, + files_total: null, + disabled: true +}); + +export const derive_benchmark_groups = (baseline: BenchmarkBaseline): Array => { const result: Array = []; - for (const [group_key, entries] of grouped) { - const parts = group_key.split('/'); - const operation = parts[0]!; - const language = parts[1]!; - const canonical_name = CANONICAL_BY_GROUP[group_key]; - const canonical_entry_raw = entries.find((e) => 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, - })); - - // 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; + for (const [group_key, entries] of Map.groupBy(baseline.entries, (e) => e.group)) { + const { operation, language } = parse_group_key(group_key); + // 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.00x + // 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_targets`.) + // An untimed row (see `BenchmarkDisplayEntry.coverage_only`) coerces to 0, so it + // can't set the bar scale; it renders inert, so the 0 is never shown or divided by. + const slowest = Math.max(...entries.map((e) => e.mean_ns ?? 0)); + + 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 mean_ns = e.mean_ns; + const untimed = mean_ns == null; + return { + name: e.name, + mean_ns: mean_ns ?? 0, + bar_fraction: mean_ns == null || slowest <= 0 ? 0 : mean_ns / slowest, + category: categorize_name(e.name), + files_processed: e.files_processed, + files_total: e.files_total, + ...(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); + + // the timed set is the per-group intersection, so every timed row carries the + // same count (a shape test pins that); the min is the intersection if they + // ever diverge, where the max would overstate what the slowest row timed const iterated_counts = entries .map((e) => e.files_iterated) .filter((v): v is number => v != null); @@ -191,146 +556,289 @@ 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.min(...iterated_counts) : null, + omissions: + baseline.omissions?.find((o) => o.group === group_key && o.omitted_files > 0) ?? 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), - ); + result.sort(compare_group_order); + + // Two tools hold a grayed-out slot in parse groups they don't run in. `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 and every one gets a + // placeholder. `oxc-parser` only parses TypeScript/JS; it is mirrored into the css + // group alone, since oxc does ship CSS tooling (oxfmt formats it) and a Svelte + // parser is nothing it claims. Then re-sort so they fall into their fixed slots + // (biome then oxc, right after the canonical row). + // + // A grayed-out slot states a SCOPE gap in a broad web toolchain, so nothing + // narrower is mirrored: not `yuku-parser`, TypeScript/JS-only by design, and not + // `@dprint/typescript` into the svelte format group — an empty slot there would + // invent a shortfall against a promise the tool 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 has = (category: ImplementationCategory) => + group.entries.some((e) => e.category === category); + if (!has('biome')) + group.entries.push(to_placeholder({ name: 'biome-wasm', category: 'biome' })); + if (group.language === 'css' && !has('oxc')) { + group.entries.push(...oxc_templates.map(to_placeholder)); + } + group.entries.sort(compare_speed_entries); + } return result; }; -export const derive_speedup_summary = (groups: Array): Array => { - const find_speedup = ( - operation: string, - language: string, - primary_name: string, - ): number | undefined => { - const group = groups.find((g) => g.operation === operation && g.language === language); - if (!group?.canonical_entry) return undefined; - const entry = group.entries.find((e) => e.name === primary_name); - if (!entry) return undefined; - return group.canonical_entry.mean_ns / entry.mean_ns; - }; - - return [ - { - variant: 'native', - format_svelte: find_speedup('format', 'svelte', PRIMARY_NATIVE_FORMAT), - format_typescript: find_speedup('format', 'typescript', PRIMARY_NATIVE_FORMAT), - format_css: find_speedup('format', 'css', PRIMARY_NATIVE_FORMAT), - }, - { - variant: 'wasm', - format_svelte: find_speedup('format', 'svelte', PRIMARY_WASM_FORMAT), - format_typescript: find_speedup('format', 'typescript', PRIMARY_WASM_FORMAT), - format_css: find_speedup('format', 'css', PRIMARY_WASM_FORMAT), - }, - ]; -}; - -// Formatting utilities - -export interface FormattedUnit { - value: string; - unit: string; -} +// Measurement stability -export const format_ns = (ns: number): FormattedUnit => { - if (ns < 1_000) return {value: `${Math.round(ns)}`, unit: 'ns'}; - if (ns < 1_000_000) - return { - value: (ns / 1_000).toFixed(ns < 10_000 ? 2 : ns < 100_000 ? 1 : 0), - unit: 'µs', - }; - const ms = Math.round(ns / 1_000_000); - return {value: ms.toLocaleString('en-US'), unit: 'ms'}; -}; +/** + * The bench's own instability thresholds, restated: a cleaned cv at or past + * `UNSTABLE_CV_THRESHOLD`, a raw cv at or past it on a row with fewer than + * `RAW_CV_SAMPLE_CEILING` raw timings, or a |drift| at or past + * `UNSTABLE_DRIFT_THRESHOLD`, and the row's mean may be neither of two modes it + * blended. Mirrors `bench.ts`. + */ +const UNSTABLE_CV_THRESHOLD = 0.1; +const UNSTABLE_DRIFT_THRESHOLD = 0.05; +/** + * Below this many raw timings the raw cv counts too: with few samples one deviant + * sweep is a real share of the row; with hundreds it is an isolated pause the + * cleaner rightly removes, and `drift` (a median-based level shift) is the detector. + */ +const RAW_CV_SAMPLE_CEILING = 30; -export const format_bytes = (bytes: number): FormattedUnit => { - if (bytes < 1_024) return {value: `${bytes}`, unit: 'B'}; - if (bytes < 1_048_576) return {value: (bytes / 1_024).toFixed(0), unit: 'KB'}; - return {value: (bytes / 1_048_576).toFixed(1), unit: 'MB'}; +/** + * Whether a timed row's measurement was stable enough to divide by — the gate every + * headline ratio on the page should pass. A coverage-only row (null timing) is not + * unstable, it is untimed. + */ +export const is_entry_unstable = (entry: BaselineEntry): boolean => + to_unstable_readings(entry).length > 0; + +// the readings past their threshold — empty for a stable or an untimed row +const to_unstable_readings = (entry: BaselineEntry): Array => { + // untimed is `mean_ns`, not `cv` — a timed row missing its cleaned cv still + // carries a raw cv and a drift to check + if (entry.mean_ns == null) return []; + const readings: Array = []; + if (entry.cv != null && entry.cv >= UNSTABLE_CV_THRESHOLD) readings.push(entry.cv); + if ( + entry.cv_raw != null && + entry.cv_raw >= UNSTABLE_CV_THRESHOLD && + entry.raw_sample_size != null && + entry.raw_sample_size < RAW_CV_SAMPLE_CEILING + ) { + readings.push(entry.cv_raw); + } + if (entry.drift != null && Math.abs(entry.drift) >= UNSTABLE_DRIFT_THRESHOLD) { + readings.push(Math.abs(entry.drift)); + } + return readings; }; /** - * Formats per-implementation corpus coverage as `processed/total`, or - * `undefined` when either value is missing (older baselines without coverage). + * The timed rows of a per-runtime report whose measurement was not stable, worst + * first by the readings that flagged them — the page's disclosure beside the + * numbers built from that report. */ -export const format_coverage = ( - processed: number | null | undefined, - total: number | null | undefined, -): string | undefined => { - if (processed == null || total == null) return undefined; - return `${processed}/${total}`; -}; +export const derive_unstable_entries = (baseline: BenchmarkBaseline): Array => + baseline.entries + .map((entry) => ({ entry, worst: Math.max(...to_unstable_readings(entry)) })) + .filter(({ worst }) => worst > 0) + .sort((a, b) => b.worst - a.worst) + .map(({ entry }) => entry); + +// Corpus source table /** - * Formats a gzipped binary size as a bar annotation (e.g. `716.6 KB gz`), or - * `undefined` when the baseline lacks it (older runs, or no `gzip` available - * on the generating machine). + * Reader-facing names for the perf report's corpus sources that have no repo to + * name them. The shape test fails on a repo-less source missing here, so a cache + * tsv adds can't reach the page as a raw path unnoticed. */ -export const format_gzip_size = (gzip_bytes: number | null | undefined): string | undefined => { - if (gzip_bytes == null) return undefined; - const {value, unit} = format_bytes(gzip_bytes); - return `${value} ${unit} gz`; +export const CORPUS_SOURCE_LABELS: Record = { + 'benches/js/.cache/svelte_styles': 'harvested diff --git a/src/routes/docs/conformance/conformance.json b/src/routes/docs/conformance/conformance.json new file mode 100644 index 0000000..0651c77 --- /dev/null +++ b/src/routes/docs/conformance/conformance.json @@ -0,0 +1,1580 @@ +{ + "version": 19, + "runtime": "node", + "corpus_kind": "conformance", + "timestamp": "2026-09-23T01:44:33.674Z", + "git_commit": "23392a6e", + "machine": { + "cpu_model": "AMD Ryzen 5 PRO 7530U with Radeon Graphics", + "os": "linux", + "arch": "x86_64", + "runtime_version": "24.14.1" + }, + "corpus": { + "svelte": 4619, + "typescript": 53323, + "css": 22449 + }, + "corpus_sources": [ + { + "path": "../prettier-plugin-svelte/test", + "files": 318, + "by_language": { + "svelte": 318, + "typescript": 0, + "css": 0 + }, + "repo": { + "url": "https://github.com/sveltejs/prettier-plugin-svelte", + "slug": "sveltejs/prettier-plugin-svelte", + "commit": "7809486a9716faa2234c8a45d88b601034de52d8", + "subpath": "test" + } + }, + { + "path": "../prettier/tests/format/typescript", + "files": 591, + "by_language": { + "svelte": 0, + "typescript": 591, + "css": 0 + }, + "repo": { + "url": "https://github.com/prettier/prettier", + "slug": "prettier/prettier", + "commit": "1dcd0b05d026356cce5bf538c6d425955b29903b", + "subpath": "tests/format/typescript" + } + }, + { + "path": "../prettier/tests/format/js", + "files": 756, + "by_language": { + "svelte": 0, + "typescript": 756, + "css": 0 + }, + "repo": { + "url": "https://github.com/prettier/prettier", + "slug": "prettier/prettier", + "commit": "1dcd0b05d026356cce5bf538c6d425955b29903b", + "subpath": "tests/format/js" + } + }, + { + "path": "../prettier/tests/format/css", + "files": 139, + "by_language": { + "svelte": 0, + "typescript": 0, + "css": 139 + }, + "repo": { + "url": "https://github.com/prettier/prettier", + "slug": "prettier/prettier", + "commit": "1dcd0b05d026356cce5bf538c6d425955b29903b", + "subpath": "tests/format/css" + } + }, + { + "path": "../prettier/tests/format/html", + "files": 82, + "by_language": { + "svelte": 82, + "typescript": 0, + "css": 0 + }, + "repo": { + "url": "https://github.com/prettier/prettier", + "slug": "prettier/prettier", + "commit": "1dcd0b05d026356cce5bf538c6d425955b29903b", + "subpath": "tests/format/html" + } + }, + { + "path": "../svelte/packages/svelte/tests", + "files": 4542, + "by_language": { + "svelte": 4219, + "typescript": 140, + "css": 183 + }, + "repo": { + "url": "https://github.com/sveltejs/svelte", + "slug": "sveltejs/svelte", + "commit": "7bc0a70fe64dbb3fa3848b741963f31d1e10a8dc", + "subpath": "packages/svelte/tests" + } + }, + { + "path": "benches/js/.cache/wpt_css", + "files": 22127, + "by_language": { + "svelte": 0, + "typescript": 0, + "css": 22127 + }, + "repo": { + "url": "https://github.com/web-platform-tests/wpt", + "slug": "web-platform-tests/wpt", + "commit": "", + "subpath": "" + } + }, + { + "path": "benches/js/.cache/test262_files.json", + "files": 43739, + "by_language": { + "svelte": 0, + "typescript": 43739, + "css": 0 + }, + "repo": { + "url": "https://github.com/tc39/test262", + "slug": "tc39/test262", + "commit": "", + "subpath": "" + } + }, + { + "path": "benches/js/.cache/ts_repo_files.json", + "files": 8097, + "by_language": { + "svelte": 0, + "typescript": 8097, + "css": 0 + }, + "repo": { + "url": "https://github.com/microsoft/TypeScript", + "slug": "microsoft/TypeScript", + "commit": "", + "subpath": "" + } + } + ], + "exclusion_caches": { + "canonical-reject": 142, + "prettier-jsx": 39 + }, + "coverage_by_source": { + "parse/svelte": { + "../prettier-plugin-svelte/test": { + "svelte/compiler": { + "processed": 318, + "total": 318 + }, + "tsv-json": { + "processed": 318, + "total": 318 + }, + "tsv-wasm-json": { + "processed": 318, + "total": 318 + }, + "tsv-json-no-locations": { + "processed": 318, + "total": 318 + }, + "tsv-wasm-json-no-locations": { + "processed": 318, + "total": 318 + }, + "tsv-internal": { + "processed": 318, + "total": 318 + }, + "tsv-wasm-internal": { + "processed": 318, + "total": 318 + }, + "rsvelte-parse": { + "processed": 318, + "total": 318 + }, + "rsvelte-parse-skip-expr-loc": { + "processed": 318, + "total": 318 + } + }, + "../prettier/tests/format/html": { + "svelte/compiler": { + "processed": 82, + "total": 82 + }, + "tsv-json": { + "processed": 82, + "total": 82 + }, + "tsv-wasm-json": { + "processed": 82, + "total": 82 + }, + "tsv-json-no-locations": { + "processed": 82, + "total": 82 + }, + "tsv-wasm-json-no-locations": { + "processed": 82, + "total": 82 + }, + "tsv-internal": { + "processed": 82, + "total": 82 + }, + "tsv-wasm-internal": { + "processed": 82, + "total": 82 + }, + "rsvelte-parse": { + "processed": 82, + "total": 82 + }, + "rsvelte-parse-skip-expr-loc": { + "processed": 82, + "total": 82 + } + }, + "../svelte/packages/svelte/tests": { + "svelte/compiler": { + "processed": 4219, + "total": 4219 + }, + "tsv-json": { + "processed": 4219, + "total": 4219 + }, + "tsv-wasm-json": { + "processed": 4219, + "total": 4219 + }, + "tsv-json-no-locations": { + "processed": 4219, + "total": 4219 + }, + "tsv-wasm-json-no-locations": { + "processed": 4219, + "total": 4219 + }, + "tsv-internal": { + "processed": 4219, + "total": 4219 + }, + "tsv-wasm-internal": { + "processed": 4219, + "total": 4219 + }, + "rsvelte-parse": { + "processed": 4219, + "total": 4219 + }, + "rsvelte-parse-skip-expr-loc": { + "processed": 4219, + "total": 4219 + } + } + }, + "parse/typescript": { + "../prettier/tests/format/typescript": { + "acorn-typescript": { + "processed": 515, + "total": 591 + }, + "tsv-json": { + "processed": 581, + "total": 591, + "script_only": 1 + }, + "tsv-wasm-json": { + "processed": 581, + "total": 591, + "script_only": 1 + }, + "tsv-json-no-locations": { + "processed": 581, + "total": 591, + "script_only": 1 + }, + "tsv-wasm-json-no-locations": { + "processed": 581, + "total": 591, + "script_only": 1 + }, + "tsv-internal": { + "processed": 581, + "total": 591, + "script_only": 1 + }, + "tsv-wasm-internal": { + "processed": 581, + "total": 591, + "script_only": 1 + }, + "oxc-parser": { + "processed": 569, + "total": 591 + }, + "oxc-parser-wasm": { + "processed": 570, + "total": 591 + }, + "tsc": { + "processed": 590, + "total": 591 + }, + "yuku-parser-wasm": { + "processed": 582, + "total": 591, + "script_only": 2 + }, + "swc": { + "processed": 557, + "total": 591 + } + }, + "../prettier/tests/format/js": { + "acorn-typescript": { + "processed": 709, + "total": 756, + "script_only": 34 + }, + "tsv-json": { + "processed": 740, + "total": 756, + "script_only": 11 + }, + "tsv-wasm-json": { + "processed": 740, + "total": 756, + "script_only": 11 + }, + "tsv-json-no-locations": { + "processed": 740, + "total": 756, + "script_only": 11 + }, + "tsv-wasm-json-no-locations": { + "processed": 740, + "total": 756, + "script_only": 11 + }, + "tsv-internal": { + "processed": 740, + "total": 756, + "script_only": 11 + }, + "tsv-wasm-internal": { + "processed": 740, + "total": 756, + "script_only": 11 + }, + "oxc-parser": { + "processed": 748, + "total": 756 + }, + "oxc-parser-wasm": { + "processed": 748, + "total": 756 + }, + "tsc": { + "processed": 743, + "total": 756 + }, + "yuku-parser-wasm": { + "processed": 751, + "total": 756, + "script_only": 1 + }, + "swc": { + "processed": 734, + "total": 756, + "script_only": 14 + } + }, + "../svelte/packages/svelte/tests": { + "acorn-typescript": { + "processed": 140, + "total": 140 + }, + "tsv-json": { + "processed": 140, + "total": 140 + }, + "tsv-wasm-json": { + "processed": 140, + "total": 140 + }, + "tsv-json-no-locations": { + "processed": 140, + "total": 140 + }, + "tsv-wasm-json-no-locations": { + "processed": 140, + "total": 140 + }, + "tsv-internal": { + "processed": 140, + "total": 140 + }, + "tsv-wasm-internal": { + "processed": 140, + "total": 140 + }, + "oxc-parser": { + "processed": 140, + "total": 140 + }, + "oxc-parser-wasm": { + "processed": 140, + "total": 140 + }, + "tsc": { + "processed": 140, + "total": 140 + }, + "yuku-parser-wasm": { + "processed": 140, + "total": 140 + }, + "swc": { + "processed": 140, + "total": 140 + } + }, + "benches/js/.cache/test262_files.json": { + "acorn-typescript": { + "processed": 43478, + "total": 43739 + }, + "tsv-json": { + "processed": 43739, + "total": 43739 + }, + "tsv-wasm-json": { + "processed": 43739, + "total": 43739 + }, + "tsv-json-no-locations": { + "processed": 43739, + "total": 43739 + }, + "tsv-wasm-json-no-locations": { + "processed": 43739, + "total": 43739 + }, + "tsv-internal": { + "processed": 43739, + "total": 43739 + }, + "tsv-wasm-internal": { + "processed": 43739, + "total": 43739 + }, + "oxc-parser": { + "processed": 43725, + "total": 43739 + }, + "oxc-parser-wasm": { + "processed": 43725, + "total": 43739 + }, + "tsc": { + "processed": 43677, + "total": 43739 + }, + "yuku-parser-wasm": { + "processed": 43698, + "total": 43739 + }, + "swc": { + "processed": 43261, + "total": 43739 + } + }, + "benches/js/.cache/ts_repo_files.json": { + "acorn-typescript": { + "processed": 7772, + "total": 8097 + }, + "tsv-json": { + "processed": 8017, + "total": 8097 + }, + "tsv-wasm-json": { + "processed": 8017, + "total": 8097 + }, + "tsv-json-no-locations": { + "processed": 8017, + "total": 8097 + }, + "tsv-wasm-json-no-locations": { + "processed": 8017, + "total": 8097 + }, + "tsv-internal": { + "processed": 8017, + "total": 8097 + }, + "tsv-wasm-internal": { + "processed": 8017, + "total": 8097 + }, + "oxc-parser": { + "processed": 7999, + "total": 8097 + }, + "oxc-parser-wasm": { + "processed": 8000, + "total": 8097 + }, + "tsc": { + "processed": 8097, + "total": 8097 + }, + "yuku-parser-wasm": { + "processed": 7991, + "total": 8097 + }, + "swc": { + "processed": 7932, + "total": 8097 + } + } + }, + "parse/css": { + "../prettier/tests/format/css": { + "svelte/compiler": { + "processed": 118, + "total": 139 + }, + "tsv-json": { + "processed": 122, + "total": 139 + }, + "tsv-wasm-json": { + "processed": 122, + "total": 139 + }, + "tsv-internal": { + "processed": 122, + "total": 139 + }, + "tsv-wasm-internal": { + "processed": 122, + "total": 139 + }, + "postcss": { + "processed": 139, + "total": 139 + } + }, + "../svelte/packages/svelte/tests": { + "svelte/compiler": { + "processed": 183, + "total": 183 + }, + "tsv-json": { + "processed": 183, + "total": 183 + }, + "tsv-wasm-json": { + "processed": 183, + "total": 183 + }, + "tsv-internal": { + "processed": 183, + "total": 183 + }, + "tsv-wasm-internal": { + "processed": 183, + "total": 183 + }, + "postcss": { + "processed": 183, + "total": 183 + } + }, + "benches/js/.cache/wpt_css": { + "svelte/compiler": { + "processed": 21941, + "total": 22127 + }, + "tsv-json": { + "processed": 21990, + "total": 22127 + }, + "tsv-wasm-json": { + "processed": 21990, + "total": 22127 + }, + "tsv-internal": { + "processed": 21990, + "total": 22127 + }, + "tsv-wasm-internal": { + "processed": 21990, + "total": 22127 + }, + "postcss": { + "processed": 22039, + "total": 22127 + } + } + } + }, + "versions": { + "tsv": "0.4.1", + "svelte": "5.57.0", + "acorn": "8.16.0", + "acorn_ts": "1.0.13", + "prettier": "3.9.6", + "prettier_svelte": "4.1.1", + "oxc_parser": "0.150.0", + "oxc_parser_wasm": "0.142.0", + "oxfmt": "0.68.0", + "yuku_parser": "0.10.1", + "yuku_parser_wasm": "0.10.1", + "biome": "2.5.13", + "dprint": "0.96.1", + "malva": "0.16.0", + "postcss": "8.5.28", + "rsvelte_fmt": "0.7.23", + "rsvelte_parse": "0.3.14", + "rsvelte_parse_svelte_target": "5.57.0", + "swc": "1.16.2", + "tsc": "6.0.3" + }, + "binary_sizes": [ + { + "label": "tsv (ffi)", + "bytes": 3367824, + "kind": "native", + "gzip_bytes": 1550678 + }, + { + "label": "tsv format (ffi)", + "bytes": 3061808, + "kind": "native", + "gzip_bytes": 1410750 + }, + { + "label": "tsv parse (ffi)", + "bytes": 1529104, + "kind": "native", + "gzip_bytes": 674282 + }, + { + "label": "tsv (napi)", + "bytes": 3751072, + "kind": "native", + "gzip_bytes": 1707953 + }, + { + "label": "tsv-format-wasm", + "bytes": 2250575, + "kind": "wasm", + "gzip_bytes": 866596 + }, + { + "label": "tsv-parse-wasm", + "bytes": 966971, + "kind": "wasm", + "gzip_bytes": 376149 + }, + { + "label": "tsv-wasm", + "bytes": 2527817, + "kind": "wasm", + "gzip_bytes": 965297 + }, + { + "label": "svelte + acorn-typescript parsers (js bundle)", + "bytes": 497215, + "kind": "js", + "gzip_bytes": 123976 + }, + { + "label": "prettier + svelte plugin (js bundle)", + "bytes": 2211694, + "kind": "js", + "gzip_bytes": 566119 + }, + { + "label": "prettier + parsers (js bundle)", + "bytes": 2212134, + "kind": "js", + "gzip_bytes": 566281 + }, + { + "label": "biome (wasm)", + "bytes": 44645975, + "kind": "wasm", + "gzip_bytes": 11431360 + }, + { + "label": "dprint (wasm)", + "bytes": 4174185, + "kind": "wasm", + "gzip_bytes": 1155127 + }, + { + "label": "oxc-parser (napi)", + "bytes": 2121248, + "kind": "native", + "gzip_bytes": 882576 + }, + { + "label": "oxfmt (napi)", + "bytes": 9106232, + "kind": "native", + "gzip_bytes": 3713019 + }, + { + "label": "oxc-parser (wasm)", + "bytes": 1486624, + "kind": "wasm", + "gzip_bytes": 481382 + }, + { + "label": "yuku-parser (napi)", + "bytes": 819208, + "kind": "native", + "gzip_bytes": 338105 + }, + { + "label": "yuku-parser (wasm)", + "bytes": 743273, + "kind": "wasm", + "gzip_bytes": 222790 + }, + { + "label": "rsvelte-fmt (binary)", + "bytes": 8924696, + "kind": "native", + "gzip_bytes": 3534045 + }, + { + "label": "malva (wasm)", + "bytes": 1465125, + "kind": "wasm", + "gzip_bytes": 414019 + }, + { + "label": "rsvelte compiler (napi)", + "bytes": 17639744, + "kind": "native", + "gzip_bytes": 7385190 + }, + { + "label": "swc (napi)", + "bytes": 32744704, + "kind": "native", + "gzip_bytes": 12223661 + } + ], + "binary_sizes_absent": [], + "entries": [ + { + "name": "svelte/compiler", + "group": "parse/svelte", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "cv_raw": null, + "drift": null, + "raw_sample_size": null, + "outlier_ratio": null, + "warmup_iterations": null, + "min_iterations": null, + "settled_heap_bytes": null, + "files_processed": 4619, + "files_total": 4619, + "files_iterated": null, + "files_iterated_digest": null, + "payload": "drop_in", + "runtime": "node" + }, + { + "name": "tsv-json", + "group": "parse/svelte", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "cv_raw": null, + "drift": null, + "raw_sample_size": null, + "outlier_ratio": null, + "warmup_iterations": null, + "min_iterations": null, + "settled_heap_bytes": null, + "files_processed": 4619, + "files_total": 4619, + "files_iterated": null, + "files_iterated_digest": null, + "payload": "drop_in", + "runtime": "node" + }, + { + "name": "tsv-wasm-json", + "group": "parse/svelte", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "cv_raw": null, + "drift": null, + "raw_sample_size": null, + "outlier_ratio": null, + "warmup_iterations": null, + "min_iterations": null, + "settled_heap_bytes": null, + "files_processed": 4619, + "files_total": 4619, + "files_iterated": null, + "files_iterated_digest": null, + "payload": "drop_in", + "runtime": "node" + }, + { + "name": "tsv-json-no-locations", + "group": "parse/svelte", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "cv_raw": null, + "drift": null, + "raw_sample_size": null, + "outlier_ratio": null, + "warmup_iterations": null, + "min_iterations": null, + "settled_heap_bytes": null, + "files_processed": 4619, + "files_total": 4619, + "files_iterated": null, + "files_iterated_digest": null, + "payload": "span_only", + "runtime": "node" + }, + { + "name": "tsv-wasm-json-no-locations", + "group": "parse/svelte", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "cv_raw": null, + "drift": null, + "raw_sample_size": null, + "outlier_ratio": null, + "warmup_iterations": null, + "min_iterations": null, + "settled_heap_bytes": null, + "files_processed": 4619, + "files_total": 4619, + "files_iterated": null, + "files_iterated_digest": null, + "payload": "span_only", + "runtime": "node" + }, + { + "name": "tsv-internal", + "group": "parse/svelte", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "cv_raw": null, + "drift": null, + "raw_sample_size": null, + "outlier_ratio": null, + "warmup_iterations": null, + "min_iterations": null, + "settled_heap_bytes": null, + "files_processed": 4619, + "files_total": 4619, + "files_iterated": null, + "files_iterated_digest": null, + "payload": "none", + "runtime": "node" + }, + { + "name": "tsv-wasm-internal", + "group": "parse/svelte", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "cv_raw": null, + "drift": null, + "raw_sample_size": null, + "outlier_ratio": null, + "warmup_iterations": null, + "min_iterations": null, + "settled_heap_bytes": null, + "files_processed": 4619, + "files_total": 4619, + "files_iterated": null, + "files_iterated_digest": null, + "payload": "none", + "runtime": "node" + }, + { + "name": "rsvelte-parse", + "group": "parse/svelte", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "cv_raw": null, + "drift": null, + "raw_sample_size": null, + "outlier_ratio": null, + "warmup_iterations": null, + "min_iterations": null, + "settled_heap_bytes": null, + "files_processed": 4619, + "files_total": 4619, + "files_iterated": null, + "files_iterated_digest": null, + "payload": "drop_in", + "runtime": "node" + }, + { + "name": "rsvelte-parse-skip-expr-loc", + "group": "parse/svelte", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "cv_raw": null, + "drift": null, + "raw_sample_size": null, + "outlier_ratio": null, + "warmup_iterations": null, + "min_iterations": null, + "settled_heap_bytes": null, + "files_processed": 4619, + "files_total": 4619, + "files_iterated": null, + "files_iterated_digest": null, + "payload": "own_shape", + "runtime": "node" + }, + { + "name": "acorn-typescript", + "group": "parse/typescript", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "cv_raw": null, + "drift": null, + "raw_sample_size": null, + "outlier_ratio": null, + "warmup_iterations": null, + "min_iterations": null, + "settled_heap_bytes": null, + "files_processed": 52614, + "files_total": 53323, + "files_iterated": null, + "files_iterated_digest": null, + "payload": "drop_in", + "runtime": "node" + }, + { + "name": "tsv-json", + "group": "parse/typescript", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "cv_raw": null, + "drift": null, + "raw_sample_size": null, + "outlier_ratio": null, + "warmup_iterations": null, + "min_iterations": null, + "settled_heap_bytes": null, + "files_processed": 53217, + "files_total": 53323, + "files_iterated": null, + "files_iterated_digest": null, + "payload": "drop_in", + "runtime": "node" + }, + { + "name": "tsv-wasm-json", + "group": "parse/typescript", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "cv_raw": null, + "drift": null, + "raw_sample_size": null, + "outlier_ratio": null, + "warmup_iterations": null, + "min_iterations": null, + "settled_heap_bytes": null, + "files_processed": 53217, + "files_total": 53323, + "files_iterated": null, + "files_iterated_digest": null, + "payload": "drop_in", + "runtime": "node" + }, + { + "name": "tsv-json-no-locations", + "group": "parse/typescript", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "cv_raw": null, + "drift": null, + "raw_sample_size": null, + "outlier_ratio": null, + "warmup_iterations": null, + "min_iterations": null, + "settled_heap_bytes": null, + "files_processed": 53217, + "files_total": 53323, + "files_iterated": null, + "files_iterated_digest": null, + "payload": "span_only", + "runtime": "node" + }, + { + "name": "tsv-wasm-json-no-locations", + "group": "parse/typescript", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "cv_raw": null, + "drift": null, + "raw_sample_size": null, + "outlier_ratio": null, + "warmup_iterations": null, + "min_iterations": null, + "settled_heap_bytes": null, + "files_processed": 53217, + "files_total": 53323, + "files_iterated": null, + "files_iterated_digest": null, + "payload": "span_only", + "runtime": "node" + }, + { + "name": "tsv-internal", + "group": "parse/typescript", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "cv_raw": null, + "drift": null, + "raw_sample_size": null, + "outlier_ratio": null, + "warmup_iterations": null, + "min_iterations": null, + "settled_heap_bytes": null, + "files_processed": 53217, + "files_total": 53323, + "files_iterated": null, + "files_iterated_digest": null, + "payload": "none", + "runtime": "node" + }, + { + "name": "tsv-wasm-internal", + "group": "parse/typescript", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "cv_raw": null, + "drift": null, + "raw_sample_size": null, + "outlier_ratio": null, + "warmup_iterations": null, + "min_iterations": null, + "settled_heap_bytes": null, + "files_processed": 53217, + "files_total": 53323, + "files_iterated": null, + "files_iterated_digest": null, + "payload": "none", + "runtime": "node" + }, + { + "name": "oxc-parser", + "group": "parse/typescript", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "cv_raw": null, + "drift": null, + "raw_sample_size": null, + "outlier_ratio": null, + "warmup_iterations": null, + "min_iterations": null, + "settled_heap_bytes": null, + "files_processed": 53181, + "files_total": 53323, + "files_iterated": null, + "files_iterated_digest": null, + "payload": "span_only", + "runtime": "node" + }, + { + "name": "oxc-parser-wasm", + "group": "parse/typescript", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "cv_raw": null, + "drift": null, + "raw_sample_size": null, + "outlier_ratio": null, + "warmup_iterations": null, + "min_iterations": null, + "settled_heap_bytes": null, + "files_processed": 53183, + "files_total": 53323, + "files_iterated": null, + "files_iterated_digest": null, + "payload": "span_only", + "runtime": "node" + }, + { + "name": "tsc", + "group": "parse/typescript", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "cv_raw": null, + "drift": null, + "raw_sample_size": null, + "outlier_ratio": null, + "warmup_iterations": null, + "min_iterations": null, + "settled_heap_bytes": null, + "files_processed": 53247, + "files_total": 53323, + "files_iterated": null, + "files_iterated_digest": null, + "payload": "own_shape", + "runtime": "node" + }, + { + "name": "yuku-parser-wasm", + "group": "parse/typescript", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "cv_raw": null, + "drift": null, + "raw_sample_size": null, + "outlier_ratio": null, + "warmup_iterations": null, + "min_iterations": null, + "settled_heap_bytes": null, + "files_processed": 53162, + "files_total": 53323, + "files_iterated": null, + "files_iterated_digest": null, + "payload": "span_only", + "runtime": "node" + }, + { + "name": "swc", + "group": "parse/typescript", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "cv_raw": null, + "drift": null, + "raw_sample_size": null, + "outlier_ratio": null, + "warmup_iterations": null, + "min_iterations": null, + "settled_heap_bytes": null, + "files_processed": 52624, + "files_total": 53323, + "files_iterated": null, + "files_iterated_digest": null, + "payload": "own_shape", + "runtime": "node" + }, + { + "name": "svelte/compiler", + "group": "parse/css", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "cv_raw": null, + "drift": null, + "raw_sample_size": null, + "outlier_ratio": null, + "warmup_iterations": null, + "min_iterations": null, + "settled_heap_bytes": null, + "files_processed": 22242, + "files_total": 22449, + "files_iterated": null, + "files_iterated_digest": null, + "payload": "drop_in", + "runtime": "node" + }, + { + "name": "tsv-json", + "group": "parse/css", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "cv_raw": null, + "drift": null, + "raw_sample_size": null, + "outlier_ratio": null, + "warmup_iterations": null, + "min_iterations": null, + "settled_heap_bytes": null, + "files_processed": 22295, + "files_total": 22449, + "files_iterated": null, + "files_iterated_digest": null, + "payload": "drop_in", + "runtime": "node" + }, + { + "name": "tsv-wasm-json", + "group": "parse/css", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "cv_raw": null, + "drift": null, + "raw_sample_size": null, + "outlier_ratio": null, + "warmup_iterations": null, + "min_iterations": null, + "settled_heap_bytes": null, + "files_processed": 22295, + "files_total": 22449, + "files_iterated": null, + "files_iterated_digest": null, + "payload": "drop_in", + "runtime": "node" + }, + { + "name": "tsv-internal", + "group": "parse/css", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "cv_raw": null, + "drift": null, + "raw_sample_size": null, + "outlier_ratio": null, + "warmup_iterations": null, + "min_iterations": null, + "settled_heap_bytes": null, + "files_processed": 22295, + "files_total": 22449, + "files_iterated": null, + "files_iterated_digest": null, + "payload": "none", + "runtime": "node" + }, + { + "name": "tsv-wasm-internal", + "group": "parse/css", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "cv_raw": null, + "drift": null, + "raw_sample_size": null, + "outlier_ratio": null, + "warmup_iterations": null, + "min_iterations": null, + "settled_heap_bytes": null, + "files_processed": 22295, + "files_total": 22449, + "files_iterated": null, + "files_iterated_digest": null, + "payload": "none", + "runtime": "node" + }, + { + "name": "postcss", + "group": "parse/css", + "mean_ns": null, + "p50_ns": null, + "p75_ns": null, + "p90_ns": null, + "p95_ns": null, + "p99_ns": null, + "min_ns": null, + "max_ns": null, + "std_dev_ns": null, + "cv": null, + "ops_per_second": null, + "sample_size": null, + "cv_raw": null, + "drift": null, + "raw_sample_size": null, + "outlier_ratio": null, + "warmup_iterations": null, + "min_iterations": null, + "settled_heap_bytes": null, + "files_processed": 22361, + "files_total": 22449, + "files_iterated": null, + "files_iterated_digest": null, + "payload": "own_shape", + "runtime": "node" + } + ], + "suppressed_noise": {}, + "output_digest_ungraded": { + "parse/typescript/tsv-json": 1, + "parse/typescript/tsv-wasm-json": 1, + "parse/typescript/tsv-json-no-locations": 1, + "parse/typescript/tsv-wasm-json-no-locations": 1 + }, + "variant_parity": [ + { + "group": "parse/typescript", + "impl": "oxc-parser", + "sibling": "oxc-parser-wasm", + "impl_only": 0, + "sibling_only": 2, + "output_mismatch": 0, + "output_mismatch_examples": [] + } + ], + "unavailable": [] +} \ No newline at end of file diff --git a/src/routes/docs/conformance/conformance.ts b/src/routes/docs/conformance/conformance.ts new file mode 100644 index 0000000..3d5f489 --- /dev/null +++ b/src/routes/docs/conformance/conformance.ts @@ -0,0 +1,5 @@ +import type { BenchmarkBaseline } from '../benchmarks/benchmark_data.ts'; + +import json from './conformance.json' with { type: 'json' }; + +export const conformance_json: BenchmarkBaseline = json as BenchmarkBaseline; diff --git a/src/routes/docs/conformance/conformance_data.ts b/src/routes/docs/conformance/conformance_data.ts new file mode 100644 index 0000000..69b990a --- /dev/null +++ b/src/routes/docs/conformance/conformance_data.ts @@ -0,0 +1,322 @@ +// The parse-conformance domain: the per-source coverage matrices +// `ConformanceTable.svelte` renders, derived from the conformance report (the +// per-runtime report shape, `corpus_kind: 'conformance'`). + +import { + compare_group_order, + corpus_source_url, + parse_group_key, + type BaselineVersions, + type BenchmarkBaseline, + type SourceCoverageCell +} from '../benchmarks/benchmark_data.ts'; + +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 at one release — 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 oxc, whose wasm + * binding is pinned to an older release, so only the native row is the current + * engine; and 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: it selected the compiler cases (`CONFORMANCE_SELECTORS`). + tsc: 'tsc', + postcss: 'PostCSS' +}; + +/** + * Qualifiers rendered beside an engine's name, keyed by report entry name. The + * table is per engine and says nothing about bindings — so a note here is for + * the one case where WHICH binding stands in is worth saying. + */ +const CONFORMANCE_ROW_NOTES: Record = { + // the native binding segfaults on this corpus, spelled out in the page's notes + 'yuku-parser-wasm': 'wasm' +}; + +/** + * The report's `versions` keys behind each engine column, keyed by display name. + * The report carries the harness's whole version map, formatters included, so the + * page's meta panel lists only these. The shape test holds every column to an + * entry and every key to the report. + */ +export const CONFORMANCE_ENGINE_VERSIONS: Record> = { + // the meta panel shows tsv's under the run + tsv: [], + 'svelte/compiler': ['svelte'], + 'acorn-typescript': ['acorn', 'acorn_ts'], + // both bindings, since the page's notes set the native column against the wasm one + 'oxc-parser': ['oxc_parser', 'oxc_parser_wasm'], + 'yuku-parser': ['yuku_parser_wasm'], + rsvelte: ['rsvelte_parse', 'rsvelte_parse_svelte_target'], + swc: ['swc'], + tsc: ['tsc'], + PostCSS: ['postcss'] +}; + +/** Every `versions` key `CONFORMANCE_ENGINE_VERSIONS` names, for the page's meta panel. */ +export const CONFORMANCE_VERSION_KEYS: Array = Object.values( + CONFORMANCE_ENGINE_VERSIONS +).flat(); + +/** + * 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 keyed = baseline.entries.flatMap((entry) => { + const { operation, language } = parse_group_key(entry.group); + const name = CONFORMANCE_ENGINE_NAMES[entry.name]; + const { files_processed, files_total } = entry; + if (operation !== 'parse' || !language || !name) return []; + if (files_processed == null || files_total == null) return []; + const row: ConformanceRow = { + name, + note: CONFORMANCE_ROW_NOTES[entry.name], + files_processed, + files_total, + coverage_fraction: files_total > 0 ? files_processed / files_total : 0 + }; + return [{ language, row }]; + }); + + const result: Array = []; + for (const [language, keyed_rows] of Map.groupBy(keyed, (k) => k.language)) { + const rows = keyed_rows.map((k) => k.row); + // 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 + }); + } + // every group here is a parse group, so this orders them by language + result.sort((a, b) => + compare_group_order({ operation: 'parse', ...a }, { operation: 'parse', ...b }) + ); + return result; +}; + +// The two harvested TypeScript caches, as the report's `coverage_by_source` keys them. +const SOURCE_TEST262 = 'benches/js/.cache/test262_files.json'; +const SOURCE_TS_REPO = 'benches/js/.cache/ts_repo_files.json'; + +/** + * Reader-facing names for the report's corpus source paths. A path missing here + * renders raw in the matrix, and the shape test fails on it, so a source tsv adds can't reach + * the page as a cache path unnoticed. + */ +export const CONFORMANCE_SOURCE_LABELS: Record = { + '../prettier-plugin-svelte/test': "prettier-plugin-svelte's tests", + '../prettier/tests/format/typescript': "Prettier's TypeScript fixtures", + '../prettier/tests/format/js': "Prettier's JS fixtures", + '../prettier/tests/format/css': "Prettier's CSS fixtures", + '../prettier/tests/format/html': "Prettier's HTML fixtures", + '../svelte/packages/svelte/tests': "Svelte's tests", + 'benches/js/.cache/wpt_css': 'web-platform-tests CSS', + [SOURCE_TEST262]: 'test262', + [SOURCE_TS_REPO]: "TypeScript compiler's cases" +}; + +/** + * The engine that SELECTED a corpus source, so reads 100% on it by construction + * rather than by achievement: group key → source path (`*` for every source of + * the group) → engine display name. Hand-stated because the report carries no + * such field — svelte/compiler's rejects are excluded from the whole Svelte set, + * and tsc kept only the compiler cases it parses cleanly. test262 has no entry: + * its own metadata picks the expected-valid tests, never tsv's verdict, so tsv's + * 100% there is a result. The shape test holds every entry to the report: it + * must resolve, and its cell must be full. + */ +export const CONFORMANCE_SELECTORS: Record> = { + 'parse/svelte': { '*': 'svelte/compiler' }, + 'parse/typescript': { [SOURCE_TS_REPO]: 'tsc' } +}; + +/** One engine's coverage of one corpus source, or of the whole group. */ +export interface ConformanceCell { + processed: number; + total: number; + // total - processed, the magnitude a near-100% percentage compresses + rejected: number; + coverage_fraction: number; + // this engine selected the source, so the cell is 100% by construction + selected: boolean; +} + +/** A matrix column — one engine, named as `derive_conformance_groups` names it. */ +export interface ConformanceEngine { + name: string; + note?: string; +} + +/** A corpus source a matrix row covers, for its linked label. */ +export interface ConformanceSourceOrigin { + path: string; + // `undefined` for a path `CONFORMANCE_SOURCE_LABELS` lacks, which renders raw + label: string | undefined; + url: string | undefined; +} + +export interface ConformanceSourceRow { + // one origin, or several for the folded row + origins: Array; + // the sources every engine accepts in full, folded into one trailing row + folded: boolean; + files: number; + // aligned to the matrix's `engines`; `undefined` where the report lacks the cell + cells: Array; +} + +export interface ConformanceMatrix { + language: string; + files_total: number; + // ordered as `derive_conformance_groups` orders its rows + engines: Array; + // largest source first, the folded row last; empty when the report lacks + // `coverage_by_source` + sources: Array; + // the whole group per engine, aligned to `engines` + aggregate: Array; +} + +const to_conformance_cell = ( + processed: number, + total: number, + selected: boolean +): ConformanceCell => ({ + processed, + total, + rejected: total - processed, + coverage_fraction: total > 0 ? processed / total : 0, + selected +}); + +// a source no engine selected and none rejects any of says nothing a reader can +// compare, so two or more of them fold into one row; a group with a `*` selector +// (Svelte) never folds, so its rows keep showing which 100% is by construction +const is_foldable = (row: ConformanceSourceRow): boolean => + row.cells.some((cell) => cell !== undefined) && + row.cells.every((cell) => cell === undefined || (!cell.selected && cell.rejected === 0)); + +const fold_conformance_rows = (rows: Array): ConformanceSourceRow => ({ + origins: rows.flatMap((row) => row.origins), + folded: true, + files: rows.reduce((sum, row) => sum + row.files, 0), + cells: (rows[0]?.cells ?? []).map((_, i) => { + const cells = rows.map((row) => row.cells[i]); + if (!cells.every((cell) => cell !== undefined)) return undefined; + return to_conformance_cell( + cells.reduce((sum, cell) => sum + cell.processed, 0), + cells.reduce((sum, cell) => sum + cell.total, 0), + false + ); + }) +}); + +/** + * Derives one coverage matrix per language from a conformance report: a row per + * corpus source and a column per engine, with the group aggregate alongside. The + * aggregate blends sources that answer different questions, so the rows are the + * finding — and a cell whose engine selected the source (`CONFORMANCE_SELECTORS`) + * is flagged rather than left to read as a result. Engines fold and order as in + * `derive_conformance_groups`. + */ +export const derive_conformance_matrices = ( + baseline: BenchmarkBaseline +): Array => + derive_conformance_groups(baseline).map((group) => { + const group_key = `parse/${group.language}`; + const selectors = CONFORMANCE_SELECTORS[group_key]; + const engines = group.rows.map(({ name, note }): ConformanceEngine => ({ name, note })); + + const rows = Object.entries(baseline.coverage_by_source?.[group_key] ?? {}).map( + ([path, by_impl]): ConformanceSourceRow => { + const by_engine: Record = {}; + for (const [impl, cell] of Object.entries(by_impl)) { + const name = CONFORMANCE_ENGINE_NAMES[impl]; + if (name) by_engine[name] ??= cell; + } + const selector = selectors?.[path] ?? selectors?.['*']; + // every impl shares the slice total, which the shape test holds + const files = Math.max(0, ...Object.values(by_impl).map((cell) => cell.total)); + const source = baseline.corpus_sources.find((s) => s.path === path); + return { + origins: [ + { + path, + label: CONFORMANCE_SOURCE_LABELS[path], + url: source && corpus_source_url(source) + } + ], + folded: false, + files, + cells: engines.map((engine) => { + const cell = by_engine[engine.name]; + return ( + cell && to_conformance_cell(cell.processed, cell.total, engine.name === selector) + ); + }) + }; + } + ); + + const foldable = rows.filter(is_foldable); + const sources = foldable.length > 1 ? rows.filter((row) => !is_foldable(row)) : rows; + // largest first, path as a stable tiebreak + sources.sort( + (a, b) => + b.files - a.files || (a.origins[0]?.path ?? '').localeCompare(b.origins[0]?.path ?? '') + ); + if (foldable.length > 1) sources.push(fold_conformance_rows(foldable)); + + return { + language: group.language, + files_total: group.files_total, + engines, + sources, + // an engine that selected every source selected the group + aggregate: group.rows.map((row) => + to_conformance_cell(row.files_processed, row.files_total, row.name === selectors?.['*']) + ) + }; + }); diff --git a/src/routes/docs/introduction/+page.svelte b/src/routes/docs/introduction/+page.svelte index dfefa5f..9469701 100644 --- a/src/routes/docs/introduction/+page.svelte +++ b/src/routes/docs/introduction/+page.svelte @@ -4,104 +4,145 @@ import TomeLink from '@fuzdev/fuz_ui/TomeLink.svelte'; import TomeSection from '@fuzdev/fuz_ui/TomeSection.svelte'; import TomeSectionHeader from '@fuzdev/fuz_ui/TomeSectionHeader.svelte'; - import {tome_get_by_slug} from '@fuzdev/fuz_ui/tome.ts'; + import { tome_get_by_slug } from '@fuzdev/fuz_ui/tome.ts'; import Svg from '@fuzdev/fuz_ui/Svg.svelte'; - import {logo_tsv} from '@fuzdev/fuz_ui/logos.ts'; + import { logo_tsv } from '@fuzdev/fuz_ui/logos.ts'; + + import { + format_example, + no_locations_example, + parse_example, + usage_example + } from './introduction_examples.ts'; const LIBRARY_ITEM_NAME = 'introduction'; const tome = tome_get_by_slug(LIBRARY_ITEM_NAME); - - const usage_example = `import {format_svelte, parse_svelte, type Root} from '@fuzdev/tsv_wasm'; - -const formatted = format_svelte('

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

    - Compared to Oxc, Biome, and SWC, tsv is a set of focused tools, not a generic language - platform, so the focus is web standards and there's no support for JSX/SCSS/etc, beyond Svelte - as the only JS framework. The extensibility story is currently limited to using its Rust - crates as libraries; bridging to JS or WASM plugins is an open question, but may not be - supported. + tsv aims to simplify its covered domains and stay lean, and so it makes opinionated choices. + The formatter has a single non-configurable style, using Svelte's Prettier config. Among other + benefits this means tsv doesn't depend on a JS runtime, which it would need to resolve configs + like Prettier. +

    +

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

    +

    + Compared to rsvelte, tsv has its own + TS/JS/CSS parsers instead of using Oxc, and rsvelte additionally has a Svelte compiler and + linter/typechecker integration (the full toolchain; tsv has some in-progress work here, scope + unknown and may never ship).

    tsv prioritizes, in order:

      -
    1. correctness (Svelte and TypeScript conformance, spec adherence for HTML/CSS/JS)
    2. +
    3. correctness (spec conformance for HTML/CSS/JS, fidelity to Svelte and TypeScript)
    4. speed
    5. binary size and memory usage
    6. -
    7. extensibility (valued but deprioritized)
    8. +
    9. extensibility, modularity, reusability

    - See the benchmarks for stats. Compared to Oxc - and Biome, tsv is significantly faster, smaller, and uses less memory to parse and format its supported - languages + Staying simple is an over-arching goal, and sometimes at odds with flexibility. Feedback is + welcome to help navigate these tradeoffs. +

    +

    + See the for measurements. Compared to Oxc/oxfmt and Biome, tsv + is smaller and faster at parsing and formatting its supported languages, but lacks their + features, extensibility, and broad language support. One reason for tsv to exist is to help + find the performance bonuses left on the table in the Web's implementations.

    - This is an early release, and reports and feedback are appreciated - see the - issues and - discussions. + tsv is near production-ready, with a long tail of rare bugs (and numerous fixes to bugs in + acorn-typescript/Prettier/prettier-plugin-svelte), and APIs may still change. Reports and + feedback are appreciated. See the issues + and discussions.

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

    - These docs are a work in progress. For design details see the readme. + These docs are a work in progress. There are more design details in the + readme.

    -

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

    +

    + For format-on-save in VSCode and vsix-compatible editors, install the + + fuzdev.tsv-format extension + . It runs tsv's wasm build, so it works in both desktop VSCode and the browser host: +

    + +

    + tsv is published to npm as + @fuzdev/tsv with native + binaries: +

    +

    + tsv format writes changed files in place; --check writes nothing + and exits 1 if any file would change. Inside a git repo, discovery honors + .gitignore, .prettierignore, and .formatignore. +

    +

    + The native package covers Linux (x64 gnu and musl, arm64 gnu), macOS (arm64 and x64), and + Windows x64 — anywhere else, use the wasm build below. +

    +

    + The same CLI binaries are also attached to each + GitHub Release with a + SHA256SUMS and a build provenance attestation, for use without npm. +

    +

    + tsv also ships as wasm, which runs everywhere including browsers and Deno, and carries the + same tsv CLI (formatting across worker threads, so --jobs works + there too): +

    +

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

    See the for size and performance details.

    -

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

    -

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

    +

    + All four packages share one API — the same function names, options, and errors — so + @fuzdev/tsv and @fuzdev/tsv-wasm are drop-in swaps for each other. + Both export the formatter and parser together: +

    The formatter alone:

    @@ -110,38 +151,74 @@ reconstruct_locations(ast, 'const x = 1;');`;

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

    +

    + Every parser also takes an acorn-style options object: + for the span-only AST (below), and + for TypeScript + (default ). format_typescript takes + sourceType too; without it, formatting retries as a script when the module + parse fails, so a legacy sloppy script needs no options. +

    +

    + The native package needs no initialization; the wasm packages work zero-config in Node.js, + Bun, and Deno (sync auto-init), and browsers and bundlers call + once first.

    - For TypeScript and Svelte, the parsers also emit a span-only AST that drops the per-node - loc (line/column) object — Svelte also drops name_loc — for a ~46% - smaller, faster-to-materialize result, mirroring acorn's locations: false. Line - and column stay derivable from the start/end offsets plus your source, - so nothing is lost when you have the source. + The parsers have a span-only mode that skips the per-node line/column, making the AST ~46% + smaller and faster to hand to JS, and you can derive line and column later without + re-parsing. It's oxc-parser's default; tsv emits loc by default so the bare + call is a drop-in for Svelte's parser, though that default may change.

    - reconstruct_locations(ast, source) walks the tree and adds loc - back, mutating in place — exact for TypeScript, approximate for Svelte (it skips the parser's - own - name_loc and a couple of position quirks). For sparse lookups, - create_locator(source) reuses one line table across calls. CSS has no - loc, so there's no span-only variant for it. + Even when you need line/column, reconstructing it in JS beats the loc-bearing + AST end to end, by ~1.7x on TypeScript, as measured in + + tsv's bench report + . reconstruct_locations ships in every package that parses.

    +

    Details:

    +
      +
    • + Span-only drops the per-node loc object (and name_loc on Svelte + nodes), mirroring acorn's . The + rest is unchanged, so every node keeps its start/end offsets. +
    • +
    • + walks the tree and + adds loc back, mutating in place — exact for TypeScript, approximate for + Svelte, where it throws on the rare input it can't reconstruct rather than guess (parse + those with loc). +
    • +
    • + For sparse lookups, + reuses one line table across calls, so you pay for the positions you actually ask for; + pass for a .svelte + document. +
    • +
    • + CSS nodes carry no loc to begin with, so + is accepted as an inert no-op + there. +
    • +
    diff --git a/src/routes/docs/introduction/introduction_examples.ts b/src/routes/docs/introduction/introduction_examples.ts new file mode 100644 index 0000000..786c50d --- /dev/null +++ b/src/routes/docs/introduction/introduction_examples.ts @@ -0,0 +1,24 @@ +// The introduction's code samples. Kept in a `.ts` module so the `` in the +// Svelte strings reads literally — inside a `.svelte` file's script block it would +// close the block, and the `<\/script>` escape it needs there would show on the page. + +export const usage_example = `import { format_svelte, parse_svelte, type Root } from '@fuzdev/tsv'; + +const formatted = format_svelte(''); +const ast: Root = parse_svelte('');`; + +export const format_example = `import { format_svelte } from '@fuzdev/tsv-format-wasm'; + +const formatted = format_svelte('');`; + +export const parse_example = `import { parse_svelte, type Root } from '@fuzdev/tsv-parse-wasm'; + +const ast: Root = parse_svelte('');`; + +export const no_locations_example = `import { parse_typescript, reconstruct_locations } from '@fuzdev/tsv-parse-wasm'; + +// span-only AST: start/end offsets, no per-node loc +const ast = parse_typescript('const x = 1;', { locations: false }); + +// derive line/column back when you need it, no re-parse +reconstruct_locations(ast, 'const x = 1;');`; diff --git a/src/routes/docs/playground/+page.svelte b/src/routes/docs/playground/+page.svelte index f9c38ed..94fe5dd 100644 --- a/src/routes/docs/playground/+page.svelte +++ b/src/routes/docs/playground/+page.svelte @@ -1,16 +1,16 @@ @@ -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..b39ec62 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,25 +24,42 @@ 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 + // 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 // — no blur or button. 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) }; } }; const formatted = $derived.by(() => run((t) => t.format_svelte(source))); - const ast = $derived.by(() => run((t) => JSON.stringify(t.parse_svelte(source), null, 2))); - // format and parse fail together on malformed input — surface one message, not two - const error = $derived(formatted?.error ?? ast?.error ?? null); + // The AST pane trails the editor by an idle beat while the formatted pane stays + // live. Formatting is cheap (~1 ms on a 9 KB component), but the AST is parsed, + // serialized to JSON, and syntax-highlighted — half a megabyte of it for that + // same component — and rebuilding that DOM per keystroke is what a large paste + // feels. The default example is small enough that the delay never shows. + const AST_DEBOUNCE_MS = 150; + // seeded from the example, like `source`, so the first render has an AST to show + let ast_source = $state(playground_example); + $effect(() => { + const next = source; + const id = setTimeout(() => (ast_source = next), AST_DEBOUNCE_MS); + return () => clearTimeout(id); + }); + const ast = $derived.by(() => run((t) => JSON.stringify(t.parse_svelte(ast_source), null, 2))); + + // The top-level error is the LIVE pane's, so it tracks what is in the editor now; + // the AST pane renders its own beside its (debounced, possibly still-broken) + // source rather than folding it in here, where it would read as a message about + // text the user has already fixed. + const error = $derived(formatted?.error ?? null); // CodeTextarea highlights via the experimental CSS Custom Highlight API; where // it's unsupported the editor still works but shows no token colors. Defaulting to @@ -55,11 +73,10 @@ }); $effect(() => { - if (tsv) return; 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,20 +89,22 @@ }; }); + const is_base = $derived(source === playground_example); + const reset = (): void => { source = playground_example; }; // format the editable source in place — writes the formatted result back into - // the editor; no-op while the WASM is loading or the input doesn't parse + // the editor; no-op while the wasm is loading or the input doesn't parse const format = (): void => { if (!formatted || formatted.error !== null) return; source = formatted.value; }; -
    - +
    + @@ -107,12 +126,21 @@ {#if !ready}

    loading the formatter…

    {:else if error} -

    {error}

    +

    {error}

    {:else} -

    formatted

    +

    formatted:

    -

    AST

    - +

    AST:

    + {#if ast?.error} + +

    {ast.error}

    + {:else} +
    + + +
    + {/if} {/if}
    @@ -132,11 +160,27 @@ section :global(.ast) { max-height: 500px; } - .error { - color: var(--color_e_40); - white-space: pre-wrap; + /* 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); } - .parse_error { + /* 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_c_50); white-space: pre-wrap; } diff --git a/src/routes/docs/tomes.ts b/src/routes/docs/tomes.ts index 733ca43..604e3e3 100644 --- a/src/routes/docs/tomes.ts +++ b/src/routes/docs/tomes.ts @@ -1,16 +1,17 @@ -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'; +import ConformancePage from './conformance/+page.svelte'; export const tomes: Array = [ { slug: 'introduction', category: 'guide', Component: IntroductionPage, - related_tomes: ['playground', 'benchmarks'], + related_tomes: ['playground', 'benchmarks', 'conformance'], related_modules: [], - related_declarations: [], + related_declarations: [] }, { slug: 'playground', @@ -18,14 +19,22 @@ export const tomes: Array = [ Component: PlaygroundPage, related_tomes: ['introduction', 'benchmarks'], related_modules: [], - related_declarations: [], + related_declarations: [] }, { slug: 'benchmarks', category: 'guide', Component: BenchmarksPage, - related_tomes: ['introduction', 'playground'], + related_tomes: ['introduction', 'playground', 'conformance'], related_modules: [], - related_declarations: [], + related_declarations: [] }, + { + slug: 'conformance', + category: 'guide', + Component: ConformancePage, + related_tomes: ['benchmarks'], + related_modules: [], + 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..735241b 100644 --- a/src/routes/style.css +++ b/src/routes/style.css @@ -1,9 +1,4 @@ -/* global stylesheet imported by `src/routes/+layout.svelte */ +/* global stylesheet imported by `src/routes/+layout.svelte` */ -/* - -:root { - --some_stylish_property: 100%; -} - -*/ +/* Nothing global yet — the benchmarks page's shared classes live beside it in + `docs/benchmarks/benchmarks.css`, so they ship with that route. */ diff --git a/src/test/benchmark_baseline.test.ts b/src/test/benchmark_baseline.test.ts new file mode 100644 index 0000000..a5f1cd0 --- /dev/null +++ b/src/test/benchmark_baseline.test.ts @@ -0,0 +1,48 @@ +import { assert, describe, test } from 'vitest'; + +import { + baseline_ratio_color, + compute_baseline_ratio, + to_baseline_key +} from '$routes/docs/benchmarks/benchmark_baseline.ts'; + +describe('compute_baseline_ratio', () => { + test('reads the anchor as the reference — a lower measurement exceeds 1', () => { + // anchor 100; an entry at 50 (ns or bytes) is 2x better, one at 200 is half as good + assert.strictEqual(compute_baseline_ratio(50, 100), 2); + assert.strictEqual(compute_baseline_ratio(200, 100), 0.5); + assert.strictEqual(compute_baseline_ratio(100, 100), 1); + }); +}); + +describe('baseline_ratio_color', () => { + test('worse than the anchor runs red to orange, better runs yellow to teal', () => { + assert.strictEqual(baseline_ratio_color(0.4), 'var(--color_c_50)'); + assert.strictEqual(baseline_ratio_color(0.9), 'var(--color_h_50)'); + assert.strictEqual(baseline_ratio_color(10), 'var(--color_j_50)'); + }); + + test('each band starts at its threshold', () => { + for (const edge of [0.5, 1, 2, 5]) { + assert.notStrictEqual(baseline_ratio_color(edge - 0.01), baseline_ratio_color(edge)); + } + }); +}); + +describe('to_baseline_key', () => { + // the tests run without a DOM, so the event target is the two members the read touches + const event = (row: { dataset: Record } | null): Event => + ({ target: { closest: () => row } }) as unknown as Event; + + test('reads the key off the row under the pointer', () => { + assert.strictEqual( + to_baseline_key(event({ dataset: { baselineKey: 'prettier' } })), + 'prettier' + ); + }); + + test('no row under the pointer, or no target at all, is no key', () => { + assert.isUndefined(to_baseline_key(event(null))); + assert.isUndefined(to_baseline_key({ target: null } as unknown as Event)); + }); +}); diff --git a/src/test/benchmark_cross_runtime.shape.test.ts b/src/test/benchmark_cross_runtime.shape.test.ts new file mode 100644 index 0000000..a24d011 --- /dev/null +++ b/src/test/benchmark_cross_runtime.shape.test.ts @@ -0,0 +1,94 @@ +import { assert, describe, test } from 'vitest'; + +import { benchmarks_json } from '$routes/docs/benchmarks/benchmarks.ts'; +import { conformance_json } from '$routes/docs/conformance/conformance.ts'; +import { benchmarks_cross_runtime_json } from '$routes/docs/benchmarks/benchmarks_cross_runtime.ts'; +import { derive_cross_runtime_groups } from '$routes/docs/benchmarks/benchmark_cross_runtime.ts'; + +// The combined report shape the committed copy is pinned to — tsv's composer's +// `COMBINED_SCHEMA_VERSION`. Exact rather than a floor, so `npm run +// update-benchmarks` pulling a newer shape fails here until +// `benchmark_cross_runtime.ts` mirrors the new fields and this is re-pinned. +const COMBINED_VERSION = 15; + +// 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, which the site + // has no banner for + assert.notStrictEqual(benchmarks_cross_runtime_json.mixed_vintage, true); + }); + + test('the conformance report is same-vintage with the perf siblings', () => { + // the combined report records it; a stale one means `update-benchmarks` + // copied a conformance report from a different refresh than the perf trio, + // which the site has no banner for + const vintage = benchmarks_cross_runtime_json.conformance_vintage; + assert.isOk(vintage, 'conformance_vintage recorded'); + assert.notStrictEqual(vintage.stale, true); + assert.strictEqual(vintage.git_commit, 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`); + // the tables print `mean_ns` beside ratios taken over `ops_per_second`, + // so the two must be one measurement + for (const [runtime, ops] of Object.entries(row.ops_per_second)) { + const mean = row.mean_ns[runtime as keyof typeof row.mean_ns]; + assert(mean !== undefined, `${group.group}/${row.name} missing ${runtime} mean`); + assert.approximately(ops * mean, 1e9, 1e3, `${group.group}/${row.name} ${runtime}`); + } + } + } + }); + + test('the committed reports timed identical file sets across runtimes', () => { + // a mismatch means part of a published ratio is corpus composition, not + // runtime, and the site has no annotation for it — recompose from same-box, + // same-commit siblings. Every runtime must have reported a count for every + // row, or the comparison would pass with nothing compared. + assert.isNotEmpty(benchmarks_cross_runtime_json.rows); + for (const row of benchmarks_cross_runtime_json.rows) { + for (const runtime of benchmarks_cross_runtime_json.runtimes) { + assert.isNumber(row.files_iterated[runtime], `${row.group}/${row.name} ${runtime}`); + } + assert.lengthOf( + [...new Set(Object.values(row.files_iterated))], + 1, + `${row.group}/${row.name} file-set mismatch` + ); + } + }); +}); diff --git a/src/test/benchmark_cross_runtime.test.ts b/src/test/benchmark_cross_runtime.test.ts new file mode 100644 index 0000000..f250eef --- /dev/null +++ b/src/test/benchmark_cross_runtime.test.ts @@ -0,0 +1,252 @@ +import { assert, describe, test } from 'vitest'; + +import { + derive_cross_runtime_groups, + cross_runtime_ratio_background, + derive_runtime_versions, + derive_unavailable_by_runtime, + derive_unstable_cells, + format_cross_runtime_label, + is_cell_unstable, + is_impl_unavailable, + is_ratio_within_noise, + order_cross_runtime_runtimes, + type BenchmarkRuntime, + type CrossRuntimeReport, + type UnstableCell +} from '$routes/docs/benchmarks/benchmark_cross_runtime.ts'; + +// a minimal healthy combined report, which each suite overrides the one field it reads +const create_report = (overrides: Partial = {}): CrossRuntimeReport => ({ + version: 15, + kind: 'combined', + generated: '2026-01-01T00:00:00.000Z', + runtimes: ['deno', 'node', 'bun'], + mixed_vintage: false, + mixed_machine: false, + unavailable_by_runtime: [], + partial_rows: [], + within_noise: [], + unstable_cells: [], + conformance_vintage: null, + sources: [], + rows: [], + ...overrides +}); + +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`). Synthetic reports: the committed one records no failures, 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']) => + create_report({ unavailable_by_runtime }); + + 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('is_impl_unavailable answers per runtime', () => { + // 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')); + }); +}); + +describe('derive_cross_runtime_groups', () => { + const row = ( + group: string, + name: string, + ops_per_second: CrossRuntimeReport['rows'][number]['ops_per_second'] + ): CrossRuntimeReport['rows'][number] => ({ + group, + name, + ops_per_second, + mean_ns: {}, + files_iterated: {} + }); + + test('ratios anchor on node whatever the storage order, and a missing side has none', () => { + const [group] = derive_cross_runtime_groups( + create_report({ + rows: [ + row('format/css', 'tsv', { deno: 30, node: 20, bun: 10 }), + row('format/css', 'biome-wasm', { deno: 5, node: 4 }) + ] + }) + ); + assert(group); + assert.deepStrictEqual(group.rows[0]?.ratio_vs_base, { node: 1, deno: 1.5, bun: 0.5 }); + assert.deepStrictEqual(group.rows[1]?.ratio_vs_base, { node: 1, deno: 1.25 }); + assert.strictEqual(group.rows[1]?.category, 'biome'); + }); + + test('a row the base runtime never measured has no ratios at all', () => { + const [group] = derive_cross_runtime_groups( + create_report({ rows: [row('format/css', 'tsv', { deno: 30, bun: 10 })] }) + ); + assert.deepStrictEqual(group?.rows[0]?.ratio_vs_base, {}); + }); + + test('groups read format before parse, then svelte, typescript, css', () => { + const groups = derive_cross_runtime_groups( + create_report({ + rows: [ + row('parse/css', 'tsv-json', { node: 1 }), + row('format/typescript', 'tsv', { node: 1 }), + row('parse/svelte', 'tsv-json', { node: 1 }), + row('format/svelte', 'tsv', { node: 1 }) + ] + }) + ); + assert.deepStrictEqual( + groups.map((g) => g.group), + ['format/svelte', 'format/typescript', 'parse/svelte', 'parse/css'] + ); + }); +}); + +describe('derive_unstable_cells', () => { + const cell = (runtime: BenchmarkRuntime): UnstableCell => ({ + group: 'format/css', + name: 'tsv', + runtime, + cv: 0.2, + cv_raw: null, + drift: null, + samples: 10 + }); + const report = (unstable_cells: Array) => create_report({ unstable_cells }); + + test('lists cells in the site column order, not the report storage order', () => { + const derived = derive_unstable_cells(report([cell('bun'), cell('deno'), cell('node')])); + assert.deepStrictEqual( + derived.map((c) => c.runtime), + ['node', 'deno', 'bun'] + ); + }); + + test('does not reorder the report in place', () => { + const cells = [cell('bun'), cell('node')]; + derive_unstable_cells(report(cells)); + assert.deepStrictEqual( + cells.map((c) => c.runtime), + ['bun', 'node'] + ); + }); + + test('is_cell_unstable matches group, row, and runtime together', () => { + const r = report([cell('bun')]); + assert.isTrue(is_cell_unstable(r, 'format/css', 'tsv', 'bun')); + assert.isFalse(is_cell_unstable(r, 'format/css', 'tsv', 'node')); + assert.isFalse(is_cell_unstable(r, 'parse/css', 'tsv', 'bun')); + assert.isFalse(is_cell_unstable(r, 'format/css', 'tsv-wasm', 'bun')); + }); +}); + +describe('is_ratio_within_noise', () => { + const report = (within_noise: CrossRuntimeReport['within_noise']) => + create_report({ within_noise }); + + test('a pairwise cell matches the ratio between exactly its two runtimes, either way round', () => { + const r = report([ + { + group: 'format/css', + name: 'oxfmt', + runtimes: ['deno', 'node'], + delta: 0.01, + noise: 0.04, + samples: [10, 10] + } + ]); + assert.isTrue(is_ratio_within_noise(r, 'format/css', 'oxfmt', 'node', 'deno')); + assert.isTrue(is_ratio_within_noise(r, 'format/css', 'oxfmt', 'deno', 'node')); + assert.isFalse(is_ratio_within_noise(r, 'format/css', 'oxfmt', 'node', 'bun')); + assert.isFalse(is_ratio_within_noise(r, 'format/css', 'tsv', 'node', 'deno')); + assert.isFalse(is_ratio_within_noise(r, 'parse/css', 'oxfmt', 'node', 'deno')); + }); +}); + +describe('derive_runtime_versions', () => { + test('reads each source machine in display order, dropping sources without one', () => { + const machine = (runtime_version: string) => ({ + cpu_model: 'cpu', + os: 'linux', + arch: 'x64', + runtime_version + }); + const source = (runtime: BenchmarkRuntime, m: ReturnType | null) => ({ + runtime, + timestamp: '2026-01-01T00:00:00.000Z', + git_commit: null, + tsv: null, + corpus_snapshot: null, + machine: m, + unavailable: null + }); + const versions = derive_runtime_versions( + create_report({ + sources: [ + source('deno', machine('2.5.0')), + source('node', machine('24.14.1')), + source('bun', null) + ] + }) + ); + assert.deepStrictEqual(versions, [ + { runtime: 'node', version: '24.14.1' }, + { runtime: 'deno', version: '2.5.0' } + ]); + }); +}); + +describe('format_cross_runtime_label', () => { + test('neutralizes the node-specific binding suffix, leaving other labels alone', () => { + assert.strictEqual(format_cross_runtime_label('tsv-json'), 'tsv json (native)'); + assert.strictEqual(format_cross_runtime_label('biome-wasm'), 'biome (wasm)'); + assert.strictEqual(format_cross_runtime_label('prettier'), 'prettier'); + }); +}); + +describe('cross_runtime_ratio_background', () => { + test('parity is transparent, and alpha saturates at a 20% delta either way', () => { + assert.include(cross_runtime_ratio_background(1), ' 0.0%,'); + assert.include(cross_runtime_ratio_background(1.1), 'var(--color_b_50) 15.0%,'); + assert.include(cross_runtime_ratio_background(1.2), 'var(--color_b_50) 30.0%,'); + assert.include(cross_runtime_ratio_background(3), 'var(--color_b_50) 30.0%,'); + assert.include(cross_runtime_ratio_background(0.5), 'var(--color_c_50) 30.0%,'); + }); +}); diff --git a/src/test/benchmark_data.prose.test.ts b/src/test/benchmark_data.prose.test.ts new file mode 100644 index 0000000..3c53197 --- /dev/null +++ b/src/test/benchmark_data.prose.test.ts @@ -0,0 +1,587 @@ +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_formatters_json } from '$routes/docs/benchmarks/benchmarks_formatters.ts'; +import { format_ratio_approx } from '$routes/docs/benchmarks/benchmark_display.ts'; +import { + benchmarks_cli, + cli_comparison_results, + cli_label_is_tsv, + cli_memory_ratio_range, + cli_scenario_find, + cli_ratio_vs_tsv, + cli_ratio_vs_tsv_npm, + cli_tsv_npm_overhead_ms_range, + cli_node_startup_ms, + CLI_DELIVERY_KEY, + CLI_SCENARIO_KEYS, + CLI_SINGLE_FILE_KEY, + CLI_SVELTE_KEY, + CLI_TS_REPO_KEY, + CLI_TSV_NPM_LABEL, + CLI_TSV_WASM_LABEL +} from '$routes/docs/benchmarks/benchmarks_cli.ts'; +import { derive_unstable_cells } from '$routes/docs/benchmarks/benchmark_cross_runtime.ts'; +import { IN_PROCESS_PAIRS as IN_PROCESS_PAIRS_BY_KEY } from '$routes/docs/benchmarks/benchmarks_prose.ts'; +import { + benchmark_speedup, + categorize_name, + derive_benchmark_groups, + derive_corpus_counts, + derive_sweep_stats, + is_entry_unstable, + is_payload_matched +} 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. +/** + * A ratio rendered inside a "~Nx faster than" (or "as long", "the memory") sentence + * must clear 1 by enough to print as one: `format_ratio_approx` rounds to one decimal, so a ratio in + * `[1, 1.05)` passes an `isAbove(1)` gate and still renders "~1.0x faster", a + * claim of nothing. + */ +const assert_reads_faster = (ratio: number, label: string): void => { + assert.isAbove(ratio, 1, label); + assert.notStrictEqual(format_ratio_approx(ratio), '1.0x', `${label} renders as ~1.0x`); +}; + +/** The report's format groups, asserted non-empty so a loop over them can't pass on nothing. */ +const format_groups = () => { + const groups = derive_benchmark_groups(benchmarks_json).filter((g) => g.operation === 'format'); + assert.isNotEmpty(groups); + return groups; +}; + +describe('prose ratios resolve', () => { + // 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. The + // table is the page's own, so a pairing added to the copy is gated by construction. + const IN_PROCESS_PAIRS = Object.values(IN_PROCESS_PAIRS_BY_KEY); + + 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_reads_faster(ratio, `${group}: ${slower} vs ${faster}`); + } + }); + + test('tsv "formats its three languages faster in every pairing measured here"', () => { + // The TLDR's absolute claim spans every timed format row, not just the pairs + // it quotes: dprint and malva are timed too. Like for like is native-vs-native + // and wasm-vs-wasm, and the JS rows face native tsv; gating tsv-wasm, tsv's + // slower build, against every non-tsv row is the stronger check and holds + for (const group of format_groups()) { + // a disabled row is a mirrored placeholder or coverage-only, timed at 0 + const timed = group.entries.filter((e) => !e.disabled && e.mean_ns > 0); + const tsv_wasm = timed.find((e) => e.name === 'tsv-wasm'); + assert(tsv_wasm, `${group.language}: no timed tsv-wasm row`); + const others = timed.filter((e) => !e.name.startsWith('tsv')); + assert.isNotEmpty(others, `${group.language}: nothing to pair against`); + for (const other of others) { + assert_reads_faster(other.mean_ns / tsv_wasm.mean_ns, `${group.language}: ${other.name}`); + } + } + }); + + test('a group that runs short of the corpus total has a note saying by how much', () => { + // "a chart that runs short of the corpus total says beneath it what was left + // out" — the note renders from `omissions`, so the sentence + // holds exactly when every shortfall is an omission the report carries + const groups = derive_benchmark_groups(benchmarks_json); + assert.isNotEmpty(groups); + for (const group of groups) { + const key = `${group.operation}/${group.language}`; + const total = benchmarks_json.corpus[group.language]; + assert.isDefined(total, group.language); + assert.isNotNull(group.files_iterated, `${key}: no timed-set count`); + assert.strictEqual( + total - group.files_iterated, + group.omissions?.omitted_files ?? 0, + `${key}: the shortfall the heading shows is not the one the note states` + ); + } + }); + + test('rsvelte\'s Svelte target is "a release apart" from the svelte/compiler row\'s', () => { + // the parse note prints that sentence whenever the two differ, so the gap must + // stay one minor release at most or the copy understates it + const { svelte, rsvelte_parse_svelte_target } = benchmarks_json.versions; + assert.isString(svelte); + if (!rsvelte_parse_svelte_target || rsvelte_parse_svelte_target === svelte) return; + const minor_of = (version: string): number => { + const match = /^(\d+)\.(\d+)\./.exec(version); + assert(match, `${version} is not a semver`); + return Number(match[1]) * 1000 + Number(match[2]); + }; + assert.isAtMost( + Math.abs(minor_of(rsvelte_parse_svelte_target) - minor_of(svelte)), + 1, + 'more than one minor release apart' + ); + }); + + test('oxfmt formats Svelte at Prettier speed, as the note says it delegates', () => { + // "delegates Svelte to a Prettier it bundles", which the TLDR leans on to quote + // only Prettier for Svelte — if a future oxfmt grows its own Svelte path the + // two rows will part ways and both are 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 ~N% of tsv's time there (the gap to the internal row)", + // offered as why the JS parsers finish ahead — 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, and an unstable row's mean may sit between + // two modes (see `is_entry_unstable`) — 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 pairings the copy compares by payload tier share one, and the ones it excludes do not', () => { + // The page names four pairings as comparing the same kind of PRODUCT and rules three + // out ("swc's AST ... matches neither tsv wire", and the `no-locs` entries, not the + // default wire, are "the closest comparison with oxc-parser"). Those are claims about + // the report's `payload` tiers, so read them off it rather than trusting prose. A + // shared tier is a shape, not a byte count: the copy says Oxc's span-only AST runs + // larger than tsv's. + const entry = (group: string, name: string) => { + const found = benchmarks_json.entries.find((e) => e.group === group && e.name === name); + assert(found, `${group}/${name} is missing`); + return found; + }; + const matched = (group: string, a: string, b: string) => + is_payload_matched(entry(group, a), entry(group, b)); + + for (const [group, a, b] of [ + ['parse/typescript', 'tsv-json-no-locations', 'oxc-parser'], + ['parse/typescript', 'tsv-json-no-locations', 'yuku-parser'], + ['parse/typescript', 'tsv-wasm-json-no-locations', 'yuku-parser-wasm'], + ['parse/svelte', 'tsv-json', 'rsvelte-parse'] + ] as const) { + assert.isTrue(matched(group, a, b), `${group}: ${a} vs ${b} is no longer payload-matched`); + } + + for (const [group, a, b] of [ + // swc's own AST shape, ruled out against both wires + ['parse/typescript', 'tsv-json', 'swc'], + ['parse/typescript', 'tsv-json-no-locations', 'swc'], + // oxc against tsv's default wire — the pairing the copy says is NOT the matched one + ['parse/typescript', 'tsv-json', 'oxc-parser'], + // rsvelte's reduced wire "sits near tsv's span-only wire without matching it" + ['parse/svelte', 'tsv-json-no-locations', 'rsvelte-parse-skip-expr-loc'] + ] as const) { + assert.isFalse(matched(group, a, b), `${group}: ${a} vs ${b} is now payload-matched`); + } + }); + + test('the cross-runtime unstable disclosure names only rows the tables carry', () => { + // the disclosure can't point at a row or runtime a reader can't find + 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', () => { + // (the TypeScript-repo wall/CPU pairs are covered by the CPU-work test below) + // the CLI note's "less than every other tool in every scenario", against the bare + // binary and the dispatcher — both read as "less" and both are floored to one + // decimal for display, so the LOW end must reach 1.1 or the range would print + // "1.0–Nx less memory", a claim of nothing + for (const range of [ + cli_memory_ratio_range(), + cli_memory_ratio_range({ baseline_label: CLI_TSV_NPM_LABEL }) + ]) { + assert.isDefined(range); + assert.isAtLeast(range.min, 1.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_ratio_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_reads_faster(ratio, `${CLI_DELIVERY_KEY}: ${label} ${metric}`); + } + }); + + test('the like-for-like dispatcher claims read the way the numbers run', () => { + // The TLDR and the CLI note lead with tsv through its Node dispatcher against the + // other tools' npm bins: "~Nx faster than Oxfmt and ~Mx faster than Biome ... + // using less memory than either". The copy has no bare-binary fallback, so + // every one must resolve or a sentence prints with a hole in it. + for (const label of ['oxfmt', 'biome']) { + const ratio = cli_ratio_vs_tsv_npm(CLI_TS_REPO_KEY, label, 'wall_ms'); + assert.isDefined(ratio, `${CLI_TS_REPO_KEY}: ${label}`); + assert_reads_faster(ratio, `${CLI_TS_REPO_KEY}: ${label}`); + } + const memory = cli_memory_ratio_range({ + scenario_key: CLI_TS_REPO_KEY, + labels: ['oxfmt', 'biome'], + baseline_label: CLI_TSV_NPM_LABEL + }); + assert.isDefined(memory); + assert.isAbove(memory.min, 1); + // "~Nx the binary's time on the delivery table's one file, but ~Mx on the + // TypeScript repo": the dispatcher's own cost must shrink on the repo + const repo_cost = cli_ratio_vs_tsv(CLI_TS_REPO_KEY, CLI_TSV_NPM_LABEL, 'wall_ms'); + const file_cost = cli_ratio_vs_tsv(CLI_DELIVERY_KEY, CLI_TSV_NPM_LABEL, 'wall_ms'); + assert.isDefined(repo_cost); + assert.isDefined(file_cost); + assert_reads_faster(repo_cost, `${CLI_TS_REPO_KEY}: dispatcher cost`); + assert.isBelow(repo_cost, file_cost, 'the dispatcher cost does not shrink on the repo'); + // the Svelte bullet's like-for-like pair: that scenario may be published + // aborted, but the copy quotes the dispatcher ratio wherever it quotes the + // bare-binary one, so the two must resolve together + for (const metric of ['wall_ms', 'memory_mb'] as const) { + const ratio = cli_ratio_vs_tsv_npm(CLI_SVELTE_KEY, 'rsvelte-fmt', metric); + const bare = cli_ratio_vs_tsv(CLI_SVELTE_KEY, 'rsvelte-fmt', metric); + assert.strictEqual(ratio !== undefined, bare !== undefined, `${CLI_SVELTE_KEY}: ${metric}`); + if (ratio !== undefined) assert_reads_faster(ratio, `${CLI_SVELTE_KEY}: ${metric}`); + } + }); + + test('the dispatcher row "reads Node\'s peak, not the binary\'s"', () => { + // the harness reports the largest single process in the tree, so the memory note + // holds only while the Node launcher outgrows the binary it spawns + for (const scenario of benchmarks_cli.scenarios) { + const npm = scenario.results.find((r) => r.label === CLI_TSV_NPM_LABEL); + const tsv = scenario.results.find((r) => r.label === 'tsv'); + if (npm?.memory_mb == null || tsv?.memory_mb == null) continue; + assert.isAbove(npm.memory_mb, tsv.memory_mb, scenario.key); + } + }); + + test('the memory note\'s "every other tool in every scenario" spans every competitor row', () => { + // the unscoped memory range skips a row without a figure, so a missed memory + // pass would narrow the claim silently rather than void it — every competitor + // row must carry one + for (const scenario of benchmarks_cli.scenarios.filter((s) => !s.tsv_only)) { + for (const r of cli_comparison_results(scenario)) { + assert.isNotNull(r.memory_mb, `${scenario.key}: ${r.label} has no memory figure`); + } + } + }); + + test('the CLI CPU-work note reads the way the numbers run', () => { + // "CPU ratios move far less than wall-clock: tsv leads Oxfmt ~C ... and ~D ..., + // against ~A and ~B wall-clock" — the two CPU leads must sit inside the wall-clock span: dispatcher + // wall < dispatcher CPU <= bare CPU < bare wall. Any one flipping on a refresh + // leaves the note explaining the opposite of what the table shows. + const defined = (value: number | undefined, name: string): number => { + assert.isDefined(value, `${CLI_TS_REPO_KEY}: ${name}`); + return value; + }; + for (const label of ['oxfmt']) { + const npm = (metric: 'wall_ms' | 'cpu_ms') => + defined(cli_ratio_vs_tsv_npm(CLI_TS_REPO_KEY, label, metric), `${label} ${metric}`); + const bare = (metric: 'wall_ms' | 'cpu_ms') => + defined(cli_ratio_vs_tsv(CLI_TS_REPO_KEY, label, metric), `${label} ${metric}`); + assert.isAbove( + npm('cpu_ms'), + npm('wall_ms'), + `${label}: like for like, CPU lead > wall lead` + ); + assert.isAbove( + bare('wall_ms'), + bare('cpu_ms'), + `${label}: bare binary, wall lead > CPU lead` + ); + assert.isAtMost(npm('cpu_ms'), bare('cpu_ms'), `${label}: dispatcher CPU lead > bare`); + // every one of the four renders inside a "~Nx faster than" sentence, and + // `format_ratio_approx` is direction-blind, so each must also clear 1 — the + // wall-clock dispatcher ratio is gated above, the other three here + for (const [ratio, name] of [ + [npm('cpu_ms'), 'dispatcher cpu_ms'], + [bare('wall_ms'), 'bare wall_ms'], + [bare('cpu_ms'), 'bare cpu_ms'] + ] as const) { + assert_reads_faster(ratio, `${label}: ${name}`); + } + } + // "even Prettier's CPU time runs above its wall-clock" + for (const key of [CLI_SINGLE_FILE_KEY, CLI_TS_REPO_KEY]) { + const prettier = cli_scenario_find(key)?.results.find((r) => r.label === 'prettier'); + assert(prettier, `${key} has no prettier row`); + assert.isAbove(prettier.cpu_ms, prettier.wall_ms, `${key}: prettier CPU > wall`); + } + }); + + test('the Node launch floor sits inside the dispatcher overhead', () => { + // "a bare node -e '' takes ~N ms on this machine": Node's startup is one part + // of what the dispatcher row pays over the bare binary, so it must not exceed + // that gap — a floor above the overhead would mean the two measure different + // things (a different node, a different PATH) + const floor = cli_node_startup_ms(); + const overhead = cli_tsv_npm_overhead_ms_range(); + assert.isDefined(overhead); + assert.isAbove(floor, 0); + assert.isBelow(floor, overhead.max); + // "most of it Node's own startup" + assert.isAbove(floor, overhead.max / 2); + }); + + test('the dispatcher overhead is the "fixed cost" the delivery note calls it', () => { + // "tsv's dispatcher adds a fixed ~N ms over the bare binary" — fixed means the + // absolute figure barely moves between a one-file run and a repo, so the span + // must stay tight around a positive cost + const overhead = cli_tsv_npm_overhead_ms_range(); + assert.isDefined(overhead); + assert.isAbove(overhead.min, 0); + assert.isAtMost(overhead.max, overhead.min * 1.25, 'the dispatcher cost is not fixed'); + }); + + test('the scenario descriptions state facts the report carries', () => { + // the delivery copy: "the wasm row's CPU ratio runs well past its time ratio" + const cpu = cli_ratio_vs_tsv(CLI_DELIVERY_KEY, CLI_TSV_WASM_LABEL, 'cpu_ms'); + const wall = cli_ratio_vs_tsv(CLI_DELIVERY_KEY, CLI_TSV_WASM_LABEL, 'wall_ms'); + assert.isDefined(cpu, 'delivery scenario has no tsv-wasm row'); + assert.isDefined(wall); + assert.isAbove(cpu, wall * 1.5, 'the wasm CPU ratio no longer runs well past its time ratio'); + // the Svelte abort context: "rsvelte-fmt 0.7.x can abort when its output and stderr share a pipe" + const rsvelte_version = benchmarks_cli.versions['rsvelte-fmt']; + assert.isDefined(rsvelte_version); + assert.match(rsvelte_version, /^0\.7\./, 'the Svelte copy names rsvelte-fmt 0.7.x'); + }); + + test('the delivery note places the wasm row among the single-file tools', () => { + // "still well ahead of both Prettier rows there, but behind Oxfmt and Biome" — + // the delivery and single-file scenarios time the same file (the shape test + // holds them to one corpus revision), so the wasm row reads against the other + // tools' single-file times across the two tables + const wasm = cli_scenario_find(CLI_DELIVERY_KEY)?.results.find( + (r) => r.label === CLI_TSV_WASM_LABEL + ); + assert(wasm, `${CLI_DELIVERY_KEY} has no ${CLI_TSV_WASM_LABEL} row`); + const single = cli_scenario_find(CLI_SINGLE_FILE_KEY); + assert(single, `no generated scenario has id "${CLI_SINGLE_FILE_KEY}"`); + const wall = (label: string): number => { + const row = single.results.find((r) => r.label === label); + assert(row, `${CLI_SINGLE_FILE_KEY} has no ${label} row`); + return row.wall_ms; + }; + for (const label of ['prettier', 'prettier + oxc-parser']) { + assert.isAbove( + wall(label), + wasm.wall_ms * 2, + `${label}: the wasm row is no longer well ahead` + ); + } + for (const label of ['oxfmt', 'biome']) { + assert.isBelow(wall(label), wasm.wall_ms, `${label}: the wasm row is no longer behind`); + } + }); + + test('the CPU-work column counts system time, as the note says', () => { + // "hyperfine's user plus system time" — a refresh that drops either half + // would relabel a different metric under the same heading + const scenario = benchmarks_formatters_json.scenarios.find((s) => s.id === CLI_TS_REPO_KEY); + assert(scenario, `no generated scenario has id "${CLI_TS_REPO_KEY}"`); + const tsv = scenario.timings.find((t) => t.name === 'tsv'); + assert(tsv); + assert.isAbove(tsv.system_ms, 0); + assert.closeTo( + cli_scenario_find(CLI_TS_REPO_KEY)!.results.find((r) => r.label === 'tsv')!.cpu_ms, + tsv.user_ms + tsv.system_ms, + 1e-9 + ); + }); + + 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(cli_label_is_tsv(r.label), `${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({ scenario_key: key })) + .filter((r) => r !== undefined); + 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({ scenario_key: CLI_DELIVERY_KEY })); + }); + + test('the wasm delivery row is "ahead of both Prettier rows … and behind Oxfmt and Biome"', () => { + // 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 === CLI_SINGLE_FILE_KEY); + assert(delivery && single); + 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}`); + } + // the sentence concedes the native-engine rows, so that half must hold too + for (const label of ['oxfmt', 'biome']) { + const native = single.results.find((r) => r.label === label); + assert(native, `large-single-file has no ${label} row`); + assert.isAbove(wasm.wall_ms, native.wall_ms, `tsv-wasm vs ${label}`); + } + }); + + test('the run-order note holds: every scenario runs the dispatcher row, then bare tsv, last', () => { + // "Every scenario puts the bare binary last, with its Node dispatcher row just + // before it" — hyperfine reports commands in the order it ran them, + // and the generated timings keep that order. An aborted scenario has no timings + // to order, so its preflight rows, which the harness runs in the same order, stand in. + for (const key of CLI_SCENARIO_KEYS) { + const scenario = benchmarks_formatters_json.scenarios.find((s) => s.id === key); + assert(scenario, `no generated scenario has id "${key}"`); + const rows = scenario.timings.length ? scenario.timings : scenario.preflight; + assert.deepStrictEqual( + rows.slice(-2).map((r) => r.name), + ['tsv-npm', 'tsv'], + key + ); + } + }); + + test('the "only parser here besides" notes still describe their groups', () => { + // "rsvelte's is the only Svelte parser here besides tsv's and the svelte/compiler + // reference" and "PostCSS is the only CSS parser here besides tsv's and the + // parseCss reference" — a row added to either group makes its note wrong + const others = (group: string): Array => [ + ...new Set( + benchmarks_json.entries + .filter((e) => e.group === group) + .map((e) => categorize_name(e.name)) + .filter((category) => category !== 'canonical' && !category.startsWith('tsv_')) + ) + ]; + assert.deepStrictEqual(others('parse/svelte'), ['rsvelte']); + assert.deepStrictEqual(others('parse/css'), ['postcss']); + }); + + test('the corpus figures the Corpus section quotes resolve', () => { + // the page has no fallback copy for them + const counts = derive_corpus_counts(benchmarks_json); + assert.isAbove(counts.harvested_css, 0); + assert(counts.standalone_css !== undefined); + assert.isAbove(counts.standalone_css, 0); + }); + + test("the parse note on oxc-parser's wasm row running an older release reads the report", () => { + // "oxc-parser's wasm row runs an older release than its native row ... so the wasm-vs-wasm + // pairing crosses oxc versions" — the bindings re-aligning makes the note a fiction + const { versions } = benchmarks_json; + assert.isDefined(versions.oxc_parser_wasm); + assert.notStrictEqual(versions.oxc_parser_wasm, versions.oxc_parser, 'bindings re-aligned'); + }); + + test('Prettier sits at the sweep floor, as Benchmarking details says', () => { + // the disclosure that Prettier, every format chart's default anchor, runs at the bench's per-row floor: + // each Prettier format row's raw timing count must be exactly its floor + const prettier_rows = benchmarks_json.entries.filter( + (e) => e.name === 'prettier' && e.group.startsWith('format/') + ); + assert.isNotEmpty(prettier_rows); + for (const entry of prettier_rows) { + assert.isDefined(entry.min_iterations, entry.group); + assert.strictEqual(entry.raw_sample_size, entry.min_iterations, `${entry.group}/prettier`); + } + }); + + test('the slowest rows have "too few timings" for the cross-runtime noise check', () => { + // tsv's report composer judges a delta against noise only where both sides kept + // at least 10 cleaned timings, so the Cross-runtime section's caveat holds while + // some row here keeps fewer + const { sample_size_min } = derive_sweep_stats(benchmarks_json); + assert.isDefined(sample_size_min); + assert.isBelow(sample_size_min, 10); + }); + + test('the higher sweep floor is "each group\'s reference row"\'s, and only theirs', () => { + // Benchmarking details quotes the lowest floor for every row and a second one in + // parentheses for the reference rows, so exactly those rows may sit above it + const timed = benchmarks_json.entries.filter((e) => e.min_iterations != null); + const floor = Math.min(...timed.map((e) => e.min_iterations!)); + for (const entry of timed) { + assert.strictEqual( + entry.min_iterations! > floor, + categorize_name(entry.name) === 'canonical', + `${entry.group}/${entry.name}: floor ${entry.min_iterations}` + ); + } + }); + + test('the corpus repos the Corpus section names are present', () => { + // "the fuz.dev ecosystem ... and upstream framework source (Svelte, SvelteKit, and + // the svelte.dev site)" — 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), `the page names ${slug} but the corpus lacks it`); + } + assert.ok( + [...slugs].some((slug) => slug?.startsWith('fuzdev/')), + 'the page names the fuz.dev repos but the corpus has none' + ); + }); + + test('the corpus is "the author\'s own and Svelte\'s", as the tldr and Corpus section say', () => { + // every source is one of the author's repos or Svelte's, or the `