diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..7122234
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,37 @@
+name: CI
+
+on:
+ pull_request:
+ push:
+ branches:
+ - develop
+ - main
+
+permissions:
+ contents: read
+
+jobs:
+ verify:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 10.30.3
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: pnpm
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Verify
+ run: pnpm run verify
+
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..6feefea
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,58 @@
+name: Release
+
+on:
+ push:
+ branches:
+ - main
+
+permissions:
+ contents: write
+ pull-requests: write
+ id-token: write
+
+jobs:
+ release:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Release Please
+ id: release
+ uses: googleapis/release-please-action@v4
+ with:
+ release-type: node
+
+ - name: Checkout
+ if: ${{ steps.release.outputs.release_created }}
+ uses: actions/checkout@v4
+
+ - name: Setup pnpm
+ if: ${{ steps.release.outputs.release_created }}
+ uses: pnpm/action-setup@v4
+ with:
+ version: 10.30.3
+
+ - name: Setup Node.js
+ if: ${{ steps.release.outputs.release_created }}
+ uses: actions/setup-node@v4
+ with:
+ node-version: 24
+ cache: pnpm
+ registry-url: https://registry.npmjs.org/
+
+ - name: Update npm
+ if: ${{ steps.release.outputs.release_created }}
+ run: npm install --global npm@latest
+
+ - name: Install dependencies
+ if: ${{ steps.release.outputs.release_created }}
+ run: pnpm install --frozen-lockfile
+
+ - name: Verify
+ if: ${{ steps.release.outputs.release_created }}
+ run: pnpm run verify
+
+ - name: Publish to npm
+ if: ${{ steps.release.outputs.release_created }}
+ run: npm publish --provenance --access public
+ env:
+ HUSKY: 0
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ab59570
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,16 @@
+.DS_Store
+.idea/
+.claude/
+
+node_modules/
+dist/
+coverage/
+.rslib/
+.rstest/
+.rsbuild/
+
+*.log
+npm-debug.log*
+pnpm-debug.log*
+yarn-debug.log*
+yarn-error.log*
diff --git a/.husky/commit-msg b/.husky/commit-msg
new file mode 100755
index 0000000..292e9e3
--- /dev/null
+++ b/.husky/commit-msg
@@ -0,0 +1,2 @@
+pnpm exec commitlint --edit "$1"
+
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100755
index 0000000..8e4e104
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1,2 @@
+pnpm exec lint-staged
+
diff --git a/.husky/pre-push b/.husky/pre-push
new file mode 100755
index 0000000..2d3d363
--- /dev/null
+++ b/.husky/pre-push
@@ -0,0 +1,2 @@
+pnpm run test && pnpm run build
+
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..7c8e2dc
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1,3 @@
+engine-strict=false
+fund=false
+
diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 0000000..42126c0
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1,2 @@
+22
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..388e394
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,15 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+This project uses Conventional Commits and Release Please to generate releases.
+
+## 0.0.1 - 2026-05-02
+
+### Added
+
+- Initial npm package scaffold.
+- TypeScript source entrypoint.
+- Rslib dual ESM/CommonJS build with TypeScript declarations.
+- Rstest, Biome, publint, commitlint, and Husky development workflow.
+- GitHub Actions workflows for CI, Release Please, and npm Trusted Publishing.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..0fcf605
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,166 @@
+# Contributing
+
+Thanks for taking the time to improve `srcset-kit`.
+
+This package is intentionally small: it parses, validates, and serializes HTML
+`srcset` values without runtime dependencies. Contributions are most helpful
+when they keep that focus clear.
+
+## Getting Started
+
+Requirements:
+
+- Node.js 22 or newer;
+- pnpm 10 or newer.
+
+Install dependencies:
+
+```sh
+pnpm install
+```
+
+Run the full local check:
+
+```sh
+pnpm run verify
+```
+
+`verify` is the same kind of check expected before a release. It runs formatting
+checks, linting, type checking, tests, the production build, and package export
+validation.
+
+## Useful Commands
+
+Use the full check before opening a pull request:
+
+```sh
+pnpm run verify
+```
+
+Use focused commands while developing:
+
+```sh
+pnpm run test
+pnpm run test:watch
+pnpm run typecheck
+pnpm run lint
+pnpm run format
+pnpm run format:check
+pnpm run build
+pnpm run pack:check
+```
+
+## Project Shape
+
+The main files are:
+
+- `src/parse.ts` for the public parser entrypoint;
+- `src/parser.ts` for internal tokenization;
+- `src/validator.ts` for validation rules and issue codes;
+- `src/stringify.ts` for serialization;
+- `src/types.ts` for public types;
+- `src/errors.ts` for package-specific errors;
+- `tests/index.test.ts` for behavior coverage;
+- `README.md` for public examples and user-facing API docs.
+
+## Public API Changes
+
+Keep the public surface small and intentional. The runtime API is:
+
+- `parse`;
+- `validate`;
+- `stringify`.
+
+When changing public behavior, update these together:
+
+- exported types in `src/types.ts`;
+- exports in `src/index.ts`;
+- user-facing examples in `README.md`;
+- tests in `tests/index.test.ts`.
+
+Please avoid adding dependencies unless the benefit is clearly worth the extra
+package weight.
+
+## Testing Guidelines
+
+Add focused tests for the behavior you change.
+
+Parser changes should cover cases like:
+
+- commas inside URLs;
+- `data:` URLs;
+- relative and absolute URLs;
+- query strings and fragments;
+- whitespace and newlines;
+- tolerant parsing of invalid descriptor sets.
+
+Validator changes should assert stable issue codes, not only messages.
+
+Stringifier changes should cover normalized output and round trips with
+`parse()` when possible.
+
+If object validation changes, add candidate-array tests in addition to string
+input tests.
+
+## Documentation Guidelines
+
+Keep README examples short, copyable, and aligned with the actual API.
+
+If a feature affects how users call `parse`, `validate`, or `stringify`, update
+the README in the same pull request. Use the repository's TypeScript formatting
+style in examples, including compact object and import braces:
+
+```ts
+import {parse, validate} from "srcset-kit";
+
+validate([{url: "image.png", density: 1}]);
+```
+
+## Commits
+
+This repository uses Conventional Commits. Examples:
+
+```text
+feat: add srcset parser
+fix: keep descriptor whitespace valid
+docs: improve validation examples
+chore(release): merge main back into develop
+```
+
+Husky runs `commitlint` for commit messages.
+
+## Branch Flow
+
+The repository follows a GitFlow-like process without requiring the GitFlow CLI:
+
+- feature and fix pull requests target `develop`;
+- release pull requests merge `develop` into `main`;
+- Release Please creates a release pull request in `main`;
+- after the release pull request is merged, GitHub Release and npm publishing run automatically;
+- merge `main` back into `develop` after every release so `develop` receives the version, changelog, and lockfile updates.
+
+When a merge commit is created manually, keep the merge message conventional:
+
+```text
+chore(release): merge main back into develop
+feat: merge feature/parser
+```
+
+## Hooks
+
+Husky hooks are installed by `pnpm install`.
+
+- `pre-commit` formats staged files with Biome through lint-staged and keeps the formatted result in the current commit.
+- `commit-msg` validates Conventional Commits.
+- `pre-push` runs tests and the build.
+
+## Release Readiness
+
+Before a release, make sure:
+
+- `pnpm run verify` passes;
+- README examples match the exported API;
+- public type changes are covered by tests;
+- `CHANGELOG.md` is ready for the release flow;
+- package metadata in `package.json` still matches the published package;
+- the license file remains `LICENSE.md`.
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..a84adc2
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,9 @@
+# MIT License
+
+Copyright (c): Anjey Tsibylskij
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a3f4e73
--- /dev/null
+++ b/README.md
@@ -0,0 +1,343 @@
+# srcset-kit
+
+[](https://www.npmjs.com/package/srcset-kit)
+[](https://www.npmjs.com/package/srcset-kit)
+[](https://github.com/atldays/srcset-kit/actions/workflows/ci.yml)
+[](LICENSE.md)
+
+Small, dependency-free tools for parsing, validating, and serializing HTML
+`srcset` values.
+
+`srcset-kit` is built for code that needs to understand responsive image
+candidates without taking shortcuts. It handles `data:` URLs with commas,
+normalizes whitespace, reports stable validation codes, and keeps URL strings
+exactly as you provided them.
+
+## Why
+
+`srcset` looks simple until URLs contain commas:
+
+```html
+
+```
+
+A plain `split(",")` breaks this value. `srcset-kit` tokenizes candidates with
+the descriptor boundary in mind, so inline image data, query strings, fragments,
+ports, relative URLs, and absolute URLs stay intact.
+
+## Installation
+
+```sh
+pnpm add srcset-kit
+```
+
+```sh
+npm install srcset-kit
+```
+
+```sh
+yarn add srcset-kit
+```
+
+## Quick Start
+
+```ts
+import {parse, stringify, validate} from "srcset-kit";
+
+const candidates = parse("image.png 1x, image@2x.png 2x");
+// [
+// {url: "image.png", density: 1},
+// {url: "image@2x.png", density: 2},
+// ]
+
+const result = validate("image.png 1x, image@2x.png 2x");
+// result.valid === true
+
+const srcset = stringify(candidates);
+// "image.png 1x, image@2x.png 2x"
+```
+
+## Parse
+
+Parse a `srcset` string into clean candidate objects.
+
+```ts
+import {parse} from "srcset-kit";
+
+parse("image.png");
+// [{url: "image.png"}]
+
+parse("image.png 1x, image@2x.png 2x");
+// [
+// {url: "image.png", density: 1},
+// {url: "image@2x.png", density: 2},
+// ]
+
+parse("small.png 640w, large.png 1280w");
+// [
+// {url: "small.png", width: 640},
+// {url: "large.png", width: 1280},
+// ]
+```
+
+Parsing is tolerant by default. It keeps the usable URL candidate shape and
+leaves validation decisions to `validate()`.
+
+```ts
+parse("image.png 1x 640w, image@2x.png 2x");
+// [
+// {url: "image.png"},
+// {url: "image@2x.png", density: 2},
+// ]
+```
+
+Use strict parsing when invalid `srcset` values should throw.
+
+```ts
+import {SrcsetValidationError, parse} from "srcset-kit";
+
+try {
+ parse("image.png 1x 640w", {strict: true});
+} catch (error) {
+ error instanceof SrcsetValidationError;
+ // true
+}
+```
+
+## Data URLs
+
+Commas inside URLs are preserved.
+
+```ts
+parse("data:image/png;base64,AAAA 1x, /image@2x.png 2x");
+// [
+// {url: "data:image/png;base64,AAAA", density: 1},
+// {url: "/image@2x.png", density: 2},
+// ]
+
+parse("data:image/svg+xml,%3Csvg%3E%3C/svg%3E 1x, /image.svg 2x");
+// [
+// {url: "data:image/svg+xml,%3Csvg%3E%3C/svg%3E", density: 1},
+// {url: "/image.svg", density: 2},
+// ]
+```
+
+## Validate
+
+Validate a string or already parsed candidates. Validation never throws for
+normal invalid input.
+
+```ts
+import {validate} from "srcset-kit";
+
+validate("image.png 1x, image@2x.png 2x");
+// {
+// valid: true,
+// descriptor: "density",
+// candidates: [
+// {url: "image.png", density: 1},
+// {url: "image@2x.png", density: 2},
+// ],
+// errors: [],
+// }
+```
+
+Invalid input returns stable issue codes.
+
+```ts
+validate("image.png 1x, image@1x.png 1x");
+// {
+// valid: false,
+// descriptor: "density",
+// candidates: [
+// {url: "image.png", density: 1},
+// {url: "image@1x.png", density: 1},
+// ],
+// errors: [
+// {
+// code: "duplicate-descriptor",
+// message: "...",
+// candidate: "image@1x.png 1x",
+// index: 1,
+// },
+// ],
+// }
+```
+
+Validate candidate arrays directly.
+
+```ts
+validate([
+ {url: "image.png", density: 1},
+ {url: "image@2x.png", density: 2},
+]);
+// valid: true
+```
+
+### Descriptor Policy
+
+Use `descriptor` when a caller expects a specific candidate style.
+
+```ts
+validate("small.png 640w, large.png 1280w", {
+ descriptor: "width",
+});
+// valid: true
+
+validate("image.png 1x, image@2x.png 2x", {
+ descriptor: "width",
+});
+// valid: false
+
+validate("image.png, image@2x.png 2x", {
+ descriptor: "density",
+});
+// valid: true
+```
+
+A fallback candidate without a descriptor is treated as implicit `1x`, so it is
+accepted by the density policy.
+
+### URL Context
+
+Use `baseUrl` when relative URLs should be checked in the context of a page URL.
+Candidate URLs are not rewritten.
+
+```ts
+validate("/images/photo.jpg 1x", {
+ baseUrl: "https://example.com/gallery/",
+});
+// valid: true
+
+parse("/images/photo.jpg 1x");
+// [{url: "/images/photo.jpg", density: 1}]
+```
+
+## Stringify
+
+Serialize candidates into a normalized `srcset` string.
+
+```ts
+import {stringify} from "srcset-kit";
+
+stringify([{url: "image.png"}]);
+// "image.png"
+
+stringify([{url: "image.png", density: 1.5}]);
+// "image.png 1.5x"
+
+stringify([{url: "image.png", width: 640}]);
+// "image.png 640w"
+
+stringify([
+ {url: "image.png", density: 1},
+ {url: "image@2x.png", density: 2},
+]);
+// "image.png 1x, image@2x.png 2x"
+```
+
+Strict stringifying validates before serializing.
+
+```ts
+stringify(
+ [
+ {url: "image.png", density: 1},
+ {url: "image-copy.png", density: 1},
+ ],
+ {strict: true},
+);
+// throws SrcsetValidationError
+```
+
+## Round Trips
+
+Use `parse()` and `stringify()` together to normalize whitespace while preserving
+URLs.
+
+```ts
+stringify(parse(" image.png 1x,\n image@2x.png 2x "));
+// "image.png 1x, image@2x.png 2x"
+
+parse(
+ stringify([
+ {url: "data:image/png;base64,AAAA+BB==", density: 1},
+ {url: "/image.png?w=100,h=200&fmt=webp", density: 2},
+ ]),
+);
+// [
+// {url: "data:image/png;base64,AAAA+BB==", density: 1},
+// {url: "/image.png?w=100,h=200&fmt=webp", density: 2},
+// ]
+```
+
+## Validation Rules
+
+`srcset-kit` checks the parts that matter when accepting or transforming
+`srcset` values:
+
+- Empty `srcset` values are invalid.
+- Candidate URLs must be non-empty.
+- Relative URLs can be validated with `baseUrl`.
+- A candidate may be fallback, density-based, or width-based.
+- A candidate must not contain both density and width.
+- Width descriptors must be positive integers.
+- Density descriptors must be positive finite numbers.
+- Width and density descriptors must not be mixed in one candidate set.
+- Duplicate descriptors are invalid.
+- A fallback candidate is equivalent to `1x`.
+- Descriptor policy mismatches are reported with stable issue codes.
+
+## API
+
+```ts
+import {
+ parse,
+ stringify,
+ validate,
+ SrcsetError,
+ SrcsetValidationError,
+ type DescriptorType,
+ type ParseOptions,
+ type SrcsetCandidate,
+ type SrcsetValidationIssue,
+ type StringifyOptions,
+ type ValidateOptions,
+ type ValidationResult,
+} from "srcset-kit";
+```
+
+```ts
+type SrcsetCandidate =
+ | {url: string; density: number}
+ | {url: string; width: number}
+ | {url: string};
+
+type ParseOptions = {
+ strict?: boolean;
+};
+
+type ValidateOptions = {
+ baseUrl?: string | URL;
+ descriptor?: "width" | "density";
+};
+
+type StringifyOptions = {
+ strict?: boolean;
+};
+```
+
+## Package
+
+- Runtime dependencies: none.
+- Works in Node and browser environments.
+- Source language: TypeScript.
+- Published formats: ESM, CommonJS, and TypeScript declarations.
+- Public API: `parse`, `validate`, `stringify`, types, and package-specific
+ errors.
+
+## Development
+
+```sh
+pnpm install
+pnpm run verify
+```
diff --git a/biome.json b/biome.json
new file mode 100644
index 0000000..1ca02c4
--- /dev/null
+++ b/biome.json
@@ -0,0 +1,35 @@
+{
+ "$schema": "https://biomejs.dev/schemas/2.4.14/schema.json",
+ "vcs": {
+ "enabled": true,
+ "clientKind": "git",
+ "useIgnoreFile": true
+ },
+ "formatter": {
+ "enabled": true,
+ "indentStyle": "space",
+ "indentWidth": 2,
+ "lineWidth": 100
+ },
+ "linter": {
+ "enabled": true,
+ "rules": {
+ "recommended": true
+ }
+ },
+ "javascript": {
+ "formatter": {
+ "bracketSpacing": false,
+ "indentWidth": 4,
+ "quoteStyle": "double",
+ "semicolons": "always",
+ "trailingCommas": "all"
+ }
+ },
+ "json": {
+ "formatter": {
+ "indentWidth": 2,
+ "trailingCommas": "none"
+ }
+ }
+}
diff --git a/commitlint.config.cjs b/commitlint.config.cjs
new file mode 100644
index 0000000..db74d15
--- /dev/null
+++ b/commitlint.config.cjs
@@ -0,0 +1,3 @@
+module.exports = {
+ extends: ["@commitlint/config-conventional"],
+};
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..9afdd4e
--- /dev/null
+++ b/package.json
@@ -0,0 +1,84 @@
+{
+ "name": "srcset-kit",
+ "version": "0.0.1",
+ "description": "srcset parser, validator, and stringifier for responsive images with zero dependencies.",
+ "keywords": [
+ "srcset",
+ "srcset-parser",
+ "srcset-validation",
+ "srcset-stringify",
+ "responsive-images",
+ "responsive-image",
+ "image-srcset",
+ "srcset-attribute",
+ "data-url",
+ "html",
+ "browser",
+ "typescript"
+ ],
+ "type": "module",
+ "license": "MIT",
+ "author": {
+ "name": "Anjey Tsibylskij",
+ "url": "https://github.com/atldays"
+ },
+ "homepage": "https://github.com/atldays/srcset-kit#readme",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/atldays/srcset-kit.git"
+ },
+ "bugs": {
+ "url": "https://github.com/atldays/srcset-kit/issues"
+ },
+ "sideEffects": false,
+ "files": [
+ "dist"
+ ],
+ "main": "./dist/index.cjs",
+ "module": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "exports": {
+ ".": {
+ "import": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.js"
+ },
+ "require": {
+ "types": "./dist/index.d.cts",
+ "default": "./dist/index.cjs"
+ },
+ "default": "./dist/index.js"
+ }
+ },
+ "publishConfig": {
+ "access": "public",
+ "registry": "https://registry.npmjs.org/"
+ },
+ "packageManager": "pnpm@10.30.3",
+ "scripts": {
+ "prepare": "husky",
+ "build": "rslib build",
+ "test": "rstest run",
+ "test:watch": "rstest watch",
+ "typecheck": "tsc --noEmit",
+ "lint": "biome lint .",
+ "format": "biome format --write .",
+ "format:check": "biome format .",
+ "pack:check": "publint",
+ "verify": "pnpm run format:check && pnpm run lint && pnpm run typecheck && pnpm run test && pnpm run build && pnpm run pack:check"
+ },
+ "lint-staged": {
+ "*.{ts,tsx,js,jsx,json,jsonc,md,yml,yaml}": "biome check --write --no-errors-on-unmatched"
+ },
+ "devDependencies": {
+ "@biomejs/biome": "^2.4.14",
+ "@commitlint/cli": "^20.5.3",
+ "@commitlint/config-conventional": "^20.5.3",
+ "@rslib/core": "^0.21.3",
+ "@rstest/core": "^0.9.10",
+ "husky": "^9.1.7",
+ "lint-staged": "^16.4.0",
+ "publint": "^0.3.18",
+ "typescript": "^6.0.3"
+ }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
new file mode 100644
index 0000000..3020bbb
--- /dev/null
+++ b/pnpm-lock.yaml
@@ -0,0 +1,1448 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ devDependencies:
+ '@biomejs/biome':
+ specifier: ^2.4.14
+ version: 2.4.14
+ '@commitlint/cli':
+ specifier: ^20.5.3
+ version: 20.5.3(@types/node@25.6.0)(conventional-commits-parser@6.4.0)(typescript@6.0.3)
+ '@commitlint/config-conventional':
+ specifier: ^20.5.3
+ version: 20.5.3
+ '@rslib/core':
+ specifier: ^0.21.3
+ version: 0.21.3(typescript@6.0.3)
+ '@rstest/core':
+ specifier: ^0.9.10
+ version: 0.9.10
+ husky:
+ specifier: ^9.1.7
+ version: 9.1.7
+ lint-staged:
+ specifier: ^16.4.0
+ version: 16.4.0
+ publint:
+ specifier: ^0.3.18
+ version: 0.3.18
+ typescript:
+ specifier: ^6.0.3
+ version: 6.0.3
+
+packages:
+
+ '@ast-grep/napi-darwin-arm64@0.37.0':
+ resolution: {integrity: sha512-QAiIiaAbLvMEg/yBbyKn+p1gX2/FuaC0SMf7D7capm/oG4xGMzdeaQIcSosF4TCxxV+hIH4Bz9e4/u7w6Bnk3Q==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@ast-grep/napi-darwin-x64@0.37.0':
+ resolution: {integrity: sha512-zvcvdgekd4ySV3zUbUp8HF5nk5zqwiMXTuVzTUdl/w08O7JjM6XPOIVT+d2o/MqwM9rsXdzdergY5oY2RdhSPA==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@ast-grep/napi-linux-arm64-gnu@0.37.0':
+ resolution: {integrity: sha512-L7Sj0lXy8X+BqSMgr1LB8cCoWk0rericdeu+dC8/c8zpsav5Oo2IQKY1PmiZ7H8IHoFBbURLf8iklY9wsD+cyA==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@ast-grep/napi-linux-arm64-musl@0.37.0':
+ resolution: {integrity: sha512-LF9sAvYy6es/OdyJDO3RwkX3I82Vkfsng1sqUBcoWC1jVb1wX5YVzHtpQox9JrEhGl+bNp7FYxB4Qba9OdA5GA==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@ast-grep/napi-linux-x64-gnu@0.37.0':
+ resolution: {integrity: sha512-TViz5/klqre6aSmJzswEIjApnGjJzstG/SE8VDWsrftMBMYt2PTu3MeluZVwzSqDao8doT/P+6U11dU05UOgxw==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@ast-grep/napi-linux-x64-musl@0.37.0':
+ resolution: {integrity: sha512-/BcCH33S9E3ovOAEoxYngUNXgb+JLg991sdyiNP2bSoYd30a9RHrG7CYwW6fMgua3ijQ474eV6cq9yZO1bCpXg==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@ast-grep/napi-win32-arm64-msvc@0.37.0':
+ resolution: {integrity: sha512-TjQA4cFoIEW2bgjLkaL9yqT4XWuuLa5MCNd0VCDhGRDMNQ9+rhwi9eLOWRaap3xzT7g+nlbcEHL3AkVCD2+b3A==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@ast-grep/napi-win32-ia32-msvc@0.37.0':
+ resolution: {integrity: sha512-uNmVka8fJCdYsyOlF9aZqQMLTatEYBynjChVTzUfFMDfmZ0bihs/YTqJVbkSm8TZM7CUX82apvn50z/dX5iWRA==}
+ engines: {node: '>= 10'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@ast-grep/napi-win32-x64-msvc@0.37.0':
+ resolution: {integrity: sha512-vCiFOT3hSCQuHHfZ933GAwnPzmL0G04JxQEsBRfqONywyT8bSdDc/ECpAfr3S9VcS4JZ9/F6tkePKW/Om2Dq2g==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [win32]
+
+ '@ast-grep/napi@0.37.0':
+ resolution: {integrity: sha512-Hb4o6h1Pf6yRUAX07DR4JVY7dmQw+RVQMW5/m55GoiAT/VRoKCWBtIUPPOnqDVhbx1Cjfil9b6EDrgJsUAujEQ==}
+ engines: {node: '>= 10'}
+
+ '@babel/code-frame@7.29.0':
+ resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.28.5':
+ resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
+ engines: {node: '>=6.9.0'}
+
+ '@biomejs/biome@2.4.14':
+ resolution: {integrity: sha512-TmAvxOEgrpLypzVGJ8FulIZnlyA9TxrO1hyqYrCz9r+bwma9xXxuLA5IuYnj55XQneFx460KjRbx6SWGLkg3bQ==}
+ engines: {node: '>=14.21.3'}
+ hasBin: true
+
+ '@biomejs/cli-darwin-arm64@2.4.14':
+ resolution: {integrity: sha512-XvgoE9XOawUOQPdmvs4J7wPhi/DLwSCGks3AlPJDmh34O0awRTqCED1HRcRDdpf1Zrp4us4MGOOdIxNpbqNF5Q==}
+ engines: {node: '>=14.21.3'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@biomejs/cli-darwin-x64@2.4.14':
+ resolution: {integrity: sha512-jE7hKBCFhOx3uUh+ZkWBfOHxAcILPfhFplNkuID/eZeSTLHzfZzoZxW8fbqY9xXRnPi7jGNAf1iPVR+0yWsM/Q==}
+ engines: {node: '>=14.21.3'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@biomejs/cli-linux-arm64-musl@2.4.14':
+ resolution: {integrity: sha512-/z+6gqAqqUQTHazwStxSXKHg9b8UvqBmDFRp+c4wYbq2KXhELQDon9EoC9RpmQ8JWkqQx/lIUy/cs+MhzDZp6A==}
+ engines: {node: '>=14.21.3'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@biomejs/cli-linux-arm64@2.4.14':
+ resolution: {integrity: sha512-2TELhZnW5RSLL063l9rc5xLpA0ZIw0Ccwy/0q384rvNAgFw3yI76bd59547yxowdQr5MNPET/xDLrLuvgSeeWQ==}
+ engines: {node: '>=14.21.3'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@biomejs/cli-linux-x64-musl@2.4.14':
+ resolution: {integrity: sha512-R6BWgJdQOwW9ulJatuTVrQkjnODjqHZkKNOqb1sz++3Noe5LYd0i3PchnOBUCYAPHoPWHhjJqbdZlHEu0hpjdA==}
+ engines: {node: '>=14.21.3'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@biomejs/cli-linux-x64@2.4.14':
+ resolution: {integrity: sha512-zHrlQZDBDUz4OLAraYpWKcnLS6HOewBFWYOzY91d1ZjdqZwibOyb6BEu6WuWLugyo0P3riCmsbV9UqV1cSXwQg==}
+ engines: {node: '>=14.21.3'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@biomejs/cli-win32-arm64@2.4.14':
+ resolution: {integrity: sha512-M3EH5hqOI/F/FUA2u4xcLoUgmxd218mvuj/6JL7Hv2toQvr2/AdOvKSpGkoRuWFCtQPVa+ZqkEV3Q5xBA9+XSA==}
+ engines: {node: '>=14.21.3'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@biomejs/cli-win32-x64@2.4.14':
+ resolution: {integrity: sha512-WL0EG5qE+EAKomGXbf2g6VnSKJhTL3tXC0QRzWRwA5VpjxNYa6H4P7ZWfymbGE4IhZZQi1KXQ2R0YjwInmz2fA==}
+ engines: {node: '>=14.21.3'}
+ cpu: [x64]
+ os: [win32]
+
+ '@commitlint/cli@20.5.3':
+ resolution: {integrity: sha512-OJdL0EXWD5y9LPa0nr/geOwzaS8BsdaybKkcloB0JgsguGxNv2R+hC2FTPqrAcprg35zF33KOQerY0x8W1aesA==}
+ engines: {node: '>=v18'}
+ hasBin: true
+
+ '@commitlint/config-conventional@20.5.3':
+ resolution: {integrity: sha512-j34Qqeaa152chJgz2ysyk0BCpHenJn1lV0Rx0VXf8k3ccQcED+48EZrzMvo9jLmJUyBrrBwvu89I+2er4gW7QQ==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/config-validator@20.5.0':
+ resolution: {integrity: sha512-T/Uh6iJUzyx7j35GmHWdIiGRQB+ouZDk0pwAaYq4SXgB54KZhFdJ0vYmxiW6AMYICTIWuyMxDBl1jK74oFp/Gw==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/ensure@20.5.3':
+ resolution: {integrity: sha512-4i4AgNvH62owG9MwSiWKrle7HGNpBHHdLnWFIp5fTsHUYe5kRuh15t08L/0pdbbrRk8JKXQxxN4hZQcn+szkrw==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/execute-rule@20.0.0':
+ resolution: {integrity: sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/format@20.5.0':
+ resolution: {integrity: sha512-TI9EwFU/qZWSK7a5qyXMpKPPv3qta7FO4tKW+Wt2al7sgMbLWTsAcDpX1cU8k16TRdsiiet9aOw0zpvRXNJu7Q==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/is-ignored@20.5.0':
+ resolution: {integrity: sha512-JWLarAsurHJhPozbuAH6GbP4p/hdOCoqS9zJMfqwswne+/GPs5V0+rrsfOkP68Y8PSLphwtFXV0EzJ+GTXTTGg==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/lint@20.5.3':
+ resolution: {integrity: sha512-M7JbWBNr2gXKaPc4i/KipsuW1gkDHpj35KPjWtKy3Z+2AQw5wu1gBi1LIO0uoaij67CqY4K8PxPZSGens4evCw==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/load@20.5.3':
+ resolution: {integrity: sha512-1FDZWuKyu98Myb8i7Tp31jPU2rZpOwAdYRyJcy2KoGg7Xk2A+bgHN8smhMaaNSNkmE8fwt53BokywZq8Gv/5XQ==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/message@20.4.3':
+ resolution: {integrity: sha512-6akwCYrzcrFcTYz9GyUaWlhisY4lmQ3KvrnabmhoeAV8nRH4dXJAh4+EUQ3uArtxxKQkvxJS78hNX2EU3USgxQ==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/parse@20.5.0':
+ resolution: {integrity: sha512-SeKWHBMk7YOTnnEWUhx+d1a9vHsjjuo6Uo1xRfPNfeY4bdYFasCH1dDpAv13Lyn+dDPOels+jP6D2GRZqzc5fA==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/read@20.5.0':
+ resolution: {integrity: sha512-JDEIJ2+GnWpK8QqwfmW7O42h0aycJEWNqcdkJnyzLD11nf9dW2dWLTVEa8Wtlo4IZFGLPATjR5neA5QlOvIH1w==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/resolve-extends@20.5.3':
+ resolution: {integrity: sha512-+ogW9v/u9JqpvAgTrLra/YTFo0KkjU6iNblF89pPsj4NebNc+DAWctsludwezI8YnsjBmfHpApSwcXprN/f/ew==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/rules@20.5.3':
+ resolution: {integrity: sha512-MPlMnb9D3wbszYMp+1hPtuhtPJndRo6I6yfkZVA4+jR8w7Kqp0u2u/Y+gzbaItx5Lltq5rw7FSZQWJMoXUC4NQ==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/to-lines@20.0.0':
+ resolution: {integrity: sha512-2l9gmwiCRqZNWgV+pX1X7z4yP0b3ex/86UmUFgoRt672Ez6cAM2lOQeHFRUTuE6sPpi8XBCGnd8Kh3bMoyHwJw==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/top-level@20.4.3':
+ resolution: {integrity: sha512-qD9xfP6dFg5jQ3NMrOhG0/w5y3bBUsVGyJvXxdWEwBm8hyx4WOk3kKXw28T5czBYvyeCVJgJJ6aoJZUWDpaacQ==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/types@20.5.0':
+ resolution: {integrity: sha512-ZJoS8oSq2CAZEpc/YI9SulLrdiIyXeHb/OGqGrkUP6Q7YV+0ouNAa7GjqRdXeQPncHQIDz/jbCTlHScvYvO/gA==}
+ engines: {node: '>=v18'}
+
+ '@conventional-changelog/git-client@2.7.0':
+ resolution: {integrity: sha512-j7A8/LBEQ+3rugMzPXoKYzyUPpw/0CBQCyvtTR7Lmu4olG4yRC/Tfkq79Mr3yuPs0SUitlO2HwGP3gitMJnRFw==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ conventional-commits-filter: ^5.0.0
+ conventional-commits-parser: ^6.4.0
+ peerDependenciesMeta:
+ conventional-commits-filter:
+ optional: true
+ conventional-commits-parser:
+ optional: true
+
+ '@emnapi/core@1.10.0':
+ resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==}
+
+ '@emnapi/runtime@1.10.0':
+ resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==}
+
+ '@emnapi/wasi-threads@1.2.1':
+ resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==}
+
+ '@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
+
+ '@publint/pack@0.1.4':
+ resolution: {integrity: sha512-HDVTWq3H0uTXiU0eeSQntcVUTPP3GamzeXI41+x7uU9J65JgWQh3qWZHblR1i0npXfFtF+mxBiU2nJH8znxWnQ==}
+ engines: {node: '>=18'}
+
+ '@rsbuild/core@2.0.3':
+ resolution: {integrity: sha512-2myp7jUgGen50saxW8OJD/eMVKp7HnuBN5MUzwRb6mDbRZZVpoorfI4LQqiGSBNjGLB6jltvx/R2yHmcmnchwg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ core-js: '>= 3.0.0'
+ peerDependenciesMeta:
+ core-js:
+ optional: true
+
+ '@rslib/core@0.21.3':
+ resolution: {integrity: sha512-3kyF273GQWIky4rAGD+Nkewlc7OraRwM2rG6wMJ19cYeomN0OKokVbk0vvfLAcQu43mtEO+dnZk6BchUoRmQOg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ '@microsoft/api-extractor': ^7
+ typescript: ^5 || ^6
+ peerDependenciesMeta:
+ '@microsoft/api-extractor':
+ optional: true
+ typescript:
+ optional: true
+
+ '@rspack/binding-darwin-arm64@2.0.1':
+ resolution: {integrity: sha512-CGFO5zmajD1Itch1lxAI7+gvKiagzyqXopHv/jHG9Su2WWQ2/Nhn2/rkSpdp6ptE9ri6+6tCOOahf099/v/Xog==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rspack/binding-darwin-x64@2.0.1':
+ resolution: {integrity: sha512-2vvBNBoS09/PurupBwSrlTZd8283o00B8v20ncsNUdEff41uCR/hzIrYoTIVWnVST+Gt5O1+cfcfORp397lajg==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rspack/binding-linux-arm64-gnu@2.0.1':
+ resolution: {integrity: sha512-uvNXk6ahE3AH3h2avnd1Mgno68YQpS4cfX1OkOGWIC/roL+NrOP2XVXV4yfVAoydPALDO7AfbIfN0QdmBK3rsA==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rspack/binding-linux-arm64-musl@2.0.1':
+ resolution: {integrity: sha512-S/a6uN9PiZ5O/PjSqyIXhuRC1lVzeJkJV69NeLk5sIEUiDQ/aQGZG97uN+tluwpbo1tPbLJkdHYETfjspOX4Pg==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@rspack/binding-linux-x64-gnu@2.0.1':
+ resolution: {integrity: sha512-C13Kk0OkZiocZVj187Sf753UH6pDXnuEu6vzUvi3qv9ltibG1ki0H2Y8isXBYL2cHQOV+hk0g1S6/4z3TTB97A==}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rspack/binding-linux-x64-musl@2.0.1':
+ resolution: {integrity: sha512-TQsiBFpEDGkuvK9tNdGj/Uc+AIytzqhxXH/1jKU6M24cWB1DTw/Cx7DdrkCBDyq3129K3POLdujvbWCGqBzQUw==}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@rspack/binding-wasm32-wasi@2.0.1':
+ resolution: {integrity: sha512-wk3gyUgBW/ayP49bI54bkY8+EQnfBHxdoe9dz3oobSTZQc8AOWwmUUDEPltW8rUvPOM6dfHECTOUMnfaf2f5yA==}
+ cpu: [wasm32]
+
+ '@rspack/binding-win32-arm64-msvc@2.0.1':
+ resolution: {integrity: sha512-rHjLcy3VcAC3+x+PxH+gwhwv6tPe0JdXTNT5eAOs9wgZIM6T9p4wre49+K4Qy98+Fb7TTbLX0ObUitlOkGwTSA==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rspack/binding-win32-ia32-msvc@2.0.1':
+ resolution: {integrity: sha512-Ad1vVqMBBnd4T8rsORngu9sl2kyRTlS4kMlvFudjzl1X2UFArEDBe0YVGNN7ZvahM12CErUx2WiN8Sd8pb+qXQ==}
+ cpu: [ia32]
+ os: [win32]
+
+ '@rspack/binding-win32-x64-msvc@2.0.1':
+ resolution: {integrity: sha512-oPM2Jtm7HOlmxl/aBfleAVlL6t9VeHx6WvEets7BBJMInemFXAQd4CErRqybf7rXutACzLeUWBOue4Jpd1/ykw==}
+ cpu: [x64]
+ os: [win32]
+
+ '@rspack/binding@2.0.1':
+ resolution: {integrity: sha512-ynV1gw4KqFtQ0P+ZZh76SUj49wBb2FuHW3zSmHverHWuxBhzvrZS6/dZ+fCFQG8bTTPtrPz0RQUTN3uEDbPVBQ==}
+
+ '@rspack/core@2.0.1':
+ resolution: {integrity: sha512-lgfZiExh8kDR/3obgi3RQKwKG5av1Xf5qDN1aVde777W9pbmx0Pqvrww1qtNvJ+gobEjbrrn5HEZWYGe0VLmcA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ peerDependencies:
+ '@module-federation/runtime-tools': ^0.24.1 || ^2.0.0
+ '@swc/helpers': '>=0.5.1'
+ peerDependenciesMeta:
+ '@module-federation/runtime-tools':
+ optional: true
+ '@swc/helpers':
+ optional: true
+
+ '@rstest/core@0.9.10':
+ resolution: {integrity: sha512-JUSUYYXWIHEBUn193u2RglZujvGPv46Blxpl17QFwc0y9vEXe2y/IfGwGrVsJfZz7PaWZ5NnbFf0J55YIIns+g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ happy-dom: ^20.8.3
+ jsdom: '*'
+ peerDependenciesMeta:
+ happy-dom:
+ optional: true
+ jsdom:
+ optional: true
+
+ '@simple-libs/child-process-utils@1.0.2':
+ resolution: {integrity: sha512-/4R8QKnd/8agJynkNdJmNw2MBxuFTRcNFnE5Sg/G+jkSsV8/UBgULMzhizWWW42p8L5H7flImV2ATi79Ove2Tw==}
+ engines: {node: '>=18'}
+
+ '@simple-libs/stream-utils@1.2.0':
+ resolution: {integrity: sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==}
+ engines: {node: '>=18'}
+
+ '@swc/helpers@0.5.21':
+ resolution: {integrity: sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==}
+
+ '@tybys/wasm-util@0.10.1':
+ resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
+
+ '@types/chai@5.2.3':
+ resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
+
+ '@types/deep-eql@4.0.2':
+ resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
+
+ '@types/node@25.6.0':
+ resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==}
+
+ ajv@8.20.0:
+ resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==}
+
+ ansi-escapes@7.3.0:
+ resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==}
+ engines: {node: '>=18'}
+
+ ansi-regex@5.0.1:
+ resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
+ engines: {node: '>=8'}
+
+ ansi-regex@6.2.2:
+ resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
+ engines: {node: '>=12'}
+
+ ansi-styles@4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+
+ ansi-styles@6.2.3:
+ resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
+ engines: {node: '>=12'}
+
+ argparse@2.0.1:
+ resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+
+ array-ify@1.0.0:
+ resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==}
+
+ assertion-error@2.0.1:
+ resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
+ engines: {node: '>=12'}
+
+ callsites@3.1.0:
+ resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
+ engines: {node: '>=6'}
+
+ cli-cursor@5.0.0:
+ resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
+ engines: {node: '>=18'}
+
+ cli-truncate@5.2.0:
+ resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==}
+ engines: {node: '>=20'}
+
+ cliui@8.0.1:
+ resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
+ engines: {node: '>=12'}
+
+ color-convert@2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+
+ color-name@1.1.4:
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
+ colorette@2.0.20:
+ resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
+
+ commander@14.0.3:
+ resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
+ engines: {node: '>=20'}
+
+ compare-func@2.0.0:
+ resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==}
+
+ conventional-changelog-angular@8.3.1:
+ resolution: {integrity: sha512-6gfI3otXK5Ph5DfCOI1dblr+kN3FAm5a97hYoQkqNZxOaYa5WKfXH+AnpsmS+iUH2mgVC2Cg2Qw9m5OKcmNrIg==}
+ engines: {node: '>=18'}
+
+ conventional-changelog-conventionalcommits@9.3.1:
+ resolution: {integrity: sha512-dTYtpIacRpcZgrvBYvBfArMmK2xvIpv2TaxM0/ZI5CBtNUzvF2x0t15HsbRABWprS6UPmvj+PzHVjSx4qAVKyw==}
+ engines: {node: '>=18'}
+
+ conventional-commits-parser@6.4.0:
+ resolution: {integrity: sha512-tvRg7FIBNlyPzjdG8wWRlPHQJJHI7DylhtRGeU9Lq+JuoPh5BKpPRX83ZdLrvXuOSu5Eo/e7SzOQhU4Hd2Miuw==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ cosmiconfig-typescript-loader@6.3.0:
+ resolution: {integrity: sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA==}
+ engines: {node: '>=v18'}
+ peerDependencies:
+ '@types/node': '*'
+ cosmiconfig: '>=9'
+ typescript: '>=5'
+
+ cosmiconfig@9.0.1:
+ resolution: {integrity: sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ typescript: '>=4.9.5'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ dot-prop@5.3.0:
+ resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==}
+ engines: {node: '>=8'}
+
+ emoji-regex@10.6.0:
+ resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
+
+ emoji-regex@8.0.0:
+ resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
+
+ env-paths@2.2.1:
+ resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
+ engines: {node: '>=6'}
+
+ environment@1.1.0:
+ resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==}
+ engines: {node: '>=18'}
+
+ error-ex@1.3.4:
+ resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
+
+ es-toolkit@1.46.1:
+ resolution: {integrity: sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==}
+
+ escalade@3.2.0:
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+ engines: {node: '>=6'}
+
+ eventemitter3@5.0.4:
+ resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
+
+ fast-deep-equal@3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+ fast-uri@3.1.0:
+ resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
+
+ get-caller-file@2.0.5:
+ resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
+ engines: {node: 6.* || 8.* || >= 10.*}
+
+ get-east-asian-width@1.5.0:
+ resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==}
+ engines: {node: '>=18'}
+
+ git-raw-commits@5.0.1:
+ resolution: {integrity: sha512-Y+csSm2GD/PCSh6Isd/WiMjNAydu0VBiG9J7EdQsNA5P9uXvLayqjmTsNlK5Gs9IhblFZqOU0yid5Il5JPoLiQ==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ global-directory@5.0.0:
+ resolution: {integrity: sha512-1pgFdhK3J2LeM+dVf2Pd424yHx2ou338lC0ErNP2hPx4j8eW1Sp0XqSjNxtk6Tc4Kr5wlWtSvz8cn2yb7/SG/w==}
+ engines: {node: '>=20'}
+
+ husky@9.1.7:
+ resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ import-fresh@3.3.1:
+ resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
+ engines: {node: '>=6'}
+
+ import-meta-resolve@4.2.0:
+ resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==}
+
+ ini@6.0.0:
+ resolution: {integrity: sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==}
+ engines: {node: ^20.17.0 || >=22.9.0}
+
+ is-arrayish@0.2.1:
+ resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
+
+ is-fullwidth-code-point@3.0.0:
+ resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
+ engines: {node: '>=8'}
+
+ is-fullwidth-code-point@5.1.0:
+ resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==}
+ engines: {node: '>=18'}
+
+ is-obj@2.0.0:
+ resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==}
+ engines: {node: '>=8'}
+
+ is-plain-obj@4.1.0:
+ resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
+ engines: {node: '>=12'}
+
+ jiti@2.6.1:
+ resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
+ hasBin: true
+
+ js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+ js-yaml@4.1.1:
+ resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
+ hasBin: true
+
+ json-parse-even-better-errors@2.3.1:
+ resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
+
+ json-schema-traverse@1.0.0:
+ resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
+
+ lines-and-columns@1.2.4:
+ resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
+
+ lint-staged@16.4.0:
+ resolution: {integrity: sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==}
+ engines: {node: '>=20.17'}
+ hasBin: true
+
+ listr2@9.0.5:
+ resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==}
+ engines: {node: '>=20.0.0'}
+
+ log-update@6.1.0:
+ resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==}
+ engines: {node: '>=18'}
+
+ meow@13.2.0:
+ resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==}
+ engines: {node: '>=18'}
+
+ mimic-function@5.0.1:
+ resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
+ engines: {node: '>=18'}
+
+ minimist@1.2.8:
+ resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
+
+ mri@1.2.0:
+ resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
+ engines: {node: '>=4'}
+
+ onetime@7.0.0:
+ resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
+ engines: {node: '>=18'}
+
+ package-manager-detector@1.6.0:
+ resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==}
+
+ parent-module@1.0.1:
+ resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
+ engines: {node: '>=6'}
+
+ parse-json@5.2.0:
+ resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
+ engines: {node: '>=8'}
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@4.0.4:
+ resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
+ engines: {node: '>=12'}
+
+ publint@0.3.18:
+ resolution: {integrity: sha512-JRJFeBTrfx4qLwEuGFPk+haJOJN97KnPuK01yj+4k/Wj5BgoOK5uNsivporiqBjk2JDaslg7qJOhGRnpltGeog==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ require-directory@2.1.1:
+ resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
+ engines: {node: '>=0.10.0'}
+
+ require-from-string@2.0.2:
+ resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
+ engines: {node: '>=0.10.0'}
+
+ resolve-from@4.0.0:
+ resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
+ engines: {node: '>=4'}
+
+ resolve-from@5.0.0:
+ resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
+ engines: {node: '>=8'}
+
+ restore-cursor@5.1.0:
+ resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
+ engines: {node: '>=18'}
+
+ rfdc@1.4.1:
+ resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
+
+ rsbuild-plugin-dts@0.21.3:
+ resolution: {integrity: sha512-8E3/npwRp99gc/Bl5bE1KKN5eIS2TQ3fuA7fBEk67R1RF7V4OtFKVI7mhk3X8zoH/9cclV9v909dguegZDgncw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ peerDependencies:
+ '@microsoft/api-extractor': ^7
+ '@rsbuild/core': ^1.0.0 || ^2.0.0-0
+ '@typescript/native-preview': 7.x
+ typescript: ^5 || ^6
+ peerDependenciesMeta:
+ '@microsoft/api-extractor':
+ optional: true
+ '@typescript/native-preview':
+ optional: true
+ typescript:
+ optional: true
+
+ sade@1.8.1:
+ resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
+ engines: {node: '>=6'}
+
+ semver@7.7.4:
+ resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ signal-exit@4.1.0:
+ resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
+ engines: {node: '>=14'}
+
+ slice-ansi@7.1.2:
+ resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==}
+ engines: {node: '>=18'}
+
+ slice-ansi@8.0.0:
+ resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==}
+ engines: {node: '>=20'}
+
+ string-argv@0.3.2:
+ resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
+ engines: {node: '>=0.6.19'}
+
+ string-width@4.2.3:
+ resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
+ engines: {node: '>=8'}
+
+ string-width@7.2.0:
+ resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
+ engines: {node: '>=18'}
+
+ string-width@8.2.1:
+ resolution: {integrity: sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==}
+ engines: {node: '>=20'}
+
+ strip-ansi@6.0.1:
+ resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
+ engines: {node: '>=8'}
+
+ strip-ansi@7.2.0:
+ resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==}
+ engines: {node: '>=12'}
+
+ tinyexec@1.1.2:
+ resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==}
+ engines: {node: '>=18'}
+
+ tinypool@2.1.0:
+ resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==}
+ engines: {node: ^20.0.0 || >=22.0.0}
+
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+ typescript@6.0.3:
+ resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ undici-types@7.19.2:
+ resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==}
+
+ wrap-ansi@7.0.0:
+ resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
+ engines: {node: '>=10'}
+
+ wrap-ansi@9.0.2:
+ resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==}
+ engines: {node: '>=18'}
+
+ y18n@5.0.8:
+ resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
+ engines: {node: '>=10'}
+
+ yaml@2.8.3:
+ resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==}
+ engines: {node: '>= 14.6'}
+ hasBin: true
+
+ yargs-parser@21.1.1:
+ resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
+ engines: {node: '>=12'}
+
+ yargs@17.7.2:
+ resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
+ engines: {node: '>=12'}
+
+snapshots:
+
+ '@ast-grep/napi-darwin-arm64@0.37.0':
+ optional: true
+
+ '@ast-grep/napi-darwin-x64@0.37.0':
+ optional: true
+
+ '@ast-grep/napi-linux-arm64-gnu@0.37.0':
+ optional: true
+
+ '@ast-grep/napi-linux-arm64-musl@0.37.0':
+ optional: true
+
+ '@ast-grep/napi-linux-x64-gnu@0.37.0':
+ optional: true
+
+ '@ast-grep/napi-linux-x64-musl@0.37.0':
+ optional: true
+
+ '@ast-grep/napi-win32-arm64-msvc@0.37.0':
+ optional: true
+
+ '@ast-grep/napi-win32-ia32-msvc@0.37.0':
+ optional: true
+
+ '@ast-grep/napi-win32-x64-msvc@0.37.0':
+ optional: true
+
+ '@ast-grep/napi@0.37.0':
+ optionalDependencies:
+ '@ast-grep/napi-darwin-arm64': 0.37.0
+ '@ast-grep/napi-darwin-x64': 0.37.0
+ '@ast-grep/napi-linux-arm64-gnu': 0.37.0
+ '@ast-grep/napi-linux-arm64-musl': 0.37.0
+ '@ast-grep/napi-linux-x64-gnu': 0.37.0
+ '@ast-grep/napi-linux-x64-musl': 0.37.0
+ '@ast-grep/napi-win32-arm64-msvc': 0.37.0
+ '@ast-grep/napi-win32-ia32-msvc': 0.37.0
+ '@ast-grep/napi-win32-x64-msvc': 0.37.0
+
+ '@babel/code-frame@7.29.0':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.28.5
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
+ '@babel/helper-validator-identifier@7.28.5': {}
+
+ '@biomejs/biome@2.4.14':
+ optionalDependencies:
+ '@biomejs/cli-darwin-arm64': 2.4.14
+ '@biomejs/cli-darwin-x64': 2.4.14
+ '@biomejs/cli-linux-arm64': 2.4.14
+ '@biomejs/cli-linux-arm64-musl': 2.4.14
+ '@biomejs/cli-linux-x64': 2.4.14
+ '@biomejs/cli-linux-x64-musl': 2.4.14
+ '@biomejs/cli-win32-arm64': 2.4.14
+ '@biomejs/cli-win32-x64': 2.4.14
+
+ '@biomejs/cli-darwin-arm64@2.4.14':
+ optional: true
+
+ '@biomejs/cli-darwin-x64@2.4.14':
+ optional: true
+
+ '@biomejs/cli-linux-arm64-musl@2.4.14':
+ optional: true
+
+ '@biomejs/cli-linux-arm64@2.4.14':
+ optional: true
+
+ '@biomejs/cli-linux-x64-musl@2.4.14':
+ optional: true
+
+ '@biomejs/cli-linux-x64@2.4.14':
+ optional: true
+
+ '@biomejs/cli-win32-arm64@2.4.14':
+ optional: true
+
+ '@biomejs/cli-win32-x64@2.4.14':
+ optional: true
+
+ '@commitlint/cli@20.5.3(@types/node@25.6.0)(conventional-commits-parser@6.4.0)(typescript@6.0.3)':
+ dependencies:
+ '@commitlint/format': 20.5.0
+ '@commitlint/lint': 20.5.3
+ '@commitlint/load': 20.5.3(@types/node@25.6.0)(typescript@6.0.3)
+ '@commitlint/read': 20.5.0(conventional-commits-parser@6.4.0)
+ '@commitlint/types': 20.5.0
+ tinyexec: 1.1.2
+ yargs: 17.7.2
+ transitivePeerDependencies:
+ - '@types/node'
+ - conventional-commits-filter
+ - conventional-commits-parser
+ - typescript
+
+ '@commitlint/config-conventional@20.5.3':
+ dependencies:
+ '@commitlint/types': 20.5.0
+ conventional-changelog-conventionalcommits: 9.3.1
+
+ '@commitlint/config-validator@20.5.0':
+ dependencies:
+ '@commitlint/types': 20.5.0
+ ajv: 8.20.0
+
+ '@commitlint/ensure@20.5.3':
+ dependencies:
+ '@commitlint/types': 20.5.0
+ es-toolkit: 1.46.1
+
+ '@commitlint/execute-rule@20.0.0': {}
+
+ '@commitlint/format@20.5.0':
+ dependencies:
+ '@commitlint/types': 20.5.0
+ picocolors: 1.1.1
+
+ '@commitlint/is-ignored@20.5.0':
+ dependencies:
+ '@commitlint/types': 20.5.0
+ semver: 7.7.4
+
+ '@commitlint/lint@20.5.3':
+ dependencies:
+ '@commitlint/is-ignored': 20.5.0
+ '@commitlint/parse': 20.5.0
+ '@commitlint/rules': 20.5.3
+ '@commitlint/types': 20.5.0
+
+ '@commitlint/load@20.5.3(@types/node@25.6.0)(typescript@6.0.3)':
+ dependencies:
+ '@commitlint/config-validator': 20.5.0
+ '@commitlint/execute-rule': 20.0.0
+ '@commitlint/resolve-extends': 20.5.3
+ '@commitlint/types': 20.5.0
+ cosmiconfig: 9.0.1(typescript@6.0.3)
+ cosmiconfig-typescript-loader: 6.3.0(@types/node@25.6.0)(cosmiconfig@9.0.1(typescript@6.0.3))(typescript@6.0.3)
+ es-toolkit: 1.46.1
+ is-plain-obj: 4.1.0
+ picocolors: 1.1.1
+ transitivePeerDependencies:
+ - '@types/node'
+ - typescript
+
+ '@commitlint/message@20.4.3': {}
+
+ '@commitlint/parse@20.5.0':
+ dependencies:
+ '@commitlint/types': 20.5.0
+ conventional-changelog-angular: 8.3.1
+ conventional-commits-parser: 6.4.0
+
+ '@commitlint/read@20.5.0(conventional-commits-parser@6.4.0)':
+ dependencies:
+ '@commitlint/top-level': 20.4.3
+ '@commitlint/types': 20.5.0
+ git-raw-commits: 5.0.1(conventional-commits-parser@6.4.0)
+ minimist: 1.2.8
+ tinyexec: 1.1.2
+ transitivePeerDependencies:
+ - conventional-commits-filter
+ - conventional-commits-parser
+
+ '@commitlint/resolve-extends@20.5.3':
+ dependencies:
+ '@commitlint/config-validator': 20.5.0
+ '@commitlint/types': 20.5.0
+ es-toolkit: 1.46.1
+ global-directory: 5.0.0
+ import-meta-resolve: 4.2.0
+ resolve-from: 5.0.0
+
+ '@commitlint/rules@20.5.3':
+ dependencies:
+ '@commitlint/ensure': 20.5.3
+ '@commitlint/message': 20.4.3
+ '@commitlint/to-lines': 20.0.0
+ '@commitlint/types': 20.5.0
+
+ '@commitlint/to-lines@20.0.0': {}
+
+ '@commitlint/top-level@20.4.3':
+ dependencies:
+ escalade: 3.2.0
+
+ '@commitlint/types@20.5.0':
+ dependencies:
+ conventional-commits-parser: 6.4.0
+ picocolors: 1.1.1
+
+ '@conventional-changelog/git-client@2.7.0(conventional-commits-parser@6.4.0)':
+ dependencies:
+ '@simple-libs/child-process-utils': 1.0.2
+ '@simple-libs/stream-utils': 1.2.0
+ semver: 7.7.4
+ optionalDependencies:
+ conventional-commits-parser: 6.4.0
+
+ '@emnapi/core@1.10.0':
+ dependencies:
+ '@emnapi/wasi-threads': 1.2.1
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/runtime@1.10.0':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/wasi-threads@1.2.1':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
+ dependencies:
+ '@emnapi/core': 1.10.0
+ '@emnapi/runtime': 1.10.0
+ '@tybys/wasm-util': 0.10.1
+ optional: true
+
+ '@publint/pack@0.1.4': {}
+
+ '@rsbuild/core@2.0.3':
+ dependencies:
+ '@rspack/core': 2.0.1(@swc/helpers@0.5.21)
+ '@swc/helpers': 0.5.21
+ transitivePeerDependencies:
+ - '@module-federation/runtime-tools'
+
+ '@rslib/core@0.21.3(typescript@6.0.3)':
+ dependencies:
+ '@rsbuild/core': 2.0.3
+ rsbuild-plugin-dts: 0.21.3(@rsbuild/core@2.0.3)(typescript@6.0.3)
+ optionalDependencies:
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - '@module-federation/runtime-tools'
+ - '@typescript/native-preview'
+ - core-js
+
+ '@rspack/binding-darwin-arm64@2.0.1':
+ optional: true
+
+ '@rspack/binding-darwin-x64@2.0.1':
+ optional: true
+
+ '@rspack/binding-linux-arm64-gnu@2.0.1':
+ optional: true
+
+ '@rspack/binding-linux-arm64-musl@2.0.1':
+ optional: true
+
+ '@rspack/binding-linux-x64-gnu@2.0.1':
+ optional: true
+
+ '@rspack/binding-linux-x64-musl@2.0.1':
+ optional: true
+
+ '@rspack/binding-wasm32-wasi@2.0.1':
+ dependencies:
+ '@emnapi/core': 1.10.0
+ '@emnapi/runtime': 1.10.0
+ '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
+ optional: true
+
+ '@rspack/binding-win32-arm64-msvc@2.0.1':
+ optional: true
+
+ '@rspack/binding-win32-ia32-msvc@2.0.1':
+ optional: true
+
+ '@rspack/binding-win32-x64-msvc@2.0.1':
+ optional: true
+
+ '@rspack/binding@2.0.1':
+ optionalDependencies:
+ '@rspack/binding-darwin-arm64': 2.0.1
+ '@rspack/binding-darwin-x64': 2.0.1
+ '@rspack/binding-linux-arm64-gnu': 2.0.1
+ '@rspack/binding-linux-arm64-musl': 2.0.1
+ '@rspack/binding-linux-x64-gnu': 2.0.1
+ '@rspack/binding-linux-x64-musl': 2.0.1
+ '@rspack/binding-wasm32-wasi': 2.0.1
+ '@rspack/binding-win32-arm64-msvc': 2.0.1
+ '@rspack/binding-win32-ia32-msvc': 2.0.1
+ '@rspack/binding-win32-x64-msvc': 2.0.1
+
+ '@rspack/core@2.0.1(@swc/helpers@0.5.21)':
+ dependencies:
+ '@rspack/binding': 2.0.1
+ optionalDependencies:
+ '@swc/helpers': 0.5.21
+
+ '@rstest/core@0.9.10':
+ dependencies:
+ '@rsbuild/core': 2.0.3
+ '@types/chai': 5.2.3
+ tinypool: 2.1.0
+ transitivePeerDependencies:
+ - '@module-federation/runtime-tools'
+ - core-js
+
+ '@simple-libs/child-process-utils@1.0.2':
+ dependencies:
+ '@simple-libs/stream-utils': 1.2.0
+
+ '@simple-libs/stream-utils@1.2.0': {}
+
+ '@swc/helpers@0.5.21':
+ dependencies:
+ tslib: 2.8.1
+
+ '@tybys/wasm-util@0.10.1':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@types/chai@5.2.3':
+ dependencies:
+ '@types/deep-eql': 4.0.2
+ assertion-error: 2.0.1
+
+ '@types/deep-eql@4.0.2': {}
+
+ '@types/node@25.6.0':
+ dependencies:
+ undici-types: 7.19.2
+
+ ajv@8.20.0:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-uri: 3.1.0
+ json-schema-traverse: 1.0.0
+ require-from-string: 2.0.2
+
+ ansi-escapes@7.3.0:
+ dependencies:
+ environment: 1.1.0
+
+ ansi-regex@5.0.1: {}
+
+ ansi-regex@6.2.2: {}
+
+ ansi-styles@4.3.0:
+ dependencies:
+ color-convert: 2.0.1
+
+ ansi-styles@6.2.3: {}
+
+ argparse@2.0.1: {}
+
+ array-ify@1.0.0: {}
+
+ assertion-error@2.0.1: {}
+
+ callsites@3.1.0: {}
+
+ cli-cursor@5.0.0:
+ dependencies:
+ restore-cursor: 5.1.0
+
+ cli-truncate@5.2.0:
+ dependencies:
+ slice-ansi: 8.0.0
+ string-width: 8.2.1
+
+ cliui@8.0.1:
+ dependencies:
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ wrap-ansi: 7.0.0
+
+ color-convert@2.0.1:
+ dependencies:
+ color-name: 1.1.4
+
+ color-name@1.1.4: {}
+
+ colorette@2.0.20: {}
+
+ commander@14.0.3: {}
+
+ compare-func@2.0.0:
+ dependencies:
+ array-ify: 1.0.0
+ dot-prop: 5.3.0
+
+ conventional-changelog-angular@8.3.1:
+ dependencies:
+ compare-func: 2.0.0
+
+ conventional-changelog-conventionalcommits@9.3.1:
+ dependencies:
+ compare-func: 2.0.0
+
+ conventional-commits-parser@6.4.0:
+ dependencies:
+ '@simple-libs/stream-utils': 1.2.0
+ meow: 13.2.0
+
+ cosmiconfig-typescript-loader@6.3.0(@types/node@25.6.0)(cosmiconfig@9.0.1(typescript@6.0.3))(typescript@6.0.3):
+ dependencies:
+ '@types/node': 25.6.0
+ cosmiconfig: 9.0.1(typescript@6.0.3)
+ jiti: 2.6.1
+ typescript: 6.0.3
+
+ cosmiconfig@9.0.1(typescript@6.0.3):
+ dependencies:
+ env-paths: 2.2.1
+ import-fresh: 3.3.1
+ js-yaml: 4.1.1
+ parse-json: 5.2.0
+ optionalDependencies:
+ typescript: 6.0.3
+
+ dot-prop@5.3.0:
+ dependencies:
+ is-obj: 2.0.0
+
+ emoji-regex@10.6.0: {}
+
+ emoji-regex@8.0.0: {}
+
+ env-paths@2.2.1: {}
+
+ environment@1.1.0: {}
+
+ error-ex@1.3.4:
+ dependencies:
+ is-arrayish: 0.2.1
+
+ es-toolkit@1.46.1: {}
+
+ escalade@3.2.0: {}
+
+ eventemitter3@5.0.4: {}
+
+ fast-deep-equal@3.1.3: {}
+
+ fast-uri@3.1.0: {}
+
+ get-caller-file@2.0.5: {}
+
+ get-east-asian-width@1.5.0: {}
+
+ git-raw-commits@5.0.1(conventional-commits-parser@6.4.0):
+ dependencies:
+ '@conventional-changelog/git-client': 2.7.0(conventional-commits-parser@6.4.0)
+ meow: 13.2.0
+ transitivePeerDependencies:
+ - conventional-commits-filter
+ - conventional-commits-parser
+
+ global-directory@5.0.0:
+ dependencies:
+ ini: 6.0.0
+
+ husky@9.1.7: {}
+
+ import-fresh@3.3.1:
+ dependencies:
+ parent-module: 1.0.1
+ resolve-from: 4.0.0
+
+ import-meta-resolve@4.2.0: {}
+
+ ini@6.0.0: {}
+
+ is-arrayish@0.2.1: {}
+
+ is-fullwidth-code-point@3.0.0: {}
+
+ is-fullwidth-code-point@5.1.0:
+ dependencies:
+ get-east-asian-width: 1.5.0
+
+ is-obj@2.0.0: {}
+
+ is-plain-obj@4.1.0: {}
+
+ jiti@2.6.1: {}
+
+ js-tokens@4.0.0: {}
+
+ js-yaml@4.1.1:
+ dependencies:
+ argparse: 2.0.1
+
+ json-parse-even-better-errors@2.3.1: {}
+
+ json-schema-traverse@1.0.0: {}
+
+ lines-and-columns@1.2.4: {}
+
+ lint-staged@16.4.0:
+ dependencies:
+ commander: 14.0.3
+ listr2: 9.0.5
+ picomatch: 4.0.4
+ string-argv: 0.3.2
+ tinyexec: 1.1.2
+ yaml: 2.8.3
+
+ listr2@9.0.5:
+ dependencies:
+ cli-truncate: 5.2.0
+ colorette: 2.0.20
+ eventemitter3: 5.0.4
+ log-update: 6.1.0
+ rfdc: 1.4.1
+ wrap-ansi: 9.0.2
+
+ log-update@6.1.0:
+ dependencies:
+ ansi-escapes: 7.3.0
+ cli-cursor: 5.0.0
+ slice-ansi: 7.1.2
+ strip-ansi: 7.2.0
+ wrap-ansi: 9.0.2
+
+ meow@13.2.0: {}
+
+ mimic-function@5.0.1: {}
+
+ minimist@1.2.8: {}
+
+ mri@1.2.0: {}
+
+ onetime@7.0.0:
+ dependencies:
+ mimic-function: 5.0.1
+
+ package-manager-detector@1.6.0: {}
+
+ parent-module@1.0.1:
+ dependencies:
+ callsites: 3.1.0
+
+ parse-json@5.2.0:
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ error-ex: 1.3.4
+ json-parse-even-better-errors: 2.3.1
+ lines-and-columns: 1.2.4
+
+ picocolors@1.1.1: {}
+
+ picomatch@4.0.4: {}
+
+ publint@0.3.18:
+ dependencies:
+ '@publint/pack': 0.1.4
+ package-manager-detector: 1.6.0
+ picocolors: 1.1.1
+ sade: 1.8.1
+
+ require-directory@2.1.1: {}
+
+ require-from-string@2.0.2: {}
+
+ resolve-from@4.0.0: {}
+
+ resolve-from@5.0.0: {}
+
+ restore-cursor@5.1.0:
+ dependencies:
+ onetime: 7.0.0
+ signal-exit: 4.1.0
+
+ rfdc@1.4.1: {}
+
+ rsbuild-plugin-dts@0.21.3(@rsbuild/core@2.0.3)(typescript@6.0.3):
+ dependencies:
+ '@ast-grep/napi': 0.37.0
+ '@rsbuild/core': 2.0.3
+ optionalDependencies:
+ typescript: 6.0.3
+
+ sade@1.8.1:
+ dependencies:
+ mri: 1.2.0
+
+ semver@7.7.4: {}
+
+ signal-exit@4.1.0: {}
+
+ slice-ansi@7.1.2:
+ dependencies:
+ ansi-styles: 6.2.3
+ is-fullwidth-code-point: 5.1.0
+
+ slice-ansi@8.0.0:
+ dependencies:
+ ansi-styles: 6.2.3
+ is-fullwidth-code-point: 5.1.0
+
+ string-argv@0.3.2: {}
+
+ string-width@4.2.3:
+ dependencies:
+ emoji-regex: 8.0.0
+ is-fullwidth-code-point: 3.0.0
+ strip-ansi: 6.0.1
+
+ string-width@7.2.0:
+ dependencies:
+ emoji-regex: 10.6.0
+ get-east-asian-width: 1.5.0
+ strip-ansi: 7.2.0
+
+ string-width@8.2.1:
+ dependencies:
+ get-east-asian-width: 1.5.0
+ strip-ansi: 7.2.0
+
+ strip-ansi@6.0.1:
+ dependencies:
+ ansi-regex: 5.0.1
+
+ strip-ansi@7.2.0:
+ dependencies:
+ ansi-regex: 6.2.2
+
+ tinyexec@1.1.2: {}
+
+ tinypool@2.1.0: {}
+
+ tslib@2.8.1: {}
+
+ typescript@6.0.3: {}
+
+ undici-types@7.19.2: {}
+
+ wrap-ansi@7.0.0:
+ dependencies:
+ ansi-styles: 4.3.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+
+ wrap-ansi@9.0.2:
+ dependencies:
+ ansi-styles: 6.2.3
+ string-width: 7.2.0
+ strip-ansi: 7.2.0
+
+ y18n@5.0.8: {}
+
+ yaml@2.8.3: {}
+
+ yargs-parser@21.1.1: {}
+
+ yargs@17.7.2:
+ dependencies:
+ cliui: 8.0.1
+ escalade: 3.2.0
+ get-caller-file: 2.0.5
+ require-directory: 2.1.1
+ string-width: 4.2.3
+ y18n: 5.0.8
+ yargs-parser: 21.1.1
diff --git a/rslib.config.ts b/rslib.config.ts
new file mode 100644
index 0000000..d16589c
--- /dev/null
+++ b/rslib.config.ts
@@ -0,0 +1,31 @@
+import {defineConfig} from "@rslib/core";
+
+export default defineConfig({
+ source: {
+ entry: {
+ index: "./src/index.ts",
+ },
+ tsconfigPath: "./tsconfig.build.json",
+ },
+ output: {
+ target: "web",
+ cleanDistPath: true,
+ sourceMap: true,
+ },
+ lib: [
+ {
+ format: "esm",
+ syntax: "es2020",
+ dts: {
+ autoExtension: true,
+ },
+ },
+ {
+ format: "cjs",
+ syntax: "es2020",
+ dts: {
+ autoExtension: true,
+ },
+ },
+ ],
+});
diff --git a/rstest.config.ts b/rstest.config.ts
new file mode 100644
index 0000000..0c2b519
--- /dev/null
+++ b/rstest.config.ts
@@ -0,0 +1,6 @@
+import {defineConfig} from "@rstest/core";
+
+export default defineConfig({
+ testEnvironment: "node",
+ include: ["tests/**/*.test.ts"],
+});
diff --git a/src/errors.ts b/src/errors.ts
new file mode 100644
index 0000000..f4b91e4
--- /dev/null
+++ b/src/errors.ts
@@ -0,0 +1,21 @@
+import type {SrcsetValidationIssue} from "./types";
+
+export class SrcsetError extends Error {
+ override name = "SrcsetError";
+}
+
+export class SrcsetValidationError extends SrcsetError {
+ override name = "SrcsetValidationError";
+
+ public constructor(public readonly errors: SrcsetValidationIssue[]) {
+ super(formatValidationMessage(errors));
+ }
+}
+
+function formatValidationMessage(errors: SrcsetValidationIssue[]): string {
+ if (errors.length === 0) {
+ return "Invalid srcset.";
+ }
+
+ return `Invalid srcset: ${errors.map((error) => error.code).join(", ")}.`;
+}
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..9fba6c6
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,16 @@
+export {SrcsetError, SrcsetValidationError} from "./errors";
+export {parse} from "./parse";
+export {stringify} from "./stringify";
+export type {
+ DensityCandidate,
+ DescriptorType,
+ FallbackCandidate,
+ ParseOptions,
+ SrcsetCandidate,
+ SrcsetValidationIssue,
+ StringifyOptions,
+ ValidateOptions,
+ ValidationResult,
+ WidthCandidate,
+} from "./types";
+export {validate} from "./validator";
diff --git a/src/parse.ts b/src/parse.ts
new file mode 100644
index 0000000..811fcb6
--- /dev/null
+++ b/src/parse.ts
@@ -0,0 +1,20 @@
+import {SrcsetValidationError} from "./errors";
+import {parseInternal} from "./parser";
+import type {ParseOptions, SrcsetCandidate} from "./types";
+import {validateParsedCandidates} from "./validator";
+
+export function parse(input: string, options: ParseOptions = {}): SrcsetCandidate[] {
+ const parsed = parseInternal(input);
+
+ if (options.strict) {
+ const result = validateParsedCandidates(parsed, {
+ inputWasEmpty: input.trim().length === 0,
+ });
+
+ if (!result.valid) {
+ throw new SrcsetValidationError(result.errors);
+ }
+ }
+
+ return parsed.map(({candidate}) => candidate);
+}
diff --git a/src/parser.ts b/src/parser.ts
new file mode 100644
index 0000000..cc0f222
--- /dev/null
+++ b/src/parser.ts
@@ -0,0 +1,146 @@
+import type {SrcsetCandidate} from "./types";
+
+export type InternalDescriptor =
+ | {kind: "density"; value: number; raw: string}
+ | {kind: "width"; value: number; raw: string}
+ | {kind: "invalid"; raw: string};
+
+export type InternalCandidate = {
+ candidate: SrcsetCandidate;
+ descriptors: InternalDescriptor[];
+ index: number;
+ raw: string;
+ url: string;
+};
+
+const ASCII_WHITESPACE = /[\t\n\f\r ]/u;
+const DENSITY_DESCRIPTOR = /^(\d+(?:\.\d+)?|\.\d+)x$/u;
+const WIDTH_DESCRIPTOR = /^(\d+)w$/u;
+
+export function parseInternal(input: string): InternalCandidate[] {
+ return splitCandidates(input).map((segment, index) => parseCandidateSegment(segment, index));
+}
+
+function splitCandidates(input: string): string[] {
+ const segments: string[] = [];
+ const length = input.length;
+ let index = 0;
+
+ while (index < length) {
+ while (index < length && isCandidateLeadingIgnored(input[index])) {
+ index += 1;
+ }
+
+ if (index >= length) {
+ break;
+ }
+
+ const start = index;
+ let seenUrlWhitespace = false;
+
+ while (index < length) {
+ const char = input[index];
+
+ if (isAsciiWhitespace(char)) {
+ seenUrlWhitespace = true;
+ index += 1;
+ continue;
+ }
+
+ if (char === "," && (seenUrlWhitespace || isFallbackSeparator(input, index))) {
+ break;
+ }
+
+ index += 1;
+ }
+
+ const segment = input.slice(start, index).trim();
+
+ if (segment.length > 0) {
+ segments.push(segment);
+ }
+
+ if (input[index] === ",") {
+ index += 1;
+ }
+ }
+
+ return segments;
+}
+
+function parseCandidateSegment(segment: string, index: number): InternalCandidate {
+ const [url = "", ...descriptorTokens] = segment.split(ASCII_WHITESPACE).filter(Boolean);
+ const descriptors = descriptorTokens.map(parseDescriptor);
+ const descriptor = descriptors[0];
+
+ return {
+ candidate: buildCandidate(url, descriptors.length === 1 ? descriptor : undefined),
+ descriptors,
+ index,
+ raw: segment,
+ url,
+ };
+}
+
+function parseDescriptor(token: string): InternalDescriptor {
+ const width = WIDTH_DESCRIPTOR.exec(token);
+
+ if (width) {
+ return {
+ kind: "width",
+ raw: token,
+ value: Number(width[1]),
+ };
+ }
+
+ const density = DENSITY_DESCRIPTOR.exec(token);
+
+ if (density) {
+ return {
+ kind: "density",
+ raw: token,
+ value: Number(density[1]),
+ };
+ }
+
+ return {
+ kind: "invalid",
+ raw: token,
+ };
+}
+
+function buildCandidate(url: string, descriptor?: InternalDescriptor): SrcsetCandidate {
+ if (descriptor?.kind === "density") {
+ return {
+ url,
+ density: descriptor.value,
+ };
+ }
+
+ if (descriptor?.kind === "width") {
+ return {
+ url,
+ width: descriptor.value,
+ };
+ }
+
+ return {url};
+}
+
+function isAsciiWhitespace(char: string | undefined): boolean {
+ return char !== undefined && ASCII_WHITESPACE.test(char);
+}
+
+function isCandidateLeadingIgnored(char: string | undefined): boolean {
+ return char === "," || isAsciiWhitespace(char);
+}
+
+function isFallbackSeparator(input: string, commaIndex: number): boolean {
+ let index = commaIndex + 1;
+
+ while (index < input.length && isAsciiWhitespace(input[index])) {
+ index += 1;
+ }
+
+ return index === input.length || index > commaIndex + 1;
+}
diff --git a/src/stringify.ts b/src/stringify.ts
new file mode 100644
index 0000000..45b12ac
--- /dev/null
+++ b/src/stringify.ts
@@ -0,0 +1,31 @@
+import {SrcsetValidationError} from "./errors";
+import type {SrcsetCandidate, StringifyOptions} from "./types";
+import {validate} from "./validator";
+
+export function stringify(candidates: SrcsetCandidate[], options: StringifyOptions = {}): string {
+ if (options.strict) {
+ const result = validate(candidates);
+
+ if (!result.valid) {
+ throw new SrcsetValidationError(result.errors);
+ }
+ }
+
+ return candidates.map(stringifyCandidate).join(", ");
+}
+
+function stringifyCandidate(candidate: SrcsetCandidate): string {
+ if ("density" in candidate) {
+ return `${candidate.url} ${formatNumber(candidate.density)}x`;
+ }
+
+ if ("width" in candidate) {
+ return `${candidate.url} ${formatNumber(candidate.width)}w`;
+ }
+
+ return candidate.url;
+}
+
+function formatNumber(value: number): string {
+ return String(value);
+}
diff --git a/src/types.ts b/src/types.ts
new file mode 100644
index 0000000..361197a
--- /dev/null
+++ b/src/types.ts
@@ -0,0 +1,58 @@
+export type DensityCandidate = {
+ url: string;
+ density: number;
+};
+
+export type WidthCandidate = {
+ url: string;
+ width: number;
+};
+
+export type FallbackCandidate = {
+ url: string;
+};
+
+export type SrcsetCandidate = DensityCandidate | WidthCandidate | FallbackCandidate;
+
+export type ParseOptions = {
+ strict?: boolean;
+};
+
+export type ValidateOptions = {
+ baseUrl?: string | URL;
+ descriptor?: "width" | "density";
+};
+
+export type StringifyOptions = {
+ strict?: boolean;
+};
+
+export type DescriptorType = "density" | "width" | "none";
+
+export type SrcsetValidationIssue = {
+ code:
+ | "empty-srcset"
+ | "invalid-url"
+ | "invalid-descriptor"
+ | "duplicate-descriptor"
+ | "mixed-descriptors"
+ | "mismatched-descriptor"
+ | "multiple-descriptors";
+ message: string;
+ candidate?: string;
+ index?: number;
+};
+
+export type ValidationResult =
+ | {
+ valid: true;
+ candidates: SrcsetCandidate[];
+ descriptor: DescriptorType;
+ errors: [];
+ }
+ | {
+ valid: false;
+ candidates: SrcsetCandidate[];
+ descriptor: DescriptorType;
+ errors: SrcsetValidationIssue[];
+ };
diff --git a/src/validator.ts b/src/validator.ts
new file mode 100644
index 0000000..b7e5d89
--- /dev/null
+++ b/src/validator.ts
@@ -0,0 +1,364 @@
+import {type InternalCandidate, parseInternal} from "./parser";
+import type {
+ DescriptorType,
+ SrcsetCandidate,
+ SrcsetValidationIssue,
+ ValidateOptions,
+ ValidationResult,
+} from "./types";
+
+type ParsedValidationOptions = ValidateOptions & {
+ inputWasEmpty?: boolean;
+};
+
+type RuntimeDescriptor =
+ | {kind: "density"; value: number}
+ | {kind: "width"; value: number}
+ | {kind: "none"}
+ | {kind: "invalid"};
+
+type NormalizedCandidate = {
+ descriptor: RuntimeDescriptor;
+ index: number;
+ raw: string;
+ url: string;
+};
+
+type IssueCode = SrcsetValidationIssue["code"];
+
+export function validate(
+ input: string | SrcsetCandidate[],
+ options: ValidateOptions = {},
+): ValidationResult {
+ if (typeof input === "string") {
+ const parsed = parseInternal(input);
+
+ return validateNormalized(
+ parsed.map(normalizeParsedCandidate),
+ parsed.map(({candidate}) => candidate),
+ options,
+ input.trim().length === 0,
+ parsed.flatMap(descriptorIssues),
+ false,
+ );
+ }
+
+ return validateNormalized(
+ input.map(normalizeCandidateObject),
+ input,
+ options,
+ input.length === 0,
+ );
+}
+
+export function validateParsedCandidates(
+ parsed: InternalCandidate[],
+ options: ParsedValidationOptions = {},
+): ValidationResult {
+ return validateNormalized(
+ parsed.map(normalizeParsedCandidate),
+ parsed.map(({candidate}) => candidate),
+ options,
+ options.inputWasEmpty,
+ parsed.flatMap(descriptorIssues),
+ false,
+ );
+}
+
+function validateNormalized(
+ normalized: NormalizedCandidate[],
+ candidates: SrcsetCandidate[],
+ options: ValidateOptions,
+ inputWasEmpty = false,
+ parserIssues: SrcsetValidationIssue[] = [],
+ includeObjectShapeIssues = true,
+): ValidationResult {
+ const errors = [
+ ...emptySrcsetIssues(normalized, inputWasEmpty),
+ ...parserIssues,
+ ...(includeObjectShapeIssues ? objectShapeIssues(normalized) : []),
+ ...urlIssues(normalized, options.baseUrl),
+ ...descriptorValueIssues(normalized),
+ ...descriptorSetIssues(normalized, options),
+ ];
+
+ return buildValidationResult(candidates, getDescriptorType(normalized), errors);
+}
+
+function descriptorIssues(candidate: InternalCandidate): SrcsetValidationIssue[] {
+ if (candidate.descriptors.length > 1) {
+ return [
+ issue(
+ "multiple-descriptors",
+ "A srcset candidate must not contain multiple descriptors.",
+ candidate,
+ ),
+ ];
+ }
+
+ const descriptor = candidate.descriptors[0];
+
+ return descriptor?.kind === "invalid"
+ ? [issue("invalid-descriptor", `Invalid srcset descriptor "${descriptor.raw}".`, candidate)]
+ : [];
+}
+
+function normalizeParsedCandidate(candidate: InternalCandidate): NormalizedCandidate {
+ const descriptor = candidate.descriptors[0];
+
+ return {
+ descriptor:
+ candidate.descriptors.length === 1 && descriptor !== undefined
+ ? toRuntimeDescriptor(
+ descriptor.kind,
+ "value" in descriptor ? descriptor.value : undefined,
+ )
+ : {kind: "none"},
+ index: candidate.index,
+ raw: candidate.raw,
+ url: candidate.url,
+ };
+}
+
+function normalizeCandidateObject(candidate: SrcsetCandidate, index: number): NormalizedCandidate {
+ const record = candidate as SrcsetCandidate & {
+ density?: unknown;
+ width?: unknown;
+ url?: unknown;
+ };
+ const hasDensity = "density" in record;
+ const hasWidth = "width" in record;
+
+ return {
+ descriptor:
+ hasDensity && hasWidth
+ ? {kind: "invalid"}
+ : hasDensity
+ ? toRuntimeDescriptor("density", record.density)
+ : hasWidth
+ ? toRuntimeDescriptor("width", record.width)
+ : {kind: "none"},
+ index,
+ raw: formatCandidate(candidate),
+ url: typeof record.url === "string" ? record.url : "",
+ };
+}
+
+function toRuntimeDescriptor(
+ kind: "density" | "width" | "invalid",
+ value: unknown,
+): RuntimeDescriptor {
+ if (kind === "invalid" || typeof value !== "number") {
+ return {kind: "invalid"};
+ }
+
+ return {kind, value};
+}
+
+function emptySrcsetIssues(
+ candidates: NormalizedCandidate[],
+ inputWasEmpty: boolean,
+): SrcsetValidationIssue[] {
+ return inputWasEmpty || candidates.length === 0
+ ? [issue("empty-srcset", "The srcset attribute must contain at least one candidate.")]
+ : [];
+}
+
+function objectShapeIssues(candidates: NormalizedCandidate[]): SrcsetValidationIssue[] {
+ return candidates
+ .filter(({descriptor}) => descriptor.kind === "invalid")
+ .map((candidate) =>
+ issue("invalid-descriptor", "Invalid srcset candidate descriptor.", candidate),
+ );
+}
+
+function urlIssues(
+ candidates: NormalizedCandidate[],
+ baseUrl?: string | URL,
+): SrcsetValidationIssue[] {
+ return candidates.flatMap((candidate) => {
+ if (candidate.url.trim().length === 0) {
+ return [issue("invalid-url", "A srcset candidate URL must not be empty.", candidate)];
+ }
+
+ if (baseUrl === undefined || typeof URL === "undefined") {
+ return [];
+ }
+
+ try {
+ new URL(candidate.url, baseUrl);
+ return [];
+ } catch {
+ return [
+ issue(
+ "invalid-url",
+ "A srcset candidate URL is not valid relative to the base URL.",
+ candidate,
+ ),
+ ];
+ }
+ });
+}
+
+function descriptorValueIssues(candidates: NormalizedCandidate[]): SrcsetValidationIssue[] {
+ return candidates.flatMap((candidate) => {
+ const {descriptor} = candidate;
+
+ if (
+ descriptor.kind === "width" &&
+ (!Number.isInteger(descriptor.value) || descriptor.value <= 0)
+ ) {
+ return [
+ issue(
+ "invalid-descriptor",
+ "Width descriptors must be positive integers.",
+ candidate,
+ ),
+ ];
+ }
+
+ if (
+ descriptor.kind === "density" &&
+ (!Number.isFinite(descriptor.value) || descriptor.value <= 0)
+ ) {
+ return [
+ issue(
+ "invalid-descriptor",
+ "Density descriptors must be positive finite numbers.",
+ candidate,
+ ),
+ ];
+ }
+
+ return [];
+ });
+}
+
+function descriptorSetIssues(
+ candidates: NormalizedCandidate[],
+ options: ValidateOptions,
+): SrcsetValidationIssue[] {
+ const seen = new Map();
+ const kinds = new Set();
+ const issues: SrcsetValidationIssue[] = [];
+
+ for (const candidate of candidates) {
+ kinds.add(candidate.descriptor.kind);
+
+ if (options.descriptor === "width" && candidate.descriptor.kind !== "width") {
+ issues.push(
+ issue(
+ "mismatched-descriptor",
+ "Every candidate must use a width descriptor.",
+ candidate,
+ ),
+ );
+ }
+
+ if (
+ options.descriptor === "density" &&
+ candidate.descriptor.kind !== "density" &&
+ candidate.descriptor.kind !== "none"
+ ) {
+ issues.push(
+ issue(
+ "mismatched-descriptor",
+ "Every candidate must use a density descriptor.",
+ candidate,
+ ),
+ );
+ }
+
+ const key = getDescriptorKey(candidate.descriptor);
+
+ if (key === undefined) {
+ continue;
+ }
+
+ if (seen.has(key)) {
+ issues.push(
+ issue(
+ "duplicate-descriptor",
+ `Duplicate srcset descriptor "${formatDescriptorKey(key)}".`,
+ candidate,
+ ),
+ );
+ } else {
+ seen.set(key, candidate);
+ }
+ }
+
+ if (kinds.has("width") && (kinds.has("density") || kinds.has("none"))) {
+ issues.push(
+ issue(
+ "mixed-descriptors",
+ "Width descriptors must not be mixed with density or fallback candidates.",
+ ),
+ );
+ }
+
+ return issues;
+}
+
+function getDescriptorKey(descriptor: RuntimeDescriptor): string | undefined {
+ if (descriptor.kind === "invalid") {
+ return undefined;
+ }
+
+ return descriptor.kind === "none" ? "density:1" : `${descriptor.kind}:${descriptor.value}`;
+}
+
+function formatDescriptorKey(key: string): string {
+ const [kind, value] = key.split(":");
+
+ return kind === "width" ? `${value}w` : `${value}x`;
+}
+
+function getDescriptorType(candidates: NormalizedCandidate[]): DescriptorType {
+ if (candidates.some(({descriptor}) => descriptor.kind === "width")) {
+ return "width";
+ }
+
+ if (candidates.some(({descriptor}) => descriptor.kind === "density")) {
+ return "density";
+ }
+
+ return "none";
+}
+
+function buildValidationResult(
+ candidates: SrcsetCandidate[],
+ descriptor: DescriptorType,
+ errors: SrcsetValidationIssue[],
+): ValidationResult {
+ return errors.length === 0
+ ? {candidates, descriptor, errors: [], valid: true}
+ : {candidates, descriptor, errors, valid: false};
+}
+
+function issue(
+ code: IssueCode,
+ message: string,
+ candidate?: Pick,
+): SrcsetValidationIssue {
+ return candidate === undefined
+ ? {code, message}
+ : {candidate: candidate.raw, code, index: candidate.index, message};
+}
+
+function formatCandidate(candidate: SrcsetCandidate): string {
+ const record = candidate as SrcsetCandidate & {
+ density?: unknown;
+ width?: unknown;
+ };
+
+ if ("width" in record) {
+ return `${record.url} ${String(record.width)}w`;
+ }
+
+ if ("density" in record) {
+ return `${record.url} ${String(record.density)}x`;
+ }
+
+ return record.url;
+}
diff --git a/tests/index.test.ts b/tests/index.test.ts
new file mode 100644
index 0000000..85c3b4b
--- /dev/null
+++ b/tests/index.test.ts
@@ -0,0 +1,307 @@
+import {describe, expect, test} from "@rstest/core";
+
+import {parse, type SrcsetCandidate, SrcsetValidationError, stringify, validate} from "../src";
+
+function expectInvalidCodes(input: string | SrcsetCandidate[], codes: string[]) {
+ const result = validate(input);
+
+ expect(result.valid).toBe(false);
+ expect(result.errors.map(({code}) => code)).toEqual(codes);
+}
+
+describe("public API", () => {
+ test("exports named functions", () => {
+ expect(typeof parse).toBe("function");
+ expect(typeof validate).toBe("function");
+ expect(typeof stringify).toBe("function");
+ });
+});
+
+describe("parse", () => {
+ test("parses empty and whitespace-only strings as no candidates", () => {
+ expect(parse("")).toEqual([]);
+ expect(parse(" \n\t ")).toEqual([]);
+ });
+
+ test("parses a single URL without descriptor", () => {
+ expect(parse("image.png")).toEqual([{url: "image.png"}]);
+ });
+
+ test("parses a single URL with 1x density", () => {
+ expect(parse("image.png 1x")).toEqual([{url: "image.png", density: 1}]);
+ });
+
+ test("parses a single URL with decimal density", () => {
+ expect(parse("image.png 1.5x")).toEqual([{url: "image.png", density: 1.5}]);
+ });
+
+ test("parses multiple density candidates", () => {
+ expect(parse("image.png 1x, image@2x.png 2x")).toEqual([
+ {url: "image.png", density: 1},
+ {url: "image@2x.png", density: 2},
+ ]);
+ });
+
+ test("parses multiple width candidates", () => {
+ expect(parse("small.png 640w, large.png 1280w")).toEqual([
+ {url: "small.png", width: 640},
+ {url: "large.png", width: 1280},
+ ]);
+ });
+
+ test("preserves relative URLs", () => {
+ expect(parse("../image.png 1x, /image@2x.png 2x")).toEqual([
+ {url: "../image.png", density: 1},
+ {url: "/image@2x.png", density: 2},
+ ]);
+ });
+
+ test("preserves absolute URLs", () => {
+ expect(parse("https://example.com/image.png 1x")).toEqual([
+ {url: "https://example.com/image.png", density: 1},
+ ]);
+ });
+
+ test("preserves query strings", () => {
+ expect(parse("/image.png?size=1,2&format=webp 1x, /image@2x.png?format=webp 2x")).toEqual([
+ {url: "/image.png?size=1,2&format=webp", density: 1},
+ {url: "/image@2x.png?format=webp", density: 2},
+ ]);
+ });
+
+ test("preserves data URL commas", () => {
+ expect(parse("data:image/png;base64,AAAA 1x, /image@2x.png 2x")).toEqual([
+ {url: "data:image/png;base64,AAAA", density: 1},
+ {url: "/image@2x.png", density: 2},
+ ]);
+ });
+
+ test("preserves URL-encoded SVG data URLs", () => {
+ expect(parse("data:image/svg+xml,%3Csvg%3E%3C/svg%3E 1x, /image.svg 2x")).toEqual([
+ {url: "data:image/svg+xml,%3Csvg%3E%3C/svg%3E", density: 1},
+ {url: "/image.svg", density: 2},
+ ]);
+ });
+
+ test("handles leading whitespace, trailing whitespace, spaces, and newlines", () => {
+ expect(parse(" \n image.png 1x,\n\timage@2x.png \t 2x ")).toEqual([
+ {url: "image.png", density: 1},
+ {url: "image@2x.png", density: 2},
+ ]);
+ });
+
+ test("is tolerant of invalid descriptor sets by default", () => {
+ expect(parse("image.png 1x 640w, image@2x.png 2x")).toEqual([
+ {url: "image.png"},
+ {url: "image@2x.png", density: 2},
+ ]);
+ });
+
+ test("throws validation errors in strict mode", () => {
+ expect(() => parse("image.png 1x, image@1x.png 1x", {strict: true})).toThrow(
+ SrcsetValidationError,
+ );
+ });
+
+ test("strict mode validates descriptor rules without URL context", () => {
+ expect(parse("/image.png 1x", {strict: true})).toEqual([{url: "/image.png", density: 1}]);
+ });
+
+ test("preserves URLs with fragments", () => {
+ expect(parse("image.png#section 1x")).toEqual([{url: "image.png#section", density: 1}]);
+ });
+
+ test("preserves URLs with ports", () => {
+ expect(parse("http://localhost:3000/image.png 2x")).toEqual([
+ {url: "http://localhost:3000/image.png", density: 2},
+ ]);
+ });
+
+ test("skips empty entries between commas", () => {
+ const result = parse("image.png 1x, , image2.png 2x");
+
+ expect(result).toEqual([
+ {url: "image.png", density: 1},
+ {url: "image2.png", density: 2},
+ ]);
+ });
+});
+
+describe("validate", () => {
+ test("accepts valid string srcsets", () => {
+ for (const input of [
+ "a.png",
+ "a.png 1x, b.png 2x",
+ "a.png 1.5x, b.png 2x",
+ "a.png 640w, b.png 1280w",
+ "data:image/png;base64,AAAA 1x, /image@2x.png 2x",
+ ]) {
+ expect(validate(input).valid).toBe(true);
+ }
+ });
+
+ test("returns the detected descriptor type", () => {
+ expect(validate("a.png").descriptor).toBe("none");
+ expect(validate("a.png 1x, b.png 2x").descriptor).toBe("density");
+ expect(validate("a.png 640w, b.png 1280w").descriptor).toBe("width");
+ });
+
+ test("reports invalid string srcsets with stable codes", () => {
+ expectInvalidCodes("", ["empty-srcset"]);
+ expectInvalidCodes(" ", ["empty-srcset"]);
+ expectInvalidCodes("a.png 1x, b.png 1x", ["duplicate-descriptor"]);
+ expectInvalidCodes("a.png, b.png 1x", ["duplicate-descriptor"]);
+ expectInvalidCodes("a.png 640w, b.png 2x", ["mixed-descriptors"]);
+ expectInvalidCodes("a.png 0w", ["invalid-descriptor"]);
+ expectInvalidCodes("a.png -1x", ["invalid-descriptor"]);
+ expectInvalidCodes("a.png 0x", ["invalid-descriptor"]);
+ expectInvalidCodes("a.png Infinityx", ["invalid-descriptor"]);
+ expectInvalidCodes("a.png 1x 640w", ["multiple-descriptors"]);
+ expectInvalidCodes("a.png 640w, b.png", ["mixed-descriptors"]);
+ });
+
+ test("requires width descriptors when descriptor is 'width'", () => {
+ expect(validate("a.png 640w, b.png 1280w", {descriptor: "width"}).valid).toBe(true);
+
+ const result = validate("a.png 1x, b.png 2x", {descriptor: "width"});
+
+ expect(result.valid).toBe(false);
+ expect(result.errors.map(({code}) => code)).toEqual([
+ "mismatched-descriptor",
+ "mismatched-descriptor",
+ ]);
+ });
+
+ test("requires density descriptors when descriptor is 'density'", () => {
+ expect(validate("a.png 1x, b.png 2x", {descriptor: "density"}).valid).toBe(true);
+
+ const result = validate("a.png 640w, b.png 1280w", {descriptor: "density"});
+
+ expect(result.valid).toBe(false);
+ expect(result.errors.map(({code}) => code)).toEqual([
+ "mismatched-descriptor",
+ "mismatched-descriptor",
+ ]);
+ });
+
+ test("allows fallback candidates with descriptor 'density'", () => {
+ expect(validate("a.png, b.png 2x", {descriptor: "density"}).valid).toBe(true);
+ });
+
+ test("validates parsed candidate arrays", () => {
+ expect(
+ validate([
+ {url: "a.png", density: 1},
+ {url: "b.png", density: 2},
+ ]).valid,
+ ).toBe(true);
+
+ const result = validate([
+ {url: "a.png", width: 640},
+ {url: "b.png", density: 2},
+ ]);
+
+ expect(result.valid).toBe(false);
+ expect(result.errors.map(({code}) => code)).toEqual(["mixed-descriptors"]);
+ });
+
+ test("validates URLs relative to baseUrl when supplied", () => {
+ expect(validate("/image.png 1x", {baseUrl: "https://example.com"}).valid).toBe(true);
+ expect(validate("/image.png 1x", {baseUrl: "not a url"}).errors[0]?.code).toBe(
+ "invalid-url",
+ );
+ });
+
+ test("rejects candidate objects with NaN width", () => {
+ expectInvalidCodes([{url: "a.png", width: NaN}], ["invalid-descriptor"]);
+ });
+
+ test("rejects candidate objects with Infinity density", () => {
+ expectInvalidCodes([{url: "a.png", density: Infinity}], ["invalid-descriptor"]);
+ });
+
+ test("rejects candidate objects with negative width", () => {
+ expectInvalidCodes([{url: "a.png", width: -100}], ["invalid-descriptor"]);
+ });
+
+ test("rejects candidate objects with empty url", () => {
+ expectInvalidCodes([{url: "", width: 640}], ["invalid-url"]);
+ });
+});
+
+describe("stringify", () => {
+ test("serializes empty candidates", () => {
+ expect(stringify([])).toBe("");
+ });
+
+ test("serializes fallback candidates", () => {
+ expect(stringify([{url: "image.png"}])).toBe("image.png");
+ });
+
+ test("serializes density candidates", () => {
+ expect(stringify([{url: "image.png", density: 1}])).toBe("image.png 1x");
+ });
+
+ test("serializes decimal density candidates", () => {
+ expect(stringify([{url: "image.png", density: 1.5}])).toBe("image.png 1.5x");
+ });
+
+ test("serializes width candidates", () => {
+ expect(stringify([{url: "image.png", width: 640}])).toBe("image.png 640w");
+ });
+
+ test("joins multiple candidates with comma-space", () => {
+ expect(
+ stringify([
+ {url: "image.png", density: 1},
+ {url: "image@2x.png", density: 2},
+ ]),
+ ).toBe("image.png 1x, image@2x.png 2x");
+ });
+
+ test("preserves data URLs", () => {
+ expect(stringify([{url: "data:image/png;base64,AAAA", density: 1}])).toBe(
+ "data:image/png;base64,AAAA 1x",
+ );
+ });
+
+ test("preserves relative URL strings", () => {
+ expect(stringify([{url: "../image.png", density: 1}])).toBe("../image.png 1x");
+ });
+
+ test("throws in strict mode for invalid candidates", () => {
+ expect(() =>
+ stringify(
+ [
+ {url: "a.png", density: 1},
+ {url: "b.png", density: 1},
+ ],
+ {strict: true},
+ ),
+ ).toThrow(SrcsetValidationError);
+ });
+
+ test("round trips parsed strings with normalized whitespace", () => {
+ expect(stringify(parse(" image.png 1x,\n image@2x.png 2x "))).toBe(
+ "image.png 1x, image@2x.png 2x",
+ );
+ });
+
+ test("round trips candidates through stringify and parse", () => {
+ const candidates: SrcsetCandidate[] = [
+ {url: "data:image/png;base64,AAAA", density: 1},
+ {url: "/image@2x.png", density: 2},
+ ];
+
+ expect(parse(stringify(candidates))).toEqual(candidates);
+ });
+
+ test("round trips data URLs and query strings with commas", () => {
+ const candidates: SrcsetCandidate[] = [
+ {url: "data:image/png;base64,AAAA+BB==", density: 1},
+ {url: "/image.png?w=100,h=200&fmt=webp", density: 2},
+ ];
+
+ expect(parse(stringify(candidates))).toEqual(candidates);
+ });
+});
diff --git a/tsconfig.build.json b/tsconfig.build.json
new file mode 100644
index 0000000..c30a6f5
--- /dev/null
+++ b/tsconfig.build.json
@@ -0,0 +1,9 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "rootDir": "src",
+ "noEmit": false
+ },
+ "include": ["src/**/*.ts"],
+ "exclude": ["tests/**/*.ts", "*.config.ts"]
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..ddfd14c
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "lib": ["ES2020", "DOM"],
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "strict": true,
+ "isolatedModules": true,
+ "verbatimModuleSyntax": true,
+ "skipLibCheck": true,
+ "noEmit": true,
+ "declaration": true,
+ "declarationMap": true
+ },
+ "include": ["src/**/*.ts", "tests/**/*.ts", "*.config.ts"]
+}