From b89c78ccd0d3ff7b1783039e564cef6017712ac0 Mon Sep 17 00:00:00 2001 From: Anjey Tsibylskij <130153594+atldays@users.noreply.github.com> Date: Fri, 1 May 2026 23:57:55 +0300 Subject: [PATCH 01/12] feat: initialize project with npm package configuration and setup files --- .github/workflows/ci.yml | 37 + .github/workflows/release.yml | 54 ++ .gitignore | 16 + .husky/commit-msg | 2 + .husky/pre-commit | 2 + .husky/pre-push | 2 + .npmrc | 3 + .nvmrc | 2 + CHANGELOG.md | 6 + CONTRIBUTING.md | 60 ++ LICENSE | 22 + README.md | 34 + biome.json | 32 + commitlint.config.cjs | 3 + package.json | 70 ++ pnpm-lock.yaml | 1448 +++++++++++++++++++++++++++++++++ rslib.config.ts | 31 + rstest.config.ts | 6 + src/index.ts | 2 + tests/index.test.ts | 7 + tsconfig.build.json | 9 + tsconfig.json | 16 + 22 files changed, 1864 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100755 .husky/commit-msg create mode 100755 .husky/pre-commit create mode 100755 .husky/pre-push create mode 100644 .npmrc create mode 100644 .nvmrc create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 biome.json create mode 100644 commitlint.config.cjs create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 rslib.config.ts create mode 100644 rstest.config.ts create mode 100644 src/index.ts create mode 100644 tests/index.test.ts create mode 100644 tsconfig.build.json create mode 100644 tsconfig.json 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..1404181 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,54 @@ +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 + package-name: srcset-kit + + - 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: 22 + cache: pnpm + registry-url: https://registry.npmjs.org/ + + - 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: pnpm publish --provenance --access public + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ee8cef1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +.DS_Store +.idea/ + +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..d6d8795 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +This project uses Conventional Commits and Release Please to generate releases. + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8d68d46 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,60 @@ +# Contributing + +## Requirements + +- Node.js 22 or newer for local development. +- pnpm 10 or newer. + +## Local checks + +```sh +pnpm install +pnpm run verify +``` + +Use focused commands while developing: + +```sh +pnpm run test +pnpm run build +pnpm run typecheck +pnpm run format +``` + +## Commits + +This repository uses Conventional Commits. Examples: + +```text +feat: add srcset parser +fix: keep descriptor whitespace valid +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, for example: + +```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. + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7557cce --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2026 Atldays + +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..939398b --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# srcset-kit + +Tools for working with the HTML `srcset` attribute. + +## Installation + +```sh +pnpm add srcset-kit +``` + +## Usage + +The package scaffold is ready for production npm publishing. The public `srcset` +API will be added in the next implementation pass. + +```ts +import {} from "srcset-kit"; +``` + +## Development + +```sh +pnpm install +pnpm run verify +``` + +## Package + +- Runtime dependencies: none. +- Source language: TypeScript. +- Build tool: Rslib. +- Test runner: Rstest. +- Published formats: ESM, CommonJS, and TypeScript declarations. + diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..77c6b90 --- /dev/null +++ b/biome.json @@ -0,0 +1,32 @@ +{ + "$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": { + "quoteStyle": "double", + "semicolons": "always", + "trailingCommas": "all" + } + }, + "json": { + "formatter": { + "trailingCommas": "none" + } + } +} diff --git a/commitlint.config.cjs b/commitlint.config.cjs new file mode 100644 index 0000000..69b4242 --- /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..1ca814d --- /dev/null +++ b/package.json @@ -0,0 +1,70 @@ +{ + "name": "srcset-kit", + "version": "0.0.0", + "description": "Tools for working with the HTML srcset attribute.", + "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+ssh://git@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..21c9b0c --- /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..ad772a4 --- /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/index.ts b/src/index.ts new file mode 100644 index 0000000..fc35cac --- /dev/null +++ b/src/index.ts @@ -0,0 +1,2 @@ +// Public entrypoint. The srcset API will be added in the implementation pass. +export {}; diff --git a/tests/index.test.ts b/tests/index.test.ts new file mode 100644 index 0000000..3a8ed5c --- /dev/null +++ b/tests/index.test.ts @@ -0,0 +1,7 @@ +import { expect, test } from "@rstest/core"; + +import * as srcsetKit from "../src/index"; + +test("exports a loadable public module", () => { + expect(srcsetKit).toBeDefined(); +}); 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..de8aad3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020"], + "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"] +} From a3b789a7a11cf75326f697785fe58583ffd1a5d2 Mon Sep 17 00:00:00 2001 From: Anjey Tsibylskij <130153594+atldays@users.noreply.github.com> Date: Sat, 2 May 2026 00:05:30 +0300 Subject: [PATCH 02/12] chore(package): prepare initial npm release with version 0.0.1 --- CHANGELOG.md | 9 +++++++++ package.json | 10 ++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6d8795..388e394 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,3 +4,12 @@ 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/package.json b/package.json index 1ca814d..eea9ba2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,13 @@ { "name": "srcset-kit", - "version": "0.0.0", + "version": "0.0.1", "description": "Tools for working with the HTML srcset attribute.", + "keywords": [ + "srcset", + "responsive-images", + "html", + "typescript" + ], "type": "module", "license": "MIT", "author": { @@ -11,7 +17,7 @@ "homepage": "https://github.com/atldays/srcset-kit#readme", "repository": { "type": "git", - "url": "git+ssh://git@github.com/atldays/srcset-kit.git" + "url": "git+https://github.com/atldays/srcset-kit.git" }, "bugs": { "url": "https://github.com/atldays/srcset-kit/issues" From c604c7de31e275aca9c80c230ad548523eb25961 Mon Sep 17 00:00:00 2001 From: Anjey Tsibylskij <130153594+atldays@users.noreply.github.com> Date: Sat, 2 May 2026 00:13:35 +0300 Subject: [PATCH 03/12] ci: update Node.js to v24 and switch npm publish configuration --- .github/workflows/release.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1404181..6e5613e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,10 +36,14 @@ jobs: if: ${{ steps.release.outputs.release_created }} uses: actions/setup-node@v4 with: - node-version: 22 + 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 @@ -50,5 +54,6 @@ jobs: - name: Publish to npm if: ${{ steps.release.outputs.release_created }} - run: pnpm publish --provenance --access public - + run: npm publish --access public + env: + HUSKY: 0 From 1a1e8996467c51ffbeb615d7f71096e71b787a8b Mon Sep 17 00:00:00 2001 From: Anjey Tsibylskij <130153594+atldays@users.noreply.github.com> Date: Sat, 2 May 2026 01:07:10 +0300 Subject: [PATCH 04/12] feat: update formatter configurations for JavaScript and JSON files - Set `bracketSpacing` and `indentWidth` for JavaScript formatter. - Defined `indentWidth` for JSON formatter. --- biome.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/biome.json b/biome.json index 77c6b90..1ca02c4 100644 --- a/biome.json +++ b/biome.json @@ -19,6 +19,8 @@ }, "javascript": { "formatter": { + "bracketSpacing": false, + "indentWidth": 4, "quoteStyle": "double", "semicolons": "always", "trailingCommas": "all" @@ -26,6 +28,7 @@ }, "json": { "formatter": { + "indentWidth": 2, "trailingCommas": "none" } } From 72c3844c32ead6d1ba99ecdb4b2904ad60ed2bde Mon Sep 17 00:00:00 2001 From: Anjey Tsibylskij <130153594+atldays@users.noreply.github.com> Date: Sat, 2 May 2026 01:13:36 +0300 Subject: [PATCH 05/12] feat: implement srcset parsing, validation, and stringification - Added `parse` function for parsing srcset strings. - Added `stringify` function for serializing srcset candidates. - Added `validate` function for srcset candidate validation. - Introduced corresponding types and errors for better type safety. - Covered functionality with comprehensive tests. --- commitlint.config.cjs | 2 +- rslib.config.ts | 52 +++---- rstest.config.ts | 6 +- src/errors.ts | 25 +++ src/index.ts | 30 +++- src/parse.ts | 20 +++ src/parser.ts | 146 ++++++++++++++++++ src/stringify.ts | 31 ++++ src/types.ts | 58 +++++++ src/validator.ts | 350 ++++++++++++++++++++++++++++++++++++++++++ tests/index.test.ts | 248 +++++++++++++++++++++++++++++- tsconfig.json | 2 +- 12 files changed, 933 insertions(+), 37 deletions(-) create mode 100644 src/errors.ts create mode 100644 src/parse.ts create mode 100644 src/parser.ts create mode 100644 src/stringify.ts create mode 100644 src/types.ts create mode 100644 src/validator.ts diff --git a/commitlint.config.cjs b/commitlint.config.cjs index 69b4242..db74d15 100644 --- a/commitlint.config.cjs +++ b/commitlint.config.cjs @@ -1,3 +1,3 @@ module.exports = { - extends: ["@commitlint/config-conventional"], + extends: ["@commitlint/config-conventional"], }; diff --git a/rslib.config.ts b/rslib.config.ts index 21c9b0c..d16589c 100644 --- a/rslib.config.ts +++ b/rslib.config.ts @@ -1,31 +1,31 @@ -import { defineConfig } from "@rslib/core"; +import {defineConfig} from "@rslib/core"; export default defineConfig({ - source: { - entry: { - index: "./src/index.ts", + source: { + entry: { + index: "./src/index.ts", + }, + tsconfigPath: "./tsconfig.build.json", }, - tsconfigPath: "./tsconfig.build.json", - }, - output: { - target: "web", - cleanDistPath: true, - sourceMap: true, - }, - lib: [ - { - format: "esm", - syntax: "es2020", - dts: { - autoExtension: true, - }, + output: { + target: "web", + cleanDistPath: true, + sourceMap: true, }, - { - format: "cjs", - syntax: "es2020", - dts: { - autoExtension: 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 index ad772a4..0c2b519 100644 --- a/rstest.config.ts +++ b/rstest.config.ts @@ -1,6 +1,6 @@ -import { defineConfig } from "@rstest/core"; +import {defineConfig} from "@rstest/core"; export default defineConfig({ - testEnvironment: "node", - include: ["tests/**/*.test.ts"], + testEnvironment: "node", + include: ["tests/**/*.test.ts"], }); diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..e0bf7ea --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,25 @@ +import type {SrcsetValidationIssue} from "./types"; + +export class SrcsetError extends Error { + override name = "SrcsetError"; +} + +export class SrcsetParseError extends SrcsetError { + override name = "SrcsetParseError"; +} + +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 index fc35cac..b3bed0d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,28 @@ -// Public entrypoint. The srcset API will be added in the implementation pass. -export {}; +import {parse} from "./parse"; +import {stringify} from "./stringify"; +import {validate} from "./validator"; + +export {SrcsetError, SrcsetParseError, 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"; + +const srcset = { + parse, + stringify, + validate, +}; + +export default srcset; 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..6e36140 --- /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; + sizes?: string; +}; + +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" + | "missing-width-descriptor" + | "multiple-descriptors"; + message: string; + candidate?: string; + index?: number; +}; + +export type ValidationResult = + | { + valid: true; + candidates: SrcsetCandidate[]; + descriptorType: DescriptorType; + errors: []; + } + | { + valid: false; + candidates: SrcsetCandidate[]; + descriptorType: DescriptorType; + errors: SrcsetValidationIssue[]; + }; diff --git a/src/validator.ts b/src/validator.ts new file mode 100644 index 0000000..2b8dafd --- /dev/null +++ b/src/validator.ts @@ -0,0 +1,350 @@ +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.sizes !== undefined && candidate.descriptor.kind !== "width") { + issues.push( + issue( + "missing-width-descriptor", + "When sizes is supplied, every srcset candidate must use a width 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[], + descriptorType: DescriptorType, + errors: SrcsetValidationIssue[], +): ValidationResult { + return errors.length === 0 + ? {candidates, descriptorType, errors: [], valid: true} + : {candidates, descriptorType, 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 index 3a8ed5c..38f79bb 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,7 +1,247 @@ -import { expect, test } from "@rstest/core"; +import {describe, expect, test} from "@rstest/core"; -import * as srcsetKit from "../src/index"; +import srcset, { + parse, + type SrcsetCandidate, + SrcsetValidationError, + stringify, + validate, +} from "../src/index"; -test("exports a loadable public module", () => { - expect(srcsetKit).toBeDefined(); +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 the default srcset object and named functions", () => { + expect(srcset.parse).toBe(parse); + expect(srcset.validate).toBe(validate); + expect(srcset.stringify).toBe(stringify); + }); +}); + +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}]); + }); +}); + +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("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 sizes is supplied", () => { + expect(validate("a.png 640w, b.png 1280w", {sizes: "100vw"}).valid).toBe(true); + + const result = validate("a.png 1x, b.png 2x", {sizes: "100vw"}); + + expect(result.valid).toBe(false); + expect(result.errors.map(({code}) => code)).toEqual([ + "missing-width-descriptor", + "missing-width-descriptor", + ]); + }); + + 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", + ); + }); +}); + +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); + }); }); diff --git a/tsconfig.json b/tsconfig.json index de8aad3..ddfd14c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "target": "ES2020", - "lib": ["ES2020"], + "lib": ["ES2020", "DOM"], "module": "ESNext", "moduleResolution": "Bundler", "strict": true, From e7e75a3964035672bcacff2436c30f1bada4f592 Mon Sep 17 00:00:00 2001 From: Anjey Tsibylskij <130153594+atldays@users.noreply.github.com> Date: Sat, 2 May 2026 01:16:12 +0300 Subject: [PATCH 06/12] ci(release): remove unused `package-name` and enable provenance for npm publish --- .github/workflows/release.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6e5613e..6feefea 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,6 @@ jobs: uses: googleapis/release-please-action@v4 with: release-type: node - package-name: srcset-kit - name: Checkout if: ${{ steps.release.outputs.release_created }} @@ -54,6 +53,6 @@ jobs: - name: Publish to npm if: ${{ steps.release.outputs.release_created }} - run: npm publish --access public + run: npm publish --provenance --access public env: HUSKY: 0 From be7be4f6232667acd1739645216a990bbffc9c80 Mon Sep 17 00:00:00 2001 From: Anjey Tsibylskij <130153594+atldays@users.noreply.github.com> Date: Sat, 2 May 2026 02:08:12 +0300 Subject: [PATCH 07/12] feat: enhance validator with descriptor-specific rules and update API docs --- README.md | 109 ++++++++++++++++++++++++++++++++++++++++++-- src/errors.ts | 4 -- src/index.ts | 14 +----- src/types.ts | 4 +- src/validator.ts | 20 ++++++-- tests/index.test.ts | 80 ++++++++++++++++++++++++++++---- 6 files changed, 195 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 939398b..42bb4e6 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,113 @@ pnpm add srcset-kit ## Usage -The package scaffold is ready for production npm publishing. The public `srcset` -API will be added in the next implementation pass. +`srcset-kit` parses, validates, and serializes HTML `srcset` attribute values +without splitting blindly on commas. ```ts -import {} from "srcset-kit"; +import { parse, stringify, validate } from "srcset-kit"; + +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 }, +// ] +``` + +### Validate + +```ts +const result = validate("image.png 1x, image@2x.png 2x"); + +if (result.valid) { + result.descriptorType; + // "density" +} +``` + +Validation returns structured issue codes instead of throwing for normal invalid +input. + +```ts +validate("image.png 1x, image@1x.png 1x"); +// { +// valid: false, +// descriptorType: "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, +// }, +// ], +// } +``` + +Use `descriptor` to require a specific descriptor type for all candidates. + +```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 1x, image@2x.png 2x", { descriptor: "density" }); +// valid: true +``` + +Use `baseUrl` when relative URLs should be checked in the context of a page URL. +Candidate URLs are not rewritten. + +```ts +validate("/image.png 1x", { baseUrl: "https://example.com" }); +// valid: true +``` + +### Stringify + +```ts +stringify([ + { url: "image.png", density: 1 }, + { url: "image@2x.png", density: 2 }, +]); +// "image.png 1x, image@2x.png 2x" +``` + +### Strict Parse + +`parse()` is tolerant by default. Use `strict: true` when invalid descriptor sets +should throw a package-specific `SrcsetValidationError`. + +```ts +parse("image.png 1x 640w"); +// [{ url: "image.png" }] + +parse("image.png 1x 640w", { strict: true }); +// throws SrcsetValidationError +``` + +### Data URLs + +The parser keeps commas inside URLs, including `data:` URLs. + +```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 }, +// ] ``` ## Development @@ -31,4 +133,3 @@ pnpm run verify - Build tool: Rslib. - Test runner: Rstest. - Published formats: ESM, CommonJS, and TypeScript declarations. - diff --git a/src/errors.ts b/src/errors.ts index e0bf7ea..f4b91e4 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -4,10 +4,6 @@ export class SrcsetError extends Error { override name = "SrcsetError"; } -export class SrcsetParseError extends SrcsetError { - override name = "SrcsetParseError"; -} - export class SrcsetValidationError extends SrcsetError { override name = "SrcsetValidationError"; diff --git a/src/index.ts b/src/index.ts index b3bed0d..9fba6c6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,4 @@ -import {parse} from "./parse"; -import {stringify} from "./stringify"; -import {validate} from "./validator"; - -export {SrcsetError, SrcsetParseError, SrcsetValidationError} from "./errors"; +export {SrcsetError, SrcsetValidationError} from "./errors"; export {parse} from "./parse"; export {stringify} from "./stringify"; export type { @@ -18,11 +14,3 @@ export type { WidthCandidate, } from "./types"; export {validate} from "./validator"; - -const srcset = { - parse, - stringify, - validate, -}; - -export default srcset; diff --git a/src/types.ts b/src/types.ts index 6e36140..be34fd5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -20,7 +20,7 @@ export type ParseOptions = { export type ValidateOptions = { baseUrl?: string | URL; - sizes?: string; + descriptor?: "width" | "density"; }; export type StringifyOptions = { @@ -36,7 +36,7 @@ export type SrcsetValidationIssue = { | "invalid-descriptor" | "duplicate-descriptor" | "mixed-descriptors" - | "missing-width-descriptor" + | "mismatched-descriptor" | "multiple-descriptors"; message: string; candidate?: string; diff --git a/src/validator.ts b/src/validator.ts index 2b8dafd..c31780e 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -245,11 +245,25 @@ function descriptorSetIssues( for (const candidate of candidates) { kinds.add(candidate.descriptor.kind); - if (options.sizes !== undefined && candidate.descriptor.kind !== "width") { + if (options.descriptor === "width" && candidate.descriptor.kind !== "width") { issues.push( issue( - "missing-width-descriptor", - "When sizes is supplied, every srcset candidate must use a width descriptor.", + "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, ), ); diff --git a/tests/index.test.ts b/tests/index.test.ts index 38f79bb..38606e4 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,6 +1,6 @@ import {describe, expect, test} from "@rstest/core"; -import srcset, { +import { parse, type SrcsetCandidate, SrcsetValidationError, @@ -16,10 +16,10 @@ function expectInvalidCodes(input: string | SrcsetCandidate[], codes: string[]) } describe("public API", () => { - test("exports the default srcset object and named functions", () => { - expect(srcset.parse).toBe(parse); - expect(srcset.validate).toBe(validate); - expect(srcset.stringify).toBe(stringify); + test("exports named functions", () => { + expect(typeof parse).toBe("function"); + expect(typeof validate).toBe("function"); + expect(typeof stringify).toBe("function"); }); }); @@ -112,6 +112,25 @@ describe("parse", () => { 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", () => { @@ -141,18 +160,34 @@ describe("validate", () => { expectInvalidCodes("a.png 640w, b.png", ["mixed-descriptors"]); }); - test("requires width descriptors when sizes is supplied", () => { - expect(validate("a.png 640w, b.png 1280w", {sizes: "100vw"}).valid).toBe(true); + 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 1x, b.png 2x", {sizes: "100vw"}); + const result = validate("a.png 640w, b.png 1280w", {descriptor: "density"}); expect(result.valid).toBe(false); expect(result.errors.map(({code}) => code)).toEqual([ - "missing-width-descriptor", - "missing-width-descriptor", + "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([ @@ -176,6 +211,22 @@ describe("validate", () => { "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", () => { @@ -244,4 +295,13 @@ describe("stringify", () => { 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); + }); }); From f56bb6494d4f248bb60b3bd3afb49fff93b3316a Mon Sep 17 00:00:00 2001 From: Anjey Tsibylskij <130153594+atldays@users.noreply.github.com> Date: Sat, 2 May 2026 02:08:57 +0300 Subject: [PATCH 08/12] test: simplify import path in index.test.ts --- tests/index.test.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/tests/index.test.ts b/tests/index.test.ts index 38606e4..d56eeb6 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,12 +1,6 @@ import {describe, expect, test} from "@rstest/core"; -import { - parse, - type SrcsetCandidate, - SrcsetValidationError, - stringify, - validate, -} from "../src/index"; +import {parse, type SrcsetCandidate, SrcsetValidationError, stringify, validate} from "../src"; function expectInvalidCodes(input: string | SrcsetCandidate[], codes: string[]) { const result = validate(input); From 6ec446d870272e3a0be476129d795feb5973d679 Mon Sep 17 00:00:00 2001 From: Anjey Tsibylskij <130153594+atldays@users.noreply.github.com> Date: Sat, 2 May 2026 02:27:45 +0300 Subject: [PATCH 09/12] refactor: rename `descriptorType` to `descriptor` in validator and types - Updated `buildValidationResult` to use `descriptor` instead of `descriptorType`. - Reflected the renaming in validation return types and test assertions. - Expanded documentation and examples in README to align with the updated term. --- .gitignore | 2 +- README.md | 294 +++++++++++++++++++++++++++++++++++++------- src/types.ts | 4 +- src/validator.ts | 6 +- tests/index.test.ts | 6 + 5 files changed, 260 insertions(+), 52 deletions(-) diff --git a/.gitignore b/.gitignore index ee8cef1..ab59570 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .DS_Store .idea/ +.claude/ node_modules/ dist/ @@ -13,4 +14,3 @@ npm-debug.log* pnpm-debug.log* yarn-debug.log* yarn-error.log* - diff --git a/README.md b/README.md index 42bb4e6..d695d9e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,24 @@ # srcset-kit -Tools for working with the HTML `srcset` attribute. +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 @@ -8,49 +26,126 @@ Tools for working with the HTML `srcset` attribute. pnpm add srcset-kit ``` -## Usage +```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"); -`srcset-kit` parses, validates, and serializes HTML `srcset` attribute values -without splitting blindly on commas. +const result = validate(candidates, { + descriptor: "density", +}); + +if (result.valid) { + stringify(result.candidates); + // "image.png 1x, image@2x.png 2x" +} +``` + +## Parse + +Parse a `srcset` string into clean candidate objects. ```ts -import { parse, stringify, validate } from "srcset-kit"; +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 }, +// {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 }, +// {url: "small.png", width: 640}, +// {url: "large.png", width: 1280}, // ] ``` -### Validate +Parsing is tolerant by default. It keeps the usable URL candidate shape and +leaves validation decisions to `validate()`. ```ts -const result = validate("image.png 1x, image@2x.png 2x"); +parse("image.png 1x 640w, image@2x.png 2x"); +// [ +// {url: "image.png"}, +// {url: "image@2x.png", density: 2}, +// ] +``` -if (result.valid) { - result.descriptorType; - // "density" +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 } ``` -Validation returns structured issue codes instead of throwing for normal invalid -input. +## 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, -// descriptorType: "density", +// descriptor: "density", // candidates: [ -// { url: "image.png", density: 1 }, -// { url: "image@1x.png", density: 1 }, +// {url: "image.png", density: 1}, +// {url: "image@1x.png", density: 1}, // ], // errors: [ // { @@ -63,73 +158,180 @@ validate("image.png 1x, image@1x.png 1x"); // } ``` -Use `descriptor` to require a specific descriptor type for all candidates. +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" }); +validate("small.png 640w, large.png 1280w", { + descriptor: "width", +}); // valid: true -validate("image.png 1x, image@2x.png 2x", { descriptor: "width" }); +validate("image.png 1x, image@2x.png 2x", { + descriptor: "width", +}); // valid: false -validate("image.png 1x, image@2x.png 2x", { descriptor: "density" }); +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("/image.png 1x", { baseUrl: "https://example.com" }); +validate("/images/photo.jpg 1x", { + baseUrl: "https://example.com/gallery/", +}); // valid: true + +parse("/images/photo.jpg 1x"); +// [{url: "/images/photo.jpg", density: 1}] ``` -### Stringify +## 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 }, + {url: "image.png", density: 1}, + {url: "image@2x.png", density: 2}, ]); // "image.png 1x, image@2x.png 2x" ``` -### Strict Parse - -`parse()` is tolerant by default. Use `strict: true` when invalid descriptor sets -should throw a package-specific `SrcsetValidationError`. +Strict stringifying validates before serializing. ```ts -parse("image.png 1x 640w"); -// [{ url: "image.png" }] - -parse("image.png 1x 640w", { strict: true }); +stringify( + [ + {url: "image.png", density: 1}, + {url: "image-copy.png", density: 1}, + ], + {strict: true}, +); // throws SrcsetValidationError ``` -### Data URLs +## Round Trips -The parser keeps commas inside URLs, including `data:` URLs. +Use `parse()` and `stringify()` together to normalize whitespace while preserving +URLs. ```ts -parse("data:image/png;base64,AAAA 1x, /image@2x.png 2x"); +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", density: 1 }, -// { url: "/image@2x.png", density: 2 }, +// {url: "data:image/png;base64,AAAA+BB==", density: 1}, +// {url: "/image.png?w=100,h=200&fmt=webp", density: 2}, // ] ``` -## Development +## Validation Rules -```sh -pnpm install -pnpm run verify +`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. -- Build tool: Rslib. -- Test runner: Rstest. - 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/src/types.ts b/src/types.ts index be34fd5..361197a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -47,12 +47,12 @@ export type ValidationResult = | { valid: true; candidates: SrcsetCandidate[]; - descriptorType: DescriptorType; + descriptor: DescriptorType; errors: []; } | { valid: false; candidates: SrcsetCandidate[]; - descriptorType: DescriptorType; + descriptor: DescriptorType; errors: SrcsetValidationIssue[]; }; diff --git a/src/validator.ts b/src/validator.ts index c31780e..b7e5d89 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -328,12 +328,12 @@ function getDescriptorType(candidates: NormalizedCandidate[]): DescriptorType { function buildValidationResult( candidates: SrcsetCandidate[], - descriptorType: DescriptorType, + descriptor: DescriptorType, errors: SrcsetValidationIssue[], ): ValidationResult { return errors.length === 0 - ? {candidates, descriptorType, errors: [], valid: true} - : {candidates, descriptorType, errors, valid: false}; + ? {candidates, descriptor, errors: [], valid: true} + : {candidates, descriptor, errors, valid: false}; } function issue( diff --git a/tests/index.test.ts b/tests/index.test.ts index d56eeb6..85c3b4b 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -140,6 +140,12 @@ describe("validate", () => { } }); + 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"]); From 84bb78da4e9dfff123f47605edde468d751f6424 Mon Sep 17 00:00:00 2001 From: Anjey Tsibylskij <130153594+atldays@users.noreply.github.com> Date: Sat, 2 May 2026 02:35:56 +0300 Subject: [PATCH 10/12] docs: update LICENSE, CONTRIBUTING, and README for clarity and completeness --- CONTRIBUTING.md | 118 +++++++++++++++++++++++++++++++++++++++++++++--- LICENSE | 22 --------- LICENSE.md | 9 ++++ README.md | 20 +++++--- 4 files changed, 134 insertions(+), 35 deletions(-) delete mode 100644 LICENSE create mode 100644 LICENSE.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8d68d46..0fcf605 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,14 +1,39 @@ # Contributing -## Requirements +Thanks for taking the time to improve `srcset-kit`. -- Node.js 22 or newer for local development. +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. -## Local checks +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 ``` @@ -16,9 +41,79 @@ Use focused commands while developing: ```sh pnpm run test -pnpm run build +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 @@ -28,12 +123,13 @@ 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 +## Branch Flow The repository follows a GitFlow-like process without requiring the GitFlow CLI: @@ -43,7 +139,7 @@ The repository follows a GitFlow-like process without requiring the GitFlow CLI: - 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, for example: +When a merge commit is created manually, keep the merge message conventional: ```text chore(release): merge main back into develop @@ -58,3 +154,13 @@ Husky hooks are installed by `pnpm install`. - `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 b/LICENSE deleted file mode 100644 index 7557cce..0000000 --- a/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -MIT License - -Copyright (c) 2026 Atldays - -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/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 index d695d9e..a3f4e73 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # srcset-kit +[![npm version](https://img.shields.io/npm/v/srcset-kit.svg?logo=npm&style=for-the-badge)](https://www.npmjs.com/package/srcset-kit) +[![npm downloads](https://img.shields.io/npm/dm/srcset-kit.svg?style=for-the-badge&color=blue)](https://www.npmjs.com/package/srcset-kit) +[![CI](https://img.shields.io/github/actions/workflow/status/atldays/srcset-kit/ci.yml?style=for-the-badge)](https://github.com/atldays/srcset-kit/actions/workflows/ci.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge)](LICENSE.md) + Small, dependency-free tools for parsing, validating, and serializing HTML `srcset` values. @@ -40,15 +45,16 @@ yarn add srcset-kit 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(candidates, { - descriptor: "density", -}); +const result = validate("image.png 1x, image@2x.png 2x"); +// result.valid === true -if (result.valid) { - stringify(result.candidates); - // "image.png 1x, image@2x.png 2x" -} +const srcset = stringify(candidates); +// "image.png 1x, image@2x.png 2x" ``` ## Parse From 9fb8c1312c45d344d1253f3166a7bb903935cdbb Mon Sep 17 00:00:00 2001 From: Anjey Tsibylskij <130153594+atldays@users.noreply.github.com> Date: Sat, 2 May 2026 02:40:31 +0300 Subject: [PATCH 11/12] docs: update package.json description and keywords for improved clarity and discoverability --- package.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index eea9ba2..85d31d6 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,18 @@ { "name": "srcset-kit", "version": "0.0.1", - "description": "Tools for working with the HTML srcset attribute.", + "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", + "data-url", "html", + "browser", "typescript" ], "type": "module", From 46955e677b30af8bc2fa12f655075f0fe54a4423 Mon Sep 17 00:00:00 2001 From: Anjey Tsibylskij <130153594+atldays@users.noreply.github.com> Date: Sat, 2 May 2026 02:47:20 +0300 Subject: [PATCH 12/12] chore(release): release 1.0.0 Release-As: 1.0.0 --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 85d31d6..9afdd4e 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "responsive-images", "responsive-image", "image-srcset", + "srcset-attribute", "data-url", "html", "browser",