diff --git a/.changeset/smooth-bars-relate.md b/.changeset/smooth-bars-relate.md new file mode 100644 index 00000000..6fa5b5ba --- /dev/null +++ b/.changeset/smooth-bars-relate.md @@ -0,0 +1,5 @@ +--- +"@stephansama/single-file": minor +--- + +created single file cli package diff --git a/.config/.cspell.json b/.config/.cspell.json index d9082118..5bffaacd 100644 --- a/.config/.cspell.json +++ b/.config/.cspell.json @@ -13,6 +13,7 @@ "barhandles", "bluwy", "catppuccin", + "cleye", "commitlint", "dotenvx", "esbuild", diff --git a/.config/lefthook.yml b/.config/lefthook.yml new file mode 100644 index 00000000..9037e6d7 --- /dev/null +++ b/.config/lefthook.yml @@ -0,0 +1,15 @@ +prepare-commit-msg: + jobs: + - run: ai-commit-msg -o {1} +commit-msg: + jobs: + - run: pnpm dlx commitlint --config .config/.commitlintrc.ts --edit {1} +pre-commit: + parallel: true + jobs: + - run: auto-readme -vg + name: Update README + - run: lint-staged -v + name: Lint staged + - run: pnpm --workspace-root run scripts:lint-examples + name: Lint examples diff --git a/.gitignore b/.gitignore index 35db97d6..6403776d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,8 +9,8 @@ .config/www/public/* .env** .next -.react-router .publish +.react-router .svelte-kit .turbo .wrangler @@ -22,4 +22,5 @@ core/types-lhci/config dist*/ examples/catppuccin-xsl/vanilla/public/**/* node_modules +single-file.html storybook-static diff --git a/.husky/commit-msg b/.husky/commit-msg deleted file mode 100644 index f5d44c79..00000000 --- a/.husky/commit-msg +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -pnpm dlx commitlint --config .config/.commitlintrc.ts --edit "$1" diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100644 index 00f78934..00000000 --- a/.husky/pre-commit +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -auto-readme -g -lint-staged -v -pnpm --workspace-root run scripts:lint-examples diff --git a/.husky/prepare-commit-msg b/.husky/prepare-commit-msg deleted file mode 100644 index e932fce7..00000000 --- a/.husky/prepare-commit-msg +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -ai-commit-msg -o "$1" diff --git a/README.md b/README.md index 4d704125..e69de29b 100644 --- a/README.md +++ b/README.md @@ -1,86 +0,0 @@ -
- -# [`@stephansama`](https://github.com/stephansama/packages) packages - -[![PNPM](https://img.shields.io/badge/PNPM-10.9-F69220.svg?logo=pnpm&logoColor=white&labelColor=F69220)](https://github.com/search?q=repo%3Astephansama%2Fnvim%20language%3Alua&type=code) -[![TypeScript](https://img.shields.io/badge/TypeScript-5.8.3-3178C6.svg?logo=typescript&logoColor=white&labelColor=3178C6)](https://github.com/search?q=repo%3Astephansama%2Fnvim%20language%3ATypeScript&type=code) -[![Turborepo](https://img.shields.io/badge/Turborepo-2.5.4-FF1E56.svg?logo=turborepo&logoColor=white&labelColor=FF1E56)](https://turborepo.com/) - -[![codecov](https://codecov.io/github/stephansama/packages/graph/badge.svg)](https://codecov.io/github/stephansama/packages) -[![🦋 Changesets Release](https://github.com/stephansama/packages/actions/workflows/release.yml/badge.svg)](https://github.com/stephansama/packages/actions/workflows/release.yml) -[![CodeQL](https://github.com/stephansama/packages/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/stephansama/packages/actions/workflows/github-code-scanning/codeql) - -Collection of open-source [npm](https://www.npmx.dev/) packages - -
- -##### Table of contents - -
Open Table of contents - -- [Introduction](#introduction) -- [📦 Packages](#-packages) - - [☂️ Codecov coverage graph](#️-codecov-coverage-graph) - - [⭐ Stargazers](#-stargazers) -- [Related repositories](#related-repositories) - -
- -## Introduction - -view examples here 👉 [![packages](https://pkg.pr.new/badge/stephansama/packages?style=flat&color=000&logoSize=auto)](https://pkg.pr.new/~/stephansama/packages) - -or install an example with [`create-stephansama-example`](https://github.com/stephansama/packages/tree/main/core/example) -via `pnpm create stephansama-example` - -## 📦 Packages - -All packages are packaged underneath the `@stephansama` scope (for example: `@stephansama/remark-asciinema`) - - - -### 🏭 workspace - -| 🏷️ Name | Version | 📥 Downloads | 📝 Description | -| ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | -| [ai-commit-msg](core/ai-commit-msg/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Fai-commit-msg?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/ai-commit-msg?labelColor=211F1F) | generate commit messages using ai | -| [alfred-kaomoji](core/alfred-kaomoji/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Falfred-kaomoji?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/alfred-kaomoji?labelColor=211F1F) | Alfred Kaomoji Picker | -| [astro-iconify-svgmap](core/astro-iconify-svgmap/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Fastro-iconify-svgmap?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/astro-iconify-svgmap?labelColor=211F1F) | Astro integration for generating iconify svgmaps for ssg sites | -| [auto-readme](core/auto-readme/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Fauto-readme?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/auto-readme?labelColor=211F1F) | Generate lists and tables for your README automagically based on your repository and comments | -| [catppuccin-jsonresume-theme](core/catppuccin-jsonresume-theme/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Fcatppuccin-jsonresume-theme?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/catppuccin-jsonresume-theme?labelColor=211F1F) | theme for resume cli website | -| [catppuccin-opml](core/catppuccin-opml/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Fcatppuccin-opml?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/catppuccin-opml?labelColor=211F1F) | Catppuccin styled opml stylesheet | -| [catppuccin-rss](core/catppuccin-rss/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Fcatppuccin-rss?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/catppuccin-rss?labelColor=211F1F) | Catppuccin x Pretty-feed-v3 | -| [catppuccin-typedoc](core/catppuccin-typedoc/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Fcatppuccin-typedoc?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/catppuccin-typedoc?labelColor=211F1F) | Catppuccin css variable theme for typedoc | -| [catppuccin-xsl](core/catppuccin-xsl/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Fcatppuccin-xsl?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/catppuccin-xsl?labelColor=211F1F) | Catppuccin styles for various xsl formats | -| [create-stephansama-example](core/example/README.md) | ![npm version image](https://img.shields.io/npm/v/create-stephansama-example?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/create-stephansama-example?labelColor=211F1F) | Download an example from the @stephansama/packages examples | -| [find-makefile-targets](core/find-makefile-targets/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Ffind-makefile-targets?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/find-makefile-targets?labelColor=211F1F) | Find makefile targets used to pipe into fzf | -| [github-env](core/github-env/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Fgithub-env?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/github-env?labelColor=211F1F) | \[Deprecated] Additional environment variable types for GitHub CI | -| [multipublish](core/multipublish/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Fmultipublish?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/multipublish?labelColor=211F1F) | Publish packages to multiple providers easily | -| [prettier-plugin-handlebars](core/prettier-plugin-handlebars/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Fprettier-plugin-handlebars?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/prettier-plugin-handlebars?labelColor=211F1F) | Prettier plugin that automatically assigns the default parser for various handlebars files | -| [remark-asciinema](core/remark-asciinema/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Fremark-asciinema?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/remark-asciinema?labelColor=211F1F) | A remark plugin that transforms Asciinema links into embedded players or screenshots. | -| [svelte-social-share-links](core/svelte-social-share-links/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Fsvelte-social-share-links?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/svelte-social-share-links?labelColor=211F1F) | Svelte/Web component to share the current url with various social media providers | -| [typed-env](core/typed-env/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Ftyped-env?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/typed-env?labelColor=211F1F) | standard schema compatible environment validator | -| [typed-events](core/typed-events/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Ftyped-events?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/typed-events?labelColor=211F1F) | Typed events store using standard schema | -| [typed-nocodb-api](core/typed-nocodb-api/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Ftyped-nocodb-api?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/typed-nocodb-api?labelColor=211F1F) | Typed API client for NocoDB using Zod | -| [typed-templates](core/typed-templates/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Ftyped-templates?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/typed-templates?labelColor=211F1F) | Use standard schema to validate and use handlebar template directories | -| [types-github-action-env](core/types-github-action-env/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Ftypes-github-action-env?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/types-github-action-env?labelColor=211F1F) | environment variable types for GitHub Action environment | -| [types-lhci](core/types-lhci/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Ftypes-lhci?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/types-lhci?labelColor=211F1F) | types for lhci configuration | - - - -
- -### ☂️ Codecov coverage graph - -![graph](https://codecov.io/github/stephansama/packages/graphs/tree.svg) - -### ⭐ Stargazers - -[![Stargazers repo roster for @stephansama/packages](https://reporoster.com/stars/stephansama/packages)](https://github.com/stephansama/packages/stargazers) - -
- -## Related repositories - -- [stow.nvim](https://github.com/stephansama/stow.nvim) -- [@stephansama/actions](https://github.com/stephansama/actions) diff --git a/core/single-file/README.md b/core/single-file/README.md new file mode 100644 index 00000000..e569d588 --- /dev/null +++ b/core/single-file/README.md @@ -0,0 +1,60 @@ +# @stephansama/single-file + +[![Source code](https://img.shields.io/badge/Source-666666?style=flat&logo=github&label=Github&labelColor=211F1F)](https://github.com/stephansama/packages/tree/main/core/single-file) +[![Documentation](https://img.shields.io/badge/Documentation-211F1F?style=flat&logo=Wikibooks&labelColor=211F1F)](https://packages.stephansama.info/api/@stephansama/single-file) +[![NPM Version](https://img.shields.io/npm/v/%40stephansama%2Fsingle-file?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F)](https://www.npmx.dev/package/@stephansama/single-file) +[![JSR](https://jsr.io/badges/@stephansama/single-file)](https://jsr.io/@stephansama/single-file) +[![socket.dev](https://badge.socket.dev/npm/package/@stephansama/single-file)](https://socket.dev/npm/package/@stephansama/single-file/overview) +[![npm downloads](https://img.shields.io/npm/dw/@stephansama/single-file?labelColor=211F1F)](https://www.npmx.dev/package/@stephansama/single-file) + +Fetch any webpage and produce a fully self-contained HTML file with all external resources — images, stylesheets, scripts, and SVGs — inlined directly into the document. + +##### Table of contents + +
Open Table of contents + +- [Installation](#installation) +- [CLI](#cli) +- [Usage](#usage) + +
+ +## Installation + +```sh +pnpm install @stephansama/single-file +``` + +## CLI + +```sh +# outputs to single-file.html by default +npx @stephansama/single-file + +# custom output path +npx @stephansama/single-file --output my-page.html +npx @stephansama/single-file -o my-page.html + +# verbose logging +npx @stephansama/single-file --verbose +npx @stephansama/single-file -v +``` + +| Flag | Alias | Default | Description | +| ----------- | ----- | ------------------ | ----------------------------- | +| `--output` | `-o` | `single-file.html` | Output path for the HTML file | +| `--verbose` | `-v` | `false` | Enable verbose output | + +## Usage + +```javascript +import singleFile from "@stephansama/single-file"; + +export async function useAPI() { + const file = await singleFile.convertPageToSingleFile( + "https://blog.stephansama.info", + ); + + console.info(file); +} +``` diff --git a/core/single-file/cli.mjs b/core/single-file/cli.mjs new file mode 100644 index 00000000..3e298e1a --- /dev/null +++ b/core/single-file/cli.mjs @@ -0,0 +1,10 @@ +#!/usr/bin/env node + +"use strict"; + +import("./dist/cli.js") + .then((mod) => mod.run()) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/core/single-file/example/index.js b/core/single-file/example/index.js new file mode 100644 index 00000000..19f106ff --- /dev/null +++ b/core/single-file/example/index.js @@ -0,0 +1,9 @@ +import singleFile from "../dist/index.cjs"; + +export async function useAPI() { + const file = await singleFile.convertPageToSingleFile( + "https://blog.stephansama.info", + ); + + console.info(file); +} diff --git a/core/single-file/package.json b/core/single-file/package.json new file mode 100644 index 00000000..7d3b0a93 --- /dev/null +++ b/core/single-file/package.json @@ -0,0 +1,60 @@ +{ + "name": "@stephansama/single-file", + "version": "0.0.0", + "description": "create a single html file from a website url", + "keywords": [ + "single-file" + ], + "homepage": "https://packages.stephansama.info/api/@stephansama/single-file", + "repository": { + "type": "git", + "url": "git+https://github.com/stephansama/packages.git", + "directory": "core/single-file" + }, + "license": "MIT", + "author": { + "name": "Stephan Randle", + "email": "stephanrandle.dev@gmail.com", + "url": "https://stephansama.info" + }, + "type": "module", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "./package.json": "./package.json" + }, + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.cts", + "bin": "./cli.mjs", + "files": [ + "cli.mjs", + "dist" + ], + "scripts": { + "build": "tsdown", + "dev": "tsdown --watch", + "lint": "eslint ./ --pass-on-no-patterns --no-error-on-unmatched-pattern", + "lint:fix": "eslint ./ --fix" + }, + "dependencies": { + "cheerio": "catalog:", + "dedent": "catalog:", + "he": "catalog:", + "ky": "catalog:", + "obug": "catalog:cli", + "oxc-parser": "catalog:" + }, + "devDependencies": { + "@types/he": "catalog:", + "cleye": "catalog:cli", + "tsdown": "catalog:" + }, + "publishConfig": { + "access": "public", + "provenance": true + }, + "readme": "./README.md" +} diff --git a/core/single-file/src/cli.ts b/core/single-file/src/cli.ts new file mode 100644 index 00000000..fdb7ed20 --- /dev/null +++ b/core/single-file/src/cli.ts @@ -0,0 +1,32 @@ +import { cli } from "cleye"; +import * as fs from "node:fs"; + +import { convertPageToSingleFile } from "."; +import * as log from "./log"; + +export async function run() { + const argv = cli({ + flags: { + output: { + alias: "o", + default: "single-file.html", + description: "output path for single html file", + type: String, + }, + verbose: { + alias: "v", + default: false, + description: "Verbose output", + type: Boolean, + }, + }, + name: "single-file", + parameters: [``], + }); + + log.enable(argv.flags.verbose); + + const file = await convertPageToSingleFile(argv._.url); + + await fs.promises.writeFile(argv.flags.output, file, "utf8"); +} diff --git a/core/single-file/src/import-map.test.ts b/core/single-file/src/import-map.test.ts new file mode 100644 index 00000000..1369e7a2 --- /dev/null +++ b/core/single-file/src/import-map.test.ts @@ -0,0 +1,184 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { + determineImportType, + imports, + loadImport, + writeImportMap, +} from "./import-map"; + +const mockKy = vi.hoisted(() => ({ get: vi.fn() })); + +vi.mock("ky", () => ({ default: mockKy })); + +function makeMockResponse(opts: { + buffer?: ArrayBuffer; + contentType?: null | string; + text?: string; +}) { + return { + arrayBuffer: vi + .fn() + .mockResolvedValue(opts.buffer ?? new ArrayBuffer(0)), + headers: { get: vi.fn().mockReturnValue(opts.contentType ?? null) }, + text: vi.fn().mockResolvedValue(opts.text ?? ""), + }; +} + +beforeEach(() => { + imports.clear(); +}); + +afterEach(vi.clearAllMocks); + +describe("determineImportType", () => { + it.each([ + ["application/javascript", "js"], + ["text/javascript", "js"], + ["application/ecmascript", "js"], + ["text/ecmascript", "js"], + ] as const)("%s → %s", (contentType, expected) => { + expect(determineImportType(contentType)).toBe(expected); + }); + + it("returns unknown for text/css", () => { + expect(determineImportType("text/css")).toBe("unknown"); + }); + + it("returns unknown for null", () => { + expect(determineImportType(null)).toBe("unknown"); + }); + + it("returns unknown for undefined", () => { + expect(determineImportType(undefined)).toBe("unknown"); + }); +}); + +describe("loadImport", () => { + it("loads text content and stores it in the imports map", async () => { + mockKy.get.mockReturnValue( + makeMockResponse({ + contentType: "text/javascript", + text: "console.log('hi')", + }), + ); + + const result = await loadImport({ file: "https://example.com/app.js" }); + + expect(result).toBe("console.log('hi')"); + const cached = imports.get("https://example.com/app.js"); + expect(cached?.type).toBe("js"); + expect(cached?.data).toBe("console.log('hi')"); + }); + + it("loads binary content when isBinary is true", async () => { + const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]).buffer; + mockKy.get.mockReturnValue( + makeMockResponse({ buffer, contentType: "image/png" }), + ); + + const result = await loadImport({ + file: "https://example.com/img.png", + isBinary: true, + }); + + expect(result).toBeInstanceOf(ArrayBuffer); + expect(imports.get("https://example.com/img.png")?.type).toBe("binary"); + }); + + it("returns cached text without making a second HTTP request", async () => { + mockKy.get.mockReturnValue( + makeMockResponse({ + contentType: "text/javascript", + text: "cached", + }), + ); + + await loadImport({ file: "https://example.com/lib.js" }); + await loadImport({ file: "https://example.com/lib.js" }); + + expect(mockKy.get).toHaveBeenCalledTimes(1); + }); + + it("returns cached binary without making a second HTTP request", async () => { + mockKy.get.mockReturnValue( + makeMockResponse({ + buffer: new ArrayBuffer(4), + contentType: "image/png", + }), + ); + + await loadImport({ + file: "https://example.com/img.png", + isBinary: true, + }); + await loadImport({ + file: "https://example.com/img.png", + isBinary: true, + }); + + expect(mockKy.get).toHaveBeenCalledTimes(1); + }); + + it("joins dirname with file when dirname is provided", async () => { + mockKy.get.mockReturnValue(makeMockResponse({ text: "body" })); + + await loadImport({ dirname: "/assets", file: "script.js" }); + + expect(mockKy.get).toHaveBeenCalledWith("/assets/script.js"); + }); +}); + +describe("writeImportMap", () => { + it("returns an empty registry when there are no JS imports", async () => { + imports.set("https://example.com/style.css", { + contentType: "text/css", + data: "body {}", + type: "unknown", + }); + + const result = await writeImportMap(); + + expect(result).toContain(" { + imports.set("https://example.com/lib.js", { + contentType: "text/javascript", + data: "export const x = 1;", + type: "js", + }); + + const result = await writeImportMap(); + + expect(result).toContain("https://example.com/lib.js"); + expect(result).toContain("URL.createObjectURL"); + }); + + it("escapes special characters in JS content", async () => { + imports.set("https://example.com/tmpl.js", { + contentType: "text/javascript", + data: "const x = `${y}`;", + type: "js", + }); + + const result = await writeImportMap(); + + expect(result).not.toContain("`${y}`"); + expect(result).toContain("\\${y}"); + }); + + it("excludes non-JS imports from the registry", async () => { + imports.set("https://example.com/img.png", { + contentType: "image/png", + data: new ArrayBuffer(4), + type: "binary", + }); + + const result = await writeImportMap(); + + expect(result).not.toContain("img.png"); + }); +}); diff --git a/core/single-file/src/import-map.ts b/core/single-file/src/import-map.ts new file mode 100644 index 00000000..c028030d --- /dev/null +++ b/core/single-file/src/import-map.ts @@ -0,0 +1,97 @@ +import ky from "ky"; +import path from "node:path"; + +import * as utilities from "./utilities"; + +export const WINDOW_KEY = "imports" as const; + +export const importTypes = ["css", "font", "js", "binary", "unknown"] as const; +export type ImportType = (typeof importTypes)[number]; + +export const imports = new Map< + string, + { + contentType: null | string; + data: ArrayBuffer | string; + path?: string; + type: ImportType; + } +>(); + +export type LoadImportArgs = { dirname?: string; file: string }; + +export function determineImportType(contentType?: null | string): ImportType { + switch (contentType) { + case "application/ecmascript": + case "application/javascript": + case "text/ecmascript": + case "text/javascript": + return "js"; + default: + return "unknown"; + } +} + +export async function loadImport( + args: LoadImportArgs & { isBinary: true }, +): Promise; +export async function loadImport( + args: LoadImportArgs & { isBinary?: false }, +): Promise; + +export async function loadImport({ + dirname, + file, + isBinary, +}: { + dirname?: string; + file: string; + isBinary?: boolean; +}): Promise { + const loaded = imports.get(file); + if (loaded) { + if (isBinary && loaded.data instanceof ArrayBuffer) return loaded.data; + if (!isBinary && typeof loaded.data === "string") return loaded.data; + } + + const resolvedFile = dirname ? path.join(dirname, file) : file; + const response = await ky.get(resolvedFile); + const contentType = response.headers.get("Content-Type"); + + if (isBinary) { + const buffer = await response.arrayBuffer(); + imports.set(file, { contentType, data: buffer, type: "binary" }); + return buffer; + } + + const text = await response.text(); + const importType = determineImportType(contentType); + imports.set(file, { contentType, data: text, type: importType }); + return text; +} + +export async function writeImportMap() { + const registryImports = imports + .entries() + .filter(([_, entry]) => entry.type === "js") + .map(([file, source]) => { + if (typeof source.data !== "string") { + throw new Error( + `source is not of type string (shouldn't happen)`, + ); + } + + const escaped = utilities.escapeScript(source.data); + return ( + '"' + + file + + '": URL.createObjectURL(new Blob([`' + + escaped + + "`], {type: 'text/javascript'}))" + ); + }); + + const registryString = Array.from(registryImports).join("\n"); + + return ``; +} diff --git a/core/single-file/src/index.ts b/core/single-file/src/index.ts new file mode 100644 index 00000000..b6740a4d --- /dev/null +++ b/core/single-file/src/index.ts @@ -0,0 +1,30 @@ +import * as cheerio from "cheerio"; +import ky from "ky"; + +import { writeImportMap } from "./import-map"; +import * as inline from "./inline"; +import * as log from "./log"; + +export async function convertPageToSingleFile(url: string) { + const pageResponse = await ky.get(url); + + log.info(`loading ${url}`); + + if (!pageResponse?.headers?.get("Content-Type")?.includes("text/html")) { + throw new Error(`requested url \`${url}\` must be an html page.`); + } + + const page = await pageResponse.text(); + + const $ = cheerio.load(page); + + for (const inlineCallback of Object.values(inline)) { + await inlineCallback($, url); + } + + const registryScript = await writeImportMap(); + + $("head").prepend(registryScript); + + return $.html(); +} diff --git a/core/single-file/src/inline.test.ts b/core/single-file/src/inline.test.ts new file mode 100644 index 00000000..d8d747fe --- /dev/null +++ b/core/single-file/src/inline.test.ts @@ -0,0 +1,266 @@ +import * as cheerio from "cheerio"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { imports } from "./import-map"; +import { img, link, script, svgUse } from "./inline"; + +const mockKy = vi.hoisted(() => ({ get: vi.fn() })); + +vi.mock("ky", () => ({ default: mockKy })); + +function makeMockResponse(opts: { + buffer?: ArrayBuffer; + contentType?: null | string; + text?: string; +}) { + return { + arrayBuffer: vi + .fn() + .mockResolvedValue(opts.buffer ?? new ArrayBuffer(0)), + headers: { get: vi.fn().mockReturnValue(opts.contentType ?? null) }, + text: vi.fn().mockResolvedValue(opts.text ?? ""), + }; +} + +beforeEach(() => { + imports.clear(); +}); + +afterEach(vi.clearAllMocks); + +describe("img", () => { + it("inlines a PNG image as a base64 data URI", async () => { + mockKy.get.mockReturnValue( + makeMockResponse({ + buffer: new Uint8Array([0x89, 0x50, 0x4e, 0x47]).buffer, + contentType: "image/png", + }), + ); + + const $ = cheerio.load(''); + await img($, "https://example.com"); + + expect($("img").attr("src")).toMatch(/^data:image\/png;base64,/); + }); + + it("inlines a plain SVG (no fragment) as a base64 data URI", async () => { + const svgContent = ``; + mockKy.get.mockReturnValue( + makeMockResponse({ + buffer: Buffer.from(svgContent).buffer as ArrayBuffer, + contentType: "image/svg+xml", + }), + ); + + const $ = cheerio.load(''); + await img($, "https://example.com"); + + expect($("img").attr("src")).toMatch(/^data:image\/svg\+xml;base64,/); + }); + + it("inlines an SVG symbol fragment as a URL-encoded data URI", async () => { + const svgContent = ``; + mockKy.get.mockReturnValue( + makeMockResponse({ + buffer: Buffer.from(svgContent).buffer as ArrayBuffer, + contentType: "image/svg+xml", + }), + ); + + const $ = cheerio.load( + '', + ); + await img($, "https://example.com"); + + expect($("img").attr("src")).toMatch(/^data:image\/svg\+xml,/); + }); + + it("skips img elements with a non-URL src", async () => { + const $ = cheerio.load(''); + await img($, "https://example.com"); + + expect($("img").attr("src")).toBe("not a valid url"); + expect(mockKy.get).not.toHaveBeenCalled(); + }); +}); + +describe("link", () => { + it("replaces a stylesheet link with an inline style tag", async () => { + mockKy.get.mockReturnValue( + makeMockResponse({ + contentType: "text/css", + text: "body { color: red; }", + }), + ); + + const $ = cheerio.load( + '', + ); + await link($, "https://example.com"); + + expect($("link").length).toBe(0); + expect($("style").text()).toContain("body { color: red; }"); + }); + + it("replaces a favicon href with a data URI", async () => { + mockKy.get.mockReturnValue( + makeMockResponse({ + buffer: new Uint8Array([0, 1, 2]).buffer, + contentType: "image/x-icon", + }), + ); + + const $ = cheerio.load( + '', + ); + await link($, "https://example.com"); + + expect($("link").attr("href")).toMatch(/^data:/); + }); + + it("replaces an apple-touch-icon href with a data URI", async () => { + mockKy.get.mockReturnValue( + makeMockResponse({ + buffer: new Uint8Array([0, 1, 2]).buffer, + contentType: "image/png", + }), + ); + + const $ = cheerio.load( + '', + ); + await link($, "https://example.com"); + + expect($("link").attr("href")).toMatch(/^data:/); + }); + + it("leaves unrecognized rel attributes unchanged", async () => { + const $ = cheerio.load( + '', + ); + await link($, "https://example.com"); + + expect(mockKy.get).not.toHaveBeenCalled(); + expect($("link").attr("href")).toBe("https://fonts.googleapis.com"); + }); + + it("skips link elements with a non-URL href", async () => { + const $ = cheerio.load(''); + await link($, "https://example.com"); + + expect(mockKy.get).not.toHaveBeenCalled(); + }); +}); + +describe("script", () => { + it("inlines script content and removes the src attribute", async () => { + mockKy.get.mockReturnValue( + makeMockResponse({ + contentType: "text/javascript", + text: "console.log('hello');", + }), + ); + + const $ = cheerio.load( + '', + ); + await script($, "https://example.com"); + + expect($("script").attr("src")).toBeUndefined(); + expect($("script").text()).toContain("console.log('hello')"); + }); + + it("rewrites static imports to use the import map", async () => { + mockKy.get.mockReturnValue( + makeMockResponse({ + contentType: "text/javascript", + text: `import { foo } from "https://example.com/lib.js";\nfoo();`, + }), + ); + + const $ = cheerio.load( + '', + ); + await script($, "https://example.com"); + + const content = $("script").text(); + expect(content).toContain(`window["imports"]`); + expect(content).toContain("await import("); + expect(content).not.toContain(`import { foo } from`); + }); + + it("rewrites dynamic imports to use the import map", async () => { + mockKy.get.mockReturnValue( + makeMockResponse({ + contentType: "text/javascript", + text: `const mod = import("https://example.com/lib.js");`, + }), + ); + + const $ = cheerio.load( + '', + ); + await script($, "https://example.com"); + + expect($("script").text()).toContain(`window["imports"]`); + }); + + it("skips script elements with a non-URL src", async () => { + const $ = cheerio.load(''); + await script($, "https://example.com"); + + expect(mockKy.get).not.toHaveBeenCalled(); + expect($("script").attr("src")).toBe("not a url"); + }); +}); + +describe("svgUse", () => { + const svgSprite = ``; + + it("replaces the use element with symbol content and sets viewBox on the parent svg", async () => { + mockKy.get.mockReturnValue( + makeMockResponse({ contentType: "image/svg+xml", text: svgSprite }), + ); + + const $ = cheerio.load( + '', + ); + await svgUse($, "https://example.com"); + + expect($("use").length).toBe(0); + expect($("svg").attr("viewBox")).toBe("0 0 32 32"); + }); + + it("warns and skips use elements without a hash fragment", async () => { + const $ = cheerio.load( + '', + ); + await svgUse($, "https://example.com"); + + expect(mockKy.get).not.toHaveBeenCalled(); + expect($("use").length).toBe(1); + }); + + it("throws when the referenced symbol is not found in the fetched SVG", async () => { + mockKy.get.mockReturnValue( + makeMockResponse({ contentType: "image/svg+xml", text: svgSprite }), + ); + + const $ = cheerio.load( + '', + ); + + await expect(svgUse($, "https://example.com")).rejects.toThrow( + "unable to parse parent", + ); + }); + + it("logs an error and skips use elements with a non-URL href", async () => { + const $ = cheerio.load( + '', + ); + await svgUse($, "https://example.com"); + + expect(mockKy.get).not.toHaveBeenCalled(); + }); +}); diff --git a/core/single-file/src/inline.ts b/core/single-file/src/inline.ts new file mode 100644 index 00000000..4d545781 --- /dev/null +++ b/core/single-file/src/inline.ts @@ -0,0 +1,184 @@ +import * as cheerio from "cheerio"; +import { default as html, default as js } from "dedent"; +import path from "node:path"; +import * as oxc from "oxc-parser"; + +import * as importMap from "./import-map"; +import * as log from "./log"; +import * as utilities from "./utilities"; + +export type InlineFunction = ( + $: cheerio.CheerioAPI, + baseUrl: string, +) => Promise; + +export const img: InlineFunction = async ($, baseUrl) => { + for (const img of $("img[src]")) { + log.info(`loading \`img\` ${img.attribs.src}`); + + const src = utilities.isUrl(img.attribs.src, baseUrl); + if (!src) continue; + + const importedImage = await importMap.loadImport({ + file: src, + isBinary: true, + }); + const extension = img.attribs.src + .split(".") + .at(-1) + ?.replace(/#.*/, "") + .replace(/\?.*/, ""); + + switch (extension) { + case "svg": { + if (img.attribs.src.includes("#")) { + const $$ = cheerio.load(Buffer.from(importedImage), { + xmlMode: true, + }); + const [_, hash] = img.attribs.src.split("#"); + if (!hash) continue; + + const symbol = $$(`symbol#${hash}`); + const viewBox = symbol.attr("viewBox") || "0 0 24 24"; + const inner = symbol.html(); + const svg = html` + + ${inner} + + `.trim(); + + const encoded = encodeURIComponent(svg) + .replace(/'/g, "%27") + .replace(/"/g, "%22"); + + $(img).attr("src", `data:image/svg+xml,${encoded}`); + break; + } else { + const dataUri = await utilities.bufferToDataUri( + importedImage, + importMap.imports.get(src)?.contentType, + ); + $(img).attr("src", dataUri); + } + break; + } + default: { + const dataUri = await utilities.bufferToDataUri( + importedImage, + importMap.imports.get(src)?.contentType, + ); + $(img).attr("src", dataUri); + } + } + } +}; + +export const link: InlineFunction = async ($, baseUrl) => { + for (const link of $("link[href]")) { + log.info(`loading \`link\` ${link.attribs.href}`); + + const src = utilities.isUrl(link.attribs.href, baseUrl); + if (!src) continue; + + switch (link.attribs.rel) { + case "apple-touch-icon": + case "icon": + case "shortcut icon": { + const buffer = await importMap.loadImport({ + file: src, + isBinary: true, + }); + const mime = importMap.imports.get(src)?.contentType; + const dataUri = await utilities.bufferToDataUri(buffer, mime); + $(link).attr("href", dataUri); + break; + } + + case "stylesheet": { + const linkSrc = await importMap.loadImport({ file: src }); + + $(link).replaceWith( + html``, + ); + break; + } + } + } +}; + +export const script: InlineFunction = async ($, baseUrl) => { + for (const script of $("script[src]")) { + log.info(`loading \`script\` ${script.attribs.src}`); + const src = utilities.isUrl(script.attribs.src, baseUrl); + if (!src) continue; + + const dirname = src.startsWith("http") + ? undefined + : path.posix.dirname(new URL(src).pathname); + + let scriptSrc = await importMap.loadImport({ dirname, file: src }); + + const parsed = await oxc.parse(src, scriptSrc); + + for (const imported of parsed.module.staticImports) { + const entries = imported.entries + .map((entry) => { + return `${entry.importName}${entry.localName ? " as " + entry.localName : ""}`; + }) + .join(","); + + scriptSrc = [ + scriptSrc.slice(0, imported.start), + js`const {${entries}}=await import(window["${importMap.WINDOW_KEY}"]["${imported.moduleRequest}"]);`, + scriptSrc.slice(imported.end), + ].join(""); + } + + for (const imported of parsed.module.dynamicImports) { + scriptSrc = [ + scriptSrc.slice(0, imported.start), + js`await import(window["${importMap.WINDOW_KEY}"]["${imported.moduleRequest}"]);`, + scriptSrc.slice(imported.end), + ].join(""); + } + + $(script).removeAttr("src"); + $(script).text(scriptSrc); + } +}; + +export const svgUse: InlineFunction = async ($, baseUrl) => { + for (const current of $("use[href]")) { + const [url, hash] = current.attribs.href.split("#"); + if (!hash) { + log.warn(`no hash found for use element ${current.attribs.href}`); + continue; + } + + log.info(`loading \`svg>use\` ${url}#${hash}`); + + const src = utilities.isUrl(url, baseUrl); + if (!src) { + log.error(`unable to load source for use element`); + continue; + } + + const svgMap = await importMap.loadImport({ file: src }); + const $$ = cheerio.load(svgMap, { xmlMode: true }); + + const symbol = $$(`symbol#${hash}`); + const viewBox = symbol.attr("viewBox") || "0 0 24 24"; + const inner = symbol.html(); + if (!inner) { + throw new Error("unable to parse parent"); + } + + $(current).parent().attr("viewBox", viewBox); + $(current).replaceWith(inner); + } +}; diff --git a/core/single-file/src/log.ts b/core/single-file/src/log.ts new file mode 100644 index 00000000..f3cd2731 --- /dev/null +++ b/core/single-file/src/log.ts @@ -0,0 +1,30 @@ +import * as obug from "obug"; + +export const DEBUG_BASE_NAMESPACE = "single-file" as const; +export const DEBUG_NAMESPACES = ["error", "info", "warn"] as const; +export type DEBUG_NAMESPACE = (typeof DEBUG_NAMESPACES)[number]; +export type DEBUG_SCOPE = `${typeof DEBUG_BASE_NAMESPACE}:${DEBUG_NAMESPACE}`; + +export const VERBOSE_SCOPE = "info" satisfies DEBUG_NAMESPACE; + +export const commonDebugOptions = { + log: console.info, + useColors: true, +} satisfies obug.DebugOptions; + +export const debug = obug.createDebug(DEBUG_BASE_NAMESPACE, commonDebugOptions); + +export const [error, info, warn] = DEBUG_NAMESPACES.map((namespace, index) => { + debug.color = index + 1; + return debug.extend(namespace); +}); + +export function enable(isVerbose: boolean) { + const enabledScopes = DEBUG_NAMESPACES.filter((scope) => { + return scope !== VERBOSE_SCOPE || isVerbose; + }) + .map((scope) => `${DEBUG_BASE_NAMESPACE}:${scope}`) + .join(","); + + obug.enable(enabledScopes); +} diff --git a/core/single-file/src/utilities.test.ts b/core/single-file/src/utilities.test.ts new file mode 100644 index 00000000..5ac7cc54 --- /dev/null +++ b/core/single-file/src/utilities.test.ts @@ -0,0 +1,146 @@ +import { describe, expect, it } from "vitest"; + +import { bufferToDataUri, escapeScript, isProbablyUrl, isUrl } from "./utilities"; + +describe("bufferToDataUri", () => { + it("defaults to image/png mime type", async () => { + const result = await bufferToDataUri(new Uint8Array([1, 2, 3]).buffer); + expect(result).toMatch(/^data:image\/png;base64,/); + }); + + it("defaults to image/png when mime is null", async () => { + const result = await bufferToDataUri(new Uint8Array([1, 2, 3]).buffer, null); + expect(result).toMatch(/^data:image\/png;base64,/); + }); + + it("uses the provided mime type", async () => { + const result = await bufferToDataUri( + new Uint8Array([1]).buffer, + "image/svg+xml", + ); + expect(result).toMatch(/^data:image\/svg\+xml;base64,/); + }); + + it("encodes buffer content as base64", async () => { + // "hello" in ASCII bytes — use Uint8Array to avoid Node Buffer shared pool + const bytes = new Uint8Array([104, 101, 108, 108, 111]); + const result = await bufferToDataUri(bytes.buffer); + expect(result).toBe("data:image/png;base64,aGVsbG8="); + }); + + it("handles empty buffer", async () => { + const result = await bufferToDataUri(new ArrayBuffer(0)); + expect(result).toBe("data:image/png;base64,"); + }); +}); + +describe("escapeScript", () => { + it("escapes backslashes", () => { + expect(escapeScript("a\\b")).toBe("a\\\\b"); + }); + + it("escapes backticks", () => { + expect(escapeScript("`foo`")).toBe("\\`foo\\`"); + }); + + it("escapes template literal expressions", () => { + expect(escapeScript("${x}")).toBe("\\${x}"); + }); + + it("escapes opening script tags", () => { + expect(escapeScript(" { + expect(escapeScript("")).toBe("<\\/script>"); + }); + + it("converts newlines to \\n literal", () => { + expect(escapeScript("a\nb")).toBe("a\\nb"); + }); + + it("removes carriage returns", () => { + expect(escapeScript("a\r\nb")).toBe("a\\nb"); + }); + + it("handles multiple replacements in one string", () => { + const input = "const x = `${y}`;\n"; + const result = escapeScript(input); + // backtick is escaped (preceded by backslash) + expect(result).toContain("\\`"); + expect(result).not.toMatch(/(?"); + expect(result).toContain("<\\/script>"); + }); +}); + +describe("isProbablyUrl", () => { + it("returns false for empty string", () => { + expect(isProbablyUrl("")).toBe(false); + }); + + it("returns false for string containing only a newline", () => { + expect(isProbablyUrl("\n")).toBe(false); + }); + + it("returns false for opening brace", () => { + expect(isProbablyUrl("{")).toBe(false); + }); + + it("returns true for https URL", () => { + expect(isProbablyUrl("https://example.com/script.js")).toBe(true); + }); + + it("returns true for http URL", () => { + expect(isProbablyUrl("http://example.com/img.png")).toBe(true); + }); + + it("returns true for root-relative path", () => { + expect(isProbablyUrl("/assets/style.css")).toBe(true); + }); + + it("returns true for ./ relative path", () => { + expect(isProbablyUrl("./foo.js")).toBe(true); + }); + + it("returns true for ../ parent-relative path", () => { + expect(isProbablyUrl("../foo.js")).toBe(true); + }); + + it("returns true for bare filename", () => { + expect(isProbablyUrl("foo.js")).toBe(true); + }); +}); + +describe("isUrl", () => { + const base = "https://example.com"; + + it("returns the resolved href for an absolute URL", () => { + expect(isUrl("https://cdn.example.com/style.css", base)).toBe( + "https://cdn.example.com/style.css", + ); + }); + + it("resolves a root-relative URL against the base", () => { + expect(isUrl("/style.css", base)).toBe("https://example.com/style.css"); + }); + + it("resolves a ./ relative URL against the base", () => { + expect(isUrl("./script.js", base)).toBe( + "https://example.com/script.js", + ); + }); + + it("returns false when isProbablyUrl fails", () => { + expect(isUrl("\n", base)).toBe(false); + }); + + it("returns false when the URL cannot be resolved against the base", () => { + expect(isUrl("foo.js", "not-a-valid-base")).toBe(false); + }); +}); diff --git a/core/single-file/src/utilities.ts b/core/single-file/src/utilities.ts new file mode 100644 index 00000000..8ab115b9 --- /dev/null +++ b/core/single-file/src/utilities.ts @@ -0,0 +1,42 @@ +export async function bufferToDataUri( + buffer: ArrayBuffer, + mime?: null | string, +) { + const base64 = Buffer.from(buffer).toString("base64"); + return `data:${mime || "image/png"};base64,${base64}`; +} + +export function escapeScript(script: string) { + return script + .replaceAll("\\", "\\\\") + .replaceAll("`", "\\`") + .replaceAll("${", "\\${") + .replaceAll("", "<\\/script>") + .replaceAll("\n", "\\n") + .replaceAll("\r", ""); +} + +const obviousQueries = ["\n", "{", "function"] as const; + +export function isProbablyUrl(str: string) { + if (!str || obviousQueries.some((query) => query.includes(str))) { + return false; + } + + // allow: + // - absolute URLs + // - root-relative (/foo.js) + // - relative (./foo.js, foo.js) + return /^(https?:\/\/|\/|\.\/|\.\.\/|[a-zA-Z0-9_\-./]+$)/.test(str); +} + +export function isUrl(url: string, base: string) { + if (!isProbablyUrl(url)) return false; + try { + return new URL(url, base).href; + } catch (error) { + console.error(`${url} is not a URL\n${error}`); + return false; + } +} diff --git a/core/single-file/tsconfig.json b/core/single-file/tsconfig.json new file mode 100644 index 00000000..0edb9472 --- /dev/null +++ b/core/single-file/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": ["../../tsconfig.base.json"], + "include": ["./src/*"] +} diff --git a/core/single-file/tsdown.config.ts b/core/single-file/tsdown.config.ts new file mode 100644 index 00000000..94d9ce68 --- /dev/null +++ b/core/single-file/tsdown.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from "tsdown"; + +export default defineConfig([ + { + attw: true, + dts: true, + entry: "src/index.ts", + exports: true, + format: ["esm", "cjs"], + publint: true, + target: "esnext", + }, + { + entry: "src/cli.ts", + format: ["esm", "cjs"], + target: "esnext", + }, +]); diff --git a/core/single-file/typedoc.json b/core/single-file/typedoc.json new file mode 100644 index 00000000..57eeff2c --- /dev/null +++ b/core/single-file/typedoc.json @@ -0,0 +1,12 @@ +{ + "entryPoints": ["src/*"], + "tsconfig": "./tsconfig.json", + "exclude": [ + "**/tests/**", + "**/*.test.ts", + "**/*.spec.ts", + "node_modules", + "**/{node_modules,test,book,doc,dist}/**/*", + "**/{pages,components}/**" + ] +} diff --git a/package.json b/package.json index cdaf884c..3ce2afa7 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "fmt:write": "prettier . --write", "knip": "knip --config ./.config/knip.ts", "lint": "turbo lint", - "prepare": "husky", + "prepare": "[ \"$CI\" = \"true\" ] || lefthook install", "publish": "changeset publish", "scripts:generate-examples": "node ./scripts/generate-examples.js", "scripts:lint-examples": "node ./scripts/lint-examples.js", @@ -68,7 +68,6 @@ "eslint-plugin-testing-library": "catalog:eslint", "eslint-plugin-zod": "catalog:eslint", "globals": "catalog:eslint", - "husky": "^9.1.7", "jsdom": "^27.4.0", "jsonc-eslint-parser": "catalog:eslint", "knip": "^5.83.1", @@ -91,6 +90,7 @@ }, "devDependencies": { "actions-up": "catalog:", + "lefthook": "catalog:", "node-plop": "catalog:", "taze": "catalog:" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index de5ac01c..b3912a92 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,9 @@ settings: catalogs: cli: + cleye: + specifier: ^2.6.0 + version: 2.6.0 cosmiconfig: specifier: 9.0.0 version: 9.0.0 @@ -62,6 +65,9 @@ catalogs: '@types/handlebars-helpers': specifier: 0.5.6 version: 0.5.6 + '@types/he': + specifier: ^1.2.3 + version: 1.2.3 '@types/mdast': specifier: 4.0.4 version: 4.0.4 @@ -83,6 +89,12 @@ catalogs: astro: specifier: 5.9.3 version: 5.9.3 + cheerio: + specifier: ^1.2.0 + version: 1.2.0 + dedent: + specifier: ^1.7.2 + version: 1.7.2 deepmerge: specifier: 4.3.1 version: 4.3.1 @@ -98,12 +110,21 @@ catalogs: happy-dom: specifier: 20.6.1 version: 20.6.1 + he: + specifier: ^1.2.0 + version: 1.2.0 json-schema-to-typescript: specifier: 15.0.4 version: 15.0.4 jsr: specifier: 0.13.5 version: 0.13.5 + ky: + specifier: ^2.0.1 + version: 2.0.1 + lefthook: + specifier: ^2.1.6 + version: 2.1.6 mdast: specifier: 3.0.0 version: 3.0.0 @@ -113,6 +134,9 @@ catalogs: node-plop: specifier: 0.32.3 version: 0.32.3 + oxc-parser: + specifier: ^0.127.0 + version: 0.127.0 remark: specifier: 15.0.1 version: 15.0.1 @@ -403,9 +427,6 @@ importers: globals: specifier: catalog:eslint version: 16.5.0 - husky: - specifier: ^9.1.7 - version: 9.1.7 jsdom: specifier: ^27.4.0 version: 27.4.0(@noble/hashes@1.8.0) @@ -467,6 +488,9 @@ importers: actions-up: specifier: 'catalog:' version: 1.11.0 + lefthook: + specifier: 'catalog:' + version: 2.1.6 node-plop: specifier: 'catalog:' version: 0.32.3(@types/node@24.10.13) @@ -829,6 +853,37 @@ importers: specifier: 'catalog:' version: 5.1.0 + core/single-file: + dependencies: + cheerio: + specifier: 'catalog:' + version: 1.2.0 + dedent: + specifier: 'catalog:' + version: 1.7.2 + he: + specifier: 'catalog:' + version: 1.2.0 + ky: + specifier: 'catalog:' + version: 2.0.1 + obug: + specifier: catalog:cli + version: 2.1.1 + oxc-parser: + specifier: 'catalog:' + version: 0.127.0 + devDependencies: + '@types/he': + specifier: 'catalog:' + version: 1.2.3 + cleye: + specifier: catalog:cli + version: 2.6.0 + tsdown: + specifier: 'catalog:' + version: 0.15.12(@arethetypeswrong/core@0.18.2)(oxc-resolver@11.17.1)(publint@0.3.18)(typescript@5.9.3)(vue-tsc@2.2.12(typescript@5.9.3)) + core/svelte-social-share-links: devDependencies: '@chromatic-com/storybook': @@ -1843,12 +1898,21 @@ packages: '@emnapi/core@1.7.1': resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + '@emnapi/runtime@1.7.1': resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} @@ -3179,6 +3243,12 @@ packages: '@napi-rs/wasm-runtime@1.1.1': resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@neoconfetti/react@1.0.0': resolution: {integrity: sha512-klcSooChXXOzIm+SE5IISIAn3bYzYfPjbX7D7HoqZL84oAfgREeSg5vSIaSFH+DaGzzvImTyWe1OyrJ67vik4A==} @@ -3269,6 +3339,136 @@ packages: '@oslojs/encoding@1.1.0': resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} + '@oxc-parser/binding-android-arm-eabi@0.127.0': + resolution: {integrity: sha512-0LC7ye4hvqbIKxAzThzvswgHLFu2AURKzYLeSVvLdu2TBOYWQDmHnTqPLeA597BcUCxiLqLsS4CJ5uoI5WYWCQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxc-parser/binding-android-arm64@0.127.0': + resolution: {integrity: sha512-b5jtVTH6AU5CJXHNdj7Jj9IEiR9yVjjnwHzPJhGyHGPdcsZSzBCkS9GBbV33niRMvKthDwQRFRJfI4a+k4PvYg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-darwin-arm64@0.127.0': + resolution: {integrity: sha512-obCE8B7ISKkJidjlhv9xRGJPOSDG2Yu6PRga9Ruaz35uintHxbp1Ki/Yc71wx4rj3Edrm0a1kzG1TAwit0wFpg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.127.0': + resolution: {integrity: sha512-JL6Xb5IwPQT8rUzlpsX7E+AgfcdNklXNPFp8pjCQQ5MQOQo5rtEB2ui+3Hgg9Sn7Y9Egj6YOLLiHhLpdAe12Aw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-freebsd-x64@0.127.0': + resolution: {integrity: sha512-SDQ/3MQFw58fqQz3Z1PhSKFF3JoCF4gmlNjziDm8X02tTahCw0qJbd7FGPDKw1i4VTBZene9JPyC3mHtSvi+wA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxc-parser/binding-linux-arm-gnueabihf@0.127.0': + resolution: {integrity: sha512-Av+D1MIqzV0YMGPT9we2SIZaMKD7Cxs4CvXSx/yxaWHewZjYEjScpOf5igc8IILASViw4WTnjlwUdI1KzVtDHQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.127.0': + resolution: {integrity: sha512-Cs2fdJ8cPpFdeebj6p4dag8A4+56hPvZ0AhQQzlaLswGz1tz7bXt1nETLeorrM9+AMcWFFkqxcXwDGfTVidY8g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm64-gnu@0.127.0': + resolution: {integrity: sha512-qdOfTcT6SY8gsJrrV92uyEUyjqMGPpIB5JZUG6QN5dukYd+7/j0kX6MwK1DgQj39jtUYixxPiaRUiEN1+0CXgQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-arm64-musl@0.127.0': + resolution: {integrity: sha512-EoTCZneNFU/P2qrpEM+RHmQwt+CvDkyGESG6qhr7KaegXLZwePfbrkCDfAk8/rhxbDUVGsZILX+2tqPzFtoFWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-ppc64-gnu@0.127.0': + resolution: {integrity: sha512-zALjmZYgxFLHjXeudcDF0xFGNydTAtkAeXAr2EuC17ywCyFxcmQra4w0BMde0Yi/re4Bi4iwEoEXtYN7l6eBLQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-gnu@0.127.0': + resolution: {integrity: sha512-fPP8M6zQLS7Jz7o9d5ArUSuAuSK3e+WCYVrCpdzeCOejidtZExJ9tjhDrAd3HEPqARBCPmdpqxESPFqy44vkBQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-musl@0.127.0': + resolution: {integrity: sha512-7IcC4Ao02oGpfnjt+X/oF4U2mllo2qoSkw5xxiXNKL9MCTsTiAC6616beOuehdxGcnz1bRoPC1RQ2f1GQDdN+g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-s390x-gnu@0.127.0': + resolution: {integrity: sha512-pbXIhiNFHoqWeqDNLiJ9JkpHz1IM9k4DXa66x+1GTWMG7iLxtkXgE53iiuKSXwmk3zIYmaPVfBvgcAhS583K4Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-gnu@0.127.0': + resolution: {integrity: sha512-MYCguB9RvBvlSd6gbuNI7QwiLoCCAlGnlRJFPrzLI6U1/9wkC/WK6LtBAUln55H1Ctqw45PWmqrobKoMhsYQzQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-musl@0.127.0': + resolution: {integrity: sha512-5eY0B/bxf1xIUxb4NOTvOI3KWtBQfPWYyKAzgcrCt0mDibSZygVpO1Pz8bkeiSZ5Jj9+M09dkggG3H8I5d0Uyg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-openharmony-arm64@0.127.0': + resolution: {integrity: sha512-Gld0ajrFTUXNtdw20fVBuTQx66FA75nIVg+//pPfR3sXkuABB4mTBhl3r9JNzrJpgW//qiwxf0nWXUWGJSL3UQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-parser/binding-wasm32-wasi@0.127.0': + resolution: {integrity: sha512-T6KVD7rhLzFlwGRXMnxUFfkCZD8FHnb968wVXW1mXzgRFc5RNXOBY2mPPDZ77x5Ln76ltLMgtPg0cOkU1NSrEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@oxc-parser/binding-win32-arm64-msvc@0.127.0': + resolution: {integrity: sha512-Ujvw4X+LD1CCGULcsQcvb4YNVoBGqt+JHgNNzGGaCImELiZLk477ifUH53gIbE7EKd933NdTi25JWEr9K2HwXw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-ia32-msvc@0.127.0': + resolution: {integrity: sha512-0cwxKO7KHQQQfo4Uf4B2SQrhgm+cJaP9OvFFhx52Tkg4bezsacu83GB2/In5bC415Ueeym+kXdnge/57rbSfTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.127.0': + resolution: {integrity: sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxc-project/types@0.127.0': + resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} + '@oxc-project/types@0.95.0': resolution: {integrity: sha512-vACy7vhpMPhjEJhULNxrdR0D943TkA/MigMpJCHmBHvMXxRStRi/dPtTlfQ3uDwWSzRpT8z+7ImjZVf8JWBocQ==} @@ -3786,27 +3986,18 @@ packages: '@shikijs/core@2.5.0': resolution: {integrity: sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==} - '@shikijs/core@3.19.0': - resolution: {integrity: sha512-L7SrRibU7ZoYi1/TrZsJOFAnnHyLTE1SwHG1yNWjZIVCqjOEmCSuK2ZO9thnRbJG6TOkPp+Z963JmpCNw5nzvA==} - '@shikijs/core@3.22.0': resolution: {integrity: sha512-iAlTtSDDbJiRpvgL5ugKEATDtHdUVkqgHDm/gbD2ZS9c88mx7G1zSYjjOxp5Qa0eaW0MAQosFRmJSk354PRoQA==} '@shikijs/engine-javascript@2.5.0': resolution: {integrity: sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==} - '@shikijs/engine-javascript@3.19.0': - resolution: {integrity: sha512-ZfWJNm2VMhKkQIKT9qXbs76RRcT0SF/CAvEz0+RkpUDAoDaCx0uFdCGzSRiD9gSlhm6AHkjdieOBJMaO2eC1rQ==} - '@shikijs/engine-javascript@3.22.0': resolution: {integrity: sha512-jdKhfgW9CRtj3Tor0L7+yPwdG3CgP7W+ZEqSsojrMzCjD1e0IxIbwUMDDpYlVBlC08TACg4puwFGkZfLS+56Tw==} '@shikijs/engine-oniguruma@2.5.0': resolution: {integrity: sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==} - '@shikijs/engine-oniguruma@3.19.0': - resolution: {integrity: sha512-1hRxtYIJfJSZeM5ivbUXv9hcJP3PWRo5prG/V2sWwiubUKTa+7P62d2qxCW8jiVFX4pgRHhnHNp+qeR7Xl+6kg==} - '@shikijs/engine-oniguruma@3.22.0': resolution: {integrity: sha512-DyXsOG0vGtNtl7ygvabHd7Mt5EY8gCNqR9Y7Lpbbd/PbJvgWrqaKzH1JW6H6qFkuUa8aCxoiYVv8/YfFljiQxA==} @@ -3816,9 +4007,6 @@ packages: '@shikijs/langs@2.5.0': resolution: {integrity: sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==} - '@shikijs/langs@3.19.0': - resolution: {integrity: sha512-dBMFzzg1QiXqCVQ5ONc0z2ebyoi5BKz+MtfByLm0o5/nbUu3Iz8uaTCa5uzGiscQKm7lVShfZHU1+OG3t5hgwg==} - '@shikijs/langs@3.22.0': resolution: {integrity: sha512-x/42TfhWmp6H00T6uwVrdTJGKgNdFbrEdhaDwSR5fd5zhQ1Q46bHq9EO61SCEWJR0HY7z2HNDMaBZp8JRmKiIA==} @@ -3828,9 +4016,6 @@ packages: '@shikijs/themes@2.5.0': resolution: {integrity: sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==} - '@shikijs/themes@3.19.0': - resolution: {integrity: sha512-H36qw+oh91Y0s6OlFfdSuQ0Ld+5CgB/VE6gNPK+Hk4VRbVG/XQgkjnt4KzfnnoO6tZPtKJKHPjwebOCfjd6F8A==} - '@shikijs/themes@3.22.0': resolution: {integrity: sha512-o+tlOKqsr6FE4+mYJG08tfCFDS+3CG20HbldXeVoyP+cYSUxDhrFf3GPjE60U55iOkkjbpY2uC3It/eeja35/g==} @@ -3843,9 +4028,6 @@ packages: '@shikijs/types@2.5.0': resolution: {integrity: sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==} - '@shikijs/types@3.19.0': - resolution: {integrity: sha512-Z2hdeEQlzuntf/BZpFG8a+Fsw9UVXdML7w0o3TgSXV3yNESGon+bs9ITkQb3Ki7zxoXOOu5oJWqZ2uto06V9iQ==} - '@shikijs/types@3.22.0': resolution: {integrity: sha512-491iAekgKDBFE67z70Ok5a8KBMsQ2IJwOWw3us/7ffQkIBCyOQfm/aNwVMBUriP02QshIfgHCBSIYAl3u2eWjg==} @@ -4393,6 +4575,9 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/he@1.2.3': + resolution: {integrity: sha512-q67/qwlxblDzEDvzHhVkwc1gzVWxaNxeyHUBF4xElrvjL11O+Ytze+1fGpBHlr/H9myiBUaUXNnNPmBHxxfAcA==} + '@types/html-minifier-terser@7.0.2': resolution: {integrity: sha512-mm2HqV22l8lFQh4r2oSsOEVea+m0qqxEmwpc9kC1p/XzmjLWrReR9D/GRs8Pex2NX/imyEH9c5IU/7tMBQCHOA==} @@ -5430,6 +5615,13 @@ packages: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} + cheerio-select@2.1.0: + resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + + cheerio@1.2.0: + resolution: {integrity: sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==} + engines: {node: '>=20.18.1'} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -5473,6 +5665,9 @@ packages: resolution: {integrity: sha512-TyUIUJgdFnCISzG5zu3291TAsE77ddchd0bepon1VVQrKLGKFED4iXFEDQ24mIPdPBbyE16PK3F8MYE1CmcBEQ==} engines: {node: '>=14.16'} + cleye@2.6.0: + resolution: {integrity: sha512-u0SQCsega/ox+2GSuUlG6wvA9c2FtH8sPmv9G9Q3JRTs7FK6+LtaziRAQgx7lrJ1J7bOd3palhwgZKMg8R6JbQ==} + cli-boxes@3.0.0: resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} engines: {node: '>=10'} @@ -5831,6 +6026,14 @@ packages: babel-plugin-macros: optional: true + dedent@1.7.2: + resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} @@ -5934,9 +6137,6 @@ packages: resolution: {integrity: sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ==} engines: {node: '>=18'} - devalue@5.5.0: - resolution: {integrity: sha512-69sM5yrHfFLJt0AZ9QqZXGCPfJ7fQjvpln3Rq5+PS03LD32Ost1Q9N+eEnaQwGRIriKkMImXD56ocjQmfjbV3w==} - devalue@5.6.2: resolution: {integrity: sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==} @@ -6060,6 +6260,9 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + encoding-sniffer@0.2.1: + resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==} + enhanced-resolve@5.20.0: resolution: {integrity: sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==} engines: {node: '>=10.13.0'} @@ -6775,9 +6978,6 @@ packages: resolution: {integrity: sha512-lh9HLdb53sC7XIZOYzTXM4lFuXElv3EVkSDhsd7DoJBj7hm+Ni7D3qYbb+Rr8DuM8nRanBvkVO9d7askreXGnQ==} deprecated: Removed event-stream from gulp-header - h3@1.15.4: - resolution: {integrity: sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==} - h3@1.15.5: resolution: {integrity: sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==} @@ -6951,6 +7151,9 @@ packages: html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + htmlparser2@10.1.0: + resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} + http-cache-semantics@4.2.0: resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} @@ -6982,15 +7185,14 @@ packages: resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} engines: {node: '>=18.18.0'} - husky@9.1.7: - resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} - engines: {node: '>=18'} - hasBin: true - iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + iconv-lite@0.7.0: resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} engines: {node: '>=0.10.0'} @@ -7563,6 +7765,10 @@ packages: kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + ky@2.0.1: + resolution: {integrity: sha512-HJPEjEpQPZQ5M3G5eu90/LWZDwysCnvqcfbLvq9FUvfizBZRi58WEixswyyI32LOLcFQd43w7kcfgkCPFxDt/Q==} + engines: {node: '>=22'} + language-subtag-registry@0.3.23: resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} @@ -7581,6 +7787,60 @@ packages: resolution: {integrity: sha512-7vp2Acd2+Kz4XkzxGxaB1FWOi8KjWIWsgdfD5MCb86DWvlLqhRPM+d6Pro3iNEL5VT9mstz5hKAlcd+QR6H3aA==} engines: {node: '>=0.10.0'} + lefthook-darwin-arm64@2.1.6: + resolution: {integrity: sha512-hyB7eeiX78BS66f70byTJacDLC/xV1vgMv9n+idFUsrM7J3Udd/ag9Ag5NP3t0eN0EqQqAtrNnt35EH01lxnRQ==} + cpu: [arm64] + os: [darwin] + + lefthook-darwin-x64@2.1.6: + resolution: {integrity: sha512-5Ka6cFxiH83krt+OMRQtmS6zqoZR5SLXSudLjTbZA1c3ZqF0+dqkeb4XcB6plx6WR0GFizabuc6Bi3iXPIe1eQ==} + cpu: [x64] + os: [darwin] + + lefthook-freebsd-arm64@2.1.6: + resolution: {integrity: sha512-VswyOg5CVN3rMaOJ2HtnkltiMKgFHW/wouWxXsV8RxSa4tgWOKxM0EmSXi8qc2jX+LRga6B0uOY6toXS01zWxA==} + cpu: [arm64] + os: [freebsd] + + lefthook-freebsd-x64@2.1.6: + resolution: {integrity: sha512-vXsCUFYuVwrVWwcypB7Zt2Hf+5pl1V1la7ZfvGYZaTRURu0zF/XUnMF/nOz/PebGv0f4x/iOWXWwP7E42xRWsg==} + cpu: [x64] + os: [freebsd] + + lefthook-linux-arm64@2.1.6: + resolution: {integrity: sha512-WDJiQhJdZOvKORZd+kF/ms2l6NSsXzdA9ahflyr65V90AC4jES223W8VtEMbGPUtHuGWMEZ/v/XvwlWv0Ioz9g==} + cpu: [arm64] + os: [linux] + + lefthook-linux-x64@2.1.6: + resolution: {integrity: sha512-C18nCd7nTX1AVL4TcvwMmLAO1VI1OuGluIOTjiPkBQ746Ls1HhL5rl//jMPACmT28YmxIQJ2ZcLPNmhvEVBZvw==} + cpu: [x64] + os: [linux] + + lefthook-openbsd-arm64@2.1.6: + resolution: {integrity: sha512-mZOMxM8HiPxVFXDO3PtCUbH4GB8rkveXhsgXF27oAZTYVzQ3gO9vT6r/pxit6msqRXz3fvcwimLVJgb8eRsa8A==} + cpu: [arm64] + os: [openbsd] + + lefthook-openbsd-x64@2.1.6: + resolution: {integrity: sha512-sG9ALLZSnnMOfXu+B7SmxFhJhuoAh4bqi5En5aaHJET48TqrLOcWWZuH+7ArFM6gr/U5KfSUvdmHFmY8WqCcIg==} + cpu: [x64] + os: [openbsd] + + lefthook-windows-arm64@2.1.6: + resolution: {integrity: sha512-lD8yFWY4Csuljd0Rqs7EQaySC0VvDf7V3rN1FhRMUISTRDHutebIom1Loc8ckQPvKYGC6mftT9k0GvipsS+Brw==} + cpu: [arm64] + os: [win32] + + lefthook-windows-x64@2.1.6: + resolution: {integrity: sha512-q4z2n3xucLscoWiyMwFViEj3N8MDSkPulMwcJYuCYFHoPhP1h+icqNu7QRLGYj6AnVrCQweiUJY3Tb2X+GbD/A==} + cpu: [x64] + os: [win32] + + lefthook@2.1.6: + resolution: {integrity: sha512-w9sBoR0mdN+kJc3SB85VzpiAAl451/rxdCRcZlwW71QLjkeH3EBQFgc4VMj5apePychYDHAlqEWTB8J8JK/j1Q==} + hasBin: true + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -8372,9 +8632,6 @@ packages: encoding: optional: true - node-mock-http@1.0.3: - resolution: {integrity: sha512-jN8dK25fsfnMrVsEhluUTPkBFY+6ybu7jSB1n+ri/vOGjJxU8J9CZhpSGkHXSkFjtUhbmoncG/YG9ta5Ludqog==} - node-mock-http@1.0.4: resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==} @@ -8552,6 +8809,10 @@ packages: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} + oxc-parser@0.127.0: + resolution: {integrity: sha512-bkgD4qHlN7WxLdX8bLXdaU54TtQtAIg/ZBAfm0aje/mo3MRDo3P0hZSgr4U7O3xfX+fQmR5AP04JS/TGcZLcFA==} + engines: {node: ^20.19.0 || >=22.12.0} + oxc-resolver@11.17.1: resolution: {integrity: sha512-pyRXK9kH81zKlirHufkFhOFBZRks8iAMLwPH8gU7lvKFiuzUH9L8MxDEllazwOb8fjXMcWjY1PMDfMJ2/yh5cw==} @@ -8638,9 +8899,6 @@ packages: package-manager-detector@0.2.11: resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} - package-manager-detector@1.5.0: - resolution: {integrity: sha512-uBj69dVlYe/+wxj8JOpr97XfsxH/eumMt6HqjNTmJDf/6NO9s+0uxeOneIz3AsPt2m6y9PqzDzd3ATcU17MNfw==} - package-manager-detector@1.6.0: resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} @@ -8676,6 +8934,12 @@ packages: resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} engines: {node: '>=18'} + parse5-htmlparser2-tree-adapter@7.1.0: + resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} + + parse5-parser-stream@7.1.2: + resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==} + parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} @@ -9568,9 +9832,6 @@ packages: shiki@2.5.0: resolution: {integrity: sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==} - shiki@3.19.0: - resolution: {integrity: sha512-77VJr3OR/VUZzPiStyRhADmO2jApMM0V2b1qf0RpfWya8Zr1PeZev5AEpPGAAKWdiYUtcZGBE4F5QvJml1PvWA==} - shiki@3.22.0: resolution: {integrity: sha512-LBnhsoYEe0Eou4e1VgJACes+O6S6QC0w71fCSp5Oya79inkwkm15gQ1UF6VtQ8j/taMDh79hAB49WUk8ALQW3g==} @@ -9971,6 +10232,9 @@ packages: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} + terminal-columns@2.0.0: + resolution: {integrity: sha512-6IByuUjyNZJXUtwDNm+OIe62zgwwaRbH+WMNTcx05O2G5V9WhvluAAHJY8OvUdwmzMPpqAD/7EUpGdI6ae1aiQ==} + terser@5.42.0: resolution: {integrity: sha512-UYCvU9YQW2f/Vwl+P0GfhxJxbUGLwd+5QrrGgLajzWAtC/23AX0vcise32kkP7Eu0Wu9VlzzHAXkLObgjQfFlQ==} engines: {node: '>=10'} @@ -10193,6 +10457,9 @@ packages: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} + type-flag@4.2.0: + resolution: {integrity: sha512-6h6QpSh5glA+BrMCq8FINo4o/BqSHNQdfIPpeSBt0s/6mynPaQWWZL+RHaYm95htTbid/spUbJer1yOCsA6ezQ==} + type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -10407,68 +10674,6 @@ packages: resolution: {integrity: sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==} engines: {node: '>=0.10.0'} - unstorage@1.17.3: - resolution: {integrity: sha512-i+JYyy0DoKmQ3FximTHbGadmIYb8JEpq7lxUjnjeB702bCPum0vzo6oy5Mfu0lpqISw7hCyMW2yj4nWC8bqJ3Q==} - peerDependencies: - '@azure/app-configuration': ^1.8.0 - '@azure/cosmos': ^4.2.0 - '@azure/data-tables': ^13.3.0 - '@azure/identity': ^4.6.0 - '@azure/keyvault-secrets': ^4.9.0 - '@azure/storage-blob': ^12.26.0 - '@capacitor/preferences': ^6.0.3 || ^7.0.0 - '@deno/kv': '>=0.9.0' - '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 - '@planetscale/database': ^1.19.0 - '@upstash/redis': ^1.34.3 - '@vercel/blob': '>=0.27.1' - '@vercel/functions': ^2.2.12 || ^3.0.0 - '@vercel/kv': ^1.0.1 - aws4fetch: ^1.0.20 - db0: '>=0.2.1' - idb-keyval: ^6.2.1 - ioredis: ^5.4.2 - uploadthing: ^7.4.4 - peerDependenciesMeta: - '@azure/app-configuration': - optional: true - '@azure/cosmos': - optional: true - '@azure/data-tables': - optional: true - '@azure/identity': - optional: true - '@azure/keyvault-secrets': - optional: true - '@azure/storage-blob': - optional: true - '@capacitor/preferences': - optional: true - '@deno/kv': - optional: true - '@netlify/blobs': - optional: true - '@planetscale/database': - optional: true - '@upstash/redis': - optional: true - '@vercel/blob': - optional: true - '@vercel/functions': - optional: true - '@vercel/kv': - optional: true - aws4fetch: - optional: true - db0: - optional: true - idb-keyval: - optional: true - ioredis: - optional: true - uploadthing: - optional: true - unstorage@1.17.4: resolution: {integrity: sha512-fHK0yNg38tBiJKp/Vgsq4j0JEsCmgqH58HAn707S7zGkArbZsVr/CwINoi+nh3h98BRCwKvx1K3Xg9u3VV83sw==} peerDependencies: @@ -10972,6 +11177,11 @@ packages: webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + whatwg-mimetype@3.0.0: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} engines: {node: '>=12'} @@ -11235,11 +11445,6 @@ packages: resolution: {integrity: sha512-RvEsa3W/NCqEBMtnoE09GRVelA3IqRcKaijEiM6CEGsD19qLurf0HjrYMHwOqImOszlLL0ja63DPJeeU4pm7oQ==} engines: {node: '>=20'} - zod-to-json-schema@3.25.0: - resolution: {integrity: sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==} - peerDependencies: - zod: ^3.25 || ^4 - zod-to-json-schema@3.25.1: resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} peerDependencies: @@ -11589,8 +11794,8 @@ snapshots: remark-parse: 11.0.0 remark-rehype: 11.1.2 remark-smartypants: 3.0.2 - shiki: 3.19.0 - smol-toml: 1.5.2 + shiki: 3.22.0 + smol-toml: 1.6.0 unified: 11.0.5 unist-util-remove-position: 5.0.0 unist-util-visit: 5.1.0 @@ -11615,7 +11820,7 @@ snapshots: remark-parse: 11.0.0 remark-rehype: 11.1.2 remark-smartypants: 3.0.2 - shiki: 3.19.0 + shiki: 3.22.0 smol-toml: 1.5.2 unified: 11.0.5 unist-util-remove-position: 5.0.0 @@ -11758,7 +11963,7 @@ snapshots: '@babel/helper-member-expression-to-functions@7.28.5': dependencies: '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color @@ -11780,7 +11985,7 @@ snapshots: '@babel/helper-optimise-call-expression@7.27.1': dependencies: - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 '@babel/helper-plugin-utils@7.27.1': {} @@ -12326,16 +12531,32 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/core@1.9.2': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.7.1': dependencies: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.9.2': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/wasi-threads@1.1.0': dependencies: tslib: 2.8.1 optional: true + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.21.5': optional: true @@ -13259,6 +13480,13 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@tybys/wasm-util': 0.10.1 + optional: true + '@neoconfetti/react@1.0.0': {} '@noble/ciphers@1.3.0': {} @@ -13357,6 +13585,72 @@ snapshots: '@oslojs/encoding@1.1.0': {} + '@oxc-parser/binding-android-arm-eabi@0.127.0': + optional: true + + '@oxc-parser/binding-android-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-darwin-x64@0.127.0': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-linux-ppc64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-openharmony-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-wasm32-wasi@0.127.0': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.127.0': + optional: true + + '@oxc-parser/binding-win32-ia32-msvc@0.127.0': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.127.0': + optional: true + + '@oxc-project/types@0.127.0': {} + '@oxc-project/types@0.95.0': {} '@oxc-resolver/binding-android-arm-eabi@11.17.1': @@ -13729,13 +14023,6 @@ snapshots: '@types/hast': 3.0.4 hast-util-to-html: 9.0.5 - '@shikijs/core@3.19.0': - dependencies: - '@shikijs/types': 3.19.0 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - hast-util-to-html: 9.0.5 - '@shikijs/core@3.22.0': dependencies: '@shikijs/types': 3.22.0 @@ -13749,12 +14036,6 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 oniguruma-to-es: 3.1.1 - '@shikijs/engine-javascript@3.19.0': - dependencies: - '@shikijs/types': 3.19.0 - '@shikijs/vscode-textmate': 10.0.2 - oniguruma-to-es: 4.3.4 - '@shikijs/engine-javascript@3.22.0': dependencies: '@shikijs/types': 3.22.0 @@ -13766,11 +14047,6 @@ snapshots: '@shikijs/types': 2.5.0 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/engine-oniguruma@3.19.0': - dependencies: - '@shikijs/types': 3.19.0 - '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/engine-oniguruma@3.22.0': dependencies: '@shikijs/types': 3.22.0 @@ -13785,10 +14061,6 @@ snapshots: dependencies: '@shikijs/types': 2.5.0 - '@shikijs/langs@3.19.0': - dependencies: - '@shikijs/types': 3.19.0 - '@shikijs/langs@3.22.0': dependencies: '@shikijs/types': 3.22.0 @@ -13801,10 +14073,6 @@ snapshots: dependencies: '@shikijs/types': 2.5.0 - '@shikijs/themes@3.19.0': - dependencies: - '@shikijs/types': 3.19.0 - '@shikijs/themes@3.22.0': dependencies: '@shikijs/types': 3.22.0 @@ -13823,11 +14091,6 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 - '@shikijs/types@3.19.0': - dependencies: - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - '@shikijs/types@3.22.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 @@ -14375,6 +14638,8 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/he@1.2.3': {} + '@types/html-minifier-terser@7.0.2': {} '@types/http-cache-semantics@4.0.4': {} @@ -15273,7 +15538,7 @@ snapshots: ast-kit@2.1.3: dependencies: - '@babel/parser': 7.28.5 + '@babel/parser': 7.29.2 pathe: 2.0.3 ast-types-flow@0.0.8: {} @@ -15418,7 +15683,7 @@ snapshots: '@capsizecss/unpack': 2.4.0 '@oslojs/encoding': 1.1.0 '@rollup/pluginutils': 5.3.0(rollup@4.52.5) - acorn: 8.15.0 + acorn: 8.16.0 aria-query: 5.3.2 axobject-query: 4.1.0 boxen: 8.0.1 @@ -15429,7 +15694,7 @@ snapshots: cssesc: 3.0.0 debug: 4.4.3 deterministic-object-hash: 2.0.2 - devalue: 5.5.0 + devalue: 5.6.2 diff: 5.2.0 dlv: 1.1.3 dset: 3.1.4 @@ -15450,19 +15715,19 @@ snapshots: neotraverse: 0.6.18 p-limit: 6.2.0 p-queue: 8.1.1 - package-manager-detector: 1.5.0 + package-manager-detector: 1.6.0 picomatch: 4.0.3 prompts: 2.4.2 rehype: 13.0.2 - semver: 7.7.3 - shiki: 3.19.0 + semver: 7.7.4 + shiki: 3.22.0 tinyexec: 0.3.2 tinyglobby: 0.2.15 tsconfck: 3.1.6(typescript@5.9.3) ultrahtml: 1.6.0 unifont: 0.5.0 - unist-util-visit: 5.0.0 - unstorage: 1.17.3 + unist-util-visit: 5.1.0 + unstorage: 1.17.4 vfile: 6.0.3 vite: 6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.42.0)(tsx@4.21.0)(yaml@2.8.2) vitefu: 1.1.1(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.42.0)(tsx@4.21.0)(yaml@2.8.2)) @@ -15470,7 +15735,7 @@ snapshots: yargs-parser: 21.1.1 yocto-spinner: 0.2.3 zod: 3.25.76 - zod-to-json-schema: 3.25.0(zod@3.25.76) + zod-to-json-schema: 3.25.1(zod@3.25.76) zod-to-ts: 1.2.0(typescript@5.9.3)(zod@3.25.76) optionalDependencies: sharp: 0.33.5 @@ -15816,6 +16081,29 @@ snapshots: check-error@2.1.1: {} + cheerio-select@2.1.0: + dependencies: + boolbase: 1.0.0 + css-select: 5.2.2 + css-what: 6.2.2 + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + + cheerio@1.2.0: + dependencies: + cheerio-select: 2.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.2.2 + encoding-sniffer: 0.2.1 + htmlparser2: 10.1.0 + parse5: 7.3.0 + parse5-htmlparser2-tree-adapter: 7.1.0 + parse5-parser-stream: 7.1.2 + undici: 7.22.0 + whatwg-mimetype: 4.0.0 + chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -15848,6 +16136,11 @@ snapshots: dependencies: escape-string-regexp: 5.0.0 + cleye@2.6.0: + dependencies: + terminal-columns: 2.0.0 + type-flag: 4.2.0 + cli-boxes@3.0.0: {} cli-cursor@3.1.0: @@ -16193,6 +16486,8 @@ snapshots: dedent@1.7.1: {} + dedent@1.7.2: {} + deep-eql@5.0.2: {} deep-extend@0.6.0: {} @@ -16279,8 +16574,6 @@ snapshots: dependencies: base-64: 1.0.0 - devalue@5.5.0: {} - devalue@5.6.2: {} devlop@1.1.0: @@ -16388,6 +16681,11 @@ snapshots: encodeurl@2.0.0: {} + encoding-sniffer@0.2.1: + dependencies: + iconv-lite: 0.6.3 + whatwg-encoding: 3.1.1 + enhanced-resolve@5.20.0: dependencies: graceful-fs: 4.2.11 @@ -17435,18 +17733,6 @@ snapshots: lodash.template: 4.5.0 through2: 2.0.5 - h3@1.15.4: - dependencies: - cookie-es: 1.2.2 - crossws: 0.3.5 - defu: 6.1.4 - destr: 2.0.5 - iron-webcrypto: 1.2.1 - node-mock-http: 1.0.3 - radix3: 1.1.2 - ufo: 1.6.1 - uncrypto: 0.1.3 - h3@1.15.5: dependencies: cookie-es: 1.2.2 @@ -17696,8 +17982,7 @@ snapshots: property-information: 7.1.0 space-separated-tokens: 2.0.2 - he@1.2.0: - optional: true + he@1.2.0: {} helper-date@1.0.1: dependencies: @@ -17767,6 +18052,13 @@ snapshots: html-void-elements@3.0.0: {} + htmlparser2@10.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 7.0.1 + http-cache-semantics@4.2.0: {} http-errors@2.0.0: @@ -17802,12 +18094,14 @@ snapshots: human-signals@8.0.1: {} - husky@9.1.7: {} - iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + iconv-lite@0.7.0: dependencies: safer-buffer: 2.1.2 @@ -18324,6 +18618,8 @@ snapshots: kolorist@1.8.0: {} + ky@2.0.1: {} + language-subtag-registry@0.3.23: {} language-tags@1.0.9: @@ -18343,6 +18639,49 @@ snapshots: dependencies: set-getter: 0.1.1 + lefthook-darwin-arm64@2.1.6: + optional: true + + lefthook-darwin-x64@2.1.6: + optional: true + + lefthook-freebsd-arm64@2.1.6: + optional: true + + lefthook-freebsd-x64@2.1.6: + optional: true + + lefthook-linux-arm64@2.1.6: + optional: true + + lefthook-linux-x64@2.1.6: + optional: true + + lefthook-openbsd-arm64@2.1.6: + optional: true + + lefthook-openbsd-x64@2.1.6: + optional: true + + lefthook-windows-arm64@2.1.6: + optional: true + + lefthook-windows-x64@2.1.6: + optional: true + + lefthook@2.1.6: + optionalDependencies: + lefthook-darwin-arm64: 2.1.6 + lefthook-darwin-x64: 2.1.6 + lefthook-freebsd-arm64: 2.1.6 + lefthook-freebsd-x64: 2.1.6 + lefthook-linux-arm64: 2.1.6 + lefthook-linux-x64: 2.1.6 + lefthook-openbsd-arm64: 2.1.6 + lefthook-openbsd-x64: 2.1.6 + lefthook-windows-arm64: 2.1.6 + lefthook-windows-x64: 2.1.6 + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -18628,8 +18967,8 @@ snapshots: magicast@0.3.5: dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 source-map-js: 1.2.1 magicast@0.5.1: @@ -19277,7 +19616,7 @@ snapshots: esbuild: 0.25.12 find-up: 7.0.0 html-minifier-terser: 7.2.0 - lightningcss: 1.30.2 + lightningcss: 1.31.1 montag: 1.2.1 readjson: 2.2.2 simport: 1.2.0 @@ -19420,8 +19759,6 @@ snapshots: dependencies: whatwg-url: 5.0.0 - node-mock-http@1.0.3: {} - node-mock-http@1.0.4: {} node-modules-inspector@1.4.2: @@ -19701,6 +20038,31 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 + oxc-parser@0.127.0: + dependencies: + '@oxc-project/types': 0.127.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.127.0 + '@oxc-parser/binding-android-arm64': 0.127.0 + '@oxc-parser/binding-darwin-arm64': 0.127.0 + '@oxc-parser/binding-darwin-x64': 0.127.0 + '@oxc-parser/binding-freebsd-x64': 0.127.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.127.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.127.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.127.0 + '@oxc-parser/binding-linux-arm64-musl': 0.127.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.127.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.127.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.127.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.127.0 + '@oxc-parser/binding-linux-x64-gnu': 0.127.0 + '@oxc-parser/binding-linux-x64-musl': 0.127.0 + '@oxc-parser/binding-openharmony-arm64': 0.127.0 + '@oxc-parser/binding-wasm32-wasi': 0.127.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.127.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.127.0 + '@oxc-parser/binding-win32-x64-msvc': 0.127.0 + oxc-resolver@11.17.1: optionalDependencies: '@oxc-resolver/binding-android-arm-eabi': 11.17.1 @@ -19798,8 +20160,6 @@ snapshots: dependencies: quansync: 0.2.10 - package-manager-detector@1.5.0: {} - package-manager-detector@1.6.0: {} pako@0.2.9: {} @@ -19851,6 +20211,15 @@ snapshots: parse-ms@4.0.0: {} + parse5-htmlparser2-tree-adapter@7.1.0: + dependencies: + domhandler: 5.0.3 + parse5: 7.3.0 + + parse5-parser-stream@7.1.2: + dependencies: + parse5: 7.3.0 + parse5@7.3.0: dependencies: entities: 6.0.1 @@ -20583,8 +20952,8 @@ snapshots: rolldown-plugin-dts@0.17.3(oxc-resolver@11.17.1)(rolldown@1.0.0-beta.45)(typescript@5.9.3)(vue-tsc@2.2.12(typescript@5.9.3)): dependencies: '@babel/generator': 7.28.5 - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 ast-kit: 2.1.3 birpc: 2.6.1 debug: 4.4.3 @@ -20879,17 +21248,6 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 - shiki@3.19.0: - dependencies: - '@shikijs/core': 3.19.0 - '@shikijs/engine-javascript': 3.19.0 - '@shikijs/engine-oniguruma': 3.19.0 - '@shikijs/langs': 3.19.0 - '@shikijs/themes': 3.19.0 - '@shikijs/types': 3.19.0 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - shiki@3.22.0: dependencies: '@shikijs/core': 3.22.0 @@ -21348,10 +21706,12 @@ snapshots: term-size@2.2.1: {} + terminal-columns@2.0.0: {} + terser@5.42.0: dependencies: '@jridgewell/source-map': 0.3.6 - acorn: 8.15.0 + acorn: 8.16.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -21532,6 +21892,8 @@ snapshots: type-fest@4.41.0: {} + type-flag@4.2.0: {} + type-is@1.6.18: dependencies: media-typer: 0.3.0 @@ -21804,17 +22166,6 @@ snapshots: has-value: 0.3.1 isobject: 3.0.1 - unstorage@1.17.3: - dependencies: - anymatch: 3.1.3 - chokidar: 4.0.3 - destr: 2.0.5 - h3: 1.15.4 - lru-cache: 10.4.3 - node-fetch-native: 1.6.7 - ofetch: 1.5.1 - ufo: 1.6.1 - unstorage@1.17.4: dependencies: anymatch: 3.1.3 @@ -21965,7 +22316,7 @@ snapshots: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 - postcss: 8.5.6 + postcss: 8.5.9 rollup: 4.52.5 tinyglobby: 0.2.15 optionalDependencies: @@ -21982,7 +22333,7 @@ snapshots: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 - postcss: 8.5.6 + postcss: 8.5.9 rollup: 4.52.5 tinyglobby: 0.2.15 optionalDependencies: @@ -22283,6 +22634,10 @@ snapshots: webpack-virtual-modules@0.6.2: {} + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + whatwg-mimetype@3.0.0: {} whatwg-mimetype@4.0.0: {} @@ -22551,10 +22906,6 @@ snapshots: dependencies: zod: 3.25.76 - zod-to-json-schema@3.25.0(zod@3.25.76): - dependencies: - zod: 3.25.76 - zod-to-json-schema@3.25.1(zod@3.25.76): dependencies: zod: 3.25.76 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9dfd498d..24367d92 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -9,17 +9,10 @@ autoInstallPeers: true # Shared version expressions (yaml anchors). # This is a yaml feature and is not interpreted by pnpm directly. __versions: - - &codecov 1.9.1 - &commitlint 19.8.1 - - &eslint 9.39.4 - &manypkg 3.1.0 - - &react 19.2.1 - - &react_router 7.10.1 - &storybook 10.2.8 - - &tailwindcss 4.2.2 - &turbo 2.8.20 - - &typescript_eslint 8.57.0 - - &vite 7.3.1 - &vitest 4.0.18 catalog: @@ -35,6 +28,7 @@ catalog: '@testing-library/react': 16.3.2 '@types/debug': 4.1.12 '@types/handlebars-helpers': 0.5.6 + '@types/he': ^1.2.3 '@types/mdast': 4.0.4 '@types/minify': 9.1.4 '@types/react': 19.2.14 @@ -42,16 +36,22 @@ catalog: '@types/yargs': 17.0.35 actions-up: 1.11.0 astro: 5.9.3 + cheerio: ^1.2.0 + dedent: ^1.7.2 deepmerge: 4.3.1 es-toolkit: 1.43.0 - handlebars: 4.7.8 handlebars-helpers: 0.10.0 + handlebars: 4.7.8 happy-dom: 20.6.1 + he: ^1.2.0 json-schema-to-typescript: 15.0.4 jsr: 0.13.5 + ky: ^2.0.1 + lefthook: ^2.1.6 mdast: 3.0.0 minify: 14.0.0 node-plop: 0.32.3 + oxc-parser: ^0.127.0 react: 19.2.0 remark: 15.0.1 tailwind-scrollbar: 4.0.2 @@ -63,14 +63,15 @@ catalog: undici: 7.22.0 unified: 11.0.5 unist-util-visit: 5.1.0 - vite: 6.3.5 vite-tsconfig-paths: 5.1.4 + vite: 6.3.5 vitepress-plugin-llms: 1.11.0 catalogMode: strict catalogs: cli: + cleye: ^2.6.0 cosmiconfig: 9.0.0 obug: 2.1.1 yargs: 18.0.0 diff --git a/tsconfig.base.json b/tsconfig.base.json index a49c55ae..c9c1bb22 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1,22 +1,21 @@ { - "extends": "@tsconfig/recommended/tsconfig.json", - "exclude": ["node_modules", "dist"], - "include": ["./**/*"], - "compilerOptions": { - "allowImportingTsExtensions": true, - "allowJs": true, - "baseUrl": ".", - "isolatedModules": true, - "lib": ["ESNext"], - "module": "ESNext", - "moduleResolution": "Bundler", - "noEmit": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "strict": true, - "strictNullChecks": true, - "target": "esnext", - "useDefineForClassFields": true, - "verbatimModuleSyntax": true - } + "extends": "@tsconfig/recommended/tsconfig.json", + "exclude": ["node_modules", "dist"], + "include": ["./**/*"], + "compilerOptions": { + "allowImportingTsExtensions": true, + "allowJs": true, + "isolatedModules": true, + "lib": ["ESNext"], + "module": "ESNext", + "moduleResolution": "Bundler", + "noEmit": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "strictNullChecks": true, + "target": "esnext", + "useDefineForClassFields": true, + "verbatimModuleSyntax": true + } }