From 7644cf0f3027a769824c8f670f06ed4176f2d52f Mon Sep 17 00:00:00 2001 From: Viet Vu Date: Wed, 13 May 2026 19:47:22 +0700 Subject: [PATCH 1/3] feat: add initial Node FormRequest package --- .aiignore | 10 + .github/pull_request_template.md | 18 + .github/skills/formrequest-package/SKILL.md | 30 + .github/workflows/ci.yml | 46 + .gitignore | 15 + .humanignore | 7 + AGENTS.md | 35 + CHANGELOG.md | 12 + CLAUDE.md | 11 + LICENSE | 21 + README.md | 62 + ai/skills/README.md | 5 + ai/skills/USAGE.md | 13 + docs/README.md | 5 + docs/ai-skills.md | 9 + docs/ci-cd.md | 5 + docs/release-process.md | 11 + eslint.config.js | 22 + package-lock.json | 4141 +++++++++++++++++++ package.json | 68 + src/FormRequest.ts | 171 + src/index.ts | 18 + src/rules/createRule.ts | 16 + src/rules/index.ts | 102 + src/types.ts | 47 + tests/formrequest.test.ts | 309 ++ tsconfig.json | 19 + vitest.config.ts | 18 + 28 files changed, 5246 insertions(+) create mode 100644 .aiignore create mode 100644 .github/pull_request_template.md create mode 100644 .github/skills/formrequest-package/SKILL.md create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .humanignore create mode 100644 AGENTS.md create mode 100644 CHANGELOG.md create mode 100644 CLAUDE.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 ai/skills/README.md create mode 100644 ai/skills/USAGE.md create mode 100644 docs/README.md create mode 100644 docs/ai-skills.md create mode 100644 docs/ci-cd.md create mode 100644 docs/release-process.md create mode 100644 eslint.config.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/FormRequest.ts create mode 100644 src/index.ts create mode 100644 src/rules/createRule.ts create mode 100644 src/rules/index.ts create mode 100644 src/types.ts create mode 100644 tests/formrequest.test.ts create mode 100644 tsconfig.json create mode 100644 vitest.config.ts diff --git a/.aiignore b/.aiignore new file mode 100644 index 0000000..c76f1a2 --- /dev/null +++ b/.aiignore @@ -0,0 +1,10 @@ +.git/ +node_modules/ +coverage/ +dist/ +*.tgz +package-lock.json +.env +.env.* +.vscode/ +.idea/ diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..7c4036a --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,18 @@ +## Summary + +- + +## Validation + +- [ ] `npm run lint` +- [ ] `npm run typecheck` +- [ ] `npm run test:coverage` +- [ ] `npm run build` +- [ ] `npm pack --dry-run` + +## Review Safety + +- [ ] CI checks pass +- [ ] Required reviews approved +- [ ] Requested changes resolved +- [ ] Conversation threads resolved diff --git a/.github/skills/formrequest-package/SKILL.md b/.github/skills/formrequest-package/SKILL.md new file mode 100644 index 0000000..5f279d0 --- /dev/null +++ b/.github/skills/formrequest-package/SKILL.md @@ -0,0 +1,30 @@ +# FormRequest Package Skill + +Use this skill when changing the FormRequest runtime, rule factories, validation errors, Express-style adapter, docs, or release metadata. + +## Runtime Ownership + +- `src/FormRequest.ts` owns validation orchestration, authorization checks, validated data, grouped errors, and Express-like input merging. +- `src/rules/` owns reusable validation rules. +- `src/types.ts` owns public TypeScript contracts. +- `tests/` must cover public behavior at 100% statement, branch, function, and line coverage. + +## Guardrails + +- Do not add Laravel string rules. +- Do not add database validation rules. +- Do not add a service container. +- Do not put business logic inside FormRequest. +- Do not require Express at runtime. + +## Validation + +Run: + +```bash +npm run lint +npm run typecheck +npm run test:coverage +npm run build +npm pack --dry-run +``` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7406591 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,46 @@ +name: CI + +on: + pull_request: + branches: + - develop + - master + push: + branches: + - develop + - master + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: Typecheck + run: npm run typecheck + + - name: Coverage + run: npm run test:coverage + + - name: Build + run: npm run build + + - name: Package dry-run + run: npm pack --dry-run diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0fb0bce --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +.DS_Store +.env +.env.* +!.env.example +.vscode/ +.idea/ +node_modules/ +coverage/ +dist/ +*.tsbuildinfo +*.tgz +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* diff --git a/.humanignore b/.humanignore new file mode 100644 index 0000000..62f9be7 --- /dev/null +++ b/.humanignore @@ -0,0 +1,7 @@ +node_modules/ +coverage/ +dist/ +package-lock.json +*.tsbuildinfo +*.tgz +.DS_Store diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..100fcf5 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,35 @@ +# JOOservices Node FormRequest Repository Instructions + +This repository is a Node.js package named `@jooservices/formrequest`. + +## Core Intent + +- Keep FormRequest focused on validation and authorization. +- Keep business logic outside FormRequest. +- Keep framework integration adapter-based and optional. +- Update tests and docs with behavior changes. +- Follow the approved `master` and `develop` Git flow. + +## Required Commands + +- `npm run lint` +- `npm run typecheck` +- `npm run test:coverage` +- `npm run build` +- `npm pack --dry-run` + +## Architecture Rules + +- No Laravel string rule parser. +- No database-backed validation rules. +- No service container. +- No persistence or business workflows. +- No framework lock-in beyond optional Express-like request input support. + +## Branch Workflow + +- `master`: stable release branch only. +- `develop`: integration branch. +- `feature/*`: branch from latest `develop` and PR back into `develop`. +- `release/`: branch from latest `develop` and PR into `master`. +- Tags are created from `master` only. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..095dd2b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## Unreleased + +### Added + +- Initial TypeScript FormRequest package with framework-neutral validation. +- Optional Express-style request adapter. +- Rule factories for common scalar, collection, comparison, and format validation. +- Documentation, AI contributor guidance, CI, and full local validation scripts. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..771a7b2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,11 @@ +# Claude Code Instructions For `jooservices/node-formrequest` + +Read [AGENTS.md](./AGENTS.md) first. + +When working in this repository: + +- Match the existing TypeScript style and public API shape. +- Keep changes small, typed, tested, and documented. +- Preserve framework-neutral behavior. +- Run lint, typecheck, coverage, build, and package dry-run before PRs. +- Follow the approved Git flow: feature work into `develop`, release PRs into `master`, and tags from `master`. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e630730 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 JOOservices + +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..c026f98 --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# JOOservices Node FormRequest + +[![CI](https://github.com/jooservices/node-formrequest/actions/workflows/ci.yml/badge.svg?branch=develop)](https://github.com/jooservices/node-formrequest/actions/workflows/ci.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) + +Framework-neutral FormRequest validation for Node.js and TypeScript with an optional Express-style adapter. + +Package name: `@jooservices/formrequest` + +## Install + +```bash +npm install @jooservices/formrequest +``` + +## Quick Example + +```ts +import { FormRequest, email, required, string } from '@jooservices/formrequest'; + +const request = new FormRequest({ + input: { + name: 'Jane Doe', + email: 'jane@example.com' + }, + rules: { + name: [required(), string()], + email: [required(), email()] + } +}); + +const data = await request.validateOrThrow(); +``` + +## Public API + +- `FormRequest`: validates declared fields and returns only validated data. +- `FormRequest.fromExpress`: merges `params`, `query`, and `body` for Express-like requests. +- `AuthorizationError`: thrown when `authorize` returns false. +- `ValidationError`: thrown by `validateOrThrow` when validation fails. +- Rule factories: `required`, `optional`, `nullable`, `string`, `number`, `boolean`, `array`, `object`, `email`, `url`, `min`, `max`, `between`, `length`, `regex`, `accepted`, and `inValues`. + +## Scope + +This package intentionally does not include Laravel string rule parsing, database validation rules, a service container, or business logic in FormRequest classes. Express support is adapter-style and optional. + +## Development + +```bash +npm run lint +npm run typecheck +npm run test:coverage +npm run build +npm pack --dry-run +``` + +Approved Git flow: + +- normal feature and fix work branches from `develop` and PRs back into `develop` +- release preparation uses `release/` from `develop`, then PRs into `master` +- releases are tagged from `master` +- `master` merges back into `develop` after release completion diff --git a/ai/skills/README.md b/ai/skills/README.md new file mode 100644 index 0000000..f3bbc3a --- /dev/null +++ b/ai/skills/README.md @@ -0,0 +1,5 @@ +# AI Skills Map + +- [FormRequest Package](../../.github/skills/formrequest-package/SKILL.md) + +Use these notes when an AI agent changes validation behavior, public API, docs, release metadata, or CI. diff --git a/ai/skills/USAGE.md b/ai/skills/USAGE.md new file mode 100644 index 0000000..9235568 --- /dev/null +++ b/ai/skills/USAGE.md @@ -0,0 +1,13 @@ +# AI Skills Usage + +Before changing source code, read `.github/skills/formrequest-package/SKILL.md`. + +After source changes, run: + +```bash +npm run lint +npm run typecheck +npm run test:coverage +npm run build +npm pack --dry-run +``` diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..4204008 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,5 @@ +# Documentation Hub + +- [Release Process](./release-process.md) +- [CI/CD](./ci-cd.md) +- [AI Skills](./ai-skills.md) diff --git a/docs/ai-skills.md b/docs/ai-skills.md new file mode 100644 index 0000000..3026b30 --- /dev/null +++ b/docs/ai-skills.md @@ -0,0 +1,9 @@ +# AI Skills + +AI contributor guidance is available in: + +- `AGENTS.md` +- `CLAUDE.md` +- `.github/skills/formrequest-package/SKILL.md` +- `ai/skills/README.md` +- `ai/skills/USAGE.md` diff --git a/docs/ci-cd.md b/docs/ci-cd.md new file mode 100644 index 0000000..abb7120 --- /dev/null +++ b/docs/ci-cd.md @@ -0,0 +1,5 @@ +# CI/CD + +Pull requests run linting, type checking, coverage tests, build, and package dry-run. + +Coverage thresholds are set to 100% for statements, branches, functions, and lines. diff --git a/docs/release-process.md b/docs/release-process.md new file mode 100644 index 0000000..ad580e2 --- /dev/null +++ b/docs/release-process.md @@ -0,0 +1,11 @@ +# Release Process + +1. Merge approved feature work into `develop`. +2. Branch `release/` from the latest `develop`. +3. Update `package.json`, lockfile, `CHANGELOG.md`, and release notes. +4. Run the full validation command set. +5. Open a release PR into `master`. +6. Merge only after CI, required reviews, and review threads are complete. +7. Tag `v` from `master`. +8. Create a GitHub release when tooling permits. +9. Merge `master` back into `develop`. diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..b59b759 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,22 @@ +import js from '@eslint/js'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + js.configs.recommended, + ...tseslint.configs.strictTypeChecked, + { + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname + } + } + }, + { + files: ['eslint.config.js'], + extends: [tseslint.configs.disableTypeChecked] + }, + { + ignores: ['dist/**', 'coverage/**', 'node_modules/**'] + } +); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..758319a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4141 @@ +{ + "name": "@jooservices/formrequest", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@jooservices/formrequest", + "version": "0.0.0", + "license": "MIT", + "devDependencies": { + "@eslint/js": "^9.26.0", + "@types/node": "^22.15.17", + "@vitest/coverage-v8": "^3.1.3", + "eslint": "^9.26.0", + "tsup": "^8.4.0", + "typescript": "^5.8.3", + "typescript-eslint": "^8.32.0", + "vitest": "^3.1.3" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "express": ">=4.18 <6" + }, + "peerDependenciesMeta": { + "express": { + "optional": true + } + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", + "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", + "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", + "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", + "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", + "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", + "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", + "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", + "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", + "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", + "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", + "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", + "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", + "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", + "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", + "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", + "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", + "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", + "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", + "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", + "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", + "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", + "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", + "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", + "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", + "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz", + "integrity": "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/type-utils": "8.59.3", + "@typescript-eslint/utils": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.59.3", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.3.tgz", + "integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.3.tgz", + "integrity": "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.3", + "@typescript-eslint/types": "^8.59.3", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz", + "integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz", + "integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz", + "integrity": "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/utils": "8.59.3", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz", + "integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz", + "integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.3", + "@typescript-eslint/tsconfig-utils": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.3.tgz", + "integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz", + "integrity": "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.3", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", + "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.3", + "@rollup/rollup-android-arm64": "4.60.3", + "@rollup/rollup-darwin-arm64": "4.60.3", + "@rollup/rollup-darwin-x64": "4.60.3", + "@rollup/rollup-freebsd-arm64": "4.60.3", + "@rollup/rollup-freebsd-x64": "4.60.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", + "@rollup/rollup-linux-arm-musleabihf": "4.60.3", + "@rollup/rollup-linux-arm64-gnu": "4.60.3", + "@rollup/rollup-linux-arm64-musl": "4.60.3", + "@rollup/rollup-linux-loong64-gnu": "4.60.3", + "@rollup/rollup-linux-loong64-musl": "4.60.3", + "@rollup/rollup-linux-ppc64-gnu": "4.60.3", + "@rollup/rollup-linux-ppc64-musl": "4.60.3", + "@rollup/rollup-linux-riscv64-gnu": "4.60.3", + "@rollup/rollup-linux-riscv64-musl": "4.60.3", + "@rollup/rollup-linux-s390x-gnu": "4.60.3", + "@rollup/rollup-linux-x64-gnu": "4.60.3", + "@rollup/rollup-linux-x64-musl": "4.60.3", + "@rollup/rollup-openbsd-x64": "4.60.3", + "@rollup/rollup-openharmony-arm64": "4.60.3", + "@rollup/rollup-win32-arm64-msvc": "4.60.3", + "@rollup/rollup-win32-ia32-msvc": "4.60.3", + "@rollup/rollup-win32-x64-gnu": "4.60.3", + "@rollup/rollup-win32-x64-msvc": "4.60.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/tsup/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.3.tgz", + "integrity": "sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.59.3", + "@typescript-eslint/parser": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/utils": "8.59.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz", + "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..fe5d504 --- /dev/null +++ b/package.json @@ -0,0 +1,68 @@ +{ + "name": "@jooservices/formrequest", + "version": "0.0.0", + "description": "Framework-neutral FormRequest validation for Node.js with an optional Express adapter.", + "type": "module", + "license": "MIT", + "author": "JOOservices ", + "repository": { + "type": "git", + "url": "git+https://github.com/jooservices/node-formrequest.git" + }, + "bugs": { + "url": "https://github.com/jooservices/node-formrequest/issues" + }, + "homepage": "https://github.com/jooservices/node-formrequest#readme", + "keywords": [ + "form-request", + "validation", + "request-validation", + "express", + "typescript" + ], + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "files": [ + "dist", + "README.md", + "CHANGELOG.md", + "LICENSE" + ], + "scripts": { + "build": "tsup src/index.ts --format esm,cjs --dts --clean", + "lint": "eslint . --max-warnings=0", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:coverage": "vitest run --coverage", + "ci": "npm run lint && npm run typecheck && npm run test:coverage && npm run build && npm pack --dry-run" + }, + "peerDependencies": { + "express": ">=4.18 <6" + }, + "peerDependenciesMeta": { + "express": { + "optional": true + } + }, + "devDependencies": { + "@eslint/js": "^9.26.0", + "@types/node": "^22.15.17", + "@vitest/coverage-v8": "^3.1.3", + "eslint": "^9.26.0", + "tsup": "^8.4.0", + "typescript": "^5.8.3", + "typescript-eslint": "^8.32.0", + "vitest": "^3.1.3" + }, + "engines": { + "node": ">=20" + } +} diff --git a/src/FormRequest.ts b/src/FormRequest.ts new file mode 100644 index 0000000..5da668a --- /dev/null +++ b/src/FormRequest.ts @@ -0,0 +1,171 @@ +import { isEmpty } from './rules/createRule.js'; +import type { + ExpressLikeRequest, + FormRequestConfig, + InputBag, + Rule, + RuleContext, + ValidationIssue, + ValidationMessage, + ValidationResult +} from './types.js'; + +export class AuthorizationError extends Error { + public constructor(message = 'This action is unauthorized.') { + super(message); + this.name = 'AuthorizationError'; + } +} + +export class ValidationError extends Error { + public constructor(public readonly result: ValidationResult) { + super('The given data was invalid.'); + this.name = 'ValidationError'; + } +} + +export class FormRequest { + private readonly input: InputBag; + private readonly ruleSet: FormRequestConfig['rules']; + private readonly customMessages: NonNullable['messages']>; + private readonly customAttributes: Partial>; + private readonly authorizeCallback: NonNullable['authorize']>; + private result?: ValidationResult; + + public constructor(config: FormRequestConfig) { + this.input = config.input; + this.ruleSet = config.rules; + this.customMessages = config.messages ?? {}; + this.customAttributes = config.attributes ?? {}; + this.authorizeCallback = config.authorize ?? (() => true); + } + + public static fromExpress( + request: ExpressLikeRequest, + config: Omit, 'input'> + ): FormRequest { + return new FormRequest({ + ...config, + input: { + ...toInputBag(request.params), + ...toInputBag(request.query), + ...toInputBag(request.body) + } + }); + } + + public async validate(): Promise> { + if (!(await this.authorizeCallback(this.input))) { + throw new AuthorizationError(); + } + + this.result = this.runRules(); + return this.result; + } + + public async validateOrThrow(): Promise { + const result = await this.validate(); + + if (!result.valid) { + throw new ValidationError(result); + } + + return result.data as TData; + } + + public validated(): Partial { + return this.result?.data ?? {}; + } + + public errors(): Record { + return this.result?.errors ?? {}; + } + + private runRules(): ValidationResult { + const issues: ValidationIssue[] = []; + const data: Partial = {}; + + for (const [field, rules] of Object.entries(this.ruleSet)) { + const value = this.input[field]; + const shouldSkip = this.shouldSkipField(value, rules); + + if (shouldSkip) { + continue; + } + + for (const rule of rules) { + if (rule.optional || rule.nullable) { + continue; + } + + const context = this.makeContext(field, value); + + if (!rule.validate(context)) { + issues.push({ + field, + rule: rule.name, + message: this.resolveMessage(field, rule, context) + }); + } + } + + if (!issues.some((issue) => issue.field === field) && Object.hasOwn(this.input, field)) { + data[field as keyof TData] = value as TData[keyof TData]; + } + } + + return { + valid: issues.length === 0, + data, + errors: groupIssues(issues), + issues + }; + } + + private shouldSkipField(value: unknown, rules: readonly Rule[]): boolean { + const allowsOptional = rules.some((rule) => rule.optional); + const allowsNull = rules.some((rule) => rule.nullable); + + return (allowsOptional && value === undefined) || (allowsNull && value === null); + } + + private makeContext(field: string, value: unknown): RuleContext { + return { + field, + input: this.input, + label: this.customAttributes[field] ?? field, + value + }; + } + + private resolveMessage(field: string, rule: Rule, context: RuleContext): string { + const specific = this.customMessages[`${field}.${rule.name}`]; + const generic = this.customMessages[rule.name]; + const message = specific ?? generic; + + if (message !== undefined) { + return formatCustomMessage(message, context); + } + + return rule.message(context); + } +} + +const formatCustomMessage = (message: ValidationMessage, context: RuleContext): string => + typeof message === 'function' ? message(context) : message.replaceAll(':attribute', context.label); + +const groupIssues = (issues: readonly ValidationIssue[]): Record => + issues.reduce>((errors, issue) => { + errors[issue.field] = [...(errors[issue.field] ?? []), issue.message]; + return errors; + }, {}); + +const toInputBag = (value: unknown): InputBag => { + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + return value as InputBag; + } + + return {}; +}; + +export { isEmpty }; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..d700145 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,18 @@ +export { + AuthorizationError, + FormRequest, + ValidationError, + isEmpty +} from './FormRequest.js'; +export * from './rules/index.js'; +export type { + ExpressLikeRequest, + FormRequestConfig, + InputBag, + Rule, + RuleContext, + RuleSet, + ValidationIssue, + ValidationMessage, + ValidationResult +} from './types.js'; diff --git a/src/rules/createRule.ts b/src/rules/createRule.ts new file mode 100644 index 0000000..30f3cab --- /dev/null +++ b/src/rules/createRule.ts @@ -0,0 +1,16 @@ +import type { Rule, RuleContext } from '../types.js'; + +export const isEmpty = (value: unknown): boolean => + value === undefined || value === null || value === ''; + +export const createRule = ( + name: string, + validate: (context: RuleContext) => boolean, + message: string, + options: Pick = {} +): Rule => ({ + name, + ...options, + validate, + message: (context) => message.replaceAll(':attribute', context.label) +}); diff --git a/src/rules/index.ts b/src/rules/index.ts new file mode 100644 index 0000000..cd0e17c --- /dev/null +++ b/src/rules/index.ts @@ -0,0 +1,102 @@ +import { createRule, isEmpty } from './createRule.js'; +import type { InputBag, Rule } from '../types.js'; + +export const required = (): Rule => + createRule('required', ({ value }) => !isEmpty(value), 'The :attribute field is required.'); + +export const optional = (): Rule => + createRule('optional', () => true, '', { optional: true }); + +export const nullable = (): Rule => + createRule('nullable', () => true, '', { nullable: true }); + +export const string = (): Rule => + createRule('string', ({ value }) => isEmpty(value) || typeof value === 'string', 'The :attribute must be a string.'); + +export const number = (): Rule => + createRule('number', ({ value }) => isEmpty(value) || typeof value === 'number', 'The :attribute must be a number.'); + +export const boolean = (): Rule => + createRule('boolean', ({ value }) => isEmpty(value) || typeof value === 'boolean', 'The :attribute must be true or false.'); + +export const array = (): Rule => + createRule('array', ({ value }) => isEmpty(value) || Array.isArray(value), 'The :attribute must be an array.'); + +export const object = (): Rule => + createRule('object', ({ value }) => isEmpty(value) || isPlainObject(value), 'The :attribute must be an object.'); + +export const email = (): Rule => + createRule('email', ({ value }) => isEmpty(value) || (typeof value === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)), 'The :attribute must be a valid email address.'); + +export const url = (): Rule => + createRule('url', ({ value }) => { + if (isEmpty(value)) { + return true; + } + + if (typeof value !== 'string') { + return false; + } + + try { + const parsed = new URL(value); + return parsed.protocol === 'http:' || parsed.protocol === 'https:'; + } catch { + return false; + } + }, 'The :attribute must be a valid URL.'); + +export const min = (minimum: number): Rule => + createRule('min', ({ value }) => { + if (isEmpty(value)) { + return true; + } + + return measurableLength(value) >= minimum; + }, `The :attribute must be at least ${String(minimum)}.`); + +export const max = (maximum: number): Rule => + createRule('max', ({ value }) => { + if (isEmpty(value)) { + return true; + } + + return measurableLength(value) <= maximum; + }, `The :attribute must be at most ${String(maximum)}.`); + +export const between = (minimum: number, maximum: number): Rule => + createRule('between', ({ value }) => { + if (isEmpty(value)) { + return true; + } + + const length = measurableLength(value); + return length >= minimum && length <= maximum; + }, `The :attribute must be between ${String(minimum)} and ${String(maximum)}.`); + +export const length = (expected: number): Rule => + createRule('length', ({ value }) => isEmpty(value) || measurableLength(value) === expected, `The :attribute must be ${String(expected)}.`); + +export const regex = (pattern: RegExp): Rule => + createRule('regex', ({ value }) => isEmpty(value) || (typeof value === 'string' && pattern.test(value)), 'The :attribute format is invalid.'); + +export const accepted = (): Rule => + createRule('accepted', ({ value }) => value === true || value === 'yes' || value === 'on' || value === 1, 'The :attribute must be accepted.'); + +export const inValues = (allowed: readonly unknown[]): Rule => + createRule('in', ({ value }) => isEmpty(value) || allowed.includes(value), 'The selected :attribute is invalid.'); + +const measurableLength = (value: unknown): number => { + if (typeof value === 'number') { + return value; + } + + if (typeof value === 'string' || Array.isArray(value)) { + return value.length; + } + + return Number.POSITIVE_INFINITY; +}; + +const isPlainObject = (value: unknown): value is InputBag => + typeof value === 'object' && value !== null && !Array.isArray(value); diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..24f7a79 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,47 @@ +export type InputBag = Record; + +export type ValidationMessage = string | ((context: RuleContext) => string); + +export interface RuleContext { + readonly field: string; + readonly label: string; + readonly value: unknown; + readonly input: InputBag; +} + +export interface ValidationIssue { + readonly field: string; + readonly rule: string; + readonly message: string; +} + +export interface ValidationResult { + readonly valid: boolean; + readonly data: Partial; + readonly errors: Record; + readonly issues: ValidationIssue[]; +} + +export interface Rule { + readonly name: string; + readonly optional?: boolean; + readonly nullable?: boolean; + validate(context: RuleContext): boolean; + message(context: RuleContext): string; +} + +export type RuleSet = Record; + +export interface FormRequestConfig { + readonly input: InputBag; + readonly rules: RuleSet; + readonly messages?: Partial>; + readonly attributes?: Partial>; + readonly authorize?: (input: InputBag) => boolean | Promise; +} + +export interface ExpressLikeRequest { + readonly body?: unknown; + readonly query?: unknown; + readonly params?: unknown; +} diff --git a/tests/formrequest.test.ts b/tests/formrequest.test.ts new file mode 100644 index 0000000..b88d993 --- /dev/null +++ b/tests/formrequest.test.ts @@ -0,0 +1,309 @@ +import { describe, expect, it } from 'vitest'; +import { + AuthorizationError, + FormRequest, + ValidationError, + accepted, + array, + between, + boolean, + email, + inValues, + isEmpty, + length, + max, + min, + nullable, + number, + object, + optional, + regex, + required, + string, + url +} from '../src/index.js'; + +interface SignupData { + name: string; + email: string; + age?: number; + website?: string | null; + newsletter: boolean; + tags: string[]; + meta: Record; + token: string; + role: string; +} + +describe('FormRequest', () => { + it('validates input and returns only declared fields', async () => { + const request = new FormRequest({ + input: { + name: 'Jane Doe', + email: 'jane@example.com', + age: 30, + website: null, + newsletter: true, + tags: ['node'], + meta: { source: 'test' }, + token: 'ABC123', + role: 'admin', + ignored: 'value' + }, + rules: { + name: [required(), string(), min(2), max(50), between(2, 50)], + email: [required(), email()], + age: [optional(), number()], + website: [nullable(), url()], + newsletter: [accepted(), boolean()], + tags: [array()], + meta: [object()], + token: [length(6), regex(/^[A-Z0-9]+$/)], + role: [inValues(['admin', 'editor'])] + } + }); + + const result = await request.validate(); + + expect(result.valid).toBe(true); + expect(result.data).toEqual({ + name: 'Jane Doe', + email: 'jane@example.com', + age: 30, + newsletter: true, + tags: ['node'], + meta: { source: 'test' }, + token: 'ABC123', + role: 'admin' + }); + expect(request.validated()).toEqual(result.data); + expect(request.errors()).toEqual({}); + }); + + it('groups errors and supports custom messages and attributes', async () => { + const request = new FormRequest({ + input: { + name: '', + email: 'invalid', + age: 'old', + website: 'ftp://example.com', + newsletter: false, + tags: 'node', + meta: [], + token: 'abc', + role: 'guest', + score: 20 + }, + attributes: { + email: 'email address' + }, + messages: { + 'email.email': 'Use a real :attribute.', + number: ({ field }) => `${field} must be numeric.` + }, + rules: { + name: [required()], + email: [email()], + age: [number()], + website: [url()], + newsletter: [accepted()], + tags: [array()], + meta: [object()], + token: [length(6), regex(/^[A-Z0-9]+$/)], + role: [inValues(['admin'])], + score: [between(1, 10)] + } + }); + + const result = await request.validate(); + + expect(result.valid).toBe(false); + expect(result.errors.email).toEqual(['Use a real email address.']); + expect(result.errors.age).toEqual(['age must be numeric.']); + expect(result.issues.map((issue) => issue.rule)).toEqual([ + 'required', + 'email', + 'number', + 'url', + 'accepted', + 'array', + 'object', + 'length', + 'regex', + 'in', + 'between' + ]); + }); + + it('throws for authorization and invalid validation paths', async () => { + const unauthorized = new FormRequest({ + input: {}, + authorize: () => false, + rules: { + name: [required()] + } + }); + + await expect(unauthorized.validate()).rejects.toThrow(AuthorizationError); + + const invalid = new FormRequest({ + input: {}, + rules: { + name: [required()] + } + }); + + await expect(invalid.validateOrThrow()).rejects.toThrow(ValidationError); + }); + + it('merges Express-like params, query, and body with body taking precedence', async () => { + const request = FormRequest.fromExpress<{ id: string; page: string; name: string }>( + { + body: { name: 'Body Name' }, + params: { id: '42', name: 'Param Name' }, + query: { page: '1' } + }, + { + rules: { + id: [required(), string()], + page: [required(), string()], + name: [required(), string()] + } + } + ); + + await expect(request.validateOrThrow()).resolves.toEqual({ + id: '42', + page: '1', + name: 'Body Name' + }); + }); + + it('handles missing or malformed Express-like sources as empty objects', async () => { + const request = FormRequest.fromExpress<{ name?: string }>( + { + body: 'not-an-object', + params: null, + query: ['bad'] + }, + { + rules: { + name: [optional(), string()] + } + } + ); + + await expect(request.validateOrThrow()).resolves.toEqual({}); + }); + + it('covers empty and measurable edge cases', async () => { + expect(isEmpty(undefined)).toBe(true); + expect(isEmpty(null)).toBe(true); + expect(isEmpty('')).toBe(true); + expect(isEmpty(0)).toBe(false); + + const request = new FormRequest({ + input: { + nested: {}, + count: 3, + unknown: {} + }, + rules: { + nested: [min(1)], + count: [min(2), max(4)], + unknown: [max(1)] + } + }); + + const result = await request.validate(); + + expect(result.valid).toBe(false); + expect(result.issues).toHaveLength(1); + expect(result.issues[0]?.field).toBe('unknown'); + }); + + it('accepts empty optional values and alternate accepted values', async () => { + const request = new FormRequest({ + input: { + title: '', + score: '', + website: '', + tags: '', + enabled: '', + termsYes: 'yes', + termsOn: 'on', + termsOne: 1, + role: '' + }, + rules: { + title: [string(), length(4)], + score: [number(), min(3), max(5), between(3, 5)], + website: [url()], + tags: [array()], + enabled: [boolean()], + termsYes: [accepted()], + termsOn: [accepted()], + termsOne: [accepted()], + role: [inValues(['admin'])] + } + }); + + const result = await request.validate(); + + expect(result.valid).toBe(true); + }); + + it('rejects non-string URLs and malformed URLs', async () => { + const request = new FormRequest({ + input: { + objectUrl: {}, + malformedUrl: 'not a url', + secureUrl: 'https://example.com' + }, + rules: { + objectUrl: [url()], + malformedUrl: [url()], + secureUrl: [url()] + } + }); + + const result = await request.validate(); + + expect(result.errors.objectUrl).toEqual(['The objectUrl must be a valid URL.']); + expect(result.errors.malformedUrl).toEqual(['The malformedUrl must be a valid URL.']); + expect(result.errors.secureUrl).toBeUndefined(); + }); + + it('exposes empty state before validation and covers default rule messages', async () => { + const request = new FormRequest({ + input: { + title: 12, + enabled: 'yes', + short: 'a', + long: 'abcdef', + maybe: undefined, + nullableValue: null + }, + rules: { + title: [string()], + enabled: [boolean()], + short: [min(2)], + long: [max(3)], + maybe: [optional()], + nullableValue: [nullable()] + } + }); + + expect(request.validated()).toEqual({}); + expect(request.errors()).toEqual({}); + expect(optional().validate({ field: 'x', input: {}, label: 'x', value: undefined })).toBe(true); + expect(nullable().validate({ field: 'x', input: {}, label: 'x', value: null })).toBe(true); + + const result = await request.validate(); + + expect(result.errors.title).toEqual(['The title must be a string.']); + expect(result.errors.enabled).toEqual(['The enabled must be true or false.']); + expect(result.errors.short).toEqual(['The short must be at least 2.']); + expect(result.errors.long).toEqual(['The long must be at most 3.']); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..eacd099 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "declaration": true, + "exactOptionalPropertyTypes": true, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noUncheckedIndexedAccess": true, + "outDir": "dist", + "rootDir": ".", + "strict": true, + "target": "ES2022" + }, + "include": [ + "src", + "tests", + "vitest.config.ts", + "eslint.config.js" + ] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..eeb4e6d --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + coverage: { + all: true, + exclude: ['src/types.ts'], + include: ['src/**/*.ts'], + provider: 'v8', + thresholds: { + branches: 100, + functions: 100, + lines: 100, + statements: 100 + } + } + } +}); From fd3657b935b62f040ae044106dd1d03408bd85fb Mon Sep 17 00:00:00 2001 From: Viet Vu Date: Wed, 13 May 2026 20:02:35 +0700 Subject: [PATCH 2/3] fix: address PR review feedback --- README.md | 4 ++-- eslint.config.js | 4 +++- package-lock.json | 2 +- package.json | 4 ++-- src/rules/index.ts | 20 +++++++++++++++----- tests/formrequest.test.ts | 9 +++++---- tsconfig.json | 7 ++----- 7 files changed, 30 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index c026f98..d64842f 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![CI](https://github.com/jooservices/node-formrequest/actions/workflows/ci.yml/badge.svg?branch=develop)](https://github.com/jooservices/node-formrequest/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -Framework-neutral FormRequest validation for Node.js and TypeScript with an optional Express-style adapter. +Framework-neutral FormRequest validation for Node.js and TypeScript with an optional Express 4-style adapter. Package name: `@jooservices/formrequest` @@ -42,7 +42,7 @@ const data = await request.validateOrThrow(); ## Scope -This package intentionally does not include Laravel string rule parsing, database validation rules, a service container, or business logic in FormRequest classes. Express support is adapter-style and optional. +This package intentionally does not include Laravel string rule parsing, database validation rules, a service container, or business logic in FormRequest classes. Express support is adapter-style, optional, and targets Express 4.x through the optional peer dependency. ## Development diff --git a/eslint.config.js b/eslint.config.js index b59b759..67e6ce4 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -7,7 +7,9 @@ export default tseslint.config( { languageOptions: { parserOptions: { - projectService: true, + projectService: { + allowDefaultProject: ['*.config.ts', 'tests/*.ts'] + }, tsconfigRootDir: import.meta.dirname } } diff --git a/package-lock.json b/package-lock.json index 758319a..da8a3c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "node": ">=20" }, "peerDependencies": { - "express": ">=4.18 <6" + "express": ">=4.18 <5" }, "peerDependenciesMeta": { "express": { diff --git a/package.json b/package.json index fe5d504..3ca8ffe 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@jooservices/formrequest", "version": "0.0.0", - "description": "Framework-neutral FormRequest validation for Node.js with an optional Express adapter.", + "description": "Framework-neutral FormRequest validation for Node.js with an optional Express 4 adapter.", "type": "module", "license": "MIT", "author": "JOOservices ", @@ -45,7 +45,7 @@ "ci": "npm run lint && npm run typecheck && npm run test:coverage && npm run build && npm pack --dry-run" }, "peerDependencies": { - "express": ">=4.18 <6" + "express": ">=4.18 <5" }, "peerDependenciesMeta": { "express": { diff --git a/src/rules/index.ts b/src/rules/index.ts index cd0e17c..3d63525 100644 --- a/src/rules/index.ts +++ b/src/rules/index.ts @@ -52,7 +52,8 @@ export const min = (minimum: number): Rule => return true; } - return measurableLength(value) >= minimum; + const length = measurableLength(value); + return !Number.isNaN(length) && length >= minimum; }, `The :attribute must be at least ${String(minimum)}.`); export const max = (maximum: number): Rule => @@ -61,7 +62,8 @@ export const max = (maximum: number): Rule => return true; } - return measurableLength(value) <= maximum; + const length = measurableLength(value); + return !Number.isNaN(length) && length <= maximum; }, `The :attribute must be at most ${String(maximum)}.`); export const between = (minimum: number, maximum: number): Rule => @@ -71,11 +73,18 @@ export const between = (minimum: number, maximum: number): Rule => } const length = measurableLength(value); - return length >= minimum && length <= maximum; + return !Number.isNaN(length) && length >= minimum && length <= maximum; }, `The :attribute must be between ${String(minimum)} and ${String(maximum)}.`); export const length = (expected: number): Rule => - createRule('length', ({ value }) => isEmpty(value) || measurableLength(value) === expected, `The :attribute must be ${String(expected)}.`); + createRule('length', ({ value }) => { + if (isEmpty(value)) { + return true; + } + + const length = measurableLength(value); + return !Number.isNaN(length) && length === expected; + }, `The :attribute must be ${String(expected)}.`); export const regex = (pattern: RegExp): Rule => createRule('regex', ({ value }) => isEmpty(value) || (typeof value === 'string' && pattern.test(value)), 'The :attribute format is invalid.'); @@ -95,7 +104,8 @@ const measurableLength = (value: unknown): number => { return value.length; } - return Number.POSITIVE_INFINITY; + // Length-based rules only support numbers, strings, and arrays. + return Number.NaN; }; const isPlainObject = (value: unknown): value is InputBag => diff --git a/tests/formrequest.test.ts b/tests/formrequest.test.ts index b88d993..b2f3f62 100644 --- a/tests/formrequest.test.ts +++ b/tests/formrequest.test.ts @@ -120,7 +120,7 @@ describe('FormRequest', () => { expect(result.valid).toBe(false); expect(result.errors.email).toEqual(['Use a real email address.']); expect(result.errors.age).toEqual(['age must be numeric.']); - expect(result.issues.map((issue) => issue.rule)).toEqual([ + expect(result.issues.map((issue) => issue.rule)).toEqual(expect.arrayContaining([ 'required', 'email', 'number', @@ -132,7 +132,8 @@ describe('FormRequest', () => { 'regex', 'in', 'between' - ]); + ])); + expect(result.issues).toHaveLength(11); }); it('throws for authorization and invalid validation paths', async () => { @@ -218,8 +219,8 @@ describe('FormRequest', () => { const result = await request.validate(); expect(result.valid).toBe(false); - expect(result.issues).toHaveLength(1); - expect(result.issues[0]?.field).toBe('unknown'); + expect(result.issues).toHaveLength(2); + expect(result.issues.map((issue) => issue.field)).toEqual(expect.arrayContaining(['nested', 'unknown'])); }); it('accepts empty optional values and alternate accepted values', async () => { diff --git a/tsconfig.json b/tsconfig.json index eacd099..2612292 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,14 +6,11 @@ "moduleResolution": "NodeNext", "noUncheckedIndexedAccess": true, "outDir": "dist", - "rootDir": ".", + "rootDir": "src", "strict": true, "target": "ES2022" }, "include": [ - "src", - "tests", - "vitest.config.ts", - "eslint.config.js" + "src" ] } From 40e9c3aca0d9f8e8ab768ae8b0b810d7fa4b64aa Mon Sep 17 00:00:00 2001 From: Viet Vu Date: Wed, 13 May 2026 20:18:30 +0700 Subject: [PATCH 3/3] feat: align FormRequest with Zod class architecture --- .github/skills/formrequest-package/SKILL.md | 22 +- AGENTS.md | 22 +- CHANGELOG.md | 18 +- CLAUDE.md | 9 +- README.md | 165 +++++- ai/skills/README.md | 5 +- ai/skills/USAGE.md | 5 +- docs/README.md | 5 + docs/ai-skills.md | 3 + docs/ci-cd.md | 12 +- docs/release-process.md | 12 +- package-lock.json | 20 +- package.json | 13 +- src/FormRequest.ts | 192 ++----- src/adapters/express.ts | 49 ++ src/exceptions/AuthorizationException.ts | 9 + src/exceptions/FormRequestException.ts | 8 + src/exceptions/ValidationException.ts | 19 + src/exceptions/index.ts | 4 + src/index.ts | 25 +- src/rules/createRule.ts | 16 - src/rules/index.ts | 112 ---- src/types.ts | 52 +- tests/formrequest.test.ts | 581 +++++++++++--------- 24 files changed, 731 insertions(+), 647 deletions(-) create mode 100644 src/adapters/express.ts create mode 100644 src/exceptions/AuthorizationException.ts create mode 100644 src/exceptions/FormRequestException.ts create mode 100644 src/exceptions/ValidationException.ts create mode 100644 src/exceptions/index.ts delete mode 100644 src/rules/createRule.ts delete mode 100644 src/rules/index.ts diff --git a/.github/skills/formrequest-package/SKILL.md b/.github/skills/formrequest-package/SKILL.md index 5f279d0..fbd5449 100644 --- a/.github/skills/formrequest-package/SKILL.md +++ b/.github/skills/formrequest-package/SKILL.md @@ -1,21 +1,34 @@ # FormRequest Package Skill -Use this skill when changing the FormRequest runtime, rule factories, validation errors, Express-style adapter, docs, or release metadata. +Use this skill when changing the FormRequest runtime, Zod validation lifecycle, exceptions, Express 4 adapter, docs, tests, or release metadata. ## Runtime Ownership -- `src/FormRequest.ts` owns validation orchestration, authorization checks, validated data, grouped errors, and Express-like input merging. -- `src/rules/` owns reusable validation rules. +- `src/FormRequest.ts` owns validation orchestration, authorization checks, validated data, context storage, and context helpers. +- `src/exceptions/` owns standardized FormRequest exceptions. +- `src/adapters/express.ts` owns optional Express 4-style middleware behavior. - `src/types.ts` owns public TypeScript contracts. - `tests/` must cover public behavior at 100% statement, branch, function, and line coverage. ## Guardrails +- Inspect source before editing; do not assume. +- `rules()` returns a Zod schema. - Do not add Laravel string rules. -- Do not add database validation rules. +- Do not add database validation rules into the base package. - Do not add a service container. - Do not put business logic inside FormRequest. +- Keep the core framework-neutral. - Do not require Express at runtime. +- Express adapter targets Express 4.x. +- Do not weaken 100% coverage. +- Update docs when behavior changes. + +## Versioning + +- Feature PRs keep version `0.0.0`. +- Initial release target is `0.1.0`. +- Version bump happens only in `release/0.1.0`. ## Validation @@ -28,3 +41,4 @@ npm run test:coverage npm run build npm pack --dry-run ``` + diff --git a/AGENTS.md b/AGENTS.md index 100fcf5..12ab20a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,13 +1,17 @@ # JOOservices Node FormRequest Repository Instructions -This repository is a Node.js package named `@jooservices/formrequest`. +This repository is a Node.js package named `@jooservices/node-formrequest`. ## Core Intent -- Keep FormRequest focused on validation and authorization. +- Inspect source before editing; do not assume docs or previous implementation are correct. +- Keep FormRequest class-based and Laravel-inspired, not Laravel-compatible. +- `rules()` returns a Zod schema and validation uses `parseAsync`. +- Keep the core framework-neutral. +- Keep the optional adapter targeted to Express 4.x. - Keep business logic outside FormRequest. -- Keep framework integration adapter-based and optional. - Update tests and docs with behavior changes. +- Do not weaken the 100% coverage gate. - Follow the approved `master` and `develop` Git flow. ## Required Commands @@ -21,10 +25,17 @@ This repository is a Node.js package named `@jooservices/formrequest`. ## Architecture Rules - No Laravel string rule parser. -- No database-backed validation rules. +- No database-backed validation rules in the base package. - No service container. - No persistence or business workflows. -- No framework lock-in beyond optional Express-like request input support. +- No Express import in the core FormRequest class. +- Do not document unimplemented behavior. + +## Versioning + +- Feature PRs into `develop` keep `package.json` at `0.0.0`. +- Initial release target is `0.1.0`. +- Version bump happens only in `release/0.1.0`. ## Branch Workflow @@ -33,3 +44,4 @@ This repository is a Node.js package named `@jooservices/formrequest`. - `feature/*`: branch from latest `develop` and PR back into `develop`. - `release/`: branch from latest `develop` and PR into `master`. - Tags are created from `master` only. + diff --git a/CHANGELOG.md b/CHANGELOG.md index 095dd2b..e435353 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,17 @@ All notable changes to this project will be documented in this file. ### Added -- Initial TypeScript FormRequest package with framework-neutral validation. -- Optional Express-style request adapter. -- Rule factories for common scalar, collection, comparison, and format validation. -- Documentation, AI contributor guidance, CI, and full local validation scripts. +- Laravel-inspired class-based FormRequest abstraction. +- Zod-based validation lifecycle using async parsing. +- Optional Express 4 adapter. +- Authorization and validation exceptions. +- Context helpers for body, params, query, headers, and user. +- 100% coverage quality gate. +- CI/CD validation workflow. +- AI contributor guidance. + +### Notes + +- Initial release target is `0.1.0`. +- Version bump will happen in `release/0.1.0`. + diff --git a/CLAUDE.md b/CLAUDE.md index 771a7b2..757dd92 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,8 +4,13 @@ Read [AGENTS.md](./AGENTS.md) first. When working in this repository: -- Match the existing TypeScript style and public API shape. +- Inspect actual source and tests before changing behavior. +- Match the class-based Zod FormRequest architecture. - Keep changes small, typed, tested, and documented. -- Preserve framework-neutral behavior. +- Preserve framework-neutral core behavior. +- Keep the Express adapter optional and Express 4.x-focused. +- Do not add Laravel string parsers, database rules, service containers, or business logic. - Run lint, typecheck, coverage, build, and package dry-run before PRs. +- Keep `package.json` at `0.0.0` until `release/0.1.0`. - Follow the approved Git flow: feature work into `develop`, release PRs into `master`, and tags from `master`. + diff --git a/README.md b/README.md index d64842f..d9d82cc 100644 --- a/README.md +++ b/README.md @@ -3,46 +3,152 @@ [![CI](https://github.com/jooservices/node-formrequest/actions/workflows/ci.yml/badge.svg?branch=develop)](https://github.com/jooservices/node-formrequest/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -Framework-neutral FormRequest validation for Node.js and TypeScript with an optional Express 4-style adapter. +Laravel-inspired FormRequest validation for Node.js and TypeScript with Zod and an optional Express 4 adapter. -Package name: `@jooservices/formrequest` +Package name: `@jooservices/node-formrequest` + +Initial release target: `0.1.0`. This feature branch keeps `package.json` at `0.0.0`; `release/0.1.0` will perform the version bump. ## Install ```bash -npm install @jooservices/formrequest +npm install @jooservices/node-formrequest zod ``` -## Quick Example +## What It Is + +- A Laravel-inspired FormRequest lifecycle for Node.js and TypeScript. +- Zod-powered: `rules()` returns a Zod schema and validation uses `parseAsync`. +- Framework-neutral core with an optional Express 4-style adapter. +- Class-based: create request classes by extending `FormRequest`. + +## What It Is Not + +- Not Laravel-compatible. +- No Laravel string rules such as `required|email|min:8`. +- No database validation rules. +- No service container. +- No business logic layer. +- No Express 5 support unless explicitly tested in a future change. + +## Basic Usage ```ts -import { FormRequest, email, required, string } from '@jooservices/formrequest'; - -const request = new FormRequest({ - input: { - name: 'Jane Doe', - email: 'jane@example.com' - }, - rules: { - name: [required(), string()], - email: [required(), email()] +import { z } from 'zod'; +import { FormRequest } from '@jooservices/node-formrequest'; + +export class CreateUserRequest extends FormRequest { + rules() { + return z.object({ + body: z.object({ + name: z.string().min(1).max(255), + email: z.email(), + password: z.string().min(8) + }), + params: z.object({}).optional(), + query: z.object({}).optional() + }); } + + authorize() { + return true; + } +} + +const request = new CreateUserRequest(); +const validated = await request.validate({ + body: req.body, + params: req.params, + query: req.query, + headers: req.headers, + user: req.user }); -const data = await request.validateOrThrow(); +const body = validated.body; ``` -## Public API +## Express 4 Adapter + +```ts +import { validateFormRequest } from '@jooservices/node-formrequest'; + +router.post( + '/users', + validateFormRequest(CreateUserRequest), + controller.store +); +``` + +On success, the adapter attaches: + +- `req.validated` +- `req.formRequest` + +On authorization or validation failure, it calls `next(error)` and does not call the downstream handler. + +## Error Handling + +```ts +import { + AuthorizationException, + ValidationException +} from '@jooservices/node-formrequest'; + +try { + await request.validate(context); +} catch (error) { + if (error instanceof AuthorizationException) { + // This action is unauthorized. + } + + if (error instanceof ValidationException) { + console.log(error.issues); + console.log(error.flattened); + } +} +``` + +## Authorization + +Override `authorize()` for synchronous or asynchronous authorization: + +```ts +class UpdateUserRequest extends FormRequest { + rules() { + return z.object({ + params: z.object({ id: z.string() }), + user: z.object({ id: z.string() }) + }); + } -- `FormRequest`: validates declared fields and returns only validated data. -- `FormRequest.fromExpress`: merges `params`, `query`, and `body` for Express-like requests. -- `AuthorizationError`: thrown when `authorize` returns false. -- `ValidationError`: thrown by `validateOrThrow` when validation fails. -- Rule factories: `required`, `optional`, `nullable`, `string`, `number`, `boolean`, `array`, `object`, `email`, `url`, `min`, `max`, `between`, `length`, `regex`, `accepted`, and `inValues`. + async authorize() { + return this.params<{ id: string }>()?.id === this.user<{ id: string }>()?.id; + } +} +``` + +## Messages And Attributes + +`messages()` and `attributes()` are metadata helpers in v0.1. Their values are included on `ValidationException`. + +This is not full Laravel i18n or message interpolation. -## Scope +## TypeScript Notes -This package intentionally does not include Laravel string rule parsing, database validation rules, a service container, or business logic in FormRequest classes. Express support is adapter-style, optional, and targets Express 4.x through the optional peer dependency. +- The core API is TypeScript-first. +- `FormRequest` may be used with a concrete Zod schema for inferred validation results. +- The core package does not import Express types. +- Zod v4 is the tested validation engine for this feature PR. + +## Public API + +- `FormRequest` +- `FormRequestContext` +- `FormRequestConstructor` +- `validateFormRequest` +- `FormRequestException` +- `AuthorizationException` +- `ValidationException` ## Development @@ -54,9 +160,12 @@ npm run build npm pack --dry-run ``` -Approved Git flow: +## Git And Release Flow + +- Feature work branches from `develop` and opens PRs back into `develop`. +- Initial release preparation will use `release/0.1.0` from `develop`. +- `release/0.1.0` will bump `package.json` from `0.0.0` to `0.1.0`. +- Release PRs target `master`. +- Tags are created from `master`. +- `master` is merged back into `develop` after release completion. -- normal feature and fix work branches from `develop` and PRs back into `develop` -- release preparation uses `release/` from `develop`, then PRs into `master` -- releases are tagged from `master` -- `master` merges back into `develop` after release completion diff --git a/ai/skills/README.md b/ai/skills/README.md index f3bbc3a..2998979 100644 --- a/ai/skills/README.md +++ b/ai/skills/README.md @@ -2,4 +2,7 @@ - [FormRequest Package](../../.github/skills/formrequest-package/SKILL.md) -Use these notes when an AI agent changes validation behavior, public API, docs, release metadata, or CI. +Use these notes when an AI agent changes validation behavior, public API, docs, tests, release metadata, or CI. + +The package is class-based and Zod-powered. Do not add Laravel string parsers, database rules, or framework lock-in to the core. + diff --git a/ai/skills/USAGE.md b/ai/skills/USAGE.md index 9235568..604331c 100644 --- a/ai/skills/USAGE.md +++ b/ai/skills/USAGE.md @@ -1,6 +1,6 @@ # AI Skills Usage -Before changing source code, read `.github/skills/formrequest-package/SKILL.md`. +Before changing source code, read `.github/skills/formrequest-package/SKILL.md` and inspect the current implementation. After source changes, run: @@ -11,3 +11,6 @@ npm run test:coverage npm run build npm pack --dry-run ``` + +Keep feature branches at version `0.0.0`; `release/0.1.0` performs the initial `0.1.0` version bump. + diff --git a/docs/README.md b/docs/README.md index 4204008..a216ca2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,5 +1,10 @@ # Documentation Hub +This package is `@jooservices/node-formrequest`. + - [Release Process](./release-process.md) - [CI/CD](./ci-cd.md) - [AI Skills](./ai-skills.md) + +Implementation source is the authority. Keep docs aligned with the class-based Zod FormRequest API and do not document unimplemented Laravel compatibility. + diff --git a/docs/ai-skills.md b/docs/ai-skills.md index 3026b30..5cd436f 100644 --- a/docs/ai-skills.md +++ b/docs/ai-skills.md @@ -7,3 +7,6 @@ AI contributor guidance is available in: - `.github/skills/formrequest-package/SKILL.md` - `ai/skills/README.md` - `ai/skills/USAGE.md` + +Agents must inspect source before editing, preserve the class-based Zod API, keep core framework-neutral, and update docs and tests with behavior changes. + diff --git a/docs/ci-cd.md b/docs/ci-cd.md index abb7120..6166455 100644 --- a/docs/ci-cd.md +++ b/docs/ci-cd.md @@ -1,5 +1,13 @@ # CI/CD -Pull requests run linting, type checking, coverage tests, build, and package dry-run. +Pull requests to `develop` and `master`, plus pushes to those branches, run: + +- `npm ci` +- `npm run lint` +- `npm run typecheck` +- `npm run test:coverage` +- `npm run build` +- `npm pack --dry-run` + +Coverage thresholds are set to 100% for statements, branches, functions, and lines. Do not lower them. -Coverage thresholds are set to 100% for statements, branches, functions, and lines. diff --git a/docs/release-process.md b/docs/release-process.md index ad580e2..33c52d6 100644 --- a/docs/release-process.md +++ b/docs/release-process.md @@ -1,11 +1,17 @@ # Release Process +Initial release target: `0.1.0`. + +The feature branch keeps `package.json` at `0.0.0`. Version bumping happens only in the release branch. + 1. Merge approved feature work into `develop`. -2. Branch `release/` from the latest `develop`. -3. Update `package.json`, lockfile, `CHANGELOG.md`, and release notes. +2. Branch `release/0.1.0` from the latest `develop`. +3. Update `package.json`, lockfile, `CHANGELOG.md`, and release notes for `0.1.0`. 4. Run the full validation command set. 5. Open a release PR into `master`. 6. Merge only after CI, required reviews, and review threads are complete. -7. Tag `v` from `master`. +7. Tag `v0.1.0` from `master`. 8. Create a GitHub release when tooling permits. 9. Merge `master` back into `develop`. +10. Clean up merged release branches when safe. + diff --git a/package-lock.json b/package-lock.json index da8a3c1..1db206f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "@jooservices/formrequest", + "name": "@jooservices/node-formrequest", "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@jooservices/formrequest", + "name": "@jooservices/node-formrequest", "version": "0.0.0", "license": "MIT", "devDependencies": { @@ -16,13 +16,15 @@ "tsup": "^8.4.0", "typescript": "^5.8.3", "typescript-eslint": "^8.32.0", - "vitest": "^3.1.3" + "vitest": "^3.1.3", + "zod": "^4.0.0" }, "engines": { "node": ">=20" }, "peerDependencies": { - "express": ">=4.18 <5" + "express": ">=4.18 <5", + "zod": "^4.0.0" }, "peerDependenciesMeta": { "express": { @@ -4136,6 +4138,16 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 3ca8ffe..84f0726 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "@jooservices/formrequest", + "name": "@jooservices/node-formrequest", "version": "0.0.0", - "description": "Framework-neutral FormRequest validation for Node.js with an optional Express 4 adapter.", + "description": "Laravel-inspired FormRequest validation for Node.js and TypeScript with Zod and an optional Express 4 adapter.", "type": "module", "license": "MIT", "author": "JOOservices ", @@ -32,8 +32,11 @@ }, "files": [ "dist", + "docs", + "AGENTS.md", "README.md", "CHANGELOG.md", + "CLAUDE.md", "LICENSE" ], "scripts": { @@ -45,7 +48,8 @@ "ci": "npm run lint && npm run typecheck && npm run test:coverage && npm run build && npm pack --dry-run" }, "peerDependencies": { - "express": ">=4.18 <5" + "express": ">=4.18 <5", + "zod": "^4.0.0" }, "peerDependenciesMeta": { "express": { @@ -60,7 +64,8 @@ "tsup": "^8.4.0", "typescript": "^5.8.3", "typescript-eslint": "^8.32.0", - "vitest": "^3.1.3" + "vitest": "^3.1.3", + "zod": "^4.0.0" }, "engines": { "node": ">=20" diff --git a/src/FormRequest.ts b/src/FormRequest.ts index 5da668a..1c33131 100644 --- a/src/FormRequest.ts +++ b/src/FormRequest.ts @@ -1,171 +1,79 @@ -import { isEmpty } from './rules/createRule.js'; -import type { - ExpressLikeRequest, - FormRequestConfig, - InputBag, - Rule, - RuleContext, - ValidationIssue, - ValidationMessage, - ValidationResult -} from './types.js'; - -export class AuthorizationError extends Error { - public constructor(message = 'This action is unauthorized.') { - super(message); - this.name = 'AuthorizationError'; - } -} - -export class ValidationError extends Error { - public constructor(public readonly result: ValidationResult) { - super('The given data was invalid.'); - this.name = 'ValidationError'; - } -} +import type { z } from 'zod'; +import { ZodError } from 'zod'; +import { AuthorizationException, ValidationException } from './exceptions/index.js'; +import type { FormRequestContext } from './types.js'; -export class FormRequest { - private readonly input: InputBag; - private readonly ruleSet: FormRequestConfig['rules']; - private readonly customMessages: NonNullable['messages']>; - private readonly customAttributes: Partial>; - private readonly authorizeCallback: NonNullable['authorize']>; - private result?: ValidationResult; - - public constructor(config: FormRequestConfig) { - this.input = config.input; - this.ruleSet = config.rules; - this.customMessages = config.messages ?? {}; - this.customAttributes = config.attributes ?? {}; - this.authorizeCallback = config.authorize ?? (() => true); - } +export abstract class FormRequest { + private storedContext?: FormRequestContext; + private storedValidated?: z.infer; - public static fromExpress( - request: ExpressLikeRequest, - config: Omit, 'input'> - ): FormRequest { - return new FormRequest({ - ...config, - input: { - ...toInputBag(request.params), - ...toInputBag(request.query), - ...toInputBag(request.body) - } - }); - } - - public async validate(): Promise> { - if (!(await this.authorizeCallback(this.input))) { - throw new AuthorizationError(); - } - - this.result = this.runRules(); - return this.result; - } - - public async validateOrThrow(): Promise { - const result = await this.validate(); - - if (!result.valid) { - throw new ValidationError(result); - } + public abstract rules(): TSchema; - return result.data as TData; + public authorize(): boolean | Promise { + return true; } - public validated(): Partial { - return this.result?.data ?? {}; + public messages(): Record { + return {}; } - public errors(): Record { - return this.result?.errors ?? {}; + public attributes(): Record { + return {}; } - private runRules(): ValidationResult { - const issues: ValidationIssue[] = []; - const data: Partial = {}; + public async validate(context: FormRequestContext): Promise> { + this.storedContext = context; - for (const [field, rules] of Object.entries(this.ruleSet)) { - const value = this.input[field]; - const shouldSkip = this.shouldSkipField(value, rules); - - if (shouldSkip) { - continue; - } + if (!(await this.authorize())) { + throw new AuthorizationException(); + } - for (const rule of rules) { - if (rule.optional || rule.nullable) { - continue; - } + const schema = this.rules(); - const context = this.makeContext(field, value); + try { + const validated = await schema.parseAsync(context); + this.storedValidated = validated; - if (!rule.validate(context)) { - issues.push({ - field, - rule: rule.name, - message: this.resolveMessage(field, rule, context) - }); - } + return validated; + } catch (error) { + if (error instanceof ZodError) { + throw new ValidationException(error, this.messages(), this.attributes()); } - if (!issues.some((issue) => issue.field === field) && Object.hasOwn(this.input, field)) { - data[field as keyof TData] = value as TData[keyof TData]; - } + throw error; } - - return { - valid: issues.length === 0, - data, - errors: groupIssues(issues), - issues - }; } - private shouldSkipField(value: unknown, rules: readonly Rule[]): boolean { - const allowsOptional = rules.some((rule) => rule.optional); - const allowsNull = rules.some((rule) => rule.nullable); - - return (allowsOptional && value === undefined) || (allowsNull && value === null); + public validated(): z.infer | undefined { + return this.storedValidated; } - private makeContext(field: string, value: unknown): RuleContext { - return { - field, - input: this.input, - label: this.customAttributes[field] ?? field, - value - }; + public context(): FormRequestContext | undefined { + return this.storedContext; } - private resolveMessage(field: string, rule: Rule, context: RuleContext): string { - const specific = this.customMessages[`${field}.${rule.name}`]; - const generic = this.customMessages[rule.name]; - const message = specific ?? generic; - - if (message !== undefined) { - return formatCustomMessage(message, context); - } - - return rule.message(context); + /* eslint-disable @typescript-eslint/no-unnecessary-type-parameters -- These generic accessors are the documented public API. */ + public body(): T | undefined { + return fromContext(this.storedContext?.body); } -} -const formatCustomMessage = (message: ValidationMessage, context: RuleContext): string => - typeof message === 'function' ? message(context) : message.replaceAll(':attribute', context.label); + public params(): T | undefined { + return fromContext(this.storedContext?.params); + } -const groupIssues = (issues: readonly ValidationIssue[]): Record => - issues.reduce>((errors, issue) => { - errors[issue.field] = [...(errors[issue.field] ?? []), issue.message]; - return errors; - }, {}); + public query(): T | undefined { + return fromContext(this.storedContext?.query); + } -const toInputBag = (value: unknown): InputBag => { - if (typeof value === 'object' && value !== null && !Array.isArray(value)) { - return value as InputBag; + public headers(): T | undefined { + return fromContext(this.storedContext?.headers); } - return {}; -}; + public user(): T | undefined { + return fromContext(this.storedContext?.user); + } + /* eslint-enable @typescript-eslint/no-unnecessary-type-parameters */ +} -export { isEmpty }; +// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- Supports the public context accessor generics above. +const fromContext = (value: unknown): T | undefined => value as T | undefined; diff --git a/src/adapters/express.ts b/src/adapters/express.ts new file mode 100644 index 0000000..49c4149 --- /dev/null +++ b/src/adapters/express.ts @@ -0,0 +1,49 @@ +import type { FormRequest } from '../FormRequest.js'; +import type { FormRequestConstructor, FormRequestContext } from '../types.js'; + +export interface ExpressLikeRequest { + readonly body?: unknown; + readonly params?: unknown; + readonly query?: unknown; + readonly headers?: unknown; + readonly user?: unknown; + validated?: unknown; + formRequest?: FormRequest; +} + +export interface ExpressLikeResponse { + readonly [key: string]: unknown; +} + +export type ExpressLikeNext = (error?: unknown) => void; + +export type ExpressLikeMiddleware = ( + request: ExpressLikeRequest, + response: ExpressLikeResponse, + next: ExpressLikeNext +) => Promise; + +export const validateFormRequest = ( + RequestClass: FormRequestConstructor +): ExpressLikeMiddleware => + async (request, _response, next): Promise => { + const formRequest = new RequestClass(); + const context = makeContext(request); + + try { + request.validated = await formRequest.validate(context); + request.formRequest = formRequest; + next(); + } catch (error) { + next(error); + } + }; + +const makeContext = (request: ExpressLikeRequest): FormRequestContext => ({ + body: request.body, + params: request.params, + query: request.query, + headers: request.headers, + user: request.user +}); + diff --git a/src/exceptions/AuthorizationException.ts b/src/exceptions/AuthorizationException.ts new file mode 100644 index 0000000..90819e4 --- /dev/null +++ b/src/exceptions/AuthorizationException.ts @@ -0,0 +1,9 @@ +import { FormRequestException } from './FormRequestException.js'; + +export class AuthorizationException extends FormRequestException { + public constructor(message = 'This action is unauthorized.') { + super(message); + this.name = 'AuthorizationException'; + } +} + diff --git a/src/exceptions/FormRequestException.ts b/src/exceptions/FormRequestException.ts new file mode 100644 index 0000000..76a6cd9 --- /dev/null +++ b/src/exceptions/FormRequestException.ts @@ -0,0 +1,8 @@ +export class FormRequestException extends Error { + public constructor(message: string) { + super(message); + this.name = 'FormRequestException'; + Object.setPrototypeOf(this, new.target.prototype); + } +} + diff --git a/src/exceptions/ValidationException.ts b/src/exceptions/ValidationException.ts new file mode 100644 index 0000000..802fb78 --- /dev/null +++ b/src/exceptions/ValidationException.ts @@ -0,0 +1,19 @@ +import { z, ZodError } from 'zod'; +import { FormRequestException } from './FormRequestException.js'; + +export class ValidationException extends FormRequestException { + public readonly issues: z.core.$ZodIssue[]; + public readonly flattened: z.core.$ZodFlattenedError; + + public constructor( + public readonly error: ZodError, + public readonly messages: Record = {}, + public readonly attributes: Record = {}, + message = 'The given data was invalid.' + ) { + super(message); + this.name = 'ValidationException'; + this.issues = error.issues; + this.flattened = z.flattenError(error); + } +} diff --git a/src/exceptions/index.ts b/src/exceptions/index.ts new file mode 100644 index 0000000..a9bba83 --- /dev/null +++ b/src/exceptions/index.ts @@ -0,0 +1,4 @@ +export { AuthorizationException } from './AuthorizationException.js'; +export { FormRequestException } from './FormRequestException.js'; +export { ValidationException } from './ValidationException.js'; + diff --git a/src/index.ts b/src/index.ts index d700145..dbe8e99 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,18 +1,9 @@ +export { validateFormRequest } from './adapters/express.js'; +export { FormRequest } from './FormRequest.js'; export { - AuthorizationError, - FormRequest, - ValidationError, - isEmpty -} from './FormRequest.js'; -export * from './rules/index.js'; -export type { - ExpressLikeRequest, - FormRequestConfig, - InputBag, - Rule, - RuleContext, - RuleSet, - ValidationIssue, - ValidationMessage, - ValidationResult -} from './types.js'; + AuthorizationException, + FormRequestException, + ValidationException +} from './exceptions/index.js'; +export type { FormRequestConstructor, FormRequestContext } from './types.js'; + diff --git a/src/rules/createRule.ts b/src/rules/createRule.ts deleted file mode 100644 index 30f3cab..0000000 --- a/src/rules/createRule.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { Rule, RuleContext } from '../types.js'; - -export const isEmpty = (value: unknown): boolean => - value === undefined || value === null || value === ''; - -export const createRule = ( - name: string, - validate: (context: RuleContext) => boolean, - message: string, - options: Pick = {} -): Rule => ({ - name, - ...options, - validate, - message: (context) => message.replaceAll(':attribute', context.label) -}); diff --git a/src/rules/index.ts b/src/rules/index.ts deleted file mode 100644 index 3d63525..0000000 --- a/src/rules/index.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { createRule, isEmpty } from './createRule.js'; -import type { InputBag, Rule } from '../types.js'; - -export const required = (): Rule => - createRule('required', ({ value }) => !isEmpty(value), 'The :attribute field is required.'); - -export const optional = (): Rule => - createRule('optional', () => true, '', { optional: true }); - -export const nullable = (): Rule => - createRule('nullable', () => true, '', { nullable: true }); - -export const string = (): Rule => - createRule('string', ({ value }) => isEmpty(value) || typeof value === 'string', 'The :attribute must be a string.'); - -export const number = (): Rule => - createRule('number', ({ value }) => isEmpty(value) || typeof value === 'number', 'The :attribute must be a number.'); - -export const boolean = (): Rule => - createRule('boolean', ({ value }) => isEmpty(value) || typeof value === 'boolean', 'The :attribute must be true or false.'); - -export const array = (): Rule => - createRule('array', ({ value }) => isEmpty(value) || Array.isArray(value), 'The :attribute must be an array.'); - -export const object = (): Rule => - createRule('object', ({ value }) => isEmpty(value) || isPlainObject(value), 'The :attribute must be an object.'); - -export const email = (): Rule => - createRule('email', ({ value }) => isEmpty(value) || (typeof value === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)), 'The :attribute must be a valid email address.'); - -export const url = (): Rule => - createRule('url', ({ value }) => { - if (isEmpty(value)) { - return true; - } - - if (typeof value !== 'string') { - return false; - } - - try { - const parsed = new URL(value); - return parsed.protocol === 'http:' || parsed.protocol === 'https:'; - } catch { - return false; - } - }, 'The :attribute must be a valid URL.'); - -export const min = (minimum: number): Rule => - createRule('min', ({ value }) => { - if (isEmpty(value)) { - return true; - } - - const length = measurableLength(value); - return !Number.isNaN(length) && length >= minimum; - }, `The :attribute must be at least ${String(minimum)}.`); - -export const max = (maximum: number): Rule => - createRule('max', ({ value }) => { - if (isEmpty(value)) { - return true; - } - - const length = measurableLength(value); - return !Number.isNaN(length) && length <= maximum; - }, `The :attribute must be at most ${String(maximum)}.`); - -export const between = (minimum: number, maximum: number): Rule => - createRule('between', ({ value }) => { - if (isEmpty(value)) { - return true; - } - - const length = measurableLength(value); - return !Number.isNaN(length) && length >= minimum && length <= maximum; - }, `The :attribute must be between ${String(minimum)} and ${String(maximum)}.`); - -export const length = (expected: number): Rule => - createRule('length', ({ value }) => { - if (isEmpty(value)) { - return true; - } - - const length = measurableLength(value); - return !Number.isNaN(length) && length === expected; - }, `The :attribute must be ${String(expected)}.`); - -export const regex = (pattern: RegExp): Rule => - createRule('regex', ({ value }) => isEmpty(value) || (typeof value === 'string' && pattern.test(value)), 'The :attribute format is invalid.'); - -export const accepted = (): Rule => - createRule('accepted', ({ value }) => value === true || value === 'yes' || value === 'on' || value === 1, 'The :attribute must be accepted.'); - -export const inValues = (allowed: readonly unknown[]): Rule => - createRule('in', ({ value }) => isEmpty(value) || allowed.includes(value), 'The selected :attribute is invalid.'); - -const measurableLength = (value: unknown): number => { - if (typeof value === 'number') { - return value; - } - - if (typeof value === 'string' || Array.isArray(value)) { - return value.length; - } - - // Length-based rules only support numbers, strings, and arrays. - return Number.NaN; -}; - -const isPlainObject = (value: unknown): value is InputBag => - typeof value === 'object' && value !== null && !Array.isArray(value); diff --git a/src/types.ts b/src/types.ts index 24f7a79..d9a4c22 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,47 +1,13 @@ -export type InputBag = Record; +import type { FormRequest } from './FormRequest.js'; -export type ValidationMessage = string | ((context: RuleContext) => string); - -export interface RuleContext { - readonly field: string; - readonly label: string; - readonly value: unknown; - readonly input: InputBag; -} - -export interface ValidationIssue { - readonly field: string; - readonly rule: string; - readonly message: string; -} - -export interface ValidationResult { - readonly valid: boolean; - readonly data: Partial; - readonly errors: Record; - readonly issues: ValidationIssue[]; -} - -export interface Rule { - readonly name: string; - readonly optional?: boolean; - readonly nullable?: boolean; - validate(context: RuleContext): boolean; - message(context: RuleContext): string; -} - -export type RuleSet = Record; - -export interface FormRequestConfig { - readonly input: InputBag; - readonly rules: RuleSet; - readonly messages?: Partial>; - readonly attributes?: Partial>; - readonly authorize?: (input: InputBag) => boolean | Promise; -} - -export interface ExpressLikeRequest { +export interface FormRequestContext { readonly body?: unknown; - readonly query?: unknown; readonly params?: unknown; + readonly query?: unknown; + readonly headers?: unknown; + readonly user?: unknown; + readonly [key: string]: unknown; } + +export type FormRequestConstructor = new () => TRequest; + diff --git a/tests/formrequest.test.ts b/tests/formrequest.test.ts index b2f3f62..59a7a01 100644 --- a/tests/formrequest.test.ts +++ b/tests/formrequest.test.ts @@ -1,310 +1,373 @@ -import { describe, expect, it } from 'vitest'; +import { z, ZodError } from 'zod'; +import { describe, expect, it, vi } from 'vitest'; import { - AuthorizationError, + AuthorizationException, FormRequest, - ValidationError, - accepted, - array, - between, - boolean, - email, - inValues, - isEmpty, - length, - max, - min, - nullable, - number, - object, - optional, - regex, - required, - string, - url + FormRequestException, + ValidationException, + validateFormRequest } from '../src/index.js'; +import type { FormRequestConstructor, FormRequestContext } from '../src/index.js'; -interface SignupData { - name: string; - email: string; - age?: number; - website?: string | null; - newsletter: boolean; - tags: string[]; - meta: Record; - token: string; - role: string; +const userSchema = z.object({ + body: z.object({ + email: z.email(), + name: z.string().min(1).max(255) + }), + headers: z.record(z.string(), z.unknown()).optional(), + params: z.object({ + id: z.string() + }), + query: z.object({ + page: z.string().optional() + }), + user: z.object({ + id: z.string() + }).optional() +}); + +class CreateUserRequest extends FormRequest { + public rules(): typeof userSchema { + return userSchema; + } } describe('FormRequest', () => { - it('validates input and returns only declared fields', async () => { - const request = new FormRequest({ - input: { - name: 'Jane Doe', - email: 'jane@example.com', - age: 30, - website: null, - newsletter: true, - tags: ['node'], - meta: { source: 'test' }, - token: 'ABC123', - role: 'admin', - ignored: 'value' - }, - rules: { - name: [required(), string(), min(2), max(50), between(2, 50)], - email: [required(), email()], - age: [optional(), number()], - website: [nullable(), url()], - newsletter: [accepted(), boolean()], - tags: [array()], - meta: [object()], - token: [length(6), regex(/^[A-Z0-9]+$/)], - role: [inValues(['admin', 'editor'])] - } - }); + it('validates, stores validated data, exposes context helpers, and supports repeated calls', async () => { + const request = new CreateUserRequest(); + const firstContext = makeContext('first@example.com'); + const secondContext = makeContext('second@example.com', { extra: 'preserved' }); - const result = await request.validate(); - - expect(result.valid).toBe(true); - expect(result.data).toEqual({ - name: 'Jane Doe', - email: 'jane@example.com', - age: 30, - newsletter: true, - tags: ['node'], - meta: { source: 'test' }, - token: 'ABC123', - role: 'admin' - }); - expect(request.validated()).toEqual(result.data); - expect(request.errors()).toEqual({}); + expect(request.validated()).toBeUndefined(); + expect(request.context()).toBeUndefined(); + expect(request.body()).toBeUndefined(); + expect(request.params()).toBeUndefined(); + expect(request.query()).toBeUndefined(); + expect(request.headers()).toBeUndefined(); + expect(request.user()).toBeUndefined(); + expect(await request.authorize()).toBe(true); + + const firstValidated = await request.validate(firstContext); + + expect(firstValidated.body.email).toBe('first@example.com'); + expect(firstValidated.body).not.toHaveProperty('extra'); + expect(request.validated()).toEqual(firstValidated); + expect(request.context()).toEqual(firstContext); + expect(request.body()).toEqual(firstContext.body); + expect(request.params()).toEqual(firstContext.params); + expect(request.query()).toEqual(firstContext.query); + expect(request.headers()).toEqual(firstContext.headers); + expect(request.user()).toEqual(firstContext.user); + expect(request.context()?.customKey).toBe('custom-value'); + + const secondValidated = await request.validate(secondContext); + + expect(secondValidated.body.email).toBe('second@example.com'); + expect(request.validated()).toEqual(secondValidated); + expect(request.context()).toEqual(secondContext); }); - it('groups errors and supports custom messages and attributes', async () => { - const request = new FormRequest({ - input: { - name: '', - email: 'invalid', - age: 'old', - website: 'ftp://example.com', - newsletter: false, - tags: 'node', - meta: [], - token: 'abc', - role: 'guest', - score: 20 - }, - attributes: { - email: 'email address' - }, - messages: { - 'email.email': 'Use a real :attribute.', - number: ({ field }) => `${field} must be numeric.` - }, - rules: { - name: [required()], - email: [email()], - age: [number()], - website: [url()], - newsletter: [accepted()], - tags: [array()], - meta: [object()], - token: [length(6), regex(/^[A-Z0-9]+$/)], - role: [inValues(['admin'])], - score: [between(1, 10)] - } + it('supports async authorization and async Zod refinements', async () => { + const schema = z.object({ + body: z.object({ + token: z.string().refine(async (value) => (await Promise.resolve(value)) === 'valid-token') + }) }); - const result = await request.validate(); - - expect(result.valid).toBe(false); - expect(result.errors.email).toEqual(['Use a real email address.']); - expect(result.errors.age).toEqual(['age must be numeric.']); - expect(result.issues.map((issue) => issue.rule)).toEqual(expect.arrayContaining([ - 'required', - 'email', - 'number', - 'url', - 'accepted', - 'array', - 'object', - 'length', - 'regex', - 'in', - 'between' - ])); - expect(result.issues).toHaveLength(11); - }); + class AsyncRequest extends FormRequest { + public rules(): typeof schema { + return schema; + } - it('throws for authorization and invalid validation paths', async () => { - const unauthorized = new FormRequest({ - input: {}, - authorize: () => false, - rules: { - name: [required()] + public async authorize(): Promise { + return Promise.resolve(true); } + } + + await expect(new AsyncRequest().validate({ body: { token: 'valid-token' } })).resolves.toEqual({ + body: { token: 'valid-token' } }); + }); - await expect(unauthorized.validate()).rejects.toThrow(AuthorizationError); + it('throws AuthorizationException and does not call rules when authorization fails', async () => { + const rules = vi.fn(() => z.object({})); - const invalid = new FormRequest({ - input: {}, - rules: { - name: [required()] + class UnauthorizedRequest extends FormRequest { + public rules(): z.ZodObject> { + return rules(); } - }); - await expect(invalid.validateOrThrow()).rejects.toThrow(ValidationError); + public authorize(): boolean { + return false; + } + } + + const request = new UnauthorizedRequest(); + + await expect(request.validate({ body: {} })).rejects.toThrow(AuthorizationException); + expect(rules).not.toHaveBeenCalled(); + expect(request.context()).toEqual({ body: {} }); + expect(request.validated()).toBeUndefined(); }); - it('merges Express-like params, query, and body with body taking precedence', async () => { - const request = FormRequest.fromExpress<{ id: string; page: string; name: string }>( - { - body: { name: 'Body Name' }, - params: { id: '42', name: 'Param Name' }, - query: { page: '1' } - }, - { - rules: { - id: [required(), string()], - page: [required(), string()], - name: [required(), string()] - } + it('supports async authorization failures', async () => { + class AsyncUnauthorizedRequest extends CreateUserRequest { + public async authorize(): Promise { + return Promise.resolve(false); } - ); + } - await expect(request.validateOrThrow()).resolves.toEqual({ - id: '42', - page: '1', - name: 'Body Name' - }); + await expect(new AsyncUnauthorizedRequest().validate(makeContext('user@example.com'))).rejects.toThrow(AuthorizationException); }); - it('handles missing or malformed Express-like sources as empty objects', async () => { - const request = FormRequest.fromExpress<{ name?: string }>( - { - body: 'not-an-object', - params: null, - query: ['bad'] - }, - { - rules: { - name: [optional(), string()] - } + it('throws ValidationException with Zod details and keeps previous valid data intact', async () => { + class MetadataRequest extends CreateUserRequest { + public messages(): Record { + return { + 'body.email': 'Use a valid email address.' + }; } - ); - await expect(request.validateOrThrow()).resolves.toEqual({}); + public attributes(): Record { + return { + 'body.email': 'email address' + }; + } + } + + const request = new MetadataRequest(); + const valid = await request.validate(makeContext('valid@example.com')); + + await expect(request.validate(makeContext('not-an-email'))).rejects.toThrow(ValidationException); + expect(request.validated()).toEqual(valid); + + try { + await request.validate(makeContext('still-not-an-email')); + throw new Error('Expected validation to fail.'); + } catch (error) { + expect(error).toBeInstanceOf(ValidationException); + + const validationError = error as ValidationException; + + expect(validationError.error).toBeInstanceOf(ZodError); + expect(validationError.issues).toHaveLength(1); + expect(validationError.flattened.fieldErrors).toHaveProperty('body'); + expect(validationError.messages).toEqual({ 'body.email': 'Use a valid email address.' }); + expect(validationError.attributes).toEqual({ 'body.email': 'email address' }); + } }); - it('covers empty and measurable edge cases', async () => { - expect(isEmpty(undefined)).toBe(true); - expect(isEmpty(null)).toBe(true); - expect(isEmpty('')).toBe(true); - expect(isEmpty(0)).toBe(false); - - const request = new FormRequest({ - input: { - nested: {}, - count: 3, - unknown: {} - }, - rules: { - nested: [min(1)], - count: [min(2), max(4)], - unknown: [max(1)] + it('does not convert non-Zod errors into ValidationException', async () => { + class BrokenRequest extends FormRequest { + public rules(): z.ZodType { + throw new Error('Unexpected failure.'); } - }); + } + + await expect(new BrokenRequest().validate({})).rejects.toThrow('Unexpected failure.'); + await expect(new BrokenRequest().validate({})).rejects.not.toThrow(ValidationException); - const result = await request.validate(); + class BrokenParserRequest extends FormRequest { + public rules(): z.ZodType { + return { + parseAsync: () => Promise.reject(new Error('Parser failure.')) + } as z.ZodType; + } + } - expect(result.valid).toBe(false); - expect(result.issues).toHaveLength(2); - expect(result.issues.map((issue) => issue.field)).toEqual(expect.arrayContaining(['nested', 'unknown'])); + await expect(new BrokenParserRequest().validate({})).rejects.toThrow('Parser failure.'); }); - it('accepts empty optional values and alternate accepted values', async () => { - const request = new FormRequest({ - input: { - title: '', - score: '', - website: '', - tags: '', - enabled: '', - termsYes: 'yes', - termsOn: 'on', - termsOne: 1, - role: '' - }, - rules: { - title: [string(), length(4)], - score: [number(), min(3), max(5), between(3, 5)], - website: [url()], - tags: [array()], - enabled: [boolean()], - termsYes: [accepted()], - termsOn: [accepted()], - termsOne: [accepted()], - role: [inValues(['admin'])] + it('validates empty contexts when the schema allows them', async () => { + const schema = z.object({}).strict(); + + class EmptyRequest extends FormRequest { + public rules(): typeof schema { + return schema; } - }); + } + + await expect(new EmptyRequest().validate({})).resolves.toEqual({}); + }); +}); - const result = await request.validate(); +describe('exceptions', () => { + it('preserves prototype, names, messages, and metadata', () => { + const base = new FormRequestException('Base failure.'); + const unauthorized = new AuthorizationException(); + const customUnauthorized = new AuthorizationException('Nope.'); + const zodError = new ZodError([ + { + code: 'custom', + message: 'Invalid.', + path: ['body'] + } + ]); + const validation = new ValidationException(zodError, { body: 'Invalid body.' }, { body: 'payload' }); + const customValidation = new ValidationException(zodError, {}, {}, 'Custom invalid.'); - expect(result.valid).toBe(true); + expect(base).toBeInstanceOf(Error); + expect(base).toBeInstanceOf(FormRequestException); + expect(base.name).toBe('FormRequestException'); + expect(unauthorized).toBeInstanceOf(FormRequestException); + expect(unauthorized.name).toBe('AuthorizationException'); + expect(unauthorized.message).toBe('This action is unauthorized.'); + expect(customUnauthorized.message).toBe('Nope.'); + expect(validation).toBeInstanceOf(FormRequestException); + expect(validation.name).toBe('ValidationException'); + expect(validation.message).toBe('The given data was invalid.'); + expect(validation.error).toBe(zodError); + expect(validation.issues).toEqual(zodError.issues); + expect(validation.flattened.formErrors).toEqual([]); + expect(validation.messages).toEqual({ body: 'Invalid body.' }); + expect(validation.attributes).toEqual({ body: 'payload' }); + expect(customValidation.message).toBe('Custom invalid.'); }); +}); + +describe('validateFormRequest', () => { + it('attaches validated data and form request, then calls next once without an error', async () => { + const request = makeExpressRequest('valid@example.com'); + const response = {}; + const next = vi.fn(); + const middleware = validateFormRequest(CreateUserRequest); + + await middleware(request, response, next); - it('rejects non-string URLs and malformed URLs', async () => { - const request = new FormRequest({ - input: { - objectUrl: {}, - malformedUrl: 'not a url', - secureUrl: 'https://example.com' + expect(request.validated).toEqual({ + body: { + email: 'valid@example.com', + name: 'Jane' }, - rules: { - objectUrl: [url()], - malformedUrl: [url()], - secureUrl: [url()] - } + headers: { authorization: 'Bearer token' }, + params: { id: '42' }, + query: { page: '1' }, + user: { id: 'u_1' } }); + expect(request.formRequest).toBeInstanceOf(CreateUserRequest); + expect(next).toHaveBeenCalledOnce(); + expect(next).toHaveBeenCalledWith(); + }); + + it('passes validation and authorization failures to next exactly once', async () => { + class AdapterUnauthorizedRequest extends CreateUserRequest { + public async authorize(): Promise { + return Promise.resolve(false); + } + } - const result = await request.validate(); + const invalidRequest = makeExpressRequest('invalid'); + const unauthorizedRequest = makeExpressRequest('valid@example.com'); + const invalidNext = vi.fn(); + const unauthorizedNext = vi.fn(); - expect(result.errors.objectUrl).toEqual(['The objectUrl must be a valid URL.']); - expect(result.errors.malformedUrl).toEqual(['The malformedUrl must be a valid URL.']); - expect(result.errors.secureUrl).toBeUndefined(); + await validateFormRequest(CreateUserRequest)(invalidRequest, {}, invalidNext); + await validateFormRequest(AdapterUnauthorizedRequest)(unauthorizedRequest, {}, unauthorizedNext); + + expect(invalidRequest.validated).toBeUndefined(); + expect(invalidRequest.formRequest).toBeUndefined(); + expect(invalidNext).toHaveBeenCalledOnce(); + expect(invalidNext.mock.calls[0]?.[0]).toBeInstanceOf(ValidationException); + expect(unauthorizedRequest.validated).toBeUndefined(); + expect(unauthorizedRequest.formRequest).toBeUndefined(); + expect(unauthorizedNext).toHaveBeenCalledOnce(); + expect(unauthorizedNext.mock.calls[0]?.[0]).toBeInstanceOf(AuthorizationException); }); - it('exposes empty state before validation and covers default rule messages', async () => { - const request = new FormRequest({ - input: { - title: 12, - enabled: 'yes', - short: 'a', - long: 'abcdef', - maybe: undefined, - nullableValue: null - }, - rules: { - title: [string()], - enabled: [boolean()], - short: [min(2)], - long: [max(3)], - maybe: [optional()], - nullableValue: [nullable()] - } + it('instantiates a new request for each HTTP request and supports async refinements', async () => { + const schema = z.object({ + body: z.object({ + token: z.string().refine(async (value) => (await Promise.resolve(value)) === 'ok') + }), + headers: z.record(z.string(), z.unknown()).optional(), + params: z.record(z.string(), z.unknown()).optional(), + query: z.record(z.string(), z.unknown()).optional(), + user: z.record(z.string(), z.unknown()).optional() }); + const instances: AsyncAdapterRequest[] = []; - expect(request.validated()).toEqual({}); - expect(request.errors()).toEqual({}); - expect(optional().validate({ field: 'x', input: {}, label: 'x', value: undefined })).toBe(true); - expect(nullable().validate({ field: 'x', input: {}, label: 'x', value: null })).toBe(true); + class AsyncAdapterRequest extends FormRequest { + public constructor() { + super(); + instances.push(this); + } - const result = await request.validate(); + public rules(): typeof schema { + return schema; + } + } - expect(result.errors.title).toEqual(['The title must be a string.']); - expect(result.errors.enabled).toEqual(['The enabled must be true or false.']); - expect(result.errors.short).toEqual(['The short must be at least 2.']); - expect(result.errors.long).toEqual(['The long must be at most 3.']); + const first = makeExpressRequest('unused'); + const second = makeExpressRequest('unused'); + first.body = { token: 'ok' }; + second.body = { token: 'ok' }; + + await validateFormRequest(AsyncAdapterRequest)(first, {}, vi.fn()); + await validateFormRequest(AsyncAdapterRequest)(second, {}, vi.fn()); + + expect(instances).toHaveLength(2); + expect(instances[0]).not.toBe(instances[1]); }); }); + +describe('public typing', () => { + it('exports usable constructor and context types', async () => { + const RequestClass: FormRequestConstructor = CreateUserRequest; + const context: FormRequestContext = makeContext('typed@example.com'); + const request = new RequestClass(); + + await expect(request.validate(context)).resolves.toMatchObject({ + body: { + email: 'typed@example.com' + } + }); + }); +}); + +const makeContext = (email: string, bodyExtras: Record = {}): FormRequestContext => ({ + body: { + email, + name: 'Jane', + ...bodyExtras + }, + customKey: 'custom-value', + headers: { + authorization: 'Bearer token' + }, + params: { + id: '42' + }, + query: { + page: '1' + }, + user: { + id: 'u_1' + } +}); + +const makeExpressRequest = (email: string): { + body: unknown; + headers: unknown; + params: unknown; + query: unknown; + user: unknown; + validated?: unknown; + formRequest?: FormRequest; +} => ({ + body: { + email, + name: 'Jane' + }, + headers: { + authorization: 'Bearer token' + }, + params: { + id: '42' + }, + query: { + page: '1' + }, + user: { + id: 'u_1' + } +});