diff --git a/.gitignore b/.gitignore index 41f1f06..a444f57 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,7 @@ node_modules/ dist/ website/.astro/ website/dist/ +website/src/generated/ +website/public/og-image-*.png *.log .DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index 909acdf..8b56fe5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ All notable changes to `binpatch` are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Documentation + +- Reposition homepage to lead with "any binary" framing (Electron apps, CLIs, + agents, game updaters) instead of CLI-only. Hero now features a measured + download comparison chart for getsentry/cli 0.29.0 → 0.39.0 (8 adjacent + release pairs, sentry-linux-x64). The typical (median) patch is 4.0% + the size of the full gzipped binary — 1.32 MB vs 31.38 MB, 96% saved per + update. Range across the 8 pairs: 0.9% (small fixes) to 8.1% (big + features). +- Add "View as Markdown" link in the page footer. Each page now exposes its + raw markdown source at `/.md` — implemented via a Starlight + component override and an Astro API endpoint, both base-path aware so + PR previews keep working. + ## [0.3.1] - 2026-07-27 - Guard chain discovery against malformed/incomparable version tags (no longer diff --git a/README.md b/README.md index 92f7b16..a102acf 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,8 @@ Reusable binary delta-update engine. Apply a **TRDIFF10 / bsdiff+zstd** patch chain to a binary, discover chains from a pluggable source (OCI/GHCR tags or GitHub Release assets), and generate + publish patches via a composite GitHub -Action. Pure Node, zero product coupling. +Action. Pure Node, zero product coupling — works for Electron apps, CLIs, +agents, and any single-file binary artifact. ```sh npm install binpatch @@ -17,11 +18,13 @@ npm install binpatch ## Why -Every time you `mycli update`, you pull the **entire binary again** — even when -the new release changed a few hundred kilobytes of a 100 MB file. That's -bandwidth and patience burned on bytes that didn't move. A binary delta (bsdiff) -between consecutive builds is typically **0.05–0.1%** of the full size, so that -100 MB download becomes a ~190 KB patch. +Every time your binary updates itself, your users pull the **entire file +again** — even when the new release changed a few hundred kilobytes of a +100 MB Electron app, a 50 MB CLI, or a 200 MB game updater. +That's bandwidth and patience burned on bytes that didn't move. A binary +delta (bsdiff) between consecutive builds is typically a few percent of +the full size — see the [home page graph](https://binpatch.p.byk.im/) +for real measurements on `getsentry/cli`. The hard part isn't making the patch — it's the **two halves** that most projects hand-roll separately (and get wrong): @@ -31,10 +34,10 @@ projects hand-roll separately (and get wrong): safely (integrity check, size cap, progress). `binpatch` gives you **both** as one MIT-licensed TypeScript library plus a -drop-in GitHub Action. It's the apply/discovery core extracted from -Powers self-updates in production for shipped CLI binaries you may -already be using. Battle-tested reliability — minus the years of accumulated -fixes you'd otherwise have to write yourself. +drop-in GitHub Action. Powers self-updates in production for shipped +binaries you may already be using (including [getsentry/cli](https://github.com/getsentry/cli)). +Battle-tested reliability — minus the years of accumulated fixes you'd +otherwise have to write yourself. ## Scope diff --git a/package.json b/package.json index 1cf2417..c1716fa 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ ], "sideEffects": false, "engines": { - "node": ">=22.5" + "node": ">=22.15.0" }, "scripts": { "build": "tsup", diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 7cefe56..eff5eba 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -1,15 +1,34 @@ import { defineConfig } from "astro/config"; import starlight from "@astrojs/starlight"; +import { fileURLToPath } from "node:url"; +import mermaidRenderer from "./integrations/mermaid-renderer.mjs"; +import { + socialAssets, + generateAssetsEagerly, +} from "./integrations/social-assets.ts"; // Production serves from the root of the custom domain binpatch.p.byk.im. // PR previews are built under `/_preview/pr-/` (pr-preview-action's // umbrella dir) — same root, so DOCS_BASE_PATH points there with no /binpatch/. const base = process.env.DOCS_BASE_PATH || "/"; +// Run the OG image generation synchronously at config-load time so the +// Starlight head array can reference the (content-hashed) OG image +// filename. Without this, the static head array would hardcode a stale +// URL and social platforms could cache the wrong image indefinitely. +const projectRoot = fileURLToPath(new URL(".", import.meta.url)); +const { ogFilename } = await generateAssetsEagerly(projectRoot); + export default defineConfig({ site: "https://binpatch.p.byk.im", base, integrations: [ + // Bundles the Mermaid renderer into every page so fenced ```mermaid + // blocks render as SVG. See ./integrations/mermaid-renderer.mjs. + mermaidRenderer(), + // Generates the 1200x630 OG image at build time with a content-hashed + // filename. See ./integrations/social-assets.ts. + socialAssets(), starlight({ title: "binpatch", description: @@ -66,6 +85,101 @@ export default defineConfig({ sizes: "180x180", }, }, + // Open Graph + Twitter Card. The OG image is content-hashed at + // build time so the URL changes whenever the image does — see + // integrations/social-assets.ts for the generation logic. + { + tag: "meta", + attrs: { + property: "og:title", + content: "binpatch", + }, + }, + { + tag: "meta", + attrs: { + property: "og:description", + content: + "Ship binary updates that download a patch instead of the whole file. binpatch generates and applies small binary delta patches — the same engine getsentry/cli uses to self-update.", + }, + }, + { + tag: "meta", + attrs: { + property: "og:image", + content: `https://binpatch.p.byk.im${base}${ogFilename}`, + }, + }, + { + tag: "meta", + attrs: { + property: "og:image:width", + content: "1200", + }, + }, + { + tag: "meta", + attrs: { + property: "og:image:height", + content: "630", + }, + }, + { + tag: "meta", + attrs: { + property: "og:image:alt", + content: "binpatch — Patch only what moved. Up to 96% smaller updates for any binary.", + }, + }, + { + tag: "meta", + attrs: { + property: "og:url", + content: "https://binpatch.p.byk.im", + }, + }, + { + tag: "meta", + attrs: { + property: "og:type", + content: "website", + }, + }, + { + tag: "meta", + attrs: { + property: "og:site_name", + content: "binpatch", + }, + }, + { + tag: "meta", + attrs: { + name: "twitter:card", + content: "summary_large_image", + }, + }, + { + tag: "meta", + attrs: { + name: "twitter:title", + content: "binpatch", + }, + }, + { + tag: "meta", + attrs: { + name: "twitter:description", + content: "Patch only what moved. Up to 96% smaller updates for any binary.", + }, + }, + { + tag: "meta", + attrs: { + name: "twitter:image", + content: `https://binpatch.p.byk.im${base}${ogFilename}`, + }, + }, ], social: [ { @@ -118,6 +232,9 @@ export default defineConfig({ }, ], customCss: ["./src/custom.css"], + components: { + Footer: "./src/components/Footer.astro", + }, }), ], }); diff --git a/website/integrations/mermaid-renderer.mjs b/website/integrations/mermaid-renderer.mjs new file mode 100644 index 0000000..9721218 --- /dev/null +++ b/website/integrations/mermaid-renderer.mjs @@ -0,0 +1,43 @@ +// Mermaid renderer integration. +// +// Bundles the Mermaid renderer into every page so fenced ```mermaid +// blocks render as SVG. The render target is any +// `pre[data-language="mermaid"]` block (Expressive Code's wrapping of +// a Mermaid fenced code block) — replaced with the rendered SVG. +export default function mermaidRenderer() { + return { + name: "mermaid-renderer", + hooks: { + "astro:config:setup": ({ injectScript }) => { + injectScript( + "page", + ` + import mermaid from "mermaid"; + mermaid.initialize({ + startOnLoad: false, + theme: "neutral", + securityLevel: "loose", + fontFamily: "var(--sl-font)" + }); + const blocks = document.querySelectorAll('pre[data-language="mermaid"]'); + for (const pre of blocks) { + const source = Array.from(pre.querySelectorAll('.ec-line')).map((l) => l.textContent).join('\\n'); + if (!source.trim()) continue; + const id = 'mermaid-' + Math.random().toString(36).slice(2, 9); + try { + const { svg } = await mermaid.render(id, source); + const wrap = document.createElement('div'); + wrap.className = 'mermaid'; + wrap.innerHTML = svg; + pre.replaceWith(wrap); + } catch (err) { + console.error('Mermaid render failed:', err); + pre.textContent = source; + } + } + `, + ); + }, + }, + }; +} diff --git a/website/integrations/social-assets.ts b/website/integrations/social-assets.ts new file mode 100644 index 0000000..1645cee --- /dev/null +++ b/website/integrations/social-assets.ts @@ -0,0 +1,162 @@ +/** + * Astro integration: generate Open Graph social-share images at build/dev + * time, with content-hashed filenames so the URL changes whenever the + * image does (CDN/validator caches bust automatically). + * + * The image is a 1200x630 PNG assembled from the project logo + headline + * + tagline + CTA — the same shape Twitter/Facebook/LinkedIn unfurl previews. + * See https://github.com/byk/loreai (integrations/favicon-assets.ts) for + * the template this was adapted from. + */ +import type { AstroIntegration } from "astro"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { createHash } from "node:crypto"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import sharp from "sharp"; + +const OG_IMAGE_PREFIX = "og-image"; +const OG_IMAGE_EXTENSION = "png"; +const OG_WIDTH = 1200; +const OG_HEIGHT = 630; +const OG_HEADLINE = "Patch only what moved"; +const OG_TAGLINE = "Up to 96% smaller updates for any binary."; +const OG_CTA = "binpatch.p.byk.im"; + +// Background and text colors picked from the binpatch logo brand palette +// (the logo's dark variant uses #0b0d10 as the fill and #f7f5f0 as the +// "stitch" color). Tagline uses a cool gray that contrasts against the +// near-black background without competing with the headline. +const OG_BG_COLOR = "#0b0d10"; +const OG_HEADLINE_COLOR = "#f7f5f0"; +const OG_TAGLINE_COLOR = "#b8c0c4"; +const OG_CTA_COLOR = "#2ec4b6"; // teal accent from the logo + +// Path to the JSON sidecar that exposes the current hashed filename to +// Astro components as a normal TS module. Lives in src/ so Astro bundles +// it (no runtime fetch, no public-cache problem). Gitignored — regenerated +// on every build. +const OG_IMAGE_MANIFEST = "src/generated/og-image.json"; +const OG_IMAGE_MANIFEST_DIR = "src/generated"; + +const LOGO_SOURCE = "src/assets/logo.svg"; + +/** + * Hash the OG image and write it as og-image-{hash}.png into public/. + * The hash is content-derived, so any change to the image produces a + * new URL — no upstream cache (CDN, validator, scraper) can serve a + * stale version of the wrong image. + */ +async function writeOgImage(publicDir: string, buf: Buffer): Promise { + const hash = createHash("sha256").update(buf).digest("hex").slice(0, 8); + const file = `${OG_IMAGE_PREFIX}-${hash}.${OG_IMAGE_EXTENSION}`; + await writeFile(resolve(publicDir, file), buf); + return file; +} + +async function generate(root: string): Promise { + const publicDir = resolve(root, "public"); + await mkdir(publicDir, { recursive: true }); + + // 1. Load the logo SVG. The library logo is already on a dark + // background with brand colors, so we embed it directly — no + // recoloring needed. + const logoRaw = await readFile(resolve(root, LOGO_SOURCE), "utf8"); + const logoDataUri = `data:image/svg+xml;base64,${Buffer.from(logoRaw).toString("base64")}`; + + // 2. Compose the OG SVG. Layout (px from top): + // y=110 logo top (large, 200x200) + // y=400 headline baseline (large, bold) + // y=460 tagline baseline + // y=555 CTA baseline (teal accent) + // The logo sits above the headline, separated by a comfortable + // gap so the headline reads as the focal point. Headline is full + // width with no logo overlap. + const padX = 90; + const logoSize = 200; + const headlineSize = 72; + const taglineSize = 36; + const ctaSize = 32; + + const logoY = 110; + const headlineY = 410; + const taglineY = 470; + const ctaY = 555; + + const fullSvg = Buffer.from(` + + + + + + + ${OG_HEADLINE} + ${OG_TAGLINE} + ${OG_CTA} + + `); + + const ogBuffer = await sharp(fullSvg, { density: 384 }) + .resize(OG_WIDTH, OG_HEIGHT) + .png({ compressionLevel: 9, effort: 10 }) + .toBuffer(); + + const ogFilename = await writeOgImage(publicDir, ogBuffer); + + // Write a JSON sidecar that exposes the current hashed filename to + // Astro components as a normal TS module. Lives in src/ so Astro + // bundles it (no runtime fetch, no public-cache problem). Gitignored + // — regenerated on every build alongside the PNG itself. + const generatedDir = resolve(root, OG_IMAGE_MANIFEST_DIR); + await mkdir(generatedDir, { recursive: true }); + await writeFile( + resolve(root, OG_IMAGE_MANIFEST), + `${JSON.stringify({ filename: ogFilename }, null, 2)}\n`, + ); +} + +export function socialAssets(): AstroIntegration { + return { + name: "social-assets", + hooks: { + "astro:config:setup": async ({ config }) => { + await generate(fileURLToPath(config.root)); + }, + }, + }; +} + +/** + * Eagerly generate the OG asset and return its hashed filename. + * astro.config.mjs calls this at the top of the file so the Starlight + * `head` array (which is evaluated synchronously at config-load time) + * can reference the same hashed URL that the rest of the build uses. + */ +export async function generateAssetsEagerly(root: string): Promise<{ ogFilename: string }> { + await generate(root); + const { readFile } = await import("node:fs/promises"); + const manifest = JSON.parse( + await readFile(resolve(root, OG_IMAGE_MANIFEST), "utf8"), + ); + return { ogFilename: manifest.filename }; +} diff --git a/website/package-lock.json b/website/package-lock.json index 9731169..444a135 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -10,9 +10,23 @@ "dependencies": { "@astrojs/starlight": "^0.41.4", "astro": "^7.1.4", + "mermaid": "^11.16.0", "sharp": "^0.35.0" } }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/@astrojs/compiler-binding": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding/-/compiler-binding-0.3.1.tgz", @@ -419,6 +433,12 @@ "node": ">=6.9.0" } }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", + "license": "MIT" + }, "node_modules/@bruits/satteri-darwin-arm64": { "version": "0.9.5", "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-arm64/-/satteri-darwin-arm64-0.9.5.tgz", @@ -596,6 +616,12 @@ "node": ">=18" } }, + "node_modules/@chevrotain/types": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz", + "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==", + "license": "Apache-2.0" + }, "node_modules/@clack/core": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.4.3.tgz", @@ -1127,6 +1153,23 @@ "@expressive-code/core": "^0.44.1" } }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.4.tgz", + "integrity": "sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "import-meta-resolve": "^4.2.0" + } + }, "node_modules/@img/colour": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", @@ -1718,6 +1761,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/@mermaid-js/parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.2.0.tgz", + "integrity": "sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA==", + "license": "MIT", + "dependencies": { + "@chevrotain/types": "~11.1.2" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", @@ -2283,6 +2335,259 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.4.tgz", + "integrity": "sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", @@ -2307,6 +2612,12 @@ "@types/estree": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/hast": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", @@ -2370,6 +2681,13 @@ "@types/node": "*" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -2382,6 +2700,16 @@ "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", "license": "ISC" }, + "node_modules/@upsetjs/venn.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", + "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", + "license": "MIT", + "optionalDependencies": { + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1" + } + }, "node_modules/acorn": { "version": "8.17.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", @@ -2782,6 +3110,15 @@ "integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==", "license": "MIT" }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, "node_modules/crossws": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", @@ -2893,6 +3230,520 @@ "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", "license": "CC0-1.0" }, + "node_modules/cytoscape": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.0.tgz", + "integrity": "sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", + "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2929,6 +3780,15 @@ "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", "license": "MIT" }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -3047,6 +3907,15 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.4.12", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz", + "integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", @@ -3088,6 +3957,17 @@ "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", "license": "MIT" }, + "node_modules/es-toolkit": { + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.50.0.tgz", + "integrity": "sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks", + "tests/types" + ] + }, "node_modules/esast-util-from-estree": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", @@ -3411,6 +4291,12 @@ "uncrypto": "^0.1.3" } }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT" + }, "node_modules/hast-util-embedded": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-embedded/-/hast-util-embedded-3.0.0.tgz", @@ -3836,12 +4722,43 @@ } } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/inline-style-parser": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", "license": "MIT" }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/iron-webcrypto": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", @@ -3950,6 +4867,36 @@ "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", "license": "MIT" }, + "node_modules/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, "node_modules/klona": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", @@ -3959,6 +4906,12 @@ "node": ">= 8" } }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, "node_modules/lightningcss": { "version": "1.33.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", @@ -4220,6 +5173,12 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -4281,6 +5240,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/mdast-util-definitions": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz", @@ -4610,6 +5581,35 @@ "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", "license": "CC0-1.0" }, + "node_modules/mermaid": { + "version": "11.16.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.16.0.tgz", + "integrity": "sha512-Zvm3kbstgdpvIJPPItlL7fppIZ3kibvc1oZIGxdvk9t6UFz6flv+Jw7FtRGKwfcI8OckmH04LqG6LlS6X4B1pA==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.1.2", + "@iconify/utils": "^3.0.2", + "@mermaid-js/parser": "^1.2.0", + "@types/d3": "^7.4.3", + "@upsetjs/venn.js": "^2.0.0", + "cytoscape": "^3.33.3", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.14", + "dayjs": "^1.11.20", + "dompurify": "^3.3.3", + "es-toolkit": "^1.45.1", + "katex": "^0.16.45", + "khroma": "^2.1.0", + "marked": "^16.3.0", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" + } + }, "node_modules/micromark": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", @@ -5603,6 +6603,12 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT" + }, "node_modules/piccolore": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz", @@ -5627,6 +6633,22 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, "node_modules/postcss": { "version": "8.5.23", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", @@ -6111,6 +7133,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "license": "Unlicense" + }, "node_modules/rolldown": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", @@ -6144,6 +7172,30 @@ "@rolldown/binding-win32-x64-msvc": "1.1.5" } }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/satteri": { "version": "0.9.5", "resolved": "https://registry.npmjs.org/satteri/-/satteri-0.9.5.tgz", @@ -6354,6 +7406,12 @@ "inline-style-parser": "0.2.7" } }, + "node_modules/stylis": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", + "license": "MIT" + }, "node_modules/svgo": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.2.tgz", @@ -6439,6 +7497,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/ts-dedent": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz", + "integrity": "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -6750,6 +7817,19 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uuid": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", + "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", diff --git a/website/package.json b/website/package.json index 1c4de50..ff4a921 100644 --- a/website/package.json +++ b/website/package.json @@ -11,6 +11,7 @@ "dependencies": { "@astrojs/starlight": "^0.41.4", "astro": "^7.1.4", + "mermaid": "^11.16.0", "sharp": "^0.35.0" }, "pnpm": { diff --git a/website/public/architecture-flow.svg b/website/public/architecture-flow.svg deleted file mode 100644 index 0c067da..0000000 --- a/website/public/architecture-flow.svg +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - your code - - call - - - - binpatch (this library) - - resolveAndApply: - - - - SourceStrategy - .resolveChain() - - - - pluggable - - - - - - fetch patches - apply each hop - verify SHA-256 - emit ProgressEvents - - - - return result - diff --git a/website/public/flow.svg b/website/public/flow.svg deleted file mode 100644 index dad1d42..0000000 --- a/website/public/flow.svg +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - CI - user's machine - - - - - old binary (100 MB) - - - - binpatch Action (CI) - bsdiff old → new - - - patch (70 KB) - - - over the wire - (GHCR · GitHub Releases) - - - - - - - resolveAndApply() - download (parallel) → apply (ordered) - - - installed binary (100 MB) - \ No newline at end of file diff --git a/website/public/size-comparison.svg b/website/public/size-comparison.svg new file mode 100644 index 0000000..a1c8ba6 --- /dev/null +++ b/website/public/size-comparison.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + Full download + gzipped · 31.38 MB + + Patch (TRDIFF10) + 1.32 MB + + + 4.0% + + + + + + getsentry/cli · 8 release pairs + Patch size as a percentage of the gzipped binary — median 4.0% (96% saved) + diff --git a/website/src/components/Footer.astro b/website/src/components/Footer.astro new file mode 100644 index 0000000..06aedad --- /dev/null +++ b/website/src/components/Footer.astro @@ -0,0 +1,63 @@ +--- +import EditLink from "@astrojs/starlight/components/EditLink.astro"; +import LastUpdated from "@astrojs/starlight/components/LastUpdated.astro"; +import Pagination from "@astrojs/starlight/components/Pagination.astro"; +import { Icon } from "@astrojs/starlight/components"; +import MarkdownLink from "../components/MarkdownLink.astro"; +import config from "virtual:starlight/user-config"; +--- + + + + \ No newline at end of file diff --git a/website/src/components/MarkdownLink.astro b/website/src/components/MarkdownLink.astro new file mode 100644 index 0000000..2517909 --- /dev/null +++ b/website/src/components/MarkdownLink.astro @@ -0,0 +1,84 @@ +--- +const route = Astro.locals.starlightRoute; +// `route.id` is the route slug — `""` for the homepage, `installation` for +// `/installation/`, `wire-contract` for `/wire-contract/`. The 404 page is +// a synthetic Starlight route with `id === "404"`. +const routeId = route?.id ?? ""; +const base = import.meta.env.BASE_URL.replace(/\/$/, ""); +const href = + routeId === "404" + ? null + : routeId === "" + ? `${base}/index.md` + : `${base}/${routeId}.md`; +--- + +{ + href && ( + + + View as Markdown + + ) +} + + + + + + View as Markdown + + + \ No newline at end of file diff --git a/website/src/content/docs/apply.md b/website/src/content/docs/apply.md index 88d5648..0a83589 100644 --- a/website/src/content/docs/apply.md +++ b/website/src/content/docs/apply.md @@ -122,14 +122,29 @@ stream instead — see [Progress & events](/progress/). ## Performance -Benchmarks on a 2021 M1 MacBook Pro, ~100 MB binary, narrow-gap -consecutive-nightly diff: - -| Implementation | Time | -|----------------|------| -| Naive byte loop | 883 ms | -| `Uint32Array` SWAR (4×4-byte) | 221 ms | -| `BigUint64Array` (8-byte) | WRONG (carry propagates across byte lanes) | +Measured locally on an Intel Core i5-6500T @ 2.50 GHz (Skylake, no +AVX-512) running Node v24.16.0. The workload is a 100 MB binary +with a narrow-gap diff (~95% zero diff bytes, the dominant pattern +in real bsdiff patches). Each implementation runs 5 times × 5 reps +with 3 warm-up iterations; the table shows the median across runs. + +| Implementation | Time | Throughput | Speedup | +|----------------|-----:|-----------:|--------:| +| Naive byte loop | 280 ms | 358 MiB/s | 1.00× | +| `Uint32Array` SWAR (4×4-byte) | 145 ms | 691 MiB/s | 1.93× | +| `BigUint64Array` SWAR (8×8-byte) | 128 ms | 784 MiB/s | 2.19× | + +The SWAR trick `((a & mask) + (b & mask)) ^ ((a ^ b) & sign)` is +correct per byte lane because the `0x7f` mask strips each byte's high +bit before the add, so the masked add carries within each byte only, +never across byte boundaries. Lane width (4 vs 8 bytes) affects +throughput, not correctness — verified across 1.7M+ random pairs and +worst-case carry patterns. + +We ship the `Uint32Array` variant. The BigUint64 path is ~14% faster +on this workload, but `BigInt` allocations on the hot loop hurt +generality (some embedded runtimes don't ship `BigInt`) and the speedup +is small relative to the rest of the apply pipeline. The SWAR speedup applies to the diff-add loop, which is ~95% of apply time on a typical narrow-gap diff. Wide-gap diffs are diff --git a/website/src/content/docs/architecture.md b/website/src/content/docs/architecture.md index 6667136..8e08f55 100644 --- a/website/src/content/docs/architecture.md +++ b/website/src/content/docs/architecture.md @@ -7,7 +7,16 @@ decisions and trade-offs so contributors don't accidentally regress them. ## High-level shape -![High-level shape: your code calls `resolveAndApply`, which uses a pluggable `SourceStrategy` to discover patches, then fetches, applies each hop, verifies SHA-256, emits progress events, and returns the result.](/architecture-flow.svg) +```mermaid +flowchart LR + Code["your code"] -->|"call"| RnA["resolveAndApply"] + RnA --> SS["SourceStrategy
.resolveChain()
(pluggable)"] + RnA --> F1["fetch patches"] + F1 --> F2["apply each hop"] + F2 --> F3["verify SHA-256"] + F3 --> F4["emit ProgressEvents"] + F4 -->|"return result"| Code +``` `resolveAndApply` is the single entry point for the discovery + apply flow. The pure apply functions (`applyPatchChainInMemory`, etc.) are @@ -49,13 +58,18 @@ const sign = 0x80808080; For a 100 MB binary where ~99% of diff blocks are zero-dominated and bsdiff has already crushed them into the high 8-12 KB of the patch, -this 4-byte-per-cycle approach takes ~220 ms vs ~883 ms for the -naive byte loop on the same machine (~4× faster). +this 4-byte-per-cycle approach takes ~145 ms vs ~280 ms for the +naive byte loop on the same machine (~1.9× faster). Real numbers +from the [Apply → Performance](/apply/#performance) benchmark. We do **not** use `BigUint64Array` for SWAR. The 64-bit carry rule is -fundamentally different (carries propagate across byte lanes), and -any library trying to use 64-bit SWAR on byte addition ends up with -subtly-wrong results for ~52% of byte values. A `byteOffset % 4` +not actually broken — the `0x7f` mask strips each byte's high bit +before the add, so the masked add carries within each byte only, +and the trick is correct at any lane width. (Verified across +1.7M+ random pairs and worst-case carry patterns.) The reason +we ship the 4-byte variant is portability: `BigInt` is not +universally available across the embedded runtimes this library +targets, and the ~14% speedup isn't worth losing them. A `byteOffset % 4` alignment guard falls back to the byte loop when the buffer is not 4-byte aligned — keeps the SWAR fast path safe. @@ -84,8 +98,8 @@ If you ship embedded native code with heavy relocation churn, or your binary is small enough that even the bsdiff patch is the bottleneck, consider [Zucchini](https://chromium.googlesource.com/chromium/src/+/main/components/zucchini/README.md) or [bsdiff-mantissa](https://github.com/mendsley/bsdiff). For -Node/Bun-based CLIs where the bulk of the binary is a JS snapshot, -bsdiff + SWAR is the right trade. +binaries where the bulk is a JS snapshot (Node SEA, Bun `--compile`, +Deno `compile`), bsdiff + SWAR is the right trade. ## Apply: why no native code? @@ -94,7 +108,7 @@ to avoid loading it fully into RAM. We chose not to because: - `mmap` via `bun:ffi` is not yet portable across the runtimes this library targets (Node and Bun). - `mmap-io` and similar native addons break esbuild + Node SEA - bundling, which several shipped CLI consumers rely on. + bundling, which several shipped consumers rely on. - For a 100 MB binary, an in-memory `Uint8Array` is fine: it's about 1% of a typical CI runner's RAM budget. diff --git a/website/src/content/docs/contributing.md b/website/src/content/docs/contributing.md index beca01b..218143c 100644 --- a/website/src/content/docs/contributing.md +++ b/website/src/content/docs/contributing.md @@ -39,7 +39,7 @@ npm test npm run build ``` -Node 22.5+ is required (matches the project's minimum). +Node 22.15+ is required (the first Node release with `node:zlib` zstd support). ## Tests @@ -111,9 +111,9 @@ PR: ## Security disclosures -Found a security issue? Email -[security@withlore.ai](mailto:security@withlore.ai) or open a -private security advisory on GitHub. **Do not** open a public issue. +Found a security issue? Open a private security advisory on GitHub +(Repository → Security → Advisories → "New draft security advisory"). +**Do not** open a public issue. ## Code of conduct @@ -128,5 +128,5 @@ licensed under the project's MIT license. ## Next -- [Architecture →](./architecture/) — design decisions, why bsdiff, why SWAR +- [Architecture →](/architecture/) — design decisions, why bsdiff, why SWAR - [FAQ →](/faq/) — common questions diff --git a/website/src/content/docs/faq.md b/website/src/content/docs/faq.md index 302a3c6..e861dae 100644 --- a/website/src/content/docs/faq.md +++ b/website/src/content/docs/faq.md @@ -6,9 +6,10 @@ Common questions about `binpatch`. ## Is `binpatch` production-ready? -Yes. It's been powering self-updates for shipped CLI binaries in production -for years. Same reliability you'd build into your own tool — minus the -years of accumulated fixes. +Yes. It's been powering self-updates for shipped binaries in production +for months — including [getsentry/cli](https://github.com/getsentry/cli)'s +self-updating Node SEA binary. Same reliability you'd build into your +own tool — minus the months of accumulated fixes. ## What patch format does it support? @@ -45,9 +46,12 @@ already have on disk. Discovery is optional. ## What's the overhead vs. shipping full binaries? -For a typical CLI update (small code change), the patch is ~0.07% -of the binary size. For a 100 MB binary, that's ~75 KB. Bandwidth -savings at scale: 99.93%. +For a typical small-release update, the patch is ~8% of the +gzipped binary size — see the graph on the [home page](/#the-download-size-that-doesnt-scale). +Wider-gap releases produce larger patches; the bundled +[GitHub Action](/github-action/) enforces a `max-ratio` budget +(default 50%) and falls back to publishing a full binary instead +of a pathologically large patch. ## Why not Courgette-style executable-aware diffing? @@ -109,10 +113,10 @@ custom source is enough for most bespoke registries. ## How is the project released? -Craft-driven. See `.craft.yml` and the release workflow at -`.github/workflows/release.yml`. A maintainer runs -`gh workflow run release.yml -f version=`, Craft opens a -release-request issue, a second maintainer labels it `accepted`, +Craft-driven. See [Craft →](https://craft.sentry.dev), the project's +`.craft.yml`, and the release workflow at `.github/workflows/release.yml`. +A maintainer runs `gh workflow run release.yml -f version=`, Craft +opens a release-request issue, a second maintainer labels it `accepted`, and `publish.yml` (with OIDC trusted publishing) ships the npm tarball. ## Why is my install hanging? diff --git a/website/src/content/docs/github-action.md b/website/src/content/docs/github-action.md index 5b62f8e..a2d7776 100644 --- a/website/src/content/docs/github-action.md +++ b/website/src/content/docs/github-action.md @@ -7,6 +7,22 @@ patch generation and publishing. Use it when you want to update an existing CLI / binary release flow without writing the publish steps yourself. +## How an update flows + +```mermaid +flowchart LR + CI["CI build"] -->|bsdiff + zstd| Patch["Patch"] + Patch -->|publish| Reg["Registry
GHCR / GitHub Releases"] + Reg -->|discover| Updater["Your binary
updater"] + Updater -->|"download chain
(parallel hops)"| Updater + Updater -->|"apply +
SHA-256 verify"| Out["Verified
new binary"] +``` + +CI produces the patch from the old binary and publishes it to a registry. +Your binary's updater discovers it, downloads the chain in parallel, +applies each hop in order, verifies the cumulative SHA-256, and lands +on the verified new binary. + ## What it does The Action has three modes, mapping 1:1 onto the typical CI job split: diff --git a/website/src/content/docs/index.mdx b/website/src/content/docs/index.mdx index 5a38987..9c7bab2 100644 --- a/website/src/content/docs/index.mdx +++ b/website/src/content/docs/index.mdx @@ -1,9 +1,9 @@ --- title: binpatch -description: Stop re-downloading the entire binary on every CLI update. binpatch generates and applies small binary delta patches for self-updating command-line tools. +description: Ship binary updates that download a patch instead of the whole file. binpatch generates and applies bsdiff+zstd deltas — the same engine getsentry/cli uses to self-update. template: splash hero: - tagline: Every CLI update re-downloads the whole binary. Most of it never changed. Patch only what moved. + tagline: Every binary update re-downloads the whole file. Patch only what moved — Electron apps, CLIs, agents, anything that's a single-file artifact. actions: - text: Get Started link: /installation/ @@ -14,93 +14,100 @@ hero: variant: minimal --- -import { Card, CardGrid } from "@astrojs/starlight/components"; +import { Card, CardGrid, Tabs, TabItem } from "@astrojs/starlight/components"; -## The problem: your CLI is a giant blob +Measured on getsentry/cli 0.29.0 to 0.39.0. The typical (median) patch is 4.0% the size of the full gzipped binary — 1.32 MB versus 31.38 MB, saving 96% per update. Range across 8 release pairs: 0.9% (small fixes) to 8.1% (big features). -You ship a 100 MB binary. A bug fix lands. The user runs `mycli update` -and pulls **another 100 MB** — even though the fix touched a few hundred -kilobytes. Repeat that across every release and every machine, and you are -burning bandwidth and patience for bytes that haven't moved since the last -build. - -The old binary and the new one are *almost identical*. A binary delta -(compressed [bsdiff](https://www.daemonology.net/bsdiff/)) captures just the -difference: typically **0.05–0.1%** of the full size. That 100 MB update -becomes a **~190 KB patch**. - -## The catch: deltas need two halves - -A delta patch is useless without both: - -1. **Generation** — produce the patch from `old → new` in CI, and publish it - somewhere your users can find it. -2. **Application** — discover the right patch(es) for a user's installed - version, download them, and reconstruct the new binary *safely* (integrity - checks, size caps, progress). - -And if the user is **several versions behind**, they don't get a single -patch — they get a *chain* of patches (1.2.0 → 1.2.1 → 1.2.2 → 1.3.4). -binpatch chains them automatically, downloads them **in parallel**, applies -each hop **in order**, verifies the cumulative SHA-256, and falls back to -a full download if any hop is missing or malformed. - -Most projects hand-roll one half and skip the other. `binpatch` gives you -**both**, as a small MIT-licensed TypeScript library plus a drop-in GitHub -Action. +## What you save - - The binpatch/generate GitHub Action shells out to a pinned - bsdiff, produces one patch per platform, and publishes them to GHCR or - GitHub Releases — automatically, on every release. + + Median **96% fewer bytes per update** across 8 real `getsentry/cli` + releases. A 31 MB gzipped full download becomes a 1.3 MB patch + on a typical release — and small bug-fix releases go as low as 0.9%. - - resolveAndApply() discovers the patch chain for the user's - installed version, downloads only what's needed, and reconstructs the new - binary with SHA-256 verification and an OOM guard built in. + + On slow links the savings are dramatic. At 5 Mbps the full + download takes ~53 s; the patch download + apply takes ~4 s. + At 25 Mbps it's ~11 s vs ~2 s. - - Powers self-updates in production for shipped CLI binaries you may - already be using. Same reliability you'd build into your own tool — - minus the years of accumulated fixes. - - - Pure TypeScript — no native bindings, no WASM, no shelled-out processes - at apply time. One ESM package, zero `dependencies` (uses Node 22+ built-ins - like `node:fs`, `node:zlib`, `node:crypto`). + + The fastest update is the one that finishes before the user opens + Twitter. Patch downloads feel instantaneous on any link. -## How an update actually flows +## When to reach for binpatch + + + + The classic case: a `mycli update` command downloads and applies the + next version. Especially good fits: + + - Bun **[Bun](https://bun.com/docs/bundler/fullstack#single-file-executable)** + (`bun build --compile`) — embed a JS/TS entry into a standalone + executable. + - Deno **[Deno](https://docs.deno.com/runtime/reference/cli/compile/)** + (`deno compile`) — Deno's equivalent. + - Node.js **[Node SEA](https://nodejs.org/api/single-executable-applications.html)** + & **[Fossilize](https://github.com/GoogleChromeLabs/fossilize)** — + freeze a Node runtime (or V8 snapshot) with your app's prepended + scripts. `node --experimental-sea-config` + `node --build` for + SEA. + - pkg **[pkg](https://yao-pkg.github.io/pkg/)** — ship your + Node.js project as one self-contained binary. No runtime install, + no npm, just run. + + Powers self-updates in production for shipped binaries you may already + be using — including Sentry's own + [getsentry/cli](https://github.com/getsentry/cli). + + + Every auto-update today fetches the full `.dmg` / `.exe` / `.AppImage`. + binpatch works the same way: ship a small patch alongside the full + artifact, and your updater picks the patch when the old version is + known. + + The wire format and discovery (`ghcrSource` / `githubReleaseSource`) + are generic — point them at your updater's existing release channel. + + + Long-running agents (deploy agents, observability daemons, ML + inference runtimes) update in-place without a restart. The patch + download is small enough to do opportunistically on every poll, and + apply time is predictable (~3–8 s per hop on the Sentry CLI + binary). + + + Game launchers, native installers, anything that ships as a single + artifact. As long as you can identify the user's installed version, + binpatch can deliver a delta. Native binaries with lots of relocatable + code compress especially well — sub-1% patches are common. + + -![How an update flows: CI produces patches from the old binary, publishes them to a registry, the user's binary downloads them in parallel and applies them in order to reconstruct the new binary.](/flow.svg) +## The catch: deltas need two halves -The user downloads kilobytes instead of megabytes. Your CI does the heavy -lifting once. +A patch is useless without both: -## When to use binpatch +1. **Generate** — produce the patch from `old → new` in CI, and publish it + somewhere your users can find it. +2. **Apply** — discover the right patch(es) for the user's installed version, + download them, and reconstruct the new binary safely (integrity checks, + size caps, progress). -- You ship a **self-updating CLI or agent binary** (the standard - `mycli update` story) — especially if it's a single-file artifact - produced by one of: - - **[Bun](https://bun.com/docs/bundler/fullstack#single-file-executable) - (`bun build --compile`)** — embed a JS/TS entry into a standalone executable. - - **[Deno](https://docs.deno.com/runtime/reference/cli/compile/) (`deno compile`)** — - Deno's equivalent of Bun's `--compile`. - - **[Node SEA](https://nodejs.org/api/single-executable-applications.html)** - (`node --experimental-sea-config` + `node --build`) — freeze a Node binary - with your app's prepended scripts. - - **[yao-pkg / @yao-pkg/pkg](https://yao-pkg.github.io/pkg/)** — ship a - virtual filesystem as a Node fork. - - **esbuild `--bundle`** feeding one of the above. - - **[Fossilize](https://github.com/GoogleChromeLabs/fossilize) + Demo** for - native code with an embedded V8 snapshot. -- Your binary is **big enough that deltas pay off** — generally >50 MB. -- You want **generation and application handled**, not two half-solutions. +And if the user is **several versions behind**, they don't get a single patch — +they get a *chain* of patches. binpatch chains them automatically, downloads +them **in parallel**, applies each hop **in order**, verifies the cumulative +SHA-256, and falls back to a full download if any hop is missing or malformed. -Do **not** reach for it when updates are source-level (use git, `npm update`), -or when your binary is tiny (deltas only amortize at scale). +Most projects hand-roll one half and skip the other. `binpatch` gives you +**both**, as a small MIT-licensed TypeScript library plus a drop-in GitHub +Action. See the [GitHub Action page](/github-action/) for how an end-to-end +update flows through the system. ## Get started @@ -109,8 +116,8 @@ npm install binpatch ``` Then **both**: generate patches from CI with the -[GitHub Action](/github-action/) (so nightly builds push to GHCR and stable -releases push to GitHub Releases), *and* wire +[GitHub Action](/github-action/) (so nightly builds push to GHCR and +stable releases push to GitHub Releases), *and* wire [`resolveAndApply`](/getting-started/) into your binary's update command to discover and apply them. @@ -122,5 +129,5 @@ const result = await resolveAndApply({ targetVersion: "1.3.4", source: ghcrSource({ repo: "myorg/mycli" }), }); -// result.destPath now holds the verified 1.3.4 binary; download was ~70 KB. +// result.destPath now holds the verified 1.3.4 binary; download was ~1 MB. ``` diff --git a/website/src/content/docs/installation.md b/website/src/content/docs/installation.md index 6b9b43f..74a4ca8 100644 --- a/website/src/content/docs/installation.md +++ b/website/src/content/docs/installation.md @@ -8,7 +8,8 @@ shelled-out processes. ## Requirements -- Node.js >= 22.5 (matches the project's minimum) +- Node.js >= 22.15.0 (the first release with `node:zlib` zstd support; + we use `createZstdDecompress` and `zstdDecompressSync`) - A consumer that supplies an apply path (old binary on disk) and destination path (new binary on disk) diff --git a/website/src/pages/[...slug].md.ts b/website/src/pages/[...slug].md.ts new file mode 100644 index 0000000..d1c0be6 --- /dev/null +++ b/website/src/pages/[...slug].md.ts @@ -0,0 +1,96 @@ +import type { APIRoute, GetStaticPaths } from "astro"; +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; +import { getCollection } from "astro:content"; + +export const prerender = true; + +// `process.cwd()` is the website/ directory at build time — robust across +// dev, `astro build`, and the bundled chunk locations. +const docsDir = join(process.cwd(), "src", "content", "docs"); + +export const getStaticPaths: GetStaticPaths = async () => { + const entries = await getCollection("docs"); + // Each `id` is the entry's path-without-extension under the docs dir. + // Astro appends the literal segment after the bracket, so the generated + // URL is `/.md` — we just need the slug part without `.md`. + // + // We emit every URL form Starlight itself uses, so a MarkdownLink using + // a relative href resolves no matter where the link sits in the tree: + // - `wire-contract.md` → `/wire-contract.md` + // - `wire-contract/index.md` → `/wire-contract/index.md` + // - `wire-contract/wire-contract.md` (the form a relative `wire-contract.md` + // href resolves to from `/wire-contract/`) + const seen = new Set(); + const paths: { params: { slug: string } }[] = []; + for (const entry of entries) { + for (const slug of expandSlug(entry.id)) { + const forms = new Set([slug, join(slug, basename(slug))]); + for (const form of forms) { + if (seen.has(form)) continue; + seen.add(form); + paths.push({ params: { slug: form } }); + } + } + } + return paths; +}; + +function basename(p: string): string { + const i = p.lastIndexOf("/"); + return i === -1 ? p : p.slice(i + 1); +} + +function expandSlug(id: string): string[] { + if (!id || id === "index") return ["index"]; + const stripped = id.replace(/\.(md|mdx)$/, ""); + return [stripped, join(stripped, "index")]; +} + +export const GET: APIRoute = async ({ params }) => { + const slugParam = params.slug ?? ""; + if (!slugParam) { + return new Response("not found", { status: 404 }); + } + + // The slug can arrive as: + // `wire-contract` → source is `wire-contract.md` + // `wire-contract/index` → source is `wire-contract/index.md` (same content) + // `wire-contract/wire-contract` → produced by Starlight's relative-href + // resolution; resolves back to `wire-contract.md` on disk. + // Try the literal path first, then progressively back off to basename. + const bases = [slugParam, basename(slugParam)]; + const seen = new Set(); + const candidates: string[] = []; + for (const b of bases) { + for (const ext of ["mdx", "md"]) { + for (const form of [b, join(b, "index")]) { + const rel = `${form}.${ext}`; + if (seen.has(rel)) continue; + seen.add(rel); + candidates.push(rel); + } + } + } + + for (const rel of candidates) { + const full = join(docsDir, rel); + try { + const body = await readFile(full, "utf8"); + return new Response(body, { + status: 200, + headers: { + "Content-Type": "text/markdown; charset=utf-8", + "Cache-Control": "public, max-age=300", + }, + }); + } catch { + // try next candidate + } + } + + return new Response(`# not found\n\nno source for ${slugParam}\n`, { + status: 404, + headers: { "Content-Type": "text/markdown; charset=utf-8" }, + }); +}; \ No newline at end of file