From 5598293607bd42f1b37fba829e08a0731e830fb7 Mon Sep 17 00:00:00 2001 From: Sergii Gnatiuk Date: Wed, 11 Feb 2026 01:35:08 +0200 Subject: [PATCH] migrated to TS --- .github/workflows/build.yaml | 16 +- .gitignore | 3 + action.yaml | 157 +- package-lock.json | 2558 +++++++++++++++++ package.json | 25 + src/build-comment-body.js | 153 - src/build-comment-body.ts | 170 ++ src/create-check-runs.js | 79 - src/create-check-runs.ts | 111 + src/generate-badges.js | 55 - src/generate-badges.ts | 68 + src/main.ts | 111 + src/pr-comments.ts | 73 + src/read-summaries.js | 21 - src/read-summaries.ts | 32 + src/suppression.ts | 37 + src/types/coverage.ts | 38 + test/build-comment-body.test.ts | 274 ++ test/create-check-runs.test.ts | 205 ++ ...delta-coverage-functionalTest-summary.json | 1 + test/data/delta-coverage-test-summary.json | 1 + test/generate-badges.test.ts | 113 + test/main.test.ts | 190 ++ test/pr-comments.test.ts | 153 + test/read-summaries.test.ts | 106 + test/suppression.test.ts | 101 + tsconfig.json | 19 + vitest.config.ts | 12 + 28 files changed, 4419 insertions(+), 463 deletions(-) create mode 100644 package-lock.json create mode 100644 package.json delete mode 100644 src/build-comment-body.js create mode 100644 src/build-comment-body.ts delete mode 100644 src/create-check-runs.js create mode 100644 src/create-check-runs.ts delete mode 100644 src/generate-badges.js create mode 100644 src/generate-badges.ts create mode 100644 src/main.ts create mode 100644 src/pr-comments.ts delete mode 100644 src/read-summaries.js create mode 100644 src/read-summaries.ts create mode 100644 src/suppression.ts create mode 100644 src/types/coverage.ts create mode 100644 test/build-comment-body.test.ts create mode 100644 test/create-check-runs.test.ts create mode 100644 test/data/delta-coverage-functionalTest-summary.json create mode 100644 test/data/delta-coverage-test-summary.json create mode 100644 test/generate-badges.test.ts create mode 100644 test/main.test.ts create mode 100644 test/pr-comments.test.ts create mode 100644 test/read-summaries.test.ts create mode 100644 test/suppression.test.ts create mode 100644 tsconfig.json create mode 100644 vitest.config.ts diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 2f299e8..8ae99eb 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -27,9 +27,23 @@ jobs: with: fetch-depth: 0 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test + + - name: Build + run: npm run build + - name: Publish Delta Coverage id: render-delta-coverage - uses: gw-kit/delta-coverage-action@v1 + uses: ./ with: summary-report-base-path: 'test/data/' diff --git a/.gitignore b/.gitignore index 485dee6..8a0091d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ .idea +node_modules/ +coverage/ +.DS_Store diff --git a/action.yaml b/action.yaml index f2bdcbe..174c641 100644 --- a/action.yaml +++ b/action.yaml @@ -33,159 +33,8 @@ inputs: outputs: badges-dir: description: 'Directory with generated badges.' - value: ${{ steps.generate-badges.outputs.badges-dir }} + value: 'badges/' runs: - using: "composite" - - steps: - - - name: Build All Summaries - id: all-summaries - uses: actions/github-script@v7 - with: - github-token: ${{ inputs.github-token }} - script: | - const readSummaries = require('${{ github.action_path }}/src/read-summaries.js'); - const summariesBaseDir = `${{ inputs.summary-report-base-path }}`; - const deltaSummariesFile = await readSummaries({ - isFullCoverageMode: false, - baseSummariesPath: summariesBaseDir, - }); - const fullCovSummariesFile = await readSummaries({ - isFullCoverageMode: true, - baseSummariesPath: summariesBaseDir, - }); - core.setOutput('delta', deltaSummariesFile); - core.setOutput('full', fullCovSummariesFile); - - - name: Install Badge Dependencies - continue-on-error: true - shell: bash - run: | - npm install gradient-badge - mv ./node_modules "${{ github.action_path }}/src/" - - - name: Generate Badges - id: generate-badges - continue-on-error: true - uses: actions/github-script@v7 - with: - script: | - const generateBadges = require('${{ github.action_path }}/src/generate-badges.js'); - await generateBadges({ - summariesFile: '${{ steps.all-summaries.outputs.full }}', - core: core - }); - - - name: Fetch PR Labels - id: fetch-labels - if: ${{ github.event_name == 'pull_request' }} - shell: 'bash' - env: - GH_TOKEN: ${{ inputs.github-token }} - PR_ENDPOINT: "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}" - run: | - LABELS=$(gh api ${PR_ENDPOINT} --jq '.labels[].name' || echo "") - LABELS_CLEAN=$(echo "$LABELS" | tr '\n' ',' | sed 's/,$//') - echo "labels=$LABELS_CLEAN" >> $GITHUB_OUTPUT - - - name: Check for Suppression - id: check-suppress - uses: actions/github-script@v7 - with: - script: | - const labels = '${{ steps.fetch-labels.outputs.labels }}'; - const suppressedByLabel = labels.includes('suppress-coverage'); - const suppressedByInput = '${{ inputs.suppress-check-failures }}' === 'true'; - const resolution = suppressedByLabel || suppressedByInput; - core.info(`Is suppress=${resolution} : by label=${suppressedByLabel}, by input=${suppressedByInput}`); - core.setOutput('suppress', resolution); - - - name: Create Check Run - id: create-check-runs - uses: actions/github-script@v7 - with: - github-token: ${{ inputs.github-token }} - script: | - const createCheckRuns = require('${{ github.action_path }}/src/create-check-runs.js'); - const extraRenderScript = (view) => { - const script = ${{ inputs.check-run-extra-render-script || '(v) => ""' }}; - try { return script(view) } catch(e) { - return `Error in custom script: ${e}` - }; - }; - const checkRuns = await createCheckRuns({ - summaryReportPath: `${{ steps.all-summaries.outputs.delta }}`, - ignoreCoverageFailure: ${{ steps.check-suppress.outputs.suppress }}, - core: core, - context: context, - github: github, - headSha: `${{ github.event.pull_request.head.sha || github.sha }}`, - externalId: `${{ inputs.external-id }}`, - summaryExtraFun: extraRenderScript - }); - core.setOutput('check-runs', checkRuns); - - - name: Create Comment Marker - id: comment-marker - if: ${{ inputs.title != '' }} - shell: bash - run: echo 'result=${{ format('', inputs.title) }}' >> $GITHUB_OUTPUT - - - name: Find Existing Comment In PR - id: find-comment - if: ${{ steps.comment-marker.outputs.result && github.event_name == 'pull_request' }} - uses: actions/github-script@v7 - with: - github-token: ${{ inputs.github-token }} - script: | - const response = await github.rest.issues.listComments({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - }) - const comment = response.data.find(it => - it.body.includes('${{ steps.comment-marker.outputs.result }}') - ) - if (comment) { - console.log(`Comment found: ${comment.id}`); - return comment.id - } - - - id: build-message - if: ${{ github.event_name == 'pull_request' }} - uses: actions/github-script@v7 - with: - github-token: ${{ inputs.github-token }} - script: | - const createCommentMsg = require('${{ github.action_path }}/src/build-comment-body.js'); - return createCommentMsg({ - checkRunsContent: `${{ steps.create-check-runs.outputs.check-runs }}`, - commentTitle: '${{ inputs.title }}', - commentMarker: `${{ steps.comment-marker.outputs.result }}`, - core: core - }); - - - name: Update or Create Comment - uses: actions/github-script@v7 - if: ${{ github.event_name == 'pull_request' }} - with: - github-token: ${{ inputs.github-token }} - script: | - if ('${{ steps.find-comment.outputs.result }}') { - github.rest.issues.updateComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: '${{ steps.find-comment.outputs.result }}', - body: ${{ steps.build-message.outputs.result }} - }) - } else { - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: ${{ steps.build-message.outputs.result }} - }) - } + using: 'node20' + main: 'dist/index.js' diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d9fdf95 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2558 @@ +{ + "name": "delta-coverage-action", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "delta-coverage-action", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@actions/core": "^1.10.1", + "@actions/github": "^6.0.0", + "gradient-badge": "^1.3.0" + }, + "devDependencies": { + "@types/node": "^20.11.0", + "@vercel/ncc": "^0.38.3", + "@vitest/coverage-v8": "^2.1.0", + "typescript": "^5.7.0", + "vitest": "^2.1.0" + } + }, + "node_modules/@actions/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", + "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", + "license": "MIT", + "dependencies": { + "@actions/exec": "^1.1.1", + "@actions/http-client": "^2.0.1" + } + }, + "node_modules/@actions/exec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "license": "MIT", + "dependencies": { + "@actions/io": "^1.0.1" + } + }, + "node_modules/@actions/github": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@actions/github/-/github-6.0.1.tgz", + "integrity": "sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw==", + "license": "MIT", + "dependencies": { + "@actions/http-client": "^2.2.0", + "@octokit/core": "^5.0.1", + "@octokit/plugin-paginate-rest": "^9.2.2", + "@octokit/plugin-rest-endpoint-methods": "^10.4.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", + "undici": "^5.28.5" + } + }, + "node_modules/@actions/http-client": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz", + "integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==", + "license": "MIT", + "dependencies": { + "tunnel": "^0.0.6", + "undici": "^5.25.4" + } + }, + "node_modules/@actions/io": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", + "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", + "license": "MIT" + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz", + "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", + "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", + "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", + "license": "MIT", + "dependencies": { + "@octokit/request": "^8.4.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.2.tgz", + "integrity": "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz", + "integrity": "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, + "node_modules/@octokit/request": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", + "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^9.0.6", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", + "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.33", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz", + "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vercel/ncc": { + "version": "0.38.4", + "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.4.tgz", + "integrity": "sha512-8LwjnlP39s08C08J5NstzriPvW1SP8Zfpp1BvC2sI35kPeZnHfxVkCwu4/+Wodgnd60UtT1n8K8zw+Mp7J9JmQ==", + "dev": true, + "license": "MIT", + "bin": { + "ncc": "dist/ncc/cli.js" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.9.tgz", + "integrity": "sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^0.2.3", + "debug": "^4.3.7", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.12", + "magicast": "^0.3.5", + "std-env": "^3.8.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "2.1.9", + "vitest": "2.1.9" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/badgen": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/badgen/-/badgen-3.2.3.tgz", + "integrity": "sha512-svDuwkc63E/z0ky3drpUppB83s/nlgDciH9m+STwwQoWyq7yCgew1qEfJ+9axkKdNq7MskByptWUN9j1PGMwFA==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "license": "Apache-2.0" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "license": "ISC" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "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" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gradient-badge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/gradient-badge/-/gradient-badge-1.3.1.tgz", + "integrity": "sha512-OvTFHZJSQ+fR7zWl6xz5PvniAfKRsHBy98NZ+kAWOnfmP27lt8wMA4oUvsQp0ppNET+2qE0oEnna2s3SiiqaVA==", + "license": "MIT", + "dependencies": { + "badgen": "^3.2.1" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "license": "ISC" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "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 + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@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.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "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 + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f8c18e2 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "delta-coverage-action", + "version": "1.2.0", + "description": "GitHub Action for Delta Coverage reports", + "main": "dist/index.js", + "scripts": { + "build": "rm -rf dist && ncc build src/main.ts -o dist --source-map --license licenses.txt", + "compile": "tsc", + "test": "vitest run --coverage", + "test:watch": "vitest" + }, + "dependencies": { + "@actions/core": "^1.10.1", + "@actions/github": "^6.0.0", + "gradient-badge": "^1.3.0" + }, + "devDependencies": { + "@types/node": "^20.11.0", + "@vercel/ncc": "^0.38.3", + "@vitest/coverage-v8": "^2.1.0", + "typescript": "^5.7.0", + "vitest": "^2.1.0" + }, + "license": "MIT" +} diff --git a/src/build-comment-body.js b/src/build-comment-body.js deleted file mode 100644 index f1d9c33..0000000 --- a/src/build-comment-body.js +++ /dev/null @@ -1,153 +0,0 @@ -const { GITHUB_SERVER_URL, GITHUB_REPOSITORY, GITHUB_RUN_ID, GITHUB_RUN_NUMBER, GITHUB_RUN_ATTEMPT } = process.env; - -module.exports = (ctx) => { - - const NO_VALUE = -1; - const ENTITIES = ['INSTRUCTION', 'BRANCH', 'LINE']; - const HEADERS = ['Check', 'Expected', 'Entity', 'Actual']; - - const NO_COVERAGE_TEXT = 'No%20diff'; - const SUCCESS_COLOR = '7AB56D'; - const FAILURE_COLOR = 'C4625A'; - const NO_COVERAGE_COLOR = '777777'; - - const TOOLTIPS = new Map([ - ['INSTRUCTION', 'The Java bytecode instructions executed during testing'], - ['BRANCH', 'The branches in conditional statements like if, switch, or loops that are executed.'], - ['LINE', 'The source code lines covered by the tests.'] - ]); - - const workflowUrl = `${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}`; - const workflowNum = `${GITHUB_RUN_NUMBER}.${GITHUB_RUN_ATTEMPT}` - - const buildViewSummaryData = (checkRun) => { - const entitiesRules = checkRun.coverageRules?.entitiesRules || []; - const entityToExpectedPercents = new Map(); - for (const [entityName, entityConfig] of Object.entries(entitiesRules)) { - const percents = entityConfig.minCoverageRatio * 100; - entityToExpectedPercents.set(entityName, percents); - } - - const entityToActualPercents = checkRun.coverageInfo.reduce((acc, it) => { - if (it.total !== 0) { - acc.set(it.coverageEntity, it.percents); - } - return acc; - }, new Map()); - - const shouldFailOnViolations = checkRun.coverageRules?.failOnViolation || false; - return ENTITIES.map((entity) => { - const expectedPercents = entityToExpectedPercents.get(entity) || NO_VALUE; - - const actualPercents = entityToActualPercents.get(entity) !== undefined - ? entityToActualPercents.get(entity) - : NO_VALUE; - - const isFailed = shouldFailOnViolations - && actualPercents > NO_VALUE - && actualPercents < expectedPercents; - return { - entity, - isFailed, - expected: expectedPercents, - actual: actualPercents - } - }); - }; - - const buildCheckRunForViewText = (checkRun) => { - const buildProgressImg = (entityData) => { - let imageLink; - if (entityData.actual > NO_VALUE ) { - const color = entityData.actual < entityData.expected ? FAILURE_COLOR : SUCCESS_COLOR; - const actualInteger = Math.floor(entityData.actual); - imageLink = `https://progress-bar.xyz/${actualInteger}/?progress_color=${color}`; - } else { - imageLink = `https://progress-bar.xyz/100/?show_text=false&width=38` - +`&title=${NO_COVERAGE_TEXT}&color=${NO_COVERAGE_COLOR}&progress_color=${NO_COVERAGE_COLOR}`; - } - return ``; - } - - const buildExpectedValue = (entityData) => { - return (entityData.expected > NO_VALUE) ? `🎯 ${entityData.expected}% 🎯` : ''; - } - - const buildCoverageValueColumnHtml = (entityData, entityIndex, shouldFold, valueProvider) => { - if (shouldFold && entityIndex > 0) { - return ''; - } - const value = valueProvider(entityData); - const rowSpanAttr = (shouldFold && entityIndex === 0) ? `rowspan=3` : ''; - return `${value}`; - } - - const obtainUniqueValuesSet = (viewSummaryData, valueProvider) => { - const allExpected = viewSummaryData.map(it => valueProvider(it)); - return new Set(allExpected); - } - - const viewSummaryData = buildViewSummaryData(checkRun); - - const hasFailure = viewSummaryData.some(it => it.isFailed); - const statusSymbol = hasFailure ? 'πŸ”΄' : '🟒'; - const viewCellValue = ` - ${statusSymbol} ${checkRun.viewName} - `.trim(); - - const foldExpectedColumn = obtainUniqueValuesSet(viewSummaryData, it => it.expected).size === 1; - - const actualUniqueValues = obtainUniqueValuesSet(viewSummaryData, it => it.actual); - const foldActualColumn = actualUniqueValues.size === 1 - && (foldExpectedColumn || actualUniqueValues.has(NO_VALUE)); - - return viewSummaryData.map((entityData, index) => { - const viewCellInRow = (index === 0) ? viewCellValue : ''; - - const actualColumnHtml = buildCoverageValueColumnHtml(entityData, index, foldActualColumn, buildProgressImg); - const ruleColumnHtml = buildCoverageValueColumnHtml(entityData, index, foldExpectedColumn, buildExpectedValue); - - const toolTipText = TOOLTIPS.get(entityData.entity) || ''; - - return ` - ${viewCellInRow} - ${ruleColumnHtml} - ${entityData.entity} - ${actualColumnHtml} - `.trim().replace(/^ +/gm, ''); - }).join('\n'); - } - - const renderHeaders = () => { - return '' + HEADERS.map(it => `${it}`).join('\n') + ''; - } - - const buildRunMetaText = () => { - const workflowRunDate = new Date(); - const options = { month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }; - - const workflowRunLink = `[Run ${workflowNum}](${workflowUrl})`; - const formattedDate = workflowRunDate.toLocaleString('en-US', options); - return `${workflowRunLink} | \`${formattedDate}\``; - }; - - const checkRuns = JSON.parse(ctx.checkRunsContent); - let summaryBuffer = ctx.core.summary - .addHeading(ctx.commentTitle, '2') - .addRaw(ctx.commentMarker, true) - .addEOL() - .addRaw(``) - .addRaw(renderHeaders()); - - checkRuns.forEach(checkRun => { - const runText = buildCheckRunForViewText(checkRun); - summaryBuffer = summaryBuffer.addRaw(runText, true); - }); - - return summaryBuffer - .addRaw(`
`) - .addEOL() - .addEOL() - .addRaw(buildRunMetaText()) - .stringify() -}; diff --git a/src/build-comment-body.ts b/src/build-comment-body.ts new file mode 100644 index 0000000..0d64337 --- /dev/null +++ b/src/build-comment-body.ts @@ -0,0 +1,170 @@ +import { CheckRunResult, CoverageEntity } from './types/coverage'; + +const NO_VALUE = -1; +const ENTITIES: CoverageEntity[] = ['INSTRUCTION', 'BRANCH', 'LINE']; +const HEADERS = ['Check', 'Expected', 'Entity', 'Actual']; + +const NO_COVERAGE_TEXT = 'No%20diff'; +const SUCCESS_COLOR = '7AB56D'; +const FAILURE_COLOR = 'C4625A'; +const NO_COVERAGE_COLOR = '777777'; + +const TOOLTIPS = new Map([ + ['INSTRUCTION', 'The Java bytecode instructions executed during testing'], + ['BRANCH', 'The branches in conditional statements like if, switch, or loops that are executed.'], + ['LINE', 'The source code lines covered by the tests.'], +]); + +interface EntityData { + entity: CoverageEntity; + isFailed: boolean; + expected: number; + actual: number; +} + +interface SummaryBuilder { + addHeading(text: string, level: string): SummaryBuilder; + addRaw(text: string, addEOL?: boolean): SummaryBuilder; + addEOL(): SummaryBuilder; + stringify(): string; +} + +export interface BuildCommentBodyParams { + checkRunsContent: string; + commentTitle: string; + commentMarker: string; + core: { summary: SummaryBuilder }; + env?: Record; +} + +function buildViewSummaryData(checkRun: CheckRunResult): EntityData[] { + const entitiesRules = checkRun.coverageRules?.entitiesRules || {}; + const entityToExpectedPercents = new Map(); + for (const [entityName, entityConfig] of Object.entries(entitiesRules)) { + const percents = entityConfig.minCoverageRatio * 100; + entityToExpectedPercents.set(entityName, percents); + } + + const entityToActualPercents = checkRun.coverageInfo.reduce((acc, it) => { + if (it.total !== 0) { + acc.set(it.coverageEntity, it.percents); + } + return acc; + }, new Map()); + + const shouldFailOnViolations = checkRun.coverageRules?.failOnViolation || false; + return ENTITIES.map((entity) => { + const expectedPercents = entityToExpectedPercents.get(entity) || NO_VALUE; + const actualPercents = entityToActualPercents.get(entity) !== undefined + ? entityToActualPercents.get(entity)! + : NO_VALUE; + + const isFailed = shouldFailOnViolations + && actualPercents > NO_VALUE + && actualPercents < expectedPercents; + return { entity, isFailed, expected: expectedPercents, actual: actualPercents }; + }); +} + +function buildProgressImg(entityData: EntityData): string { + let imageLink: string; + if (entityData.actual > NO_VALUE) { + const color = entityData.actual < entityData.expected ? FAILURE_COLOR : SUCCESS_COLOR; + const actualInteger = Math.floor(entityData.actual); + imageLink = `https://progress-bar.xyz/${actualInteger}/?progress_color=${color}`; + } else { + imageLink = `https://progress-bar.xyz/100/?show_text=false&width=38` + + `&title=${NO_COVERAGE_TEXT}&color=${NO_COVERAGE_COLOR}&progress_color=${NO_COVERAGE_COLOR}`; + } + return ``; +} + +function buildExpectedValue(entityData: EntityData): string { + return (entityData.expected > NO_VALUE) ? `🎯 ${entityData.expected}% 🎯` : ''; +} + +function buildCoverageValueColumnHtml( + entityData: EntityData, + entityIndex: number, + shouldFold: boolean, + valueProvider: (data: EntityData) => string, +): string { + if (shouldFold && entityIndex > 0) { + return ''; + } + const value = valueProvider(entityData); + const rowSpanAttr = (shouldFold && entityIndex === 0) ? 'rowspan=3' : ''; + return `${value}`; +} + +function obtainUniqueValuesSet(viewSummaryData: EntityData[], valueProvider: (data: EntityData) => T): Set { + const allValues = viewSummaryData.map(it => valueProvider(it)); + return new Set(allValues); +} + +function buildCheckRunForViewText(checkRun: CheckRunResult): string { + const viewSummaryData = buildViewSummaryData(checkRun); + + const hasFailure = viewSummaryData.some(it => it.isFailed); + const statusSymbol = hasFailure ? 'πŸ”΄' : '🟒'; + const viewCellValue = `${statusSymbol} ${checkRun.viewName}`; + + const foldExpectedColumn = obtainUniqueValuesSet(viewSummaryData, it => it.expected).size === 1; + const actualUniqueValues = obtainUniqueValuesSet(viewSummaryData, it => it.actual); + const foldActualColumn = actualUniqueValues.size === 1 + && (foldExpectedColumn || actualUniqueValues.has(NO_VALUE)); + + return viewSummaryData.map((entityData, index) => { + const viewCellInRow = (index === 0) ? viewCellValue : ''; + const actualColumnHtml = buildCoverageValueColumnHtml(entityData, index, foldActualColumn, buildProgressImg); + const ruleColumnHtml = buildCoverageValueColumnHtml(entityData, index, foldExpectedColumn, buildExpectedValue); + const toolTipText = TOOLTIPS.get(entityData.entity) || ''; + + return ` +${viewCellInRow} +${ruleColumnHtml} +${entityData.entity} +${actualColumnHtml} +`.trim().replace(/^ +/gm, ''); + }).join('\n'); +} + +function renderHeaders(): string { + return '' + HEADERS.map(it => `${it}`).join('\n') + ''; +} + +export function buildCommentBody(params: BuildCommentBodyParams): string { + const env = params.env || process.env; + const { GITHUB_SERVER_URL, GITHUB_REPOSITORY, GITHUB_RUN_ID, GITHUB_RUN_NUMBER, GITHUB_RUN_ATTEMPT } = env; + + const workflowUrl = `${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}`; + const workflowNum = `${GITHUB_RUN_NUMBER}.${GITHUB_RUN_ATTEMPT}`; + + const buildRunMetaText = (): string => { + const workflowRunDate = new Date(); + const options: Intl.DateTimeFormatOptions = { month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }; + const workflowRunLink = `[Run ${workflowNum}](${workflowUrl})`; + const formattedDate = workflowRunDate.toLocaleString('en-US', options); + return `${workflowRunLink} | \`${formattedDate}\``; + }; + + const checkRuns: CheckRunResult[] = JSON.parse(params.checkRunsContent); + let summaryBuffer = params.core.summary + .addHeading(params.commentTitle, '2') + .addRaw(params.commentMarker, true) + .addEOL() + .addRaw('') + .addRaw(renderHeaders()); + + checkRuns.forEach(checkRun => { + const runText = buildCheckRunForViewText(checkRun); + summaryBuffer = summaryBuffer.addRaw(runText, true); + }); + + return summaryBuffer + .addRaw('
') + .addEOL() + .addEOL() + .addRaw(buildRunMetaText()) + .stringify(); +} diff --git a/src/create-check-runs.js b/src/create-check-runs.js deleted file mode 100644 index 67b05e0..0000000 --- a/src/create-check-runs.js +++ /dev/null @@ -1,79 +0,0 @@ -module.exports = async (ctx) => { - const fs = require('fs'); - - const buildPathToReport = (viewName) => { - return `build/reports/coverage-reports/delta-coverage/${viewName}/report.md`; - }; - - const viewHasViolations = (view) => { - return view.verifications.length > 0; - }; - - const readViewMarkdownReport = (view) => { - const reportPath = buildPathToReport(view.view); - try { - return fs.readFileSync(reportPath, 'utf8'); - } catch (e) { - return `NO REPORT by path: ${reportPath}`; - } - } - - const capitalize = (s) => { - return s.charAt(0).toUpperCase() + s.slice(1); - } - - const computeViewConclusion = (view) => { - return !ctx.ignoreCoverageFailure && viewHasViolations(view) - ? 'failure' - : 'success'; - } - - const createCheckRun = async (view) => { - const conclusion = computeViewConclusion(view); - const viewName = capitalize(view.view); - const summaryExtra = ctx.summaryExtraFun ? ctx.summaryExtraFun(view) : ''; - const summary = `${readViewMarkdownReport(view)}\n\n\n${summaryExtra}`; - - const response = await ctx.github.rest.checks.create({ - owner: ctx.context.repo.owner, - repo: ctx.context.repo.repo, - name: `πŸ“ˆ${viewName} Coverage`, - head_sha: ctx.headSha, - status: 'completed', - external_id: ctx.externalId, - conclusion: conclusion, - output: { - title: `${viewName} Coverage`, - summary: summary, - } - }); - return { - viewName: viewName, - verifications: view.verifications, - coverageRules: view.coverageRulesConfig, - url: response.data.html_url, - conclusion: conclusion, - coverageInfo: view.coverageInfo - } - } - - const createAnnotations = (view) => { - const hasViolations = viewHasViolations(view); - if (hasViolations) { - const viewName = capitalize(view.view); - const violations = view.verifications.map(it => it.violation).join(';\n'); - const msg = `[${viewName}]: Code Coverage check failed:\n${violations}`; - ctx.core.error(msg); - } - } - - const reportContent = fs.readFileSync(ctx.summaryReportPath); - const summaryArray = JSON.parse(reportContent); - const checkRuns = []; - for (const view of summaryArray) { - createAnnotations(view); - const checkRun = await createCheckRun(view); - checkRuns.push(checkRun); - } - return checkRuns; -}; diff --git a/src/create-check-runs.ts b/src/create-check-runs.ts new file mode 100644 index 0000000..86cda13 --- /dev/null +++ b/src/create-check-runs.ts @@ -0,0 +1,111 @@ +import * as fs from 'fs'; +import { CoverageSummary, CheckRunResult } from './types/coverage'; + +export interface GitHubClient { + rest: { + checks: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + create(params: any): Promise<{ data: { html_url: string | null } }>; + }; + }; +} + +export interface GitHubContext { + repo: { owner: string; repo: string }; +} + +export interface CoreLogger { + error(message: string): void; +} + +export interface CreateCheckRunsParams { + summaryReportPath: string; + ignoreCoverageFailure: boolean; + core: CoreLogger; + context: GitHubContext; + github: GitHubClient; + headSha: string; + externalId: string; + summaryExtraFun?: (view: CoverageSummary) => string; +} + +function buildPathToReport(viewName: string): string { + return `build/reports/coverage-reports/delta-coverage/${viewName}/report.md`; +} + +function viewHasViolations(view: CoverageSummary): boolean { + return view.verifications.length > 0; +} + +function readViewMarkdownReport(view: CoverageSummary): string { + const reportPath = buildPathToReport(view.view); + try { + return fs.readFileSync(reportPath, 'utf8'); + } catch { + return `NO REPORT by path: ${reportPath}`; + } +} + +function capitalize(s: string): string { + return s.charAt(0).toUpperCase() + s.slice(1); +} + +function computeViewConclusion(view: CoverageSummary, ignoreCoverageFailure: boolean): 'success' | 'failure' { + return !ignoreCoverageFailure && viewHasViolations(view) ? 'failure' : 'success'; +} + +async function createCheckRun( + view: CoverageSummary, + params: CreateCheckRunsParams, +): Promise { + const conclusion = computeViewConclusion(view, params.ignoreCoverageFailure); + const viewName = capitalize(view.view); + const summaryExtra = params.summaryExtraFun ? params.summaryExtraFun(view) : ''; + const summary = `${readViewMarkdownReport(view)}\n\n\n${summaryExtra}`; + + const response = await params.github.rest.checks.create({ + owner: params.context.repo.owner, + repo: params.context.repo.repo, + name: `πŸ“ˆ${viewName} Coverage`, + head_sha: params.headSha, + status: 'completed', + external_id: params.externalId, + conclusion, + output: { + title: `${viewName} Coverage`, + summary, + }, + }); + + return { + viewName, + verifications: view.verifications, + coverageRules: view.coverageRulesConfig, + url: response.data.html_url || '', + conclusion, + coverageInfo: view.coverageInfo, + }; +} + +function createAnnotations(view: CoverageSummary, core: CoreLogger): void { + if (viewHasViolations(view)) { + const viewName = capitalize(view.view); + const violations = view.verifications.map(it => it.violation).join(';\n'); + const msg = `[${viewName}]: Code Coverage check failed:\n${violations}`; + core.error(msg); + } +} + +export async function createCheckRuns(params: CreateCheckRunsParams): Promise { + const reportContent = fs.readFileSync(params.summaryReportPath, 'utf8'); + const summaryArray: CoverageSummary[] = JSON.parse(reportContent); + const checkRuns: CheckRunResult[] = []; + + for (const view of summaryArray) { + createAnnotations(view, params.core); + const checkRun = await createCheckRun(view, params); + checkRuns.push(checkRun); + } + + return checkRuns; +} diff --git a/src/generate-badges.js b/src/generate-badges.js deleted file mode 100644 index 6a27fa7..0000000 --- a/src/generate-badges.js +++ /dev/null @@ -1,55 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const gradientBadge = require('gradient-badge'); - -const badgesOutputDir = 'badges/'; - -const secondColor = '#117efa'; // blue -const firstColors = [ - '#ea00ff', // purple - '#16a41f', // green - '#16019f', // blue - '#ff1500', // red - '#ffcc00', // yellow -]; - -const normalizeColor = (color) => color.replace('#', ''); - -const mapToBadgeInputs = (index, summary) => { - const lineCoverage = summary.coverageInfo.find(coverage => coverage.coverageEntity === 'LINE'); - const firstColor = firstColors[index % firstColors.length]; - return { - subject: summary.view, - status: `${lineCoverage.percents}%`, - gradient: [normalizeColor(firstColor), normalizeColor(secondColor)], - }; -}; - -module.exports = async (ctx) => { - const { core, summariesFile } = ctx; - - fs.mkdirSync(badgesOutputDir, {recursive: true}); - - const summaries = JSON.parse(fs.readFileSync(summariesFile, 'utf8')); - summaries - .sort((a, b) => a.view.localeCompare(b.view)) - .map((summary, index) => { - return { - view: summary.view, - badgeInputs: mapToBadgeInputs(index, summary), - } - }) - .map(viewBadgeData => { - return { - view: viewBadgeData.view, - file: path.join(badgesOutputDir, `${viewBadgeData.view}.svg`), - badgeContent: gradientBadge(viewBadgeData.badgeInputs), - }; - }).forEach(badge => { - fs.writeFileSync(badge.file, badge.badgeContent); - core.info(`🏷️ Generated badge for ${badge.view} at ${badge.file}`); - core.setOutput(badge.view, badge.file); - }); - - core.setOutput('badges-dir', badgesOutputDir); -}; diff --git a/src/generate-badges.ts b/src/generate-badges.ts new file mode 100644 index 0000000..5272d36 --- /dev/null +++ b/src/generate-badges.ts @@ -0,0 +1,68 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { CoverageSummary } from './types/coverage'; + +const BADGES_OUTPUT_DIR = 'badges/'; + +const SECOND_COLOR = '#117efa'; // blue +const FIRST_COLORS = [ + '#ea00ff', // purple + '#16a41f', // green + '#16019f', // dark blue + '#ff1500', // red + '#ffcc00', // yellow +]; + +export interface GenerateBadgesParams { + summariesFile: string; + core: { + info(message: string): void; + setOutput(name: string, value: string): void; + }; + outputDir?: string; +} + +interface BadgeInputs { + subject: string; + status: string; + gradient: [string, string]; +} + +const normalizeColor = (color: string): string => color.replace('#', ''); + +function mapToBadgeInputs(index: number, summary: CoverageSummary): BadgeInputs { + const lineCoverage = summary.coverageInfo.find(c => c.coverageEntity === 'LINE'); + const firstColor = FIRST_COLORS[index % FIRST_COLORS.length]; + return { + subject: summary.view, + status: `${lineCoverage!.percents}%`, + gradient: [normalizeColor(firstColor), normalizeColor(SECOND_COLOR)], + }; +} + +export function generateBadges(params: GenerateBadgesParams): void { + const { core, summariesFile, outputDir = BADGES_OUTPUT_DIR } = params; + const gradientBadge = require('gradient-badge'); + + fs.mkdirSync(outputDir, { recursive: true }); + + const summaries: CoverageSummary[] = JSON.parse(fs.readFileSync(summariesFile, 'utf8')); + summaries + .sort((a, b) => a.view.localeCompare(b.view)) + .map((summary, index) => ({ + view: summary.view, + badgeInputs: mapToBadgeInputs(index, summary), + })) + .map(viewBadgeData => ({ + view: viewBadgeData.view, + file: path.join(outputDir, `${viewBadgeData.view}.svg`), + badgeContent: gradientBadge(viewBadgeData.badgeInputs), + })) + .forEach(badge => { + fs.writeFileSync(badge.file, badge.badgeContent); + core.info(`Generated badge for ${badge.view} at ${badge.file}`); + core.setOutput(badge.view, badge.file); + }); + + core.setOutput('badges-dir', outputDir); +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..ef25609 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,111 @@ +import * as core from '@actions/core'; +import * as github from '@actions/github'; +import { readSummaries } from './read-summaries'; +import { generateBadges } from './generate-badges'; +import { checkSuppression } from './suppression'; +import { createCheckRuns } from './create-check-runs'; +import { buildCommentBody } from './build-comment-body'; +import { buildCommentMarker, findExistingComment, upsertComment } from './pr-comments'; +import { CoverageSummary } from './types/coverage'; + +async function run(): Promise { + try { + const token = core.getInput('github-token'); + const octokit = github.getOctokit(token); + const context = github.context; + + const summariesBasePath = core.getInput('summary-report-base-path'); + const title = core.getInput('title'); + const suppressInput = core.getInput('suppress-check-failures') === 'true'; + const externalId = core.getInput('external-id'); + const extraRenderScriptInput = core.getInput('check-run-extra-render-script'); + + // Step 1: Read summaries + const deltaSummariesFile = readSummaries({ + isFullCoverageMode: false, + baseSummariesPath: summariesBasePath, + }); + const fullCovSummariesFile = readSummaries({ + isFullCoverageMode: true, + baseSummariesPath: summariesBasePath, + }); + + // Step 2: Generate badges (continue on error) + try { + generateBadges({ + summariesFile: fullCovSummariesFile, + core, + }); + } catch (e) { + core.warning(`Badge generation failed: ${e}`); + } + + // Step 3: Check suppression + const pullNumber = context.payload.pull_request?.number ?? 0; + const suppress = await checkSuppression({ + github: octokit, + context, + pullNumber, + suppressByInput: suppressInput, + core, + }); + + // Step 4: Create check runs + const extraRenderScript = (view: CoverageSummary): string => { + if (!extraRenderScriptInput) return ''; + try { + const script = new Function('return ' + extraRenderScriptInput)(); + return script(view); + } catch (e) { + return `Error in custom script: ${e}`; + } + }; + + const headSha = context.payload.pull_request?.head?.sha ?? context.sha; + const checkRuns = await createCheckRuns({ + summaryReportPath: deltaSummariesFile, + ignoreCoverageFailure: suppress, + core, + context, + github: octokit, + headSha, + externalId, + summaryExtraFun: extraRenderScript, + }); + + // Step 5: Post PR comment (only for pull requests) + if (context.eventName === 'pull_request') { + const marker = title ? buildCommentMarker(title) : ''; + const body = buildCommentBody({ + checkRunsContent: JSON.stringify(checkRuns), + commentTitle: title, + commentMarker: marker, + core, + }); + + let existingCommentId: number | undefined; + if (marker) { + existingCommentId = await findExistingComment({ + github: octokit, + context, + marker, + }); + } + + await upsertComment({ + github: octokit, + context, + existingCommentId, + body, + }); + } + } catch (error) { + if (error instanceof Error) { + core.setFailed(error.message); + } else { + core.setFailed(String(error)); + } + } +} + +run(); diff --git a/src/pr-comments.ts b/src/pr-comments.ts new file mode 100644 index 0000000..40c53eb --- /dev/null +++ b/src/pr-comments.ts @@ -0,0 +1,73 @@ +export interface PrCommentsGitHubClient { + rest: { + issues: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + listComments(params: any): Promise<{ data: Array<{ id: number; body?: string }> }>; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + createComment(params: any): Promise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + updateComment(params: any): Promise; + }; + }; +} + +export interface FindExistingCommentParams { + github: PrCommentsGitHubClient; + context: { + issue: { number: number }; + repo: { owner: string; repo: string }; + }; + marker: string; +} + +export async function findExistingComment(params: FindExistingCommentParams): Promise { + const { github, context, marker } = params; + + const response = await github.rest.issues.listComments({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + }); + + const comment = response.data.find(it => it.body?.includes(marker)); + if (comment) { + console.log(`Comment found: ${comment.id}`); + return comment.id; + } + return undefined; +} + +export interface UpsertCommentParams { + github: PrCommentsGitHubClient; + context: { + issue: { number: number }; + repo: { owner: string; repo: string }; + }; + existingCommentId?: number; + body: string; +} + +export async function upsertComment(params: UpsertCommentParams): Promise { + const { github, context, existingCommentId, body } = params; + + if (existingCommentId) { + await github.rest.issues.updateComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existingCommentId, + body, + }); + } else { + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body, + }); + } +} + +export function buildCommentMarker(title: string): string { + return ``; +} diff --git a/src/read-summaries.js b/src/read-summaries.js deleted file mode 100644 index 6b8dbab..0000000 --- a/src/read-summaries.js +++ /dev/null @@ -1,21 +0,0 @@ -module.exports = async (ctx) => { - const fs = require('fs'); - - const fullCoverageFilter = file => file.includes('full-coverage-'); - const deltaCoverageFilter = file => !fullCoverageFilter(file); - - const allSummariesFile = ctx.isFullCoverageMode ? 'full-cov-summaries.json' : 'delta-cov-summaries.json'; - - const chosenFilter = ctx.isFullCoverageMode ? fullCoverageFilter : deltaCoverageFilter; - - const files = fs.readdirSync(ctx.baseSummariesPath); - const summaryFiles = files.filter(file => file.includes('-summary.json')).filter(chosenFilter); - console.log(`Reading summaries from ${ctx.baseSummariesPath}: ${JSON.stringify(summaryFiles, null, 2)}`); - - const summaries = summaryFiles.map(file => - JSON.parse(fs.readFileSync(`${ctx.baseSummariesPath}/${file}`, 'utf8')) - ); - fs.writeFileSync(allSummariesFile, JSON.stringify(summaries)); - - return allSummariesFile; -}; diff --git a/src/read-summaries.ts b/src/read-summaries.ts new file mode 100644 index 0000000..a49611f --- /dev/null +++ b/src/read-summaries.ts @@ -0,0 +1,32 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { CoverageSummary } from './types/coverage'; + +export interface ReadSummariesParams { + isFullCoverageMode: boolean; + baseSummariesPath: string; +} + +export function readSummaries(params: ReadSummariesParams): string { + const { isFullCoverageMode, baseSummariesPath } = params; + + const fullCoverageFilter = (file: string) => file.includes('full-coverage-'); + const deltaCoverageFilter = (file: string) => !fullCoverageFilter(file); + + const allSummariesFile = isFullCoverageMode ? 'full-cov-summaries.json' : 'delta-cov-summaries.json'; + const chosenFilter = isFullCoverageMode ? fullCoverageFilter : deltaCoverageFilter; + + const files = fs.readdirSync(baseSummariesPath); + const summaryFiles = files + .filter(file => file.includes('-summary.json')) + .filter(chosenFilter); + console.log(`Reading summaries from ${baseSummariesPath}: ${JSON.stringify(summaryFiles, null, 2)}`); + + const summaries: CoverageSummary[] = summaryFiles.map(file => + JSON.parse(fs.readFileSync(path.join(baseSummariesPath, file), 'utf8')) + ); + fs.writeFileSync(allSummariesFile, JSON.stringify(summaries)); + + return allSummariesFile; +} + diff --git a/src/suppression.ts b/src/suppression.ts new file mode 100644 index 0000000..c837d7d --- /dev/null +++ b/src/suppression.ts @@ -0,0 +1,37 @@ +export interface SuppressionGitHubClient { + rest: { + pulls: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + get(params: any): Promise<{ data: { labels: Array<{ name: string }> } }>; + }; + }; +} + +export interface CheckSuppressionParams { + github: SuppressionGitHubClient; + context: { + repo: { owner: string; repo: string }; + }; + pullNumber: number; + suppressByInput: boolean; + core: { info(message: string): void }; +} + +export async function checkSuppression(params: CheckSuppressionParams): Promise { + const { github, context, pullNumber, suppressByInput, core } = params; + + let suppressedByLabel = false; + if (pullNumber > 0) { + const response = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pullNumber, + }); + const labels = response.data.labels.map(l => l.name); + suppressedByLabel = labels.includes('suppress-coverage'); + } + + const resolution = suppressedByLabel || suppressByInput; + core.info(`Is suppress=${resolution} : by label=${suppressedByLabel}, by input=${suppressByInput}`); + return resolution; +} diff --git a/src/types/coverage.ts b/src/types/coverage.ts new file mode 100644 index 0000000..7717655 --- /dev/null +++ b/src/types/coverage.ts @@ -0,0 +1,38 @@ +export type CoverageEntity = 'INSTRUCTION' | 'BRANCH' | 'LINE'; + +export interface CoverageInfo { + coverageEntity: CoverageEntity; + covered: number; + total: number; + percents: number; +} + +export interface EntityRule { + minCoverageRatio: number; +} + +export interface CoverageRulesConfig { + failOnViolation: boolean; + entitiesRules: Record; +} + +export interface Verification { + violation: string; +} + +export interface CoverageSummary { + view: string; + reportBound: string; + coverageRulesConfig: CoverageRulesConfig; + verifications: Verification[]; + coverageInfo: CoverageInfo[]; +} + +export interface CheckRunResult { + viewName: string; + verifications: Verification[]; + coverageRules: CoverageRulesConfig; + url: string; + conclusion: 'success' | 'failure'; + coverageInfo: CoverageInfo[]; +} diff --git a/test/build-comment-body.test.ts b/test/build-comment-body.test.ts new file mode 100644 index 0000000..dfc46ed --- /dev/null +++ b/test/build-comment-body.test.ts @@ -0,0 +1,274 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { buildCommentBody, BuildCommentBodyParams } from '../src/build-comment-body'; +import { CheckRunResult } from '../src/types/coverage'; + +function createMockSummaryBuilder() { + let buffer = ''; + const builder: any = { + addHeading: vi.fn((text: string, _level: string) => { buffer += `

${text}

`; return builder; }), + addRaw: vi.fn((text: string, _addEOL?: boolean) => { buffer += text; return builder; }), + addEOL: vi.fn(() => { buffer += '\n'; return builder; }), + stringify: vi.fn(() => buffer), + }; + return builder; +} + +function makeCheckRun(overrides: Partial = {}): CheckRunResult { + return { + viewName: 'Test', + verifications: [], + coverageRules: { failOnViolation: false, entitiesRules: {} }, + url: 'https://github.com/owner/repo/runs/123', + conclusion: 'success', + coverageInfo: [ + { coverageEntity: 'INSTRUCTION', covered: 80, total: 100, percents: 80 }, + { coverageEntity: 'BRANCH', covered: 50, total: 100, percents: 50 }, + { coverageEntity: 'LINE', covered: 70, total: 100, percents: 70 }, + ], + ...overrides, + }; +} + +const defaultEnv = { + GITHUB_SERVER_URL: 'https://github.com', + GITHUB_REPOSITORY: 'owner/repo', + GITHUB_RUN_ID: '12345', + GITHUB_RUN_NUMBER: '42', + GITHUB_RUN_ATTEMPT: '1', +}; + +describe('buildCommentBody', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should render table with check run data', () => { + const checkRun = makeCheckRun(); + const mockBuilder = createMockSummaryBuilder(); + + const result = buildCommentBody({ + checkRunsContent: JSON.stringify([checkRun]), + commentTitle: 'Coverage Report', + commentMarker: '', + core: { summary: mockBuilder }, + env: defaultEnv, + }); + + expect(result).toContain(''); + expect(result).toContain('
'); + expect(result).toContain('Test'); + expect(result).toContain('INSTRUCTION'); + expect(result).toContain('BRANCH'); + expect(result).toContain('LINE'); + }); + + it('should include comment marker', () => { + const mockBuilder = createMockSummaryBuilder(); + + const result = buildCommentBody({ + checkRunsContent: JSON.stringify([makeCheckRun()]), + commentTitle: 'Title', + commentMarker: '', + core: { summary: mockBuilder }, + env: defaultEnv, + }); + + expect(result).toContain(''); + }); + + it('should show success status symbol for passing checks', () => { + const mockBuilder = createMockSummaryBuilder(); + + const result = buildCommentBody({ + checkRunsContent: JSON.stringify([makeCheckRun({ conclusion: 'success' })]), + commentTitle: 'Title', + commentMarker: '', + core: { summary: mockBuilder }, + env: defaultEnv, + }); + + expect(result).toContain('🟒'); + expect(result).not.toContain('πŸ”΄'); + }); + + it('should show failure status symbol when violations exist and failOnViolation is true', () => { + const checkRun = makeCheckRun({ + coverageRules: { + failOnViolation: true, + entitiesRules: { + INSTRUCTION: { minCoverageRatio: 0.9 }, + BRANCH: { minCoverageRatio: 0.9 }, + LINE: { minCoverageRatio: 0.9 }, + }, + }, + }); + const mockBuilder = createMockSummaryBuilder(); + + const result = buildCommentBody({ + checkRunsContent: JSON.stringify([checkRun]), + commentTitle: 'Title', + commentMarker: '', + core: { summary: mockBuilder }, + env: defaultEnv, + }); + + expect(result).toContain('πŸ”΄'); + }); + + it('should use progress bar with success color when actual >= expected', () => { + const checkRun = makeCheckRun({ + coverageRules: { + failOnViolation: true, + entitiesRules: { LINE: { minCoverageRatio: 0.5 } }, + }, + }); + const mockBuilder = createMockSummaryBuilder(); + + const result = buildCommentBody({ + checkRunsContent: JSON.stringify([checkRun]), + commentTitle: 'Title', + commentMarker: '', + core: { summary: mockBuilder }, + env: defaultEnv, + }); + + expect(result).toContain('progress_color=7AB56D'); + }); + + it('should use progress bar with failure color when actual < expected', () => { + const checkRun = makeCheckRun({ + coverageRules: { + failOnViolation: true, + entitiesRules: { + INSTRUCTION: { minCoverageRatio: 0.99 }, + }, + }, + }); + const mockBuilder = createMockSummaryBuilder(); + + const result = buildCommentBody({ + checkRunsContent: JSON.stringify([checkRun]), + commentTitle: 'Title', + commentMarker: '', + core: { summary: mockBuilder }, + env: defaultEnv, + }); + + expect(result).toContain('progress_color=C4625A'); + }); + + it('should show no-coverage indicator when total is 0', () => { + const checkRun = makeCheckRun({ + coverageInfo: [ + { coverageEntity: 'INSTRUCTION', covered: 0, total: 0, percents: 0 }, + { coverageEntity: 'BRANCH', covered: 0, total: 0, percents: 0 }, + { coverageEntity: 'LINE', covered: 0, total: 0, percents: 0 }, + ], + }); + const mockBuilder = createMockSummaryBuilder(); + + const result = buildCommentBody({ + checkRunsContent: JSON.stringify([checkRun]), + commentTitle: 'Title', + commentMarker: '', + core: { summary: mockBuilder }, + env: defaultEnv, + }); + + expect(result).toContain('No%20diff'); + expect(result).toContain('777777'); + }); + + it('should fold expected column when all values are the same', () => { + const checkRun = makeCheckRun({ + coverageRules: { + failOnViolation: true, + entitiesRules: { + INSTRUCTION: { minCoverageRatio: 0.8 }, + BRANCH: { minCoverageRatio: 0.8 }, + LINE: { minCoverageRatio: 0.8 }, + }, + }, + }); + const mockBuilder = createMockSummaryBuilder(); + + const result = buildCommentBody({ + checkRunsContent: JSON.stringify([checkRun]), + commentTitle: 'Title', + commentMarker: '', + core: { summary: mockBuilder }, + env: defaultEnv, + }); + + expect(result).toContain('rowspan=3'); + }); + + it('should include workflow metadata link', () => { + const mockBuilder = createMockSummaryBuilder(); + + const result = buildCommentBody({ + checkRunsContent: JSON.stringify([makeCheckRun()]), + commentTitle: 'Title', + commentMarker: '', + core: { summary: mockBuilder }, + env: defaultEnv, + }); + + expect(result).toContain('Run 42.1'); + expect(result).toContain('https://github.com/owner/repo/actions/runs/12345'); + }); + + it('should include entity tooltips', () => { + const mockBuilder = createMockSummaryBuilder(); + + const result = buildCommentBody({ + checkRunsContent: JSON.stringify([makeCheckRun()]), + commentTitle: 'Title', + commentMarker: '', + core: { summary: mockBuilder }, + env: defaultEnv, + }); + + expect(result).toContain('title="The Java bytecode instructions executed during testing"'); + expect(result).toContain('title="The source code lines covered by the tests."'); + }); + + it('should render multiple check runs', () => { + const checkRuns = [ + makeCheckRun({ viewName: 'Test' }), + makeCheckRun({ viewName: 'FunctionalTest' }), + ]; + const mockBuilder = createMockSummaryBuilder(); + + const result = buildCommentBody({ + checkRunsContent: JSON.stringify(checkRuns), + commentTitle: 'Title', + commentMarker: '', + core: { summary: mockBuilder }, + env: defaultEnv, + }); + + expect(result).toContain('Test'); + expect(result).toContain('FunctionalTest'); + }); + + it('should show expected percentage with target emoji', () => { + const checkRun = makeCheckRun({ + coverageRules: { + failOnViolation: true, + entitiesRules: { LINE: { minCoverageRatio: 0.75 } }, + }, + }); + const mockBuilder = createMockSummaryBuilder(); + + const result = buildCommentBody({ + checkRunsContent: JSON.stringify([checkRun]), + commentTitle: 'Title', + commentMarker: '', + core: { summary: mockBuilder }, + env: defaultEnv, + }); + + expect(result).toContain('🎯 75% 🎯'); + }); +}); diff --git a/test/create-check-runs.test.ts b/test/create-check-runs.test.ts new file mode 100644 index 0000000..92b97e6 --- /dev/null +++ b/test/create-check-runs.test.ts @@ -0,0 +1,205 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import * as fs from 'fs'; +import { CoverageSummary } from '../src/types/coverage'; + +vi.mock('fs', () => ({ + readFileSync: vi.fn(), +})); + +const mockedFs = vi.mocked(fs); + +import { createCheckRuns, CreateCheckRunsParams } from '../src/create-check-runs'; + +function makeSummary(overrides: Partial = {}): CoverageSummary { + return { + view: 'test', + reportBound: 'DELTA_REPORT', + coverageRulesConfig: { failOnViolation: false, entitiesRules: {} }, + verifications: [], + coverageInfo: [ + { coverageEntity: 'INSTRUCTION', covered: 80, total: 100, percents: 80 }, + { coverageEntity: 'BRANCH', covered: 50, total: 100, percents: 50 }, + { coverageEntity: 'LINE', covered: 70, total: 100, percents: 70 }, + ], + ...overrides, + }; +} + +function makeParams(overrides: Partial = {}): CreateCheckRunsParams { + return { + summaryReportPath: 'delta-cov-summaries.json', + ignoreCoverageFailure: false, + core: { error: vi.fn() }, + context: { repo: { owner: 'testOwner', repo: 'testRepo' } }, + github: { + rest: { + checks: { + create: vi.fn().mockResolvedValue({ + data: { html_url: 'https://github.com/testOwner/testRepo/runs/456' }, + }), + }, + }, + }, + headSha: 'abc123', + externalId: 'delta-coverage', + ...overrides, + }; +} + +describe('createCheckRuns', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should create a check run for each view', async () => { + const summaries = [makeSummary({ view: 'test' }), makeSummary({ view: 'functionalTest' })]; + mockedFs.readFileSync.mockImplementation((filePath: any) => { + if (String(filePath) === 'delta-cov-summaries.json') return JSON.stringify(summaries); + return 'Report content'; + }); + + const params = makeParams(); + const result = await createCheckRuns(params); + + expect(result).toHaveLength(2); + expect(result[0].viewName).toBe('Test'); + expect(result[1].viewName).toBe('FunctionalTest'); + expect(params.github.rest.checks.create).toHaveBeenCalledTimes(2); + }); + + it('should set conclusion to success when no violations', async () => { + const summaries = [makeSummary({ verifications: [] })]; + mockedFs.readFileSync.mockImplementation((filePath: any) => { + if (String(filePath) === 'delta-cov-summaries.json') return JSON.stringify(summaries); + return 'Report content'; + }); + + const params = makeParams(); + const result = await createCheckRuns(params); + + expect(result[0].conclusion).toBe('success'); + }); + + it('should set conclusion to failure when violations exist', async () => { + const summaries = [makeSummary({ verifications: [{ violation: 'LINE: min=80%, actual=50%' }] })]; + mockedFs.readFileSync.mockImplementation((filePath: any) => { + if (String(filePath) === 'delta-cov-summaries.json') return JSON.stringify(summaries); + return 'Report content'; + }); + + const params = makeParams({ ignoreCoverageFailure: false }); + const result = await createCheckRuns(params); + + expect(result[0].conclusion).toBe('failure'); + }); + + it('should suppress failures when ignoreCoverageFailure is true', async () => { + const summaries = [makeSummary({ verifications: [{ violation: 'LINE: min=80%, actual=50%' }] })]; + mockedFs.readFileSync.mockImplementation((filePath: any) => { + if (String(filePath) === 'delta-cov-summaries.json') return JSON.stringify(summaries); + return 'Report content'; + }); + + const params = makeParams({ ignoreCoverageFailure: true }); + const result = await createCheckRuns(params); + + expect(result[0].conclusion).toBe('success'); + }); + + it('should log error annotations for violations', async () => { + const summaries = [makeSummary({ + view: 'test', + verifications: [{ violation: 'LINE: min=80%, actual=50%' }], + })]; + mockedFs.readFileSync.mockImplementation((filePath: any) => { + if (String(filePath) === 'delta-cov-summaries.json') return JSON.stringify(summaries); + return 'Report content'; + }); + + const params = makeParams(); + await createCheckRuns(params); + + expect(params.core.error).toHaveBeenCalledWith( + expect.stringContaining('[Test]: Code Coverage check failed:') + ); + }); + + it('should handle missing report file gracefully', async () => { + const summaries = [makeSummary({ view: 'test' })]; + mockedFs.readFileSync.mockImplementation((filePath: any) => { + if (String(filePath) === 'delta-cov-summaries.json') return JSON.stringify(summaries); + throw new Error('ENOENT'); + }); + + const params = makeParams(); + const result = await createCheckRuns(params); + + expect(result[0]).toBeDefined(); + expect(params.github.rest.checks.create).toHaveBeenCalledWith( + expect.objectContaining({ + output: expect.objectContaining({ + summary: expect.stringContaining('NO REPORT by path:'), + }), + }) + ); + }); + + it('should pass correct parameters to checks.create', async () => { + const summaries = [makeSummary({ view: 'test' })]; + mockedFs.readFileSync.mockImplementation((filePath: any) => { + if (String(filePath) === 'delta-cov-summaries.json') return JSON.stringify(summaries); + return 'Report content'; + }); + + const params = makeParams(); + await createCheckRuns(params); + + expect(params.github.rest.checks.create).toHaveBeenCalledWith({ + owner: 'testOwner', + repo: 'testRepo', + name: 'πŸ“ˆTest Coverage', + head_sha: 'abc123', + status: 'completed', + external_id: 'delta-coverage', + conclusion: 'success', + output: { + title: 'Test Coverage', + summary: expect.stringContaining('Report content'), + }, + }); + }); + + it('should include summaryExtraFun output in check run', async () => { + const summaries = [makeSummary({ view: 'test' })]; + mockedFs.readFileSync.mockImplementation((filePath: any) => { + if (String(filePath) === 'delta-cov-summaries.json') return JSON.stringify(summaries); + return 'Report content'; + }); + + const extraFun = vi.fn().mockReturnValue('data'); + const params = makeParams({ summaryExtraFun: extraFun }); + await createCheckRuns(params); + + expect(extraFun).toHaveBeenCalledWith(summaries[0]); + expect(params.github.rest.checks.create).toHaveBeenCalledWith( + expect.objectContaining({ + output: expect.objectContaining({ + summary: expect.stringContaining('data'), + }), + }) + ); + }); + + it('should return check run URL from API response', async () => { + const summaries = [makeSummary()]; + mockedFs.readFileSync.mockImplementation((filePath: any) => { + if (String(filePath) === 'delta-cov-summaries.json') return JSON.stringify(summaries); + return 'Report content'; + }); + + const params = makeParams(); + const result = await createCheckRuns(params); + + expect(result[0].url).toBe('https://github.com/testOwner/testRepo/runs/456'); + }); +}); diff --git a/test/data/delta-coverage-functionalTest-summary.json b/test/data/delta-coverage-functionalTest-summary.json new file mode 100644 index 0000000..21c27fb --- /dev/null +++ b/test/data/delta-coverage-functionalTest-summary.json @@ -0,0 +1 @@ +{"view":"functionalTest","reportBound":"DELTA_REPORT","coverageRulesConfig":{"failOnViolation":true,"entitiesRules":{"INSTRUCTION":{"minCoverageRatio":0.6},"BRANCH":{"minCoverageRatio":0.4},"LINE":{"minCoverageRatio":0.6}}},"verifications":[{"violation":"BRANCH: expectedMin=40.0%, actual=35.0%"}],"coverageInfo":[{"coverageEntity":"INSTRUCTION","covered":80,"total":120,"percents":66.67},{"coverageEntity":"BRANCH","covered":14,"total":40,"percents":35.0},{"coverageEntity":"LINE","covered":70,"total":100,"percents":70.0}]} diff --git a/test/data/delta-coverage-test-summary.json b/test/data/delta-coverage-test-summary.json new file mode 100644 index 0000000..a4c5cf1 --- /dev/null +++ b/test/data/delta-coverage-test-summary.json @@ -0,0 +1 @@ +{"view":"test","reportBound":"DELTA_REPORT","coverageRulesConfig":{"failOnViolation":true,"entitiesRules":{"INSTRUCTION":{"minCoverageRatio":0.8},"BRANCH":{"minCoverageRatio":0.5},"LINE":{"minCoverageRatio":0.8}}},"verifications":[],"coverageInfo":[{"coverageEntity":"INSTRUCTION","covered":120,"total":150,"percents":80.0},{"coverageEntity":"BRANCH","covered":30,"total":50,"percents":60.0},{"coverageEntity":"LINE","covered":100,"total":120,"percents":83.33}]} diff --git a/test/generate-badges.test.ts b/test/generate-badges.test.ts new file mode 100644 index 0000000..a82c16e --- /dev/null +++ b/test/generate-badges.test.ts @@ -0,0 +1,113 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import * as fs from 'fs'; +import { CoverageSummary } from '../src/types/coverage'; + +vi.mock('fs', () => ({ + mkdirSync: vi.fn(), + readFileSync: vi.fn(), + writeFileSync: vi.fn(), +})); + +vi.mock('gradient-badge', () => ({ + default: vi.fn((inputs: any) => `${inputs.subject}: ${inputs.status}`), +})); + +const mockedFs = vi.mocked(fs); + +import { generateBadges } from '../src/generate-badges'; + +describe('generateBadges', () => { + const mockCore = { + info: vi.fn(), + setOutput: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + const makeSummary = (view: string, linePercent: number): CoverageSummary => ({ + view, + reportBound: 'DELTA_REPORT', + coverageRulesConfig: { failOnViolation: false, entitiesRules: {} }, + verifications: [], + coverageInfo: [ + { coverageEntity: 'INSTRUCTION', covered: 80, total: 100, percents: 80 }, + { coverageEntity: 'BRANCH', covered: 50, total: 100, percents: 50 }, + { coverageEntity: 'LINE', covered: 70, total: 100, percents: linePercent }, + ], + }); + + it('should create badges directory', () => { + mockedFs.readFileSync.mockReturnValue('[]' as any); + + generateBadges({ summariesFile: 'test.json', core: mockCore }); + + expect(mockedFs.mkdirSync).toHaveBeenCalledWith('badges/', { recursive: true }); + }); + + it('should generate badges for each view sorted alphabetically', () => { + const summaries = [ + makeSummary('test', 82.3), + makeSummary('aggregated', 87.96), + ]; + mockedFs.readFileSync.mockReturnValue(JSON.stringify(summaries) as any); + + generateBadges({ summariesFile: 'full-cov-summaries.json', core: mockCore }); + + expect(mockedFs.writeFileSync).toHaveBeenCalledTimes(2); + // Sorted alphabetically: aggregated first, then test + expect(mockedFs.writeFileSync).toHaveBeenCalledWith( + 'badges/aggregated.svg', + expect.stringContaining('aggregated') + ); + expect(mockedFs.writeFileSync).toHaveBeenCalledWith( + 'badges/test.svg', + expect.stringContaining('test') + ); + }); + + it('should set output for each badge and badges-dir', () => { + const summaries = [makeSummary('test', 82.3)]; + mockedFs.readFileSync.mockReturnValue(JSON.stringify(summaries) as any); + + generateBadges({ summariesFile: 'test.json', core: mockCore }); + + expect(mockCore.setOutput).toHaveBeenCalledWith('test', 'badges/test.svg'); + expect(mockCore.setOutput).toHaveBeenCalledWith('badges-dir', 'badges/'); + }); + + it('should use custom output directory', () => { + mockedFs.readFileSync.mockReturnValue('[]' as any); + + generateBadges({ summariesFile: 'test.json', core: mockCore, outputDir: 'custom/' }); + + expect(mockedFs.mkdirSync).toHaveBeenCalledWith('custom/', { recursive: true }); + expect(mockCore.setOutput).toHaveBeenCalledWith('badges-dir', 'custom/'); + }); + + it('should cycle through colors for many views', () => { + const summaries = [ + makeSummary('a', 80), + makeSummary('b', 70), + makeSummary('c', 60), + makeSummary('d', 50), + makeSummary('e', 40), + makeSummary('f', 30), // cycles back to first color + ]; + mockedFs.readFileSync.mockReturnValue(JSON.stringify(summaries) as any); + + generateBadges({ summariesFile: 'test.json', core: mockCore }); + + expect(mockedFs.writeFileSync).toHaveBeenCalledTimes(6); + }); + + it('should handle empty summaries', () => { + mockedFs.readFileSync.mockReturnValue('[]' as any); + + generateBadges({ summariesFile: 'test.json', core: mockCore }); + + expect(mockedFs.writeFileSync).not.toHaveBeenCalled(); + expect(mockCore.setOutput).toHaveBeenCalledWith('badges-dir', 'badges/'); + }); +}); diff --git a/test/main.test.ts b/test/main.test.ts new file mode 100644 index 0000000..ecde9c7 --- /dev/null +++ b/test/main.test.ts @@ -0,0 +1,190 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +vi.mock('@actions/core'); +vi.mock('@actions/github'); +vi.mock('../src/read-summaries'); +vi.mock('../src/generate-badges'); +vi.mock('../src/suppression'); +vi.mock('../src/create-check-runs'); +vi.mock('../src/build-comment-body'); +vi.mock('../src/pr-comments'); + +import * as core from '@actions/core'; +import * as github from '@actions/github'; +import { readSummaries } from '../src/read-summaries'; +import { generateBadges } from '../src/generate-badges'; +import { checkSuppression } from '../src/suppression'; +import { createCheckRuns } from '../src/create-check-runs'; +import { buildCommentBody } from '../src/build-comment-body'; +import { buildCommentMarker, findExistingComment, upsertComment } from '../src/pr-comments'; + +const mockedCore = vi.mocked(core); +const mockedGithub = vi.mocked(github); +const mockedReadSummaries = vi.mocked(readSummaries); +const mockedGenerateBadges = vi.mocked(generateBadges); +const mockedCheckSuppression = vi.mocked(checkSuppression); +const mockedCreateCheckRuns = vi.mocked(createCheckRuns); +const mockedBuildCommentBody = vi.mocked(buildCommentBody); +const mockedBuildCommentMarker = vi.mocked(buildCommentMarker); +const mockedFindExistingComment = vi.mocked(findExistingComment); +const mockedUpsertComment = vi.mocked(upsertComment); + +describe('main', () => { + const mockOctokit = {} as any; + + beforeEach(() => { + vi.clearAllMocks(); + + mockedCore.getInput.mockImplementation((name: string) => { + const inputs: Record = { + 'github-token': 'test-token', + 'summary-report-base-path': 'test/data/', + 'title': 'Coverage Report', + 'suppress-check-failures': 'false', + 'external-id': 'delta-coverage', + 'check-run-extra-render-script': '', + }; + return inputs[name] || ''; + }); + + mockedGithub.getOctokit.mockReturnValue(mockOctokit); + Object.defineProperty(mockedGithub, 'context', { + value: { + eventName: 'pull_request', + sha: 'abc123', + payload: { + pull_request: { number: 42, head: { sha: 'pr-sha-456' } }, + }, + issue: { number: 42 }, + repo: { owner: 'testOwner', repo: 'testRepo' }, + }, + writable: true, + }); + + mockedReadSummaries.mockReturnValueOnce('delta-cov-summaries.json'); + mockedReadSummaries.mockReturnValueOnce('full-cov-summaries.json'); + mockedCheckSuppression.mockResolvedValue(false); + mockedCreateCheckRuns.mockResolvedValue([]); + mockedBuildCommentBody.mockReturnValue('body'); + mockedBuildCommentMarker.mockReturnValue(''); + mockedFindExistingComment.mockResolvedValue(undefined); + mockedUpsertComment.mockResolvedValue(undefined); + }); + + async function runMain() { + // Dynamically import to trigger the run() call + // We need to reset the module cache so it re-executes + vi.resetModules(); + // Re-mock after resetModules + vi.doMock('@actions/core', () => mockedCore); + vi.doMock('@actions/github', () => mockedGithub); + vi.doMock('../src/read-summaries', () => ({ readSummaries: mockedReadSummaries })); + vi.doMock('../src/generate-badges', () => ({ generateBadges: mockedGenerateBadges })); + vi.doMock('../src/suppression', () => ({ checkSuppression: mockedCheckSuppression })); + vi.doMock('../src/create-check-runs', () => ({ createCheckRuns: mockedCreateCheckRuns })); + vi.doMock('../src/build-comment-body', () => ({ buildCommentBody: mockedBuildCommentBody })); + vi.doMock('../src/pr-comments', () => ({ + buildCommentMarker: mockedBuildCommentMarker, + findExistingComment: mockedFindExistingComment, + upsertComment: mockedUpsertComment, + })); + await import('../src/main'); + // Allow microtasks to complete + await new Promise(resolve => setTimeout(resolve, 10)); + } + + it('should read both delta and full coverage summaries', async () => { + await runMain(); + + expect(mockedReadSummaries).toHaveBeenCalledWith({ + isFullCoverageMode: false, + baseSummariesPath: 'test/data/', + }); + expect(mockedReadSummaries).toHaveBeenCalledWith({ + isFullCoverageMode: true, + baseSummariesPath: 'test/data/', + }); + }); + + it('should generate badges from full coverage summaries', async () => { + await runMain(); + + expect(mockedGenerateBadges).toHaveBeenCalledWith({ + summariesFile: 'full-cov-summaries.json', + core: mockedCore, + }); + }); + + it('should check suppression with correct params', async () => { + await runMain(); + + expect(mockedCheckSuppression).toHaveBeenCalledWith({ + github: mockOctokit, + context: expect.objectContaining({ repo: { owner: 'testOwner', repo: 'testRepo' } }), + pullNumber: 42, + suppressByInput: false, + core: mockedCore, + }); + }); + + it('should create check runs with correct params', async () => { + await runMain(); + + expect(mockedCreateCheckRuns).toHaveBeenCalledWith( + expect.objectContaining({ + summaryReportPath: 'delta-cov-summaries.json', + ignoreCoverageFailure: false, + headSha: 'pr-sha-456', + externalId: 'delta-coverage', + }) + ); + }); + + it('should build and post PR comment', async () => { + await runMain(); + + expect(mockedBuildCommentBody).toHaveBeenCalled(); + expect(mockedUpsertComment).toHaveBeenCalledWith( + expect.objectContaining({ + body: 'body', + }) + ); + }); + + it('should find existing comment when marker exists', async () => { + await runMain(); + + expect(mockedFindExistingComment).toHaveBeenCalledWith( + expect.objectContaining({ + marker: '', + }) + ); + }); + + it('should not post comment when not a pull request', async () => { + Object.defineProperty(mockedGithub, 'context', { + value: { + eventName: 'push', + sha: 'abc123', + payload: {}, + repo: { owner: 'testOwner', repo: 'testRepo' }, + }, + writable: true, + }); + + await runMain(); + + expect(mockedUpsertComment).not.toHaveBeenCalled(); + }); + + it('should continue when badge generation fails', async () => { + mockedGenerateBadges.mockImplementation(() => { + throw new Error('badge error'); + }); + + await runMain(); + + expect(mockedCore.warning).toHaveBeenCalledWith(expect.stringContaining('badge error')); + expect(mockedCheckSuppression).toHaveBeenCalled(); + }); +}); diff --git a/test/pr-comments.test.ts b/test/pr-comments.test.ts new file mode 100644 index 0000000..b35f8c4 --- /dev/null +++ b/test/pr-comments.test.ts @@ -0,0 +1,153 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { + findExistingComment, + upsertComment, + buildCommentMarker, + FindExistingCommentParams, + UpsertCommentParams, +} from '../src/pr-comments'; + +const mockContext = { + issue: { number: 42 }, + repo: { owner: 'testOwner', repo: 'testRepo' }, +}; + +describe('findExistingComment', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should return comment id when marker found', async () => { + const params: FindExistingCommentParams = { + github: { + rest: { + issues: { + listComments: vi.fn().mockResolvedValue({ + data: [ + { id: 1, body: 'some other comment' }, + { id: 2, body: 'has in it' }, + ], + }), + createComment: vi.fn(), + updateComment: vi.fn(), + }, + }, + }, + context: mockContext, + marker: '', + }; + + const result = await findExistingComment(params); + expect(result).toBe(2); + }); + + it('should return undefined when marker not found', async () => { + const params: FindExistingCommentParams = { + github: { + rest: { + issues: { + listComments: vi.fn().mockResolvedValue({ + data: [{ id: 1, body: 'no marker here' }], + }), + createComment: vi.fn(), + updateComment: vi.fn(), + }, + }, + }, + context: mockContext, + marker: '', + }; + + const result = await findExistingComment(params); + expect(result).toBeUndefined(); + }); + + it('should return undefined for empty comments list', async () => { + const params: FindExistingCommentParams = { + github: { + rest: { + issues: { + listComments: vi.fn().mockResolvedValue({ data: [] }), + createComment: vi.fn(), + updateComment: vi.fn(), + }, + }, + }, + context: mockContext, + marker: '', + }; + + const result = await findExistingComment(params); + expect(result).toBeUndefined(); + }); +}); + +describe('upsertComment', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should update existing comment when id is provided', async () => { + const updateComment = vi.fn(); + const createComment = vi.fn(); + const params: UpsertCommentParams = { + github: { + rest: { + issues: { + listComments: vi.fn(), + updateComment, + createComment, + }, + }, + }, + context: mockContext, + existingCommentId: 123, + body: 'updated body', + }; + + await upsertComment(params); + + expect(updateComment).toHaveBeenCalledWith({ + issue_number: 42, + owner: 'testOwner', + repo: 'testRepo', + comment_id: 123, + body: 'updated body', + }); + expect(createComment).not.toHaveBeenCalled(); + }); + + it('should create new comment when no existing id', async () => { + const updateComment = vi.fn(); + const createComment = vi.fn(); + const params: UpsertCommentParams = { + github: { + rest: { + issues: { + listComments: vi.fn(), + updateComment, + createComment, + }, + }, + }, + context: mockContext, + body: 'new comment body', + }; + + await upsertComment(params); + + expect(createComment).toHaveBeenCalledWith({ + issue_number: 42, + owner: 'testOwner', + repo: 'testRepo', + body: 'new comment body', + }); + expect(updateComment).not.toHaveBeenCalled(); + }); +}); + +describe('buildCommentMarker', () => { + it('should build marker from title', () => { + expect(buildCommentMarker('My Title')).toBe(''); + }); +}); diff --git a/test/read-summaries.test.ts b/test/read-summaries.test.ts new file mode 100644 index 0000000..70c288c --- /dev/null +++ b/test/read-summaries.test.ts @@ -0,0 +1,106 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import * as fs from 'fs'; + +vi.mock('fs', () => ({ + readdirSync: vi.fn(), + readFileSync: vi.fn(), + writeFileSync: vi.fn(), +})); + +const mockedFs = vi.mocked(fs); + +// Import after mock setup +import { readSummaries } from '../src/read-summaries'; + +describe('readSummaries', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should filter and aggregate full-coverage summaries', () => { + mockedFs.readdirSync.mockReturnValue([ + 'full-coverage-test-summary.json', + 'full-coverage-functionalTest-summary.json', + 'delta-coverage-test-summary.json', + ] as any); + + const testSummary = { view: 'test', coverageInfo: [] }; + const funcSummary = { view: 'functionalTest', coverageInfo: [] }; + mockedFs.readFileSync.mockImplementation((filePath: any) => { + if (String(filePath).includes('full-coverage-test')) return JSON.stringify(testSummary); + if (String(filePath).includes('full-coverage-functionalTest')) return JSON.stringify(funcSummary); + throw new Error(`Unexpected file: ${filePath}`); + }); + + const result = readSummaries({ + isFullCoverageMode: true, + baseSummariesPath: '/some/path', + }); + + expect(result).toBe('full-cov-summaries.json'); + expect(mockedFs.writeFileSync).toHaveBeenCalledWith( + 'full-cov-summaries.json', + JSON.stringify([testSummary, funcSummary]) + ); + }); + + it('should filter and aggregate delta-coverage summaries', () => { + mockedFs.readdirSync.mockReturnValue([ + 'full-coverage-test-summary.json', + 'delta-coverage-test-summary.json', + 'delta-coverage-functionalTest-summary.json', + ] as any); + + const deltaSummary = { view: 'test', coverageInfo: [] }; + const deltaFuncSummary = { view: 'functionalTest', coverageInfo: [] }; + mockedFs.readFileSync.mockImplementation((filePath: any) => { + if (String(filePath).includes('delta-coverage-test')) return JSON.stringify(deltaSummary); + if (String(filePath).includes('delta-coverage-functionalTest')) return JSON.stringify(deltaFuncSummary); + throw new Error(`Unexpected file: ${filePath}`); + }); + + const result = readSummaries({ + isFullCoverageMode: false, + baseSummariesPath: '/some/path', + }); + + expect(result).toBe('delta-cov-summaries.json'); + expect(mockedFs.writeFileSync).toHaveBeenCalledWith( + 'delta-cov-summaries.json', + JSON.stringify([deltaSummary, deltaFuncSummary]) + ); + }); + + it('should handle empty directory', () => { + mockedFs.readdirSync.mockReturnValue([] as any); + + const result = readSummaries({ + isFullCoverageMode: true, + baseSummariesPath: '/empty/dir', + }); + + expect(result).toBe('full-cov-summaries.json'); + expect(mockedFs.writeFileSync).toHaveBeenCalledWith( + 'full-cov-summaries.json', + '[]' + ); + }); + + it('should ignore non-summary files', () => { + mockedFs.readdirSync.mockReturnValue([ + 'full-coverage-test-summary.json', + 'some-other-file.txt', + 'report.json', + ] as any); + + const testSummary = { view: 'test' }; + mockedFs.readFileSync.mockReturnValue(JSON.stringify(testSummary) as any); + + readSummaries({ + isFullCoverageMode: true, + baseSummariesPath: '/path', + }); + + expect(mockedFs.readFileSync).toHaveBeenCalledTimes(1); + }); +}); diff --git a/test/suppression.test.ts b/test/suppression.test.ts new file mode 100644 index 0000000..b6f6af3 --- /dev/null +++ b/test/suppression.test.ts @@ -0,0 +1,101 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { checkSuppression, CheckSuppressionParams } from '../src/suppression'; + +function makeParams(overrides: Partial = {}): CheckSuppressionParams { + return { + github: { + rest: { + pulls: { + get: vi.fn().mockResolvedValue({ + data: { labels: [] }, + }), + }, + }, + }, + context: { repo: { owner: 'testOwner', repo: 'testRepo' } }, + pullNumber: 42, + suppressByInput: false, + core: { info: vi.fn() }, + ...overrides, + }; +} + +describe('checkSuppression', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should return false when no suppression conditions met', async () => { + const params = makeParams(); + const result = await checkSuppression(params); + expect(result).toBe(false); + }); + + it('should return true when suppress-coverage label is present', async () => { + const params = makeParams({ + github: { + rest: { + pulls: { + get: vi.fn().mockResolvedValue({ + data: { labels: [{ name: 'suppress-coverage' }, { name: 'other' }] }, + }), + }, + }, + }, + }); + + const result = await checkSuppression(params); + expect(result).toBe(true); + }); + + it('should return true when suppressByInput is true', async () => { + const params = makeParams({ suppressByInput: true }); + const result = await checkSuppression(params); + expect(result).toBe(true); + }); + + it('should return true when both label and input are true', async () => { + const params = makeParams({ + suppressByInput: true, + github: { + rest: { + pulls: { + get: vi.fn().mockResolvedValue({ + data: { labels: [{ name: 'suppress-coverage' }] }, + }), + }, + }, + }, + }); + + const result = await checkSuppression(params); + expect(result).toBe(true); + }); + + it('should skip label check when pullNumber is 0', async () => { + const params = makeParams({ pullNumber: 0 }); + const result = await checkSuppression(params); + expect(result).toBe(false); + expect(params.github.rest.pulls.get).not.toHaveBeenCalled(); + }); + + it('should call pulls.get with correct parameters', async () => { + const params = makeParams(); + await checkSuppression(params); + + expect(params.github.rest.pulls.get).toHaveBeenCalledWith({ + owner: 'testOwner', + repo: 'testRepo', + pull_number: 42, + }); + }); + + it('should log suppression status', async () => { + const params = makeParams(); + await checkSuppression(params); + + expect(params.core.info).toHaveBeenCalledWith( + 'Is suppress=false : by label=false, by input=false' + ); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..6507d57 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist", "test"] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..4b7b711 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + coverage: { + provider: 'v8', + reporter: ['text', 'lcov', 'json-summary'], + include: ['src/**/*.ts'], + exclude: ['src/types/**/*.ts'], + }, + }, +});