From 7f864dbb362a52e252c280fcdf480c174f8ec797 Mon Sep 17 00:00:00 2001 From: ren Date: Tue, 14 Oct 2025 12:22:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9D=E5=A7=8B=E5=8C=96=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=93=88=E5=B8=8C=20GitHub=20Action=20=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .editorconfig | 17 + .eslintignore | 63 + .eslintrc.js | 32 + .github/workflows/ci.yml | 46 + .github/workflows/examples.yml | 157 +++ .github/workflows/test.yml | 148 +++ .gitignore | 147 +++ .npmrc | 2 + .nvmrc | 1 + .prettierignore | 51 + .prettierrc | 15 + README.md | 201 +++ action.yml | 25 + examples/config.json | 11 + examples/sample1.txt | 6 + examples/sample2.js | 26 + index.js | 689 ++++++++++ package.json | 49 + pnpm-lock.yaml | 2138 ++++++++++++++++++++++++++++++++ tests/index.test.js | 1021 +++++++++++++++ tests/utils.js | 53 + vitest.config.js | 57 + 22 files changed, 4955 insertions(+) create mode 100644 .editorconfig create mode 100644 .eslintignore create mode 100644 .eslintrc.js create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/examples.yml create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 .nvmrc create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 README.md create mode 100644 action.yml create mode 100644 examples/config.json create mode 100644 examples/sample1.txt create mode 100644 examples/sample2.js create mode 100644 index.js create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 tests/index.test.js create mode 100644 tests/utils.js create mode 100644 vitest.config.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8d5eca1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +root = true + +[*] +indent_style = space +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false + +[*.{ts,js}] +indent_size = 2 + +[*.{yaml,yml}] +indent_size = 2 diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..dd90740 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,63 @@ +# Dependencies +node_modules/ +package-lock.json + +# Build outputs +dist/ +build/ +coverage/ + +# Logs +*.log +logs/ + +# Runtime data +pids/ +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ +.nyc_output/ + +# Dependency directories +node_modules/ +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test +.env.production +.env.local + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Temporary files +tmp/ +temp/ \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..dad381e --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,32 @@ +module.exports = { + env: { + browser: false, + es2021: true, + node: true, + }, + extends: ['eslint:recommended', 'prettier', 'plugin:jsdoc/recommended'], + plugins: ['prettier', 'jsdoc'], + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'script', + }, + rules: { + 'prettier/prettier': 'error', + 'no-console': 'off', + 'no-unused-vars': 'error', + 'no-undef': 'error', + 'jsdoc/require-description': 'error', + 'jsdoc/require-param-description': 'error', + 'jsdoc/require-returns-description': 'error', + 'jsdoc/require-description-complete-sentence': 'off', + 'jsdoc/tag-lines': ['error', 'any', { startLines: 1 }], + }, + overrides: [ + { + files: ['vitest.config.js', 'tests/**/*.js'], + parserOptions: { + sourceType: 'module', + }, + }, + ], +}; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f738cca --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,46 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + lint-and-test: + runs-on: ubuntu-latest + name: Lint and Test + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Check JavaScript Syntax + run: | + echo "🔍 Checking JavaScript syntax..." + node -c index.js + echo "✅ All JavaScript files have valid syntax" + + - name: Run Tests + run: | + echo "🧪 Running test suite..." + pnpm test + + - name: Test Action Execution + uses: ./ + with: + files: 'examples/*' + algorithm: 'sha256' diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml new file mode 100644 index 0000000..fa0c297 --- /dev/null +++ b/.github/workflows/examples.yml @@ -0,0 +1,157 @@ +name: Usage Examples + +on: + workflow_dispatch: + schedule: + - cron: '0 0 * * 0' # Weekly on Sunday + +jobs: + example-basic: + runs-on: ubuntu-latest + name: Basic Usage Example + + steps: + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Checkout + uses: actions/checkout@v4 + + - name: Calculate hash of a single file + id: single-file + uses: ./ + with: + files: 'README.md' + algorithm: 'sha256' + + - name: Display result + run: | + echo "📄 Single file hash:" + echo "Hash: ${{ steps.single-file.outputs.hash }}" + echo "Files processed: ${{ steps.single-file.outputs.file-count }}" + + example-multiple: + runs-on: ubuntu-latest + name: Multiple Files Example + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Calculate hash of multiple files + id: multiple-files + uses: ./ + with: + files: 'examples/*' + algorithm: 'md5' + + - name: Display result + run: | + echo "📁 Multiple files hash:" + echo "Hash: ${{ steps.multiple-files.outputs.hash }}" + echo "Files processed: ${{ steps.multiple-files.outputs.file-count }}" + + example-patterns: + runs-on: ubuntu-latest + name: Pattern Matching Example + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Calculate hash of JavaScript files + id: js-files + uses: ./ + with: + files: '**/*.js' + algorithm: 'sha1' + + - name: Display result + run: | + echo "🔧 JavaScript files hash:" + echo "Hash: ${{ steps.js-files.outputs.hash }}" + echo "Files processed: ${{ steps.js-files.outputs.file-count }}" + + - name: Calculate hash of JSON files + id: json-files + uses: ./ + with: + files: '**/*.json' + algorithm: 'sha256' + + - name: Display result + run: | + echo "📋 JSON files hash:" + echo "Hash: ${{ steps.json-files.outputs.hash }}" + echo "Files processed: ${{ steps.json-files.outputs.file-count }}" + + example-algorithms: + runs-on: ubuntu-latest + name: Different Algorithms Example + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: MD5 Hash + id: md5 + uses: ./ + with: + files: 'action.yml' + algorithm: 'md5' + + - name: SHA1 Hash + id: sha1 + uses: ./ + with: + files: 'action.yml' + algorithm: 'sha1' + + - name: SHA256 Hash + id: sha256 + uses: ./ + with: + files: 'action.yml' + algorithm: 'sha256' + + - name: SHA512 Hash + id: sha512 + uses: ./ + with: + files: 'action.yml' + algorithm: 'sha512' + + - name: Display all results + run: | + echo "🔐 Different algorithm results for action.yml:" + echo "MD5: ${{ steps.md5.outputs.hash }}" + echo "SHA1: ${{ steps.sha1.outputs.hash }}" + echo "SHA256: ${{ steps.sha256.outputs.hash }}" + echo "SHA512: ${{ steps.sha512.outputs.hash }}" + + example-error-handling: + runs-on: ubuntu-latest + name: Error Handling Example + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Handle missing files (expected to fail) + id: graceful + continue-on-error: true + uses: ./ + with: + files: 'missing-*.txt' + algorithm: 'sha256' + + - name: Check graceful result + run: | + if [ "${{ steps.graceful.outcome }}" == "failure" ]; then + echo "✅ Correctly failed when files are missing" + else + echo "❌ Should have failed but didn't" + exit 1 + fi diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..3cc6e36 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,148 @@ +name: Test Files Hash Action + +on: + push: + branches: [main, develop] + pull_request: + branches: [main] + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + name: Test Action + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Run Unit Tests + run: node run-tests.js + + - name: Test Action - Single File + id: test-single + uses: ./ + with: + files: 'examples/sample1.txt' + algorithm: 'sha256' + + - name: Verify Single File Output + run: | + echo "Hash: ${{ steps.test-single.outputs.hash }}" + echo "File Count: ${{ steps.test-single.outputs.file-count }}" + if [ "${{ steps.test-single.outputs.file-count }}" != "1" ]; then + echo "❌ Expected file count 1, got ${{ steps.test-single.outputs.file-count }}" + exit 1 + fi + if [ -z "${{ steps.test-single.outputs.hash }}" ]; then + echo "❌ Hash output is empty" + exit 1 + fi + echo "✅ Single file test passed" + + - name: Test Action - Multiple Files + id: test-multiple + uses: ./ + with: + files: 'examples/*.txt' + algorithm: 'md5' + + - name: Verify Multiple Files Output + run: | + echo "Hash: ${{ steps.test-multiple.outputs.hash }}" + echo "File Count: ${{ steps.test-multiple.outputs.file-count }}" + if [ "${{ steps.test-multiple.outputs.file-count }}" != "1" ]; then + echo "❌ Expected file count 1, got ${{ steps.test-multiple.outputs.file-count }}" + exit 1 + fi + echo "✅ Multiple files test passed" + + - name: Test Action - All Examples + id: test-all + uses: ./ + with: + files: 'examples/*' + algorithm: 'sha1' + + - name: Verify All Files Output + run: | + echo "Hash: ${{ steps.test-all.outputs.hash }}" + echo "File Count: ${{ steps.test-all.outputs.file-count }}" + if [ "${{ steps.test-all.outputs.file-count }}" != "3" ]; then + echo "❌ Expected file count 3, got ${{ steps.test-all.outputs.file-count }}" + exit 1 + fi + echo "✅ All files test passed" + + - name: Test Action - Different Algorithm + id: test-sha512 + uses: ./ + with: + files: 'README.md' + algorithm: 'sha512' + + - name: Verify SHA512 Output + run: | + echo "Hash: ${{ steps.test-sha512.outputs.hash }}" + echo "File Count: ${{ steps.test-sha512.outputs.file-count }}" + if [ "${{ steps.test-sha512.outputs.file-count }}" != "1" ]; then + echo "❌ Expected file count 1, got ${{ steps.test-sha512.outputs.file-count }}" + exit 1 + fi + # SHA512 hash should be 128 characters long + hash_length=$(echo "${{ steps.test-sha512.outputs.hash }}" | wc -c) + if [ $hash_length -ne 129 ]; then # 128 + 1 for newline + echo "❌ SHA512 hash length should be 128, got $((hash_length-1))" + exit 1 + fi + echo "✅ SHA512 test passed" + + - name: Test Action - Missing Files (should fail) + id: test-missing + continue-on-error: true + uses: ./ + with: + files: 'non-existent-file.txt' + algorithm: 'sha256' + + - name: Verify Missing Files Handling + run: | + if [ "${{ steps.test-missing.outcome }}" == "success" ]; then + echo "❌ Expected action to fail on missing file" + exit 1 + fi + echo "✅ Missing files test passed" + + test-matrix: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + node-version: ['18', '20'] + name: Test on ${{ matrix.os }} with Node ${{ matrix.node-version }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + + - name: Test Action + uses: ./ + with: + files: 'examples/sample1.txt' + algorithm: 'sha256' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..13a3736 --- /dev/null +++ b/.gitignore @@ -0,0 +1,147 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.* +!.env.example + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist +.output + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Sveltekit cache directory +.svelte-kit/ + +# vitepress build output +**/.vitepress/dist + +# vitepress cache directory +**/.vitepress/cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# Firebase cache directory +.firebase/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v3 +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# Vite files +vite.config.js.timestamp-* +vite.config.ts.timestamp-* +.vite/ + +# Editors +.idea +.vscode +.cursor +.trae diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..c2ee59b --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +package-manager=pnpm +engine-strict=true diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..0946473 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20.19 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..9746527 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,51 @@ +# Dependencies +node_modules/ +package-lock.json + +# Build outputs +dist/ +build/ +coverage/ + +# Logs +*.log +logs/ + +# Generated files +*.min.js +*.min.css + +# Binary files +*.jpg +*.jpeg +*.png +*.gif +*.ico +*.svg +*.woff +*.woff2 +*.ttf +*.eot + +# Documentation that should not be formatted +CHANGELOG.md +LICENSE + +# IDE files +.vscode/ +.idea/ + +# OS generated files +.DS_Store +Thumbs.db + +# Temporary files +tmp/ +temp/ + +# Environment files +.env* + +# Lock files +yarn.lock +pnpm-lock.yaml \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..9576ca9 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,15 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 120, + "tabWidth": 2, + "useTabs": false, + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "avoid", + "endOfLine": "lf", + "quoteProps": "as-needed", + "jsxSingleQuote": true, + "embeddedLanguageFormatting": "auto" +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..8a4c0ef --- /dev/null +++ b/README.md @@ -0,0 +1,201 @@ +# 文件哈希 GitHub Action + +一个轻量级的 GitHub Action,用于计算指定文件或文件组的哈希值。非常适合需要文件完整性验证、变更检测或文件指纹识别的 CI/CD 工作流。 + +## 使用方法 + +### 基本示例 + +```yaml +name: Calculate File Hashes +on: [push] + +jobs: + hash: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Calculate hash for source files + uses: actions/files-hash@v0.1 + id: hash-step + with: + files: | + src/**/*.js + package.json + algorithm: sha256 + + - name: Use the hash + run: | + echo "Hash: ${{ steps.hash-step.outputs.hash }}" + echo "Files processed: ${{ steps.hash-step.outputs.file-count }}" +``` + +### 高级示例 + +#### 多种文件模式 + +```yaml +- name: Hash multiple file types + uses: actions/files-hash@v0.1 + with: + files: | + src/**/*.js + src/**/*.ts + *.json + !node_modules/** + algorithm: sha512 +``` + +#### 不同的哈希算法 + +```yaml +- name: MD5 hash for quick comparison + uses: actions/files-hash@v0.1 + with: + files: dist/* + algorithm: md5 +``` + +#### 条件处理 + +```yaml +- name: Hash only if files exist + uses: actions/files-hash@v0.1 + with: + files: | + build/**/* + dist/**/* + algorithm: sha256 + continue-on-error: true +``` + +## 输入参数 + +| 输入参数 | 描述 | 必需 | 默认值 | +| ----------------- | ------------------------------------------- | ----- | -------- | +| `files` | 文件路径或 glob 模式(每行一个) | ✅ 是 | - | +| `algorithm` | 哈希算法:`md5`、`sha1`、`sha256`、`sha512` | ❌ 否 | `sha256` | + +### 文件模式 + +`files` 输入支持多种模式: + +- **单个文件**: `package.json` +- **多个文件**: + ```yaml + files: | + file1.txt + file2.txt + ``` +- **Glob 模式**: `src/**/*.js`(src 目录及子目录中的所有 JS 文件) +- **通配符**: `*.json`(当前目录中的所有 JSON 文件) +- **目录**: `src/`(递归处理 src 目录中的所有文件) + +## 输出结果 + +| 输出 | 描述 | 类型 | +| ------------ | ------------------------ | ------ | +| `hash` | 所有处理文件的组合哈希值 | string | +| `file-count` | 成功处理的文件数量 | number | + +## 工作原理 + +1. **文件发现**: 解析输入模式并查找匹配的文件 +2. **验证**: 检查文件存在性和可读性 +3. **哈希计算**: 使用指定算法计算各个文件的哈希值 +4. **组合**: 从所有单个文件哈希值创建组合哈希值 +5. **输出**: 设置 GitHub Actions 输出供后续步骤使用 + +组合哈希值的计算方式: + +1. 按字母顺序对所有文件路径进行排序(确保一致性) +2. 计算每个文件的哈希值 +3. 从所有单个哈希值的连接创建新的哈希值 + +这确保了相同的文件集始终产生相同的组合哈希值,无论它们的处理顺序如何。 + +## 支持的算法 + +| 算法 | 输出长度 | 使用场景 | +| -------- | -------- | --------------------------------- | +| `md5` | 32 字符 | 快速比较、传统系统 | +| `sha1` | 40 字符 | Git 兼容性、通用用途 | +| `sha256` | 64 字符 | **推荐** - 安全性和性能的良好平衡 | +| `sha512` | 128 字符 | 最高安全性、加密应用 | + +## 错误处理 + +该 Action 优雅地处理各种错误场景: + +- **文件缺失**: 直接抛出错误 +- **权限错误**: 提供包含文件路径的清晰错误信息 +- **无效算法**: 列出支持的算法 +- **空文件集**: 失败并提供描述性消息 + +## 性能 + +- **并发处理**: 同时处理多个文件 +- **流式处理**: 高效处理大文件,无需加载到内存中 +- **内存管理**: 针对大文件集和大文件进行优化 +- **进度日志**: 为长时间运行的操作提供清晰的进度指示器 + +## 实际项目示例 + +### 缓存键生成 + +```yaml +- name: Generate cache key + uses: actions/files-hash@v0.1 + id: cache-key + with: + files: | + package-lock.json + yarn.lock + pnpm-lock.yaml + algorithm: sha256 + +- name: Cache dependencies + uses: actions/cache@v3 + with: + path: node_modules + key: deps-${{ steps.cache-key.outputs.hash }} +``` + +### 构建产物验证 + +```yaml +- name: Build application + run: npm run build + +- name: Calculate build hash + uses: actions/files-hash@v0.1 + id: build-hash + with: + files: dist/**/* + algorithm: sha256 + +- name: Upload artifacts with hash + uses: actions/upload-artifact@v3 + with: + name: build-${{ steps.build-hash.outputs.hash }} + path: dist/ +``` + +### 配置变更检测 + +```yaml +- name: Check config changes + uses: actions/files-hash@v0.1 + id: config-hash + with: + algorithm: sha1 + files: | + .github/workflows/** + config/** + *.config.js + +- name: Notify on config changes + if: steps.config-hash.outputs.hash != env.LAST_CONFIG_HASH + run: echo "Configuration files have changed!" +``` diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..1a84f1e --- /dev/null +++ b/action.yml @@ -0,0 +1,25 @@ +name: 'Files Hash' +description: 'Calculate hash values for specified files or file groups' +author: 'files-hash' +branding: + icon: 'hash' + color: 'blue' + +inputs: + files: + description: 'File paths or glob patterns (one per line)' + required: true + algorithm: + description: 'Hash algorithm to use (md5, sha1, sha256, sha512)' + required: false + default: 'sha256' + +outputs: + hash: + description: 'Combined hash of all files' + file-count: + description: 'Number of files processed' + +runs: + using: 'node20' + main: 'index.js' diff --git a/examples/config.json b/examples/config.json new file mode 100644 index 0000000..3cff46d --- /dev/null +++ b/examples/config.json @@ -0,0 +1,11 @@ +{ + "name": "files-hash-example", + "version": "1.0.0", + "description": "Example configuration file for testing", + "settings": { + "debug": false, + "timeout": 5000, + "retries": 3 + }, + "features": ["hash-calculation", "glob-matching", "error-handling"] +} diff --git a/examples/sample1.txt b/examples/sample1.txt new file mode 100644 index 0000000..e401ba3 --- /dev/null +++ b/examples/sample1.txt @@ -0,0 +1,6 @@ +This is a sample text file for testing the Files Hash Action. +It contains multiple lines of text to ensure proper hash calculation. + +Line 3 +Line 4 +Final line. \ No newline at end of file diff --git a/examples/sample2.js b/examples/sample2.js new file mode 100644 index 0000000..adf3f96 --- /dev/null +++ b/examples/sample2.js @@ -0,0 +1,26 @@ +// Sample JavaScript file for testing +/** + * 测试函数 + * + * @param {string} name 姓名 + * @returns {string} 问候语 + */ +function greet(name) { + return `Hello, ${name}!`; +} + +/** + * 计算两个数的和. + * + * @param {number} a 第一个数 + * @param {number} b 第二个数 + * @returns {number} 两个数的和 + */ +function calculateSum(a, b) { + return a + b; +} + +module.exports = { + greet, + calculateSum, +}; diff --git a/index.js b/index.js new file mode 100644 index 0000000..aced4a0 --- /dev/null +++ b/index.js @@ -0,0 +1,689 @@ +const fs = require('fs').promises; +const fsSync = require('fs'); +const path = require('path'); +const crypto = require('crypto'); + +/** + * 错误类型常量. + * + * @readonly + * @enum {string} + */ +const ErrorType = { + /** + * 文件未找到错误. + */ + FILE_NOT_FOUND: 'FILE_NOT_FOUND', + /** + * 权限被拒绝错误. + */ + PERMISSION_DENIED: 'PERMISSION_DENIED', + /** + * 无效的哈希算法错误. + */ + INVALID_ALGORITHM: 'INVALID_ALGORITHM', + /** + * 哈希计算失败错误. + */ + HASH_CALCULATION_FAILED: 'HASH_CALCULATION_FAILED', +}; + +/** + * 自定义错误类,用于处理 GitHub Action 执行过程中的各种错误. + * + * @augments Error + */ +class ActionError extends Error { + /** + * 创建一个新的 ActionError 实例 + * + * @param {string} type - 错误类型,应该是 ErrorType 中的一个值 + * @param {string} message - 错误消息 + * @param {string|null} [file] - 相关的文件路径(可选) + * @param {string|null} [details] - 额外的错误详情(可选) + */ + constructor(type, message, file = null, details = null) { + super(message); + this.type = type; + this.file = file; + this.details = details; + this.name = 'ActionError'; + } +} + +/** + * GitHub Actions 输入处理类 + * 提供从环境变量中读取 GitHub Actions 输入参数的静态方法. + */ +class ActionInputs { + /** + * 获取字符串类型的输入参数 + * + * @param {string} name - 输入参数名称 + * @param {boolean} [required] - 是否为必需参数 + * @returns {string} 输入参数的值 + * @throws {ActionError} 当必需参数未提供时抛出错误 + */ + static getInput(name, required = false) { + const envName = `INPUT_${name.toUpperCase().replace(/-/g, '_')}`; + const value = process.env[envName] || ''; + + if (required && !value) { + throw new ActionError(ErrorType.INVALID_INPUT, `Input required and not supplied: ${name}`); + } + + return value; + } + + /** + * 获取布尔类型的输入参数 + * + * @param {string} name - 输入参数名称 + * @param {boolean} [required] - 是否为必需参数 + * @returns {boolean} 转换后的布尔值 + * @throws {ActionError} 当必需参数未提供时抛出错误 + */ + static getBooleanInput(name, required = false) { + const value = this.getInput(name, required).toLowerCase(); + return value === 'true' || value === '1'; + } + + /** + * 获取多行字符串类型的输入参数 + * + * @param {string} name - 输入参数名称 + * @param {boolean} [required] - 是否为必需参数 + * @returns {string[]} 按行分割并过滤空行后的字符串数组 + * @throws {ActionError} 当必需参数未提供时抛出错误 + */ + static getMultilineInput(name, required = false) { + const value = this.getInput(name, required); + return value.split('\n').filter(line => line.trim()); + } +} + +/** + * GitHub Actions 输出处理类 + * 提供设置 GitHub Actions 输出和日志记录的静态方法. + */ +class ActionOutputs { + /** + * 设置 GitHub Actions 输出变量 + * + * @param {string} name - 输出变量名称 + * @param {string} value - 输出变量值 + */ + static setOutput(name, value) { + const outputFile = process.env.GITHUB_OUTPUT; + if (outputFile) { + const fs = require('fs'); + fs.appendFileSync(outputFile, `${name}=${value}\n`); + } else { + console.log(`::set-output name=${name}::${value}`); + } + } + + /** + * 设置 Action 失败状态并退出 + * + * @param {string} message - 失败消息 + */ + static setFailed(message) { + console.log(`::error::${message}`); + process.exit(1); + } + + /** + * 输出信息日志 + * + * @param {string} message - 信息内容 + */ + static info(message) { + console.log(message); + } + + /** + * 输出警告日志 + * + * @param {string} message - 警告内容 + */ + static warning(message) { + console.log(`::warning::${message}`); + } + + /** + * 输出错误日志 + * + * @param {string} message - 错误内容 + */ + static error(message) { + console.log(`::error::${message}`); + } +} + +/** + * 文件发现引擎:提供文件模式解析、文件查找和验证功能,支持 glob 模式匹配 + */ +class FileDiscovery { + /** + * 解析输入的文件路径模式 + * + * @param {string|string[]} input - 文件路径模式,可以是字符串或字符串数组 + * @returns {string[]} 解析后的文件路径模式数组 + */ + parseFilePatterns(input) { + if (Array.isArray(input)) { + return input.map(line => line.trim()).filter(line => line); + } + return input + .split('\n') + .map(line => line.trim()) + .filter(line => line); + } + + /** + * 查找匹配的文件 + * + * @param {string[]} patterns - 文件路径模式数组 + * @returns {Promise} 匹配的文件路径数组(已排序且去重) + */ + async findFiles(patterns) { + const files = new Set(); + + for (const pattern of patterns) { + const matchedFiles = await this.expandGlob(pattern.trim()); + matchedFiles.forEach(file => files.add(file)); + } + + return Array.from(files).sort(); + } + + /** + * 验证文件存在性和可读性 + * + * @param {string[]} files - 文件路径数组 + * @returns {Promise} 验证结果数组,包含文件状态信息 + */ + async validateFiles(files) { + const results = []; + + for (const file of files) { + try { + const stats = await fs.stat(file); + if (stats.isFile()) { + results.push({ + file, + exists: true, + readable: true, + size: stats.size, + }); + } + } catch (error) { + results.push({ + file, + exists: false, + readable: false, + error: error.message, + }); + } + } + + return results; + } + + /** + * 自实现的 glob 模式匹配 + * + * @param {string} pattern - Glob 模式字符串 + * @returns {Promise} 匹配的文件路径数组 + */ + async expandGlob(pattern) { + // 如果是普通文件路径,直接返回 + if (!pattern.includes('*') && !pattern.includes('?')) { + try { + const stats = await fs.stat(pattern); + if (stats.isFile()) { + return [pattern]; + } else if (stats.isDirectory()) { + // 如果是目录,返回目录下所有文件 + return await this.getAllFilesInDirectory(pattern); + } + } catch (error) { + return []; + } + } + + // 处理 glob 模式 + return await this.matchGlobPattern(pattern); + } + + /** + * 获取目录下所有文件(递归) + * + * @param {string} dirPath - 目录路径 + * @param {boolean} [includeHidden] - 是否包含隐藏文件 + * @returns {Promise} 目录下所有文件的路径数组 + */ + async getAllFilesInDirectory(dirPath, includeHidden = false) { + const files = []; + + try { + const entries = await fs.readdir(dirPath, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dirPath, entry.name); + + // 默认忽略隐藏文件(以.开头的文件和目录) + if (!includeHidden && entry.name.startsWith('.')) { + continue; + } + + if (entry.isFile()) { + files.push(fullPath); + } else if (entry.isDirectory()) { + const subFiles = await this.getAllFilesInDirectory(fullPath, includeHidden); + files.push(...subFiles); + } + } + } catch (error) { + // 忽略无法访问的目录 + } + + return files; + } + + /** + * 匹配 glob 模式 + * + * @param {string} pattern - Glob 模式字符串 + * @returns {Promise} 匹配的文件路径数组 + * @private + */ + async matchGlobPattern(pattern) { + const files = []; + + // 分离目录部分和文件名部分 + const dir = path.dirname(pattern); + const filename = path.basename(pattern); + + // 如果目录部分包含通配符,需要递归处理 + if (dir.includes('*')) { + return await this.matchGlobPatternRecursive(pattern); + } + + // 处理简单的文件名通配符 + try { + const entries = await fs.readdir(dir, { withFileTypes: true }); + + for (const entry of entries) { + // 默认忽略隐藏文件(以.开头的文件) + if (entry.name.startsWith('.')) { + continue; + } + + if (entry.isFile() && this.matchPattern(entry.name, filename)) { + files.push(path.join(dir, entry.name)); + } + } + } catch (error) { + // 目录不存在或无法访问 + } + + return files; + } + + /** + * 递归匹配 glob 模式 + * + * @param {string} pattern - Glob 模式字符串 + * @returns {Promise} 匹配的文件路径数组 + * @private + */ + async matchGlobPatternRecursive(pattern) { + const files = []; + + // 处理 ** 通配符 + if (pattern.includes('**')) { + const parts = pattern.split('**'); + const basePath = parts[0].replace(/\*+$/, '').replace(/\/$/, '') || '.'; + const remainingPattern = parts[1].replace(/^\//, ''); + + try { + const allFiles = await this.getAllFilesInDirectory(basePath); + + for (const file of allFiles) { + const relativePath = path.relative(basePath, file); + if (this.matchPattern(relativePath, remainingPattern)) { + files.push(file); + } + } + } catch (error) { + // 忽略错误 + } + } else { + // 处理单层通配符 + const parts = pattern.split('/'); + files.push(...(await this.matchPatternParts(parts, '.'))); + } + + return files; + } + + /** + * 匹配模式部分 + * + * @param {string[]} parts - 模式部分数组 + * @param {string} currentPath - 当前路径 + * @returns {Promise} 匹配的文件路径数组 + * @private + */ + async matchPatternParts(parts, currentPath) { + if (parts.length === 0) { + return []; + } + + const [currentPart, ...remainingParts] = parts; + const files = []; + + try { + const entries = await fs.readdir(currentPath, { withFileTypes: true }); + + for (const entry of entries) { + // 默认忽略隐藏文件和目录(以.开头的) + if (entry.name.startsWith('.')) { + continue; + } + + const fullPath = path.join(currentPath, entry.name); + + if (this.matchPattern(entry.name, currentPart)) { + if (remainingParts.length === 0) { + // 最后一部分,检查是否为文件 + if (entry.isFile()) { + files.push(fullPath); + } + } else { + // 还有更多部分,递归处理 + if (entry.isDirectory()) { + const subFiles = await this.matchPatternParts(remainingParts, fullPath); + files.push(...subFiles); + } + } + } + } + } catch (error) { + // 忽略错误 + } + + return files; + } + + /** + * 简单的模式匹配函数 + * + * @param {string} text - 要匹配的文本 + * @param {string} pattern - Glob 模式 + * @returns {boolean} 是否匹配 + * @private + */ + matchPattern(text, pattern) { + // 将 glob 模式转换为正则表达式 + const regexPattern = pattern.replace(/\./g, '\\.').replace(/\*/g, '.*').replace(/\?/g, '.'); + + const regex = new RegExp(`^${regexPattern}$`); + return regex.test(text); + } +} + +/** + * 哈希计算器 + * 提供文件哈希计算功能,支持多种哈希算法 + */ +class HashCalculator { + /** + * 构造函数 + * 初始化支持的哈希算法列表 + */ + constructor() { + this.supportedAlgorithms = ['md5', 'sha1', 'sha256', 'sha512']; + } + + /** + * 验证算法是否支持 + * + * @param {string} algorithm - 哈希算法名称 + * @throws {ActionError} 当算法不支持时抛出错误 + */ + validateAlgorithm(algorithm) { + if (!this.supportedAlgorithms.includes(algorithm.toLowerCase())) { + throw new ActionError( + ErrorType.INVALID_ALGORITHM, + `Unsupported algorithm: ${algorithm}. Supported algorithms: ${this.supportedAlgorithms.join(', ')}` + ); + } + } + + /** + * 计算单个文件的哈希 + * + * @param {string} filePath - 文件路径 + * @param {string} algorithm - 哈希算法 + * @returns {Promise} 文件的哈希值(十六进制字符串) + * @throws {ActionError} 当文件读取或哈希计算失败时抛出错误 + */ + async calculateFileHash(filePath, algorithm) { + return new Promise((resolve, reject) => { + const hash = crypto.createHash(algorithm); + const stream = fsSync.createReadStream(filePath); + + stream.on('data', data => hash.update(data)); + stream.on('end', () => resolve(hash.digest('hex'))); + stream.on('error', error => + reject( + new ActionError( + ErrorType.HASH_CALCULATION_FAILED, + `Failed to calculate hash for ${filePath}: ${error.message}`, + filePath, + error + ) + ) + ); + }); + } + + /** + * 计算多个文件的组合哈希 + * + * @param {string[]} files - 文件路径数组 + * @param {string} algorithm - 哈希算法 + * @returns {Promise} 组合哈希值(十六进制字符串) + * @throws {ActionError} 当算法不支持或文件哈希计算失败时抛出错误 + */ + async calculateCombinedHash(files, algorithm) { + this.validateAlgorithm(algorithm); + + const hash = crypto.createHash(algorithm); + + // 按文件路径排序确保一致性 + const sortedFiles = [...files].sort(); + + ActionOutputs.info(`Processing ${sortedFiles.length} files with ${algorithm.toUpperCase()} algorithm...`); + + for (const file of sortedFiles) { + try { + const fileHash = await this.calculateFileHash(file, algorithm); + hash.update(fileHash); + ActionOutputs.info(`✓ ${file}`); + } catch (error) { + ActionOutputs.warning(`✗ ${file}: ${error.message}`); + throw error; + } + } + + const combinedHash = hash.digest('hex'); + ActionOutputs.info(`Combined hash: ${combinedHash}`); + + return combinedHash; + } + + /** + * 获取支持的算法列表 + * + * @returns {string[]} 支持的哈希算法数组的副本 + */ + getSupportedAlgorithms() { + return [...this.supportedAlgorithms]; + } +} + +/** + * 输出格式化器 + * 提供结果格式化和 GitHub Actions 输出设置功能 + */ +class OutputFormatter { + /** + * 格式化输出 + * + * @param {string|string[]} files - 文件列表或组合哈希值 + * @param {string} algorithm - 哈希算法 + * @param {string} combinedHash - 组合哈希值 + * @returns {string|object} 格式化后的输出字符串或对象 + */ + formatOutput(files, algorithm, combinedHash) { + // 如果第一个参数是数组,生成格式化的字符串输出 + if (Array.isArray(files)) { + const fileCount = files.length; + let output = ''; + + if (fileCount === 0) { + output += 'No files found\n'; + } else if (fileCount === 1) { + output += `Processing 1 file\n`; + output += `File: ${files[0]}\n`; + } else { + output += `Total files: ${fileCount}\n`; + files.forEach(file => { + output += ` ${file}\n`; + }); + } + + output += `Algorithm: ${algorithm.toLowerCase()}\n`; + output += `Combined Hash: ${combinedHash}`; + + return output; + } + + // 否则使用原来的对象格式(兼容模式) + return { + hash: files, + fileCount: algorithm, + algorithm: combinedHash.toUpperCase(), + }; + } + + /** + * 设置 GitHub Actions 输出 + * + * @param {string} hash - 哈希值 + * @param {number} fileCount - 文件数量 + */ + setGitHubOutput(hash, fileCount) { + ActionOutputs.setOutput('hash', hash); + ActionOutputs.setOutput('file-count', fileCount); + } +} + +/** + * 主要的 Action 类;协调文件发现、哈希计算和输出格式化的完整工作流程 + */ +class FilesHashAction { + /** + * 构造函数。初始化所需的组件实例 + */ + constructor() { + this.fileDiscovery = new FileDiscovery(); + this.hashCalculator = new HashCalculator(); + this.outputFormatter = new OutputFormatter(); + } + + /** + * 运行 Action 的主要方法。执行完整的文件哈希计算工作流程 + * + * @throws {ActionError} 当输入无效、文件不存在或哈希计算失败时抛出错误 + */ + async run() { + try { + // 读取输入参数 + const filesInput = ActionInputs.getMultilineInput('files', true); + const algorithm = ActionInputs.getInput('algorithm') || 'sha256'; + + ActionOutputs.info(`Files Hash Action started`); + ActionOutputs.info(`Algorithm: ${algorithm.toUpperCase()}`); + + // 解析文件模式 + const patterns = this.fileDiscovery.parseFilePatterns(filesInput); + ActionOutputs.info(`File patterns: ${patterns.join(', ')}`); + + // 查找文件 + const foundFiles = await this.fileDiscovery.findFiles(patterns); + ActionOutputs.info(`Found ${foundFiles.length} potential files`); + + // 验证文件 + const validationResults = await this.fileDiscovery.validateFiles(foundFiles); + const validFiles = validationResults + .filter(result => result.exists && result.readable) + .map(result => result.file); + + const missingFiles = validationResults.filter(result => !result.exists).map(result => result.file); + + // 处理缺失文件 - 直接抛出错误 + if (missingFiles.length > 0) { + const message = `Missing files: ${missingFiles.join(', ')}`; + throw new ActionError(ErrorType.FILE_NOT_FOUND, message); + } + + // 检查是否有有效文件 - 直接抛出错误 + if (validFiles.length === 0) { + throw new ActionError(ErrorType.FILE_NOT_FOUND, 'No valid files found to process'); + } + + ActionOutputs.info(`Processing ${validFiles.length} valid files`); + + // 计算组合哈希 + const combinedHash = await this.hashCalculator.calculateCombinedHash(validFiles, algorithm); + + // 格式化输出 + const result = this.outputFormatter.formatOutput(combinedHash, validFiles.length, algorithm); + + // 设置 GitHub Actions 输出 + this.outputFormatter.setGitHubOutput(result.hash, result.fileCount); + + ActionOutputs.info(`Action completed successfully`); + ActionOutputs.info(`Hash: ${result.hash}`); + ActionOutputs.info(`File count: ${result.fileCount}`); + } catch (error) { + if (error instanceof ActionError) { + ActionOutputs.setFailed(`${error.type}: ${error.message}`); + } else { + ActionOutputs.setFailed(`Unexpected error: ${error.message}`); + } + } + } +} + +// 运行 Action +if (require.main === module) { + const action = new FilesHashAction(); + action.run(); +} + +module.exports = { + FilesHashAction, + FileDiscovery, + HashCalculator, + OutputFormatter, + ActionInputs, + ActionOutputs, + ActionError, + ErrorType, +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..8b24d81 --- /dev/null +++ b/package.json @@ -0,0 +1,49 @@ +{ + "name": "files-hash-action", + "version": "0.1.0", + "description": "A lightweight GitHub Action to calculate hash of multiple files", + "main": "index.js", + "scripts": { + "test": "vitest run", + "test:watch": "vitest watch", + "test:coverage": "vitest run --coverage", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "format": "prettier --write .", + "format:check": "prettier --check .", + "check": "pnpm run lint && pnpm run format:check", + "preinstall": "npx only-allow pnpm" + }, + "devDependencies": { + "@vitest/coverage-v8": "^1.6.1", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-jsdoc": "^61.1.1", + "eslint-plugin-prettier": "^5.1.3", + "prettier": "^3.2.5", + "vitest": "^1.0.0" + }, + "keywords": [ + "github-action", + "hash", + "files", + "checksum", + "md5", + "sha1", + "sha256", + "sha512" + ], + "author": "Files Hash Action", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "repository": { + "type": "git", + "url": "https://github.com/your-username/files-hash-action.git" + }, + "bugs": { + "url": "https://github.com/your-username/files-hash-action/issues" + }, + "homepage": "https://github.com/your-username/files-hash-action#readme" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..ead25ea --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,2138 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@vitest/coverage-v8': + specifier: ^1.6.1 + version: 1.6.1(vitest@1.6.1) + eslint: + specifier: ^8.57.0 + version: 8.57.1 + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.2(eslint@8.57.1) + eslint-plugin-jsdoc: + specifier: ^61.1.1 + version: 61.1.2(eslint@8.57.1) + eslint-plugin-prettier: + specifier: ^5.1.3 + version: 5.5.4(eslint-config-prettier@9.1.2(eslint@8.57.1))(eslint@8.57.1)(prettier@3.6.2) + prettier: + specifier: ^3.2.5 + version: 3.6.2 + vitest: + specifier: ^1.0.0 + version: 1.6.1 + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.4': + resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.28.4': + resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + + '@es-joy/jsdoccomment@0.76.0': + resolution: {integrity: sha512-g+RihtzFgGTx2WYCuTHbdOXJeAlGnROws0TeALx9ow/ZmOROOZkVg5wp/B44n0WJgI4SQFP1eWM2iRPlU2Y14w==} + engines: {node: '>=20.11.0'} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@8.57.1': + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@humanwhocodes/config-array@0.13.0': + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@pkgr/core@0.2.9': + resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@rollup/rollup-android-arm-eabi@4.52.4': + resolution: {integrity: sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.52.4': + resolution: {integrity: sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.52.4': + resolution: {integrity: sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.52.4': + resolution: {integrity: sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.52.4': + resolution: {integrity: sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.52.4': + resolution: {integrity: sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.52.4': + resolution: {integrity: sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.52.4': + resolution: {integrity: sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.52.4': + resolution: {integrity: sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.52.4': + resolution: {integrity: sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.52.4': + resolution: {integrity: sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-gnu@4.52.4': + resolution: {integrity: sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-gnu@4.52.4': + resolution: {integrity: sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.52.4': + resolution: {integrity: sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.52.4': + resolution: {integrity: sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.52.4': + resolution: {integrity: sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.52.4': + resolution: {integrity: sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openharmony-arm64@4.52.4': + resolution: {integrity: sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.52.4': + resolution: {integrity: sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.52.4': + resolution: {integrity: sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.52.4': + resolution: {integrity: sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.52.4': + resolution: {integrity: sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==} + cpu: [x64] + os: [win32] + + '@sinclair/typebox@0.27.8': + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@typescript-eslint/types@8.46.1': + resolution: {integrity: sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@vitest/coverage-v8@1.6.1': + resolution: {integrity: sha512-6YeRZwuO4oTGKxD3bijok756oktHSIm3eczVVzNe3scqzuhLwltIF3S9ZL/vwOVIpURmU6SnZhziXXAfw8/Qlw==} + peerDependencies: + vitest: 1.6.1 + + '@vitest/expect@1.6.1': + resolution: {integrity: sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==} + + '@vitest/runner@1.6.1': + resolution: {integrity: sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==} + + '@vitest/snapshot@1.6.1': + resolution: {integrity: sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==} + + '@vitest/spy@1.6.1': + resolution: {integrity: sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==} + + '@vitest/utils@1.6.1': + resolution: {integrity: sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + are-docs-informative@0.0.2: + resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==} + engines: {node: '>=14'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + assertion-error@1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + chai@4.5.0: + resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==} + engines: {node: '>=4'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + check-error@1.0.3: + resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + comment-parser@1.4.1: + resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==} + engines: {node: '>= 12.0.0'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@4.1.4: + resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} + engines: {node: '>=6'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@9.1.2: + resolution: {integrity: sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-jsdoc@61.1.2: + resolution: {integrity: sha512-gbb4VVKRsMJZ+YqJXte6RQ/pDTIQcMauyQb03n5GTfWG+gAb6T3FaOLeoSHAIrBT90ykLkxzRh3z95DoIrUk3A==} + engines: {node: '>=20.11.0'} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 + + eslint-plugin-prettier@5.5.4: + resolution: {integrity: sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + + get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + html-entities@2.6.0: + resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsdoc-type-pratt-parser@6.10.0: + resolution: {integrity: sha512-+LexoTRyYui5iOhJGn13N9ZazL23nAHGkXsa1p/C8yeq79WRfLBag6ZZ0FQG2aRoc9yfo59JT9EYCQonOkHKkQ==} + engines: {node: '>=20.0.0'} + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + local-pkg@0.5.1: + resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==} + engines: {node: '>=14'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + loupe@2.3.7: + resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + + magic-string@0.30.19: + resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + object-deep-merge@1.0.5: + resolution: {integrity: sha512-3DioFgOzetbxbeUq8pB2NunXo8V0n4EvqsWM/cJoI6IA9zghd7cl/2pBOuWRf4dlvA+fcg5ugFMZaN2/RuoaGg==} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-limit@5.0.0: + resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} + engines: {node: '>=18'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-imports-exports@0.2.4: + resolution: {integrity: sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==} + + parse-statements@1.0.11: + resolution: {integrity: sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + + pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rollup@4.52.4: + resolution: {integrity: sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@4.0.0: + resolution: {integrity: sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==} + + spdx-license-ids@3.0.22: + resolution: {integrity: sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.9.0: + resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-literal@2.1.1: + resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + synckit@0.11.11: + resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} + engines: {node: ^14.18.0 || >=16.0.0} + + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinypool@0.8.4: + resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==} + engines: {node: '>=14.0.0'} + + tinyspy@2.2.1: + resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} + engines: {node: '>=14.0.0'} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-detect@4.1.0: + resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} + engines: {node: '>=4'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + type-fest@4.2.0: + resolution: {integrity: sha512-5zknd7Dss75pMSED270A1RQS3KloqRJA9XbXLe0eCxyw7xXFb3rd+9B0UQ/0E+LQT6lnrLviEolYORlRWamn4w==} + engines: {node: '>=16'} + + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + vite-node@1.6.1: + resolution: {integrity: sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite@5.4.20: + resolution: {integrity: sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitest@1.6.1: + resolution: {integrity: sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 1.6.1 + '@vitest/ui': 1.6.1 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yocto-queue@1.2.1: + resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} + engines: {node: '>=12.20'} + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/parser@7.28.4': + dependencies: + '@babel/types': 7.28.4 + + '@babel/types@7.28.4': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@bcoe/v8-coverage@0.2.3': {} + + '@es-joy/jsdoccomment@0.76.0': + dependencies: + '@types/estree': 1.0.8 + '@typescript-eslint/types': 8.46.1 + comment-parser: 1.4.1 + esquery: 1.6.0 + jsdoc-type-pratt-parser: 6.10.0 + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@eslint-community/eslint-utils@4.9.0(eslint@8.57.1)': + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.1': {} + + '@humanwhocodes/config-array@0.13.0': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@istanbuljs/schema@0.1.3': {} + + '@jest/schemas@29.6.3': + dependencies: + '@sinclair/typebox': 0.27.8 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@pkgr/core@0.2.9': {} + + '@rollup/rollup-android-arm-eabi@4.52.4': + optional: true + + '@rollup/rollup-android-arm64@4.52.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.52.4': + optional: true + + '@rollup/rollup-darwin-x64@4.52.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.52.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.52.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.52.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.52.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.52.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.52.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.52.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.52.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.52.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.52.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.52.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.52.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.52.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.52.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.52.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.52.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.52.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.52.4': + optional: true + + '@sinclair/typebox@0.27.8': {} + + '@types/estree@1.0.8': {} + + '@typescript-eslint/types@8.46.1': {} + + '@ungap/structured-clone@1.3.0': {} + + '@vitest/coverage-v8@1.6.1(vitest@1.6.1)': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 0.2.3 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magic-string: 0.30.19 + magicast: 0.3.5 + picocolors: 1.1.1 + std-env: 3.9.0 + strip-literal: 2.1.1 + test-exclude: 6.0.0 + vitest: 1.6.1 + transitivePeerDependencies: + - supports-color + + '@vitest/expect@1.6.1': + dependencies: + '@vitest/spy': 1.6.1 + '@vitest/utils': 1.6.1 + chai: 4.5.0 + + '@vitest/runner@1.6.1': + dependencies: + '@vitest/utils': 1.6.1 + p-limit: 5.0.0 + pathe: 1.1.2 + + '@vitest/snapshot@1.6.1': + dependencies: + magic-string: 0.30.19 + pathe: 1.1.2 + pretty-format: 29.7.0 + + '@vitest/spy@1.6.1': + dependencies: + tinyspy: 2.2.1 + + '@vitest/utils@1.6.1': + dependencies: + diff-sequences: 29.6.3 + estree-walker: 3.0.3 + loupe: 2.3.7 + pretty-format: 29.7.0 + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn-walk@8.3.4: + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + are-docs-informative@0.0.2: {} + + argparse@2.0.1: {} + + assertion-error@1.1.0: {} + + balanced-match@1.0.2: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + cac@6.7.14: {} + + callsites@3.1.0: {} + + chai@4.5.0: + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.3 + deep-eql: 4.1.4 + get-func-name: 2.0.2 + loupe: 2.3.7 + pathval: 1.1.1 + type-detect: 4.1.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + check-error@1.0.3: + dependencies: + get-func-name: 2.0.2 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + comment-parser@1.4.1: {} + + concat-map@0.0.1: {} + + confbox@0.1.8: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@4.1.4: + dependencies: + type-detect: 4.1.0 + + deep-is@0.1.4: {} + + diff-sequences@29.6.3: {} + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@9.1.2(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + + eslint-plugin-jsdoc@61.1.2(eslint@8.57.1): + dependencies: + '@es-joy/jsdoccomment': 0.76.0 + are-docs-informative: 0.0.2 + comment-parser: 1.4.1 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint: 8.57.1 + espree: 10.4.0 + esquery: 1.6.0 + html-entities: 2.6.0 + object-deep-merge: 1.0.5 + parse-imports-exports: 0.2.4 + semver: 7.7.3 + spdx-expression-parse: 4.0.0 + transitivePeerDependencies: + - supports-color + + eslint-plugin-prettier@5.5.4(eslint-config-prettier@9.1.2(eslint@8.57.1))(eslint@8.57.1)(prettier@3.6.2): + dependencies: + eslint: 8.57.1 + prettier: 3.6.2 + prettier-linter-helpers: 1.0.0 + synckit: 0.11.11 + optionalDependencies: + eslint-config-prettier: 9.1.2(eslint@8.57.1) + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@8.57.1: + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.1 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.3.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + espree@9.6.1: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 3.4.3 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + esutils@2.0.3: {} + + execa@8.0.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.0: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@3.2.0: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + rimraf: 3.0.2 + + flatted@3.3.3: {} + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + get-func-name@2.0.2: {} + + get-stream@8.0.1: {} + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + graphemer@1.4.0: {} + + has-flag@4.0.0: {} + + html-entities@2.6.0: {} + + html-escaper@2.0.2: {} + + human-signals@5.0.0: {} + + ignore@5.3.2: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-path-inside@3.0.3: {} + + is-stream@3.0.0: {} + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + js-tokens@9.0.1: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsdoc-type-pratt-parser@6.10.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + local-pkg@0.5.1: + dependencies: + mlly: 1.8.0 + pkg-types: 1.3.1 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + loupe@2.3.7: + dependencies: + get-func-name: 2.0.2 + + magic-string@0.30.19: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magicast@0.3.5: + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.7.3 + + merge-stream@2.0.0: {} + + mimic-fn@4.0.0: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + mlly@1.8.0: + dependencies: + acorn: 8.15.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.1 + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + + object-deep-merge@1.0.5: + dependencies: + type-fest: 4.2.0 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-limit@5.0.0: + dependencies: + yocto-queue: 1.2.1 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-imports-exports@0.2.4: + dependencies: + parse-statements: 1.0.11 + + parse-statements@1.0.11: {} + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + pathe@1.1.2: {} + + pathe@2.0.3: {} + + pathval@1.1.1: {} + + picocolors@1.1.1: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier-linter-helpers@1.0.0: + dependencies: + fast-diff: 1.3.0 + + prettier@3.6.2: {} + + pretty-format@29.7.0: + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.3.1 + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + react-is@18.3.1: {} + + resolve-from@4.0.0: {} + + reusify@1.1.0: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + rollup@4.52.4: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.52.4 + '@rollup/rollup-android-arm64': 4.52.4 + '@rollup/rollup-darwin-arm64': 4.52.4 + '@rollup/rollup-darwin-x64': 4.52.4 + '@rollup/rollup-freebsd-arm64': 4.52.4 + '@rollup/rollup-freebsd-x64': 4.52.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.52.4 + '@rollup/rollup-linux-arm-musleabihf': 4.52.4 + '@rollup/rollup-linux-arm64-gnu': 4.52.4 + '@rollup/rollup-linux-arm64-musl': 4.52.4 + '@rollup/rollup-linux-loong64-gnu': 4.52.4 + '@rollup/rollup-linux-ppc64-gnu': 4.52.4 + '@rollup/rollup-linux-riscv64-gnu': 4.52.4 + '@rollup/rollup-linux-riscv64-musl': 4.52.4 + '@rollup/rollup-linux-s390x-gnu': 4.52.4 + '@rollup/rollup-linux-x64-gnu': 4.52.4 + '@rollup/rollup-linux-x64-musl': 4.52.4 + '@rollup/rollup-openharmony-arm64': 4.52.4 + '@rollup/rollup-win32-arm64-msvc': 4.52.4 + '@rollup/rollup-win32-ia32-msvc': 4.52.4 + '@rollup/rollup-win32-x64-gnu': 4.52.4 + '@rollup/rollup-win32-x64-msvc': 4.52.4 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + semver@7.7.3: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + signal-exit@4.1.0: {} + + source-map-js@1.2.1: {} + + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@4.0.0: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.22 + + spdx-license-ids@3.0.22: {} + + stackback@0.0.2: {} + + std-env@3.9.0: {} + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-final-newline@3.0.0: {} + + strip-json-comments@3.1.1: {} + + strip-literal@2.1.1: + dependencies: + js-tokens: 9.0.1 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + synckit@0.11.11: + dependencies: + '@pkgr/core': 0.2.9 + + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + + text-table@0.2.0: {} + + tinybench@2.9.0: {} + + tinypool@0.8.4: {} + + tinyspy@2.2.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-detect@4.1.0: {} + + type-fest@0.20.2: {} + + type-fest@4.2.0: {} + + ufo@1.6.1: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + vite-node@1.6.1: + dependencies: + cac: 6.7.14 + debug: 4.4.3 + pathe: 1.1.2 + picocolors: 1.1.1 + vite: 5.4.20 + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite@5.4.20: + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.52.4 + optionalDependencies: + fsevents: 2.3.3 + + vitest@1.6.1: + dependencies: + '@vitest/expect': 1.6.1 + '@vitest/runner': 1.6.1 + '@vitest/snapshot': 1.6.1 + '@vitest/spy': 1.6.1 + '@vitest/utils': 1.6.1 + acorn-walk: 8.3.4 + chai: 4.5.0 + debug: 4.4.3 + execa: 8.0.1 + local-pkg: 0.5.1 + magic-string: 0.30.19 + pathe: 1.1.2 + picocolors: 1.1.1 + std-env: 3.9.0 + strip-literal: 2.1.1 + tinybench: 2.9.0 + tinypool: 0.8.4 + vite: 5.4.20 + vite-node: 1.6.1 + why-is-node-running: 2.3.0 + transitivePeerDependencies: + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + wrappy@1.0.2: {} + + yocto-queue@0.1.0: {} + + yocto-queue@1.2.1: {} diff --git a/tests/index.test.js b/tests/index.test.js new file mode 100644 index 0000000..e31e1d7 --- /dev/null +++ b/tests/index.test.js @@ -0,0 +1,1021 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import path from 'path'; +import fs from 'fs'; +import { + FilesHashAction, + FileDiscovery, + HashCalculator, + OutputFormatter, + ActionError, + ErrorType, + ActionInputs, + ActionOutputs, +} from '../index.js'; +import { createTestFile, createTestDir, cleanupTestFiles, cleanupTestDirs } from './utils.js'; + +describe('测试 FileDiscovery 类', () => { + describe('测试 FileDiscovery.parseFilePatterns 方法', () => { + it('应该解析包含多个模式的字符串输入', () => { + const discovery = new FileDiscovery(); + + const patterns = discovery.parseFilePatterns('file1.txt\nfile2.txt\n\n file3.txt \n'); + expect(patterns).toHaveLength(3); + expect(patterns[0]).toBe('file1.txt'); + expect(patterns[2]).toBe('file3.txt'); + }); + + it('应该解析包含空白和空格模式的数组输入', () => { + const discovery = new FileDiscovery(); + + const patterns = discovery.parseFilePatterns(['file1.txt', '', ' file2.txt ']); + expect(patterns).toHaveLength(2); + expect(patterns).toEqual(['file1.txt', 'file2.txt']); + }); + }); + + describe('测试 FileDiscovery.matchPattern 方法', () => { + const discovery = new FileDiscovery(); + + it('应该匹配确切的文件名', () => { + expect(discovery.matchPattern('test.js', 'test.js')).toBe(true); + expect(discovery.matchPattern('test.js', 'other.js')).toBe(false); + }); + + it('应该匹配通配符模式', () => { + expect(discovery.matchPattern('test.js', '*.js')).toBe(true); + expect(discovery.matchPattern('file.txt', 'file.*')).toBe(true); + expect(discovery.matchPattern('test.js', '*.txt')).toBe(false); + }); + + it('应该匹配问号通配符', () => { + expect(discovery.matchPattern('test1.js', 'test?.js')).toBe(true); + expect(discovery.matchPattern('test12.js', 'test?.js')).toBe(false); + }); + }); + + describe('测试 FileDiscovery.findFiles 方法', () => { + const testFiles = ['test-dir/file1.js', 'test-dir/file2.txt', 'test-dir/subdir/file3.js']; + + beforeEach(async () => { + // 创建测试文件 + for (const file of testFiles) { + await createTestFile(file, 'test content'); + } + }); + + afterEach(async () => { + await cleanupTestFiles(testFiles); + await cleanupTestDirs(['test-dir/subdir', 'test-dir']); + }); + + it('应该找到确切的文件', async () => { + const discovery = new FileDiscovery(); + + const files = await discovery.findFiles(['test-dir/file1.js']); + expect(files).toHaveLength(1); + expect(files[0]).toBe('test-dir/file1.js'); + }); + + it('应该使用通配符模式查找文件', async () => { + const discovery = new FileDiscovery(); + + const files = await discovery.findFiles(['test-dir/*.js']); + expect(files).toHaveLength(1); + }); + + it('应该递归查找文件', async () => { + const discovery = new FileDiscovery(); + + const files = await discovery.findFiles(['test-dir/**/*.js']); + expect(files).toHaveLength(2); + }); + + it('应该处理空目录', async () => { + const tempDir = await createTestDir(); + const discovery = new FileDiscovery(); + + const files = await discovery.findFiles([tempDir]); + + expect(files).toEqual([]); + cleanupTestDirs(tempDir); + }); + + it('应该处理无效模式', async () => { + const discovery = new FileDiscovery(); + const files = await discovery.findFiles(['nonexistent/**/*.txt']); + + expect(files).toEqual([]); + }); + + it('应该处理混合有效和无效模式', async () => { + const tempDir = await createTestDir(); + const testFile = path.join(tempDir, 'test.txt'); + await createTestFile(testFile, 'content'); + const discovery = new FileDiscovery(); + + const files = await discovery.findFiles([path.join(tempDir, '*.txt'), 'nonexistent/**/*.txt']); + + expect(files).toContain(testFile); + cleanupTestFiles([testFile]); + cleanupTestDirs(tempDir); + }); + + it('应该处理无权限的目录', async () => { + const tempDir = await createTestDir(); + const restrictedDir = path.join(tempDir, 'restricted'); + const discovery = new FileDiscovery(); + + try { + fs.mkdirSync(restrictedDir, { mode: 0o000 }); + + const files = await discovery.findFiles([restrictedDir + '/*.txt']); + + expect(files).toEqual([]); + } catch (error) { + // 某些系统可能不允许创建无权限目录,这是预期的 + expect(error.code).toBe('EACCES'); + } finally { + // 清理:先恢复权限再删除 + try { + fs.chmodSync(restrictedDir, 0o755); + cleanupTestDirs(tempDir); + } catch (cleanupError) { + // 忽略清理错误 + } + } + }); + + it('应该忽略隐藏文件(默认)', async () => { + const tempDir = await createTestDir(); + const hiddenFile = path.join(tempDir, '.hidden.txt'); + const visibleFile = path.join(tempDir, 'visible.txt'); + const discovery = new FileDiscovery(); + + await createTestFile(hiddenFile, 'hidden content'); + await createTestFile(visibleFile, 'visible content'); + + const files = await discovery.findFiles([path.join(tempDir, '*.txt')]); + + expect(files).toContain(visibleFile); + expect(files).not.toContain(hiddenFile); + + cleanupTestFiles(hiddenFile, visibleFile); + cleanupTestDirs(tempDir); + }); + + it('应该处理空文件', async () => { + const tempDir = await createTestDir(); + const emptyFile = path.join(tempDir, 'empty.txt'); + const discovery = new FileDiscovery(); + + await createTestFile(emptyFile, ''); + + const files = await discovery.findFiles([path.join(tempDir, '*.txt')]); + + expect(files).toContain(emptyFile); + + cleanupTestFiles(emptyFile); + cleanupTestDirs(tempDir); + }); + + it('应该处理嵌套很深的目录', async () => { + const tempDir = await createTestDir(); + const deepFile = path.join(tempDir, 'level1', 'level2', 'level3', 'deep.txt'); + const discovery = new FileDiscovery(); + + fs.mkdirSync(path.dirname(deepFile), { recursive: true }); + await createTestFile(deepFile, 'deep content'); + + const files = await discovery.findFiles([path.join(tempDir, '**/*.txt')]); + + expect(files).toContain(deepFile); + + cleanupTestFiles(deepFile); + cleanupTestDirs(tempDir); + }); + }); + + describe('测试 FileDiscovery.validateFiles 方法', () => { + const existingFile = 'existing-file.txt'; + const nonExistingFile = 'non-existing-file.txt'; + + beforeEach(async () => { + await createTestFile(existingFile, 'test content'); + }); + + afterEach(async () => { + await cleanupTestFiles([existingFile]); + }); + + it('应该验证存在和不存在的文件', async () => { + const discovery = new FileDiscovery(); + + const results = await discovery.validateFiles([existingFile, nonExistingFile]); + + expect(results).toHaveLength(2); + + const existingResult = results.find(r => r.file === existingFile); + const nonExistingResult = results.find(r => r.file === nonExistingFile); + + expect(existingResult.exists).toBe(true); + expect(existingResult.readable).toBe(true); + expect(existingResult.size).toBeGreaterThan(0); + + expect(nonExistingResult.exists).toBe(false); + expect(nonExistingResult.readable).toBe(false); + }); + }); +}); + +describe('测试 HashCalculator 类', () => { + describe('测试 HashCalculator.validateAlgorithm 方法', () => { + const calculator = new HashCalculator(); + + it('应该接受有效的算法', () => { + expect(() => calculator.validateAlgorithm('sha256')).not.toThrow(); + expect(() => calculator.validateAlgorithm('MD5')).not.toThrow(); + }); + + it('应该对无效算法抛出错误', () => { + expect(() => calculator.validateAlgorithm('invalid')).toThrow(ActionError); + + try { + calculator.validateAlgorithm('invalid'); + } catch (error) { + expect(error.type).toBe(ErrorType.INVALID_ALGORITHM); + } + }); + + it('应该处理算法名称大小写', () => { + // 验证算法验证方法是否大小写不敏感 + expect(() => calculator.validateAlgorithm('SHA256')).not.toThrow(); + expect(() => calculator.validateAlgorithm('sha256')).not.toThrow(); + expect(() => calculator.validateAlgorithm('Sha256')).not.toThrow(); + + expect(() => calculator.validateAlgorithm('md5')).not.toThrow(); + expect(() => calculator.validateAlgorithm('MD5')).not.toThrow(); + }); + }); + + describe('测试 HashCalculator.calculateFileHash 方法', () => { + const testFile = 'test-temp-file.txt'; + const testContent = 'Hello, World!'; + + beforeEach(async () => { + await createTestFile(testFile, testContent); + }); + + afterEach(async () => { + await cleanupTestFiles([testFile]); + }); + + it('应该正确计算SHA256哈希', async () => { + const calculator = new HashCalculator(); + + const hash = await calculator.calculateFileHash(testFile, 'sha256'); + + expect(hash).toHaveLength(64); + expect(hash).toMatch(/^[a-f0-9]+$/); + }); + + it('应该产生一致的哈希值', async () => { + const calculator = new HashCalculator(); + + const hash1 = await calculator.calculateFileHash(testFile, 'sha256'); + const hash2 = await calculator.calculateFileHash(testFile, 'sha256'); + + expect(hash1).toBe(hash2); + }); + + it('应该处理空文件', async () => { + const tempDir = await createTestDir(); + const emptyFile = path.join(tempDir, 'empty.txt'); + await createTestFile(emptyFile, ''); + + const calculator = new HashCalculator(); + const hash = await calculator.calculateFileHash(emptyFile, 'sha256'); + + // 空文件的 SHA256 哈希值 + expect(hash).toBe('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'); + + cleanupTestFiles(emptyFile); + cleanupTestDirs(tempDir); + }); + + it('应该处理大文件', async () => { + const tempDir = await createTestDir(); + const largeFile = path.join(tempDir, 'large.txt'); + // 创建 1MB 大小的文件 + const largeContent = 'x'.repeat(1024 * 1024); + await createTestFile(largeFile, largeContent); + + const calculator = new HashCalculator(); + const hash = await calculator.calculateFileHash(largeFile, 'sha256'); + + expect(hash).toBeTruthy(); + expect(hash.length).toBe(64); // SHA256 哈希长度 + + cleanupTestFiles(largeFile); + cleanupTestDirs(tempDir); + }); + + it('应该处理不存在的文件', async () => { + const calculator = new HashCalculator(); + + await expect(async () => { + await calculator.calculateFileHash('/nonexistent/file.txt', 'sha256'); + }).rejects.toThrow(); + }); + }); + + describe('测试 HashCalculator.calculateCombinedHash 方法', () => { + const testFiles = ['test-file1.txt', 'test-file2.txt']; + + beforeEach(async () => { + await createTestFile(testFiles[0], 'Content 1'); + await createTestFile(testFiles[1], 'Content 2'); + }); + + afterEach(async () => { + await cleanupTestFiles(testFiles); + }); + + it('应该计算组合哈希', async () => { + const calculator = new HashCalculator(); + + const hash = await calculator.calculateCombinedHash(testFiles, 'sha256'); + expect(hash).toHaveLength(64); + }); + + it('应该与顺序无关', async () => { + const calculator = new HashCalculator(); + + const hash1 = await calculator.calculateCombinedHash(testFiles, 'sha256'); + const hash2 = await calculator.calculateCombinedHash([testFiles[1], testFiles[0]], 'sha256'); + + expect(hash1).toBe(hash2); + }); + + it('应该处理空文件列表的组合哈希', async () => { + const calculator = new HashCalculator(); + const hash = await calculator.calculateCombinedHash([], 'sha256'); + + // 空列表的组合哈希应该是空字符串的哈希 + expect(hash).toBeTruthy(); + }); + + it('应该处理单文件的组合哈希', async () => { + const tempDir = await createTestDir(); + const testFile = path.join(tempDir, 'test.txt'); + await createTestFile(testFile, 'test content'); + + const calculator = new HashCalculator(); + const hash = await calculator.calculateCombinedHash([testFile], 'sha256'); + + expect(hash).toBeTruthy(); + expect(hash.length).toBe(64); + + cleanupTestFiles(testFile); + cleanupTestDirs(tempDir); + }); + + it('应该处理多文件的组合哈希', async () => { + const tempDir = await createTestDir(); + const file1 = path.join(tempDir, 'file1.txt'); + const file2 = path.join(tempDir, 'file2.txt'); + + await createTestFile(file1, 'content1'); + await createTestFile(file2, 'content2'); + + const calculator = new HashCalculator(); + const hash = await calculator.calculateCombinedHash([file1, file2], 'sha256'); + + expect(hash).toBeTruthy(); + expect(hash.length).toBe(64); + + cleanupTestFiles(file1, file2); + cleanupTestDirs(tempDir); + }); + + it('应该处理文件顺序无关的组合哈希', async () => { + const tempDir = await createTestDir(); + const file1 = path.join(tempDir, 'file1.txt'); + const file2 = path.join(tempDir, 'file2.txt'); + + await createTestFile(file1, 'content1'); + await createTestFile(file2, 'content2'); + + const calculator = new HashCalculator(); + const hash1 = await calculator.calculateCombinedHash([file1, file2], 'sha256'); + const hash2 = await calculator.calculateCombinedHash([file2, file1], 'sha256'); + + expect(hash1).toBe(hash2); + + cleanupTestFiles(file1, file2); + cleanupTestDirs(tempDir); + }); + }); + + describe('测试 HashCalculator.getSupportedAlgorithms 方法', () => { + it('应该获取支持的算法列表', () => { + const calculator = new HashCalculator(); + const algorithms = calculator.getSupportedAlgorithms(); + + expect(algorithms).toContain('sha256'); + expect(algorithms).toContain('md5'); + expect(algorithms).toContain('sha1'); + expect(algorithms).toContain('sha512'); + }); + }); +}); + +describe('测试 OutputFormatter 类', () => { + describe('测试 OutputFormatter.formatOutput 方法', () => { + it('应该正确格式化输出', () => { + const formatter = new OutputFormatter(); + const result = formatter.formatOutput(['file1.txt', 'file2.txt'], 'sha256', 'test-hash'); + + expect(result).toContain('file1.txt'); + expect(result).toContain('file2.txt'); + expect(result).toContain('Algorithm: sha256'); + expect(result).toContain('Combined Hash: test-hash'); + }); + + it('应该处理空文件列表', () => { + const formatter = new OutputFormatter(); + const result = formatter.formatOutput([], 'sha256', 'test-hash'); + + expect(result).toContain('No files found'); + expect(result).toContain('Algorithm: sha256'); + expect(result).toContain('Combined Hash: test-hash'); + }); + + it('应该处理空算法名称', () => { + const formatter = new OutputFormatter(); + const result = formatter.formatOutput(['file1.txt'], '', 'test-hash'); + + expect(result).toContain('Algorithm: '); + expect(result).toContain('Combined Hash: test-hash'); + }); + + it('应该处理空哈希值', () => { + const formatter = new OutputFormatter(); + const result = formatter.formatOutput(['file1.txt'], 'sha256', ''); + + expect(result).toContain('Algorithm: sha256'); + expect(result).toContain('Combined Hash: '); + }); + + it('应该处理特殊字符文件名', () => { + const formatter = new OutputFormatter(); + const specialFiles = [ + 'file with spaces.txt', + 'file-with-dashes.txt', + 'file_with_underscores.txt', + 'file.with.dots.txt', + 'file@with#symbols.txt', + ]; + + const result = formatter.formatOutput(specialFiles, 'sha256', 'test-hash'); + + specialFiles.forEach(file => { + expect(result).toContain(file); + }); + }); + + it('应该处理很长的文件列表', () => { + const formatter = new OutputFormatter(); + const manyFiles = Array.from({ length: 100 }, (_, i) => `file${i}.txt`); + + const result = formatter.formatOutput(manyFiles, 'sha256', 'test-hash'); + + expect(result).toContain('Total files: 100'); + expect(result).toContain('Algorithm: sha256'); + expect(result).toContain('Combined Hash: test-hash'); + }); + + it('应该处理包含路径的文件名', () => { + const formatter = new OutputFormatter(); + const pathFiles = [ + '/absolute/path/file1.txt', + './relative/path/file2.txt', + '../parent/path/file3.txt', + 'C:\\Windows\\path\\file4.txt', + ]; + + const result = formatter.formatOutput(pathFiles, 'sha256', 'test-hash'); + + pathFiles.forEach(file => { + expect(result).toContain(file); + }); + }); + }); + + describe('测试 OutputFormatter.setGitHubOutput 方法', () => { + let originalEnv; + + beforeEach(() => { + originalEnv = process.env.GITHUB_OUTPUT; + }); + + afterEach(() => { + process.env.GITHUB_OUTPUT = originalEnv; + }); + + it('应该处理无 GitHub 输出文件的情况', () => { + delete process.env.GITHUB_OUTPUT; + const consoleSpy = vi.spyOn(console, 'log'); + const formatter = new OutputFormatter(); + + formatter.setGitHubOutput('abcd1234', 2); + + expect(consoleSpy).toHaveBeenCalledWith('::set-output name=hash::abcd1234'); + expect(consoleSpy).toHaveBeenCalledWith('::set-output name=file-count::2'); + consoleSpy.mockRestore(); + }); + + it('应该处理空的 GitHub 输出值', () => { + delete process.env.GITHUB_OUTPUT; + const consoleSpy = vi.spyOn(console, 'log'); + const formatter = new OutputFormatter(); + + formatter.setGitHubOutput('', 0); + + expect(consoleSpy).toHaveBeenCalledWith('::set-output name=hash::'); + expect(consoleSpy).toHaveBeenCalledWith('::set-output name=file-count::0'); + consoleSpy.mockRestore(); + }); + + it('应该处理包含特殊字符的 GitHub 输出', () => { + delete process.env.GITHUB_OUTPUT; + const consoleSpy = vi.spyOn(console, 'log'); + const formatter = new OutputFormatter(); + + formatter.setGitHubOutput('special-value!@#', 1); + + expect(consoleSpy).toHaveBeenCalledWith('::set-output name=hash::special-value!@#'); + expect(consoleSpy).toHaveBeenCalledWith('::set-output name=file-count::1'); + consoleSpy.mockRestore(); + }); + }); +}); + +describe('测试 ActionInputs 类', () => { + describe('测试 ActionInputs.getInput 方法', () => { + it('应该获取环境变量中的输入值', () => { + process.env.INPUT_TEST_VALUE = 'test input'; + + const result = ActionInputs.getInput('test-value'); + expect(result).toBe('test input'); + + delete process.env.INPUT_TEST_VALUE; + }); + + it('应该返回空字符串当环境变量不存在', () => { + const result = ActionInputs.getInput('non-existent'); + expect(result).toBe(''); + }); + + it('应该处理带连字符的参数名', () => { + process.env.INPUT_MY_TEST_INPUT = 'value with dash'; + + const result = ActionInputs.getInput('my-test-input'); + expect(result).toBe('value with dash'); + + delete process.env.INPUT_MY_TEST_INPUT; + }); + + it('应该在必需参数缺失时抛出错误', () => { + expect(() => ActionInputs.getInput('required-param', true)).toThrow(ActionError); + }); + + it('不应该在必需参数存在时抛出错误', () => { + process.env.INPUT_REQUIRED_PARAM = 'exists'; + + expect(() => ActionInputs.getInput('required-param', true)).not.toThrow(); + + delete process.env.INPUT_REQUIRED_PARAM; + }); + }); + + describe('测试 ActionInputs.getBooleanInput 方法', () => { + it('应该正确解析 true 值', () => { + process.env.INPUT_TEST_BOOL = 'true'; + + const result = ActionInputs.getBooleanInput('test-bool'); + expect(result).toBe(true); + + delete process.env.INPUT_TEST_BOOL; + }); + + it('应该正确解析 1 为 true', () => { + process.env.INPUT_TEST_BOOL = '1'; + + const result = ActionInputs.getBooleanInput('test-bool'); + expect(result).toBe(true); + + delete process.env.INPUT_TEST_BOOL; + }); + + it('应该正确解析 false 值', () => { + process.env.INPUT_TEST_BOOL = 'false'; + + const result = ActionInputs.getBooleanInput('test-bool'); + expect(result).toBe(false); + + delete process.env.INPUT_TEST_BOOL; + }); + + it('应该对非布尔值返回 false', () => { + process.env.INPUT_TEST_BOOL = 'random-string'; + + const result = ActionInputs.getBooleanInput('test-bool'); + expect(result).toBe(false); + + delete process.env.INPUT_TEST_BOOL; + }); + + it('应该处理大小写不敏感', () => { + process.env.INPUT_TEST_BOOL = 'TRUE'; + + const result = ActionInputs.getBooleanInput('test-bool'); + expect(result).toBe(true); + + delete process.env.INPUT_TEST_BOOL; + }); + }); + + describe('测试 ActionInputs.getMultilineInput 方法', () => { + it('应该正确解析多行输入', () => { + process.env.INPUT_MULTI_LINE = 'line1\nline2\nline3'; + + const result = ActionInputs.getMultilineInput('multi-line'); + expect(result).toHaveLength(3); + expect(result).toEqual(['line1', 'line2', 'line3']); + + delete process.env.INPUT_MULTI_LINE; + }); + + it('应该过滤空行', () => { + process.env.INPUT_MULTI_LINE = 'line1\n\n \nline2\n \nline3'; + + const result = ActionInputs.getMultilineInput('multi-line'); + expect(result).toHaveLength(3); + expect(result).toEqual(['line1', 'line2', 'line3']); + + delete process.env.INPUT_MULTI_LINE; + }); + + it('应该处理空输入', () => { + process.env.INPUT_MULTI_LINE = ''; + + const result = ActionInputs.getMultilineInput('multi-line'); + expect(result).toHaveLength(0); + + delete process.env.INPUT_MULTI_LINE; + }); + }); +}); + +describe('测试 ActionOutputs 类', () => { + describe('测试 ActionOutputs.setOutput 方法', () => { + let originalEnv; + let outputContent = ''; + + beforeEach(() => { + originalEnv = process.env.GITHUB_OUTPUT; + // 模拟文件写入 + const fs = require('fs'); + fs.appendFileSync = (file, content) => { + outputContent += content; + }; + }); + + afterEach(() => { + process.env.GITHUB_OUTPUT = originalEnv; + outputContent = ''; + }); + + it('应该写入 GitHub 输出文件', () => { + process.env.GITHUB_OUTPUT = '/tmp/github_output.txt'; + + ActionOutputs.setOutput('test-name', 'test-value'); + + expect(outputContent).toBe('test-name=test-value\n'); + }); + + it('应该在无输出文件时使用控制台输出', () => { + delete process.env.GITHUB_OUTPUT; + const consoleSpy = vi.spyOn(console, 'log'); + + ActionOutputs.setOutput('test-name', 'test-value'); + + expect(consoleSpy).toHaveBeenCalledWith('::set-output name=test-name::test-value'); + consoleSpy.mockRestore(); + }); + }); + + describe('测试 ActionOutputs 日志方法', () => { + it('info 应该输出到控制台', () => { + const consoleSpy = vi.spyOn(console, 'log'); + + ActionOutputs.info('test message'); + + expect(consoleSpy).toHaveBeenCalledWith('test message'); + consoleSpy.mockRestore(); + }); + + it('warning 应该输出带格式的警告', () => { + const consoleSpy = vi.spyOn(console, 'log'); + + ActionOutputs.warning('test warning'); + + expect(consoleSpy).toHaveBeenCalledWith('::warning::test warning'); + consoleSpy.mockRestore(); + }); + + it('error 应该输出带格式的错误', () => { + const consoleSpy = vi.spyOn(console, 'log'); + + ActionOutputs.error('test error'); + + expect(consoleSpy).toHaveBeenCalledWith('::error::test error'); + consoleSpy.mockRestore(); + }); + }); + + describe('测试 ActionOutputs.setFailed 方法', () => { + it('应该输出错误并退出进程', () => { + const consoleSpy = vi.spyOn(console, 'log'); + const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {}); + + ActionOutputs.setFailed('test failure'); + + expect(consoleSpy).toHaveBeenCalledWith('::error::test failure'); + expect(exitSpy).toHaveBeenCalledWith(1); + + consoleSpy.mockRestore(); + exitSpy.mockRestore(); + }); + }); +}); + +describe('测试 ActionError 类', () => { + it('应该创建带有正确属性的错误实例', () => { + const error = new ActionError(ErrorType.FILE_NOT_FOUND, 'File not found', 'test.txt', 'Additional details'); + + expect(error).toBeInstanceOf(Error); + expect(error).toBeInstanceOf(ActionError); + expect(error.type).toBe(ErrorType.FILE_NOT_FOUND); + expect(error.message).toBe('File not found'); + expect(error.file).toBe('test.txt'); + expect(error.details).toBe('Additional details'); + expect(error.name).toBe('ActionError'); + }); + + it('应该处理可选参数为 null 的情况', () => { + const error = new ActionError(ErrorType.INVALID_ALGORITHM, 'Invalid algorithm'); + + expect(error.type).toBe(ErrorType.INVALID_ALGORITHM); + expect(error.message).toBe('Invalid algorithm'); + expect(error.file).toBeNull(); + expect(error.details).toBeNull(); + }); + + it('应该包含正确的错误类型常量', () => { + expect(ErrorType.FILE_NOT_FOUND).toBe('FILE_NOT_FOUND'); + expect(ErrorType.PERMISSION_DENIED).toBe('PERMISSION_DENIED'); + expect(ErrorType.INVALID_ALGORITHM).toBe('INVALID_ALGORITHM'); + expect(ErrorType.HASH_CALCULATION_FAILED).toBe('HASH_CALCULATION_FAILED'); + }); +}); + +describe('测试 FilesHashAction 类', () => { + describe('测试 FilesHashAction 边界情况 - 无文件和空输入', () => { + it('应该处理空的文件模式', async () => { + const action = new FilesHashAction(); + + // 模拟空的文件模式输入 + vi.spyOn(ActionInputs, 'getInput').mockImplementation(name => { + if (name === 'algorithm') return 'sha256'; + return ''; + }); + vi.spyOn(ActionInputs, 'getMultilineInput').mockImplementation(name => { + if (name === 'files') return ['']; + return []; + }); + vi.spyOn(ActionInputs, 'getBooleanInput').mockReturnValue(false); + + const consoleSpy = vi.spyOn(console, 'log'); + const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {}); + + await action.run(); + + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('::error::')); + expect(exitSpy).toHaveBeenCalledWith(1); + + consoleSpy.mockRestore(); + exitSpy.mockRestore(); + }); + + it('应该处理无效的文件模式', async () => { + const action = new FilesHashAction(); + + vi.spyOn(ActionInputs, 'getInput').mockImplementation(name => { + if (name === 'algorithm') return 'sha256'; + return ''; + }); + vi.spyOn(ActionInputs, 'getMultilineInput').mockImplementation(name => { + if (name === 'files') return ['nonexistent/**/*.txt']; + return []; + }); + vi.spyOn(ActionInputs, 'getBooleanInput').mockReturnValue(false); + + const consoleSpy = vi.spyOn(console, 'log'); + const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {}); + + await action.run(); + + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('::error::')); + expect(exitSpy).toHaveBeenCalledWith(1); + + consoleSpy.mockRestore(); + exitSpy.mockRestore(); + }); + }); + + describe('测试 FilesHashAction 边界情况 - 权限和文件系统错误', () => { + it('应该处理文件读取权限错误', async () => { + const action = new FilesHashAction(); + const tempDir = await createTestDir(); + const restrictedFile = path.join(tempDir, 'restricted.txt'); + + await createTestFile(restrictedFile, 'content'); + + // 模拟文件读取错误 + vi.spyOn(action.hashCalculator, 'calculateFileHash').mockRejectedValue( + new ActionError(ErrorType.HASH_CALCULATION_FAILED, 'Permission denied', restrictedFile) + ); + + vi.spyOn(ActionInputs, 'getInput').mockImplementation(name => { + if (name === 'algorithm') return 'sha256'; + return ''; + }); + vi.spyOn(ActionInputs, 'getMultilineInput').mockImplementation(name => { + if (name === 'files') return [restrictedFile]; + return []; + }); + vi.spyOn(ActionInputs, 'getBooleanInput').mockReturnValue(false); + + const consoleSpy = vi.spyOn(console, 'log'); + const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {}); + + await action.run(); + + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('::error::')); + expect(exitSpy).toHaveBeenCalledWith(1); + + cleanupTestFiles(restrictedFile); + cleanupTestDirs(tempDir); + consoleSpy.mockRestore(); + exitSpy.mockRestore(); + }); + }); + + describe('测试 FilesHashAction 边界情况 - 算法和配置错误', () => { + it('应该处理不支持算法', async () => { + const action = new FilesHashAction(); + const tempDir = await createTestDir(); + const testFile = path.join(tempDir, 'test.txt'); + + await createTestFile(testFile, 'content'); + + vi.spyOn(ActionInputs, 'getInput').mockImplementation(name => { + if (name === 'algorithm') return 'unsupported-algorithm'; + return ''; + }); + vi.spyOn(ActionInputs, 'getMultilineInput').mockImplementation(name => { + if (name === 'files') return [testFile]; + return []; + }); + vi.spyOn(ActionInputs, 'getBooleanInput').mockReturnValue(false); + + const consoleSpy = vi.spyOn(console, 'log'); + const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {}); + + await action.run(); + + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('::error::')); + expect(exitSpy).toHaveBeenCalledWith(1); + + cleanupTestFiles(testFile); + cleanupTestDirs(tempDir); + consoleSpy.mockRestore(); + exitSpy.mockRestore(); + }); + }); + + describe('测试 FilesHashAction 边界情况 - 输出和格式边界情况', () => { + it('应该处理很长的文件列表输出', async () => { + const action = new FilesHashAction(); + const tempDir = await createTestDir(); + + // 创建多个文件 + const files = []; + for (let i = 0; i < 10; i++) { + const file = path.join(tempDir, `file${i}.txt`); + await createTestFile(file, `content${i}`); + files.push(file); + } + + vi.spyOn(ActionInputs, 'getInput').mockImplementation(name => { + if (name === 'algorithm') return 'sha256'; + return ''; + }); + vi.spyOn(ActionInputs, 'getMultilineInput').mockImplementation(name => { + if (name === 'files') return [path.join(tempDir, '*.txt')]; + return []; + }); + vi.spyOn(ActionInputs, 'getBooleanInput').mockReturnValue(false); + + const consoleSpy = vi.spyOn(console, 'log'); + + await action.run(); + + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('File count: 10')); + + cleanupTestFiles(...files); + cleanupTestDirs(tempDir); + consoleSpy.mockRestore(); + }); + + it('应该处理空文件内容', async () => { + const action = new FilesHashAction(); + const tempDir = await createTestDir(); + const emptyFile = path.join(tempDir, 'empty.txt'); + + await createTestFile(emptyFile, ''); + + vi.spyOn(ActionInputs, 'getInput').mockImplementation(name => { + if (name === 'algorithm') return 'sha256'; + return ''; + }); + vi.spyOn(ActionInputs, 'getMultilineInput').mockImplementation(name => { + if (name === 'files') return [emptyFile]; + return []; + }); + vi.spyOn(ActionInputs, 'getBooleanInput').mockReturnValue(false); + + const consoleSpy = vi.spyOn(console, 'log'); + + await action.run(); + + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Combined hash:')); + + cleanupTestFiles(emptyFile); + cleanupTestDirs(tempDir); + consoleSpy.mockRestore(); + }); + }); +}); + +describe('测试 FilesHashAction 集成测试', () => { + describe('测试 FilesHashAction 完整工作流', () => { + const testFiles = ['integration-test/file1.txt', 'integration-test/file2.js', 'integration-test/subdir/file3.json']; + + beforeEach(async () => { + // 创建测试文件 + await createTestFile(testFiles[0], 'File 1 content'); + await createTestFile(testFiles[1], 'console.log("Hello");'); + await createTestFile(testFiles[2], '{"test": true}'); + + // 模拟环境变量 + process.env.INPUT_FILES = 'integration-test/**/*'; + process.env.INPUT_ALGORITHM = 'sha256'; + }); + + afterEach(async () => { + // 清理环境变量 + delete process.env.INPUT_FILES; + delete process.env.INPUT_ALGORITHM; + + // 清理测试文件 + await cleanupTestFiles(testFiles); + await cleanupTestDirs(['integration-test/subdir', 'integration-test']); + }); + + it('应该成功完成完整工作流程', async () => { + const action = new FilesHashAction(); + + // 测试文件发现 + const patterns = action.fileDiscovery.parseFilePatterns(['integration-test/**/*']); + const foundFiles = await action.fileDiscovery.findFiles(patterns); + + expect(foundFiles.length).toBeGreaterThanOrEqual(3); + + // 测试哈希计算 + const hash = await action.hashCalculator.calculateCombinedHash(foundFiles, 'sha256'); + expect(hash).toHaveLength(64); + }); + }); +}); diff --git a/tests/utils.js b/tests/utils.js new file mode 100644 index 0000000..f1255f0 --- /dev/null +++ b/tests/utils.js @@ -0,0 +1,53 @@ +import fs from 'fs/promises'; +import path from 'path'; +import os from 'os'; + +/** + * 创建一个临时测试目录 + * + * @returns {Promise} 临时目录路径 + */ +export async function createTestDir() { + return await fs.mkdtemp(path.join(os.tmpdir(), 'files-hash-test-')); +} + +/** + * 创建一个测试文件 + * + * @param {string} filePath - 要创建的文件路径 + * @param {string} content - 文件内容 + */ +export async function createTestFile(filePath, content) { + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, content); +} + +/** + * 清理测试文件 + * + * @param {string[]} files - 要清理的文件路径数组 + */ +export async function cleanupTestFiles(files) { + for (const file of files) { + try { + await fs.unlink(file); + } catch (error) { + // 忽略文件不存在的错误 + } + } +} + +/** + * 清理测试目录 + * + * @param {string[]} dirs - 要清理的目录路径数组 + */ +export async function cleanupTestDirs(dirs) { + for (const dir of dirs) { + try { + await fs.rmdir(dir, { recursive: true }); + } catch (error) { + // 忽略目录不存在的错误 + } + } +} diff --git a/vitest.config.js b/vitest.config.js new file mode 100644 index 0000000..a0dce5f --- /dev/null +++ b/vitest.config.js @@ -0,0 +1,57 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + // 测试环境 + environment: 'node', + + // 测试文件匹配模式 + include: ['**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + + // 排除的文件 + exclude: ['node_modules', 'dist', '.git', '.github'], + + // 全局设置 + globals: false, + + // 测试超时时间(毫秒) + testTimeout: 10000, + + // 钩子超时时间(毫秒) + hookTimeout: 10000, + + // 并发运行测试 + pool: 'threads', + + // 覆盖率配置 + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + exclude: [ + 'coverage/**', + 'dist/**', + 'packages/*/test{,s}/**', + '**/*.d.ts', + 'cypress/**', + 'test{,s}/**', + 'test{,-*}.{js,cjs,mjs,ts,tsx,jsx}', + '**/*{.,-}test.{js,cjs,mjs,ts,tsx,jsx}', + '**/*{.,-}spec.{js,cjs,mjs,ts,tsx,jsx}', + '**/__tests__/**', + '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*', + '**/.{eslint,mocha,prettier}rc.{js,cjs,yml}', + 'run-tests.js', + 'examples/**', + ], + }, + + // 报告器 + reporter: ['verbose'], + + // 在测试失败时停止 + bail: 0, + + // 重试次数 + retry: 0, + }, +});