diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 19440b0..d8e5e7d 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -7,7 +7,7 @@ on: push: branches: [main] pull_request: - branches: ['**'] + branches: ["**"] # Allow manually re-running the check from the Actions tab. workflow_dispatch: @@ -28,7 +28,7 @@ jobs: strategy: matrix: - node-version: ['24.14'] + node-version: ["24.14"] steps: # persist-credentials: false keeps the GITHUB_TOKEN out of .git/config, so a diff --git a/CLAUDE.md b/CLAUDE.md index 46c2c85..b1ca6bd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,19 +23,21 @@ IMPORTANT for AI agents: Do NOT run `gro dev` - the developer will manage the de - Svelte 5 - component framework with runes - SvelteKit - application framework with static adapter -- fuz_css (`@fuzdev/fuz_css`) - CSS framework and design system +- fuz_css (`@fuzdev/fuz_css`) - semantic-first CSS framework and design system - fuz_ui (`@fuzdev/fuz_ui`) - UI components, theming, docs system - fuz_util (`@fuzdev/fuz_util`) - utility functions - fuz_code (`@fuzdev/fuz_code`) - syntax highlighting - Gro (`@fuzdev/gro`) - build system and task runner -- prettier + prettier-plugin-svelte - code formatting +- tsv (via Gro) - code formatting - mdz (`@fuzdev/mdz`) - markdown preprocessor wired into `svelte.config.js` - `@fuzdev/tsv_wasm` - tsv's formatter + parser as WASM; powers the playground, loaded lazily in the browser -Note: `@fuzdev/tsv_wasm` is loaded only on `/docs/playground` via a browser-only dynamic `import()`, so the ~900KB WASM never weighs down `/docs` or the prerendered pages. +Note: `@fuzdev/tsv_wasm` is loaded only on `/docs/playground` via a browser-only dynamic `import()`, so the ~900KB-gzipped WASM (~2.9MB decoded) never weighs down `/docs` or the prerendered pages. Note: several devDependencies — `@webref/css` (CSS spec data), `zimmerframe` (AST traversal), `@sveltejs/acorn-typescript`, `zod`, and `@fuzdev/blake3_wasm` — are *optional peer dependencies* of `@fuzdev/fuz_css`'s `vite_plugin_fuz_css`, declared here so its build-time CSS generation resolves them (e.g. `css_literal.ts` imports `@webref/css`, `css_class_extractor.ts` walks ASTs with `zimmerframe`). They aren't imported by this app's own source, so don't mistake them for dead deps. +Note: `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: @@ -45,26 +47,23 @@ tsv.fuz.dev is the public face of the tsv tool: - 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 - Theme controls via fuz_ui's `ThemeRoot` in the root layout (no separate about/settings page) -- Shows install instructions for `@fuzdev/tsv_wasm` (full tool + `tsv` CLI; format/parse subsets mentioned in the intro) +- Shows install instructions led by the native `@fuzdev/tsv` (prebuilt N-API addon for Node/Bun, ships the `tsv` CLI), then `@fuzdev/tsv_wasm` (universal, same `tsv` CLI) and the format/parse subsets ### What tsv.fuz.dev does NOT include (yet) - Authentication or backend -- Native-binary downloads (tsv ships WASM-only at v0.1) +- Standalone native-binary downloads (the native path ships as the `@fuzdev/tsv` npm addon, not as a downloadable executable) ## Routes ``` src/ -├── lib/ -│ └── sample.ts # placeholder constant (not yet wired up) ├── routes/ │ ├── +page.svelte # Home: hero, links to docs and benchmarks │ ├── +layout.svelte # Root layout: fuz_css/fuz_code CSS, ThemeRoot, SiteState │ ├── +layout.ts # prerender: true, ssr: true │ ├── style.css # global styles │ ├── library.ts # builds library_json from virtual:svelte-docinfo + pkg.json -│ ├── example.test.ts # placeholder test │ └── docs/ │ ├── +layout.svelte # Docs layout (Docs wrapper; sets library_context) │ ├── +page.svelte # Docs index (DocsContent) @@ -81,23 +80,70 @@ src/ Benchmark data comes from `tsv`. Full workflow to update: ```bash -# 1. In ~/dev/tsv — run benchmarks (builds ffi + wasm automatically) +# 1. In ~/dev/tsv — run benchmarks across deno/node/bun (builds artifacts automatically) deno task bench -# 2. In ~/dev/tsv.fuz.dev — copy the latest result +# 2. In ~/dev/tsv.fuz.dev — copy the latest results npm run update-benchmarks ``` -Step 1 writes `benches/deno/results/report.json` (committed to tsv). -Step 2 copies it to `src/routes/docs/benchmarks/benchmarks.json`. -The JSON format matches `BenchmarkBaseline` in `benchmark_data.ts`. +Step 1 writes the per-runtime `benches/js/results/report..{json,md}` +siblings, the composed cross-runtime `report.{json,md}`, and the conformance +coverage report `report.conformance.node.json` (committed to tsv). +Step 2 copies three of them — `report.node.json` → `benchmarks.json`, the +composed `report.json` → `benchmarks_cross_runtime.json`, and +`report.conformance.node.json` → `benchmarks_conformance.json` — then runs +`gro format` over the copies. Note the script's source paths are hardcoded to +`../tsv` — if the reports were generated in a different worktree, copy them +into `~/dev/tsv` (or copy manually) first. +The JSON formats match the types in `benchmark_data.ts`. + +The end-to-end CLI comparison against Prettier, Biome, and Oxfmt comes from a +separate harness, a fork of Oxc's `bench-formatter` that adds tsv +(../oxc-bench-formatter). It publishes no JSON — its numbers live only as the +hyperfine console dump in its README, between the +`` / `END` markers, plus a `## Versions` list +and a `_Measured on: …_` machine line. To update: + +```bash +# 1. In ~/dev/oxc-bench-formatter — re-run and rewrite its README (needs a built ../tsv) +pnpm run update-readme + +# 2. In ~/dev/tsv.fuz.dev — reparse the README into JSON +gro gen +``` + +`benchmarks_formatters.gen.json.ts` parses that README and writes +`benchmarks_formatters.json`, keeping only the scenarios tsv participates in +(it has no JSX/TSX parser, so the harness runs it on the JSX-free corpora +only). That includes the harness's Svelte scenario, which benches tsv against +rsvelte-fmt (`@rsvelte/fmt`); a scenario renders on the page only once it has +an entry in `SCENARIO_COPY` (`benchmarks_cli.ts`), and prose claims about the +Svelte head-to-head are conditional on its data being present, so the site +stays correct whether or not the harness README has been regenerated with it. + +A **missing** sibling checkout is the one tolerated case — generation is +skipped, the committed JSON stands, and `gro gen --check` passes on any machine +or CI that has only this repo (CI never checks out the harness, so it always +takes this path; no `--no-gen` needed). A README that **is** present but has +drifted fails the task loudly, naming the scenario and the section that stopped +parsing, rather than publishing stale or scenario-stripped numbers. A drift that +still parses but renames a scenario is caught on the site side instead: every +key in `benchmarks_cli.ts`'s `SCENARIO_COPY` must resolve to generated data, and +a test asserts it. Key files in `src/routes/docs/benchmarks/`: -- `benchmarks.json` — raw benchmark data (copied from tsv) +- `benchmarks.json` — per-runtime Node report (copied from tsv) +- `benchmarks_cross_runtime.json` — composed cross-runtime report (copied from tsv) +- `benchmarks_conformance.json` — conformance parse-coverage report (copied from tsv) +- `benchmarks_formatters.json` — formatter CLI comparison, generated from the sibling harness's README - `benchmark_data.ts` — TypeScript types matching the JSON format -- `benchmarks.ts` — re-exports the JSON with types +- `formatter_benchmark_data.ts` — types plus `parse_formatter_benchmarks`, the README parser +- `benchmarks_cli.ts` — shapes `benchmarks_formatters.json` for `BenchmarksCli.svelte` and owns the per-scenario prose; the numbers are all generated +- `benchmarks.ts`, `benchmarks_cross_runtime.ts`, `benchmarks_conformance.ts`, `benchmarks_formatters.ts` — re-export the JSON with types - `BenchmarksBar.svelte`, `BenchmarksGroup.svelte`, etc. — visualization components +- `BenchmarksBaselineGroup.svelte` — shared interactive column behind the format, parse, and binary-size groups: hovering a row re-baselines that group's ratios (each of the three groups per section is independent), restoring the default anchor (the canonical reference — Prettier for format, the JS baseline for parse — and the smallest build for size) on mouseleave. `benchmark_data.ts`'s `compute_baseline_ratio`/`format_baseline_ratio`/`baseline_ratio_color` carry the per-`BaselineDirection` (`speed`/`size`) formulas it and the derivations share ## Architecture @@ -105,6 +151,7 @@ Key files in `src/routes/docs/benchmarks/`: - 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)) +- The benchmarks page quotes no hand-written ratios — its prose computes them from the same reports the charts render, via `benchmark_data.ts`'s `benchmark_speedup`/`format_ratio_approx`/`format_ratio_range`/`format_ratio_range_approx` and `benchmarks_cli.ts`'s `cli_speedup_vs_tsv`/`cli_memory_ratio_range`. A test gates that every pair the copy names still resolves - `library.ts` builds component metadata at runtime from the `virtual:svelte-docinfo` module (provided by the `svelte-docinfo` Vite plugin); the docs index passes it to `DocsContent` - The playground (`/docs/playground`) loads `@fuzdev/tsv_wasm` via a browser-only dynamic `import()` inside `Playground.svelte`, so the WASM code-splits into its own chunk fetched only on that route — the same lazy discipline `library.ts` uses for the heavy svelte-docinfo data, keeping `/docs` and the prerendered pages WASM-free. `@fuzdev/tsv_wasm` is in `vite.config.ts` `optimizeDeps.exclude` (like `blake3_wasm`) - The playground'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) @@ -117,7 +164,7 @@ Deploys to `https://tsv.fuz.dev/` via `gro deploy` (builds and pushes to deploy - TypeScript strict mode - Svelte 5 with runes API -- Prettier with tabs, 100 char width +- tsv with tabs, 100 char width - Node >= 24.14 - Private package (not published to npm) diff --git a/eslint.config.js b/eslint.config.js index 100b57f..d903ae8 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,3 +1,3 @@ -import {configs} from '@ryanatkn/eslint-config'; +import { configs } from '@ryanatkn/eslint-config'; export default configs; diff --git a/package-lock.json b/package-lock.json index ee9152a..a898e19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,35 +10,32 @@ "license": "MIT", "devDependencies": { "@fuzdev/blake3_wasm": "^0.1.1", - "@fuzdev/fuz_code": "^0.46.3", + "@fuzdev/fuz_code": "^0.48.0", "@fuzdev/fuz_css": "^0.63.2", - "@fuzdev/fuz_ui": "^0.206.5", + "@fuzdev/fuz_ui": "^0.206.7", "@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/gro": "^0.209.0", + "@fuzdev/mdz": "^0.3.0", + "@fuzdev/tsv_wasm": "^0.2.0", + "@ryanatkn/eslint-config": "^0.12.2", "@sveltejs/acorn-typescript": "^1.0.9", "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.61.1", - "@sveltejs/vite-plugin-svelte": "^7.1.2", + "@sveltejs/kit": "^2.69.1", + "@sveltejs/vite-plugin-svelte": "^6.2.4", "@types/estree": "^1.0.8", "@types/node": "^24.12.4", "@webref/css": "^8.2.0", "eslint": "^9.39.1", "eslint-plugin-svelte": "^3.13.1", "esm-env": "^1.2.2", - "magic-string": "^0.30.21", - "prettier": "^3.7.4", - "prettier-plugin-svelte": "^3.4.1", - "svelte": "^5.56.0", - "svelte-check": "^4.3.6", + "svelte": "^5.56.4", + "svelte-check": "^4.7.2", "svelte-docinfo": "^0.5.4", "svelte2tsx": "^0.7.47", "tslib": "^2.8.1", "typescript": "^5.9.3", "typescript-eslint": "^8.48.1", - "vite": "^8.0.16", + "vite": "^7.3.6", "vitest": "^4.1.9", "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" } @@ -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": { @@ -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.206.7", + "resolved": "https://registry.npmjs.org/@fuzdev/fuz_ui/-/fuz_ui-0.206.7.tgz", + "integrity": "sha512-UUO+6zVK6Ot9NmZLaKWASJ6odjjLOUDObIWOjSKtoPBnSu62UvzlGYdSGEHYk1hSR58wNj0qndhWNDzmf/WDdg==", "dev": true, "license": "MIT", "engines": { @@ -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.209.0", + "resolved": "https://registry.npmjs.org/@fuzdev/gro/-/gro-0.209.0.tgz", + "integrity": "sha512-i2eViCE4aRoJgcaVEf2Xiq6iqHmrKmYpoyHGDDoFVSs4WeKs34ZacqBbMe8vkmaeUQauFIVUnFcFbudArngAtw==", "dev": true, "license": "MIT", "dependencies": { + "@fuzdev/tsv_wasm": "^0.2.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" }, @@ -935,9 +908,9 @@ } }, "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": { @@ -967,9 +940,9 @@ } }, "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==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@fuzdev/tsv_wasm/-/tsv_wasm-0.2.0.tgz", + "integrity": "sha512-mbnFSWnAG8otz0SPY1baF0E6mbyJHhuxPxhJ607aVZ8N7vm6xb2rV9M/+PStgzt1p1KX2fLuQ2WKZcj1S0CZdg==", "dev": true, "license": "MIT", "bin": { @@ -1393,27 +1366,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 +1406,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 +1420,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 +1448,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 +1499,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 +1516,46 @@ "optional": true, "os": [ "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", - "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", "cpu": [ "ppc64" ], @@ -1533,17 +1567,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 +1601,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 +1635,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 +1669,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 +1697,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 +1739,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 +1804,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.61.1", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.61.1.tgz", - "integrity": "sha512-Ny8s1SR1TyQS2hD2Rvw0XKzU2Nw1eUF52dTb6T2bdcgz7wSC+Nyb5IwjWYlR4b2dvbbR5NJDiQwHg3rnNseghg==", + "version": "2.69.1", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.69.1.tgz", + "integrity": "sha512-+nz8Fx/cElzb2ZPHC+6Ll3y3NEVIe4Na75PeplLlyTmd1dBXAjz2KR14y1ZgNjb2ThfAYzulu+PFy1UE3RCUzA==", "dev": true, "license": "MIT", "dependencies": { @@ -1765,24 +1845,53 @@ } } }, + "node_modules/@sveltejs/load-config": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@sveltejs/load-config/-/load-config-0.2.0.tgz", + "integrity": "sha512-1LgZ/qUqSoq+QorD83lk2hka79Px0wXNW2q5V1nZlxGhQgw1jrsIbVz5YiCeucVLo4XvFLjXukUaQjIiqowkcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + } + }, "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.1.2.tgz", - "integrity": "sha512-DrUBA2UXRfDmUX/ZTiEopd3X40yavsJF1FX2RygcuIScHL7o5YX1fMvoYnDhjeJQC4weCOklirpNWlcb2NiSeA==", + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.4.tgz", + "integrity": "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==", "dev": true, "license": "MIT", "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", - "vitefu": "^1.1.2" + "vitefu": "^1.1.1" }, "engines": { "node": "^20.19 || ^22.12 || >=24" }, "peerDependencies": { - "svelte": "^5.46.4", - "vite": "^8.0.0-beta.7 || ^8.0.0" + "svelte": "^5.0.0", + "vite": "^6.3.0 || ^7.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.2.tgz", + "integrity": "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "obug": "^2.1.0" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", + "svelte": "^5.0.0", + "vite": "^6.3.0 || ^7.0.0" } }, "node_modules/@tybys/wasm-util": { @@ -1822,9 +1931,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" }, @@ -2545,16 +2654,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", @@ -2589,7 +2688,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -2801,9 +2899,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": { @@ -3181,279 +3279,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", @@ -3879,33 +3704,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 +3738,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": { @@ -4120,9 +3919,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 +3936,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 +3947,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", @@ -4432,17 +4232,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 +4259,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 +4274,15 @@ "@types/node": { "optional": true }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, "jiti": { "optional": true }, "less": { "optional": true }, + "lightningcss": { + "optional": true + }, "sass": { "optional": true }, diff --git a/package.json b/package.json index e1c6a86..802c7b4 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "test": "gro test", "preview": "vite preview", "deploy": "gro deploy", - "update-benchmarks": "cp ../tsv/benches/deno/results/report.json src/routes/docs/benchmarks/benchmarks.json" + "update-benchmarks": "cp ../tsv/benches/js/results/report.node.json src/routes/docs/benchmarks/benchmarks.json && cp ../tsv/benches/js/results/report.json src/routes/docs/benchmarks/benchmarks_cross_runtime.json && cp ../tsv/benches/js/results/report.conformance.node.json src/routes/docs/benchmarks/benchmarks_conformance.json && gro format" }, "type": "module", "engines": { @@ -24,56 +24,36 @@ }, "devDependencies": { "@fuzdev/blake3_wasm": "^0.1.1", - "@fuzdev/fuz_code": "^0.46.3", + "@fuzdev/fuz_code": "^0.48.0", "@fuzdev/fuz_css": "^0.63.2", - "@fuzdev/fuz_ui": "^0.206.5", + "@fuzdev/fuz_ui": "^0.206.7", "@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/gro": "^0.209.0", + "@fuzdev/mdz": "^0.3.0", + "@fuzdev/tsv_wasm": "^0.2.0", + "@ryanatkn/eslint-config": "^0.12.2", "@sveltejs/acorn-typescript": "^1.0.9", "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.61.1", - "@sveltejs/vite-plugin-svelte": "^7.1.2", + "@sveltejs/kit": "^2.69.1", + "@sveltejs/vite-plugin-svelte": "^6.2.4", "@types/estree": "^1.0.8", "@types/node": "^24.12.4", "@webref/css": "^8.2.0", "eslint": "^9.39.1", "eslint-plugin-svelte": "^3.13.1", "esm-env": "^1.2.2", - "magic-string": "^0.30.21", - "prettier": "^3.7.4", - "prettier-plugin-svelte": "^3.4.1", - "svelte": "^5.56.0", - "svelte-check": "^4.3.6", + "svelte": "^5.56.4", + "svelte-check": "^4.7.2", "svelte-docinfo": "^0.5.4", "svelte2tsx": "^0.7.47", "tslib": "^2.8.1", "typescript": "^5.9.3", "typescript-eslint": "^8.48.1", - "vite": "^8.0.16", + "vite": "^7.3.6", "vitest": "^4.1.9", "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..7a47499 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,12 +1,12 @@ // Registers ambient types for the `virtual:svelte-docinfo` module (Vite plugin). // eslint-disable-next-line @typescript-eslint/triple-slash-reference /// -declare module 'virtual:fuz.css' { - const css: string; - export default css; +declare module "virtual:fuz.css" { + const css: string; + export default css; } -declare module 'virtual:pkg.json' { - import type {PkgJson} from '@fuzdev/fuz_util/pkg_json.ts'; - const pkg_json: PkgJson; - export default pkg_json; +declare module "virtual:pkg.json" { + 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..a1c58fd 100644 --- a/src/app.html +++ b/src/app.html @@ -1,23 +1,29 @@ - - - - - + + + + + - + - %sveltekit.head% - - -
%sveltekit.body%
- + %sveltekit.head% + + +
%sveltekit.body%
+ diff --git a/src/lib/sample.ts b/src/lib/sample.ts deleted file mode 100644 index 429c922..0000000 --- a/src/lib/sample.ts +++ /dev/null @@ -1 +0,0 @@ -export const sample = 'TODO - publish tsv!'; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 1e580bb..6411db4 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -4,13 +4,13 @@ import './style.css'; import ThemeRoot from '@fuzdev/fuz_ui/ThemeRoot.svelte'; - import {SiteState, site_context} from '@fuzdev/fuz_ui/site.svelte.ts'; - import {logo_tsv} from '@fuzdev/fuz_ui/logos.ts'; - import type {Snippet} from 'svelte'; + import { SiteState, site_context } from '@fuzdev/fuz_ui/site.svelte.ts'; + import { logo_tsv } from '@fuzdev/fuz_ui/logos.ts'; + import type { Snippet } from 'svelte'; import pkg_json from 'virtual:pkg.json'; const { - children, + children }: { children: Snippet; } = $props(); @@ -19,7 +19,7 @@ // and `repo_url` points to the tsv tool repo rather than this website's repo // (`pkg_json.repository`). site_context.set( - new SiteState({icon: logo_tsv, pkg_json, repo_url: 'https://github.com/fuzdev/tsv'}), + new SiteState({ icon: logo_tsv, pkg_json, repo_url: 'https://github.com/fuzdev/tsv' }) ); diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index e98aa2e..c29ee24 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,10 +1,10 @@ @@ -24,9 +24,9 @@
diff --git a/src/routes/docs/+layout.svelte b/src/routes/docs/+layout.svelte index a161ea3..28a2d09 100644 --- a/src/routes/docs/+layout.svelte +++ b/src/routes/docs/+layout.svelte @@ -1,13 +1,13 @@

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

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

- Rather than supporting many languages, tsv focuses on Svelte/HTML, TypeScript/JS, and CSS. - This lets it be smaller when it's all you need, a quality that's more relevant when used in - the browser via wasm: + Compared to Oxc and Biome, tsv is faster, smaller, and uses less memory to parse and format + its supported languages. This section has a prose summary; skip ahead for charts.

- - + {/if} + +

+ Each section below has notes that attempt to fairly contextualize its numbers. The charts show + numbers using {node_display}, and at the end of the page is a + cross-runtime comparison. +

- - + + - +

- tsv's formatter is similar to Oxfmt - and Biome. Today it can format Svelte, TypeScript, and CSS, - plus HTML and JS (as strict-mode TypeScript): + tsv's formatter is similar to + Oxfmt, + Biome, and + dprint. It formats Svelte, TypeScript, + and CSS, plus JS 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 + The parse entries that build a full JS AST are comparable in mechanism: tsv and oxc-parser + both serialize the AST to JSON in Rust and deserialize it in JS, native and wasm alike. But + the deliverables differ — tsv's default wire (tsv-json / + tsv_wasm-json) carries a per-node loc (line/column) object that + oxc-parser's default span-only AST omits, and that loc is roughly half the wire + bytes and most of its JSON.parse cost. The tsv-json-no-locations / + tsv_wasm-json-no-locations + entries drop it, emitting the same span-only shape oxc does, so those are the payload-matched, + apples-to-apples comparison with oxc-parser (line/column stays derivable from the offsets plus + source, so nothing is lost). The tsv-internal and tsv_wasm-internal entries build the native AST but skip JS-side materialization, so they show raw in-engine speed rather than a cross-tool comparison.

+

+ yuku-parser, a JS/TS parser written in Zig, emits the same span-only AST as oxc, so it too + compares against the no-locs entries rather than tsv's loc-bearing + default. It reaches that AST differently: instead of serializing to JSON it returns a compact + binary buffer that its JS side decodes into objects. The deliverable is the same fully + materialized tree, so the entries stay comparable — though every one of them times parsing and + the hand-off into JS together, and this benchmark doesn't split the two. +

{#each parse_groups as group (group.language)} {/each} @@ -143,7 +276,268 @@ JS parsers skip the Rust-to-JS serialization step that tsv and oxc pay for, which keeps them competitive. -
  • Some parts of tsv are not well-optimized yet, particularly parsing CSS.
  • +
  • + Biome is shown grayed-out across all three parse groups because its + @biomejs/js-api + package doesn't expose a parser to JS (only formatting and linting). Biome parses + internally but never surfaces the AST across the JS boundary, so it can't be measured. +
  • +
  • + oxc-parser only parses TypeScript and JS (and JSX, not measured here). oxc-parser doesn't + expose a CSS parser, and it doesn't parse Svelte. +
  • +
  • + yuku-parser and swc both parse TypeScript and JS and nothing else — no CSS, no Svelte, no + formatter — so they appear in the TypeScript parse group only. +
  • +
  • + rsvelte's parser is the only other engine here that parses Svelte, and it's matched to + tsv's default wire on both counts that matter: it also hands JS a compact JSON string, and + on a real component the two payloads are within a couple of percent. So + rsvelte-parse compares against tsv-json directly, not against + the no-locs entries. Its second entry passes rsvelte's own + skipExpressionLoc, which drops loc only on embedded JS + expressions and keeps the top-level offsets — a different trade than tsv's span-only wire, + which is why that entry is named for the option rather than for tsv's. +
  • +
  • + swc parses into its own AST shape — a Module root carrying span + offsets rather than ESTree's loc/range — so it isn't + payload-matched to either tsv wire. +
  • +
  • + PostCSS is the only other CSS parser here, and the only kind available: no Rust CSS parser + exposes an AST to JS. Lightning CSS transforms rather than handing back a tree, Biome + doesn't surface a parser at all, and malva is a formatter. It's also the parser behind + Prettier's CSS printer, which makes it the parse-side counterpart to the Prettier entry in + the format group. +
  • +
  • + a CSS coverage gap is a grammar difference, not a verdict. The CSS reference row is + Svelte's parseCss, which tsv is a drop-in for — and unlike Svelte's parser on + the Svelte side, it isn't an authority on what valid CSS is in either direction. PostCSS + rejects a handful of files parseCss accepts (unclosed strings, a missing + semicolon) and accepts more that it rejects, mostly modern CSS Svelte's parser doesn't + implement yet — @supports selector(…), mixins — rather than anything + malformed. So PostCSS landing a shade above tsv here is two grammars, not a gap. +
  • +
  • + the no-locs entries are the payload-matched comparison with oxc-parser (see + above); the plain tsv-json entries carry the larger loc-bearing + AST, so their slower number isn't an engine-speed difference. +
  • +
  • + tsv internal (node napi) and tsv_wasm internal aren't a fair + comparison to the other entries — they skip JS-side AST materialization entirely, so + they're included only to show tsv's own JSON-serialization overhead against its + non-internal entry. +
  • + + +
    + + + +

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

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

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

    + + +
    + + + +

    + The numbers above measure tsv's engine in-process, one file at a time, using tsv's original + benchmarks. This section describes the results from a fork of Oxc's official + + bench-formatter + + that adds tsv, + timing the whole CLI end-to-end — process spawn, file discovery, I/O, and each tool's default + multi-file parallelism — plus peak memory. It's the "what you experience typing the command" + measure, run on real repositories. tsv appears only in the JSX-free scenarios (it has no + JSX/TSX parser). The tsv binary measured here is the same one @fuzdev/tsv ships + in its platform packages and execs from npx tsv — the npx path just adds Node's + ~20 ms launcher on top. +

    + +
    @@ -152,39 +546,51 @@

    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. + sequentially with no cross-file parallelism. Each row is the total time to process the whole + corpus once on a single core — 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). + What's measured: {corpus_file_count.toLocaleString('en-US')} files of .svelte, + .ts/.js, and .css — real-world code only, from two + sources: the author's libraries, apps, and sites (the fuz.dev ecosystem plus personal + SvelteKit sites), and upstream framework source (Svelte, SvelteKit, and the svelte.dev site). + The CSS set also includes real-authored CSS extracted from those components' + <style> + blocks, concatenated per repo — standalone CSS files are rare in this ecosystem, and the same + bytes appear in the Svelte rows (rows are never summed). Test files count as real code and + stay in; fixture files (the formatter test suites that used to be part of this corpus, and + fixture subtrees inside the measured repos) are excluded — deliberately tricky edge cases + measure conformance, not typical throughput, and are covered by the parse-conformance section + above.

    + + + +

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

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

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

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

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

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

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

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

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

    wasm

    -
    - {#each wasm_display as s (s.label)} - - {/each} -
    -
    -{/if} - -{#if native_display.length > 0} +{#each size_groups as group (group.capability)}
    -

    native

    -
    - {#each native_display as s (s.label)} - - {/each} -
    +

    {group.heading}

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

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

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

    {/if} diff --git a/src/routes/docs/playground/Playground.svelte b/src/routes/docs/playground/Playground.svelte index ba20a09..980fba7 100644 --- a/src/routes/docs/playground/Playground.svelte +++ b/src/routes/docs/playground/Playground.svelte @@ -5,13 +5,14 @@ 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 + // so the ~900KB-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 load_error: string | null = $state(null); @@ -23,7 +24,7 @@ const ready = $derived(tsv !== null); // A tsv call's outcome: its string result, or the thrown error's message. - type Outcome = {value: string; error: null} | {value: null; error: string}; + type Outcome = { value: string; error: null } | { value: null; error: string }; // Run a tsv call, capturing a thrown error as a message; `null` until the WASM // loads. Lets `formatted` and `ast` share one shape and recompute as `source` changes @@ -31,9 +32,9 @@ const run = (fn: (t: NonNullable) => string): Outcome | null => { if (!tsv) return null; try { - return {value: fn(tsv), error: null}; + return { value: fn(tsv), error: null }; } catch (err) { - return {value: null, error: to_error_message(err)}; + return { value: null, error: to_error_message(err) }; } }; @@ -72,6 +73,8 @@ }; }); + const is_base = $derived(source === playground_example); + const reset = (): void => { source = playground_example; }; @@ -84,8 +87,8 @@ }; -
    - +
    + @@ -112,7 +115,10 @@

    formatted

    AST

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