diff --git a/.github/workflows/api-breakage-check.yml b/.github/workflows/api-breakage-check.yml new file mode 100644 index 00000000000..06e5140a10c --- /dev/null +++ b/.github/workflows/api-breakage-check.yml @@ -0,0 +1,234 @@ +name: SNAPI API Breakage Check + +on: + pull_request: + paths: + - 'packages/**' + - 'tools/snapi/**' + push: + branches: + - main + paths: + - 'packages/**' + - 'tools/snapi/**' + workflow_dispatch: # Keep workflow_dispatch + +concurrency: + group: snapi-${{ github.ref }} + cancel-in-progress: true + +jobs: + detect-api-changes: + runs-on: ubuntu-latest # Changed from blacksmith runner for simplicity, adjust if needed + timeout-minutes: 15 # Adjusted timeout + permissions: + contents: read + pull-requests: write + statuses: write + # id-token: write # Not needed if not using WIF for GCS + + # env: # GCS env vars removed + # GCS_PROJECT_ID: ${{ secrets.GCS_PROJECT_ID }} + # GCS_BUCKET: ${{ secrets.GCS_BUCKET }} + # TURBO_TEAM: ${{ vars.TURBO_TEAM }} + # TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Need full history + + - name: Setup + id: config + uses: ./.github/actions/init-blacksmith + with: + turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} + turbo-team: ${{ vars.TURBO_TEAM }} # Keep turbo if init-blacksmith needs it + turbo-token: ${{ secrets.TURBO_TOKEN }} # Keep turbo if init-blacksmith needs it + + # GCS Auth step removed + # - name: Authenticate to Google Cloud + # uses: 'google-github-actions/auth@v2' + # with: + # workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} + # service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} + + - name: Build packages + run: pnpm build + + - name: Build Snapi + run: pnpm turbo build --filter=@clerk/snapi + + # Storage health check removed + # - name: Check storage health + # id: storage-health + # run: | + # echo "GCS_PROJECT_ID: $GCS_PROJECT_ID" + # echo "GCS_BUCKET: $GCS_BUCKET" + # if ! pnpm snapi storage health --ci; then + # echo "::error::Storage health check failed" + # exit 1 + # fi + + - name: Download LATEST baseline from main (for PRs) + if: github.event_name == 'pull_request' + uses: dawidd6/action-download-artifact@v6 # Using v6, check for latest stable + with: + workflow: ${{ github.workflow_id }} # Use workflow_id for more robust self-referencing + branch: main + name: api-baseline-latest-main + path: .api-snapshots/baseline # Download to a 'baseline' subdirectory + if_no_artifact_found: warn # Don't fail if not found, just warn + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate and store initial snapshots if needed + if: github.event_name == 'pull_request' + id: initial-snapshots + run: | + # Check if baseline directory is empty + if [ ! "$(ls -A .api-snapshots/baseline)" ]; then + echo "No baseline snapshots found. Generating initial snapshots..." + # Generate current snapshots with --no-cleanup to prevent premature deletion + pnpm snapi:snapshot --config snapi.config.json --no-cleanup + + # Copy current snapshots to baseline + mkdir -p .api-snapshots/baseline + cp -r .api-snapshots/current/* .api-snapshots/baseline/ + + # Upload initial snapshots as artifacts + echo "COMMIT_SHA=$(git rev-parse HEAD)" >> $GITHUB_ENV + echo "initial_snapshots_generated=true" >> $GITHUB_OUTPUT + echo "Uploading initial snapshots as artifacts..." + else + echo "Baseline snapshots found, skipping initial snapshot generation." + echo "initial_snapshots_generated=false" >> $GITHUB_OUTPUT + fi + + - name: Upload initial snapshots (if generated) + if: github.event_name == 'pull_request' && steps.initial-snapshots.outputs.initial_snapshots_generated == 'true' + uses: actions/upload-artifact@v4 + with: + name: api-baseline-${{ env.COMMIT_SHA }} + path: .api-snapshots/ + retention-days: 90 + + - name: Run API breakage detection (PR) + if: github.event_name == 'pull_request' + id: api-check-pr + run: | + # Ensure baseline directory exists if artifact wasn't downloaded, to prevent errors if SNAPI expects it + mkdir -p .api-snapshots/baseline + # Generate current snapshots + pnpm snapi:snapshot --config snapi.config.json + + # Run breaking changes detection + echo "🔍 Detecting API changes..." + if pnpm snapi:check --config snapi.config.json --format markdown --output api-changes-report.md; then + echo "api_check_passed=true" >> $GITHUB_OUTPUT + else + echo "api_check_passed=false" >> $GITHUB_OUTPUT + fi + + - name: Update baseline cache and upload LATEST (main branch) + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + run: | + pnpm snapi:snapshot --config snapi.config.json --force + echo "COMMIT_SHA=$(git rev-parse HEAD)" >> $GITHUB_ENV + + - name: Upload commit-specific baseline snapshots (main branch) + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + uses: actions/upload-artifact@v4 + with: + name: api-baseline-${{ env.COMMIT_SHA }} + path: .api-snapshots/ + retention-days: 90 + + - name: Upload LATEST baseline snapshot (main branch) + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + uses: actions/upload-artifact@v4 + with: + name: api-baseline-latest-main + path: .api-snapshots/ # This should contain the generated snapshots + retention-days: 90 # Adjust as needed, this will be overwritten frequently + + - name: Comment on PR + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + + // Read the generated report + let reportContent = ''; + try { + reportContent = fs.readFileSync('api-changes-report.md', 'utf8'); + } catch (error) { + reportContent = '❌ **Error**: Could not generate API changes report.\n\n' + error.message; + } + + // Find existing comment + const comments = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number + }); + + const existingComment = comments.data.find( + comment => comment.user.login === 'github-actions[bot]' && + comment.body.includes('API Changes Report') + ); + + const commentBody = `## 🔍 API Changes Report + + ${reportContent} + + --- + 🤖 This comment was automatically generated by the API Breakage Detector`; + + if (existingComment) { + // Update existing comment + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existingComment.id, + body: commentBody + }); + } else { + // Create new comment + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: commentBody + }); + } + + - name: Set commit status + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const apiCheckPassed = '${{ steps.api-check-pr.outputs.api_check_passed }}' === 'true'; + + await github.rest.repos.createCommitStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + sha: context.payload.pull_request.head.sha, + state: apiCheckPassed ? 'success' : 'failure', + target_url: `${context.payload.pull_request.html_url}/checks`, + description: apiCheckPassed ? + 'No breaking API changes detected' : + 'Breaking API changes detected - review required', + context: 'snapi' + }); + + - name: Upload API snapshots and reports (debug) + if: always() # Keep this for debugging all runs + uses: actions/upload-artifact@v4 + with: + name: api-debug-${{ github.run_id }} + path: | + .api-snapshots/ + api-changes-report.md + retention-days: 7 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 473948c0d6d..f850235755a 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,7 @@ testem.log /typings **/coverage/** *.tsbuildinfo +.tmp # System Files .DS_Store @@ -99,3 +100,10 @@ scripts/.env # typedoc .typedoc/docs .typedoc/docs.json + +# API Breakage Detection +.api-snapshots/current/ +.api-snapshots/temp/ +.api-snapshots/.baseline-cache.json +# Keep baseline snapshots in Git for CI +!.api-snapshots/baseline/ diff --git a/package.json b/package.json index 29d75bf844a..15f7a5bdc1f 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,11 @@ "release:canary": "changeset publish --tag canary --no-git-tag", "release:snapshot": "changeset publish --tag snapshot --no-git-tag", "release:verdaccio": "if [ \"$(npm config get registry)\" = \"https://registry.npmjs.org/\" ]; then echo 'Error: Using default registry' && exit 1; else TURBO_CONCURRENCY=1 pnpm build && changeset publish --no-git-tag; fi", + "snapi:detect": "pnpm snapi detect", + "snapi:health": "pnpm snapi storage health", + "snapi:init": "pnpm snapi init --output snapi.config.json", + "snapi:snapshot": "pnpm snapi snapshot", + "snapi:suppress": "pnpm snapi suppress", "test": "FORCE_COLOR=1 turbo test --concurrency=${TURBO_CONCURRENCY:-80%}", "test:cache:clear": "FORCE_COLOR=1 turbo test:cache:clear --continue --concurrency=${TURBO_CONCURRENCY:-80%}", "test:integration:ap-flows": "pnpm test:integration:base --grep @ap-flows", @@ -68,6 +73,7 @@ "@changesets/get-github-info": "^0.6.0", "@clerk/backend": "workspace:*", "@clerk/shared": "workspace:*", + "@clerk/snapi": "workspace:*", "@clerk/testing": "workspace:*", "@commitlint/cli": "^19.8.0", "@commitlint/config-conventional": "^19.8.0", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index e8215708399..dcddb161b09 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -50,6 +50,7 @@ export function createClerkClient(options: ClerkOptions): ClerkClient { */ export type { OrganizationMembershipRole } from './api/resources'; export type { VerifyTokenOptions } from './tokens/verify'; +export type { LoadClerkJWKFromRemoteOptions } from './tokens/keys'; /** * JSON types */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ec205479983..6769b782305 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -70,6 +70,9 @@ importers: '@clerk/shared': specifier: workspace:* version: link:packages/shared + '@clerk/snapi': + specifier: workspace:* + version: link:tools/snapi '@clerk/testing': specifier: workspace:* version: link:packages/testing @@ -105,7 +108,7 @@ importers: version: 10.1.0 '@testing-library/jest-dom': specifier: ^6.4.6 - version: 6.4.6(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@22.18.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29(@swc/helpers@0.5.17))(@types/node@22.18.6)(typescript@5.8.3)))(vitest@3.0.5(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.18.6)(jiti@2.5.1)(jsdom@24.1.3)(lightningcss@1.27.0)(msw@2.11.2(@types/node@22.18.6)(typescript@5.8.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) + version: 6.4.6(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@22.18.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29(@swc/helpers@0.5.17))(@types/node@22.18.6)(typescript@5.8.3)))(vitest@3.0.5(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.18.6)(jiti@2.5.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.27.0)(msw@2.11.2(@types/node@22.18.6)(typescript@5.8.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) '@testing-library/react': specifier: ^16.0.0 version: 16.0.0(@testing-library/dom@10.1.0)(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -132,7 +135,7 @@ importers: version: 4.5.2(vite@7.1.5(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.27.0)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) '@vitest/coverage-v8': specifier: 3.0.5 - version: 3.0.5(vitest@3.0.5(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.18.6)(jiti@2.5.1)(jsdom@24.1.3)(lightningcss@1.27.0)(msw@2.11.2(@types/node@22.18.6)(typescript@5.8.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) + version: 3.0.5(vitest@3.0.5(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.18.6)(jiti@2.5.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.27.0)(msw@2.11.2(@types/node@22.18.6)(typescript@5.8.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) chalk: specifier: 4.1.2 version: 4.1.2 @@ -276,7 +279,7 @@ importers: version: 29.2.5(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(jest@29.7.0(@types/node@22.18.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29(@swc/helpers@0.5.17))(@types/node@22.18.6)(typescript@5.8.3)))(typescript@5.8.3) tsup: specifier: catalog:repo - version: 8.5.0(@swc/core@1.11.29(@swc/helpers@0.5.17))(jiti@2.5.1)(postcss@8.5.6)(tsx@4.19.2)(typescript@5.8.3)(yaml@2.8.1) + version: 8.5.0(@microsoft/api-extractor@7.52.13(@types/node@22.18.6))(@swc/core@1.11.29(@swc/helpers@0.5.17))(jiti@2.5.1)(postcss@8.5.6)(tsx@4.19.2)(typescript@5.8.3)(yaml@2.8.1) turbo: specifier: ^2.5.4 version: 2.5.4 @@ -306,7 +309,7 @@ importers: version: 5.33.0(typanion@3.14.0) vitest: specifier: 3.0.5 - version: 3.0.5(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.18.6)(jiti@2.5.1)(jsdom@24.1.3)(lightningcss@1.27.0)(msw@2.11.2(@types/node@22.18.6)(typescript@5.8.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) + version: 3.0.5(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.18.6)(jiti@2.5.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.27.0)(msw@2.11.2(@types/node@22.18.6)(typescript@5.8.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) yalc: specifier: 1.0.0-pre.53 version: 1.0.0-pre.53(patch_hash=2737a4bf8dd6ebdc410626225fe706ae0bb73b142e398279afff04d1b02dfc1f) @@ -399,7 +402,7 @@ importers: version: 9.0.2 vitest-environment-miniflare: specifier: 2.14.4 - version: 2.14.4(vitest@3.0.5(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@24.3.1)(jiti@2.5.1)(jsdom@24.1.3)(lightningcss@1.27.0)(msw@2.11.2(@types/node@24.3.1)(typescript@5.8.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) + version: 2.14.4(vitest@3.0.5(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@24.3.1)(jiti@2.5.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.27.0)(msw@2.11.2(@types/node@24.3.1)(typescript@5.8.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) packages/chrome-extension: dependencies: @@ -1036,7 +1039,7 @@ importers: devDependencies: tsup: specifier: catalog:repo - version: 8.5.0(@swc/core@1.11.29(@swc/helpers@0.5.17))(jiti@2.5.1)(postcss@8.5.6)(tsx@4.19.2)(typescript@5.8.3)(yaml@2.8.1) + version: 8.5.0(@microsoft/api-extractor@7.52.13(@types/node@24.3.1))(@swc/core@1.11.29(@swc/helpers@0.5.17))(jiti@2.5.1)(postcss@8.5.6)(tsx@4.19.2)(typescript@5.8.3)(yaml@2.8.1) packages/types: dependencies: @@ -1139,6 +1142,71 @@ importers: specifier: ^2.2.12 version: 2.2.12(typescript@5.8.3) + tools/snapi: + dependencies: + '@microsoft/api-extractor': + specifier: ^7.47.9 + version: 7.52.13(@types/node@20.19.17) + '@microsoft/api-extractor-model': + specifier: ^7.29.4 + version: 7.30.7(@types/node@20.19.17) + '@octokit/rest': + specifier: ^21.0.0 + version: 21.1.1 + chalk: + specifier: ^5.3.0 + version: 5.6.0 + commander: + specifier: ^12.1.0 + version: 12.1.0 + diff: + specifier: ^7.0.0 + version: 7.0.0 + fs-extra: + specifier: ^11.2.0 + version: 11.3.0 + globby: + specifier: ^14.0.2 + version: 14.1.0 + semver: + specifier: ^7.6.0 + version: 7.7.2 + yaml: + specifier: ^2.4.0 + version: 2.8.1 + zod: + specifier: ^3.23.8 + version: 3.24.2 + devDependencies: + '@types/diff': + specifier: ^5.2.0 + version: 5.2.3 + '@types/fs-extra': + specifier: ^11.0.4 + version: 11.0.4 + '@types/node': + specifier: ^20.11.0 + version: 20.19.17 + '@types/semver': + specifier: ^7.5.0 + version: 7.7.1 + eslint: + specifier: ^9.0.0 + version: 9.31.0(jiti@2.5.1) + tsup: + specifier: catalog:repo + version: 8.5.0(@microsoft/api-extractor@7.52.13(@types/node@20.19.17))(@swc/core@1.11.29(@swc/helpers@0.5.17))(jiti@2.5.1)(postcss@8.5.6)(tsx@4.19.2)(typescript@5.8.3)(yaml@2.8.1) + typescript: + specifier: catalog:repo + version: 5.8.3 + vitest: + specifier: ^2.0.0 + version: 2.1.9(@edge-runtime/vm@5.0.0)(@types/node@20.19.17)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.27.0)(msw@2.11.2(@types/node@20.19.17)(typescript@5.8.3))(terser@5.44.0) + optionalDependencies: + '@google-cloud/storage': + specifier: ^7.16.0 + version: 7.17.1 + packages: '@0no-co/graphql.web@1.1.2': @@ -1221,8 +1289,17 @@ packages: resolution: {integrity: sha512-Izvir8iIoU+X4SKtDAa5kpb+9cpifclzsbA8x/AZY0k0gIfXYQ1fa1B6Epfe6vNA2YfDX8VtrZFgvnXB6aPEoQ==} engines: {node: '>=18'} - '@asamuzakjp/css-color@3.1.5': - resolution: {integrity: sha512-w7AmVyTTiU41fNLsFDf+gA2Dwtbx2EJtn2pbJNAGSRAg50loXy1uLXA3hEpD8+eydcomTurw09tq5/AyceCaGg==} + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + + '@asamuzakjp/css-color@4.0.4': + resolution: {integrity: sha512-cKjSKvWGmAziQWbCouOsFwb14mp1betm8Y7Fn+yglDMUUu3r9DCbJ9iJbeFDenLMqFbIMC0pQP8K+B8LAxX3OQ==} + + '@asamuzakjp/dom-selector@6.5.5': + resolution: {integrity: sha512-kI2MX9pmImjxWT8nxDZY+MuN6r1jJGe7WxizEbsAEPB/zxfW5wYLIiPG1v3UKgEOOP8EsDkp0ZL99oRFAdPM8g==} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} '@astrojs/compiler@2.12.2': resolution: {integrity: sha512-w2zfvhjNCkNMmMMOn5b0J8+OmUaBL1o40ipMvqcG6NRpdC+lKxmTi48DT8Xw0SzJ3AfmeFLB45zXZXtmbsjcgw==} @@ -2139,32 +2216,38 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} - '@csstools/color-helpers@5.0.2': - resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 - '@csstools/css-calc@2.1.3': - resolution: {integrity: sha512-XBG3talrhid44BY1x3MHzUx/aTG8+x/Zi57M4aTKK9RFB4aLlF3TTSzfzn8nWVHWL3FgAXAxmupmDd6VWww+pw==} + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} engines: {node: '>=18'} peerDependencies: - '@csstools/css-parser-algorithms': ^3.0.4 - '@csstools/css-tokenizer': ^3.0.3 + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 - '@csstools/css-color-parser@3.0.9': - resolution: {integrity: sha512-wILs5Zk7BU86UArYBJTPy/FMPPKVKHMj1ycCEyf3VUptol0JNRLFU/BZsJ4aiIHJEbSLiizzRrw8Pc1uAEDrXw==} + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} engines: {node: '>=18'} peerDependencies: - '@csstools/css-parser-algorithms': ^3.0.4 - '@csstools/css-tokenizer': ^3.0.3 + '@csstools/css-tokenizer': ^3.0.4 - '@csstools/css-parser-algorithms@3.0.4': - resolution: {integrity: sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==} + '@csstools/css-syntax-patches-for-csstree@1.0.14': + resolution: {integrity: sha512-zSlIxa20WvMojjpCSy8WrNpcZ61RqfTfX3XTaOeVlGJrt/8HF3YbzgFZa01yTbT4GWQLwfTcC3EB8i3XnB647Q==} engines: {node: '>=18'} peerDependencies: - '@csstools/css-tokenizer': ^3.0.3 + postcss: ^8.4 - '@csstools/css-tokenizer@3.0.3': - resolution: {integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==} + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} '@cypress/request@3.0.6': @@ -2261,6 +2344,12 @@ packages: resolution: {integrity: sha512-YAdE/IJSpwbOTiaURNCKECdAwqrJuFiZhylmesBcIRawtYKnBR2wxPhoIewMg+Yu+QuYvHfJNReWpoxGBKOChA==} engines: {node: '>=18'} + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.23.1': resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} engines: {node: '>=18'} @@ -2279,6 +2368,12 @@ packages: 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-arm64@0.23.1': resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==} engines: {node: '>=18'} @@ -2297,6 +2392,12 @@ packages: 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-arm@0.23.1': resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==} engines: {node: '>=18'} @@ -2315,6 +2416,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.23.1': resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==} engines: {node: '>=18'} @@ -2333,6 +2440,12 @@ packages: 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-arm64@0.23.1': resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==} engines: {node: '>=18'} @@ -2351,6 +2464,12 @@ packages: 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/darwin-x64@0.23.1': resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==} engines: {node: '>=18'} @@ -2369,6 +2488,12 @@ packages: 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-arm64@0.23.1': resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==} engines: {node: '>=18'} @@ -2387,6 +2512,12 @@ packages: 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/freebsd-x64@0.23.1': resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==} engines: {node: '>=18'} @@ -2405,6 +2536,12 @@ packages: 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-arm64@0.23.1': resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==} engines: {node: '>=18'} @@ -2423,6 +2560,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.23.1': resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==} engines: {node: '>=18'} @@ -2441,6 +2584,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.23.1': resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==} engines: {node: '>=18'} @@ -2459,6 +2608,12 @@ packages: 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-loong64@0.23.1': resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==} engines: {node: '>=18'} @@ -2477,6 +2632,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.23.1': resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==} engines: {node: '>=18'} @@ -2495,6 +2656,12 @@ packages: 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-ppc64@0.23.1': resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==} engines: {node: '>=18'} @@ -2513,6 +2680,12 @@ packages: 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-riscv64@0.23.1': resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==} engines: {node: '>=18'} @@ -2531,6 +2704,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.23.1': resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==} engines: {node: '>=18'} @@ -2549,6 +2728,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.23.1': resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==} engines: {node: '>=18'} @@ -2579,6 +2764,12 @@ packages: cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.23.1': resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==} engines: {node: '>=18'} @@ -2615,6 +2806,12 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.23.1': resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==} engines: {node: '>=18'} @@ -2639,6 +2836,12 @@ packages: cpu: [arm64] os: [openharmony] + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.23.1': resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==} engines: {node: '>=18'} @@ -2657,6 +2860,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.23.1': resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==} engines: {node: '>=18'} @@ -2675,6 +2884,12 @@ packages: 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-ia32@0.23.1': resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==} engines: {node: '>=18'} @@ -2693,6 +2908,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.23.1': resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==} engines: {node: '>=18'} @@ -2755,7 +2976,7 @@ packages: '@expo/bunyan@4.0.1': resolution: {integrity: sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg==} - engines: {node: '>=0.10.0'} + engines: {'0': node >=0.10.0} '@expo/cli@0.22.26': resolution: {integrity: sha512-I689wc8Fn/AX7aUGiwrh3HnssiORMJtR2fpksX+JIe8Cj/EDleblYMSwRPd0025wrwOV9UN1KM/RuEt/QjCS3Q==} @@ -2904,6 +3125,22 @@ packages: '@gerrit0/mini-shiki@3.2.2': resolution: {integrity: sha512-vaZNGhGLKMY14HbF53xxHNgFO9Wz+t5lTlGNpl2N9xFiKQ0I5oIe0vKjU9dh7Nb3Dw6lZ7wqUE0ri+zcdpnK+Q==} + '@google-cloud/paginator@5.0.2': + resolution: {integrity: sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==} + engines: {node: '>=14.0.0'} + + '@google-cloud/projectify@4.0.0': + resolution: {integrity: sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==} + engines: {node: '>=14.0.0'} + + '@google-cloud/promisify@4.0.0': + resolution: {integrity: sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==} + engines: {node: '>=14'} + + '@google-cloud/storage@7.17.1': + resolution: {integrity: sha512-2FMQbpU7qK+OtBPaegC6n+XevgZksobUGo6mGKnXNmeZpvLiAo1gTAE3oTKsrMGDV4VtL8Zzpono0YsK/Q7Iqg==} + engines: {node: '>=14'} + '@hapi/hoek@9.3.0': resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} @@ -3160,8 +3397,8 @@ packages: resolution: {integrity: sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - '@jest/create-cache-key-function@30.0.2': - resolution: {integrity: sha512-AwlDAHwEHDi+etw9vKWx9HeIApVos8GD/sSTpHtDkqhm9OWuEUPKKPP6EaS17yv0GSzBB3TeeJFLyJ5LPjRqWg==} + '@jest/create-cache-key-function@30.0.5': + resolution: {integrity: sha512-W1kmkwPq/WTMQWgvbzWSCbXSqvjI6rkqBQCxuvYmd+g6o4b5gHP98ikfh/Ei0SKzHvWdI84TOXp0hRcbpr8Q0w==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/environment@29.7.0': @@ -3201,8 +3438,8 @@ packages: resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - '@jest/schemas@30.0.1': - resolution: {integrity: sha512-+g/1TKjFuGrf1Hh0QPCv0gISwBxJ+MQSNXmG9zjHy7BmFhtoJ9fdNhWJp3qUKRi93AOZHXtdxZgJ1vAtz6z65w==} + '@jest/schemas@30.0.5': + resolution: {integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/source-map@29.6.3': @@ -3229,8 +3466,8 @@ packages: resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - '@jest/types@30.0.1': - resolution: {integrity: sha512-HGwoYRVF0QSKJu1ZQX0o5ZrUrrhj0aOOFA8hXrumD7SIzjouevhawbTjmXdwOmURdGluU9DM/XvGm3NyFoiQjw==} + '@jest/types@30.0.5': + resolution: {integrity: sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jridgewell/gen-mapping@0.3.13': @@ -3300,6 +3537,19 @@ packages: engines: {node: '>=18'} hasBin: true + '@microsoft/api-extractor-model@7.30.7': + resolution: {integrity: sha512-TBbmSI2/BHpfR9YhQA7nH0nqVmGgJ0xH0Ex4D99/qBDAUpnhA2oikGmdXanbw9AWWY/ExBYIpkmY8dBHdla3YQ==} + + '@microsoft/api-extractor@7.52.13': + resolution: {integrity: sha512-K6/bBt8zZfn9yc06gNvA+/NlBGJC/iJlObpdufXHEJtqcD4Dln4ITCLZpwP3DNZ5NyBFeTkKdv596go3V72qlA==} + hasBin: true + + '@microsoft/tsdoc-config@0.17.1': + resolution: {integrity: sha512-UtjIFe0C6oYgTnad4q1QP4qXwLhe6tIpNTRStJ2RZEPIkqQPREAwE5spzVxsdn9UaEMUqhh0AqSx3X4nWAKXWw==} + + '@microsoft/tsdoc@0.15.1': + resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} + '@miniflare/cache@2.14.4': resolution: {integrity: sha512-ayzdjhcj+4mjydbNK7ZGDpIXNliDbQY4GPcY2KrYw0v1OSUdj5kZUkygD09fqoGRfAks0d91VelkyRsAXX8FQA==} engines: {node: '>=16.13'} @@ -3582,10 +3832,22 @@ packages: resolution: {integrity: sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==} engines: {node: '>= 18'} + '@octokit/auth-token@5.1.2': + resolution: {integrity: sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==} + engines: {node: '>= 18'} + '@octokit/core@5.2.0': resolution: {integrity: sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg==} engines: {node: '>= 18'} + '@octokit/core@6.1.6': + resolution: {integrity: sha512-kIU8SLQkYWGp3pVKiYzA5OSaNF5EE03P/R8zEmmrG6XwOg5oBjXyQVVIauQ0dgau4zYhpZEhJrvIYt6oM+zZZA==} + engines: {node: '>= 18'} + + '@octokit/endpoint@10.1.4': + resolution: {integrity: sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==} + engines: {node: '>= 18'} + '@octokit/endpoint@9.0.6': resolution: {integrity: sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==} engines: {node: '>= 18'} @@ -3594,42 +3856,88 @@ packages: resolution: {integrity: sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==} engines: {node: '>= 18'} + '@octokit/graphql@8.2.2': + resolution: {integrity: sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA==} + engines: {node: '>= 18'} + '@octokit/openapi-types@23.0.1': resolution: {integrity: sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==} + '@octokit/openapi-types@24.2.0': + resolution: {integrity: sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==} + + '@octokit/openapi-types@25.1.0': + resolution: {integrity: sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==} + '@octokit/plugin-paginate-rest@11.4.4-cjs.2': resolution: {integrity: sha512-2dK6z8fhs8lla5PaOTgqfCGBxgAv/le+EhPs27KklPhm1bKObpu6lXzwfUEQ16ajXzqNrKMujsFyo9K2eaoISw==} engines: {node: '>= 18'} peerDependencies: '@octokit/core': '5' + '@octokit/plugin-paginate-rest@11.6.0': + resolution: {integrity: sha512-n5KPteiF7pWKgBIBJSk8qzoZWcUkza2O6A0za97pMGVrGfPdltxrfmfF5GucHYvHGZD8BdaZmmHGz5cX/3gdpw==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + '@octokit/plugin-request-log@4.0.0': resolution: {integrity: sha512-2uJI1COtYCq8Z4yNSnM231TgH50bRkheQ9+aH8TnZanB6QilOnx8RMD2qsnamSOXtDj0ilxvevf5fGsBhBBzKA==} engines: {node: '>= 18'} peerDependencies: '@octokit/core': '>=5' + '@octokit/plugin-request-log@5.3.1': + resolution: {integrity: sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + '@octokit/plugin-rest-endpoint-methods@13.3.2-cjs.1': resolution: {integrity: sha512-VUjIjOOvF2oELQmiFpWA1aOPdawpyaCUqcEBc/UOUnj3Xp6DJGrJ1+bjUIIDzdHjnFNO6q57ODMfdEZnoBkCwQ==} engines: {node: '>= 18'} peerDependencies: '@octokit/core': ^5 + '@octokit/plugin-rest-endpoint-methods@13.5.0': + resolution: {integrity: sha512-9Pas60Iv9ejO3WlAX3maE1+38c5nqbJXV5GrncEfkndIpZrJ/WPMRd2xYDcPPEt5yzpxcjw9fWNoPhsSGzqKqw==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + '@octokit/request-error@5.1.1': resolution: {integrity: sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==} engines: {node: '>= 18'} + '@octokit/request-error@6.1.8': + resolution: {integrity: sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==} + engines: {node: '>= 18'} + '@octokit/request@8.4.1': resolution: {integrity: sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==} engines: {node: '>= 18'} + '@octokit/request@9.2.4': + resolution: {integrity: sha512-q8ybdytBmxa6KogWlNa818r0k1wlqzNC+yNkcQDECHvQo8Vmstrg18JwqJHdJdUiHD2sjlwBgSm9kHkOKe2iyA==} + engines: {node: '>= 18'} + '@octokit/rest@20.1.2': resolution: {integrity: sha512-GmYiltypkHHtihFwPRxlaorG5R9VAHuk/vbszVoRTGXnAsY60wYLkh/E2XiFmdZmqrisw+9FaazS1i5SbdWYgA==} engines: {node: '>= 18'} + '@octokit/rest@21.1.1': + resolution: {integrity: sha512-sTQV7va0IUVZcntzy1q3QqPm/r8rWtDCqpRAmb8eXXnKkjoQEtFe3Nt5GTVsHft+R6jJoHeSiVLcgcvhtue/rg==} + engines: {node: '>= 18'} + + '@octokit/types@13.10.0': + resolution: {integrity: sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==} + '@octokit/types@13.8.0': resolution: {integrity: sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==} + '@octokit/types@14.1.0': + resolution: {integrity: sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==} + '@one-ini/wasm@0.1.1': resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} @@ -4626,6 +4934,28 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@rushstack/node-core-library@5.14.0': + resolution: {integrity: sha512-eRong84/rwQUlATGFW3TMTYVyqL1vfW9Lf10PH+mVGfIb9HzU3h5AASNIw+axnBLjnD0n3rT5uQBwu9fvzATrg==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + '@rushstack/rig-package@0.5.3': + resolution: {integrity: sha512-olzSSjYrvCNxUFZowevC3uz8gvKr3WTpHQ7BkpjtRpA3wK+T0ybep/SRUMfr195gBzJm5gaXw0ZMgjIyHqJUow==} + + '@rushstack/terminal@0.16.0': + resolution: {integrity: sha512-WEvNuKkoR1PXorr9SxO0dqFdSp1BA+xzDrIm/Bwlc5YHg2FFg6oS+uCTYjerOhFuqCW+A3vKBm6EmKWSHfgx/A==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + '@rushstack/ts-command-line@5.0.3': + resolution: {integrity: sha512-bgPhQEqLVv/2hwKLYv/XvsTWNZ9B/+X1zJ7WgQE9rO5oiLzrOZvkIW4pk13yOQBhHyjcND5qMOa6p83t+Z66iQ==} + '@scure/base@1.2.6': resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==} @@ -4674,8 +5004,8 @@ packages: '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} - '@sinclair/typebox@0.34.38': - resolution: {integrity: sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA==} + '@sinclair/typebox@0.34.41': + resolution: {integrity: sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==} '@sindresorhus/is@4.6.0': resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} @@ -5103,6 +5433,9 @@ packages: '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/argparse@1.0.38': + resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} + '@types/aria-query@5.0.1': resolution: {integrity: sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==} @@ -5133,6 +5466,9 @@ packages: '@types/bonjour@3.5.13': resolution: {integrity: sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==} + '@types/caseless@0.12.5': + resolution: {integrity: sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==} + '@types/chrome@0.0.114': resolution: {integrity: sha512-i7qRr74IrxHtbnrZSKUuP5Uvd5EOKwlwJq/yp7+yTPihOXnPhNQO4Z5bqb1XTnrjdbUKEJicaVVbhcgtRijmLA==} @@ -5169,6 +5505,9 @@ packages: '@types/diff-match-patch@1.0.36': resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==} + '@types/diff@5.2.3': + resolution: {integrity: sha512-K0Oqlrq3kQMaO2RhfrNQX5trmt+XLyom88zS0u84nnIcLvFnRUMRRHmrGny5GSM+kNO9IZLARsdQHDzkhAgmrQ==} + '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} @@ -5268,6 +5607,9 @@ packages: '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + '@types/node@20.19.17': + resolution: {integrity: sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==} + '@types/node@22.18.6': resolution: {integrity: sha512-r8uszLPpeIWbNKtvWRt/DbVi5zbqZyj1PTmhRMqBMvDnaz1QpmSKujUtJLrqGZeoM8v72MfYggDceY4K1itzWQ==} @@ -5300,6 +5642,9 @@ packages: '@types/react@18.3.24': resolution: {integrity: sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==} + '@types/request@2.48.13': + resolution: {integrity: sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==} + '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} @@ -5666,9 +6011,23 @@ packages: '@vitest/browser': optional: true + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + '@vitest/expect@3.0.5': resolution: {integrity: sha512-nNIOqupgZ4v5jWuQx2DSlHLEs7Q4Oh/7AYwNyE+k0UQzG7tSmjPXShUikn1mpNGzYEN2jJbTvLejwShMitovBA==} + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/mocker@3.0.5': resolution: {integrity: sha512-CLPNBFBIE7x6aEGbIjaQAX03ZZlBMaWwAjBdMkIf/cAn6xzLTiM3zYqO/WAbieEjsAZir6tO71mzeHZoodThvw==} peerDependencies: @@ -5680,18 +6039,36 @@ packages: vite: optional: true + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + '@vitest/pretty-format@3.0.5': resolution: {integrity: sha512-CjUtdmpOcm4RVtB+up8r2vVDLR16Mgm/bYdkGFe3Yj/scRfCpbSi2W/BDSDcFK7ohw8UXvjMbOp9H4fByd/cOA==} + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + '@vitest/runner@3.0.5': resolution: {integrity: sha512-BAiZFityFexZQi2yN4OX3OkJC6scwRo8EhRB0Z5HIGGgd2q+Nq29LgHU/+ovCtd0fOfXj5ZI6pwdlUmC5bpi8A==} + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + '@vitest/snapshot@3.0.5': resolution: {integrity: sha512-GJPZYcd7v8QNUJ7vRvLDmRwl+a1fGg4T/54lZXe+UOGy47F9yUfE18hRCtXL5aHN/AONu29NGzIXSVFh9K0feA==} + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + '@vitest/spy@3.0.5': resolution: {integrity: sha512-5fOzHj0WbUNqPK6blI/8VzZdkBlQLnT25knX0r4dbZI9qoZDf3qAdjoMmDcLG5A83W6oUUFJgUd0EYBc2P5xqg==} + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + '@vitest/utils@3.0.5': resolution: {integrity: sha512-N9AX0NUoUtVwKwy21JtwzaqR5L5R5A99GAbrHfCCXK1lp593i/3AZAXhSP43wRQuxYsflrdzEfXZFo1reR1Nkg==} @@ -6025,6 +6402,14 @@ packages: zod: optional: true + ajv-draft-04@1.0.0: + resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} + peerDependencies: + ajv: ^8.5.0 + peerDependenciesMeta: + ajv: + optional: true + ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -6054,6 +6439,12 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@8.12.0: + resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + + ajv@8.13.0: + resolution: {integrity: sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==} + ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} @@ -6232,6 +6623,10 @@ packages: resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} engines: {node: '>=0.10.0'} + arrify@2.0.1: + resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} + engines: {node: '>=8'} + arrify@3.0.0: resolution: {integrity: sha512-tLkvA81vQG/XqE2mjDkGQHoOINtMHtysSnemrmoGe6PydDPMRbVugqyk4A6V/WDWEfm3l+0d8anA9r8cv/5Jaw==} engines: {node: '>=12'} @@ -6289,6 +6684,9 @@ packages: async-limiter@1.0.1: resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==} + async-retry@1.3.3: + resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + async-sema@3.1.1: resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==} @@ -6467,6 +6865,9 @@ packages: before-after-hook@2.2.3: resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} + before-after-hook@3.0.2: + resolution: {integrity: sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==} + better-opn@3.0.2: resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==} engines: {node: '>=12.0.0'} @@ -6475,10 +6876,16 @@ packages: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + big-integer@1.6.52: resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} engines: {node: '>=0.6'} + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -6706,9 +7113,9 @@ packages: engines: {node: '>=10'} hasBin: true - chai@5.1.2: - resolution: {integrity: sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==} - engines: {node: '>=12'} + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} @@ -7363,10 +7770,14 @@ packages: resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} engines: {node: '>=8'} - cssstyle@4.3.1: - resolution: {integrity: sha512-ZgW+Jgdd7i52AaLYCriF8Mxqft0gD/R9i9wi6RWBhs1pqdPEzPjym7rvRKi397WmQFf3SlyUsszhw+VVCbx79Q==} + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} engines: {node: '>=18'} + cssstyle@5.3.0: + resolution: {integrity: sha512-RveJPnk3m7aarYQ2bJ6iw+Urh55S6FzUiqtBq+TihnTDP4cI8y/TYDqGOyqgnG1J1a6BxJXZsV9JFSTulm9Z7g==} + engines: {node: '>=20'} + csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} @@ -7398,6 +7809,10 @@ packages: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} engines: {node: '>=18'} + data-urls@6.0.0: + resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==} + engines: {node: '>=20'} + data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -7687,6 +8102,10 @@ packages: resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} engines: {node: '>=0.3.1'} + diff@7.0.0: + resolution: {integrity: sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==} + engines: {node: '>=0.3.1'} + diff@8.0.2: resolution: {integrity: sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==} engines: {node: '>=0.3.1'} @@ -7957,6 +8376,11 @@ packages: resolution: {integrity: sha512-lNjylaAsJMprYg28zjUyBivP3y0ms9b7RJZ5tdhDUFLa3sCbqZw4wDnbFUSmnyZYWhCYDPxxp7KkXM2TXGw3PQ==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + esbuild@0.23.1: resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} engines: {node: '>=18'} @@ -8275,8 +8699,8 @@ packages: resolution: {integrity: sha512-uHaC9LYNv6BcW+8SvXcwUUDCrrUxt3GSa61DFvTHj8JC+M0hekMFBwMlCarLQDk5bbpZ2vStpnQPIwRuV98YMw==} engines: {node: '>=12.0.0'} - expect-type@1.1.0: - resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} + expect-type@1.2.2: + resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} expect@29.7.0: @@ -8428,6 +8852,9 @@ packages: resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} engines: {'0': node >=0.6.0} + fast-content-type-parse@2.0.1: + resolution: {integrity: sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==} + fast-decode-uri-component@1.0.1: resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} @@ -8650,6 +9077,10 @@ packages: forever-agent@0.6.1: resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} + form-data@2.5.5: + resolution: {integrity: sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==} + engines: {node: '>= 0.12'} + form-data@3.0.2: resolution: {integrity: sha512-sJe+TQb2vIaIyO783qN6BlMYWMw3WBOHA1Ay2qxsnjuafEOQFJ2JakedOQirT6D5XPRxDvS7AHYyem9fTpb4LQ==} engines: {node: '>= 6'} @@ -8743,6 +9174,14 @@ packages: resolution: {integrity: sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==} engines: {node: '>=10'} + gaxios@6.7.1: + resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} + engines: {node: '>=14'} + + gcp-metadata@6.1.1: + resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==} + engines: {node: '>=14'} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -8908,6 +9347,14 @@ packages: resolution: {integrity: sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==} engines: {node: '>=18'} + google-auth-library@9.15.1: + resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==} + engines: {node: '>=14'} + + google-logging-utils@0.0.2: + resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} + engines: {node: '>=14'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -8930,6 +9377,10 @@ packages: resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} engines: {node: '>=6.0'} + gtoken@7.1.0: + resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} + engines: {node: '>=14.0.0'} + gunzip-maybe@1.4.2: resolution: {integrity: sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==} hasBin: true @@ -9261,6 +9712,10 @@ packages: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} + import-lazy@4.0.0: + resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} + engines: {node: '>=8'} + import-local@3.1.0: resolution: {integrity: sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==} engines: {node: '>=8'} @@ -9936,6 +10391,9 @@ packages: resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==} hasBin: true + jju@1.4.0: + resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + joi@17.13.3: resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} @@ -10016,6 +10474,15 @@ packages: canvas: optional: true + jsdom@27.0.0: + resolution: {integrity: sha512-lIHeR1qlIRrIN5VMccd8tI2Sgw6ieYXSVktcSHaNe3Z5nE/tcPQYQWOq00wxMvYOsz+73eAkNenVvmPC6bba9A==} + engines: {node: '>=20'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jsesc@3.0.2: resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} engines: {node: '>=6'} @@ -10026,6 +10493,9 @@ packages: engines: {node: '>=6'} hasBin: true + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -10109,9 +10579,15 @@ packages: jwa@1.4.1: resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + jws@3.2.2: resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + jws@4.0.0: + resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} + keygrip@1.1.0: resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==} engines: {node: '>= 0.6'} @@ -10429,8 +10905,8 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - loupe@3.1.2: - resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} lowdb@1.0.0: resolution: {integrity: sha512-2+x8esE/Wb9SQ1F9IHaYWfsC9FIecLOPrK4g17FGEayjUWH172H6nwicRovGvSE2CPZouc2MCIqCI7h9d+GftQ==} @@ -10439,8 +10915,8 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.0.2: - resolution: {integrity: sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==} + lru-cache@11.2.1: + resolution: {integrity: sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ==} engines: {node: 20 || >=22} lru-cache@5.1.1: @@ -12651,6 +13127,10 @@ packages: retext@9.0.0: resolution: {integrity: sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==} + retry-request@7.0.2: + resolution: {integrity: sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==} + engines: {node: '>=14'} + retry@0.13.1: resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} engines: {node: '>= 4'} @@ -12830,6 +13310,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + semver@7.6.3: resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} engines: {node: '>=10'} @@ -13214,6 +13699,9 @@ packages: resolution: {integrity: sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==} engines: {node: '>= 0.10.0'} + stream-events@1.0.5: + resolution: {integrity: sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==} + stream-shift@1.0.3: resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} @@ -13355,6 +13843,9 @@ packages: structured-headers@0.4.1: resolution: {integrity: sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==} + stubs@3.0.0: + resolution: {integrity: sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==} + styled-jsx@5.1.1: resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} engines: {node: '>= 12.0.0'} @@ -13499,6 +13990,10 @@ packages: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} engines: {node: '>=18'} + teeny-request@9.0.0: + resolution: {integrity: sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==} + engines: {node: '>=14'} + temp-dir@2.0.0: resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} engines: {node: '>=8'} @@ -13632,10 +14127,14 @@ packages: tinygradient@1.1.5: resolution: {integrity: sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw==} - tinypool@1.0.2: - resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} engines: {node: ^18.0.0 || >=20.0.0} + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + tinyrainbow@2.0.0: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} @@ -13714,6 +14213,10 @@ packages: resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + tree-dump@1.0.2: resolution: {integrity: sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ==} engines: {node: '>=10.0'} @@ -13984,6 +14487,11 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@5.8.2: + resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} + engines: {node: '>=14.17'} + hasBin: true + typescript@5.8.3: resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} engines: {node: '>=14.17'} @@ -14134,6 +14642,9 @@ packages: universal-user-agent@6.0.1: resolution: {integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==} + universal-user-agent@7.0.3: + resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} + universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} @@ -14407,6 +14918,11 @@ packages: peerDependencies: vite: ^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + vite-node@3.0.5: resolution: {integrity: sha512-02JEJl7SbtwSDJdYS537nU6l+ktdvcREfLksk/NDAqtdKWGqHl+joXzEubHROmS3E6pip+Xgu2tFezMu75jH7A==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -14467,6 +14983,37 @@ packages: vite: ^6.0.0 || ^7.0.0 vue: ^3.5.0 + 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 + vite@6.3.6: resolution: {integrity: sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -14562,6 +15109,31 @@ packages: peerDependencies: vitest: '>=0.23.0' + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + 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 + vitest@3.0.5: resolution: {integrity: sha512-4dof+HvqONw9bvsYxtkfUp2uHsTN9bV2CZIi1pWgoFpL1Lld8LA1ka9q/ONSsoScAKG7NVGf2stJTI7XRkXb2Q==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -14672,6 +15244,10 @@ packages: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} + webidl-conversions@8.0.0: + resolution: {integrity: sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==} + engines: {node: '>=20'} + webpack-bundle-analyzer@4.10.2: resolution: {integrity: sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==} engines: {node: '>= 10.13.0'} @@ -14759,6 +15335,10 @@ packages: resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} engines: {node: '>=18'} + whatwg-url@15.1.0: + resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} + engines: {node: '>=20'} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -15206,14 +15786,34 @@ snapshots: typescript: 5.6.1-rc validate-npm-package-name: 5.0.1 - '@asamuzakjp/css-color@3.1.5': + '@asamuzakjp/css-color@3.2.0': dependencies: - '@csstools/css-calc': 2.1.3(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-color-parser': 3.0.9(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 lru-cache: 10.4.3 + '@asamuzakjp/css-color@4.0.4': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 11.2.1 + optional: true + + '@asamuzakjp/dom-selector@6.5.5': + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.1.0 + is-potential-custom-element-name: 1.0.1 + optional: true + + '@asamuzakjp/nwsapi@2.3.9': + optional: true + '@astrojs/compiler@2.12.2': {} '@astrojs/internal-helpers@0.7.2': {} @@ -16508,25 +17108,30 @@ snapshots: '@jridgewell/trace-mapping': 0.3.9 optional: true - '@csstools/color-helpers@5.0.2': {} + '@csstools/color-helpers@5.1.0': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 - '@csstools/css-calc@2.1.3(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': dependencies: - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 - '@csstools/css-color-parser@3.0.9(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': dependencies: - '@csstools/color-helpers': 5.0.2 - '@csstools/css-calc': 2.1.3(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 + '@csstools/css-tokenizer': 3.0.4 - '@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3)': + '@csstools/css-syntax-patches-for-csstree@1.0.14(postcss@8.5.6)': dependencies: - '@csstools/css-tokenizer': 3.0.3 + postcss: 8.5.6 + optional: true - '@csstools/css-tokenizer@3.0.3': {} + '@csstools/css-tokenizer@3.0.4': {} '@cypress/request@3.0.6': dependencies: @@ -16690,6 +17295,9 @@ snapshots: esquery: 1.6.0 jsdoc-type-pratt-parser: 4.1.0 + '@esbuild/aix-ppc64@0.21.5': + optional: true + '@esbuild/aix-ppc64@0.23.1': optional: true @@ -16699,6 +17307,9 @@ snapshots: '@esbuild/aix-ppc64@0.25.9': optional: true + '@esbuild/android-arm64@0.21.5': + optional: true + '@esbuild/android-arm64@0.23.1': optional: true @@ -16708,6 +17319,9 @@ snapshots: '@esbuild/android-arm64@0.25.9': optional: true + '@esbuild/android-arm@0.21.5': + optional: true + '@esbuild/android-arm@0.23.1': optional: true @@ -16717,6 +17331,9 @@ snapshots: '@esbuild/android-arm@0.25.9': optional: true + '@esbuild/android-x64@0.21.5': + optional: true + '@esbuild/android-x64@0.23.1': optional: true @@ -16726,6 +17343,9 @@ snapshots: '@esbuild/android-x64@0.25.9': optional: true + '@esbuild/darwin-arm64@0.21.5': + optional: true + '@esbuild/darwin-arm64@0.23.1': optional: true @@ -16735,6 +17355,9 @@ snapshots: '@esbuild/darwin-arm64@0.25.9': optional: true + '@esbuild/darwin-x64@0.21.5': + optional: true + '@esbuild/darwin-x64@0.23.1': optional: true @@ -16744,6 +17367,9 @@ snapshots: '@esbuild/darwin-x64@0.25.9': optional: true + '@esbuild/freebsd-arm64@0.21.5': + optional: true + '@esbuild/freebsd-arm64@0.23.1': optional: true @@ -16753,6 +17379,9 @@ snapshots: '@esbuild/freebsd-arm64@0.25.9': optional: true + '@esbuild/freebsd-x64@0.21.5': + optional: true + '@esbuild/freebsd-x64@0.23.1': optional: true @@ -16762,6 +17391,9 @@ snapshots: '@esbuild/freebsd-x64@0.25.9': optional: true + '@esbuild/linux-arm64@0.21.5': + optional: true + '@esbuild/linux-arm64@0.23.1': optional: true @@ -16771,6 +17403,9 @@ snapshots: '@esbuild/linux-arm64@0.25.9': optional: true + '@esbuild/linux-arm@0.21.5': + optional: true + '@esbuild/linux-arm@0.23.1': optional: true @@ -16780,6 +17415,9 @@ snapshots: '@esbuild/linux-arm@0.25.9': optional: true + '@esbuild/linux-ia32@0.21.5': + optional: true + '@esbuild/linux-ia32@0.23.1': optional: true @@ -16789,6 +17427,9 @@ snapshots: '@esbuild/linux-ia32@0.25.9': optional: true + '@esbuild/linux-loong64@0.21.5': + optional: true + '@esbuild/linux-loong64@0.23.1': optional: true @@ -16798,6 +17439,9 @@ snapshots: '@esbuild/linux-loong64@0.25.9': optional: true + '@esbuild/linux-mips64el@0.21.5': + optional: true + '@esbuild/linux-mips64el@0.23.1': optional: true @@ -16807,6 +17451,9 @@ snapshots: '@esbuild/linux-mips64el@0.25.9': optional: true + '@esbuild/linux-ppc64@0.21.5': + optional: true + '@esbuild/linux-ppc64@0.23.1': optional: true @@ -16816,6 +17463,9 @@ snapshots: '@esbuild/linux-ppc64@0.25.9': optional: true + '@esbuild/linux-riscv64@0.21.5': + optional: true + '@esbuild/linux-riscv64@0.23.1': optional: true @@ -16825,6 +17475,9 @@ snapshots: '@esbuild/linux-riscv64@0.25.9': optional: true + '@esbuild/linux-s390x@0.21.5': + optional: true + '@esbuild/linux-s390x@0.23.1': optional: true @@ -16834,6 +17487,9 @@ snapshots: '@esbuild/linux-s390x@0.25.9': optional: true + '@esbuild/linux-x64@0.21.5': + optional: true + '@esbuild/linux-x64@0.23.1': optional: true @@ -16849,6 +17505,9 @@ snapshots: '@esbuild/netbsd-arm64@0.25.9': optional: true + '@esbuild/netbsd-x64@0.21.5': + optional: true + '@esbuild/netbsd-x64@0.23.1': optional: true @@ -16867,6 +17526,9 @@ snapshots: '@esbuild/openbsd-arm64@0.25.9': optional: true + '@esbuild/openbsd-x64@0.21.5': + optional: true + '@esbuild/openbsd-x64@0.23.1': optional: true @@ -16879,6 +17541,9 @@ snapshots: '@esbuild/openharmony-arm64@0.25.9': optional: true + '@esbuild/sunos-x64@0.21.5': + optional: true + '@esbuild/sunos-x64@0.23.1': optional: true @@ -16888,6 +17553,9 @@ snapshots: '@esbuild/sunos-x64@0.25.9': optional: true + '@esbuild/win32-arm64@0.21.5': + optional: true + '@esbuild/win32-arm64@0.23.1': optional: true @@ -16897,6 +17565,9 @@ snapshots: '@esbuild/win32-arm64@0.25.9': optional: true + '@esbuild/win32-ia32@0.21.5': + optional: true + '@esbuild/win32-ia32@0.23.1': optional: true @@ -16906,6 +17577,9 @@ snapshots: '@esbuild/win32-ia32@0.25.9': optional: true + '@esbuild/win32-x64@0.21.5': + optional: true + '@esbuild/win32-x64@0.23.1': optional: true @@ -17380,6 +18054,40 @@ snapshots: '@shikijs/types': 3.12.2 '@shikijs/vscode-textmate': 10.0.2 + '@google-cloud/paginator@5.0.2': + dependencies: + arrify: 2.0.1 + extend: 3.0.2 + optional: true + + '@google-cloud/projectify@4.0.0': + optional: true + + '@google-cloud/promisify@4.0.0': + optional: true + + '@google-cloud/storage@7.17.1': + dependencies: + '@google-cloud/paginator': 5.0.2 + '@google-cloud/projectify': 4.0.0 + '@google-cloud/promisify': 4.0.0 + abort-controller: 3.0.0 + async-retry: 1.3.3 + duplexify: 4.1.3 + fast-xml-parser: 4.5.3 + gaxios: 6.7.1 + google-auth-library: 9.15.1 + html-entities: 2.6.0 + mime: 3.0.0 + p-limit: 3.1.0 + retry-request: 7.0.2 + teeny-request: 9.0.0 + uuid: 8.3.2 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + '@hapi/hoek@9.3.0': optional: true @@ -17497,6 +18205,14 @@ snapshots: figures: 6.1.0 ink: 5.0.1(@types/react@18.3.24)(react-devtools-core@4.28.5)(react@18.3.1) + '@inquirer/confirm@5.1.13(@types/node@20.19.17)': + dependencies: + '@inquirer/core': 10.1.14(@types/node@20.19.17) + '@inquirer/type': 3.0.7(@types/node@20.19.17) + optionalDependencies: + '@types/node': 20.19.17 + optional: true + '@inquirer/confirm@5.1.13(@types/node@22.18.6)': dependencies: '@inquirer/core': 10.1.14(@types/node@22.18.6) @@ -17512,6 +18228,20 @@ snapshots: optionalDependencies: '@types/node': 24.3.1 + '@inquirer/core@10.1.14(@types/node@20.19.17)': + dependencies: + '@inquirer/figures': 1.0.12 + '@inquirer/type': 3.0.7(@types/node@20.19.17) + ansi-escapes: 4.3.2 + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.2 + optionalDependencies: + '@types/node': 20.19.17 + optional: true + '@inquirer/core@10.1.14(@types/node@22.18.6)': dependencies: '@inquirer/figures': 1.0.12 @@ -17541,6 +18271,11 @@ snapshots: '@inquirer/figures@1.0.12': {} + '@inquirer/type@3.0.7(@types/node@20.19.17)': + optionalDependencies: + '@types/node': 20.19.17 + optional: true + '@inquirer/type@3.0.7(@types/node@22.18.6)': optionalDependencies: '@types/node': 22.18.6 @@ -17638,9 +18373,9 @@ snapshots: dependencies: '@jest/types': 29.6.3 - '@jest/create-cache-key-function@30.0.2': + '@jest/create-cache-key-function@30.0.5': dependencies: - '@jest/types': 30.0.1 + '@jest/types': 30.0.5 '@jest/environment@29.7.0': dependencies: @@ -17716,9 +18451,9 @@ snapshots: dependencies: '@sinclair/typebox': 0.27.8 - '@jest/schemas@30.0.1': + '@jest/schemas@30.0.5': dependencies: - '@sinclair/typebox': 0.34.38 + '@sinclair/typebox': 0.34.41 '@jest/source-map@29.6.3': dependencies: @@ -17778,10 +18513,10 @@ snapshots: '@types/yargs': 17.0.33 chalk: 4.1.2 - '@jest/types@30.0.1': + '@jest/types@30.0.5': dependencies: '@jest/pattern': 30.0.1 - '@jest/schemas': 30.0.1 + '@jest/schemas': 30.0.5 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 '@types/node': 22.18.6 @@ -17894,6 +18629,97 @@ snapshots: - encoding - supports-color + '@microsoft/api-extractor-model@7.30.7(@types/node@20.19.17)': + dependencies: + '@microsoft/tsdoc': 0.15.1 + '@microsoft/tsdoc-config': 0.17.1 + '@rushstack/node-core-library': 5.14.0(@types/node@20.19.17) + transitivePeerDependencies: + - '@types/node' + + '@microsoft/api-extractor-model@7.30.7(@types/node@22.18.6)': + dependencies: + '@microsoft/tsdoc': 0.15.1 + '@microsoft/tsdoc-config': 0.17.1 + '@rushstack/node-core-library': 5.14.0(@types/node@22.18.6) + transitivePeerDependencies: + - '@types/node' + optional: true + + '@microsoft/api-extractor-model@7.30.7(@types/node@24.3.1)': + dependencies: + '@microsoft/tsdoc': 0.15.1 + '@microsoft/tsdoc-config': 0.17.1 + '@rushstack/node-core-library': 5.14.0(@types/node@24.3.1) + transitivePeerDependencies: + - '@types/node' + optional: true + + '@microsoft/api-extractor@7.52.13(@types/node@20.19.17)': + dependencies: + '@microsoft/api-extractor-model': 7.30.7(@types/node@20.19.17) + '@microsoft/tsdoc': 0.15.1 + '@microsoft/tsdoc-config': 0.17.1 + '@rushstack/node-core-library': 5.14.0(@types/node@20.19.17) + '@rushstack/rig-package': 0.5.3 + '@rushstack/terminal': 0.16.0(@types/node@20.19.17) + '@rushstack/ts-command-line': 5.0.3(@types/node@20.19.17) + lodash: 4.17.21 + minimatch: 10.0.3 + resolve: 1.22.10 + semver: 7.5.4 + source-map: 0.6.1 + typescript: 5.8.2 + transitivePeerDependencies: + - '@types/node' + + '@microsoft/api-extractor@7.52.13(@types/node@22.18.6)': + dependencies: + '@microsoft/api-extractor-model': 7.30.7(@types/node@22.18.6) + '@microsoft/tsdoc': 0.15.1 + '@microsoft/tsdoc-config': 0.17.1 + '@rushstack/node-core-library': 5.14.0(@types/node@22.18.6) + '@rushstack/rig-package': 0.5.3 + '@rushstack/terminal': 0.16.0(@types/node@22.18.6) + '@rushstack/ts-command-line': 5.0.3(@types/node@22.18.6) + lodash: 4.17.21 + minimatch: 10.0.3 + resolve: 1.22.10 + semver: 7.5.4 + source-map: 0.6.1 + typescript: 5.8.2 + transitivePeerDependencies: + - '@types/node' + optional: true + + '@microsoft/api-extractor@7.52.13(@types/node@24.3.1)': + dependencies: + '@microsoft/api-extractor-model': 7.30.7(@types/node@24.3.1) + '@microsoft/tsdoc': 0.15.1 + '@microsoft/tsdoc-config': 0.17.1 + '@rushstack/node-core-library': 5.14.0(@types/node@24.3.1) + '@rushstack/rig-package': 0.5.3 + '@rushstack/terminal': 0.16.0(@types/node@24.3.1) + '@rushstack/ts-command-line': 5.0.3(@types/node@24.3.1) + lodash: 4.17.21 + minimatch: 10.0.3 + resolve: 1.22.10 + semver: 7.5.4 + source-map: 0.6.1 + typescript: 5.8.2 + transitivePeerDependencies: + - '@types/node' + optional: true + + '@microsoft/tsdoc-config@0.17.1': + dependencies: + '@microsoft/tsdoc': 0.15.1 + ajv: 8.12.0 + jju: 1.4.0 + resolve: 1.22.10 + + '@microsoft/tsdoc@0.15.1': {} + '@miniflare/cache@2.14.4': dependencies: '@miniflare/core': 2.14.4 @@ -18393,6 +19219,8 @@ snapshots: '@octokit/auth-token@4.0.0': {} + '@octokit/auth-token@5.1.2': {} + '@octokit/core@5.2.0': dependencies: '@octokit/auth-token': 4.0.0 @@ -18403,6 +19231,21 @@ snapshots: before-after-hook: 2.2.3 universal-user-agent: 6.0.1 + '@octokit/core@6.1.6': + dependencies: + '@octokit/auth-token': 5.1.2 + '@octokit/graphql': 8.2.2 + '@octokit/request': 9.2.4 + '@octokit/request-error': 6.1.8 + '@octokit/types': 14.1.0 + before-after-hook: 3.0.2 + universal-user-agent: 7.0.3 + + '@octokit/endpoint@10.1.4': + dependencies: + '@octokit/types': 14.1.0 + universal-user-agent: 7.0.3 + '@octokit/endpoint@9.0.6': dependencies: '@octokit/types': 13.8.0 @@ -18414,28 +19257,56 @@ snapshots: '@octokit/types': 13.8.0 universal-user-agent: 6.0.1 + '@octokit/graphql@8.2.2': + dependencies: + '@octokit/request': 9.2.4 + '@octokit/types': 14.1.0 + universal-user-agent: 7.0.3 + '@octokit/openapi-types@23.0.1': {} + '@octokit/openapi-types@24.2.0': {} + + '@octokit/openapi-types@25.1.0': {} + '@octokit/plugin-paginate-rest@11.4.4-cjs.2(@octokit/core@5.2.0)': dependencies: '@octokit/core': 5.2.0 '@octokit/types': 13.8.0 + '@octokit/plugin-paginate-rest@11.6.0(@octokit/core@6.1.6)': + dependencies: + '@octokit/core': 6.1.6 + '@octokit/types': 13.10.0 + '@octokit/plugin-request-log@4.0.0(@octokit/core@5.2.0)': dependencies: '@octokit/core': 5.2.0 + '@octokit/plugin-request-log@5.3.1(@octokit/core@6.1.6)': + dependencies: + '@octokit/core': 6.1.6 + '@octokit/plugin-rest-endpoint-methods@13.3.2-cjs.1(@octokit/core@5.2.0)': dependencies: '@octokit/core': 5.2.0 '@octokit/types': 13.8.0 + '@octokit/plugin-rest-endpoint-methods@13.5.0(@octokit/core@6.1.6)': + dependencies: + '@octokit/core': 6.1.6 + '@octokit/types': 13.10.0 + '@octokit/request-error@5.1.1': dependencies: '@octokit/types': 13.8.0 deprecation: 2.3.1 once: 1.4.0 + '@octokit/request-error@6.1.8': + dependencies: + '@octokit/types': 14.1.0 + '@octokit/request@8.4.1': dependencies: '@octokit/endpoint': 9.0.6 @@ -18443,6 +19314,14 @@ snapshots: '@octokit/types': 13.8.0 universal-user-agent: 6.0.1 + '@octokit/request@9.2.4': + dependencies: + '@octokit/endpoint': 10.1.4 + '@octokit/request-error': 6.1.8 + '@octokit/types': 14.1.0 + fast-content-type-parse: 2.0.1 + universal-user-agent: 7.0.3 + '@octokit/rest@20.1.2': dependencies: '@octokit/core': 5.2.0 @@ -18450,10 +19329,25 @@ snapshots: '@octokit/plugin-request-log': 4.0.0(@octokit/core@5.2.0) '@octokit/plugin-rest-endpoint-methods': 13.3.2-cjs.1(@octokit/core@5.2.0) + '@octokit/rest@21.1.1': + dependencies: + '@octokit/core': 6.1.6 + '@octokit/plugin-paginate-rest': 11.6.0(@octokit/core@6.1.6) + '@octokit/plugin-request-log': 5.3.1(@octokit/core@6.1.6) + '@octokit/plugin-rest-endpoint-methods': 13.5.0(@octokit/core@6.1.6) + + '@octokit/types@13.10.0': + dependencies: + '@octokit/openapi-types': 24.2.0 + '@octokit/types@13.8.0': dependencies: '@octokit/openapi-types': 23.0.1 + '@octokit/types@14.1.0': + dependencies: + '@octokit/openapi-types': 25.1.0 + '@one-ini/wasm@0.1.1': {} '@oozcitak/dom@1.15.10': @@ -19470,6 +20364,104 @@ snapshots: '@rtsao/scc@1.1.0': {} + '@rushstack/node-core-library@5.14.0(@types/node@20.19.17)': + dependencies: + ajv: 8.13.0 + ajv-draft-04: 1.0.0(ajv@8.13.0) + ajv-formats: 3.0.1(ajv@8.13.0) + fs-extra: 11.3.0 + import-lazy: 4.0.0 + jju: 1.4.0 + resolve: 1.22.10 + semver: 7.5.4 + optionalDependencies: + '@types/node': 20.19.17 + + '@rushstack/node-core-library@5.14.0(@types/node@22.18.6)': + dependencies: + ajv: 8.13.0 + ajv-draft-04: 1.0.0(ajv@8.13.0) + ajv-formats: 3.0.1(ajv@8.13.0) + fs-extra: 11.3.0 + import-lazy: 4.0.0 + jju: 1.4.0 + resolve: 1.22.10 + semver: 7.5.4 + optionalDependencies: + '@types/node': 22.18.6 + optional: true + + '@rushstack/node-core-library@5.14.0(@types/node@24.3.1)': + dependencies: + ajv: 8.13.0 + ajv-draft-04: 1.0.0(ajv@8.13.0) + ajv-formats: 3.0.1(ajv@8.13.0) + fs-extra: 11.3.0 + import-lazy: 4.0.0 + jju: 1.4.0 + resolve: 1.22.10 + semver: 7.5.4 + optionalDependencies: + '@types/node': 24.3.1 + optional: true + + '@rushstack/rig-package@0.5.3': + dependencies: + resolve: 1.22.10 + strip-json-comments: 3.1.1 + + '@rushstack/terminal@0.16.0(@types/node@20.19.17)': + dependencies: + '@rushstack/node-core-library': 5.14.0(@types/node@20.19.17) + supports-color: 8.1.1 + optionalDependencies: + '@types/node': 20.19.17 + + '@rushstack/terminal@0.16.0(@types/node@22.18.6)': + dependencies: + '@rushstack/node-core-library': 5.14.0(@types/node@22.18.6) + supports-color: 8.1.1 + optionalDependencies: + '@types/node': 22.18.6 + optional: true + + '@rushstack/terminal@0.16.0(@types/node@24.3.1)': + dependencies: + '@rushstack/node-core-library': 5.14.0(@types/node@24.3.1) + supports-color: 8.1.1 + optionalDependencies: + '@types/node': 24.3.1 + optional: true + + '@rushstack/ts-command-line@5.0.3(@types/node@20.19.17)': + dependencies: + '@rushstack/terminal': 0.16.0(@types/node@20.19.17) + '@types/argparse': 1.0.38 + argparse: 1.0.10 + string-argv: 0.3.2 + transitivePeerDependencies: + - '@types/node' + + '@rushstack/ts-command-line@5.0.3(@types/node@22.18.6)': + dependencies: + '@rushstack/terminal': 0.16.0(@types/node@22.18.6) + '@types/argparse': 1.0.38 + argparse: 1.0.10 + string-argv: 0.3.2 + transitivePeerDependencies: + - '@types/node' + optional: true + + '@rushstack/ts-command-line@5.0.3(@types/node@24.3.1)': + dependencies: + '@rushstack/terminal': 0.16.0(@types/node@24.3.1) + '@types/argparse': 1.0.38 + argparse: 1.0.10 + string-argv: 0.3.2 + transitivePeerDependencies: + - '@types/node' + optional: true + '@scure/base@1.2.6': {} '@scure/bip32@1.7.0': @@ -19536,7 +20528,7 @@ snapshots: '@sinclair/typebox@0.27.8': {} - '@sinclair/typebox@0.34.38': {} + '@sinclair/typebox@0.34.41': {} '@sindresorhus/is@4.6.0': {} @@ -19730,7 +20722,7 @@ snapshots: '@swc/jest@0.2.39(@swc/core@1.11.29(@swc/helpers@0.5.17))': dependencies: - '@jest/create-cache-key-function': 30.0.2 + '@jest/create-cache-key-function': 30.0.5 '@swc/core': 1.11.29(@swc/helpers@0.5.17) '@swc/counter': 0.1.3 jsonc-parser: 3.3.1 @@ -20079,7 +21071,7 @@ snapshots: lz-string: 1.5.0 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.4.6(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@22.18.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29(@swc/helpers@0.5.17))(@types/node@22.18.6)(typescript@5.8.3)))(vitest@3.0.5(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.18.6)(jiti@2.5.1)(jsdom@24.1.3)(lightningcss@1.27.0)(msw@2.11.2(@types/node@22.18.6)(typescript@5.8.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1))': + '@testing-library/jest-dom@6.4.6(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@22.18.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29(@swc/helpers@0.5.17))(@types/node@22.18.6)(typescript@5.8.3)))(vitest@3.0.5(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.18.6)(jiti@2.5.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.27.0)(msw@2.11.2(@types/node@22.18.6)(typescript@5.8.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1))': dependencies: '@adobe/css-tools': 4.4.0 '@babel/runtime': 7.27.6 @@ -20093,7 +21085,7 @@ snapshots: '@jest/globals': 29.7.0 '@types/jest': 29.5.12 jest: 29.7.0(@types/node@22.18.6)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29(@swc/helpers@0.5.17))(@types/node@22.18.6)(typescript@5.8.3)) - vitest: 3.0.5(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.18.6)(jiti@2.5.1)(jsdom@24.1.3)(lightningcss@1.27.0)(msw@2.11.2(@types/node@22.18.6)(typescript@5.8.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) + vitest: 3.0.5(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.18.6)(jiti@2.5.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.27.0)(msw@2.11.2(@types/node@22.18.6)(typescript@5.8.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) '@testing-library/react@16.0.0(@testing-library/dom@10.1.0)(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -20139,6 +21131,8 @@ snapshots: tslib: 2.8.1 optional: true + '@types/argparse@1.0.38': {} + '@types/aria-query@5.0.1': {} '@types/babel__code-frame@7.0.6': {} @@ -20179,6 +21173,9 @@ snapshots: dependencies: '@types/node': 22.18.6 + '@types/caseless@0.12.5': + optional: true + '@types/chrome@0.0.114': dependencies: '@types/filesystem': 0.0.32 @@ -20222,6 +21219,8 @@ snapshots: '@types/diff-match-patch@1.0.36': {} + '@types/diff@5.2.3': {} + '@types/estree@1.0.5': {} '@types/estree@1.0.8': {} @@ -20253,7 +21252,7 @@ snapshots: '@types/fs-extra@11.0.4': dependencies: '@types/jsonfile': 6.1.1 - '@types/node': 22.18.6 + '@types/node': 20.19.17 '@types/glob-to-regexp@0.4.4': {} @@ -20311,7 +21310,7 @@ snapshots: '@types/jsonfile@6.1.1': dependencies: - '@types/node': 22.18.6 + '@types/node': 20.19.17 '@types/mdast@4.0.4': dependencies: @@ -20335,6 +21334,10 @@ snapshots: '@types/node@12.20.55': {} + '@types/node@20.19.17': + dependencies: + undici-types: 6.21.0 + '@types/node@22.18.6': dependencies: undici-types: 6.21.0 @@ -20365,6 +21368,14 @@ snapshots: '@types/prop-types': 15.7.5 csstype: 3.1.3 + '@types/request@2.48.13': + dependencies: + '@types/caseless': 0.12.5 + '@types/node': 20.19.17 + '@types/tough-cookie': 4.0.5 + form-data: 2.5.5 + optional: true + '@types/resolve@1.20.2': {} '@types/retry@0.12.0': {} @@ -20834,7 +21845,7 @@ snapshots: vite: 7.1.5(@types/node@24.3.1)(jiti@2.5.1)(lightningcss@1.27.0)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) vue: 3.5.21(typescript@5.8.3) - '@vitest/coverage-v8@3.0.5(vitest@3.0.5(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.18.6)(jiti@2.5.1)(jsdom@24.1.3)(lightningcss@1.27.0)(msw@2.11.2(@types/node@22.18.6)(typescript@5.8.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1))': + '@vitest/coverage-v8@3.0.5(vitest@3.0.5(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.18.6)(jiti@2.5.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.27.0)(msw@2.11.2(@types/node@22.18.6)(typescript@5.8.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -20848,17 +21859,33 @@ snapshots: std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.0.5(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.18.6)(jiti@2.5.1)(jsdom@24.1.3)(lightningcss@1.27.0)(msw@2.11.2(@types/node@22.18.6)(typescript@5.8.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) + vitest: 3.0.5(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.18.6)(jiti@2.5.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.27.0)(msw@2.11.2(@types/node@22.18.6)(typescript@5.8.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) transitivePeerDependencies: - supports-color + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + tinyrainbow: 1.2.0 + '@vitest/expect@3.0.5': dependencies: '@vitest/spy': 3.0.5 '@vitest/utils': 3.0.5 - chai: 5.1.2 + chai: 5.3.3 tinyrainbow: 2.0.0 + '@vitest/mocker@2.1.9(msw@2.11.2(@types/node@20.19.17)(typescript@5.8.3))(vite@5.4.20(@types/node@20.19.17)(lightningcss@1.27.0)(terser@5.44.0))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.19 + optionalDependencies: + msw: 2.11.2(@types/node@20.19.17)(typescript@5.8.3) + vite: 5.4.20(@types/node@20.19.17)(lightningcss@1.27.0)(terser@5.44.0) + '@vitest/mocker@3.0.5(msw@2.11.2(@types/node@22.18.6)(typescript@5.8.3))(vite@6.3.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.27.0)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.0.5 @@ -20877,29 +21904,58 @@ snapshots: msw: 2.11.2(@types/node@24.3.1)(typescript@5.8.3) vite: 6.3.6(@types/node@24.3.1)(jiti@2.5.1)(lightningcss@1.27.0)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + '@vitest/pretty-format@3.0.5': dependencies: tinyrainbow: 2.0.0 + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + '@vitest/runner@3.0.5': dependencies: '@vitest/utils': 3.0.5 pathe: 2.0.3 + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.19 + pathe: 1.1.2 + '@vitest/snapshot@3.0.5': dependencies: '@vitest/pretty-format': 3.0.5 magic-string: 0.30.19 pathe: 2.0.3 + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + '@vitest/spy@3.0.5': dependencies: tinyspy: 3.0.2 + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + '@vitest/utils@3.0.5': dependencies: '@vitest/pretty-format': 3.0.5 - loupe: 3.1.2 + loupe: 3.2.1 tinyrainbow: 2.0.0 '@volar/language-core@2.1.6': @@ -21350,10 +22406,18 @@ snapshots: react: 18.3.1 zod: 3.24.2 + ajv-draft-04@1.0.0(ajv@8.13.0): + optionalDependencies: + ajv: 8.13.0 + ajv-formats@2.1.1(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 + ajv-formats@3.0.1(ajv@8.13.0): + optionalDependencies: + ajv: 8.13.0 + ajv-formats@3.0.1(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 @@ -21374,6 +22438,20 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.12.0: + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + + ajv@8.13.0: + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + ajv@8.17.1: dependencies: fast-deep-equal: 3.1.3 @@ -21572,6 +22650,9 @@ snapshots: arrify@1.0.1: {} + arrify@2.0.1: + optional: true + arrify@3.0.0: {} asap@2.0.6: {} @@ -21717,6 +22798,11 @@ snapshots: async-limiter@1.0.1: {} + async-retry@1.3.3: + dependencies: + retry: 0.13.1 + optional: true + async-sema@3.1.1: {} async@2.6.4: @@ -21936,6 +23022,8 @@ snapshots: before-after-hook@2.2.3: {} + before-after-hook@3.0.2: {} + better-opn@3.0.2: dependencies: open: 8.4.2 @@ -21944,8 +23032,16 @@ snapshots: dependencies: is-windows: 1.0.2 + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + optional: true + big-integer@1.6.52: {} + bignumber.js@9.3.1: + optional: true + binary-extensions@2.3.0: {} bindings@1.5.0: @@ -22235,12 +23331,12 @@ snapshots: supports-color: 8.1.1 window-size: 1.1.1 - chai@5.1.2: + chai@5.3.3: dependencies: assertion-error: 2.0.1 check-error: 2.1.1 deep-eql: 5.0.2 - loupe: 3.1.2 + loupe: 3.2.1 pathval: 2.0.0 chalk@2.4.2: @@ -22944,10 +24040,19 @@ snapshots: dependencies: cssom: 0.3.8 - cssstyle@4.3.1: + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + + cssstyle@5.3.0(postcss@8.5.6): dependencies: - '@asamuzakjp/css-color': 3.1.5 - rrweb-cssom: 0.8.0 + '@asamuzakjp/css-color': 4.0.4 + '@csstools/css-syntax-patches-for-csstree': 1.0.14(postcss@8.5.6) + css-tree: 3.1.0 + transitivePeerDependencies: + - postcss + optional: true csstype@3.1.3: {} @@ -23020,6 +24125,12 @@ snapshots: whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 + data-urls@6.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + optional: true + data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -23242,6 +24353,8 @@ snapshots: diff@5.2.0: {} + diff@7.0.0: {} + diff@8.0.2: {} dir-glob@3.0.1: @@ -23582,6 +24695,32 @@ snapshots: esbuild-plugin-file-path-extensions@2.1.4: {} + 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 + esbuild@0.23.1: optionalDependencies: '@esbuild/aix-ppc64': 0.23.1 @@ -24069,7 +25208,7 @@ snapshots: expect-type@0.20.0: {} - expect-type@1.1.0: {} + expect-type@1.2.2: {} expect@29.7.0: dependencies: @@ -24397,6 +25536,8 @@ snapshots: extsprintf@1.3.0: {} + fast-content-type-parse@2.0.1: {} + fast-decode-uri-component@1.0.1: {} fast-deep-equal@3.1.3: {} @@ -24669,6 +25810,16 @@ snapshots: forever-agent@0.6.1: {} + form-data@2.5.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + safe-buffer: 5.2.1 + optional: true + form-data@3.0.2: dependencies: asynckit: 0.4.0 @@ -24770,6 +25921,28 @@ snapshots: fuse.js@7.1.0: {} + gaxios@6.7.1: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + is-stream: 2.0.1 + node-fetch: 2.7.0 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + gcp-metadata@6.1.1: + dependencies: + gaxios: 6.7.1 + google-logging-utils: 0.0.2 + json-bigint: 1.0.0 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -24970,6 +26143,22 @@ snapshots: slash: 5.1.0 unicorn-magic: 0.3.0 + google-auth-library@9.15.1: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 6.7.1 + gcp-metadata: 6.1.1 + gtoken: 7.1.0 + jws: 4.0.0 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + google-logging-utils@0.0.2: + optional: true + gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -24990,6 +26179,15 @@ snapshots: section-matter: 1.0.0 strip-bom-string: 1.0.0 + gtoken@7.1.0: + dependencies: + gaxios: 6.7.1 + jws: 4.0.0 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + gunzip-maybe@1.4.2: dependencies: browserify-zlib: 0.1.4 @@ -25405,6 +26603,8 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + import-lazy@4.0.0: {} + import-local@3.1.0: dependencies: pkg-dir: 4.2.0 @@ -26246,6 +27446,8 @@ snapshots: jiti@2.5.1: {} + jju@1.4.0: {} + joi@17.13.3: dependencies: '@hapi/hoek': 9.3.0 @@ -26377,7 +27579,7 @@ snapshots: jsdom@24.1.3: dependencies: - cssstyle: 4.3.1 + cssstyle: 4.6.0 data-urls: 5.0.0 decimal.js: 10.5.0 form-data: 4.0.4 @@ -26403,10 +27605,44 @@ snapshots: - supports-color - utf-8-validate + jsdom@27.0.0(postcss@8.5.6): + dependencies: + '@asamuzakjp/dom-selector': 6.5.5 + cssstyle: 5.3.0(postcss@8.5.6) + data-urls: 6.0.0 + decimal.js: 10.5.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + parse5: 7.3.0 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + ws: 8.18.3 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - postcss + - supports-color + - utf-8-validate + optional: true + jsesc@3.0.2: {} jsesc@3.1.0: {} + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + optional: true + json-buffer@3.0.1: {} json-cycle@1.5.0: {} @@ -26494,11 +27730,24 @@ snapshots: ecdsa-sig-formatter: 1.0.11 safe-buffer: 5.2.1 + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + optional: true + jws@3.2.2: dependencies: jwa: 1.4.1 safe-buffer: 5.2.1 + jws@4.0.0: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + optional: true + keygrip@1.1.0: dependencies: tsscmp: 1.0.6 @@ -26810,7 +28059,7 @@ snapshots: dependencies: js-tokens: 4.0.0 - loupe@3.1.2: {} + loupe@3.2.1: {} lowdb@1.0.0: dependencies: @@ -26822,7 +28071,7 @@ snapshots: lru-cache@10.4.3: {} - lru-cache@11.0.2: {} + lru-cache@11.2.1: {} lru-cache@5.1.1: dependencies: @@ -27603,6 +28852,33 @@ snapshots: ms@2.1.3: {} + msw@2.11.2(@types/node@20.19.17)(typescript@5.8.3): + dependencies: + '@bundled-es-modules/cookie': 2.0.1 + '@bundled-es-modules/statuses': 1.0.1 + '@inquirer/confirm': 5.1.13(@types/node@20.19.17) + '@mswjs/interceptors': 0.39.3 + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/until': 2.1.0 + '@types/cookie': 0.6.0 + '@types/statuses': 2.0.6 + graphql: 16.11.0 + headers-polyfill: 4.0.3 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.7.0 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.0 + type-fest: 4.41.0 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - '@types/node' + optional: true + msw@2.11.2(@types/node@22.18.6)(typescript@5.8.3): dependencies: '@bundled-es-modules/cookie': 2.0.1 @@ -28624,7 +29900,7 @@ snapshots: path-scurry@2.0.0: dependencies: - lru-cache: 11.0.2 + lru-cache: 11.2.1 minipass: 7.1.2 path-to-regexp@0.1.10: {} @@ -29662,6 +30938,16 @@ snapshots: retext-stringify: 4.0.0 unified: 11.0.5 + retry-request@7.0.2: + dependencies: + '@types/request': 2.48.13 + extend: 3.0.2 + teeny-request: 9.0.0 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + retry@0.13.1: {} rettime@0.7.0: {} @@ -29845,6 +31131,10 @@ snapshots: dependencies: lru-cache: 6.0.0 + semver@7.5.4: + dependencies: + lru-cache: 6.0.0 + semver@7.6.3: {} semver@7.7.2: {} @@ -30348,6 +31638,11 @@ snapshots: stream-buffers@2.2.0: {} + stream-events@1.0.5: + dependencies: + stubs: 3.0.0 + optional: true + stream-shift@1.0.3: {} streamsearch@1.1.0: {} @@ -30508,6 +31803,9 @@ snapshots: structured-headers@0.4.1: {} + stubs@3.0.0: + optional: true + styled-jsx@5.1.1(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react@18.3.1): dependencies: client-only: 0.0.1 @@ -30677,6 +31975,18 @@ snapshots: mkdirp: 3.0.1 yallist: 5.0.0 + teeny-request@9.0.0: + dependencies: + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + node-fetch: 2.7.0 + stream-events: 1.0.5 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + temp-dir@2.0.0: {} temp-dir@3.0.0: {} @@ -30804,7 +32114,9 @@ snapshots: '@types/tinycolor2': 1.4.6 tinycolor2: 1.6.0 - tinypool@1.0.2: {} + tinypool@1.1.1: {} + + tinyrainbow@1.2.0: {} tinyrainbow@2.0.0: {} @@ -30871,6 +32183,11 @@ snapshots: dependencies: punycode: 2.3.1 + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + optional: true + tree-dump@1.0.2(tslib@2.8.1): dependencies: tslib: 2.8.1 @@ -30944,7 +32261,67 @@ snapshots: tsscmp@1.0.6: {} - tsup@8.5.0(@swc/core@1.11.29(@swc/helpers@0.5.17))(jiti@2.5.1)(postcss@8.5.6)(tsx@4.19.2)(typescript@5.8.3)(yaml@2.8.1): + tsup@8.5.0(@microsoft/api-extractor@7.52.13(@types/node@20.19.17))(@swc/core@1.11.29(@swc/helpers@0.5.17))(jiti@2.5.1)(postcss@8.5.6)(tsx@4.19.2)(typescript@5.8.3)(yaml@2.8.1): + dependencies: + bundle-require: 5.1.0(esbuild@0.25.0) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.1(supports-color@8.1.1) + esbuild: 0.25.0 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(jiti@2.5.1)(postcss@8.5.6)(tsx@4.19.2)(yaml@2.8.1) + resolve-from: 5.0.0 + rollup: 4.50.1 + source-map: 0.8.0-beta.0 + sucrase: 3.35.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + optionalDependencies: + '@microsoft/api-extractor': 7.52.13(@types/node@20.19.17) + '@swc/core': 1.11.29(@swc/helpers@0.5.17) + postcss: 8.5.6 + typescript: 5.8.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + + tsup@8.5.0(@microsoft/api-extractor@7.52.13(@types/node@22.18.6))(@swc/core@1.11.29(@swc/helpers@0.5.17))(jiti@2.5.1)(postcss@8.5.6)(tsx@4.19.2)(typescript@5.8.3)(yaml@2.8.1): + dependencies: + bundle-require: 5.1.0(esbuild@0.25.0) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.1(supports-color@8.1.1) + esbuild: 0.25.0 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(jiti@2.5.1)(postcss@8.5.6)(tsx@4.19.2)(yaml@2.8.1) + resolve-from: 5.0.0 + rollup: 4.50.1 + source-map: 0.8.0-beta.0 + sucrase: 3.35.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + optionalDependencies: + '@microsoft/api-extractor': 7.52.13(@types/node@22.18.6) + '@swc/core': 1.11.29(@swc/helpers@0.5.17) + postcss: 8.5.6 + typescript: 5.8.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + + tsup@8.5.0(@microsoft/api-extractor@7.52.13(@types/node@24.3.1))(@swc/core@1.11.29(@swc/helpers@0.5.17))(jiti@2.5.1)(postcss@8.5.6)(tsx@4.19.2)(typescript@5.8.3)(yaml@2.8.1): dependencies: bundle-require: 5.1.0(esbuild@0.25.0) cac: 6.7.14 @@ -30964,6 +32341,7 @@ snapshots: tinyglobby: 0.2.15 tree-kill: 1.2.2 optionalDependencies: + '@microsoft/api-extractor': 7.52.13(@types/node@24.3.1) '@swc/core': 1.11.29(@swc/helpers@0.5.17) postcss: 8.5.6 typescript: 5.8.3 @@ -31125,6 +32503,8 @@ snapshots: typescript@5.6.1-rc: {} + typescript@5.8.2: {} + typescript@5.8.3: {} ua-parser-js@1.0.40: {} @@ -31307,6 +32687,8 @@ snapshots: universal-user-agent@6.0.1: {} + universal-user-agent@7.0.3: {} + universalify@0.1.2: {} universalify@0.2.0: {} @@ -31645,6 +33027,24 @@ snapshots: dependencies: vite: 7.1.5(@types/node@24.3.1)(jiti@2.5.1)(lightningcss@1.27.0)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) + vite-node@2.1.9(@types/node@20.19.17)(lightningcss@1.27.0)(terser@5.44.0): + dependencies: + cac: 6.7.14 + debug: 4.4.1(supports-color@8.1.1) + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.20(@types/node@20.19.17)(lightningcss@1.27.0)(terser@5.44.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vite-node@3.0.5(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.27.0)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1): dependencies: cac: 6.7.14 @@ -31753,6 +33153,17 @@ snapshots: vite: 7.1.5(@types/node@24.3.1)(jiti@2.5.1)(lightningcss@1.27.0)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) vue: 3.5.21(typescript@5.8.3) + vite@5.4.20(@types/node@20.19.17)(lightningcss@1.27.0)(terser@5.44.0): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.50.1 + optionalDependencies: + '@types/node': 20.19.17 + fsevents: 2.3.3 + lightningcss: 1.27.0 + terser: 5.44.0 + vite@6.3.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.27.0)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1): dependencies: esbuild: 0.25.0 @@ -31829,36 +33240,73 @@ snapshots: optionalDependencies: vite: 7.1.5(@types/node@24.3.1)(jiti@2.5.1)(lightningcss@1.27.0)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) - vitest-environment-miniflare@2.14.4(vitest@3.0.5(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@24.3.1)(jiti@2.5.1)(jsdom@24.1.3)(lightningcss@1.27.0)(msw@2.11.2(@types/node@24.3.1)(typescript@5.8.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)): + vitest-environment-miniflare@2.14.4(vitest@3.0.5(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@24.3.1)(jiti@2.5.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.27.0)(msw@2.11.2(@types/node@24.3.1)(typescript@5.8.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)): dependencies: '@miniflare/queues': 2.14.4 '@miniflare/runner-vm': 2.14.4 '@miniflare/shared': 2.14.4 '@miniflare/shared-test-environment': 2.14.4 undici: 5.28.4 - vitest: 3.0.5(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@24.3.1)(jiti@2.5.1)(jsdom@24.1.3)(lightningcss@1.27.0)(msw@2.11.2(@types/node@24.3.1)(typescript@5.8.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) + vitest: 3.0.5(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@24.3.1)(jiti@2.5.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.27.0)(msw@2.11.2(@types/node@24.3.1)(typescript@5.8.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) transitivePeerDependencies: - bufferutil - utf-8-validate - vitest@3.0.5(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.18.6)(jiti@2.5.1)(jsdom@24.1.3)(lightningcss@1.27.0)(msw@2.11.2(@types/node@22.18.6)(typescript@5.8.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1): + vitest@2.1.9(@edge-runtime/vm@5.0.0)(@types/node@20.19.17)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.27.0)(msw@2.11.2(@types/node@20.19.17)(typescript@5.8.3))(terser@5.44.0): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(msw@2.11.2(@types/node@20.19.17)(typescript@5.8.3))(vite@5.4.20(@types/node@20.19.17)(lightningcss@1.27.0)(terser@5.44.0)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.1(supports-color@8.1.1) + expect-type: 1.2.2 + magic-string: 0.30.19 + pathe: 1.1.2 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.20(@types/node@20.19.17)(lightningcss@1.27.0)(terser@5.44.0) + vite-node: 2.1.9(@types/node@20.19.17)(lightningcss@1.27.0)(terser@5.44.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@edge-runtime/vm': 5.0.0 + '@types/node': 20.19.17 + jsdom: 27.0.0(postcss@8.5.6) + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vitest@3.0.5(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.18.6)(jiti@2.5.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.27.0)(msw@2.11.2(@types/node@22.18.6)(typescript@5.8.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1): dependencies: '@vitest/expect': 3.0.5 '@vitest/mocker': 3.0.5(msw@2.11.2(@types/node@22.18.6)(typescript@5.8.3))(vite@6.3.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.27.0)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) - '@vitest/pretty-format': 3.0.5 + '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.0.5 '@vitest/snapshot': 3.0.5 '@vitest/spy': 3.0.5 '@vitest/utils': 3.0.5 - chai: 5.1.2 + chai: 5.3.3 debug: 4.4.1(supports-color@8.1.1) - expect-type: 1.1.0 + expect-type: 1.2.2 magic-string: 0.30.19 pathe: 2.0.3 std-env: 3.9.0 tinybench: 2.9.0 tinyexec: 0.3.2 - tinypool: 1.0.2 + tinypool: 1.1.1 tinyrainbow: 2.0.0 vite: 6.3.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.27.0)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) vite-node: 3.0.5(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.27.0)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) @@ -31867,7 +33315,7 @@ snapshots: '@edge-runtime/vm': 5.0.0 '@types/debug': 4.1.12 '@types/node': 22.18.6 - jsdom: 24.1.3 + jsdom: 27.0.0(postcss@8.5.6) transitivePeerDependencies: - jiti - less @@ -31882,24 +33330,24 @@ snapshots: - tsx - yaml - vitest@3.0.5(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@24.3.1)(jiti@2.5.1)(jsdom@24.1.3)(lightningcss@1.27.0)(msw@2.11.2(@types/node@24.3.1)(typescript@5.8.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1): + vitest@3.0.5(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@24.3.1)(jiti@2.5.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.27.0)(msw@2.11.2(@types/node@24.3.1)(typescript@5.8.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1): dependencies: '@vitest/expect': 3.0.5 '@vitest/mocker': 3.0.5(msw@2.11.2(@types/node@24.3.1)(typescript@5.8.3))(vite@6.3.6(@types/node@24.3.1)(jiti@2.5.1)(lightningcss@1.27.0)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) - '@vitest/pretty-format': 3.0.5 + '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.0.5 '@vitest/snapshot': 3.0.5 '@vitest/spy': 3.0.5 '@vitest/utils': 3.0.5 - chai: 5.1.2 + chai: 5.3.3 debug: 4.4.1(supports-color@8.1.1) - expect-type: 1.1.0 + expect-type: 1.2.2 magic-string: 0.30.19 pathe: 2.0.3 std-env: 3.9.0 tinybench: 2.9.0 tinyexec: 0.3.2 - tinypool: 1.0.2 + tinypool: 1.1.1 tinyrainbow: 2.0.0 vite: 6.3.6(@types/node@24.3.1)(jiti@2.5.1)(lightningcss@1.27.0)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) vite-node: 3.0.5(@types/node@24.3.1)(jiti@2.5.1)(lightningcss@1.27.0)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) @@ -31908,7 +33356,7 @@ snapshots: '@edge-runtime/vm': 5.0.0 '@types/debug': 4.1.12 '@types/node': 24.3.1 - jsdom: 24.1.3 + jsdom: 27.0.0(postcss@8.5.6) transitivePeerDependencies: - jiti - less @@ -32000,6 +33448,9 @@ snapshots: webidl-conversions@7.0.0: {} + webidl-conversions@8.0.0: + optional: true + webpack-bundle-analyzer@4.10.2: dependencies: '@discoveryjs/json-ext': 0.5.7 @@ -32145,6 +33596,12 @@ snapshots: tr46: 5.1.1 webidl-conversions: 7.0.0 + whatwg-url@15.1.0: + dependencies: + tr46: 6.0.0 + webidl-conversions: 8.0.0 + optional: true + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 5f78eb562ab..1ae89055b89 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,6 @@ packages: - 'packages/*' + - 'tools/*' minimumReleaseAge: 2880 diff --git a/snapi.config.json b/snapi.config.json new file mode 100644 index 00000000000..ac63d7d2391 --- /dev/null +++ b/snapi.config.json @@ -0,0 +1,36 @@ +{ + "packages": ["@clerk/backend"], + "excludePackages": ["@clerk/testing", "@clerk/eslint-config"], + "snapshotsDir": ".api-snapshots", + "mainBranch": "main", + "checkVersionBump": true, + "suppressedChanges": [], + "enableLLMAnalysis": false, + "llmProvider": "openai", + "ci": { + "enableStatusChecks": true, + "failOnBreakingChanges": true, + "artifactRetentionDays": 7, + "parallelSnapshots": true, + "batchSize": 5 + }, + "github": { + "token": "${GITHUB_TOKEN}", + "repository": "${GITHUB_REPOSITORY}", + "commentOnPR": true, + "addStatusChecks": true, + "createAnnotations": true + }, + "reporting": { + "format": "markdown", + "includeCodeDiffs": true, + "includeSuggestions": true, + "maxChangesPerPackage": 50 + }, + "performance": { + "maxConcurrentPackages": 3, + "timeoutMs": 300000, + "cacheBaseline": true, + "cacheExpirationDays": 1 + } +} diff --git a/tools/snapi/README.md b/tools/snapi/README.md new file mode 100644 index 00000000000..5ed59aeaac2 --- /dev/null +++ b/tools/snapi/README.md @@ -0,0 +1,371 @@ +# SNAPI (Snapshot API) + +A comprehensive tool for detecting breaking changes in TypeScript package APIs, designed for production use with robust cloud storage integration. + +## 🚀 Features + +### Core Functionality + +- **API Snapshot Generation**: Uses Microsoft API Extractor to create detailed API snapshots +- **Breaking Change Detection**: Intelligent analysis of API changes with severity classification +- **Version Bump Validation**: Ensures semantic versioning compliance +- **Suppression Management**: Flexible system for managing false positives +- **Rich Reporting**: Markdown reports with code diffs and actionable insights + +### Production Storage Solutions + +- **Google Cloud Storage (GCS)**: Primary production storage with lifecycle management +- **Multi-Backend Support**: Automatic failover between storage backends +- **Health Monitoring**: Real-time storage backend health checks +- **Cost Optimization**: Intelligent storage class transitions and cleanup +- **Batch Operations**: Efficient bulk snapshot operations + +### CI/CD Integration + +- **GitHub Actions**: Automated PR checking and status updates +- **Turborepo Integration**: Leverages existing cache infrastructure +- **Parallel Processing**: Concurrent package analysis for speed +- **Artifact Management**: Efficient storage and retrieval of snapshots + +## 📦 Installation + +```bash +# Install the tool +pnpm install @clerk/snapi + +# Install optional cloud storage dependencies +pnpm install @google-cloud/storage # For GCS support +pnpm install @aws-sdk/client-s3 # For S3 support +pnpm install @azure/storage-blob # For Azure support +``` + +## 🔧 Configuration + +### Basic Configuration + +Create `snapi.config.json` in your repository root: + +```json +{ + "packages": ["@your-org/package1", "@your-org/package2"], + "excludePackages": ["@your-org/testing"], + "snapshotsDir": ".api-snapshots", + "mainBranch": "main", + "checkVersionBump": true, + "suppressedChanges": [] +} +``` + +### Production GCS Configuration + +```json +{ + "packages": ["@your-org/package1", "@your-org/package2"], + "excludePackages": ["@your-org/testing"], + "snapshotsDir": ".api-snapshots", + "mainBranch": "main", + "checkVersionBump": true, + "suppressedChanges": [], + "storage": { + "primary": { + "type": "gcs", + "options": { + "projectId": "your-gcp-project", + "bucket": "your-api-snapshots-bucket", + "prefix": "api-snapshots", + "multiRegion": true, + "enableVersioning": true, + "lifecycle": { + "enabled": true, + "deleteAfterDays": 365, + "archiveAfterDays": 90 + }, + "storageClass": "STANDARD" + } + }, + "fallback": [ + { + "type": "git-lfs", + "options": { + "repositoryUrl": "https://github.com/your-org/api-snapshots.git", + "branch": "main", + "snapshotsPath": "snapshots" + } + } + ], + "healthCheckInterval": 10, + "retryAttempts": 3, + "retryDelay": 1000 + } +} +``` + +## 🛠️ Usage + +### CLI Commands + +```bash +# Initialize configuration +snapi init + +# Generate API snapshots +snapi snapshot + +# Detect breaking changes +snapi detect + +# Storage management +snapi storage health +snapi storage stats +snapi storage cleanup --days 30 + +# Suppression management +snapi suppress -p @your-org/package -i change-id -r "Reason" +snapi cleanup +``` + +### Programmatic Usage + +```typescript +import { BreakingChangesDetector } from '@clerk/snapi'; + +const detector = new BreakingChangesDetector({ + workspaceRoot: process.cwd(), + config: { + packages: ['@your-org/package'], + excludePackages: [], + snapshotsDir: '.api-snapshots', + mainBranch: 'main', + checkVersionBump: true, + suppressedChanges: [], + storage: { + primary: { + type: 'gcs', + options: { + projectId: 'your-project', + bucket: 'your-bucket', + }, + }, + }, + }, +}); + +const result = await detector.detectBreakingChanges(); +console.log(result); +``` + +## ☁️ Google Cloud Storage Setup + +### Prerequisites + +1. Google Cloud Project with billing enabled +2. Cloud Storage API enabled +3. Service account with Storage Object Admin permissions + +### Quick Setup + +```bash +# Set up GCS bucket +export PROJECT_ID="your-project-id" +export BUCKET_NAME="your-api-snapshots" + +gsutil mb -p $PROJECT_ID -c STANDARD -l us-central1 gs://$BUCKET_NAME +gsutil versioning set on gs://$BUCKET_NAME + +# Create service account +gcloud iam service-accounts create snapi \ + --display-name="SNAPI (Snapshot API)" + +# Generate key +gcloud iam service-accounts keys create sa-key.json \ + --iam-account=snapi@$PROJECT_ID.iam.gserviceaccount.com + +# Set permissions +gsutil iam ch serviceAccount:snapi@$PROJECT_ID.iam.gserviceaccount.com:objectAdmin gs://$BUCKET_NAME +``` + +### Environment Variables + +```bash +export GOOGLE_APPLICATION_CREDENTIALS="/path/to/sa-key.json" +export GOOGLE_CLOUD_PROJECT="your-project-id" +export GCS_BUCKET="your-api-snapshots" +``` + +## 🔄 GitHub Actions Integration + +```yaml +name: API Breakage Check + +on: + pull_request: + branches: [main] + +jobs: + api-breakage-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + + - name: Setup GCS credentials + run: | + echo '${{ secrets.GCS_SERVICE_ACCOUNT_KEY }}' > /tmp/gcs-key.json + echo "GOOGLE_APPLICATION_CREDENTIALS=/tmp/gcs-key.json" >> $GITHUB_ENV + + - name: Install dependencies + run: pnpm install + + - name: Check API breaking changes + run: | + snapi detect \ + --fail-on-breaking \ + --output api-breakage-report.md + env: + GOOGLE_CLOUD_PROJECT: ${{ secrets.GCS_PROJECT_ID }} + GCS_BUCKET: ${{ secrets.GCS_BUCKET }} + + - name: Comment PR + uses: actions/github-script@v7 + if: always() + with: + script: | + const fs = require('fs'); + if (fs.existsSync('api-breakage-report.md')) { + const report = fs.readFileSync('api-breakage-report.md', 'utf8'); + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: report + }); + } +``` + +## 📊 Storage Features + +### Cost Optimization + +- **Lifecycle Management**: Automatic transition to cheaper storage classes +- **Compression**: Automatic gzip compression for files > 1KB +- **Cleanup**: Configurable retention policies +- **Monitoring**: Built-in cost tracking and alerts + +### Reliability + +- **Multi-Regional**: Global availability and redundancy +- **Versioning**: Object versioning for data protection +- **Health Checks**: Continuous monitoring and failover +- **Retry Logic**: Exponential backoff for transient failures + +### Performance + +- **Parallel Operations**: Concurrent uploads and downloads +- **Caching**: Intelligent baseline caching +- **Batch Processing**: Efficient bulk operations +- **Edge Caching**: Global CDN integration + +## 🔍 Monitoring and Observability + +### Health Checks + +```bash +# Check storage backend health +snapi storage health + +# Get storage statistics +snapi storage stats + +# Monitor costs and usage +gsutil du -sh gs://your-bucket +``` + +### Metrics + +- Storage usage and costs +- API request rates and errors +- Upload/download latency +- Health check failures +- Snapshot generation times + +## 🛡️ Security + +### Best Practices + +- **Least Privilege**: Minimal IAM permissions +- **Encryption**: Automatic encryption at rest +- **Audit Logging**: Comprehensive access logs +- **Private Access**: No public bucket access +- **Key Management**: Secure credential handling + +### Compliance + +- **Data Retention**: Configurable retention policies +- **Access Control**: Fine-grained permissions +- **Audit Trail**: Complete operation logging +- **Backup**: Multi-region redundancy + +## 📚 Documentation + +- [GCS Production Setup Guide](./docs/GCS_PRODUCTION_SETUP.md) +- [Configuration Reference](./docs/CONFIGURATION.md) +- [Troubleshooting Guide](./docs/TROUBLESHOOTING.md) +- [API Reference](./docs/API.md) + +## 🤝 Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Add tests +5. Submit a pull request + +## 📄 License + +MIT License - see [LICENSE](./LICENSE) for details. + +## 🆘 Support + +- [GitHub Issues](https://github.com/your-org/api-breakage-detector/issues) +- [Documentation](./docs/) +- [GCS Support](https://cloud.google.com/support) + +--- + +Built with ❤️ for reliable API evolution in production environments. + +## 🔐 Environment Variables + +The API Breakage Detector uses the following environment variables for authentication: + +### Google Cloud Storage Configuration + +| Variable | Description | Required | Example | +| ---------------- | -------------------- | -------- | -------------------- | +| `GCS_PROJECT_ID` | Your GCP project ID | **Yes** | `'my-gcp-project'` | +| `GCS_BUCKET` | Your GCS bucket name | **Yes** | `'my-api-snapshots'` | + +### Google Cloud Storage Authentication (choose one) + +| Variable | Description | Example | +| -------------------------------- | ------------------------------------ | --------------------------------------- | +| `GOOGLE_SERVICE_ACCOUNT_KEY` | JSON service account key content | `'{"type":"service_account",...}'` | +| `GOOGLE_APPLICATION_CREDENTIALS` | Path to service account key file | `'/path/to/key.json'` | +| _(none)_ | Uses default application credentials | `gcloud auth application-default login` | + +### GitHub Integration (for CI/CD) + +| Variable | Description | Example | +| ------------------- | ------------------------------- | -------------------- | +| `GITHUB_TOKEN` | GitHub API token | `'ghp_...'` | +| `GITHUB_REPOSITORY` | Repository in owner/repo format | `'clerk/javascript'` | + +For detailed setup instructions, see [`docs/GCS_ENV_SETUP.md`](./docs/GCS_ENV_SETUP.md). + +## �� Storage Backends diff --git a/tools/snapi/bin/cli.js b/tools/snapi/bin/cli.js new file mode 100755 index 00000000000..2ec4997953a --- /dev/null +++ b/tools/snapi/bin/cli.js @@ -0,0 +1,10 @@ +#!/usr/bin/env node + +import('../dist/cli.js') + .then(({ default: cli }) => { + // The CLI module should handle everything + }) + .catch(error => { + console.error('Failed to load CLI:', error); + process.exit(1); + }); diff --git a/tools/snapi/package.json b/tools/snapi/package.json new file mode 100644 index 00000000000..01541286ff0 --- /dev/null +++ b/tools/snapi/package.json @@ -0,0 +1,58 @@ +{ + "name": "@clerk/snapi", + "version": "1.0.0", + "description": "Snapshot API (SNAPI) - Detect breaking changes in TypeScript package APIs", + "type": "module", + "exports": "./dist/index.js", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "bin": { + "snapi": "./bin/cli.js" + }, + "scripts": { + "build": "tsup", + "clean": "rimraf dist", + "cleanup": "node ./dist/cli.js cleanup", + "detect": "node ./dist/cli.js detect", + "dev": "tsup --watch", + "format": "prettier --write .", + "format:check": "prettier --check .", + "health:storage": "node dist/cli.js storage health", + "init": "node ./dist/cli.js init", + "lint": "eslint src", + "setup:gcs": "node scripts/setup-gcs.js", + "storage:stats": "node dist/cli.js storage stats", + "suppress": "node ./dist/cli.js suppress", + "test": "vitest run", + "test:watch": "vitest watch" + }, + "dependencies": { + "@microsoft/api-extractor": "^7.47.9", + "@microsoft/api-extractor-model": "^7.29.4", + "@octokit/rest": "^21.0.0", + "chalk": "^5.3.0", + "commander": "^12.1.0", + "diff": "^7.0.0", + "fs-extra": "^11.2.0", + "globby": "^14.0.2", + "semver": "^7.6.0", + "yaml": "^2.4.0", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/diff": "^5.2.0", + "@types/fs-extra": "^11.0.4", + "@types/node": "^20.11.0", + "@types/semver": "^7.5.0", + "eslint": "^9.0.0", + "tsup": "catalog:repo", + "typescript": "catalog:repo", + "vitest": "^2.0.0" + }, + "optionalDependencies": { + "@google-cloud/storage": "^7.16.0" + }, + "engines": { + "node": ">=22.0.0" + } +} diff --git a/tools/snapi/src/analyzers/api-diff.ts b/tools/snapi/src/analyzers/api-diff.ts new file mode 100644 index 00000000000..32232cee807 --- /dev/null +++ b/tools/snapi/src/analyzers/api-diff.ts @@ -0,0 +1,461 @@ +import type { + ApiClass, + ApiEnum, + ApiFunction, + ApiInterface, + ApiItem, + ExcerptToken, +} from '@microsoft/api-extractor-model'; +import { ApiItemKind, ApiModel } from '@microsoft/api-extractor-model'; +import { createHash } from 'crypto'; +import fs from 'fs-extra'; + +import type { ApiChange } from '../types.js'; +import { ChangeSeverity, ChangeType } from '../types.js'; + +interface ComparisonContext { + currentModel: ApiModel; + previousModel: ApiModel; + packageName: string; +} + +/** + * Analyzes differences between API models and categorizes changes + */ +export class ApiDiffAnalyzer { + async compareApiModels( + currentApiJsonPath: string, + previousApiJsonPath: string, + packageName: string, + ): Promise { + if (!(await fs.pathExists(previousApiJsonPath))) { + // No previous version to compare against - all current exports are additions + return this.analyzeAsNewPackage(currentApiJsonPath, packageName); + } + + const currentModel = new ApiModel(); + const previousModel = new ApiModel(); + + try { + currentModel.loadPackage(currentApiJsonPath); + previousModel.loadPackage(previousApiJsonPath); + } catch (error) { + throw new Error(`Failed to load API models: ${error}`); + } + + const context: ComparisonContext = { + currentModel, + previousModel, + packageName, + }; + + return this.analyzeChanges(context); + } + + private async analyzeAsNewPackage(apiJsonPath: string, packageName: string): Promise { + const model = new ApiModel(); + model.loadPackage(apiJsonPath); + + const changes: ApiChange[] = []; + const apiPackage = model.packages[0]; + + if (!apiPackage) return changes; + + // All items in new package are additions + this.visitApiItems(apiPackage, item => { + if (this.isPublicApi(item)) { + changes.push(this.createAdditionChange(item, packageName)); + } + }); + + return changes; + } + + private analyzeChanges(context: ComparisonContext): ApiChange[] { + const changes: ApiChange[] = []; + + const currentPackage = context.currentModel.packages[0]; + const previousPackage = context.previousModel.packages[0]; + + if (!currentPackage || !previousPackage) { + return changes; + } + + // Create maps for efficient lookup + const currentItems = this.createItemMap(currentPackage); + const previousItems = this.createItemMap(previousPackage); + + // Find removed items (breaking changes) + for (const [key, previousItem] of previousItems) { + if (!currentItems.has(key) && this.isPublicApi(previousItem)) { + changes.push(this.createRemovalChange(previousItem, context.packageName)); + } + } + + // Find added items (additions) + for (const [key, currentItem] of currentItems) { + if (!previousItems.has(key) && this.isPublicApi(currentItem)) { + changes.push(this.createAdditionChange(currentItem, context.packageName)); + } + } + + // Find modified items (potential breaking changes) + for (const [key, currentItem] of currentItems) { + const previousItem = previousItems.get(key); + if (previousItem && this.isPublicApi(currentItem)) { + const itemChanges = this.compareApiItems(currentItem, previousItem, context.packageName); + changes.push(...itemChanges); + } + } + + return changes; + } + + private createItemMap(packageItem: ApiItem): Map { + const map = new Map(); + + this.visitApiItems(packageItem, item => { + const key = this.getItemKey(item); + map.set(key, item); + }); + + return map; + } + + private visitApiItems(root: ApiItem, callback: (item: ApiItem) => void): void { + const visit = (item: ApiItem) => { + callback(item); + for (const child of item.members) { + visit(child); + } + }; + + visit(root); + } + + private getItemKey(item: ApiItem): string { + // Create a unique key that includes parent context + const parts = []; + let current: ApiItem | undefined = item; + + while (current && current.kind !== ApiItemKind.Package) { + parts.unshift(current.displayName); + current = current.parent; + } + + return parts.join('.'); + } + + private compareApiItems(currentItem: ApiItem, previousItem: ApiItem, packageName: string): ApiChange[] { + const changes: ApiChange[] = []; + + // Compare based on item type + switch (currentItem.kind) { + case ApiItemKind.Function: + changes.push(...this.compareFunctions(currentItem as ApiFunction, previousItem as ApiFunction, packageName)); + break; + + case ApiItemKind.Interface: + changes.push(...this.compareInterfaces(currentItem as ApiInterface, previousItem as ApiInterface, packageName)); + break; + + case ApiItemKind.Class: + changes.push(...this.compareClasses(currentItem as ApiClass, previousItem as ApiClass, packageName)); + break; + + case ApiItemKind.Enum: + changes.push(...this.compareEnums(currentItem as ApiEnum, previousItem as ApiEnum, packageName)); + break; + + default: + // Generic comparison for other types + changes.push(...this.compareGenericItems(currentItem, previousItem, packageName)); + } + + return changes; + } + + private compareFunctions(currentFunc: ApiFunction, previousFunc: ApiFunction, packageName: string): ApiChange[] { + const changes: ApiChange[] = []; + + // Get simplified signatures for comparison + const currentSig = this.getFunctionSignature(currentFunc); + const previousSig = this.getFunctionSignature(previousFunc); + + if (currentSig !== previousSig) { + const isBreaking = this.isFunctionChangeBreaking(currentFunc, previousFunc); + + changes.push({ + id: this.generateChangeId(packageName, 'function', currentFunc.displayName, currentSig), + type: isBreaking ? ChangeType.BREAKING : ChangeType.NON_BREAKING, + severity: isBreaking ? ChangeSeverity.MAJOR : ChangeSeverity.MINOR, + category: 'function', + description: `Function '${currentFunc.displayName}' signature changed`, + beforeSnippet: previousSig, + afterSnippet: currentSig, + location: this.getItemLocation(currentFunc), + }); + } + + return changes; + } + + private compareInterfaces( + currentInterface: ApiInterface, + previousInterface: ApiInterface, + packageName: string, + ): ApiChange[] { + const changes: ApiChange[] = []; + + // Compare interface members + const currentMembers = this.getInterfaceMembers(currentInterface); + const previousMembers = this.getInterfaceMembers(previousInterface); + + // Check for removed members (breaking) + for (const [name, previousMember] of previousMembers) { + if (!currentMembers.has(name)) { + changes.push({ + id: this.generateChangeId(packageName, 'interface', currentInterface.displayName, `removed-${name}`), + type: ChangeType.BREAKING, + severity: ChangeSeverity.MAJOR, + category: 'interface', + description: `Interface '${currentInterface.displayName}' removed member '${name}'`, + beforeSnippet: previousMember, + location: this.getItemLocation(currentInterface), + }); + } + } + + // Check for added members (potentially breaking if required) + for (const [name, currentMember] of currentMembers) { + if (!previousMembers.has(name)) { + const isRequired = this.isRequiredMember(currentMember); + changes.push({ + id: this.generateChangeId(packageName, 'interface', currentInterface.displayName, `added-${name}`), + type: isRequired ? ChangeType.BREAKING : ChangeType.ADDITION, + severity: isRequired ? ChangeSeverity.MAJOR : ChangeSeverity.MINOR, + category: 'interface', + description: `Interface '${currentInterface.displayName}' added ${isRequired ? 'required' : 'optional'} member '${name}'`, + afterSnippet: currentMember, + location: this.getItemLocation(currentInterface), + }); + } + } + + // Check for modified members + for (const [name, currentMember] of currentMembers) { + const previousMember = previousMembers.get(name); + if (previousMember && currentMember !== previousMember) { + const isBreaking = this.isMemberChangeBreaking(currentMember, previousMember); + changes.push({ + id: this.generateChangeId(packageName, 'interface', currentInterface.displayName, `modified-${name}`), + type: isBreaking ? ChangeType.BREAKING : ChangeType.NON_BREAKING, + severity: isBreaking ? ChangeSeverity.MAJOR : ChangeSeverity.MINOR, + category: 'interface', + description: `Interface '${currentInterface.displayName}' modified member '${name}'`, + beforeSnippet: previousMember, + afterSnippet: currentMember, + location: this.getItemLocation(currentInterface), + }); + } + } + + return changes; + } + + private compareClasses(currentClass: ApiClass, previousClass: ApiClass, packageName: string): ApiChange[] { + // Similar logic to interfaces but with additional considerations for: + // - Constructor changes + // - Method visibility changes + // - Inheritance changes + return this.compareGenericItems(currentClass, previousClass, packageName); + } + + private compareEnums(currentEnum: ApiEnum, previousEnum: ApiEnum, packageName: string): ApiChange[] { + const changes: ApiChange[] = []; + + const currentMembers = this.getEnumMembers(currentEnum); + const previousMembers = this.getEnumMembers(previousEnum); + + // Removed enum members are breaking + for (const previousMember of previousMembers) { + if (!currentMembers.includes(previousMember)) { + changes.push({ + id: this.generateChangeId(packageName, 'enum', currentEnum.displayName, `removed-${previousMember}`), + type: ChangeType.BREAKING, + severity: ChangeSeverity.MAJOR, + category: 'enum', + description: `Enum '${currentEnum.displayName}' removed member '${previousMember}'`, + beforeSnippet: previousMember, + location: this.getItemLocation(currentEnum), + }); + } + } + + // Added enum members are additions + for (const currentMember of currentMembers) { + if (!previousMembers.includes(currentMember)) { + changes.push({ + id: this.generateChangeId(packageName, 'enum', currentEnum.displayName, `added-${currentMember}`), + type: ChangeType.ADDITION, + severity: ChangeSeverity.MINOR, + category: 'enum', + description: `Enum '${currentEnum.displayName}' added member '${currentMember}'`, + afterSnippet: currentMember, + location: this.getItemLocation(currentEnum), + }); + } + } + + return changes; + } + + private compareGenericItems(currentItem: ApiItem, previousItem: ApiItem, packageName: string): ApiChange[] { + // Generic comparison based on excerpt text + const currentExcerpt = this.getItemExcerpt(currentItem); + const previousExcerpt = this.getItemExcerpt(previousItem); + + if (currentExcerpt !== previousExcerpt) { + return [ + { + id: this.generateChangeId(packageName, 'generic', currentItem.displayName, 'modified'), + type: ChangeType.BREAKING, // Conservative assumption + severity: ChangeSeverity.MAJOR, + category: this.getCategoryFromKind(currentItem.kind), + description: `${this.getCategoryFromKind(currentItem.kind)} '${currentItem.displayName}' was modified`, + beforeSnippet: previousExcerpt, + afterSnippet: currentExcerpt, + location: this.getItemLocation(currentItem), + }, + ]; + } + + return []; + } + + // Helper methods + private isPublicApi(item: ApiItem): boolean { + // Only consider items that are not marked as internal + return ( + !item.displayName.startsWith('_') && item.kind !== ApiItemKind.Package && item.kind !== ApiItemKind.EntryPoint + ); + } + + private getFunctionSignature(func: ApiFunction): string { + // Simplified signature extraction using the excerpt tokens + try { + if (func.excerpt && Array.isArray((func.excerpt as any).tokens)) { + return (func.excerpt as any).tokens.map((token: ExcerptToken) => token.text).join(''); + } + } catch { + // Fall back to display name + } + return func.displayName; + } + + private isFunctionChangeBreaking(current: ApiFunction, previous: ApiFunction): boolean { + // This is a simplified check - you'd want more sophisticated analysis + const currentParams = this.extractParameters(current); + const previousParams = this.extractParameters(previous); + + // More required parameters = breaking + // Fewer parameters = potentially breaking + // Changed parameter types = breaking + + return currentParams.required.length > previousParams.required.length || currentParams.total < previousParams.total; + } + + private extractParameters(_func: ApiFunction): { required: string[]; total: number } { + // This would need proper implementation based on API Extractor model + return { required: [], total: 0 }; + } + + private getInterfaceMembers(interfaceItem: ApiInterface): Map { + const members = new Map(); + + for (const member of interfaceItem.members) { + members.set(member.displayName, this.getItemExcerpt(member)); + } + + return members; + } + + private getEnumMembers(enumItem: ApiEnum): string[] { + return enumItem.members.map(member => member.displayName); + } + + private isRequiredMember(memberSignature: string): boolean { + // Check if member is optional (ends with ?) + return !memberSignature.includes('?:'); + } + + private isMemberChangeBreaking(current: string, previous: string): boolean { + // Conservative approach - most changes are considered breaking + return current !== previous; + } + + private getItemExcerpt(item: ApiItem): string { + // Use the excerpt tokens to build the text representation + try { + const itemWithExcerpt = item as any; + if (itemWithExcerpt.excerpt && Array.isArray(itemWithExcerpt.excerpt.tokens)) { + return itemWithExcerpt.excerpt.tokens.map((token: ExcerptToken) => token.text).join(''); + } + } catch { + // Fall back to display name + } + return item.displayName || ''; + } + + private getCategoryFromKind(kind: ApiItemKind): ApiChange['category'] { + switch (kind) { + case ApiItemKind.Function: + return 'function'; + case ApiItemKind.Interface: + return 'interface'; + case ApiItemKind.Class: + return 'class'; + case ApiItemKind.Enum: + return 'enum'; + case ApiItemKind.TypeAlias: + return 'type'; + default: + return 'export'; + } + } + + private getItemLocation(_item: ApiItem): { file: string; line?: number } | undefined { + // This would need proper implementation to extract source location + // API Extractor model doesn't directly provide source locations + return undefined; + } + + private createRemovalChange(item: ApiItem, packageName: string): ApiChange { + return { + id: this.generateChangeId(packageName, 'removal', item.displayName, 'removed'), + type: ChangeType.BREAKING, + severity: ChangeSeverity.MAJOR, + category: this.getCategoryFromKind(item.kind), + description: `Removed ${this.getCategoryFromKind(item.kind)} '${item.displayName}'`, + beforeSnippet: this.getItemExcerpt(item), + location: this.getItemLocation(item), + }; + } + + private createAdditionChange(item: ApiItem, packageName: string): ApiChange { + return { + id: this.generateChangeId(packageName, 'addition', item.displayName, 'added'), + type: ChangeType.ADDITION, + severity: ChangeSeverity.MINOR, + category: this.getCategoryFromKind(item.kind), + description: `Added ${this.getCategoryFromKind(item.kind)} '${item.displayName}'`, + afterSnippet: this.getItemExcerpt(item), + location: this.getItemLocation(item), + }; + } + + private generateChangeId(packageName: string, category: string, itemName: string, changeType: string): string { + const content = `${packageName}:${category}:${itemName}:${changeType}`; + return createHash('sha256').update(content).digest('hex').substring(0, 8); + } +} diff --git a/tools/snapi/src/analyzers/version-analyzer.ts b/tools/snapi/src/analyzers/version-analyzer.ts new file mode 100644 index 00000000000..67a834ab41c --- /dev/null +++ b/tools/snapi/src/analyzers/version-analyzer.ts @@ -0,0 +1,147 @@ +import semver from 'semver'; + +import type { ApiChange } from '../types.js'; +import { ChangeSeverity } from '../types.js'; + +export interface VersionBumpAnalysis { + recommendedBump: ChangeSeverity; + actualBump?: ChangeSeverity; + isValid: boolean; + reasons: string[]; +} + +/** + * Analyzes version bumps in relation to API changes + */ +export class VersionAnalyzer { + async analyzeVersionBump( + changes: ApiChange[], + currentVersion: string, + previousVersion: string, + ): Promise { + const recommendedBump = this.calculateRecommendedBump(changes); + const actualBump = this.calculateActualBump(currentVersion, previousVersion); + const isValid = this.isValidVersionBump(recommendedBump, actualBump); + const reasons = this.generateReasons(changes, recommendedBump, actualBump); + + return { + recommendedBump, + actualBump, + isValid, + reasons, + }; + } + + public calculateRecommendedBump(changes: ApiChange[]): ChangeSeverity { + // No changes = patch bump (or no bump) + if (changes.length === 0) { + return ChangeSeverity.PATCH; + } + + // Any breaking change requires major bump + const hasBreakingChanges = changes.some(c => c.type === 'breaking' && !c.isSuppressed); + if (hasBreakingChanges) { + return ChangeSeverity.MAJOR; + } + + // New features or non-breaking changes require minor bump + const hasNewFeatures = changes.some(c => (c.type === 'addition' || c.type === 'non-breaking') && !c.isSuppressed); + if (hasNewFeatures) { + return ChangeSeverity.MINOR; + } + + // Fallback to patch + return ChangeSeverity.PATCH; + } + + private calculateActualBump(currentVersion: string, previousVersion: string): ChangeSeverity | undefined { + if (!semver.valid(currentVersion) || !semver.valid(previousVersion)) { + return undefined; + } + + if (semver.eq(currentVersion, previousVersion)) { + return undefined; // No version bump + } + + const diff = semver.diff(previousVersion, currentVersion); + + switch (diff) { + case 'major': + return ChangeSeverity.MAJOR; + case 'minor': + return ChangeSeverity.MINOR; + case 'patch': + return ChangeSeverity.PATCH; + default: + return undefined; + } + } + + private isValidVersionBump(recommended: ChangeSeverity, actual?: ChangeSeverity): boolean { + if (!actual) { + // No version bump is only valid if recommended is patch and there are no changes + return recommended === ChangeSeverity.PATCH; + } + + // Convert to numeric for comparison + const severityOrder = { + [ChangeSeverity.PATCH]: 1, + [ChangeSeverity.MINOR]: 2, + [ChangeSeverity.MAJOR]: 3, + }; + + // Actual bump must be >= recommended bump + return severityOrder[actual] >= severityOrder[recommended]; + } + + private generateReasons(changes: ApiChange[], recommended: ChangeSeverity, actual?: ChangeSeverity): string[] { + const reasons: string[] = []; + + // Explain why the recommended bump is needed + const breakingChanges = changes.filter(c => c.type === 'breaking' && !c.isSuppressed); + const newFeatures = changes.filter(c => c.type === 'addition' && !c.isSuppressed); + const nonBreakingChanges = changes.filter(c => c.type === 'non-breaking' && !c.isSuppressed); + + if (breakingChanges.length > 0) { + reasons.push(`Major version bump required due to ${breakingChanges.length} breaking change(s)`); + } else if (newFeatures.length > 0) { + reasons.push(`Minor version bump required due to ${newFeatures.length} new feature(s)`); + } else if (nonBreakingChanges.length > 0) { + reasons.push(`Minor version bump recommended due to ${nonBreakingChanges.length} non-breaking change(s)`); + } + + // Explain version bump validation + if (!actual) { + if (recommended !== ChangeSeverity.PATCH) { + reasons.push('⚠️ No version bump detected, but changes require version update'); + } + } else if (!this.isValidVersionBump(recommended, actual)) { + reasons.push(`⚠️ Version bump (${actual}) is insufficient for the detected changes (requires ${recommended})`); + } else { + reasons.push(`✅ Version bump (${actual}) is appropriate for the detected changes`); + } + + return reasons; + } + + /** + * Check if a version is a prerelease + */ + isPrerelease(version: string): boolean { + return semver.prerelease(version) !== null; + } + + /** + * Get next version based on bump type + */ + getNextVersion(currentVersion: string, bump: ChangeSeverity): string { + switch (bump) { + case ChangeSeverity.MAJOR: + return semver.inc(currentVersion, 'major') || currentVersion; + case ChangeSeverity.MINOR: + return semver.inc(currentVersion, 'minor') || currentVersion; + case ChangeSeverity.PATCH: + return semver.inc(currentVersion, 'patch') || currentVersion; + } + } +} diff --git a/tools/snapi/src/cli.ts b/tools/snapi/src/cli.ts new file mode 100644 index 00000000000..4741058a38c --- /dev/null +++ b/tools/snapi/src/cli.ts @@ -0,0 +1,584 @@ +#!/usr/bin/env node + +import chalk from 'chalk'; +import { Command } from 'commander'; +import fs from 'fs-extra'; +import * as path from 'path'; + +import { BreakingChangesDetector } from './core/detector.js'; +import { type Config, ConfigSchema } from './types.js'; + +const program = new Command(); + +// Helper to find the workspace root +function findWorkspaceRoot(startPath: string = process.cwd()): string { + let currentPath = startPath; + + while (currentPath !== path.dirname(currentPath)) { + // Check for pnpm-workspace.yaml or package.json with workspaces + const pnpmWorkspace = path.join(currentPath, 'pnpm-workspace.yaml'); + const packageJson = path.join(currentPath, 'package.json'); + + if (fs.existsSync(pnpmWorkspace)) { + return currentPath; + } + + if (fs.existsSync(packageJson)) { + try { + const pkg = fs.readJsonSync(packageJson); + if (pkg.workspaces) { + return currentPath; + } + } catch { + // Continue searching + } + } + + currentPath = path.dirname(currentPath); + } + + // Fallback to current directory + return process.cwd(); +} + +program + .name('snapi') + .description('Snapshot API (SNAPI) - Detect breaking changes in TypeScript package APIs') + .version('1.0.0'); + +program + .command('detect') + .description('Detect API breaking changes') + .option('-c, --config ', 'Path to configuration file', 'snapi.config.json') + .option('-o, --output ', 'Output path for report') + .option('--format ', 'Output format (markdown|json)', 'markdown') + .option('--workspace ', 'Workspace root path', findWorkspaceRoot()) + .option('--main-branch ', 'Main branch name', 'main') + .option('--no-version-check', 'Skip version bump validation') + .option('--fail-on-breaking', 'Exit with error code if breaking changes detected') + .action( + async (options: { + config: string; + output?: string; + format: 'markdown' | 'json'; + workspace: string; + mainBranch: string; + noVersionCheck?: boolean; + failOnBreaking?: boolean; + }) => { + try { + const config = await loadConfig(options.config, options); + const detector = new BreakingChangesDetector({ + workspaceRoot: options.workspace, + config, + }); + + console.log(chalk.blue('🔍 Starting API breaking changes detection...')); + + const result = await detector.detectBreakingChanges(); + const report = await detector.generateReport(result, options.format); + + // Output report + if (options.output) { + await fs.writeFile(options.output, report); + console.log(chalk.green(`📄 Report saved to ${options.output}`)); + } else { + console.log('\n' + report); + } + + // Exit codes + if (options.failOnBreaking && result.hasBreakingChanges) { + console.error(chalk.red('💥 Breaking changes detected!')); + process.exit(1); + } + + if (result.ciShouldFail) { + console.error(chalk.red('❌ CI validation failed - version bumps required')); + process.exit(1); + } + + console.log(chalk.green('✅ API breaking changes detection completed successfully')); + } catch (error) { + console.error(chalk.red('❌ Error:'), error instanceof Error ? error.message : error); + process.exit(1); + } + }, + ); + +program + .command('snapshot') + .description('Generate API snapshots without comparison') + .option('-c, --config ', 'Path to configuration file', 'snapi.config.json') + .option('-o, --output ', 'Output directory for snapshots') + .option('--workspace ', 'Workspace root path', findWorkspaceRoot()) + .option('--no-cleanup', 'Skip cleanup of temporary files (useful for caching)') + .action(async (options: { config: string; output?: string; workspace: string; cleanup?: boolean }) => { + try { + const config = await loadConfig(options.config, {}); + + // Override snapshots directory if provided + if (options.output) { + config.snapshotsDir = options.output; + } + + const detector = new BreakingChangesDetector({ + workspaceRoot: options.workspace, + config, + skipCleanup: !options.cleanup, + }); + + console.log(chalk.blue('📸 Starting API snapshot generation...')); + + // Discover packages + const packages = await detector.discoverPackages(); + console.log(chalk.blue(`📦 Found ${packages.length} packages to snapshot`)); + + // Generate snapshots only + const snapshots = await detector.generateCurrentSnapshots(packages); + + console.log(`DEBUG: options.cleanup = ${options.cleanup}`); + console.log(`DEBUG: !options.cleanup = ${!options.cleanup}`); + + // Clean up temporary files unless --no-cleanup is specified + if (options.cleanup) { + console.log('DEBUG: Calling cleanup because options.cleanup is true'); + await detector.cleanup(); + } else { + console.log('DEBUG: Skipping cleanup because options.cleanup is false (--no-cleanup was used)'); + } + + console.log(chalk.green(`✅ Generated ${snapshots.size} API snapshots`)); + + // List what was created + for (const [packageName, snapshotPath] of snapshots) { + console.log(chalk.gray(` 📄 ${packageName}: ${snapshotPath}`)); + } + } catch (error) { + console.error(chalk.red('❌ Error:'), error instanceof Error ? error.message : error); + process.exit(1); + } + }); + +program + .command('init') + .description('Initialize configuration file') + .option('-o, --output ', 'Output path for config file', 'snapi.config.json') + .action(async (options: { output: string }) => { + try { + const defaultConfig: Config = { + excludePackages: [], + snapshotsDir: '.api-snapshots', + mainBranch: 'main', + checkVersionBump: true, + suppressedChanges: [], + enableLLMAnalysis: false, + llmProvider: 'openai', + }; + + await fs.writeJson(options.output, defaultConfig, { spaces: 2 }); + console.log(chalk.green(`✅ Configuration file created at ${options.output}`)); + } catch (error) { + console.error(chalk.red('❌ Error:'), error instanceof Error ? error.message : error); + process.exit(1); + } + }); + +program + .command('suppress') + .description('Add a suppression for a specific change') + .requiredOption('-p, --package ', 'Package name') + .requiredOption('-i, --change-id ', 'Change ID to suppress') + .requiredOption('-r, --reason ', 'Reason for suppression') + .option('-d, --days ', 'Days until suppression expires', '0') + .option('-c, --config ', 'Path to configuration file', 'snapi.config.json') + .action(async (options: { package: string; changeId: string; reason: string; days: string; config: string }) => { + try { + const configPath = path.resolve(options.config); + let config: Config; + + if (await fs.pathExists(configPath)) { + config = await fs.readJson(configPath); + } else { + console.log(chalk.yellow('⚠️ Config file not found, creating new one')); + config = { + excludePackages: [], + snapshotsDir: '.api-snapshots', + mainBranch: 'main', + checkVersionBump: true, + suppressedChanges: [], + enableLLMAnalysis: false, + llmProvider: 'openai', + }; + } + + // Add suppression + const expires = + parseInt(options.days) > 0 + ? new Date(Date.now() + parseInt(options.days) * 24 * 60 * 60 * 1000).toISOString() + : undefined; + + config.suppressedChanges.push({ + package: options.package, + changeId: options.changeId, + reason: options.reason, + expires, + }); + + await fs.writeJson(configPath, config, { spaces: 2 }); + console.log(chalk.green(`✅ Suppression added for ${options.package}:${options.changeId}`)); + } catch (error) { + console.error(chalk.red('❌ Error:'), error instanceof Error ? error.message : error); + process.exit(1); + } + }); + +program + .command('cleanup') + .description('Clean up expired suppressions and temporary files') + .option('-c, --config ', 'Path to configuration file', 'snapi.config.json') + .option('--workspace ', 'Workspace root path', findWorkspaceRoot()) + .action(async (options: { config: string; workspace: string }) => { + try { + const configPath = path.resolve(options.config); + + if (await fs.pathExists(configPath)) { + const config: Config = await fs.readJson(configPath); + + // Remove expired suppressions + const now = new Date(); + const validSuppressions = config.suppressedChanges.filter((suppression: { expires?: string }) => { + if (!suppression.expires) return true; + return new Date(suppression.expires) > now; + }); + + const removedCount = config.suppressedChanges.length - validSuppressions.length; + + if (removedCount > 0) { + config.suppressedChanges = validSuppressions; + await fs.writeJson(configPath, config, { spaces: 2 }); + console.log(chalk.green(`✅ Removed ${removedCount} expired suppression(s)`)); + } else { + console.log(chalk.blue('ℹ️ No expired suppressions found')); + } + } + + // Clean up snapshots + const snapshotsDir = path.join(options.workspace, '.api-snapshots'); + if (await fs.pathExists(snapshotsDir)) { + const currentDir = path.join(snapshotsDir, 'current'); + if (await fs.pathExists(currentDir)) { + await fs.remove(currentDir); + console.log(chalk.green('✅ Cleaned up temporary snapshots')); + } + } + } catch (error) { + console.error(chalk.red('❌ Error:'), error instanceof Error ? error.message : error); + process.exit(1); + } + }); + +// Add storage commands +program + .command('storage') + .description('Storage management commands') + .addCommand( + new Command('health') + .description('Check storage backend health') + .option('-c, --config ', 'Configuration file path', 'snapi.config.json') + .option('-v, --verbose', 'Show detailed health information') + .option('--ci', 'Run in CI mode with minimal output') + .action(async options => { + try { + const configPath = path.resolve(process.cwd(), options.config); + const config = await loadConfig(configPath); + + if (!config.storage) { + console.log('❌ No storage configuration found'); + process.exit(1); + } + + const { StorageManager } = await import('./storage/storage-manager.js'); + const storageManager = new StorageManager(config.storage); + + if (!options.ci) { + console.log('🔍 Checking storage health...\n'); + } + + // Get basic health status + const [healthStatus, stats] = await Promise.all([ + storageManager.getHealthStatus(), + storageManager.getStats(), + ]); + + // Check if any backend is unhealthy + const unhealthyBackends = healthStatus.filter(h => !h.healthy); + if (unhealthyBackends.length > 0) { + if (options.ci) { + console.log('FAILED: Unhealthy storage backends detected'); + for (const health of unhealthyBackends) { + console.log(`ERROR: ${health.backend} - ${health.error || 'Unknown error'}`); + } + } else { + for (const health of healthStatus) { + const status = health.healthy ? '✅' : '❌'; + const latency = health.latency ? ` (${health.latency}ms)` : ''; + console.log(`${status} ${health.backend}${latency}`); + if (health.error) { + console.log(` Error: ${health.error}`); + } + if (health.lastCheck) { + console.log(` Last check: ${health.lastCheck.toISOString()}`); + } + } + } + process.exit(1); + } + + // Perform read-write-delete test + if (!options.ci) { + console.log('\n🧪 Performing read-write-delete test...'); + } + const testKey = `health-test_test.api.json`; + const testData = { timestamp: new Date().toISOString(), test: true }; + const testMetadata = { + commitHash: 'test', + timestamp: new Date().toISOString(), + branch: 'test', + packageName: 'health-test', + version: '0.0.0', + fileSize: 0, + checksum: 'test', + }; + + try { + // Write test + if (!options.ci) { + console.log(' Writing test file...'); + } + const tempPath = path.join(process.cwd(), '.tmp', 'health-test', testKey); + await fs.ensureDir(path.dirname(tempPath)); + await fs.writeJson(tempPath, testData); + await storageManager.store('health-test', tempPath, testMetadata); + if (!options.ci) { + console.log(' ✅ Write successful'); + } + + // Read test + if (!options.ci) { + console.log(' Reading test file...'); + } + const readData = await storageManager.retrieve('health-test', 'test'); + if (!readData) { + throw new Error('Read failed - no data returned'); + } + const parsedData = await fs.readJson(readData); + if (parsedData.test !== testData.test) { + throw new Error('Read data mismatch'); + } + if (!options.ci) { + console.log(' ✅ Read successful'); + } + + // Delete test + if (!options.ci) { + console.log(' Deleting test file...'); + } + await storageManager.delete(testKey); + if (!options.ci) { + console.log(' ✅ Delete successful'); + } + + // Clean up temp files + await fs.remove(path.dirname(tempPath)); + + if (!options.ci) { + console.log('\n✅ All storage operations successful'); + } + } catch (error) { + if (options.ci) { + console.log(`FAILED: Storage operation failed - ${error instanceof Error ? error.message : error}`); + } else { + console.log(`\n❌ Storage operation failed: ${error instanceof Error ? error.message : error}`); + } + process.exit(1); + } + + // Print storage stats + if (!options.ci) { + console.log('\n📊 Storage Statistics:'); + console.log(`Total Size: ${formatBytes(stats.totalSize)}`); + console.log(`Snapshot Count: ${stats.snapshotCount}`); + console.log(`Oldest Snapshot: ${stats.oldestSnapshot || 'N/A'}`); + console.log(`Newest Snapshot: ${stats.newestSnapshot || 'N/A'}`); + + // Print detailed backend information if verbose + if (options.verbose) { + console.log('\n🔧 Backend Details:'); + for (const backend of stats.backendStats) { + const status = backend.healthy ? '✅' : '❌'; + console.log(`\n${status} ${backend.backend}`); + + if (backend.stats) { + console.log(` Size: ${formatBytes(backend.stats.totalSize)}`); + console.log(` Count: ${backend.stats.snapshotCount}`); + console.log(` Oldest: ${backend.stats.oldestSnapshot || 'N/A'}`); + console.log(` Newest: ${backend.stats.newestSnapshot || 'N/A'}`); + } + + if (!backend.healthy) { + console.log(' Status: Unhealthy'); + } + } + } + } else { + console.log('PASSED: Storage health check completed successfully'); + } + + await storageManager.shutdown(); + } catch (error) { + if (options.ci) { + console.log(`FAILED: ${error instanceof Error ? error.message : error}`); + } else { + console.error('❌ Storage health check failed:', error); + } + process.exit(1); + } + }), + ) + .addCommand( + new Command('stats') + .description('Get storage statistics') + .option('-c, --config ', 'Configuration file path', 'snapi.config.json') + .action(async options => { + try { + const configPath = path.resolve(process.cwd(), options.config); + const config = await loadConfig(configPath); + + if (!config.storage) { + console.log('❌ No storage configuration found'); + process.exit(1); + } + + const { StorageManager } = await import('./storage/storage-manager.js'); + const storageManager = new StorageManager(config.storage); + + console.log('📊 Getting storage statistics...\n'); + + const stats = await storageManager.getStats(); + + console.log(`Total Size: ${formatBytes(stats.totalSize)}`); + console.log(`Snapshot Count: ${stats.snapshotCount}`); + console.log(`Oldest Snapshot: ${stats.oldestSnapshot || 'N/A'}`); + console.log(`Newest Snapshot: ${stats.newestSnapshot || 'N/A'}\n`); + + console.log('Backend Details:'); + for (const backend of stats.backendStats) { + const status = backend.healthy ? '✅' : '❌'; + console.log(`${status} ${backend.backend}`); + + if (backend.stats) { + console.log(` Size: ${formatBytes(backend.stats.totalSize)}`); + console.log(` Count: ${backend.stats.snapshotCount}`); + } + } + + await storageManager.shutdown(); + } catch (error) { + console.error('❌ Failed to get storage stats:', error); + process.exit(1); + } + }), + ) + .addCommand( + new Command('cleanup') + .description('Clean up old snapshots') + .option('-c, --config ', 'Configuration file path', 'snapi.config.json') + .option('-d, --days ', 'Retention period in days', '30') + .action(async options => { + try { + const configPath = path.resolve(process.cwd(), options.config); + const config = await loadConfig(configPath); + + if (!config.storage) { + console.log('❌ No storage configuration found'); + process.exit(1); + } + + const { StorageManager } = await import('./storage/storage-manager.js'); + const storageManager = new StorageManager(config.storage); + + const days = parseInt(options.days); + console.log(`🧹 Cleaning up snapshots older than ${days} days...`); + + await storageManager.cleanup(days); + + console.log('✅ Cleanup completed'); + await storageManager.shutdown(); + } catch (error) { + console.error('❌ Cleanup failed:', error); + process.exit(1); + } + }), + ); + +async function loadConfig( + configPath: string, + cliOptions?: { + mainBranch?: string; + noVersionCheck?: boolean; + }, +): Promise { + let config: Partial = {}; + + // Load from file if it exists + const resolvedConfigPath = path.resolve(configPath); + if (await fs.pathExists(resolvedConfigPath)) { + const fileConfig = await fs.readJson(resolvedConfigPath); + config = { ...fileConfig }; + } + + // Override with CLI options if provided + if (cliOptions?.mainBranch) { + config.mainBranch = cliOptions.mainBranch; + } + if (cliOptions?.noVersionCheck) { + config.checkVersionBump = false; + } + + // Parse environment variables + if (process.env.GITHUB_TOKEN) { + config.githubToken = process.env.GITHUB_TOKEN; + } + if (process.env.OPENAI_API_KEY) { + config.llmApiKey = process.env.OPENAI_API_KEY; + config.enableLLMAnalysis = true; + } + if (process.env.ANTHROPIC_API_KEY) { + config.llmApiKey = process.env.ANTHROPIC_API_KEY; + config.llmProvider = 'anthropic'; + config.enableLLMAnalysis = true; + } + + // Validate configuration + try { + return ConfigSchema.parse(config); + } catch (error) { + throw new Error(`Invalid configuration: ${error instanceof Error ? error.message : error}`); + } +} + +function formatBytes(bytes: number): string { + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + let size = bytes; + let unitIndex = 0; + + while (size >= 1024 && unitIndex < units.length - 1) { + size /= 1024; + unitIndex++; + } + + return `${size.toFixed(1)} ${units[unitIndex]}`; +} + +// Run the CLI +program.parse(); diff --git a/tools/snapi/src/core/detector.ts b/tools/snapi/src/core/detector.ts new file mode 100644 index 00000000000..6e4aedffe23 --- /dev/null +++ b/tools/snapi/src/core/detector.ts @@ -0,0 +1,383 @@ +import fs from 'fs-extra'; +import path from 'path'; + +import { ApiDiffAnalyzer } from '../analyzers/api-diff.js'; +import { VersionAnalyzer, type VersionBumpAnalysis } from '../analyzers/version-analyzer.js'; +import { ReportGenerator } from '../reporters/markdown-reporter.js'; +import type { BaselineSnapshot, SnapshotMetadata } from '../storage/base-storage.js'; +import { StorageManager } from '../storage/storage-manager.js'; +import { + type AnalysisResult, + type ApiChange, + type ApiSnapshot, + ChangeSeverity, + type Config, + type PackageAnalysis, + type PackageInfo, +} from '../types.js'; +import { ApiExtractorRunner } from '../utils/api-extractor.js'; +import { PackageDiscovery } from '../utils/package-discovery.js'; +import { SuppressionManager } from '../utils/suppression-manager.js'; + +export interface BreakingChangesDetectorOptions { + workspaceRoot: string; + config: Config; + skipCleanup?: boolean; +} + +/** + * Main orchestrator for API breaking changes detection + */ +export class BreakingChangesDetector { + private packageDiscovery: PackageDiscovery; + private extractorRunner: ApiExtractorRunner; + private diffAnalyzer: ApiDiffAnalyzer; + private versionAnalyzer: VersionAnalyzer; + private suppressionManager: SuppressionManager; + private storageManager?: StorageManager; + private options: BreakingChangesDetectorOptions; + private reportGenerator: ReportGenerator; + + constructor(options: BreakingChangesDetectorOptions) { + this.options = options; + this.packageDiscovery = new PackageDiscovery(options.workspaceRoot); + this.extractorRunner = new ApiExtractorRunner(options.workspaceRoot); + this.diffAnalyzer = new ApiDiffAnalyzer(); + this.versionAnalyzer = new VersionAnalyzer(); + this.suppressionManager = new SuppressionManager(options.config.suppressedChanges || []); + this.reportGenerator = new ReportGenerator(); + + if (options.config.storage) { + this.storageManager = new StorageManager(options.config.storage); + } + } + + async detectBreakingChanges(): Promise { + console.log('[SNAPI] 🔍 Starting API breaking changes detection...'); + + try { + const packages = await this.discoverPackages(); + console.log(`[SNAPI] 📦 Found ${packages.length} packages to analyze`); + + console.log('[SNAPI] 📸 Generating current API snapshots...'); + const currentSnapshots = await this.generateCurrentSnapshots(packages); + + console.log('[SNAPI] 🌿 Fetching baseline snapshots...'); + const baselineSnapshots = await this.getBaselineSnapshots(packages); + + console.log('[SNAPI] 🔄 Analyzing API changes...'); + const packageAnalyses = await this.analyzePackageChanges(packages, currentSnapshots, baselineSnapshots); + + const suppressedAnalyses = this.applySuppressions(packageAnalyses); + const result = this.buildAnalysisResult(suppressedAnalyses); + + console.log('[SNAPI] ✅ API breaking changes detection completed'); + return result; + } catch (error) { + console.error('[SNAPI] ❌ API breaking changes detection failed:', error); + throw error; + } finally { + await this.cleanup(); + } + } + + async discoverPackages(): Promise { + return this.packageDiscovery.discoverPackages(this.options.config.packages, this.options.config.excludePackages); + } + + async generateCurrentSnapshots(packages: PackageInfo[]): Promise> { + const snapshotsDir = path.join( + this.options.workspaceRoot, + this.options.config.snapshotsDir || '.api-snapshots', + 'current', + ); + await fs.ensureDir(snapshotsDir); + const snapshots = new Map(); + + for (const pkg of packages) { + try { + const snapshotResult: ApiSnapshot = await this.extractorRunner.generateApiSnapshot({ + packageInfo: pkg, + outputDir: snapshotsDir, + }); + snapshots.set(pkg.name, snapshotResult.apiJsonPath); + console.log(`[SNAPI] Generated current snapshot for ${pkg.name} at ${snapshotResult.apiJsonPath}`); + } catch (error) { + console.warn( + `[SNAPI] Failed to generate snapshot for ${pkg.name}: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } + return snapshots; + } + + async getBaselineSnapshots(packages: PackageInfo[]): Promise> { + const baselineSnapshots = new Map(); + console.log('[SNAPI] Loading baseline snapshots...'); + + for (const pkg of packages) { + let baseline: BaselineSnapshot | null = null; + + if (this.storageManager) { + try { + console.log(`[SNAPI] Attempting to load baseline for ${pkg.name} from StorageManager...`); + baseline = await this.storageManager.getBaseline(pkg.name, this.options.config.mainBranch); + if (baseline) { + console.log(`[SNAPI] Successfully loaded baseline for ${pkg.name} from StorageManager.`); + } else { + console.log(`[SNAPI] StorageManager returned no baseline for ${pkg.name}.`); + } + } catch (error) { + console.warn( + `[SNAPI] Failed to get baseline for ${pkg.name} from StorageManager: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } + + if (!baseline) { + console.log( + `[SNAPI] Attempting to load baseline for ${pkg.name} directly from filesystem: ${path.join( + this.options.config.snapshotsDir || '.api-snapshots', + 'baseline', + )}`, + ); + const safePackageName = pkg.name.startsWith('@') + ? `@${pkg.name.substring(1).replace(/\//g, '__')}` + : pkg.name.replace(/\//g, '__'); + const baselinePath = path.join( + this.options.workspaceRoot, + this.options.config.snapshotsDir || '.api-snapshots', + 'baseline', + `${safePackageName}.api.json`, + ); + + if (await fs.pathExists(baselinePath)) { + try { + const metadataPath = baselinePath.replace('.api.json', '.metadata.json'); + let metadata: SnapshotMetadata; + + if (await fs.pathExists(metadataPath)) { + metadata = await fs.readJson(metadataPath); + } else { + console.warn(`[SNAPI] No .metadata.json found for ${baselinePath}, creating placeholder metadata.`); + const fileStats = await fs.stat(baselinePath); + metadata = { + packageName: pkg.name, + commitHash: 'unknown-artifact-baseline', + timestamp: fileStats.mtime.toISOString(), + branch: this.options.config.mainBranch || 'main', + version: pkg.version, + fileSize: fileStats.size, + checksum: '', + }; + } + + baseline = { + packageName: pkg.name, + filePath: baselinePath, + metadata, + }; + console.log(`[SNAPI] Successfully loaded baseline for ${pkg.name} from filesystem: ${baselinePath}`); + } catch (error) { + console.warn( + `[SNAPI] Error loading baseline or metadata for ${pkg.name} from filesystem path ${baselinePath}: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } else { + console.warn(`[SNAPI] No baseline snapshot found for ${pkg.name} at filesystem path: ${baselinePath}`); + } + } + + if (baseline) { + baselineSnapshots.set(pkg.name, baseline); + console.log(`[SNAPI] Baseline for ${pkg.name} (commit: ${baseline.metadata.commitHash}) ready.`); + } else { + console.warn( + `[SNAPI] No baseline found for ${pkg.name} using any method. It will be treated as a new package or all its APIs as new.`, + ); + } + } + + if (baselineSnapshots.size === 0 && packages.length > 0) { + console.warn( + '[SNAPI] No baseline snapshots could be loaded for any package. All packages will be treated as new, or all APIs as new.', + ); + } + return baselineSnapshots; + } + + private async analyzePackageChanges( + packages: PackageInfo[], + currentSnapshots: Map, + baselineSnapshots: Map, + ): Promise { + const analyses: PackageAnalysis[] = []; + for (const pkg of packages) { + const currentSnapshotPath = currentSnapshots.get(pkg.name); + const baseline = baselineSnapshots.get(pkg.name); + + if (!currentSnapshotPath) { + console.warn(`[SNAPI] Current snapshot missing for package: ${pkg.name}. Skipping analysis.`); + continue; + } + + let changes: ApiChange[] = []; + let versionAnalysisResult: VersionBumpAnalysis; + + if (baseline) { + changes = await this.diffAnalyzer.compareApiModels(currentSnapshotPath, baseline.filePath, pkg.name); + if (this.options.config.checkVersionBump && baseline.metadata.version) { + versionAnalysisResult = await this.versionAnalyzer.analyzeVersionBump( + changes, + pkg.version, + baseline.metadata.version, + ); + } else { + versionAnalysisResult = { + recommendedBump: this.versionAnalyzer.calculateRecommendedBump(changes), + actualBump: undefined, + isValid: true, + reasons: ['Version bump check skipped or baseline version unavailable'], + }; + } + } else { + console.warn( + `[SNAPI] No baseline for ${pkg.name}, treating as new. Change detection for new packages may be incomplete.`, + ); + changes = []; + versionAnalysisResult = { + recommendedBump: ChangeSeverity.MINOR, + actualBump: undefined, + isValid: true, + reasons: ['New package, defaulting to minor bump recommendation'], + }; + } + + const hasBreakingChanges = changes.some(c => c.severity === ChangeSeverity.MAJOR && !c.isSuppressed); + + analyses.push({ + packageName: pkg.name, + version: { + current: pkg.version, + previous: baseline?.metadata.version || 'N/A', + }, + changes, + hasBreakingChanges, + recommendedVersionBump: versionAnalysisResult.recommendedBump, + actualVersionBump: versionAnalysisResult.actualBump, + isValidBump: versionAnalysisResult.isValid, + }); + } + return analyses; + } + + applySuppressions(packageAnalyses: PackageAnalysis[]): PackageAnalysis[] { + return packageAnalyses.map(analysis => { + const newChanges = analysis.changes.map(change => { + const suppression = this.suppressionManager.getSuppression(analysis.packageName, change.id); + if (suppression) { + return { ...change, isSuppressed: true, suppressionReason: suppression.reason }; + } + return change; + }); + return { + ...analysis, + changes: newChanges, + hasBreakingChanges: newChanges.some(c => c.severity === ChangeSeverity.MAJOR && !c.isSuppressed), + }; + }); + } + + buildAnalysisResult(packageAnalyses: PackageAnalysis[]): AnalysisResult { + let totalBreakingChanges = 0; + let totalNonBreakingChanges = 0; + let totalAdditions = 0; + let packagesWithChanges = 0; + + for (const analysis of packageAnalyses) { + if (analysis.changes.length > 0) packagesWithChanges++; + for (const change of analysis.changes) { + if (change.isSuppressed) continue; + if (change.severity === ChangeSeverity.MAJOR) totalBreakingChanges++; + else if (change.type === 'addition') totalAdditions++; + else totalNonBreakingChanges++; + } + } + + const overallHasBreakingChanges = packageAnalyses.some(p => p.hasBreakingChanges); + const ciShouldFail = + (this.options.config.ci?.failOnBreakingChanges && overallHasBreakingChanges) || + packageAnalyses.some(p => this.options.config.checkVersionBump && p.isValidBump === false); + + return { + packages: packageAnalyses, + hasBreakingChanges: overallHasBreakingChanges, + ciShouldFail, + summary: { + totalPackages: packageAnalyses.length, + packagesWithChanges, + breakingChanges: totalBreakingChanges, + nonBreakingChanges: totalNonBreakingChanges, + additions: totalAdditions, + }, + }; + } + + async generateReport(result: AnalysisResult, format: 'markdown' | 'json' = 'markdown'): Promise { + if (format === 'json') { + return JSON.stringify(result, null, 2); + } + return this.reportGenerator.generateMarkdownReport(result); + } + + async cleanup(): Promise { + if (this.options.skipCleanup) { + console.log('[SNAPI] Skipping cleanup of temporary files.'); + return; + } + const currentSnapshotsDir = path.join( + this.options.workspaceRoot, + this.options.config.snapshotsDir || '.api-snapshots', + 'current', + ); + + try { + console.log(`[SNAPI] Attempting to remove current snapshots directory: ${currentSnapshotsDir}`); + await fs.remove(currentSnapshotsDir); + if (typeof this.extractorRunner.cleanup === 'function') { + await this.extractorRunner.cleanup(); + } + console.log('[SNAPI] Temporary files and current snapshots cleaned up.'); + } catch (error) { + console.warn(`[SNAPI] Error during cleanup: ${error instanceof Error ? error.message : String(error)}`); + } + } + + /** + * Get storage health and statistics + */ + async getStorageInfo(): Promise<{ + health: any[]; + stats: any; + }> { + if (!this.storageManager) { + return { + health: [], + stats: { totalSize: 0, snapshotCount: 0, oldestSnapshot: '', newestSnapshot: '' }, + }; + } + + const [health, stats] = await Promise.all([this.storageManager.getHealthStatus(), this.storageManager.getStats()]); + + return { health, stats }; + } + + /** + * Shutdown and cleanup resources + */ + async shutdown(): Promise { + if (this.storageManager) { + await this.storageManager.shutdown(); + } + } +} diff --git a/tools/snapi/src/env.d.ts b/tools/snapi/src/env.d.ts new file mode 100644 index 00000000000..5ba3f7333e4 --- /dev/null +++ b/tools/snapi/src/env.d.ts @@ -0,0 +1,13 @@ +declare global { + namespace NodeJS { + interface ProcessEnv { + GOOGLE_SERVICE_ACCOUNT_KEY?: string; + GOOGLE_APPLICATION_CREDENTIALS?: string; + GCS_PROJECT_ID?: string; + GCS_BUCKET?: string; + OPENAI_API_KEY?: string; + ANTHROPIC_API_KEY?: string; + GITHUB_TOKEN?: string; + } + } +} diff --git a/tools/snapi/src/index.ts b/tools/snapi/src/index.ts new file mode 100644 index 00000000000..30290942269 --- /dev/null +++ b/tools/snapi/src/index.ts @@ -0,0 +1,21 @@ +export { BreakingChangesDetector } from './core/detector.js'; +export { PackageDiscovery } from './utils/package-discovery.js'; +export { ApiExtractorRunner } from './utils/api-extractor.js'; +export { ApiDiffAnalyzer } from './analyzers/api-diff.js'; +export { VersionAnalyzer } from './analyzers/version-analyzer.js'; +export { ReportGenerator } from './reporters/markdown-reporter.js'; +export { SuppressionManager } from './utils/suppression-manager.js'; +export { GitManager } from './utils/git-manager.js'; + +export type { + Config, + PackageInfo, + ApiChange, + ChangeType, + ChangeSeverity, + PackageAnalysis, + AnalysisResult, + ApiSnapshot, +} from './types.js'; + +export { ConfigSchema } from './types.js'; diff --git a/tools/snapi/src/reporters/markdown-reporter.ts b/tools/snapi/src/reporters/markdown-reporter.ts new file mode 100644 index 00000000000..0b0f160bb61 --- /dev/null +++ b/tools/snapi/src/reporters/markdown-reporter.ts @@ -0,0 +1,289 @@ +import type { AnalysisResult, ApiChange, PackageAnalysis } from '../types.js'; + +/** + * Generates human-readable Markdown reports for GitHub PR comments + */ +export class ReportGenerator { + generateMarkdownReport(result: AnalysisResult): string { + const sections: string[] = []; + + // Header + sections.push(this.generateHeader(result)); + + // Summary + sections.push(this.generateSummary(result)); + + // Detailed changes per package + if (result.summary.packagesWithChanges > 0) { + sections.push(this.generatePackageDetails(result)); + } + + // Suppressions info + const suppressedChanges = this.getSuppressedChanges(result); + if (suppressedChanges.length > 0) { + sections.push(this.generateSuppressionsSection(suppressedChanges)); + } + + // Footer with actions + sections.push(this.generateFooter(result)); + + return sections.join('\n\n---\n\n'); + } + + private generateHeader(result: AnalysisResult): string { + const icon = result.hasBreakingChanges ? '💥' : result.summary.packagesWithChanges > 0 ? '📝' : '✅'; + const title = result.hasBreakingChanges + ? 'Breaking Changes Detected' + : result.summary.packagesWithChanges > 0 + ? 'API Changes Detected' + : 'No API Changes'; + + return `# ${icon} ${title}\n\n${this.generateStatusBadges(result)}`; + } + + private generateStatusBadges(result: AnalysisResult): string { + const badges: string[] = []; + + // CI Status badge + if (result.ciShouldFail) { + badges.push('![CI Status](https://img.shields.io/badge/CI-FAIL-red)'); + } else { + badges.push('![CI Status](https://img.shields.io/badge/CI-PASS-green)'); + } + + // Breaking changes badge + if (result.hasBreakingChanges) { + badges.push( + `![Breaking Changes](https://img.shields.io/badge/Breaking%20Changes-${result.summary.breakingChanges}-red)`, + ); + } + + // Packages affected badge + if (result.summary.packagesWithChanges > 0) { + badges.push( + `![Packages Affected](https://img.shields.io/badge/Packages%20Affected-${result.summary.packagesWithChanges}-blue)`, + ); + } + + return badges.join(' '); + } + + private generateSummary(result: AnalysisResult): string { + let summary = '## Summary\n\n'; + + if (result.summary.packagesWithChanges === 0) { + summary += '✅ No API changes detected in any packages.\n\n'; + summary += 'All public APIs remain stable and backward compatible.'; + return summary; + } + + summary += `📊 **${result.summary.packagesWithChanges}** out of **${result.summary.totalPackages}** packages have API changes:\n\n`; + + if (result.summary.breakingChanges > 0) { + summary += `- 💥 **${result.summary.breakingChanges}** breaking changes\n`; + } + if (result.summary.nonBreakingChanges > 0) { + summary += `- 🔄 **${result.summary.nonBreakingChanges}** non-breaking changes\n`; + } + if (result.summary.additions > 0) { + summary += `- ✨ **${result.summary.additions}** new additions\n`; + } + + // Version bump recommendations + summary += '\n### Version Bump Recommendations\n\n'; + + const packagesNeedingVersionBumps = result.packages.filter( + p => !p.isValidBump && p.changes.some(c => !c.isSuppressed), + ); + + if (packagesNeedingVersionBumps.length > 0) { + summary += '⚠️ The following packages need version bumps:\n\n'; + for (const pkg of packagesNeedingVersionBumps) { + summary += `- **${pkg.packageName}**: ${pkg.version.current} → recommended **${pkg.recommendedVersionBump}** bump\n`; + } + } else { + summary += '✅ All packages have appropriate version bumps.'; + } + + return summary; + } + + private generatePackageDetails(result: AnalysisResult): string { + let details = '## Package Details\n\n'; + + const packagesWithChanges = result.packages.filter(p => p.changes.length > 0); + + for (const pkg of packagesWithChanges) { + details += this.generatePackageSection(pkg); + details += '\n'; + } + + return details; + } + + private generatePackageSection(pkg: PackageAnalysis): string { + const unsuppressedChanges = pkg.changes.filter(c => !c.isSuppressed); + const icon = pkg.hasBreakingChanges ? '💥' : '📝'; + + let section = `### ${icon} ${pkg.packageName}\n\n`; + + // Version info + section += `**Version:** ${pkg.version.previous} → ${pkg.version.current}`; + if (pkg.actualVersionBump) { + section += ` (${pkg.actualVersionBump} bump)`; + } + section += '\n\n'; + + // Version bump validation + if (!pkg.isValidBump && unsuppressedChanges.length > 0) { + section += `⚠️ **Version bump required:** This package needs a **${pkg.recommendedVersionBump}** version bump\n\n`; + } + + // Changes by category + const changesByType = this.groupChangesByType(unsuppressedChanges); + + if (changesByType.breaking.length > 0) { + section += '#### 💥 Breaking Changes\n\n'; + section += this.generateChangesList(changesByType.breaking); + section += '\n'; + } + + if (changesByType.nonBreaking.length > 0) { + section += '#### 🔄 Non-Breaking Changes\n\n'; + section += this.generateChangesList(changesByType.nonBreaking); + section += '\n'; + } + + if (changesByType.additions.length > 0) { + section += '#### ✨ New Additions\n\n'; + section += this.generateChangesList(changesByType.additions); + section += '\n'; + } + + // Show suppressed changes if any + const suppressedChanges = pkg.changes.filter(c => c.isSuppressed); + if (suppressedChanges.length > 0) { + section += '
\n🔇 Suppressed Changes\n\n'; + section += this.generateChangesList(suppressedChanges); + section += '\n
\n\n'; + } + + return section; + } + + private generateChangesList(changes: ApiChange[]): string { + let list = ''; + + for (const change of changes) { + list += `- **${change.category}** \`${change.description}\``; + + if (change.isSuppressed && change.suppressionReason) { + list += ` _(suppressed: ${change.suppressionReason})_`; + } + list += '\n'; + + // Add code snippets if available + if (change.beforeSnippet || change.afterSnippet) { + if (change.beforeSnippet && change.afterSnippet) { + list += '
\n Show diff\n\n'; + list += ' ```diff\n'; + list += ` - ${change.beforeSnippet}\n`; + list += ` + ${change.afterSnippet}\n`; + list += ' ```\n
\n'; + } else if (change.beforeSnippet) { + list += `
\n Removed\n\n \`\`\`typescript\n ${change.beforeSnippet}\n \`\`\`\n
\n`; + } else if (change.afterSnippet) { + list += `
\n Added\n\n \`\`\`typescript\n ${change.afterSnippet}\n \`\`\`\n
\n`; + } + } + } + + return list; + } + + private groupChangesByType(changes: ApiChange[]): Record { + return { + breaking: changes.filter(c => c.type === 'breaking'), + nonBreaking: changes.filter(c => c.type === 'non-breaking'), + additions: changes.filter(c => c.type === 'addition'), + }; + } + + private getSuppressedChanges(result: AnalysisResult): ApiChange[] { + return result.packages.flatMap(p => p.changes).filter(c => c.isSuppressed); + } + + private generateSuppressionsSection(suppressedChanges: ApiChange[]): string { + let section = '## 🔇 Suppressed Changes\n\n'; + section += `${suppressedChanges.length} changes have been suppressed and will not affect CI status:\n\n`; + + const suppressionsByPackage = new Map(); + for (const change of suppressedChanges) { + const packageChanges = suppressionsByPackage.get(change.location?.file || 'unknown') || []; + packageChanges.push(change); + suppressionsByPackage.set(change.location?.file || 'unknown', packageChanges); + } + + for (const [pkg, changes] of suppressionsByPackage) { + section += `### ${pkg}\n\n`; + section += this.generateChangesList(changes); + section += '\n'; + } + + return section; + } + + private generateFooter(result: AnalysisResult): string { + let footer = '## Next Steps\n\n'; + + if (result.ciShouldFail) { + footer += '❌ **CI will fail** until the version bumps are corrected.\n\n'; + footer += '### Required Actions:\n\n'; + + const packagesNeedingBumps = result.packages.filter(p => !p.isValidBump && p.changes.some(c => !c.isSuppressed)); + + for (const pkg of packagesNeedingBumps) { + footer += `1. Update \`${pkg.packageName}\` version to require a **${pkg.recommendedVersionBump}** bump\n`; + } + + footer += '\n### Or:\n\n'; + footer += '- Suppress specific changes if they are not actually breaking\n'; + footer += '- Update the API to maintain backward compatibility\n'; + } else if (result.hasBreakingChanges) { + footer += '⚠️ Breaking changes detected but appropriate version bumps are in place.\n\n'; + footer += '**Please ensure these changes are intentional and properly documented.**'; + } else if (result.summary.packagesWithChanges > 0) { + footer += '✅ All changes are backward compatible and properly versioned.\n\n'; + footer += '**Safe to merge when ready.**'; + } else { + footer += '✅ No API changes detected.\n\n'; + footer += '**Safe to merge.**'; + } + + footer += '\n\n---\n\n'; + footer += '_This report was generated by the API Breakage Detector. '; + footer += 'For more information, see the [documentation](https://github.com/your-org/snapi)._'; + + return footer; + } + + /** + * Generate a compact summary for GitHub status checks + */ + generateStatusSummary(result: AnalysisResult): string { + if (result.hasBreakingChanges) { + return `💥 ${result.summary.breakingChanges} breaking changes detected`; + } else if (result.summary.packagesWithChanges > 0) { + return `📝 ${result.summary.packagesWithChanges} packages with API changes`; + } else { + return '✅ No API changes'; + } + } + + /** + * Generate JSON report + */ + generateJsonReport(result: AnalysisResult): string { + return JSON.stringify(result, null, 2); + } +} diff --git a/tools/snapi/src/storage/base-storage.ts b/tools/snapi/src/storage/base-storage.ts new file mode 100644 index 00000000000..b4e1fcb16ad --- /dev/null +++ b/tools/snapi/src/storage/base-storage.ts @@ -0,0 +1,135 @@ +export interface SnapshotMetadata { + packageName: string; + version: string; + timestamp: string; + commitHash: string; + branch: string; + author?: string; + fileSize: number; + checksum: string; +} + +export interface StorageConfig { + type: 'gcs' | 'turborepo'; + options: Record; +} + +export interface BaselineSnapshot { + packageName: string; + filePath: string; + metadata: SnapshotMetadata; +} + +export interface SnapshotStorageBackend { + /** + * Store a snapshot file with metadata + */ + store(packageName: string, snapshotPath: string, metadata: SnapshotMetadata): Promise; + + /** + * Retrieve a snapshot file for a specific package and commit + */ + retrieve(packageName: string, commitHash: string): Promise; + + /** + * Delete a snapshot file and its metadata + */ + delete(key: string): Promise; + + /** + * Get the baseline snapshot for a package from the main branch + */ + getBaseline(packageName: string, branch?: string): Promise; + + /** + * List all snapshots for a package + */ + listSnapshots(packageName: string, options?: { limit?: number; branch?: string }): Promise; + + /** + * Clean up old snapshots based on retention policy + */ + cleanup(retentionDays: number): Promise; + + /** + * Check if the storage backend is healthy and accessible + */ + healthCheck(): Promise; + + /** + * Get storage statistics and usage information + */ + getStats(): Promise<{ + totalSize: number; + snapshotCount: number; + oldestSnapshot: string; + newestSnapshot: string; + }>; +} + +/** + * Base implementation with common utilities + */ +export abstract class BaseStorageBackend implements SnapshotStorageBackend { + protected config: StorageConfig; + + constructor(config: StorageConfig) { + this.config = config; + } + + abstract store(packageName: string, snapshotPath: string, metadata: SnapshotMetadata): Promise; + abstract retrieve(packageName: string, commitHash: string): Promise; + abstract delete(key: string): Promise; + abstract getBaseline(packageName: string, branch?: string): Promise; + abstract listSnapshots( + packageName: string, + options?: { limit?: number; branch?: string }, + ): Promise; + abstract cleanup(retentionDays: number): Promise; + abstract healthCheck(): Promise; + abstract getStats(): Promise<{ + totalSize: number; + snapshotCount: number; + oldestSnapshot: string; + newestSnapshot: string; + }>; + + /** + * Generate a storage key for a package snapshot + */ + protected generateKey(packageName: string, commitHash: string): string { + const safeName = packageName.replace(/[@/]/g, '_'); + return `${safeName}_${commitHash}.api.json`; + } + + /** + * Generate a metadata key for a package snapshot + */ + protected generateMetadataKey(packageName: string, commitHash: string): string { + const safeName = packageName.replace(/[@/]/g, '_'); + return `${safeName}_${commitHash}.metadata.json`; + } + + /** + * Validate snapshot metadata + */ + protected validateMetadata(metadata: SnapshotMetadata): void { + const required = ['packageName', 'version', 'timestamp', 'commitHash', 'branch']; + for (const field of required) { + if (!metadata[field as keyof SnapshotMetadata]) { + throw new Error(`Missing required metadata field: ${field}`); + } + } + } + + /** + * Calculate file checksum for integrity verification + */ + protected async calculateChecksum(filePath: string): Promise { + const crypto = await import('crypto'); + const fs = await import('fs-extra'); + + const content = await fs.readFile(filePath); + return crypto.createHash('sha256').update(content).digest('hex'); + } +} diff --git a/tools/snapi/src/storage/gcs-storage.ts b/tools/snapi/src/storage/gcs-storage.ts new file mode 100644 index 00000000000..cd68567cafd --- /dev/null +++ b/tools/snapi/src/storage/gcs-storage.ts @@ -0,0 +1,632 @@ +import fs from 'fs-extra'; +import path from 'path'; + +import { + type BaselineSnapshot, + BaseStorageBackend, + type SnapshotMetadata, + type StorageConfig, +} from './base-storage.js'; + +export interface GcsStorageOptions { + prefix?: string; + enableVersioning?: boolean; + lifecycle?: { + enabled: boolean; + deleteAfterDays?: number; + archiveAfterDays?: number; + }; + multiRegion?: boolean; + storageClass?: 'STANDARD' | 'NEARLINE' | 'COLDLINE' | 'ARCHIVE'; +} + +/** + * Google Cloud Storage Backend + * + * Production-optimized storage solution for API snapshots using Google Cloud Storage. + * Designed for high reliability, global accessibility, and cost efficiency. + * + * Features: + * - Multi-regional replication for high availability + * - Automatic lifecycle management and archiving + * - Cost optimization with storage classes + * - Batch operations for efficiency + * - Object versioning for safety + * - Built-in caching and compression + * - IAM integration for security + * + * Benefits for Production: + * - 99.95% availability SLA + * - Global edge caching + * - Automatic encryption at rest + * - Fine-grained access control + * - Seamless scaling + */ +export class GcsStorageBackend extends BaseStorageBackend { + private storage: any; + private bucket: any; + private options: GcsStorageOptions; + private projectId: string; + private bucketName: string; + private prefix: string = ''; + private readonly LATEST_POINTER = 'latest.json'; + + constructor(config: StorageConfig) { + super(config); + this.options = config.options as GcsStorageOptions; + + // Get projectId from config or environment variable + this.projectId = process.env.GCS_PROJECT_ID || ''; + if (!this.projectId) { + throw new Error('GCS Project ID must be provided via GCS_PROJECT_ID environment variable'); + } + + // Get bucket from config or environment variable + this.bucketName = process.env.GCS_BUCKET || ''; + if (!this.bucketName) { + throw new Error('GCS Bucket must be provided via GCS_BUCKET environment variable'); + } + + console.log(`🪣 Using GCS: Project=${this.projectId}, Bucket=${this.bucketName}`); + } + + async store(packageName: string, snapshotPath: string, metadata: SnapshotMetadata): Promise { + await this.ensureStorage(); + + const key = this.generateKey(packageName, metadata.commitHash); + const metadataKey = this.generateMetadataKey(packageName, metadata.commitHash); + const latestPointerKey = this.generateLatestPointerKey(packageName); + + try { + // Upload the snapshot file + const fullKey = `${this.getPackagePrefix(packageName)}/${key}`; + await this.bucket.upload(snapshotPath, { + destination: fullKey, + metadata: { + contentType: 'application/json', + }, + }); + + // Upload metadata + const fullMetadataKey = `${this.getPackagePrefix(packageName)}/${metadataKey}`; + await this.bucket.file(fullMetadataKey).save(JSON.stringify(metadata, null, 2), { + contentType: 'application/json', + }); + + // Update latest pointer + await this.bucket.file(latestPointerKey).save(JSON.stringify(metadata, null, 2), { + contentType: 'application/json', + }); + + return key; + } catch (error) { + console.warn(`Failed to store snapshot in GCS:`, error); + throw error; + } + } + + async retrieve(packageName: string, commitHash: string): Promise { + await this.ensureStorage(); + + const key = this.generateKey(packageName, commitHash); + const file = this.bucket.file(key); + + try { + const [exists] = await file.exists(); + if (!exists) { + return null; + } + + const localPath = path.join( + process.cwd(), + '.tmp', + 'retrieved-snapshots', + `${packageName.replace(/[@/]/g, '_')}_${commitHash}.api.json`, + ); + + await fs.ensureDir(path.dirname(localPath)); + await file.download({ destination: localPath }); + + return localPath; + } catch (error) { + console.warn(`Failed to retrieve snapshot from GCS:`, error); + return null; + } + } + + /** + * Delete a snapshot file and its metadata + */ + async delete(key: string): Promise { + await this.ensureStorage(); + + try { + // Extract package name from the key (format: packageName_commitHash.api.json) + const packageName = key.split('_')[0]; + const fullKey = `${this.getPackagePrefix(packageName)}/${key}`; + const file = this.bucket.file(fullKey); + const metadataKey = fullKey.replace('.api.json', '.metadata.json'); + + // Delete the snapshot file + await file.delete(); + + // Delete metadata if it exists + const metadataFile = this.bucket.file(metadataKey); + const [exists] = await metadataFile.exists(); + if (exists) { + await metadataFile.delete(); + } + + console.log(`✅ Deleted snapshot ${key} from GCS`); + } catch (error) { + console.warn(`Failed to delete snapshot from GCS:`, error); + throw error; + } + } + + async getBaseline(packageName: string, branch: string = 'main'): Promise { + await this.ensureStorage(); + + try { + // Get the latest pointer + const latestPointerKey = this.generateLatestPointerKey(packageName); + const latestFile = this.bucket.file(latestPointerKey); + + const [exists] = await latestFile.exists(); + if (!exists) { + return null; + } + + // Download the latest pointer + const [content] = await latestFile.download(); + const metadata: SnapshotMetadata = JSON.parse(content.toString()); + + // Filter by branch if specified + if (branch && metadata.branch !== branch) { + return null; + } + + // Get the actual snapshot file + const localPath = await this.retrieve(packageName, metadata.commitHash); + if (!localPath) { + return null; + } + + return { + packageName, + filePath: localPath, + metadata, + }; + } catch (error) { + console.warn(`Failed to get baseline from GCS:`, error); + return null; + } + } + + async listSnapshots(packageName: string, options?: { limit?: number; branch?: string }): Promise { + await this.ensureStorage(); + + const prefix = this.getPackagePrefix(packageName); + + try { + const [files] = await this.bucket.getFiles({ + prefix: `${prefix}/`, + maxResults: (options?.limit || 100) * 2, // Get more files to account for .metadata.json files + }); + + const snapshots: SnapshotMetadata[] = []; + + // Process metadata files in parallel + const metadataFiles = files.filter((file: any) => file.name.endsWith('.metadata.json')); + const metadataPromises = metadataFiles.slice(0, options?.limit || 100).map(async (file: any) => { + try { + const [content] = await file.download(); + const metadata: SnapshotMetadata = JSON.parse(content.toString()); + + // Filter by branch if specified + if (!options?.branch || metadata.branch === options.branch) { + return metadata; + } + } catch (error) { + console.warn(`Failed to read metadata from ${file.name}:`, error); + } + return null; + }); + + const results = await Promise.all(metadataPromises); + snapshots.push(...(results.filter(Boolean) as SnapshotMetadata[])); + + // Sort by timestamp (newest first) + return snapshots.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); + } catch (error) { + console.warn(`Failed to list snapshots from GCS:`, error); + return []; + } + } + + async cleanup(retentionDays: number): Promise { + await this.ensureStorage(); + + const cutoffDate = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000); + let cleanedCount = 0; + + try { + // Use lifecycle management if enabled + if (this.options.lifecycle?.enabled) { + await this.configureLifecycle(retentionDays); + console.log(`✅ Configured GCS lifecycle management for ${retentionDays} day retention`); + return; + } + + // Manual cleanup for fine-grained control + const [files] = await this.bucket.getFiles({ + prefix: this.getPrefix(), + }); + + // Process files in batches for better performance + const batchSize = 100; + for (let i = 0; i < files.length; i += batchSize) { + const batch = files.slice(i, i + batchSize); + + const deletePromises = batch.map(async (file: any) => { + if (file.name.endsWith('.metadata.json')) { + try { + const [content] = await file.download(); + const metadata: SnapshotMetadata = JSON.parse(content.toString()); + const snapshotDate = new Date(metadata.timestamp); + + if (snapshotDate < cutoffDate) { + const snapshotFileName = file.name.replace('.metadata.json', '.api.json'); + const snapshotFile = this.bucket.file(snapshotFileName); + + // Delete both metadata and snapshot files + await Promise.all([ + file.delete(), + snapshotFile.delete().catch(() => {}), // Ignore if snapshot file doesn't exist + ]); + + return 1; + } + } catch (error) { + console.warn(`Failed to process file ${file.name}:`, error); + } + } + return 0; + }); + + const results = await Promise.all(deletePromises); + cleanedCount += results.reduce((sum, count) => sum + count, 0); + } + + console.log(`✅ Cleaned up ${cleanedCount} old snapshots from GCS`); + } catch (error) { + console.warn(`Failed to cleanup snapshots from GCS:`, error); + } + } + + async healthCheck(): Promise { + try { + await this.ensureStorage(); + + // Test bucket access + await this.bucket.getMetadata(); + + // Test file operations with a small test file + const testFile = this.bucket.file(`${this.getPrefix()}health-check-${Date.now()}.txt`); + await testFile.save('health-check'); + await testFile.delete(); + + return true; + } catch (error) { + console.error('GCS storage health check failed:', error); + return false; + } + } + + async getStats(): Promise<{ + totalSize: number; + snapshotCount: number; + oldestSnapshot: string; + newestSnapshot: string; + }> { + await this.ensureStorage(); + + try { + const [files] = await this.bucket.getFiles({ + prefix: this.getPrefix(), + }); + + let totalSize = 0; + let snapshotCount = 0; + let oldestDate = new Date(); + let newestDate = new Date(0); + + // Process metadata files to get accurate stats + const metadataFiles = files.filter((file: any) => file.name.endsWith('.metadata.json')); + + const statsPromises = metadataFiles.map(async (file: any) => { + try { + const [content] = await file.download(); + const metadata: SnapshotMetadata = JSON.parse(content.toString()); + const snapshotDate = new Date(metadata.timestamp); + + return { + fileSize: metadata.fileSize, + timestamp: snapshotDate, + }; + } catch (error) { + console.warn(`Failed to read metadata from ${file.name}:`, error); + return null; + } + }); + + const results = await Promise.all(statsPromises); + + for (const result of results) { + if (result) { + totalSize += result.fileSize; + snapshotCount++; + + if (result.timestamp < oldestDate) { + oldestDate = result.timestamp; + } + if (result.timestamp > newestDate) { + newestDate = result.timestamp; + } + } + } + + return { + totalSize, + snapshotCount, + oldestSnapshot: snapshotCount > 0 ? oldestDate.toISOString() : '', + newestSnapshot: snapshotCount > 0 ? newestDate.toISOString() : '', + }; + } catch (error) { + console.warn(`Failed to get stats from GCS:`, error); + return { + totalSize: 0, + snapshotCount: 0, + oldestSnapshot: '', + newestSnapshot: '', + }; + } + } + + private async ensureStorage(): Promise { + if (this.storage && this.bucket) { + return; + } + + try { + // Dynamic import to handle optional dependency + const { Storage } = await import('@google-cloud/storage').catch(() => { + throw new Error('@google-cloud/storage is not installed. Install it with: pnpm install @google-cloud/storage'); + }); + + const storageOptions: any = { + projectId: this.projectId, + }; + + // Handle authentication via environment variables + if (process.env.GOOGLE_SERVICE_ACCOUNT_KEY) { + // Use service account key from GOOGLE_SERVICE_ACCOUNT_KEY environment variable + try { + const credentials = JSON.parse(process.env.GOOGLE_SERVICE_ACCOUNT_KEY); + storageOptions.credentials = credentials; + console.log('🔐 Using service account key from GOOGLE_SERVICE_ACCOUNT_KEY'); + } catch (error) { + throw new Error(`Failed to parse service account key from GOOGLE_SERVICE_ACCOUNT_KEY: ${error}`); + } + } else if (process.env.GOOGLE_APPLICATION_CREDENTIALS) { + // Use service account key file from GOOGLE_APPLICATION_CREDENTIALS + console.log('🔐 Using service account key file from GOOGLE_APPLICATION_CREDENTIALS'); + } else { + // Use default application credentials (gcloud auth application-default login) + console.log('🔐 Using default application credentials'); + } + + this.storage = new Storage(storageOptions); + this.bucket = this.storage.bucket(this.bucketName); + + // Ensure bucket exists and is properly configured + await this.ensureBucketConfiguration(); + } catch (error) { + throw new Error(`Failed to initialize GCS client: ${error}`); + } + } + + private async ensureBucketConfiguration(): Promise { + try { + const [exists] = await this.bucket.exists(); + + if (!exists) { + console.log(`Creating GCS bucket: ${this.bucketName}`); + await this.bucket.create({ + location: this.options.multiRegion ? 'US' : 'US-CENTRAL1', + storageClass: this.options.storageClass || 'STANDARD', + versioning: { + enabled: this.options.enableVersioning || true, + }, + }); + } + + // Configure CORS for web access if needed + await this.bucket.setCorsConfiguration([ + { + maxAgeSeconds: 3600, + method: ['GET', 'HEAD'], + origin: ['*'], + responseHeader: ['Content-Type', 'Content-Range', 'Content-Length'], + }, + ]); + + // Set up lifecycle management if enabled + if (this.options.lifecycle?.enabled) { + await this.configureLifecycle(this.options.lifecycle.deleteAfterDays || 30); + } + } catch (error) { + console.warn('Failed to configure bucket (may not have permissions):', error); + } + } + + private async configureLifecycle(retentionDays: number): Promise { + const lifecycleRules = []; + + // Delete old snapshots + lifecycleRules.push({ + action: { type: 'Delete' }, + condition: { + age: retentionDays, + matchesPrefix: [this.getPrefix()], + }, + }); + + // Archive older snapshots if archiving is enabled + if (this.options.lifecycle?.archiveAfterDays) { + lifecycleRules.push({ + action: { type: 'SetStorageClass', storageClass: 'ARCHIVE' }, + condition: { + age: this.options.lifecycle.archiveAfterDays, + matchesPrefix: [this.getPrefix()], + matchesStorageClass: ['STANDARD', 'NEARLINE', 'COLDLINE'], + }, + }); + } + + await this.bucket.setMetadata({ + lifecycle: { rule: lifecycleRules }, + }); + } + + private async uploadFile(key: string, filePath: string, metadata: SnapshotMetadata): Promise { + const file = this.bucket.file(key); + + const uploadOptions: any = { + metadata: { + contentType: 'application/json', + cacheControl: 'public, max-age=3600', + metadata: { + packageName: metadata.packageName, + version: metadata.version, + commitHash: metadata.commitHash, + branch: metadata.branch, + checksum: metadata.checksum, + }, + }, + }; + + // Enable compression for better performance and cost + if (metadata.fileSize > 1024) { + // Only compress files larger than 1KB + uploadOptions.gzip = true; + } + + await file.save(await fs.readFile(filePath), uploadOptions); + return file; + } + + private async uploadMetadata(key: string, metadata: SnapshotMetadata): Promise { + const file = this.bucket.file(key); + + await file.save(JSON.stringify(metadata, null, 2), { + metadata: { + contentType: 'application/json', + cacheControl: 'public, max-age=3600', + }, + }); + + return file; + } + + private getPackagePrefix(packageName: string): string { + const prefix = this.getPrefix(); + const safeName = packageName.replace(/[@/]/g, '_'); + return `${prefix}snapshots/${safeName}`; + } + + private getPrefix(): string { + const prefix = this.options.prefix || ''; + return prefix ? `${prefix}/` : ''; + } + + private formatFileSize(bytes: number): string { + const units = ['B', 'KB', 'MB', 'GB']; + let size = bytes; + let unitIndex = 0; + + while (size >= 1024 && unitIndex < units.length - 1) { + size /= 1024; + unitIndex++; + } + + return `${size.toFixed(1)} ${units[unitIndex]}`; + } + + /** + * Get presigned URLs for direct browser uploads (useful for large files) + */ + async getUploadUrl(packageName: string, commitHash: string, expiresIn: number = 3600): Promise { + await this.ensureStorage(); + + const key = this.generateKey(packageName, commitHash); + const file = this.bucket.file(key); + + const [url] = await file.getSignedUrl({ + version: 'v4', + action: 'write', + expires: Date.now() + expiresIn * 1000, + contentType: 'application/json', + }); + + return url; + } + + /** + * Get presigned URLs for direct browser downloads + */ + async getDownloadUrl(packageName: string, commitHash: string, expiresIn: number = 3600): Promise { + await this.ensureStorage(); + + const key = this.generateKey(packageName, commitHash); + const file = this.bucket.file(key); + + const [url] = await file.getSignedUrl({ + version: 'v4', + action: 'read', + expires: Date.now() + expiresIn * 1000, + }); + + return url; + } + + /** + * Batch upload multiple snapshots for efficiency + */ + async storeBatch( + snapshots: Array<{ + packageName: string; + snapshotPath: string; + metadata: SnapshotMetadata; + }>, + ): Promise { + await this.ensureStorage(); + + const uploadPromises = snapshots.map(async ({ packageName, snapshotPath, metadata }) => { + try { + return await this.store(packageName, snapshotPath, metadata); + } catch (error) { + console.warn(`Failed to upload ${packageName}:`, error); + return null; + } + }); + + const results = await Promise.all(uploadPromises); + return results.filter(Boolean) as string[]; + } + + private generateLatestPointerKey(packageName: string): string { + const safeName = packageName.replace(/[@/]/g, '_'); + return `${this.getPackagePrefix(packageName)}/${safeName}_${this.LATEST_POINTER}`; + } +} diff --git a/tools/snapi/src/storage/storage-manager.ts b/tools/snapi/src/storage/storage-manager.ts new file mode 100644 index 00000000000..31e7e725de1 --- /dev/null +++ b/tools/snapi/src/storage/storage-manager.ts @@ -0,0 +1,521 @@ +import type { BaselineSnapshot, SnapshotMetadata, SnapshotStorageBackend, StorageConfig } from './base-storage.js'; +import { GcsStorageBackend } from './gcs-storage.js'; +import { TurborepoStorageBackend } from './turborepo-storage.js'; + +export interface StorageManagerConfig { + primary: StorageConfig; + fallback?: StorageConfig[]; + healthCheckInterval?: number; // minutes + retryAttempts?: number; + retryDelay?: number; // milliseconds +} + +export interface StorageHealth { + backend: string; + healthy: boolean; + lastCheck: Date; + error?: string; + latency?: number; +} + +/** + * Storage Manager + * + * Orchestrates GCS and Turborepo storage backends with intelligent failover, health monitoring, + * and optimized routing. Designed for production reliability with GCS as the primary + * storage solution and Turborepo for local caching. + * + * Features: + * - Automatic failover between GCS and Turborepo + * - Health monitoring and recovery + * - Intelligent routing based on performance + * - Retry logic with exponential backoff + * - Metrics and monitoring + * - Cost optimization strategies + */ +export class StorageManager { + private primaryBackend: SnapshotStorageBackend; + private fallbackBackends: SnapshotStorageBackend[] = []; + private healthStatus: Map = new Map(); + private config: StorageManagerConfig; + private healthCheckTimer?: NodeJS.Timeout; + + constructor(config: StorageManagerConfig) { + this.config = { + healthCheckInterval: 15, // 15 minutes + retryAttempts: 3, + retryDelay: 1000, + ...config, + }; + + this.primaryBackend = this.createBackend(config.primary); + + if (config.fallback) { + this.fallbackBackends = config.fallback.map(cfg => this.createBackend(cfg)); + } + + this.startHealthMonitoring(); + } + + /** + * Store a snapshot with automatic failover + */ + async store(packageName: string, snapshotPath: string, metadata: SnapshotMetadata): Promise { + const backends = [this.primaryBackend, ...this.fallbackBackends]; + + for (const backend of backends) { + if (await this.isBackendHealthy(backend)) { + try { + const result = await this.executeWithRetry( + () => backend.store(packageName, snapshotPath, metadata), + `store ${packageName}`, + ); + + // Update health status on success + this.updateHealthStatus(backend, true); + return result; + } catch (error) { + console.warn(`Storage backend failed for ${packageName}:`, error); + this.updateHealthStatus(backend, false, error as Error); + } + } + } + + throw new Error(`All storage backends failed for ${packageName}`); + } + + /** + * Retrieve a snapshot with automatic failover + */ + async retrieve(packageName: string, commitHash: string): Promise { + const backends = [this.primaryBackend, ...this.fallbackBackends]; + + for (const backend of backends) { + if (await this.isBackendHealthy(backend)) { + try { + const result = await this.executeWithRetry( + () => backend.retrieve(packageName, commitHash), + `retrieve ${packageName}@${commitHash}`, + ); + + if (result) { + this.updateHealthStatus(backend, true); + return result; + } + } catch (error) { + console.warn(`Failed to retrieve ${packageName}@${commitHash}:`, error); + this.updateHealthStatus(backend, false, error as Error); + } + } + } + + return null; + } + + /** + * Delete a snapshot with automatic failover + */ + async delete(key: string): Promise { + const backends = [this.primaryBackend, ...this.fallbackBackends]; + + for (const backend of backends) { + if (await this.isBackendHealthy(backend)) { + try { + await this.executeWithRetry(() => backend.delete(key), `delete ${key}`); + + this.updateHealthStatus(backend, true); + return; + } catch (error) { + console.warn(`Failed to delete ${key}:`, error); + this.updateHealthStatus(backend, false, error as Error); + } + } + } + + throw new Error(`All storage backends failed to delete ${key}`); + } + + /** + * Get baseline snapshot with smart backend selection + */ + async getBaseline(packageName: string, branch?: string): Promise { + const backends = [this.primaryBackend, ...this.fallbackBackends]; + + for (const backend of backends) { + if (await this.isBackendHealthy(backend)) { + try { + const result = await this.executeWithRetry( + () => backend.getBaseline(packageName, branch), + `getBaseline ${packageName}@${branch}`, + ); + + if (result) { + this.updateHealthStatus(backend, true); + return result; + } + } catch (error) { + console.warn(`Failed to get baseline for ${packageName}:`, error); + this.updateHealthStatus(backend, false, error as Error); + } + } + } + + return null; + } + + /** + * List snapshots from the healthiest backend + */ + async listSnapshots(packageName: string, options?: { limit?: number; branch?: string }): Promise { + const backend = await this.selectBestBackend(); + + try { + const result = await this.executeWithRetry( + () => backend.listSnapshots(packageName, options), + `listSnapshots ${packageName}`, + ); + + this.updateHealthStatus(backend, true); + return result; + } catch (error) { + console.warn(`Failed to list snapshots for ${packageName}:`, error); + this.updateHealthStatus(backend, false, error as Error); + return []; + } + } + + /** + * Clean up old snapshots across all backends + */ + async cleanup(retentionDays: number): Promise { + const backends = [this.primaryBackend, ...this.fallbackBackends]; + + const cleanupPromises = backends.map(async backend => { + if (await this.isBackendHealthy(backend)) { + try { + await this.executeWithRetry(() => backend.cleanup(retentionDays), `cleanup`); + this.updateHealthStatus(backend, true); + } catch (error) { + console.warn(`Cleanup failed for backend:`, error); + this.updateHealthStatus(backend, false, error as Error); + } + } + }); + + await Promise.allSettled(cleanupPromises); + } + + /** + * Get comprehensive health status for all backends + */ + async getHealthStatus(): Promise { + return Array.from(this.healthStatus.values()); + } + + /** + * Get aggregated statistics from all backends + */ + async getStats(): Promise<{ + totalSize: number; + snapshotCount: number; + oldestSnapshot: string; + newestSnapshot: string; + backendStats: Array<{ + backend: string; + stats: any; + healthy: boolean; + }>; + }> { + const backends = [this.primaryBackend, ...this.fallbackBackends]; + + let totalSize = 0; + let snapshotCount = 0; + let oldestDate = new Date(); + let newestDate = new Date(0); + const backendStats: Array<{ backend: string; stats: any; healthy: boolean }> = []; + + for (const backend of backends) { + const isHealthy = await this.isBackendHealthy(backend); + + if (isHealthy) { + try { + const stats = await backend.getStats(); + backendStats.push({ + backend: this.getBackendName(backend), + stats, + healthy: true, + }); + + totalSize += stats.totalSize; + snapshotCount += stats.snapshotCount; + + if (stats.oldestSnapshot) { + const oldestBackendDate = new Date(stats.oldestSnapshot); + if (oldestBackendDate < oldestDate) { + oldestDate = oldestBackendDate; + } + } + + if (stats.newestSnapshot) { + const newestBackendDate = new Date(stats.newestSnapshot); + if (newestBackendDate > newestDate) { + newestDate = newestBackendDate; + } + } + } catch (error) { + backendStats.push({ + backend: this.getBackendName(backend), + stats: null, + healthy: false, + }); + } + } else { + backendStats.push({ + backend: this.getBackendName(backend), + stats: null, + healthy: false, + }); + } + } + + return { + totalSize, + snapshotCount, + oldestSnapshot: snapshotCount > 0 ? oldestDate.toISOString() : '', + newestSnapshot: snapshotCount > 0 ? newestDate.toISOString() : '', + backendStats, + }; + } + + /** + * Force health check for all backends + */ + async performHealthCheck(): Promise { + const backends = [this.primaryBackend, ...this.fallbackBackends]; + + const healthPromises = backends.map(async backend => { + const start = Date.now(); + try { + const healthy = await backend.healthCheck(); + const latency = Date.now() - start; + + this.healthStatus.set(this.getBackendName(backend), { + backend: this.getBackendName(backend), + healthy, + lastCheck: new Date(), + latency, + }); + } catch (error) { + this.healthStatus.set(this.getBackendName(backend), { + backend: this.getBackendName(backend), + healthy: false, + lastCheck: new Date(), + error: error instanceof Error ? error.message : String(error), + }); + } + }); + + await Promise.allSettled(healthPromises); + } + + /** + * Create production GCS configuration + */ + static createGcsConfig( + options: { + prefix?: string; + multiRegion?: boolean; + enableLifecycle?: boolean; + } = {}, + ): StorageManagerConfig { + const gcsConfig: StorageConfig = { + type: 'gcs', + options: { + prefix: options.prefix || 'api-snapshots', + multiRegion: options.multiRegion ?? true, + enableVersioning: true, + lifecycle: { + enabled: options.enableLifecycle ?? true, + deleteAfterDays: 90, + archiveAfterDays: 30, + }, + storageClass: 'STANDARD', + }, + }; + + return { + primary: gcsConfig, + fallback: [], // Add fallback backends as needed + healthCheckInterval: 10, + retryAttempts: 3, + retryDelay: 1000, + }; + } + + /** + * Create hybrid configuration with GCS primary and Turborepo fallback + */ + static createHybridConfig( + options: { + gcs?: { + prefix?: string; + multiRegion?: boolean; + enableLifecycle?: boolean; + }; + turborepo?: { + cacheDir?: string; + remoteCache?: boolean; + teamId?: string; + token?: string; + }; + } = {}, + ): StorageManagerConfig { + const gcsConfig = this.createGcsConfig(options.gcs); + + if (options.turborepo) { + gcsConfig.fallback = [ + { + type: 'turborepo', + options: { + cacheDir: options.turborepo.cacheDir, + remoteCache: options.turborepo.remoteCache || false, + teamId: options.turborepo.teamId, + token: options.turborepo.token, + namespace: 'api-snapshots', + }, + }, + ]; + } + + return gcsConfig; + } + + /** + * Clean shutdown + */ + async shutdown(): Promise { + if (this.healthCheckTimer) { + clearInterval(this.healthCheckTimer); + } + } + + private createBackend(config: StorageConfig): SnapshotStorageBackend { + switch (config.type) { + case 'gcs': + return new GcsStorageBackend(config); + case 'turborepo': + return new TurborepoStorageBackend(config); + default: + throw new Error(`Unsupported storage backend: ${config.type}`); + } + } + + private async isBackendHealthy(backend: SnapshotStorageBackend): Promise { + const backendName = this.getBackendName(backend); + const health = this.healthStatus.get(backendName); + + if (!health) { + // No health data, assume healthy for first attempt + return true; + } + + // Check if health data is recent (within 2x health check interval) + const maxAge = (this.config.healthCheckInterval || 15) * 2 * 60 * 1000; + const age = Date.now() - health.lastCheck.getTime(); + + if (age > maxAge) { + // Health data is stale, perform fresh check + try { + const healthy = await backend.healthCheck(); + this.updateHealthStatus(backend, healthy); + return healthy; + } catch (error) { + this.updateHealthStatus(backend, false, error as Error); + return false; + } + } + + return health.healthy; + } + + private async selectBestBackend(): Promise { + const backends = [this.primaryBackend, ...this.fallbackBackends]; + + // Prefer primary if healthy + if (await this.isBackendHealthy(this.primaryBackend)) { + return this.primaryBackend; + } + + // Find the best fallback (lowest latency) + let bestBackend = backends[0]; + let bestLatency = Infinity; + + for (const backend of backends) { + if (await this.isBackendHealthy(backend)) { + const health = this.healthStatus.get(this.getBackendName(backend)); + const latency = health?.latency || Infinity; + + if (latency < bestLatency) { + bestLatency = latency; + bestBackend = backend; + } + } + } + + return bestBackend; + } + + private async executeWithRetry(operation: () => Promise, description: string): Promise { + let lastError: Error | null = null; + + for (let attempt = 0; attempt < this.config.retryAttempts!; attempt++) { + try { + return await operation(); + } catch (error) { + lastError = error as Error; + + if (attempt < this.config.retryAttempts! - 1) { + const delay = this.config.retryDelay! * Math.pow(2, attempt); + console.warn(`${description} failed (attempt ${attempt + 1}), retrying in ${delay}ms:`, error); + await new Promise(resolve => setTimeout(resolve, delay)); + } + } + } + + throw new Error(`${description} failed after ${this.config.retryAttempts} attempts: ${lastError?.message}`); + } + + private updateHealthStatus(backend: SnapshotStorageBackend, healthy: boolean, error?: Error): void { + const backendName = this.getBackendName(backend); + + this.healthStatus.set(backendName, { + backend: backendName, + healthy, + lastCheck: new Date(), + error: error?.message, + }); + } + + private getBackendName(backend: SnapshotStorageBackend): string { + if (backend instanceof GcsStorageBackend) return 'gcs'; + if (backend instanceof TurborepoStorageBackend) return 'turborepo'; + return 'unknown'; + } + + /** + * Generate a storage key for a package snapshot + */ + generateKey(packageName: string, commitHash: string): string { + const safeName = packageName.replace(/[@/]/g, '_'); + return `${safeName}_${commitHash}.api.json`; + } + + private startHealthMonitoring(): void { + if (this.config.healthCheckInterval && this.config.healthCheckInterval > 0) { + this.healthCheckTimer = setInterval(() => this.performHealthCheck(), this.config.healthCheckInterval * 60 * 1000); + + // Perform initial health check + setTimeout(() => this.performHealthCheck(), 5000); + } + } +} diff --git a/tools/snapi/src/storage/turborepo-storage.ts b/tools/snapi/src/storage/turborepo-storage.ts new file mode 100644 index 00000000000..15d474daf85 --- /dev/null +++ b/tools/snapi/src/storage/turborepo-storage.ts @@ -0,0 +1,323 @@ +import { exec } from 'child_process'; +import fs from 'fs-extra'; +import path from 'path'; +import { promisify } from 'util'; + +import { + type BaselineSnapshot, + BaseStorageBackend, + type SnapshotMetadata, + type StorageConfig, +} from './base-storage.js'; + +const execAsync = promisify(exec); + +export interface TurborepoStorageOptions { + cacheDir?: string; + teamId?: string; + token?: string; + remoteCache?: boolean; + namespace?: string; +} + +/** + * Turborepo Cache Storage Backend + * + * Leverages Turborepo's caching system for storing API snapshots. + * Integrates with existing build cache infrastructure and supports + * both local and remote caching. + * + * Features: + * - Integration with Turborepo cache + * - Local and remote cache support + * - Automatic cache key generation + * - Built-in deduplication + * - Fast retrieval and storage + */ +export class TurborepoStorageBackend extends BaseStorageBackend { + private options: TurborepoStorageOptions; + private cacheDir: string; + + constructor(config: StorageConfig) { + super(config); + this.options = config.options as TurborepoStorageOptions; + this.cacheDir = this.options.cacheDir || path.join(process.cwd(), 'node_modules/.cache/turbo'); + } + + async store(packageName: string, snapshotPath: string, metadata: SnapshotMetadata): Promise { + this.validateMetadata(metadata); + + const cacheKey = this.generateCacheKey(packageName, metadata.commitHash); + const cacheDir = path.join(this.cacheDir, this.getCacheNamespace(), cacheKey); + + try { + await fs.ensureDir(cacheDir); + + // Store the snapshot file + const snapshotFile = path.join(cacheDir, 'snapshot.api.json'); + await fs.copy(snapshotPath, snapshotFile); + + // Store metadata + const metadataFile = path.join(cacheDir, 'metadata.json'); + await fs.writeJson(metadataFile, metadata, { spaces: 2 }); + + console.log(`✅ Stored snapshot for ${packageName} in Turborepo cache`); + return cacheKey; + } catch (error) { + throw new Error(`Failed to store snapshot in Turborepo cache: ${error}`); + } + } + + async retrieve(packageName: string, commitHash: string): Promise { + const cacheKey = this.generateCacheKey(packageName, commitHash); + const cacheDir = path.join(this.cacheDir, this.getCacheNamespace(), cacheKey); + const snapshotFile = path.join(cacheDir, 'snapshot.api.json'); + + try { + if (!(await fs.pathExists(snapshotFile))) { + return null; + } + + // Copy to a temporary location for retrieval + const tempDir = path.join(process.cwd(), '.tmp', 'retrieved-snapshots'); + await fs.ensureDir(tempDir); + + const localPath = path.join(tempDir, `${packageName.replace(/[@/]/g, '_')}_${commitHash}.api.json`); + await fs.copy(snapshotFile, localPath); + + return localPath; + } catch (error) { + console.warn(`Failed to retrieve snapshot from Turborepo cache:`, error); + return null; + } + } + + async getBaseline(packageName: string, branch: string = 'main'): Promise { + try { + // Find the most recent snapshot for this package on the main branch + const snapshots = await this.listSnapshots(packageName, { branch, limit: 1 }); + + if (snapshots.length === 0) { + return null; + } + + const latest = snapshots[0]; + const localPath = await this.retrieve(packageName, latest.commitHash); + + if (!localPath) { + return null; + } + + return { + packageName, + filePath: localPath, + metadata: latest, + }; + } catch (error) { + console.warn(`Failed to get baseline from Turborepo cache:`, error); + return null; + } + } + + async listSnapshots(packageName: string, options?: { limit?: number; branch?: string }): Promise { + const namespaceDir = path.join(this.cacheDir, this.getCacheNamespace()); + + try { + if (!(await fs.pathExists(namespaceDir))) { + return []; + } + + const entries = await fs.readdir(namespaceDir); + const snapshots: SnapshotMetadata[] = []; + + // Filter cache entries that match this package + const packagePrefix = packageName.replace(/[@/]/g, '_'); + const matchingEntries = entries.filter(entry => entry.startsWith(`${packagePrefix}_`)); + + for (const entry of matchingEntries) { + try { + const metadataFile = path.join(namespaceDir, entry, 'metadata.json'); + + if (await fs.pathExists(metadataFile)) { + const metadata: SnapshotMetadata = await fs.readJson(metadataFile); + + // Filter by branch if specified + if (!options?.branch || metadata.branch === options.branch) { + snapshots.push(metadata); + } + } + } catch (error) { + console.warn(`Failed to read metadata for ${entry}:`, error); + } + } + + // Sort by timestamp (newest first) and apply limit + const sortedSnapshots = snapshots.sort( + (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(), + ); + + return options?.limit ? sortedSnapshots.slice(0, options.limit) : sortedSnapshots; + } catch (error) { + console.warn(`Failed to list snapshots from Turborepo cache:`, error); + return []; + } + } + + async delete(key: string): Promise { + const cacheDir = path.join(this.cacheDir, this.getCacheNamespace(), key); + + try { + if (await fs.pathExists(cacheDir)) { + await fs.remove(cacheDir); + console.log(`✅ Deleted snapshot ${key} from Turborepo cache`); + } else { + console.warn(`Snapshot ${key} not found in Turborepo cache for deletion`); + } + } catch (error) { + throw new Error(`Failed to delete snapshot ${key} from Turborepo cache: ${error}`); + } + } + + async cleanup(retentionDays: number): Promise { + const cutoffDate = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000); + const namespaceDir = path.join(this.cacheDir, this.getCacheNamespace()); + let cleanedCount = 0; + + try { + if (!(await fs.pathExists(namespaceDir))) { + return; + } + + const entries = await fs.readdir(namespaceDir); + + for (const entry of entries) { + try { + const entryDir = path.join(namespaceDir, entry); + const metadataFile = path.join(entryDir, 'metadata.json'); + + if (await fs.pathExists(metadataFile)) { + const metadata: SnapshotMetadata = await fs.readJson(metadataFile); + const snapshotDate = new Date(metadata.timestamp); + + if (snapshotDate < cutoffDate) { + await fs.remove(entryDir); + cleanedCount++; + } + } + } catch (error) { + console.warn(`Failed to process cache entry ${entry}:`, error); + } + } + + console.log(`✅ Cleaned up ${cleanedCount} old snapshots from Turborepo cache`); + } catch (error) { + console.warn(`Failed to cleanup Turborepo cache:`, error); + } + } + + async healthCheck(): Promise { + try { + // Check if we can access the cache directory + await fs.ensureDir(this.cacheDir); + + // Test write/read operations + const testDir = path.join(this.cacheDir, this.getCacheNamespace(), 'health-check'); + const testFile = path.join(testDir, 'test.json'); + + await fs.ensureDir(testDir); + await fs.writeJson(testFile, { test: true }); + await fs.readJson(testFile); + await fs.remove(testDir); + + // Check Turborepo availability if remote cache is enabled + if (this.options.remoteCache) { + try { + await execAsync('npx turbo --version', { timeout: 5000 }); + } catch { + console.warn('Turborepo CLI not available, remote cache may not work'); + } + } + + return true; + } catch (error) { + console.error('Turborepo cache health check failed:', error); + return false; + } + } + + async getStats(): Promise<{ + totalSize: number; + snapshotCount: number; + oldestSnapshot: string; + newestSnapshot: string; + }> { + const namespaceDir = path.join(this.cacheDir, this.getCacheNamespace()); + + try { + if (!(await fs.pathExists(namespaceDir))) { + return { + totalSize: 0, + snapshotCount: 0, + oldestSnapshot: '', + newestSnapshot: '', + }; + } + + const entries = await fs.readdir(namespaceDir); + let totalSize = 0; + let snapshotCount = 0; + let oldestDate = new Date(); + let newestDate = new Date(0); + + for (const entry of entries) { + try { + const entryDir = path.join(namespaceDir, entry); + const metadataFile = path.join(entryDir, 'metadata.json'); + const snapshotFile = path.join(entryDir, 'snapshot.api.json'); + + if ((await fs.pathExists(metadataFile)) && (await fs.pathExists(snapshotFile))) { + const metadata: SnapshotMetadata = await fs.readJson(metadataFile); + const stats = await fs.stat(snapshotFile); + const snapshotDate = new Date(metadata.timestamp); + + totalSize += stats.size; + snapshotCount++; + + if (snapshotDate < oldestDate) { + oldestDate = snapshotDate; + } + if (snapshotDate > newestDate) { + newestDate = snapshotDate; + } + } + } catch (error) { + console.warn(`Failed to process cache entry ${entry}:`, error); + } + } + + return { + totalSize, + snapshotCount, + oldestSnapshot: snapshotCount > 0 ? oldestDate.toISOString() : '', + newestSnapshot: snapshotCount > 0 ? newestDate.toISOString() : '', + }; + } catch (error) { + console.warn(`Failed to get Turborepo cache stats:`, error); + return { + totalSize: 0, + snapshotCount: 0, + oldestSnapshot: '', + newestSnapshot: '', + }; + } + } + + private generateCacheKey(packageName: string, commitHash: string): string { + const safeName = packageName.replace(/[@/]/g, '_'); + return `${safeName}_${commitHash}`; + } + + private getCacheNamespace(): string { + return this.options.namespace || 'api-snapshots'; + } +} diff --git a/tools/snapi/src/types.ts b/tools/snapi/src/types.ts new file mode 100644 index 00000000000..d618cb998ee --- /dev/null +++ b/tools/snapi/src/types.ts @@ -0,0 +1,130 @@ +import { z } from 'zod'; + +// Configuration schema +export const ConfigSchema = z.object({ + packages: z.array(z.string()).optional(), + excludePackages: z.array(z.string()).default([]), + snapshotsDir: z.string().default('.api-snapshots'), + mainBranch: z.string().default('main'), + checkVersionBump: z.boolean().default(true), + suppressedChanges: z + .array( + z.object({ + package: z.string(), + changeId: z.string(), + reason: z.string(), + expires: z.string().optional(), // ISO date string + }), + ) + .default([]), + githubToken: z.string().optional(), + enableLLMAnalysis: z.boolean().default(false), + llmProvider: z.enum(['openai', 'anthropic']).default('openai'), + llmApiKey: z.string().optional(), + storage: z.any().optional(), // Storage configuration + turborepo: z + .object({ + enabled: z.boolean().default(false), + remoteCache: z + .object({ + enabled: z.boolean().default(false), + teamId: z.string().optional(), + token: z.string().optional(), + }) + .optional(), + tasks: z + .object({ + snapshot: z.string().default('api:snapshot'), + check: z.string().default('api:check'), + }) + .optional(), + }) + .optional(), + ci: z + .object({ + enableStatusChecks: z.boolean().default(true), + failOnBreakingChanges: z.boolean().default(true), + baselineStorage: z.enum(['gcs', 'turborepo']).default('turborepo'), + baselinePath: z.string().optional(), + artifactRetentionDays: z.number().default(7), + }) + .optional(), +}); + +export type Config = z.infer & { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + storage?: import('./storage/storage-manager.js').StorageManagerConfig; +}; + +// Package information +export interface PackageInfo { + name: string; + version: string; + path: string; + entrypoints: string[]; + hasExportsField: boolean; +} + +// API change types +export enum ChangeType { + BREAKING = 'breaking', + NON_BREAKING = 'non-breaking', + ADDITION = 'addition', +} + +export enum ChangeSeverity { + MAJOR = 'major', + MINOR = 'minor', + PATCH = 'patch', +} + +export interface ApiChange { + id: string; + type: ChangeType; + severity: ChangeSeverity; + category: 'export' | 'function' | 'interface' | 'type' | 'class' | 'enum'; + description: string; + beforeSnippet?: string; + afterSnippet?: string; + location?: { + file: string; + line?: number; + }; + isSuppressed?: boolean; + suppressionReason?: string; +} + +export interface PackageAnalysis { + packageName: string; + version: { + current: string; + previous: string; + }; + changes: ApiChange[]; + hasBreakingChanges: boolean; + recommendedVersionBump: ChangeSeverity; + actualVersionBump?: ChangeSeverity; + isValidBump: boolean; +} + +export interface AnalysisResult { + packages: PackageAnalysis[]; + hasBreakingChanges: boolean; + ciShouldFail: boolean; + summary: { + totalPackages: number; + packagesWithChanges: number; + breakingChanges: number; + nonBreakingChanges: number; + additions: number; + }; +} + +// API Extractor related types +export interface ApiSnapshot { + packageName: string; + version: string; + timestamp: string; + apiJsonPath: string; + extractorConfigPath: string; +} diff --git a/tools/snapi/src/utils/api-extractor.ts b/tools/snapi/src/utils/api-extractor.ts new file mode 100644 index 00000000000..9b6c3100e41 --- /dev/null +++ b/tools/snapi/src/utils/api-extractor.ts @@ -0,0 +1,366 @@ +import { Extractor, ExtractorConfig, ExtractorLogLevel } from '@microsoft/api-extractor'; +import fs from 'fs-extra'; +import { globby } from 'globby'; +import path from 'path'; + +import type { ApiSnapshot, PackageInfo } from '../types.js'; + +export interface ExtractorOptions { + packageInfo: PackageInfo; + outputDir: string; + tempDir?: string; +} + +/** + * Handles API Extractor operations for generating .api.json files + */ +export class ApiExtractorRunner { + private tempConfigDir: string; + + constructor(private workspaceRoot: string) { + this.tempConfigDir = path.join(workspaceRoot, '.tmp/api-extractor'); + } + + async generateApiSnapshot(options: ExtractorOptions): Promise { + const { packageInfo, outputDir } = options; + + await fs.ensureDir(outputDir); + await fs.ensureDir(this.tempConfigDir); + + const configPath = await this.createExtractorConfig(packageInfo); + const apiJsonPath = path.join(outputDir, `${packageInfo.name.replace('/', '__')}.api.json`); + + try { + const extractorConfig = ExtractorConfig.loadFileAndPrepare(configPath); + const extractorResult = Extractor.invoke(extractorConfig, { + localBuild: true, + showVerboseMessages: false, + messageCallback: message => { + // Filter out common warnings that aren't actionable + if (message.logLevel === ExtractorLogLevel.Warning) { + // Log warnings but don't fail the build + console.warn(`API Extractor [${packageInfo.name}]:`, message.text); + } + }, + }); + + if (!extractorResult.succeeded) { + throw new Error(`API Extractor failed for ${packageInfo.name}`); + } + + // Move the generated .api.json to our desired location + const generatedApiJsonPath = path.join(packageInfo.path, 'temp', `${path.basename(packageInfo.name)}.api.json`); + + console.log(` Looking for generated file at: ${generatedApiJsonPath}`); + if (await fs.pathExists(generatedApiJsonPath)) { + console.log(` Moving file to: ${apiJsonPath}`); + await fs.ensureDir(path.dirname(apiJsonPath)); + await fs.move(generatedApiJsonPath, apiJsonPath, { overwrite: true }); + console.log(` File moved successfully`); + + // Verify the file exists after move + if (await fs.pathExists(apiJsonPath)) { + console.log(` ✅ File verified at destination: ${apiJsonPath}`); + } else { + console.error(` ❌ File missing after move: ${apiJsonPath}`); + } + } else { + console.warn(` Generated API file not found at: ${generatedApiJsonPath}`); + } + + return { + packageName: packageInfo.name, + version: packageInfo.version, + timestamp: new Date().toISOString(), + apiJsonPath, + extractorConfigPath: configPath, + }; + } catch (error) { + throw new Error(`Failed to generate API snapshot for ${packageInfo.name}: ${error}`); + } + } + + private async createExtractorConfig(packageInfo: PackageInfo): Promise { + const configFileName = `api-extractor-${packageInfo.name.replace(/\//g, '__')}.json`; + const configPath = path.join(this.tempConfigDir, configFileName); + + const mainSourceEntrypoint = this.findMainEntrypoint(packageInfo.entrypoints); + if (!mainSourceEntrypoint) { + throw new Error(`No valid source entrypoint found for ${packageInfo.name}: ${mainSourceEntrypoint}`); + } + + // Get the declaration file path - this is the path to our generated .d.ts + const mainEntryDtsPath = await this.ensureDeclarationFile(packageInfo, mainSourceEntrypoint); + console.log(`[SNAPI Debug] Temp DTS Path from declarations: ${mainEntryDtsPath}`); + console.log(`[SNAPI Debug] Main Entry DTS Path for API Extractor: ${mainEntryDtsPath}`); + + // Ensure the path is correctly formatted for API Extractor + // This is a full replacement of the normalization logic to address persistent issues + // First, ensure the path is absolute and normalized + const absolutePath = path.isAbsolute(mainEntryDtsPath) + ? mainEntryDtsPath + : path.join(packageInfo.path, mainEntryDtsPath); + + // Remove all extensions and add a single .d.ts + let pathWithoutExt = absolutePath; + const exts = ['.d.ts', '.ts', '.tsx', '.js', '.jsx']; + + // Remove any extensions we recognize + for (const ext of exts) { + if (pathWithoutExt.endsWith(ext)) { + pathWithoutExt = pathWithoutExt.slice(0, -ext.length); + break; // Stop after removing the first extension + } + } + + // Add a single .d.ts extension + const normalizedPath = `${pathWithoutExt}.d.ts`; + + console.log(`[SNAPI Debug] Path without extension: ${pathWithoutExt}`); + console.log(`[SNAPI Debug] Normalized Main Entry DTS Path: ${normalizedPath}`); + + // Check if we fixed the issue + if (normalizedPath.includes('.d.d.ts')) { + console.error(`[SNAPI Error] Failed to fix double .d.ts extension: ${normalizedPath}`); + } + + const apiJsonOutputName = `${packageInfo.name.replace(/\//g, '__')}.api.json`; + const apiJsonTempPathInPackage = path.join('temp', apiJsonOutputName); // Relative to packageInfo.path + + // Customize the message reporting based on the package name + // For nextjs, we need to ignore wrong-input-file-type errors + const extractorMessageReporting: Record = { + default: { logLevel: ExtractorLogLevel.Warning }, + 'ae-missing-release-tag': { logLevel: ExtractorLogLevel.None }, + 'ae-unresolved-link': { logLevel: ExtractorLogLevel.None }, + }; + + // For nextjs package, suppress wrong-input-file-type errors + if (packageInfo.name === '@clerk/nextjs') { + console.log(`[SNAPI Debug] Adding special handling for @clerk/nextjs package`); + extractorMessageReporting['ae-wrong-input-file-type'] = { + logLevel: ExtractorLogLevel.None, + addToApiReportFile: false, + }; + } else { + extractorMessageReporting['ae-wrong-input-file-type'] = { + logLevel: ExtractorLogLevel.Error, + addToApiReportFile: true, + }; + } + + const config = { + $schema: 'https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json', + projectFolder: packageInfo.path, + mainEntryPointFilePath: normalizedPath, + bundledPackages: [], + + compiler: { + tsconfigFilePath: await this.findTsConfig(packageInfo.path), + }, + + apiReport: { + enabled: false, // We only need the .api.json file + }, + + docModel: { + enabled: true, + apiJsonFilePath: apiJsonTempPathInPackage, + }, + + dtsRollup: { + enabled: false, // Not needed for our use case + }, + + tsdocMetadata: { + enabled: false, + }, + + messages: { + compilerMessageReporting: { + default: { + logLevel: 'warning', + }, + }, + extractorMessageReporting: extractorMessageReporting, + tsdocMessageReporting: { + default: { + logLevel: 'none', // Focus on API changes, not docs + }, + }, + }, + }; + + await fs.writeJson(configPath, config, { spaces: 2 }); + return configPath; + } + + private findMainEntrypoint(entrypoints: string[]): string | null { + // Priority: index.ts > index.tsx > first available + const priorities = ['index.ts', 'index.tsx']; + + for (const priority of priorities) { + const found = entrypoints.find(ep => ep.endsWith(priority)); + if (found) return found; + } + + return entrypoints[0] || null; + } + + private async ensureDeclarationFile(packageInfo: PackageInfo, sourceEntrypoint: string): Promise { + const tempDtsPath = await this.generateDeclarations(packageInfo, sourceEntrypoint); + console.log(`[SNAPI Debug] Temp DTS Path from declarations: ${tempDtsPath}`); + return tempDtsPath; + } + + private async generateDeclarations(packageInfo: PackageInfo, sourceEntrypoint: string): Promise { + // First, try to find existing declarations in the dist directory + const distDir = path.join(packageInfo.path, 'dist'); + const typesDir = path.join(packageInfo.path, 'dist', 'types'); + + // Look for declarations in both dist and dist/types + const possibleDirs = [distDir, typesDir]; + + for (const dir of possibleDirs) { + if (await fs.pathExists(dir)) { + // Convert source entrypoint to a relative path if it's absolute + const relativeEntrypoint = path.isAbsolute(sourceEntrypoint) + ? path.relative(packageInfo.path, sourceEntrypoint) + : sourceEntrypoint; + + // Remove any extensions to find the base name + const baseName = relativeEntrypoint.replace(/\.(tsx?|d\.ts)$/i, ''); + + // Look for the declaration file in the dist directory + const possiblePaths = [ + path.join(dir, baseName + '.d.ts'), + path.join(dir, baseName, 'index.d.ts'), + path.join(dir, 'index.d.ts'), + ]; + + for (const possiblePath of possiblePaths) { + if (await fs.pathExists(possiblePath)) { + console.log(`[SNAPI] Found existing declaration file at: ${possiblePath}`); + return possiblePath; + } + } + } + } + + // If no existing declarations are found, create a stub + console.warn(`[SNAPI] No existing declaration files found for ${packageInfo.name}. Creating stub.`); + + const tempDtsRoot = path.join(packageInfo.path, 'temp', 'dts'); + await fs.ensureDir(tempDtsRoot); + + const relativeEntrypoint = path.isAbsolute(sourceEntrypoint) + ? path.relative(packageInfo.path, sourceEntrypoint) + : sourceEntrypoint; + + const sourceWithoutExtension = relativeEntrypoint.replace(/\.(tsx?|d\.ts)$/i, ''); + const outputEntryDtsPath = path.join(tempDtsRoot, sourceWithoutExtension + '.d.ts'); + + await this.generateStubDtsFile(outputEntryDtsPath, sourceEntrypoint, packageInfo); + + if (await fs.pathExists(outputEntryDtsPath)) { + console.log(`[SNAPI] ✅ Stub DTS created for ${packageInfo.name} at ${outputEntryDtsPath}`); + return outputEntryDtsPath; + } + + throw new Error( + `[SNAPI] Failed to find or create declaration file for ${packageInfo.name}. Target ${outputEntryDtsPath} could not be created.`, + ); + } + + private async findTsConfig(packagePath: string): Promise { + const candidates = [ + 'tsconfig.json', + 'tsconfig.build.json', + '../tsconfig.json', // Check parent directory + '../../tsconfig.json', // Check workspace root + ]; + + for (const candidate of candidates) { + const tsconfigPath = path.resolve(packagePath, candidate); + if (await fs.pathExists(tsconfigPath)) { + return tsconfigPath; + } + } + + // Create a minimal tsconfig.json as fallback + const fallbackPath = path.join(packagePath, 'tsconfig.temp.json'); + const fallbackConfig = { + compilerOptions: { + target: 'ES2020', + module: 'ESNext', + moduleResolution: 'node', + declaration: true, + strict: true, + esModuleInterop: true, + skipLibCheck: true, + forceConsistentCasingInFileNames: true, + }, + include: ['src/**/*', '*.ts', '*.tsx'], + exclude: ['node_modules', 'dist', 'build'], + }; + + await fs.writeJson(fallbackPath, fallbackConfig, { spaces: 2 }); + return fallbackPath; + } + + async cleanup(): Promise { + // Clean up temporary configuration files + if (await fs.pathExists(this.tempConfigDir)) { + await fs.remove(this.tempConfigDir); + } + + // Clean up temp directories in packages using globby + const tempDirs = await globby('packages/*/temp', { + cwd: this.workspaceRoot, + absolute: true, + onlyDirectories: true, + }); + + for (const tempDir of tempDirs) { + await fs.remove(tempDir); + } + } + + private async generateStubDtsFile( + outputPath: string, + sourceEntrypoint: string, + packageInfo: PackageInfo, + ): Promise { + // Ensure we're using a clean path with proper extension + let finalOutputPath = outputPath; + + // Remove any existing .d.ts extension + const exts = ['.d.ts', '.ts', '.tsx', '.js', '.jsx']; + for (const ext of exts) { + if (finalOutputPath.endsWith(ext)) { + finalOutputPath = finalOutputPath.slice(0, -ext.length); + break; // Stop after removing the first extension + } + } + + // Add a single .d.ts extension + finalOutputPath += '.d.ts'; + + console.log(`[SNAPI Debug] Creating stub declaration: ${finalOutputPath}`); + await fs.ensureDir(path.dirname(finalOutputPath)); + + // For the import, use a simpler approach that will work in most cases + // Extract just the filename without any extension + const sourceFileName = path.basename(sourceEntrypoint).replace(/\.(d\.ts|tsx?|jsx?)$/i, ''); + + // Create a stub that doesn't rely on complex relative paths + const stubContent = `// Fallback SNAPI generated declaration file for ${packageInfo.name} +// This is a minimal stub created because TypeScript declaration generation failed +export * from './${sourceFileName}'; +export {}; // Ensures it's treated as a module +`; + + await fs.writeFile(finalOutputPath, stubContent); + console.log(`[SNAPI Debug] Generated stub with content:\n${stubContent}`); + } +} diff --git a/tools/snapi/src/utils/git-manager.ts b/tools/snapi/src/utils/git-manager.ts new file mode 100644 index 00000000000..dfa6ef798bb --- /dev/null +++ b/tools/snapi/src/utils/git-manager.ts @@ -0,0 +1,100 @@ +import { execSync } from 'child_process'; + +/** + * Handles Git operations for the API breakage detector + */ +export class GitManager { + constructor(private workspaceRoot: string) {} + + async getCurrentBranch(): Promise { + try { + const branch = execSync('git rev-parse --abbrev-ref HEAD', { + cwd: this.workspaceRoot, + encoding: 'utf8', + }).trim(); + return branch; + } catch (error) { + throw new Error(`Failed to get current branch: ${error}`); + } + } + + async checkoutBranch(branch: string): Promise { + try { + execSync(`git checkout ${branch}`, { + cwd: this.workspaceRoot, + stdio: 'pipe', + }); + } catch (error) { + throw new Error(`Failed to checkout branch ${branch}: ${error}`); + } + } + + async getLastCommit(branch: string): Promise { + try { + const commit = execSync(`git rev-parse ${branch}`, { + cwd: this.workspaceRoot, + encoding: 'utf8', + }).trim(); + return commit; + } catch (error) { + throw new Error(`Failed to get last commit for ${branch}: ${error}`); + } + } + + async hasUncommittedChanges(): Promise { + try { + const status = execSync('git status --porcelain', { + cwd: this.workspaceRoot, + encoding: 'utf8', + }).trim(); + return status.length > 0; + } catch (error) { + throw new Error(`Failed to check Git status: ${error}`); + } + } + + async stashChanges(): Promise { + try { + const stashName = `snapi-${Date.now()}`; + execSync(`git stash push -m "${stashName}"`, { + cwd: this.workspaceRoot, + stdio: 'pipe', + }); + return stashName; + } catch (error) { + throw new Error(`Failed to stash changes: ${error}`); + } + } + + async popStash(stashName: string): Promise { + try { + // Find the stash by name and pop it + const stashList = execSync('git stash list', { + cwd: this.workspaceRoot, + encoding: 'utf8', + }); + + const stashIndex = stashList.split('\n').findIndex(line => line.includes(stashName)); + + if (stashIndex >= 0) { + execSync(`git stash pop stash@{${stashIndex}}`, { + cwd: this.workspaceRoot, + stdio: 'pipe', + }); + } + } catch (error) { + console.warn(`Failed to pop stash ${stashName}:`, error); + } + } + + async fetchBranch(branch: string): Promise { + try { + execSync(`git fetch origin ${branch}`, { + cwd: this.workspaceRoot, + stdio: 'pipe', + }); + } catch (error) { + console.warn(`Failed to fetch branch ${branch}:`, error); + } + } +} diff --git a/tools/snapi/src/utils/package-discovery.ts b/tools/snapi/src/utils/package-discovery.ts new file mode 100644 index 00000000000..856783f875b --- /dev/null +++ b/tools/snapi/src/utils/package-discovery.ts @@ -0,0 +1,237 @@ +import fs from 'fs-extra'; +import { globby } from 'globby'; +import path from 'path'; + +import type { PackageInfo } from '../types.js'; + +interface PackageJson { + name: string; + version: string; + exports?: Record; + main?: string; + module?: string; + types?: string; + private?: boolean; +} + +/** + * Discovers packages in the monorepo and extracts their public API entrypoints + */ +export class PackageDiscovery { + constructor(private workspaceRoot: string) {} + + async discoverPackages(includePackages?: string[], excludePackages: string[] = []): Promise { + const packagePaths = await this.findPackageDirectories(); + const packages: PackageInfo[] = []; + + for (const packagePath of packagePaths) { + try { + const packageInfo = await this.analyzePackage(packagePath); + + if (!packageInfo) continue; + + // Skip private packages + if (await this.isPrivatePackage(packagePath)) continue; + + // Apply filters + if (includePackages && !includePackages.includes(packageInfo.name)) continue; + if (excludePackages.includes(packageInfo.name)) continue; + + packages.push(packageInfo); + } catch (error) { + console.warn(`Failed to analyze package at ${packagePath}:`, error); + } + } + + return packages; + } + + private async findPackageDirectories(): Promise { + // Look for packages based on workspace configuration + const workspacePatterns = await this.getWorkspacePatterns(); + const packageJsonPaths = await globby( + workspacePatterns.map(pattern => `${pattern}/package.json`), + { cwd: this.workspaceRoot, absolute: true }, + ); + + return packageJsonPaths.map(p => path.dirname(p)); + } + + private async getWorkspacePatterns(): Promise { + // Check for pnpm-workspace.yaml + const pnpmWorkspace = path.join(this.workspaceRoot, 'pnpm-workspace.yaml'); + if (await fs.pathExists(pnpmWorkspace)) { + const yaml = await import('yaml'); + const content = await fs.readFile(pnpmWorkspace, 'utf8'); + const config = yaml.parse(content); + return config.packages || []; + } + + // Check for package.json workspaces + const rootPackageJson = path.join(this.workspaceRoot, 'package.json'); + if (await fs.pathExists(rootPackageJson)) { + const pkg = await fs.readJson(rootPackageJson); + if (pkg.workspaces) { + return Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces.packages || []; + } + } + + // Default fallback + return ['packages/*']; + } + + private async analyzePackage(packagePath: string): Promise { + const packageJsonPath = path.join(packagePath, 'package.json'); + + if (!(await fs.pathExists(packageJsonPath))) { + return null; + } + + const packageJson: PackageJson = await fs.readJson(packageJsonPath); + + if (!packageJson.name || !packageJson.version) { + return null; + } + + const entrypoints = await this.extractEntrypoints(packagePath, packageJson); + + if (entrypoints.length === 0) { + return null; // No TypeScript entrypoints found + } + + return { + name: packageJson.name, + version: packageJson.version, + path: packagePath, + entrypoints, + hasExportsField: !!packageJson.exports, + }; + } + + private async extractEntrypoints(packagePath: string, packageJson: PackageJson): Promise { + const entrypoints: string[] = []; + + // Priority 1: exports field (modern approach) + if (packageJson.exports) { + const exportEntrypoints = this.extractFromExports(packageJson.exports, packagePath); + // Filter to ensure only strings are added + entrypoints.push(...exportEntrypoints.filter(ep => typeof ep === 'string')); + } + + // Priority 2: traditional fields (fallback) + if (entrypoints.length === 0) { + const traditionalEntrypoints = await this.extractTraditionalEntrypoints(packagePath, packageJson); + // Filter to ensure only strings are added + entrypoints.push(...traditionalEntrypoints.filter(ep => typeof ep === 'string')); + } + + // Filter for TypeScript source files and validate they exist + const validEntrypoints: string[] = []; + + for (const entrypoint of entrypoints) { + // Double-check that entrypoint is a string + if (typeof entrypoint === 'string') { + const tsPath = await this.findTypeScriptSource(packagePath, entrypoint); + if (tsPath) { + validEntrypoints.push(tsPath); + } + } + } + + return [...new Set(validEntrypoints)]; // Remove duplicates + } + + private extractFromExports(exports: Record, _packagePath: string): string[] { + const entrypoints: string[] = []; + + const processExportValue = (value: any) => { + if (typeof value === 'string') { + entrypoints.push(value); + } else if (typeof value === 'object' && value !== null) { + // Handle conditional exports + if (typeof value.types === 'string') entrypoints.push(value.types); + if (typeof value.import === 'string') entrypoints.push(value.import); + if (typeof value.require === 'string') entrypoints.push(value.require); + if (typeof value.default === 'string') entrypoints.push(value.default); + + // For nested objects like { import: { types: "...", default: "..." } } + if (typeof value.import === 'object' && value.import !== null) { + if (typeof value.import.types === 'string') entrypoints.push(value.import.types); + if (typeof value.import.default === 'string') entrypoints.push(value.import.default); + } + if (typeof value.require === 'object' && value.require !== null) { + if (typeof value.require.types === 'string') entrypoints.push(value.require.types); + if (typeof value.require.default === 'string') entrypoints.push(value.require.default); + } + } + }; + + Object.entries(exports).forEach(([key, value]) => { + // Skip non-public exports + if (key.startsWith('./internal') || key === './package.json') return; + + processExportValue(value); + }); + + return entrypoints; + } + + private async extractTraditionalEntrypoints(_packagePath: string, packageJson: PackageJson): Promise { + const entrypoints: string[] = []; + + // Check traditional fields + if (packageJson.types) entrypoints.push(packageJson.types); + if (packageJson.main) entrypoints.push(packageJson.main); + if (packageJson.module) entrypoints.push(packageJson.module); + + return entrypoints; + } + + private async findTypeScriptSource(packagePath: string, entrypoint: string): Promise { + // Ensure entrypoint is a string (safety check) + if (typeof entrypoint !== 'string') { + console.warn(`Warning: Non-string entrypoint passed to findTypeScriptSource: ${typeof entrypoint}`, entrypoint); + return null; + } + + // Remove leading ./ + const cleanPath = entrypoint.replace(/^\.\//, ''); + + // If it's already a .ts file + if (cleanPath.endsWith('.ts') || cleanPath.endsWith('.tsx')) { + const fullPath = path.join(packagePath, cleanPath); + if (await fs.pathExists(fullPath)) { + return fullPath; + } + } + + // Try to find corresponding TypeScript source + const possibleSources = [ + cleanPath.replace(/\.d\.(m?ts)$/, '.$1'), // .d.ts -> .ts + cleanPath.replace(/\.(m?js)$/, '.$1'), // .js -> .ts + cleanPath.replace(/\.js$/, '.ts'), + cleanPath.replace(/\.mjs$/, '.mts'), + path.join('src', cleanPath.replace(/\.d\.(m?ts)$/, '.$1')), + path.join('src', cleanPath.replace(/\.(m?js)$/, '.$1')), + path.join('src', 'index.ts'), + ]; + + for (const sourcePath of possibleSources) { + const fullPath = path.join(packagePath, sourcePath); + if (await fs.pathExists(fullPath)) { + return fullPath; + } + } + + return null; + } + + private async isPrivatePackage(packagePath: string): Promise { + try { + const packageJson: PackageJson = await fs.readJson(path.join(packagePath, 'package.json')); + return !!packageJson.private; + } catch { + return false; + } + } +} diff --git a/tools/snapi/src/utils/suppression-manager.ts b/tools/snapi/src/utils/suppression-manager.ts new file mode 100644 index 00000000000..3e871448040 --- /dev/null +++ b/tools/snapi/src/utils/suppression-manager.ts @@ -0,0 +1,120 @@ +export interface SuppressedChange { + package: string; + changeId: string; + reason: string; + expires?: string; // ISO date string +} + +/** + * Manages suppressed/ignored API changes + */ +export class SuppressionManager { + private suppressions: Map; + + constructor(suppressedChanges: SuppressedChange[]) { + this.suppressions = new Map(); + + // Load suppressions, filtering out expired ones + const now = new Date(); + for (const suppression of suppressedChanges) { + if (this.isSuppressionValid(suppression, now)) { + const key = this.getSuppressionKey(suppression.package, suppression.changeId); + this.suppressions.set(key, suppression); + } + } + } + + getSuppression(packageName: string, changeId: string): SuppressedChange | undefined { + const key = this.getSuppressionKey(packageName, changeId); + return this.suppressions.get(key); + } + + isSuppressed(packageName: string, changeId: string): boolean { + return this.getSuppression(packageName, changeId) !== undefined; + } + + addSuppression(suppression: SuppressedChange): void { + if (this.isSuppressionValid(suppression, new Date())) { + const key = this.getSuppressionKey(suppression.package, suppression.changeId); + this.suppressions.set(key, suppression); + } + } + + removeSuppression(packageName: string, changeId: string): boolean { + const key = this.getSuppressionKey(packageName, changeId); + return this.suppressions.delete(key); + } + + getAllSuppressions(): SuppressedChange[] { + return Array.from(this.suppressions.values()); + } + + getValidSuppressions(): SuppressedChange[] { + const now = new Date(); + return this.getAllSuppressions().filter(suppression => this.isSuppressionValid(suppression, now)); + } + + getExpiredSuppressions(): SuppressedChange[] { + const now = new Date(); + return this.getAllSuppressions().filter(suppression => !this.isSuppressionValid(suppression, now)); + } + + private isSuppressionValid(suppression: SuppressedChange, now: Date): boolean { + if (!suppression.expires) { + return true; // No expiration = always valid + } + + try { + const expirationDate = new Date(suppression.expires); + return now <= expirationDate; + } catch { + // Invalid date format = treat as expired + return false; + } + } + + private getSuppressionKey(packageName: string, changeId: string): string { + return `${packageName}:${changeId}`; + } + + /** + * Generate a suppression entry for a given change + */ + static createSuppression( + packageName: string, + changeId: string, + reason: string, + expiresInDays?: number, + ): SuppressedChange { + let expires: string | undefined; + + if (expiresInDays && expiresInDays > 0) { + const expirationDate = new Date(); + expirationDate.setDate(expirationDate.getDate() + expiresInDays); + expires = expirationDate.toISOString(); + } + + return { + package: packageName, + changeId, + reason, + expires, + }; + } + + /** + * Clean up expired suppressions from the configuration + */ + cleanupExpiredSuppressions(): SuppressedChange[] { + const validSuppressions = this.getValidSuppressions(); + + // Update internal map + this.suppressions.clear(); + for (const suppression of validSuppressions) { + const key = this.getSuppressionKey(suppression.package, suppression.changeId); + this.suppressions.set(key, suppression); + } + + return validSuppressions; + } +} diff --git a/tools/snapi/tsconfig.json b/tools/snapi/tsconfig.json new file mode 100644 index 00000000000..53806a67b04 --- /dev/null +++ b/tools/snapi/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/tools/snapi/tsup.config.ts b/tools/snapi/tsup.config.ts new file mode 100644 index 00000000000..fc51f8ea128 --- /dev/null +++ b/tools/snapi/tsup.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/cli.ts', 'src/index.ts'], + format: ['esm'], + dts: true, + sourcemap: true, + clean: true, + splitting: false, + treeshake: true, + target: 'node22', + external: ['@google-cloud/storage', 'fs', 'path', 'crypto', 'child_process', 'util'], + esbuildOptions(options) { + options.banner = { + js: `import { createRequire as __createRequire } from 'module';const require = __createRequire(import.meta.url);`, + }; + }, +}); diff --git a/turbo.json b/turbo.json index 2a410faebd0..035a0bf98e8 100644 --- a/turbo.json +++ b/turbo.json @@ -58,7 +58,7 @@ "!node_modules/**" ], "outputLogs": "new-only", - "outputs": ["*/package.json", "dist/**", "scripts/**"] + "outputs": ["*/package.json", "dist/**", "scripts/**", ".next/**", "!.next/cache/**"] }, "build:declarations": { "dependsOn": ["build"], @@ -367,6 +367,26 @@ "env": ["CLEANUP", "DEBUG", "E2E_*", "INTEGRATION_INSTANCE_KEYS"], "inputs": ["integration/**"], "outputLogs": "new-only" + }, + "api:snapshot": { + "dependsOn": ["build"], + "inputs": [ + "src/**/*.ts", + "src/**/*.tsx", + "package.json", + "tsconfig.json", + "api-extractor.json", + "../../snapi.config.json" + ], + "outputs": ["../../.api-snapshots/current/**"], + "cache": true, + "persistent": false + }, + "api:check": { + "dependsOn": ["api:snapshot"], + "inputs": ["src/**/*.ts", "src/**/*.tsx", "package.json", ".api-snapshots/baseline/**"], + "outputs": ["api-changes-report.md", ".api-snapshots/current/**"], + "cache": false } } }