commit 7f864dbb362a52e252c280fcdf480c174f8ec797 Author: ren Date: Tue Oct 14 12:22:24 2025 +0800 feat: 初始化文件哈希 GitHub Action 项目 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, + }, +});