From d3b550db200c0d7c7a49905e09718530010bf9a7 Mon Sep 17 00:00:00 2001 From: napalm255 Date: Sat, 3 Oct 2026 01:59:43 -0400 Subject: [PATCH 1/7] ci: standardize quick extension tooling and security Adopt the immutable shared workflows and tooling, generate consistent installation and development docs, and fix findings exposed by strict checks without suppressing them. --- .github/dependabot.yml | 19 +- .github/workflows/ci.yml | 108 +- .github/workflows/pages.yml | 25 + .github/workflows/release.yml | 149 +- .github/workflows/security.yml | 95 +- .github/workflows/sonar.yml | 88 +- .github/workflows/template.yml | 23 + .gitignore | 6 + .gitleaks.toml | 14 - .prettierignore | 5 +- .prettierrc.json | 13 +- AGENTS.md | 104 +- README.md | 150 +- docs/index.html | 447 ++- docs/project.json | 5 + docs/project.schema.json | 20 + docs/style.css | 56 +- eslint.config.js | 11 +- justfile | 207 +- metadata.json | 16 +- mise.toml | 33 +- package-lock.json | 4844 ++++++++++++++++---------------- package.json | 39 +- playwright.config.js | 22 +- project.just | 16 + pyproject.toml | 6 + quick-project.json | 22 + quick-project.schema.json | 17 + quick-template.lock.json | 5 + quick-template.schema.json | 11 + scripts/build.py | 137 + scripts/clean.py | 16 + scripts/docs.py | 354 +++ scripts/headless-check.sh | 7 +- scripts/pack-check.sh | 86 - scripts/security_source.py | 75 + scripts/sonar_gate.py | 91 + scripts/template-check.sh | 74 - scripts/template.py | 176 ++ scripts/workflow_lint.py | 43 + sonar-project.properties | 21 +- template.list | 29 - template.sha256 | 29 - tests/docs.config.js | 1 + tests/docs.spec.js | 68 +- tests/stubs/shell-extension.js | 4 +- tests/support/schema.js | 18 +- tests/test_tooling.py | 194 ++ vitest.config.js | 36 +- 49 files changed, 4501 insertions(+), 3534 deletions(-) create mode 100644 .github/workflows/pages.yml create mode 100644 .github/workflows/template.yml create mode 100644 docs/project.json create mode 100644 docs/project.schema.json create mode 100644 project.just create mode 100644 pyproject.toml create mode 100644 quick-project.json create mode 100644 quick-project.schema.json create mode 100644 quick-template.lock.json create mode 100644 quick-template.schema.json create mode 100644 scripts/build.py create mode 100644 scripts/clean.py create mode 100644 scripts/docs.py delete mode 100755 scripts/pack-check.sh create mode 100644 scripts/security_source.py create mode 100644 scripts/sonar_gate.py delete mode 100755 scripts/template-check.sh create mode 100644 scripts/template.py create mode 100644 scripts/workflow_lint.py delete mode 100644 template.list delete mode 100644 template.sha256 create mode 100644 tests/test_tooling.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 808f052..daaf48a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,11 +1,12 @@ version: 2 updates: - - package-ecosystem: npm - directory: / - schedule: - interval: weekly - - - package-ecosystem: github-actions - directory: / - schedule: - interval: weekly + - package-ecosystem: npm + directory: / + open-pull-requests-limit: 0 + schedule: + interval: weekly + - package-ecosystem: github-actions + directory: / + open-pull-requests-limit: 0 + schedule: + interval: weekly diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6813ab9..111813b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,42 +1,76 @@ name: CI - on: - push: - branches: [main] - pull_request: - + push: + branches: [main] + pull_request: + workflow_dispatch: permissions: - contents: read - + contents: read concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - + group: ci-${{ github.ref }} + cancel-in-progress: true jobs: - ci: - runs-on: ubuntu-latest - timeout-minutes: 20 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - # gitleaks scans history; a shallow clone gives it one commit. - fetch-depth: 0 - persist-credentials: false - - # glib-compile-schemas validates the gschema; it is not a mise tool - # because it must match the GLib the target Shell was built against. - - name: Install GLib schema compiler - run: | - sudo apt-get update - sudo apt-get install -y --no-install-recommends libglib2.0-bin - - - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4 - - - run: npm ci - - # The docs site's suite runs in real browsers. - - name: Install test browsers - run: npx playwright install --with-deps chromium firefox - - # The same recipe a developer runs: lint, test, test-docs, security, build. - - run: just ci + checks: + name: checks + runs-on: ubuntu-24.04 + timeout-minutes: 30 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 + with: + version: 2026.9.1 + - name: Install native test tools + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends gjs gnome-shell libglib2.0-bin gir1.2-soup-3.0 gir1.2-secret-1 dbus-daemon + - run: npm ci --ignore-scripts + - run: ./node_modules/.bin/playwright install --with-deps chromium firefox + - run: just ci + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: extension-bundle + path: '*.shell-extension.zip' + if-no-files-found: error + retention-days: 90 + - uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3 + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + with: + path: docs + security: + name: security + uses: $/.github/workflows/security.yml + permissions: + contents: read + security-events: write # Upload CodeQL findings to GitHub code scanning. + actions: read # Inspect workflow runs and download their tested artifacts. + sonar: + name: sonar + uses: $/.github/workflows/sonar.yml + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + ci: + name: ci + if: always() + needs: [checks, security, sonar] + runs-on: ubuntu-24.04 + timeout-minutes: 2 + steps: + - name: Require every verification job + env: + CHECKS: ${{ needs.checks.result }} + SECURITY: ${{ needs.security.result }} + SONAR: ${{ needs.sonar.result }} + run: test "$CHECKS" = success && test "$SECURITY" = success && test "$SONAR" = success + docs: + name: docs + needs: ci + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: $/.github/workflows/pages.yml + permissions: + contents: read + actions: read # Inspect workflow runs and download their tested artifacts. + pages: write # Publish the tested documentation artifact to Pages. + id-token: write # Authenticate the Pages deployment with GitHub OIDC. diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..cd0c32f --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,25 @@ +name: Docs +on: + workflow_call: +permissions: + contents: read +concurrency: + group: pages + cancel-in-progress: false +jobs: + deploy: + name: deploy + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + actions: read # Inspect workflow runs and download their tested artifacts. + pages: write # Publish the tested documentation artifact to Pages. + id-token: write # Authenticate the Pages deployment with GitHub OIDC. + environment: + name: github-pages + url: ${{ steps.deploy.outputs.page_url }} + steps: + - uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4 + id: deploy + with: + artifact_name: github-pages diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3d83f1a..c62a5fa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,84 +1,73 @@ name: Release - on: - push: - tags: ['v*'] - + push: + tags: ['v*'] permissions: - contents: read - + contents: read + actions: read # Inspect workflow runs and download their tested artifacts. +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false jobs: - release: - runs-on: ubuntu-latest - timeout-minutes: 20 - permissions: - contents: write - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - persist-credentials: false - - # Before anything is built or published: a tag that disagrees with - # the tree ships a release titled v0.2.0 containing a zip that tells - # GNOME it is 0.1.0, and nothing downstream would notice. - - name: Check the tag matches the version in the tree - env: - TAG: ${{ github.ref_name }} - run: | - version="${TAG#v}" - meta="$(jq -r '."version-name"' metadata.json)" - pkg="$(jq -r .version package.json)" - status=0 - - if [ "$meta" != "$version" ]; then - echo "::error::metadata.json version-name is '$meta', tag is '$version'" - status=1 - fi - - if [ "$pkg" != "$version" ]; then - echo "::error::package.json version is '$pkg', tag is '$version'" - status=1 - fi - - exit "$status" - - - name: Install GLib schema compiler - run: | - sudo apt-get update - sudo apt-get install -y --no-install-recommends libglib2.0-bin - - - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4 - with: - # A release publishes a runtime artifact, so it must not restore a - # cache that a pull request could have poisoned. - cache: false - - - run: npm ci - - # The docs site's suite runs in real browsers. - - name: Install test browsers - run: npx playwright install --with-deps chromium firefox - - # Gate the release on the full suite; never publish an untested build. - - run: just ci - - - name: Create the release - env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ github.ref_name }} - run: | - # Derived from metadata.json, like the justfile's, so renaming - # the extension cannot leave this uploading a file that - # `just build` no longer produces. - zip="$(jq -r .uuid metadata.json).shell-extension.zip" - - if [ ! -f "$zip" ]; then - echo "::error::just ci did not produce $zip" - exit 1 - fi - - gh release create "$TAG" \ - --title "$TAG" \ - --generate-notes \ - "$zip" + verify: + name: verify + runs-on: ubuntu-24.04 + timeout-minutes: 10 + outputs: + run: ${{ steps.verify.outputs.run }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 + with: + version: 2026.9.1 + cache: false + - name: Verify tag and tested commit + id: verify + env: + TAG: ${{ github.ref_name }} + GH_TOKEN: ${{ github.token }} + REPOSITORY: ${{ github.repository }} + run: | + version="${TAG#v}" + test "$version" = "$(jq -r '."version-name"' metadata.json)" + test "$version" = "$(jq -r .version package.json)" + git merge-base --is-ancestor HEAD origin/main + revision="$(git rev-parse HEAD)" + run="$(gh run list --repo "$REPOSITORY" --workflow ci.yml --branch main --event push --commit "$revision" --status success --limit 1 --json databaseId --jq '.[0].databaseId')" + test -n "$run" && test "$run" != null + echo "run=$run" >> "$GITHUB_OUTPUT" + publish: + name: publish + needs: verify + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: write # Create the release and attach its tested ZIP. + actions: read # Inspect workflow runs and download their tested artifacts. + steps: + - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 + with: + version: 2026.9.1 + cache: false + mise_toml: | + [tools] + gh = "2.99.0" + - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5 + with: + name: extension-bundle + run-id: ${{ needs.verify.outputs.run }} + github-token: ${{ github.token }} + path: bundle + - name: Publish the tested artifact + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + TAG: ${{ github.ref_name }} + run: | + shopt -s nullglob + files=(bundle/*.shell-extension.zip) + test "${#files[@]}" -eq 1 + gh release create "$TAG" --verify-tag --title "$TAG" --generate-notes "${files[0]}" diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index ff9a77f..dccb6d4 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -1,61 +1,44 @@ name: Security - on: - push: - branches: [main] - pull_request: - workflow_dispatch: - schedule: - # Weekly, so new advisories are caught without needing a code change. - - cron: '17 5 * * 1' - + workflow_call: + workflow_dispatch: + schedule: + - cron: '17 5 * * 1' permissions: - contents: read - + contents: read concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - + group: security-${{ github.ref }}-${{ github.event_name }} + cancel-in-progress: true jobs: - codeql: - name: CodeQL - runs-on: ubuntu-latest - timeout-minutes: 20 - permissions: - contents: read - security-events: write - actions: read - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - uses: github/codeql-action/init@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1 - with: - languages: javascript-typescript - queries: security-extended - - - uses: github/codeql-action/analyze@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1 - - scanners: - name: Scanners - # ci.yml runs `just ci`, which already includes `just security`, on every - # push and pull request. Running it again here would scan the same tree - # twice. What this job adds is the weekly schedule: the same scanners - # against unchanged code, catching advisories published since the last - # commit. - if: github.event_name != 'push' && github.event_name != 'pull_request' - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - # gitleaks scans history; a shallow clone gives it one commit. - fetch-depth: 0 - persist-credentials: false - - - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4 - - # Same recipe as `just security` locally: gitleaks, trivy, osv-scanner, - # actionlint and zizmor, all pinned by mise.toml. - - run: just security + codeql: + name: codeql + runs-on: ubuntu-24.04 + timeout-minutes: 20 + permissions: + contents: read + security-events: write # Upload CodeQL findings to GitHub code scanning. + actions: read # Inspect workflow runs and download their tested artifacts. + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: github/codeql-action/init@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1 + with: + languages: javascript-typescript,python + queries: security-extended + - uses: github/codeql-action/analyze@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1 + scanners: + name: scanners + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 + with: + version: 2026.9.1 + - run: npm ci --ignore-scripts + - run: just security diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index e89fab1..516a35b 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -1,53 +1,45 @@ name: Sonar - on: - push: - branches: [main] - pull_request: - + workflow_call: + secrets: + SONAR_TOKEN: + required: true + workflow_dispatch: + schedule: + - cron: '37 5 * * 1' permissions: - contents: read - + contents: read concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - + group: sonar-${{ github.ref }}-${{ github.event_name }} + cancel-in-progress: true jobs: - sonar: - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - # Sonar uses git history for blame and new-code detection. - fetch-depth: 0 - persist-credentials: false - - - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4 - - - run: npm ci - - # Coverage is produced here rather than pulled from the CI workflow, so - # this job stays independent of cross-workflow artifact plumbing. - - run: just coverage - - # A secret cannot be read from a job-level `if`, so the check has - # to happen in a step. Without it this workflow is permanently red - # on a repository whose Sonar project has not been created yet, - # which trains everyone to ignore a failing check — the one habit - # that makes every other check worthless. - - name: Check whether Sonar is configured - id: sonar - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: echo "configured=${SONAR_TOKEN:+true}" >> "$GITHUB_OUTPUT" - - - if: steps.sonar.outputs.configured == 'true' - uses: SonarSource/sonarqube-scan-action@ba9859eae8dd6bd29e412f25ddbbef3d032000f4 # v8.2.2 - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - - - if: steps.sonar.outputs.configured != 'true' - run: | - echo "::notice::Sonar is not configured for this repository." - echo "::notice::Import it at sonarcloud.io and add SONAR_TOKEN to the repository secrets." + sonar: + name: sonar + runs-on: ubuntu-24.04 + timeout-minutes: 25 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 + with: + version: 2026.9.1 + - name: Require Sonar configuration + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: test -n "$SONAR_TOKEN" || { echo '::error::SONAR_TOKEN is required; scan cannot be skipped'; exit 1; } + - run: npm ci --ignore-scripts + - run: just coverage + - name: Analyze the full checked branch + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + REVIEW: ${{ github.event.pull_request.number }} + BRANCH: ${{ github.ref_name }} + run: | + branch="$BRANCH" + if [ -n "$REVIEW" ]; then branch="review-$REVIEW"; fi + revision="$(git rev-parse HEAD)" + project="Ghost-Assembly_$(jq -r .name package.json)" + sonar-scanner -Dsonar.projectKey="$project" -Dsonar.branch.name="$branch" -Dsonar.scm.revision="$revision" -Dsonar.qualitygate.wait=true + python3 scripts/sonar_gate.py --project "$project" --branch "$branch" --revision "$revision" diff --git a/.github/workflows/template.yml b/.github/workflows/template.yml new file mode 100644 index 0000000..5feedf4 --- /dev/null +++ b/.github/workflows/template.yml @@ -0,0 +1,23 @@ +name: Template freshness +on: + workflow_dispatch: + schedule: + - cron: '47 5 * * 1' +permissions: + contents: read +concurrency: + group: template-${{ github.ref }} + cancel-in-progress: true +jobs: + template: + name: template + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 + with: + version: 2026.9.1 + - run: just template-check template-status diff --git a/.gitignore b/.gitignore index 5d63bce..9563c4d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,9 @@ schemas/gschemas.compiled *.shell-extension.zip test-results/ playwright-report/ +__pycache__/ +.ruff_cache/ +.scannerwork/ +.env +.env.* +.superpowers/ diff --git a/.gitleaks.toml b/.gitleaks.toml index d6d2c5b..5474857 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -1,16 +1,2 @@ -# gitleaks configuration. -# -# `just security` runs `gitleaks detect`, which scans committed history, so a -# false positive here fails CI on every push rather than once. - [extend] useDefault = true - -[[allowlists]] -description = "Sonar project key and organization are public identifiers, not credentials. They are the values SonarQube Cloud assigns when the project is imported and are visible on the public dashboard; the actual credential is SONAR_TOKEN, which lives in GitHub Actions secrets. gitleaks' generic-api-key rule matches them only because the setting is spelled 'projectKey'." -paths = ['''^sonar-project\.properties$'''] -regexes = ['''sonar\.(projectKey|organization)\s*=.*'''] - -[[allowlists]] -description = "Unit tests feed parsers fake password, secret and passphrase keys on purpose: they are what proves a parser drops them. The values are literal placeholders, not credentials." -paths = ['''^tests/.*\.test\.js$'''] diff --git a/.prettierignore b/.prettierignore index f25a2ea..63b90a2 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,8 +4,5 @@ schemas/gschemas.compiled package-lock.json test-results/ playwright-report/ - -# SDD workspace scratch. Ignored by its own nested .gitignore -# (.superpowers/sdd/.gitignore), which Prettier does not read, so it must be -# named here too or `just lint` fails on any machine where it happens to exist. +.scannerwork/ .superpowers/ diff --git a/.prettierrc.json b/.prettierrc.json index c651b1f..4888c42 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,7 +1,10 @@ { - "singleQuote": true, - "printWidth": 88, - "tabWidth": 4, - "semi": true, - "arrowParens": "avoid" + "singleQuote": true, + "printWidth": 88, + "tabWidth": 4, + "semi": true, + "arrowParens": "avoid", + "overrides": [ + { "files": ["*.yml", "*.yaml", "*.json"], "options": { "tabWidth": 2 } } + ] } diff --git a/AGENTS.md b/AGENTS.md index 0c142df..2972a3c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,37 +7,42 @@ anyone — human or agent — changing it. ## What gets published -- The installable artifact is `quicktiler@napalm255.github.io.shell-extension.zip`, - built by `just build` (plain `zip`, not `gnome-extensions pack` — CI has no - GNOME) and attached to a GitHub release by `.github/workflows/release.yml` - when a `vX.Y.Z` tag is pushed. -- Docs are published at https://ghost-assembly.com/quicktiler/, served - straight from `docs/` on `main` (GitHub Pages branch deploy — no build step, - no Actions workflow for it). Whatever is committed under `docs/` is what a - visitor sees on the next push to `main`. -- A tag is what triggers a release, not a push to `main`. `release.yml` - checks the tag against `metadata.json`'s `version-name` and `package.json`'s - `version` before building anything, and refuses to ship if either disagrees. +- `just build` produces `quicktiler@napalm255.github.io.shell-extension.zip`. + A `vX.Y.Z` tag triggers `.github/workflows/release.yml`, which verifies + version agreement, main ancestry, and successful CI for the exact commit, + then publishes that tested artifact without rebuilding it. +- Docs at https://ghost-assembly.com/quicktiler/ are deployed by the Pages + workflow from the tested `docs/` artifact after all required checks pass on main. +- GNOME Extension Store submission and review remain manual. ## Commands -| Command | Does | -| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| `just setup` | mise install, npm ci, Playwright browsers, checks system tools exist | -| `just fmt` | `prettier --write`, `eslint --fix` | -| `just lint` | template-check, eslint, prettier --check, gschema, shellcheck | -| `just template-check [--write]` | the shared files (`template.list`) against `template.sha256` | -| `just test` | Vitest unit suite, on plain Node | -| `just test-docs` | the docs site in Chromium and Firefox: accessibility (axe), no JS, no third-party requests, 360px | -| `just coverage` | the unit suite with a coverage report | -| `just test-live` | headless Shell smoke test, then the packer check — needs a real GNOME Shell, never runs in CI | -| `just pack-check` | the built zip against `gnome-extensions pack` | -| `just security` | osv-scanner, gitleaks, trivy, actionlint, zizmor | -| `just build` | the installable zip | -| `just run` | a Shell in a window, via `mutter-devkit` — needs a real Shell | -| `just install` / `enable` / `disable` / `prefs` / `logs` | try it against your own logged-in Shell | -| `just ci` | lint, test, test-docs, security, build — run this before claiming anything done | -| `just clean` | remove build/test output | +Tool versions live in `mise.toml`; common commands live in the canonical +`justfile`; project-specific commands and hooks live in `project.just`. +Run `just ci` before claiming a change works. + +| Command | Does | +| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| `just setup` | Install pinned tools, npm development dependencies, and Chromium/Firefox; check host tools | +| `just fmt` | Format JavaScript, Python, configuration, and generated documentation | +| `just lint` | Verify canonical files, generated docs, ESLint, Prettier, Ruff, schemas, and shell scripts | +| `just template-check` | Compare managed files with the immutable GitHub revision in `quick-template.lock.json` | +| `just template-sync SHA` | Synchronize a reviewed canonical revision; then install dependencies and regenerate docs | +| `just test` | Run Vitest, Python tooling tests, and project offline integration tests | +| `just coverage` | Measure all runtime JavaScript, including untested files | +| `just test-docs` | Check docs in Chromium and Firefox, including axe accessibility audits | +| `just security` | Run OSV, source and history secret scans, Trivy, actionlint, and Zizmor | +| `just build` | Build a deterministic runtime-only ZIP with Python's standard library | +| `just pack-check` | Compare every ZIP filename and byte with GNOME's official packer; validate icons | +| `just test-live` | Check packaging, then isolated GNOME lifecycle and project integration hooks | +| `just run` | Run GNOME Shell in a development window | +| `just install` / `enable` / `disable` / `uninstall` / `prefs` / `logs` | Work with the extension in your logged-in session | +| `just docs` | Serve the static site at localhost:8000 | +| `just ci` | Run lint, tests, coverage, docs, security, and packaging; GitHub also requires CodeQL and Sonar | +| `just clean` | Confirm before removing generated build and test output | + +Live checks require an installed GNOME Shell and run outside hosted CI. +Complete the manual checklist and test each declared GNOME version before releasing. ## Hard constraints @@ -61,12 +66,8 @@ anyone — human or agent — changing it. the two of them. - No JavaScript ships on the docs pages. `just test-docs` fails the build if any does. -- The shared template files (everything listed in `template.list`) are - byte-locked: `just template-check` fails if they drift from - `template.sha256`. Don't hand-edit them. Project-specific CSS goes in - `docs/project.css`, project-only `just` recipes would go in `project.just` - (quicktiler doesn't need one), and this repo's docs sections/URLs live in - `tests/docs.config.js`. +- Shared tooling comes from the pinned canonical `quick-template` revision. + Keep local hooks in `project.just` and generated documentation current. ## Tests @@ -77,12 +78,12 @@ Failing test first (RED), then make it pass (GREEN). Layers: `tests/stubs/shell-*.js`) and the fake Mutter in `tests/support/actors.js`. - `scripts/headless-check.sh` — boots a throwaway headless GNOME Shell and checks that enable/disable/re-enable produces no JS error and no leaked - signal. Only `just test-live`; never CI, because CI has no Shell. + signal. Only `just test-live`; hosted CI does not boot an isolated Shell session. - `tests/docs.spec.js` (shared across the extensions) + `tests/docs.config.js` (this repo's title, site URL, repo URL and section list) — Playwright: accessibility in both color schemes, no JavaScript, no request to another origin, no sideways scroll at 360px. -- `scripts/pack-check.sh` — the zip `just build` makes against what +- `scripts/build.py --check` — the zip `just build` makes against what `gnome-extensions pack` would produce, so the two cannot drift. ## Conventions @@ -105,28 +106,25 @@ Failing test first (RED), then make it pass (GREEN). Layers: not just this repo. - The hub repo's `site.spec.js` and card content need updating alongside any change here that changes the one-liner or what's true about QuickTiler. -- Shared template files arrive here as a `chore:` sync commit from the +- Shared template files arrive here as a reviewed sync commit from the canonical template; never edit them directly in this repo (see Hard constraints and Template files below). ## Template files -`template.list` names every file this repo shares byte-for-byte with the -other quick* extensions; `template.sha256` is the manifest `just -template-check` verifies them against. Regenerate it with `just -template-check --write` after a legitimate sync, never by hand-editing the -hash. Anything quicktiler needs that the shared files don't cover goes in: - -- `docs/project.css` — this repo's additions to the shared `docs/style.css`. -- `project.just` — project-only `just` recipes, imported by the shared - `justfile`'s `import? 'project.just'` (quicktiler has none today; the - import silently tolerates that). -- `tests/docs.config.js` — this repo's docs title, site URL, repository URL - and section list, read by the shared `tests/docs.spec.js`. - -A change that should apply to every quick* extension belongs in the shared -template, synced out to each repo as its own `chore:` commit — never patched -into one repo's copy alone. +`quick-template.lock.json` pins a full commit SHA from +`Ghost-Assembly/quick-template`. `just template-check` compares managed files +with that immutable GitHub archive; a local manifest cannot approve drift. +Change shared tooling in the canonical repository, then run +`just template-sync SHA`, `npm ci --ignore-scripts`, `just docs-generate`, and +`just ci` in this checkout. The weekly freshness check reports newer approved +releases without adopting them automatically. + +Project hooks belong in `project.just`, runtime packaging inputs in +`quick-project.json`, documentation identity in `docs/project.json`, and local +styling in `docs/project.css`. Common README and site sections are generated; +keep extension-specific content outside their markers. Lifecycle test scripts +remain specific to the extension. ## Settings keys diff --git a/README.md b/README.md index 5951ac5..5c4c31c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,21 @@ # QuickTiler + + +[![CI](https://github.com/Ghost-Assembly/quicktiler/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/Ghost-Assembly/quicktiler/actions/workflows/ci.yml) +[![Security](https://github.com/Ghost-Assembly/quicktiler/actions/workflows/security.yml/badge.svg?branch=main)](https://github.com/Ghost-Assembly/quicktiler/actions/workflows/security.yml) +[![Docs](https://img.shields.io/website?url=https%3A%2F%2Fghost-assembly.com%2Fquicktiler%2F&label=docs)](https://ghost-assembly.com/quicktiler/) +[![Release](https://img.shields.io/github/v/release/Ghost-Assembly/quicktiler)](https://github.com/Ghost-Assembly/quicktiler/releases/latest) +[![License](https://img.shields.io/github/license/Ghost-Assembly/quicktiler)](https://github.com/Ghost-Assembly/quicktiler/blob/main/LICENSE) +[![GNOME](https://img.shields.io/badge/GNOME-49%20%7C%2050-blue)](https://ghost-assembly.com/quicktiler/#install) +[![Security issues](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fsonarcloud.io%2Fapi%2Fmeasures%2Fcomponent%3Fcomponent%3DGhost-Assembly_quicktiler%26metricKeys%3Dsoftware_quality_security_issues&query=%24.component.measures%5B0%5D.value&label=Security+issues)](https://sonarcloud.io/dashboard?id=Ghost-Assembly_quicktiler) +[![Reliability issues](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fsonarcloud.io%2Fapi%2Fmeasures%2Fcomponent%3Fcomponent%3DGhost-Assembly_quicktiler%26metricKeys%3Dsoftware_quality_reliability_issues&query=%24.component.measures%5B0%5D.value&label=Reliability+issues)](https://sonarcloud.io/dashboard?id=Ghost-Assembly_quicktiler) +[![Maintainability issues](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fsonarcloud.io%2Fapi%2Fmeasures%2Fcomponent%3Fcomponent%3DGhost-Assembly_quicktiler%26metricKeys%3Dsoftware_quality_maintainability_issues&query=%24.component.measures%5B0%5D.value&label=Maintainability+issues)](https://sonarcloud.io/dashboard?id=Ghost-Assembly_quicktiler) +[![Duplication](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fsonarcloud.io%2Fapi%2Fmeasures%2Fcomponent%3Fcomponent%3DGhost-Assembly_quicktiler%26metricKeys%3Dduplicated_lines_density&query=%24.component.measures%5B0%5D.value&label=Duplication)](https://sonarcloud.io/dashboard?id=Ghost-Assembly_quicktiler) +[![Coverage](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fsonarcloud.io%2Fapi%2Fmeasures%2Fcomponent%3Fcomponent%3DGhost-Assembly_quicktiler%26metricKeys%3Dcoverage&query=%24.component.measures%5B0%5D.value&label=Coverage)](https://sonarcloud.io/dashboard?id=Ghost-Assembly_quicktiler) +[![Sonar policy](https://github.com/Ghost-Assembly/quicktiler/actions/workflows/sonar.yml/badge.svg?branch=main)](https://sonarcloud.io/dashboard?id=Ghost-Assembly_quicktiler) + + Keyboard-driven zone tiling with a Quick Settings tile, no overlay and no timers. Press a direction repeatedly and the focused window cycles through the zones on @@ -72,63 +88,119 @@ tile because it has to be — with the tile hidden, it is the only way back. ## Install -Needs GNOME Shell 49 or 50. From the latest release, with no clone and no -toolchain — `gnome-extensions` ships with GNOME Shell itself: + + +Requires GNOME Shell 49 or 50. Requires a GNOME desktop session; window behavior also depends on each application and its minimum size. + +### From a release + +Download the latest release ZIP and install it for your user. xh is a download tool; you can also download the ZIP from GitHub in a browser. Installing compiles the settings schema. -```bash -curl -LO https://github.com/Ghost-Assembly/quicktiler/releases/latest/download/quicktiler@napalm255.github.io.shell-extension.zip +```sh +xh --download GET https://github.com/Ghost-Assembly/quicktiler/releases/latest/download/quicktiler@napalm255.github.io.shell-extension.zip gnome-extensions install --force quicktiler@napalm255.github.io.shell-extension.zip ``` -That unpacks the extension and compiles its settings schema, so there is no -separate `glib-compile-schemas` step. Log out and back in — Wayland cannot -reload the Shell in place — then turn it on: +Log out and back in so GNOME discovers the extension, then enable it: -```bash +```sh gnome-extensions enable quicktiler@napalm255.github.io ``` -From a clone: +### From a clone -```bash -just setup -just install -just enable +Install mise and activate it in your shell. Clone the repository, install its pinned tools, and build and install the same ZIP used for releases: + +```sh +git clone https://github.com/Ghost-Assembly/quicktiler.git +cd quicktiler +mise install +mise exec -- just setup +mise exec -- just install ``` -Log out and back in if the Shell does not pick it up. `just prefs` opens the -preferences window, `just logs` follows the extension's output, and -`just disable` turns it off again without uninstalling. +Log out and back in, then run just enable. Run just prefs to open preferences. After updating a loaded extension, start a new session to load its new code; opening preferences does not reload GNOME Shell. + -## Development +## Uninstall + + + +Disable and uninstall the extension for your user. These commands preserve saved settings and other user data. -```bash -just # list every recipe -just test # unit suite, runs on Node in about a fifth of a second -just test-docs # the docs site in Chromium and Firefox -just lint # eslint, prettier, gschema and shellcheck -just ci # everything CI runs -just test-live # headless Shell smoke test, then the packer check -just docs # serve the documentation site locally +```sh +gnome-extensions disable quicktiler@napalm255.github.io +gnome-extensions uninstall quicktiler@napalm255.github.io ``` -`extension.js` is the entry point: it is deliberately thin, owning a settings -object, a `QuickTiler` and a `Panel`, and pairing each construction with its -teardown. `modules/zones.js`, `windows.js`, `neighbors.js`, `actions.js`, -`shortcuts.js`, `accelerator.js` and `settings.js` import nothing at all, so -Vitest runs them on plain Node. `modules/quicktiler.js` is the only file that -touches Meta or Shell, and `modules/panel.js` the only one that touches St, -Clutter, PopupMenu or QuickSettings; both are unit-tested through stubs aliased -in `vitest.config.js`. The -[architecture](https://ghost-assembly.com/quicktiler/#architecture) and -[testing](https://ghost-assembly.com/quicktiler/#testing) sections of the -documentation go into why. +From a clone, just uninstall performs the same steps. Disabling with just disable leaves the extension installed. + + +## Testing + + + +just test runs the JavaScript suite with Vitest, the shared tooling tests, and any project-specific offline suites. just coverage reports the JavaScript coverage universe, including untested runtime files. Test stubs and generated reports are not runtime source. + +just test-docs runs Playwright and axe in Chromium and Firefox: dark and light accessibility checks, keyboard navigation, mobile layout, reduced motion, links, metadata, local assets, and no page JavaScript. Automated accessibility checks still require human review of reading and focus order. + +just test-live checks the package and runs isolated GNOME lifecycle checks. It is a separate local check, not proof of compatibility from a hosted runner. Verify each declared GNOME version and complete the project's manual checks before releasing. + + +### Project checks + +Recording Mutter stubs model maximized windows, deferred Wayland frame changes, client minimum sizes, and signal lifetimes. The isolated Shell check verifies enable/disable/re-enable and lifetime cleanup. Verify tiling geometry, monitors, minimum sizes, and shortcuts with real applications before release. + +## Packaging + + + +```sh +just build +just pack-check +``` + +The output is quicktiler@napalm255.github.io.shell-extension.zip at the repository root, with metadata.json at the archive root. Python's standard library packages the explicit runtimeFiles allowlist in quick-project.json, using stable file order and timestamps. + +just pack-check compares both filenames and file contents with GNOME's official packer and validates shipped icons. Docs, tests, dependencies, credentials, downloaded binaries, and development artifacts stay outside the ZIP. Update the runtime allowlist when adding a runtime file. + ## Releasing -Set the version in `metadata.json` and `package.json`, commit, then tag and -push. CI checks the tag against both files before it builds anything. See -[releasing](https://ghost-assembly.com/quicktiler/#releasing). + + +Run just ci, just test-live, and the project manual checklist. Set metadata.json version-name and package.json version to the same new version and increment metadata.json version for the GNOME Extension Store. Update the npm lockfile, regenerate the docs, and commit the reviewed changes to main through a passing pull request. + +Create and push a v-prefixed tag for that version. The release workflow verifies the version, main ancestry, and successful required checks for the tagged commit, then attaches its tested ZIP to a GitHub release. It does not upload to extensions.gnome.org; that submission and its review remain manual. + + +## Development + + + +mise.toml pins runtime and CLI versions; justfile owns commands; npm owns development dependencies and the lockfile. GNOME libraries come from the host. On image-based Fedora, use the host's available tools or a toolbox/distrobox for missing system packages; do not layer packages onto the OS. + +```sh +just setup # install pinned tools, dependencies, and browsers +just fmt # format source and configuration +just lint # verify template, generated docs, source, and schemas +just test # JavaScript, Python, and project offline tests +just coverage # report JavaScript coverage without source exclusions +just test-docs # Chromium and Firefox documentation checks +just security # dependencies, secrets, and workflow checks +just build # build the runtime-only extension ZIP +just pack-check # compare files and contents with GNOME's packer +just ci # complete local verification and packaging +just test-live # isolated GNOME lifecycle and project integration checks +just docs # serve the static site at localhost:8000 +just template-check # verify the pinned canonical template +just template-status # report a newer approved template revision +``` + +GitHub requires local verification, security analysis, and completed Sonar analysis. The shared Sonar policy requires zero security, reliability, and maintainability issues and zero duplicated lines. Missing configuration fails instead of silently skipping analysis. Pages publishes the tested docs only after the required checks pass on main. + +Common tooling and these instructions are generated from a pinned canonical template. Change that source and synchronize its approved revision; do not edit generated sections or locally bless drift. Extension-specific behavior belongs in project configuration and project.just. + ## License diff --git a/docs/index.html b/docs/index.html index 2cb72f2..c4e9491 100644 --- a/docs/index.html +++ b/docs/index.html @@ -373,7 +373,16 @@

QuickTiler

-
-

02

Install

-

Requires

- + +

+ Requires GNOME Shell 49 or 50. Requires a GNOME desktop session; + window behavior also depends on each application and its minimum + size. +

From a release

+

+ Download the latest release ZIP and install it for your user. xh + is a download tool; you can also download the ZIP from GitHub in + a browser. Installing compiles the settings schema. +

-
- Shell -
-
$ curl -LO https://github.com/Ghost-Assembly/quicktiler/releases/latest/download/quicktiler@napalm255.github.io.shell-extension.zip
-$ gnome-extensions install --force quicktiler@napalm255.github.io.shell-extension.zip
-# log out and back in, then:
-$ gnome-extensions enable quicktiler@napalm255.github.io
+
Shell
+
xh --download GET https://github.com/Ghost-Assembly/quicktiler/releases/latest/download/quicktiler@napalm255.github.io.shell-extension.zip
+gnome-extensions install --force quicktiler@napalm255.github.io.shell-extension.zip

- gnome-extensions install unpacks the extension and - compiles its settings schema, so there is no separate - glib-compile-schemas step. + Log out and back in so GNOME discovers the extension, then + enable it:

+
+
Shell
+
gnome-extensions enable quicktiler@napalm255.github.io
+

From a clone

+

+ Install mise and activate it in your shell. Clone the + repository, install its pinned tools, and build and install the + same ZIP used for releases: +

-
- Shell -
-
$ just setup
-$ just install
-$ just enable
+
Shell
+
git clone https://github.com/Ghost-Assembly/quicktiler.git
+cd quicktiler
+mise install
+mise exec -- just setup
+mise exec -- just install

- just prefs opens the preferences window, - just logs follows the extension’s output, and - just disable turns it off without uninstalling. + Log out and back in, then run just enable. Run just prefs to + open preferences. After updating a loaded extension, start a new + session to load its new code; opening preferences does not + reload GNOME Shell.

-
- -

- Log out and back in. A newly installed - extension is only picked up when the Shell next starts. On - Wayland that means a new session. -

+ +
+
+

03

+

Uninstall

+ +

+ Disable and uninstall the extension for your user. These + commands preserve saved settings and other user data. +

+
+
Shell
+
gnome-extensions disable quicktiler@napalm255.github.io
+gnome-extensions uninstall quicktiler@napalm255.github.io
+

+ From a clone, just uninstall performs the same steps. Disabling + with just disable leaves the extension installed. +

+
-
-

03

+

04

Zones

Both common ultrawide layouts — 1/4 + 1/2 + 1/4 and @@ -868,13 +814,12 @@

The gap

pixel: no seam between thirds, and no overlap on an odd gap.

-
-

04

+

05

Quick Settings

The tile sits in the system menu, beside the volume and network @@ -935,13 +880,12 @@

Quick Settings

-
-

05

+

06

Preferences

Open them from the Extensions app, from the tile’s Settings row, @@ -1004,9 +948,8 @@

Preferences

that.

-
-

06

+

07

Keyboard

Every default was checked against the 90 Super accelerators @@ -1146,13 +1089,12 @@

Neighbors and monitors

vertical navigation yet.

-
-

07

+

08

Architecture

Every decision is testable off the Shell. The pure modules @@ -1318,168 +1260,145 @@

Architecture

but never the Shell layer.

-
-

08

+

09

Testing

+

- The Shell layer is unit-tested too. - vitest.config.js aliases the gi:// and - resource:/// imports to stubs, and - tests/support is a fake Mutter that models what the - extension depends on: + just test runs the JavaScript suite with Vitest, the shared + tooling tests, and any project-specific offline suites. just + coverage reports the JavaScript coverage universe, including + untested runtime files. Test stubs and generated reports are not + runtime source.

-

- just test-live adds what CI cannot have. It boots a - throwaway headless GNOME Shell and checks that the extension - enables, disables and enables again with no JavaScript error and - no leaked signal; it is a lifetime check, not a geometry one. It - then runs the packaging check below. just test-docs - holds this site to its rules in Chromium and Firefox: - accessibility in both color schemes, no JavaScript, no other - origin, and no sideways scrolling on a phone. + just test-docs runs Playwright and axe in Chromium and Firefox: + dark and light accessibility checks, keyboard navigation, mobile + layout, reduced motion, links, metadata, local assets, and no + page JavaScript. Automated accessibility checks still require + human review of reading and focus order.

- prefs.js has no coverage, by - design. Everything it used to decide lives in - shortcuts.js; what is left is widget construction, - which a unit test could only check against stubs of the toolkit. - The exclusion is written in both - vitest.config.js and - sonar-project.properties so the two agree. + just test-live checks the package and runs isolated GNOME + lifecycle checks. It is a separate local check, not proof of + compatibility from a hosted runner. Verify each declared GNOME + version and complete the project's manual checks before + releasing. +

+ +

Project checks

+

+ Recording Mutter stubs model maximized windows, deferred Wayland + frame changes, client minimum sizes, and signal lifetimes. The + isolated Shell check verifies enable/disable/re-enable and + lifetime cleanup. Verify tiling geometry, monitors, minimum + sizes, and shortcuts with real applications before release.

-
-

09

+

10

Packaging

+ +
+
Shell
+
just build
+just pack-check
+

- An extension ships as a plain zip, conventionally named - <uuid>.shell-extension.zip, with - metadata.json at the archive root. - just build makes it with zip rather - than GNOME’s gnome-extensions pack, because that - tool comes with the gnome-shell package and CI has - no GNOME. -

-

- The official packer stays the authority all the same: - just pack-check runs it and fails on any difference - from the built zip, so the two cannot drift. + The output is + quicktiler@napalm255.github.io.shell-extension.zip + at the repository root, with metadata.json at the archive root. + Python's standard library packages the explicit + runtimeFiles allowlist in quick-project.json, using stable file + order and timestamps.

- Every shipped icon is decoded, not just packed. - An icon that fails to load is silent — the Shell draws nothing - and logs nothing. A comment placed before the - <svg> element is enough to break gdk-pixbuf’s - format sniffing, so the pack check loads each icon at 16×16 and - fails if it does not decode to something visible. + just pack-check compares both filenames and file contents with + GNOME's official packer and validates shipped icons. Docs, + tests, dependencies, credentials, downloaded binaries, and + development artifacts stay outside the ZIP. Update the runtime + allowlist when adding a runtime file.

+
-
-

10

+

11

Releasing

+

- Set version-name in metadata.json and - version in package.json, commit, then - tag and push. Run just test-live first: neither the - headless check nor the packer check runs in CI. + Run just ci, just test-live, and the project manual checklist. + Set metadata.json version-name and package.json version to the + same new version and increment metadata.json version for the + GNOME Extension Store. Update the npm lockfile, regenerate the + docs, and commit the reviewed changes to main through a passing + pull request.

-
-
- Shell -
-
$ git tag -a v0.2.3 -m 'release v0.2.3'
-$ git push origin v0.2.3
-

- The release workflow checks the tag against both files and stops - before building if they disagree — a release whose zip tells - GNOME it is another version is worse than none. It then runs - just ci and attaches the zip to a GitHub release. -

-

- Uploading to extensions.gnome.org is deliberately left out of - CI. gnome-extensions upload authenticates with the - account password rather than a scoped token, and every upload - goes to human review anyway. + Create and push a v-prefixed tag for that version. The release + workflow verifies the version, main ancestry, and successful + required checks for the tagged commit, then attaches its tested + ZIP to a GitHub release. It does not upload to + extensions.gnome.org; that submission and its review remain + manual.

+
-
-

11

+

12

Development

+ +

+ mise.toml pins runtime and CLI versions; justfile owns commands; + npm owns development dependencies and the lockfile. GNOME + libraries come from the host. On image-based Fedora, use the + host's available tools or a toolbox/distrobox for missing + system packages; do not layer packages onto the OS. +

-
- justfile -
-
just              # list every recipe
-just setup        # tools, dependencies and test browsers
-just fmt          # prettier and eslint --fix, in place
-just test         # unit suite, on Node
-just test-docs    # this site, in Chromium and Firefox
-just coverage     # the unit suite with a coverage report
-just lint         # eslint, prettier, gschema, shellcheck
-just security     # osv-scanner, gitleaks, trivy, actionlint, zizmor
-just build        # the installable zip
-just ci           # what CI runs: lint, test, test-docs, security, build
-just test-live    # headless Shell check, then the packer check
-just run          # a Shell in a window, via mutter-devkit
-just docs         # serve this site on localhost:8000
+
Shell
+
just setup        # install pinned tools, dependencies, and browsers
+just fmt          # format source and configuration
+just lint         # verify template, generated docs, source, and schemas
+just test         # JavaScript, Python, and project offline tests
+just coverage     # report JavaScript coverage without source exclusions
+just test-docs    # Chromium and Firefox documentation checks
+just security     # dependencies, secrets, and workflow checks
+just build        # build the runtime-only extension ZIP
+just pack-check   # compare files and contents with GNOME's packer
+just ci           # complete local verification and packaging
+just test-live    # isolated GNOME lifecycle and project integration checks
+just docs         # serve the static site at localhost:8000
+just template-check  # verify the pinned canonical template
+just template-status # report a newer approved template revision

- mise.toml pins the toolchain, so - just ci behaves the same on a laptop and on a - runner. gjs, glib-compile-schemas, - gnome-shell and gnome-extensions come - from the system instead, because they have to match the Shell - being targeted; just setup checks for them. + GitHub requires local verification, security analysis, and + completed Sonar analysis. The shared Sonar policy requires zero + security, reliability, and maintainability issues and zero + duplicated lines. Missing configuration fails instead of + silently skipping analysis. Pages publishes the tested docs only + after the required checks pass on main. +

+

+ Common tooling and these instructions are generated from a + pinned canonical template. Change that source and synchronize + its approved revision; do not edit generated sections or locally + bless drift. Extension-specific behavior belongs in project + configuration and project.just.

+
diff --git a/docs/project.json b/docs/project.json new file mode 100644 index 0000000..4b5805c --- /dev/null +++ b/docs/project.json @@ -0,0 +1,5 @@ +{ + "$schema": "project.schema.json", + "repository": "quicktiler", + "requirements": "Requires a GNOME desktop session; window behavior also depends on each application and its minimum size." +} diff --git a/docs/project.schema.json b/docs/project.schema.json new file mode 100644 index 0000000..513bdd2 --- /dev/null +++ b/docs/project.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": false, + "required": ["repository", "requirements"], + "properties": { + "$schema": { "type": "string" }, + "repository": { + "enum": [ + "quickclip", + "quickmusic", + "quickrem", + "quickspot", + "quicktiler", + "quickts" + ] + }, + "requirements": { "type": "string" } + } +} diff --git a/docs/style.css b/docs/style.css index 0c7c5de..9cdb3f7 100644 --- a/docs/style.css +++ b/docs/style.css @@ -663,8 +663,7 @@ svg { text-transform: uppercase; } -.toc ol, -.toc-m ol { +.toc ol { margin: 0; padding: 0; list-style: none; @@ -674,8 +673,7 @@ svg { border-inline-start: 1px solid var(--line); } -.toc a, -.toc-m a { +.toc a { display: flex; align-items: center; gap: 12px; @@ -691,15 +689,13 @@ svg { border-inline-start: 2px solid transparent; } -.toc a .n, -.toc-m a .n { +.toc a .n { min-width: 20px; font-family: var(--mono); font-size: 12px; } -.toc a:hover, -.toc-m a:hover { +.toc a:hover { color: var(--ink); background: var(--raised); } @@ -731,19 +727,18 @@ svg { color: var(--ink); } -/* The phone's contents list; hidden until the breakpoint below. */ -.toc-m { +/* One contents list serves both layouts; this native toggle is phone-only. */ +.toc-controls { display: none; - margin: 0 0 48px; border: 1px solid var(--line); border-radius: 12px; background: var(--raised); } -.toc-m summary { +.toc-controls label { display: flex; align-items: center; - justify-content: space-between; + gap: 12px; min-height: 52px; padding-inline: 16px; color: var(--mist); @@ -751,21 +746,13 @@ svg { font-size: 13px; letter-spacing: 0.12em; text-transform: uppercase; - list-style: none; cursor: pointer; } -.toc-m summary::-webkit-details-marker { - display: none; -} - -.toc-m[open] summary svg { - transform: rotate(180deg); -} - -.toc-m ol { - padding: 6px 4px 10px; - border-block-start: 1px solid var(--line); +.toc-controls input { + width: 20px; + height: 20px; + accent-color: var(--link); } .content { @@ -922,7 +909,8 @@ svg { background: var(--sunken); } -.code header { +.code header, +.code-label { display: flex; align-items: center; justify-content: space-between; @@ -951,7 +939,8 @@ svg { .code pre { margin: 0; padding: 18px 20px; - overflow-x: auto; + white-space: pre-wrap; + overflow-wrap: anywhere; color: #dfe6ff; font-family: var(--mono); font-size: 14px; @@ -1294,10 +1283,21 @@ svg { .toc { display: none; + position: static; + margin-block-end: 48px; } - .toc-m { + .toc-controls { display: block; + margin-block-end: 24px; + } + + .toc-controls:has(input:checked) + .toc { + display: block; + } + + .toc-title { + display: none; } .section + .section { diff --git a/eslint.config.js b/eslint.config.js index 11c1e7f..22e9338 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -68,7 +68,7 @@ export default [ // Tooling and tests: run on Node. files: ['tests/**/*.js', '*.config.js', 'eslint.config.js'], languageOptions: { - ecmaVersion: 2022, + ecmaVersion: 2025, sourceType: 'module', globals: globals.node, }, @@ -79,6 +79,15 @@ export default [ 'no-unused-vars': ['error', { argsIgnorePattern: '^_' }], }, }, + { + // Native integration and GTK smoke tests run in GJS, outside Node. + files: ['tests/gnome-*.js', 'tests/prefs_smoke.js'], + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module', + globals: gjsGlobals, + }, + }, { // The docs site's browser suite: Node, plus the callbacks it hands to // page.evaluate(), which run in the page. Scoped to this file alone; diff --git a/justfile b/justfile index e004d35..2ed9e2c 100644 --- a/justfile +++ b/justfile @@ -1,138 +1,133 @@ set shell := ["bash", "-euo", "pipefail", "-c"] - -# This file is shared, byte for byte, by every Ghost Assembly GNOME Shell -# extension (see template.list). Recipes only this project needs live in -# project.just, imported at the end. - -# Derived, so metadata.json is the only place the uuid is written down. Read -# with just's own functions rather than jq: every variable is evaluated before -# any recipe runs, so a jq here would stop `just setup` from ever getting as far -# as saying that jq is missing. The scripts still need jq, and setup checks it. _uuid := replace_regex(read("metadata.json"), '(?s)^.*?"uuid"\s*:\s*"([^"]+)".*$', '$1') -uuid := if _uuid =~ '^[A-Za-z0-9._@-]+$' { _uuid } else { error("no uuid in metadata.json") } -name := replace_regex(uuid, '@.*$', '') -install_dir := env_var('HOME') / ".local/share/gnome-shell/extensions" / uuid - -# stylesheet.css ships only in the projects that have one. -src := "metadata.json extension.js prefs.js modules schemas icons" + if path_exists("stylesheet.css") == "true" { " stylesheet.css" } else { "" } +uuid := if _uuid =~ '^[A-Za-z0-9._@-]+$' { _uuid } else { error("invalid extension UUID") } +_name := replace_regex(read("package.json"), '(?s)^.*?"name"\s*:\s*"([^"]+)".*$', '$1') +project_name := if _name =~ '^quick[a-z]+$' { _name } else { error("invalid project name") } -# List available recipes +# List project commands default: @just --list -# Install dependencies and dev tooling +# Install the exact toolchain, dependencies, and browser engines setup: mise install - npm ci - npx playwright install chromium firefox - @for pair in gjs:gjs glib-compile-schemas:glib2 gnome-shell:gnome-shell \ - gnome-extensions:gnome-shell rsync:rsync zip:zip unzip:unzip jq:jq; do \ - tool="${pair%%:*}"; package="${pair#*:}"; \ - command -v "$tool" >/dev/null \ - || { echo "missing $tool — dnf install $package"; exit 1; }; \ - done - @test -x /usr/libexec/mutter-devkit \ - || echo "optional: just run needs mutter-devkit — dnf install mutter-devkit" - @echo "ready" - -# Format code in place -fmt: - npx prettier --write . - npx eslint --fix . - -# Static analysis; changes nothing -lint: template-check - npx eslint . - npx prettier --check . - glib-compile-schemas --strict --dry-run schemas - shellcheck scripts/*.sh + npm ci --ignore-scripts + ./node_modules/.bin/playwright install chromium firefox + @for tool in gjs glib-compile-schemas gnome-extensions jq; do command -v "$tool" >/dev/null || { echo "missing host tool: $tool; use a development container if needed" >&2; exit 1; }; done -# Check the files shared across the extensions against template.sha256; --write regenerates it -template-check *args: - ./scripts/template-check.sh {{ args }} - -# Run the unit suite +# Format source and generated documentation +fmt: + ./node_modules/.bin/prettier --write . + ./node_modules/.bin/eslint --fix . + ruff format scripts tests + ruff check --fix scripts tests + python3 scripts/docs.py + +# Verify canonical files, documentation, source, and schemas +lint: template-check docs-check + ./node_modules/.bin/eslint --max-warnings=0 --no-inline-config . + ./node_modules/.bin/prettier --check . + ruff check --ignore-noqa scripts tests + ruff format --check scripts tests + /usr/bin/glib-compile-schemas --strict --dry-run schemas + @if compgen -G 'scripts/*.sh' >/dev/null; then shellcheck scripts/*.sh; fi + +# Verify against the immutable canonical template +template-check: + python3 scripts/template.py check + +# Adopt a reviewed canonical revision +template-sync $revision: + python3 scripts/template.py sync "$revision" + +# Report newer approved template revisions +template-status: + python3 scripts/template.py status + +# Regenerate shared instructions and README badges +docs-generate: + python3 scripts/docs.py + +# Fail on stale generated documentation +docs-check: + python3 scripts/docs.py --check + +# Run offline behavior tests and shared tooling regressions test *args: - npx vitest run {{ args }} + ./node_modules/.bin/vitest run {{args}} + python3 -m unittest discover -s tests -p 'test_*.py' -v + just test-extra -# The docs site in Chromium and Firefox: accessibility, layout, no JavaScript -test-docs *args: - npx playwright test {{ args }} - -# Unit suite with a coverage report +# Measure all JavaScript runtime source coverage: - npx vitest run --coverage - -# None of it runs in CI: it needs a real Shell, and whatever live-extra in -# project.just probes. Builds first, so pack-check never compares a stale zip. -# Smoke-test in a headless gnome-shell, check the bundle, run any live-extra -test-live: build - ./scripts/headless-check.sh - ./scripts/pack-check.sh - @if {{ just_executable() }} --justfile {{ justfile() }} --summary | tr ' ' '\n' | grep -qx live-extra; then \ - {{ just_executable() }} --justfile {{ justfile() }} live-extra; \ - fi - -# Compare the built zip against what gnome-extensions pack produces -pack-check: build - ./scripts/pack-check.sh + ./node_modules/.bin/vitest run --coverage -# Serve the documentation site locally -docs: - @echo "http://localhost:8000" - python3 -m http.server 8000 --directory docs +# Test static documentation in Chromium and Firefox +test-docs *args: + ./node_modules/.bin/playwright test {{args}} -# Full local security scan +# Check dependencies, working files, Git history, and workflow security security: osv-scanner scan source --lockfile=package-lock.json - gitleaks detect --no-banner --redact - trivy fs --scanners vuln,secret,misconfig --exit-code 1 . - actionlint - zizmor .github/workflows/ - -# `ci` runs lint before this; a standalone `just build` deliberately does not, -# so it stays quick to iterate with. -# Produce the installable zip + python3 scripts/security_source.py + gitleaks git --redact --no-banner . + python3 scripts/workflow_lint.py + zizmor --offline --persona auditor --no-ignores .github/workflows/ + +# Build an explicit runtime-only ZIP build: - rm -f {{ uuid }}.shell-extension.zip - zip -qr {{ uuid }}.shell-extension.zip {{ src }} -x 'schemas/gschemas.compiled' - @echo "built {{ uuid }}.shell-extension.zip" + python3 scripts/build.py + +# Compare every runtime file with GNOME's official packer +pack-check: build + python3 scripts/build.py --check + @for icon in icons/*.svg; do [[ ! -f "$icon" ]] || /usr/bin/gjs -m scripts/icon-check.js "$icon"; done -# GNOME 49 and later have no nested mode: --devkit opens the Shell in a -# window through mutter-devkit (dnf install mutter-devkit). -# Run a gnome-shell in a window to try the extension by hand +# Perform isolated lifecycle and project integration checks +test-live: pack-check + just live-check + just live-extra + +# Run GNOME in a development window run: - dbus-run-session -- gnome-shell --devkit --wayland + /usr/bin/dbus-run-session -- /usr/bin/gnome-shell --devkit --wayland -# Copy the extension into the user extensions directory -install: - mkdir -p {{ install_dir }} - rsync -a --delete --exclude '.git' {{ src }} {{ install_dir }}/ - glib-compile-schemas {{ install_dir }}/schemas +# Install the same ZIP used for releases +install: build + /usr/bin/gnome-extensions install --force '{{uuid}}.shell-extension.zip' -# Enable the extension +# Enable the installed extension enable: - gnome-extensions enable {{ uuid }} + /usr/bin/gnome-extensions enable '{{uuid}}' -# Disable the extension +# Disable the installed extension disable: - gnome-extensions disable {{ uuid }} + /usr/bin/gnome-extensions disable '{{uuid}}' + +# Remove the extension while preserving user data +uninstall: + just uninstall-extra + /usr/bin/gnome-extensions disable '{{uuid}}' + /usr/bin/gnome-extensions uninstall '{{uuid}}' -# Open the preferences window +# Open preferences prefs: - gnome-extensions prefs {{ uuid }} + /usr/bin/gnome-extensions prefs '{{uuid}}' -# Follow the extension's log output +# Follow GNOME Shell logs logs: - journalctl -f -o cat /usr/bin/gnome-shell | grep -i --line-buffered "{{ name }}" + journalctl --user -f -o cat /usr/bin/gnome-shell --grep '\[{{project_name}}\]' + +# Serve the static documentation site +docs: + python3 -m http.server 8000 --bind 127.0.0.1 --directory docs -# Remove build output -[confirm("remove node_modules, coverage, test output, the zip and compiled schemas?")] +# Remove generated output only +[confirm("Remove generated test and build output?")] clean: - rm -rf node_modules coverage test-results playwright-report - rm -f {{ uuid }}.shell-extension.zip schemas/gschemas.compiled + python3 scripts/clean.py -# Everything CI runs, in order -ci: lint test test-docs security build +# Run all local checks; GitHub additionally requires CodeQL and Sonar +ci: lint test coverage test-docs security pack-check -import? 'project.just' +import 'project.just' diff --git a/metadata.json b/metadata.json index e4e36bc..412fe8d 100644 --- a/metadata.json +++ b/metadata.json @@ -1,10 +1,10 @@ { - "uuid": "quicktiler@napalm255.github.io", - "name": "QuickTiler", - "description": "Keyboard-driven zone tiling with a Quick Settings tile, no overlay and no timers. Cycle windows through quarter, half and center zones.", - "shell-version": ["49", "50"], - "url": "https://github.com/Ghost-Assembly/quicktiler", - "settings-schema": "org.gnome.shell.extensions.quicktiler", - "gettext-domain": "quicktiler", - "version-name": "0.3.0" + "uuid": "quicktiler@napalm255.github.io", + "name": "QuickTiler", + "description": "Keyboard-driven zone tiling with a Quick Settings tile, no overlay and no timers. Cycle windows through quarter, half and center zones.", + "shell-version": ["49", "50"], + "url": "https://github.com/Ghost-Assembly/quicktiler", + "settings-schema": "org.gnome.shell.extensions.quicktiler", + "gettext-domain": "quicktiler", + "version-name": "0.3.0" } diff --git a/mise.toml b/mise.toml index 30dafc3..e445bd2 100644 --- a/mise.toml +++ b/mise.toml @@ -1,20 +1,17 @@ -# Toolchain versions only. Commands live in the justfile. -# -# gjs, glib-compile-schemas and gnome-shell are deliberately absent: they are -# system packages tied to the running Shell and must match it, so mise must not -# shadow them. `just setup` verifies they exist instead. +# Exact tool versions shared by every Quick extension. [tools] -# Node's major is pinned so a new LTS line is a deliberate change; the other -# tools track "latest" and are listed here so a laptop and a runner install the -# same set. -node = "24" -just = "latest" +node = "24.21.0" +python = "3.14.7" +just = "1.58.0" +ruff = "0.16.9" +actionlint = "1.7.12" +gitleaks = "8.30.1" +osv-scanner = "2.6.0" +shellcheck = "0.11.0" +trivy = "0.74.0" +zizmor = "1.30.1" +gh = "2.99.0" +sonar-scanner-cli = "8.1.0.6389" -# Security and lint tooling. Scanners track "latest" on purpose — pinning a -# scanner also pins its vulnerability data, which defeats the point of running it. -actionlint = "latest" -gitleaks = "latest" -osv-scanner = "latest" -shellcheck = "latest" -trivy = "latest" -zizmor = "latest" +[env] +UV_NO_MANAGED_PYTHON = "1" diff --git a/package-lock.json b/package-lock.json index 9a59712..31ddbec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,2435 +1,2417 @@ { - "name": "quicktiler", - "version": "0.3.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "quicktiler", - "version": "0.3.0", - "license": "GPL-3.0-or-later", - "devDependencies": { - "@axe-core/playwright": "^4.13.0", - "@eslint/js": "^10.0.1", - "@playwright/test": "^1.63.0", - "@vitest/coverage-v8": "^5.0.1", - "eslint": "^10.11.0", - "eslint-plugin-security": "^4.0.1", - "globals": "^17.12.0", - "prettier": "^3.9.9", - "vitest": "^5.0.1" - }, - "engines": { - "node": ">=24" - } - }, - "node_modules/@axe-core/playwright": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.13.0.tgz", - "integrity": "sha512-6YLx+kxXu5GJceG4ozFg+33a2EMTdjYwWGloJ3sb9Kta5pp+ZNS53uxGVog5JetIY8s++P5UrtX+cri+u0VAVg==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "axe-core": "~4.13.0" - }, - "peerDependencies": { - "playwright-core": ">= 1.0.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", - "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.8" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", - "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", - "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@cacheable/memory": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", - "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cacheable/utils": "^2.5.0", - "@keyv/bigmap": "^1.3.1", - "hookified": "^1.15.1", - "keyv": "^5.6.0" - } - }, - "node_modules/@cacheable/utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", - "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hashery": "^1.5.1", - "keyv": "^5.6.0" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", - "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.23.5", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", - "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^3.0.5", - "debug": "^4.3.1", - "minimatch": "^10.2.4" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", - "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/core": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", - "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/js": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", - "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "eslint": "^10.0.0" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/@eslint/object-schema": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", - "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", - "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1", - "levn": "^0.4.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", - "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/types": "^0.15.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", - "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.2", - "@humanfs/types": "^0.15.0", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/types": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", - "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@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.6.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", - "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", - "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/@keyv/bigmap": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", - "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "hashery": "^1.4.0", - "hookified": "^1.15.0" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "keyv": "^5.6.0" - } - }, - "node_modules/@keyv/serialize": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", - "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@oxc-project/types": { - "version": "0.151.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.151.0.tgz", - "integrity": "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==", - "dev": true, - "license": "MIT", - "peer": true, - "funding": { - "url": "https://github.com/sponsors/oxc-project" - } - }, - "node_modules/@playwright/test": { - "version": "1.63.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz", - "integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.63.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@rolldown/binding-android-arm-eabi": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.11.tgz", - "integrity": "sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.11.tgz", - "integrity": "sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.11.tgz", - "integrity": "sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.11.tgz", - "integrity": "sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.11.tgz", - "integrity": "sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.11.tgz", - "integrity": "sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.11.tgz", - "integrity": "sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.11.tgz", - "integrity": "sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.11.tgz", - "integrity": "sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.11.tgz", - "integrity": "sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.11.tgz", - "integrity": "sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.11.tgz", - "integrity": "sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.11.tgz", - "integrity": "sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.11.tgz", - "integrity": "sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.11.tgz", - "integrity": "sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/esrecurse": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", - "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vitest/coverage-v8": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-5.0.1.tgz", - "integrity": "sha512-FRC8ACiudC3dI6MTplzRSYWHDRnIv2IPfbzs4FdoJNsMal/35sWV8hwIfV8ZcqzSPy+uXHeMVONt9CEqtOU17w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^1.0.2", - "@vitest/istanbul-lib-coverage": "^1.0.0", - "@vitest/istanbul-lib-report": "^1.0.0", - "ast-v8-to-istanbul": "^1.0.5", - "magicast": "^0.5.4", - "obug": "^2.1.4", - "std-env": "^4.2.0", - "tinyrainbow": "^3.1.1" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@vitest/browser": "5.0.1", - "vitest": "5.0.1" - }, - "peerDependenciesMeta": { - "@vitest/browser": { - "optional": true - } - } - }, - "node_modules/@vitest/istanbul-lib-coverage": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@vitest/istanbul-lib-coverage/-/istanbul-lib-coverage-1.0.1.tgz", - "integrity": "sha512-k3DJZ8LhMBK9NS4SclF1ASD3OgXEWDorbIcPTRDK0/Zae6fRvu+fJRxtFdLfHsa9Y24beCdPnoNZ4LviTNstfA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=22" - } - }, - "node_modules/@vitest/istanbul-lib-report": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@vitest/istanbul-lib-report/-/istanbul-lib-report-1.0.1.tgz", - "integrity": "sha512-1EOLRfsTMnyAr3+kEAsP4o9dhaDlGPpD7H5iLBBeq//YpNB1VIahkPhB+eRp9N2Dkfw8oySROjE3yf9XDeaIkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/istanbul-lib-coverage": "1.0.1" - }, - "engines": { - "node": ">=22" - } - }, - "node_modules/@vitest/mocker": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.1.tgz", - "integrity": "sha512-6K1DoBNAPGvuOcSsGA4D6x+5zEEff/KmOOP3uetT2TrGpVfI+HRHRnJJfKi5ib/g1vx8IYHQD8s0pbJz8WQI7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.31", - "@vitest/spy": "5.0.1", - "estree-walker": "^3.0.3", - "magic-string": "^1.2.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/spy": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.1.tgz", - "integrity": "sha512-rbto/mF/SGERxEgYOek7Xm6B9b+y+mVoo+f4b2LymYO8zM1b7uB5nHuhVMTP2hxdzgxvGiZYGxGIaMvL5y180Q==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/acorn": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", - "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/ast-v8-to-istanbul": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.5.tgz", - "integrity": "sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.31", - "estree-walker": "^3.0.3", - "js-tokens": "^10.0.0" - } - }, - "node_modules/axe-core": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.13.0.tgz", - "integrity": "sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==", - "dev": true, - "license": "MPL-2.0", - "engines": { - "node": ">=4" - } - }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/brace-expansion": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", - "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/cacheable": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", - "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cacheable/memory": "^2.2.0", - "@cacheable/utils": "^2.5.0", - "hookified": "^1.15.0", - "keyv": "^5.6.0", - "qified": "^0.10.1" - } - }, - "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "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-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/es-module-lexer": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", - "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", - "dev": true, - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "10.11.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.11.0.tgz", - "integrity": "sha512-P7a6UEEqb9G95MYAtqkmsTbVXIYyzIfl6NGOIJk162PaahFxFyeGcrlXYFSiagECg4sEm8IseJdZBKR3rx6MsQ==", - "dev": true, - "license": "MIT", - "workspaces": [ - "packages/*" - ], - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.5", - "@eslint/config-helpers": "^0.7.0", - "@eslint/core": "^1.2.1", - "@eslint/plugin-kit": "^0.7.3", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.2", - "eslint-visitor-keys": "^5.0.1", - "espree": "^11.2.0", - "esquery": "^1.7.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "11.1.5 || >11.1.6 <12", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.5", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-security": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-4.0.1.tgz", - "integrity": "sha512-/lZCkOxPOWaf1jXAqgICrS8St3BMBccIPvhOSUYuV6VCr1o5nFVG998FnTLt6w2Nxb8Uo0nM8fzmnhp+GY/aEg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "safe-regex": "^2.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-scope": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", - "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@types/esrecurse": "^4.3.1", - "@types/estree": "^1.0.8", - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", - "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.16.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^5.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expect-type": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", - "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/file-entry-cache": { - "version": "11.1.5", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", - "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^6.1.23" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "6.1.23", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", - "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cacheable": "^2.5.0", - "flatted": "^3.4.2", - "hookified": "^1.15.0" - } - }, - "node_modules/flatted": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", - "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", - "dev": true, - "license": "ISC" - }, - "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" - ], - "peer": true, - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "17.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.12.0.tgz", - "integrity": "sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/hashery": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", - "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "hookified": "^1.15.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/hookified": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", - "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", - "dev": true, - "license": "MIT" - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/js-tokens": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", - "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/keyv": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", - "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@keyv/serialize": "^1.1.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lightningcss": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", - "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", - "dev": true, - "license": "MPL-2.0", - "peer": true, - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.33.0", - "lightningcss-darwin-arm64": "1.33.0", - "lightningcss-darwin-x64": "1.33.0", - "lightningcss-freebsd-x64": "1.33.0", - "lightningcss-linux-arm-gnueabihf": "1.33.0", - "lightningcss-linux-arm64-gnu": "1.33.0", - "lightningcss-linux-arm64-musl": "1.33.0", - "lightningcss-linux-x64-gnu": "1.33.0", - "lightningcss-linux-x64-musl": "1.33.0", - "lightningcss-win32-arm64-msvc": "1.33.0", - "lightningcss-win32-x64-msvc": "1.33.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", - "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", - "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", - "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", - "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", - "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", - "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", - "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", - "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", - "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", - "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", - "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/magic-string": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz", - "integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.6.0" - } - }, - "node_modules/magicast": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.4.tgz", - "integrity": "sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7", - "source-map-js": "^1.2.1" - } - }, - "node_modules/minimatch": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", - "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.8" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "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.19", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", - "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "peer": true, - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/obug": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", - "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", - "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], - "license": "MIT", - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/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", - "peer": true - }, - "node_modules/picomatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", - "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/playwright": { - "version": "1.63.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", - "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.63.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/playwright-core": { - "version": "1.63.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz", - "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/postcss": { - "version": "8.5.28", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", - "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", - "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", - "peer": true, - "dependencies": { - "nanoid": "^3.3.18", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.9.9", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.9.tgz", - "integrity": "sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/qified": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", - "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hookified": "^2.1.1" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/qified/node_modules/hookified": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", - "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", - "dev": true, - "license": "MIT" - }, - "node_modules/regexp-tree": { - "version": "0.1.27", - "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", - "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", - "dev": true, - "license": "MIT", - "bin": { - "regexp-tree": "bin/regexp-tree" - } - }, - "node_modules/rolldown": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.11.tgz", - "integrity": "sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@oxc-project/types": "=0.151.0", - "@rolldown/pluginutils": "^1.0.0" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm-eabi": "1.2.11", - "@rolldown/binding-android-arm64": "1.2.11", - "@rolldown/binding-darwin-arm64": "1.2.11", - "@rolldown/binding-darwin-x64": "1.2.11", - "@rolldown/binding-freebsd-x64": "1.2.11", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.11", - "@rolldown/binding-linux-arm64-gnu": "1.2.11", - "@rolldown/binding-linux-arm64-musl": "1.2.11", - "@rolldown/binding-linux-ppc64-gnu": "1.2.11", - "@rolldown/binding-linux-s390x-gnu": "1.2.11", - "@rolldown/binding-linux-x64-gnu": "1.2.11", - "@rolldown/binding-linux-x64-musl": "1.2.11", - "@rolldown/binding-openharmony-arm64": "1.2.11", - "@rolldown/binding-win32-arm64-msvc": "1.2.11", - "@rolldown/binding-win32-x64-msvc": "1.2.11" - } - }, - "node_modules/safe-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", - "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==", - "dev": true, - "license": "MIT", - "dependencies": { - "regexp-tree": "~0.1.1" - } - }, - "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/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": "4.2.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", - "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinybench": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.1.4.tgz", - "integrity": "sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/tinyexec": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", - "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyrainbow": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", - "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/vite": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.1.tgz", - "integrity": "sha512-/bvH9E9tmCXRGp2uXY3WbOldqpTwFkbha/8ANaEQ6VkxhH60KyqLwgZq6lG2y+4uT55x9+9eUHMpQ7uGnOCKjA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "lightningcss": "^1.33.0", - "picomatch": "^4.0.7", - "postcss": "^8.5.28", - "rolldown": "~1.2.9", - "tinyglobby": "^0.2.17" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.7.1", - "esbuild": "^0.27.0 || ^0.28.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vitest": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.1.tgz", - "integrity": "sha512-iA95lQbKEkvrtTkdAgnWbXfbipWiiWe/hDl2P5tMi6WFwD76G0NxXAGp/M9EOcYupeGJRr6wppMc7CoA41TQjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/mocker": "5.0.1", - "chai": "^6.2.2", - "es-module-lexer": "^2.3.2", - "expect-type": "^1.4.0", - "magic-string": "^1.2.3", - "obug": "^2.1.4", - "picomatch": "^4.0.7", - "std-env": "^4.2.0", - "tinybench": "6.1.4", - "tinyexec": "1.3.0", - "tinyglobby": "^0.2.17", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^22.12.0 || ^24.0.0 || >=26.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@opentelemetry/api": "^1.9.0", - "@types/node": "^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "5.0.1", - "@vitest/browser-preview": "5.0.1", - "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", - "@vitest/coverage-istanbul": "5.0.1", - "@vitest/coverage-v8": "5.0.1", - "@vitest/ui": "5.0.1", - "happy-dom": "*", - "jsdom": "*", - "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser-playwright": { - "optional": true - }, - "@vitest/browser-preview": { - "optional": true - }, - "@vitest/browser-webdriverio": { - "optional": true - }, - "@vitest/coverage-istanbul": { - "optional": true - }, - "@vitest/coverage-v8": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - }, - "vite": { - "optional": false - } - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "name": "quicktiler", + "version": "0.3.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "quicktiler", + "version": "0.3.0", + "license": "GPL-3.0-or-later", + "devDependencies": { + "@axe-core/playwright": "4.13.0", + "@eslint/js": "10.0.1", + "@playwright/test": "1.63.0", + "@vitest/coverage-v8": "5.0.2", + "eslint": "10.11.0", + "eslint-plugin-security": "4.0.1", + "globals": "17.12.0", + "prettier": "3.9.9", + "vitest": "5.0.2" + }, + "engines": { + "node": ">=24" + } + }, + "node_modules/@axe-core/playwright": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.13.0.tgz", + "integrity": "sha512-6YLx+kxXu5GJceG4ozFg+33a2EMTdjYwWGloJ3sb9Kta5pp+ZNS53uxGVog5JetIY8s++P5UrtX+cri+u0VAVg==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "axe-core": "~4.13.0" + }, + "peerDependencies": { + "playwright-core": ">= 1.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@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.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "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/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@oxc-project/types": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.152.0.tgz", + "integrity": "sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@playwright/test": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz", + "integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.12.tgz", + "integrity": "sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.12.tgz", + "integrity": "sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.12.tgz", + "integrity": "sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.12.tgz", + "integrity": "sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.12.tgz", + "integrity": "sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.12.tgz", + "integrity": "sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.12.tgz", + "integrity": "sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.12.tgz", + "integrity": "sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.12.tgz", + "integrity": "sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.12.tgz", + "integrity": "sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.12.tgz", + "integrity": "sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.12.tgz", + "integrity": "sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.12.tgz", + "integrity": "sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.12.tgz", + "integrity": "sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.12.tgz", + "integrity": "sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/coverage-v8": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-5.0.2.tgz", + "integrity": "sha512-3ffHBEi8DOOBLwIGBhOBZbRfYFYWjMUuxZicONdhuFEFEG5AVsMOSrVeuROskqnqpbOmEJwxM2eTVZ9N3a1t+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/istanbul-lib-coverage": "^1.0.0", + "@vitest/istanbul-lib-report": "^1.0.0", + "ast-v8-to-istanbul": "^1.0.5", + "magicast": "^0.5.4", + "obug": "^2.1.4", + "std-env": "^4.2.0", + "tinyrainbow": "^3.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "5.0.2", + "vitest": "5.0.2" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/istanbul-lib-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@vitest/istanbul-lib-coverage/-/istanbul-lib-coverage-1.0.2.tgz", + "integrity": "sha512-9J/JMwOf9AoJhAywhrn7ScKTL38hsWQP/qPG60OtaAFcQ5OXPwKsxZFlbnuCKmZ61m8/lGgHYnFpdyQZUvG/iA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/@vitest/istanbul-lib-report": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@vitest/istanbul-lib-report/-/istanbul-lib-report-1.0.2.tgz", + "integrity": "sha512-gUsfXZJbzPamoIY5TvHFiMMoXESBrUMo+xqaj+rYrWI69+EnvRlYBlP96ZnHPY4vX8kyUpgAnFUCg5wZG/HkDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/istanbul-lib-coverage": "1.0.2" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@vitest/mocker": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.2.tgz", + "integrity": "sha512-Z5FS00Q1SJHkB35xATsmWGdQ5WA1/0MV3CDjqyv7GavHv1OfOj145MNfHOlHk7QLes21dKFDHr8EO2zvL+9WGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.2", + "estree-walker": "^3.0.3", + "magic-string": "^1.2.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/spy": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.2.tgz", + "integrity": "sha512-Ijc7T1nT9efNb5LxvjaBrEqw3f/QwUv5EE0nKqZxgqsaV/FxAAZ8baGylA8X/Z2oS4Lp+K74Jr6dTJsDKxJDeg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.7.tgz", + "integrity": "sha512-kFL68AG6ajd8fg248zwM9GQrUWEp79gsmjum34OEXjs4yHuUMZfYKwOLW9GMmB4oNvVrj+EAGxsP7ye2UR9UlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/axe-core": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.13.0.tgz", + "integrity": "sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, + "node_modules/chai": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.11.0.tgz", + "integrity": "sha512-P7a6UEEqb9G95MYAtqkmsTbVXIYyzIfl6NGOIJk162PaahFxFyeGcrlXYFSiagECg4sEm8IseJdZBKR3rx6MsQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.3", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "11.1.5 || >11.1.6 <12", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-security": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-4.0.1.tgz", + "integrity": "sha512-/lZCkOxPOWaf1jXAqgICrS8St3BMBccIPvhOSUYuV6VCr1o5nFVG998FnTLt6w2Nxb8Uo0nM8fzmnhp+GY/aEg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-regex": "^2.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^6.1.23" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "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" + ], + "peer": true, + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.12.0.tgz", + "integrity": "sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "peer": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/magic-string": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz", + "integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/magicast": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.5.tgz", + "integrity": "sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "source-map-js": "^1.2.1" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/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", + "peer": true + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", + "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz", + "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "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", + "peer": true, + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.9", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.9.tgz", + "integrity": "sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "dev": true, + "license": "MIT" + }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, + "node_modules/rolldown": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.12.tgz", + "integrity": "sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@oxc-project/types": "=0.152.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.12", + "@rolldown/binding-android-arm64": "1.2.12", + "@rolldown/binding-darwin-arm64": "1.2.12", + "@rolldown/binding-darwin-x64": "1.2.12", + "@rolldown/binding-freebsd-x64": "1.2.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.12", + "@rolldown/binding-linux-arm64-gnu": "1.2.12", + "@rolldown/binding-linux-arm64-musl": "1.2.12", + "@rolldown/binding-linux-ppc64-gnu": "1.2.12", + "@rolldown/binding-linux-s390x-gnu": "1.2.12", + "@rolldown/binding-linux-x64-gnu": "1.2.12", + "@rolldown/binding-linux-x64-musl": "1.2.12", + "@rolldown/binding-openharmony-arm64": "1.2.12", + "@rolldown/binding-win32-arm64-msvc": "1.2.12", + "@rolldown/binding-win32-x64-msvc": "1.2.12" + } + }, + "node_modules/safe-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", + "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "regexp-tree": "~0.1.1" + } + }, + "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/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/std-env": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.0.tgz", + "integrity": "sha512-78U2TlB2CnVenajOFzf3BKSm0J6oz5L0NV7g32LCPccvYc0lbWvys4d3uUUCS2B1N8PAf2+aekR8i1KbC3HO7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.2.0.tgz", + "integrity": "sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.2.tgz", + "integrity": "sha512-SQr1x6W5vVSbROg7vsyXIaxK9b0G7zsT68acdWWRmnBUsgDieLCRG+Rep9WdZgcposvv/GSnr4GUUBqB3vXq6w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.11", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.2.tgz", + "integrity": "sha512-7MQrx9pDv5aHiUcovIb/70Ys3tgtkUVgCtledvKdCmEO+/1Dicq5ZqoSxOW034m03oqC+oHOKui2dM6qtMLoJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.0.2", + "chai": "^6.2.2", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.2.3", + "obug": "^2.1.4", + "picomatch": "^4.0.7", + "std-env": "^4.2.0", + "tinybench": "^6.1.4", + "tinyexec": "^1.3.0", + "tinyglobby": "^0.2.17", + "why-is-node-running": "^3.2.1" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.2", + "@vitest/browser-preview": "5.0.2", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.2", + "@vitest/coverage-v8": "5.0.2", + "@vitest/ui": "5.0.2", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false } + } + }, + "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": "3.2.2", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.2.tgz", + "integrity": "sha512-NKUzAelcoCXhXL4dJzKIwXeR8iEVqsA0Lq6Vnd0UXvgaKbzVo4ZTHROF2Jidrv+SgxOQ03fMinnNhzZATxOD3A==", + "dev": true, + "license": "MIT", + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=20.11" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } + } } diff --git a/package.json b/package.json index 8fffd2a..da05e1c 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,21 @@ { - "name": "quicktiler", - "version": "0.3.0", - "description": "Keyboard-driven zone tiling for GNOME Shell", - "license": "GPL-3.0-or-later", - "private": true, - "type": "module", - "devDependencies": { - "@axe-core/playwright": "^4.13.0", - "@eslint/js": "^10.0.1", - "@playwright/test": "^1.63.0", - "@vitest/coverage-v8": "^5.0.1", - "eslint": "^10.11.0", - "eslint-plugin-security": "^4.0.1", - "globals": "^17.12.0", - "prettier": "^3.9.9", - "vitest": "^5.0.1" - }, - "engines": { - "node": ">=24" - } + "name": "quicktiler", + "version": "0.3.0", + "license": "GPL-3.0-or-later", + "private": true, + "type": "module", + "devDependencies": { + "@axe-core/playwright": "4.13.0", + "@eslint/js": "10.0.1", + "@playwright/test": "1.63.0", + "@vitest/coverage-v8": "5.0.2", + "eslint": "10.11.0", + "eslint-plugin-security": "4.0.1", + "globals": "17.12.0", + "prettier": "3.9.9", + "vitest": "5.0.2" + }, + "engines": { + "node": ">=24" + } } diff --git a/playwright.config.js b/playwright.config.js index 31a40da..ee4edd2 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -1,27 +1,25 @@ import { defineConfig, devices } from '@playwright/test'; - -const port = 8323; - -// The docs site under docs/, served exactly as GitHub Pages serves it: static -// files, no build. Chromium and Firefox only: WebKit is not supported on this -// Fedora base. +import project from './quick-project.json' with { type: 'json' }; +const { docsPort } = project; +if (!Number.isInteger(docsPort) || docsPort < 1024 || docsPort > 65535) { + throw new Error('docsPort must be an integer between 1024 and 65535'); +} export default defineConfig({ testDir: './tests', - // *.spec.js is the browser suite; *.test.js is Vitest's. testMatch: '**/*.spec.js', fullyParallel: true, + workers: 4, forbidOnly: !!process.env.CI, reporter: [['list']], - use: { baseURL: `http://127.0.0.1:${port}/`, trace: 'off' }, + use: { baseURL: `http://127.0.0.1:${docsPort}/`, trace: 'off' }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, { name: 'firefox', use: { ...devices['Desktop Firefox'] } }, ], webServer: { - command: `python3 -m http.server ${port} --bind 127.0.0.1 --directory docs`, - url: `http://127.0.0.1:${port}/`, - reuseExistingServer: !process.env.CI, - // http.server logs every request to stderr; the suite reports failures itself. + command: `python3 -m http.server ${docsPort} --bind 127.0.0.1 --directory docs`, + url: `http://127.0.0.1:${docsPort}/`, + reuseExistingServer: false, stderr: 'ignore', }, }); diff --git a/project.just b/project.just new file mode 100644 index 0000000..f45c198 --- /dev/null +++ b/project.just @@ -0,0 +1,16 @@ + +# Project-specific offline integration tests +test-extra: + @true + +# Isolated GNOME lifecycle test +live-check: + bash scripts/headless-check.sh + +# Additional integration checks +live-extra: + @true + +# Project services stopped before uninstalling +uninstall-extra: + @true diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..bae54d0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[tool.ruff] +target-version = "py312" +line-length = 100 + +[tool.ruff.lint] +select = ["E", "F", "I", "UP", "B", "S", "SIM", "RUF"] diff --git a/quick-project.json b/quick-project.json new file mode 100644 index 0000000..eef909c --- /dev/null +++ b/quick-project.json @@ -0,0 +1,22 @@ +{ + "$schema": "quick-project.schema.json", + "runtimeFiles": [ + "metadata.json", + "extension.js", + "prefs.js", + "LICENSE", + "modules/accelerator.js", + "modules/actions.js", + "modules/neighbors.js", + "modules/panel.js", + "modules/quicktiler.js", + "modules/settings.js", + "modules/shortcuts.js", + "modules/windows.js", + "modules/zones.js", + "schemas/org.gnome.shell.extensions.quicktiler.gschema.xml", + "icons/quicktiler-symbolic.svg" + ], + "docsPort": 8325, + "nativeTests": false +} diff --git a/quick-project.schema.json b/quick-project.schema.json new file mode 100644 index 0000000..3bbc888 --- /dev/null +++ b/quick-project.schema.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": false, + "required": ["runtimeFiles", "docsPort", "nativeTests"], + "properties": { + "$schema": { "type": "string" }, + "runtimeFiles": { + "type": "array", + "minItems": 4, + "uniqueItems": true, + "items": { "type": "string", "minLength": 1 } + }, + "docsPort": { "type": "integer", "minimum": 1024, "maximum": 65535 }, + "nativeTests": { "type": "boolean" } + } +} diff --git a/quick-template.lock.json b/quick-template.lock.json new file mode 100644 index 0000000..9267414 --- /dev/null +++ b/quick-template.lock.json @@ -0,0 +1,5 @@ +{ + "$schema": "quick-template.schema.json", + "repository": "Ghost-Assembly/quick-template", + "revision": "effd43bd6c74609886a2574f9a1151eff46b7ff9" +} diff --git a/quick-template.schema.json b/quick-template.schema.json new file mode 100644 index 0000000..6608429 --- /dev/null +++ b/quick-template.schema.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": false, + "required": ["repository", "revision"], + "properties": { + "$schema": { "type": "string" }, + "repository": { "const": "Ghost-Assembly/quick-template" }, + "revision": { "type": "string", "pattern": "^[0-9a-f]{40}$" } + } +} diff --git a/scripts/build.py b/scripts/build.py new file mode 100644 index 0000000..e086d3c --- /dev/null +++ b/scripts/build.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +"""Package only explicitly declared runtime files and verify the official ZIP.""" + +import argparse +import json +import shutil +import subprocess +import tempfile +import zipfile +from pathlib import Path, PurePosixPath + +ROOT = Path(__file__).resolve().parent.parent + + +def runtime_files(root: Path) -> list[str]: + """Validate the runtime manifest before reading or copying any file.""" + names = json.loads((root / "quick-project.json").read_text())["runtimeFiles"] + if ( + not isinstance(names, list) + or not names + or not all(isinstance(name, str) for name in names) + or len(names) != len(set(names)) + ): + raise ValueError("Runtime files must be a nonempty list of unique paths") + for name in names: + path = PurePosixPath(name) + if path.is_absolute() or ".." in path.parts or str(path) != name: + raise ValueError("Runtime path must be a normalized relative path") + source = root / name + if source.is_symlink() or not source.resolve().is_relative_to(root.resolve()): + raise ValueError("Runtime files must remain inside the project") + if not source.is_file() or ( + source.suffix not in {".js", ".py", ".json", ".xml", ".svg", ".css"} + and name != "LICENSE" + ): + raise ValueError("Runtime manifest contains a missing or unsupported file") + if not {"metadata.json", "extension.js", "prefs.js", "LICENSE"}.issubset(names): + raise ValueError("Runtime manifest is missing required extension files") + return names + + +def artifact_name(root: Path) -> str: + """Read a safe extension identity from GNOME metadata.""" + uuid = json.loads((root / "metadata.json").read_text())["uuid"] + if ( + not isinstance(uuid, str) + or not uuid + or any( + c not in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._@-" + for c in uuid + ) + ): + raise ValueError("Invalid extension UUID") + return f"{uuid}.shell-extension.zip" + + +def stage(root: Path, destination: Path) -> list[str]: + """Copy the declared source without dependencies, credentials, or output.""" + names = runtime_files(root) + for name in names: + target = destination / name + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(root / name, target) + subprocess.run( + ["/usr/bin/glib-compile-schemas", "--strict", "--dry-run", "schemas"], + cwd=destination, + check=True, + timeout=30, + ) + return names + + +def build(root: Path = ROOT) -> Path: + """Build atomically with a stable archive layout and timestamps.""" + artifact = root / artifact_name(root) + with tempfile.TemporaryDirectory(prefix="quick-build-", dir=root) as work: + staging = Path(work) + names = stage(root, staging) + staged_zip = staging / artifact.name + with zipfile.ZipFile(staged_zip, "w", zipfile.ZIP_DEFLATED) as bundle: + for name in sorted(names): + info = zipfile.ZipInfo(name, (1980, 1, 1, 0, 0, 0)) + info.compress_type = zipfile.ZIP_DEFLATED + info.external_attr = 0o100644 << 16 + bundle.writestr(info, (staging / name).read_bytes()) + staged_zip.replace(artifact) + return artifact + + +def check(root: Path = ROOT) -> None: + """Compare file contents, not only filenames, with GNOME's packer.""" + artifact = root / artifact_name(root) + with tempfile.TemporaryDirectory(prefix="quick-pack-") as work: + staging = Path(work) + names = stage(root, staging) + # All optional directories exist, so every extension uses one invocation. + for directory in ["modules", "icons", "scripts", "packed"]: + (staging / directory).mkdir(exist_ok=True) + subprocess.run( + [ + "/usr/bin/gnome-extensions", + "pack", + "--force", + "--out-dir=packed", + "--extra-source=modules", + "--extra-source=icons", + "--extra-source=scripts", + "--extra-source=LICENSE", + ".", + ], + cwd=staging, + check=True, + timeout=30, + ) + with ( + zipfile.ZipFile(artifact) as actual, + zipfile.ZipFile(staging / "packed" / artifact.name) as official, + ): + actual_files = {n for n in actual.namelist() if not n.endswith("/")} + official_files = {n for n in official.namelist() if not n.endswith("/")} + if ( + actual_files != set(names) + or actual_files != official_files + or any(actual.read(n) != official.read(n) for n in names) + ): + raise ValueError("Bundle differs from GNOME's official packer") + print("PASS: runtime files and contents match GNOME's official packer") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--check", action="store_true") + args = parser.parse_args() + if args.check: + check() + else: + print(f"Built {build().name}") diff --git a/scripts/clean.py b/scripts/clean.py new file mode 100644 index 0000000..71f9e91 --- /dev/null +++ b/scripts/clean.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +"""Remove only known generated files from this checkout.""" + +import shutil +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +for name in ["coverage", "test-results", "playwright-report", ".scannerwork"]: + target = ROOT / name + if target.is_symlink(): + target.unlink() + elif target.is_dir(): + shutil.rmtree(target) +for artifact in ROOT.glob("*.shell-extension.zip"): + artifact.unlink() +(ROOT / "schemas/gschemas.compiled").unlink(missing_ok=True) diff --git a/scripts/docs.py b/scripts/docs.py new file mode 100644 index 0000000..552581c --- /dev/null +++ b/scripts/docs.py @@ -0,0 +1,354 @@ +#!/usr/bin/env python3 +"""Render shared instructions and README badges without a website runtime.""" + +import argparse +import html +import json +import subprocess +from pathlib import Path +from urllib.parse import quote, urlencode + +ROOT = Path(__file__).resolve().parent.parent +SECTIONS = ["install", "uninstall", "testing", "packaging", "releasing", "development"] +COMMANDS = """just setup # install pinned tools, dependencies, and browsers +just fmt # format source and configuration +just lint # verify template, generated docs, source, and schemas +just test # JavaScript, Python, and project offline tests +just coverage # report JavaScript coverage without source exclusions +just test-docs # Chromium and Firefox documentation checks +just security # dependencies, secrets, and workflow checks +just build # build the runtime-only extension ZIP +just pack-check # compare files and contents with GNOME's packer +just ci # complete local verification and packaging +just test-live # isolated GNOME lifecycle and project integration checks +just docs # serve the static site at localhost:8000 +just template-check # verify the pinned canonical template +just template-status # report a newer approved template revision""" + + +def blocks(metadata: dict, project: dict) -> dict[str, list[tuple[str, str]]]: + """Keep shared prose in one place and identity in metadata.""" + uuid = metadata["uuid"] + name = project["repository"] + install = ( + f"xh --download GET https://github.com/Ghost-Assembly/{name}/releases/latest/download/{uuid}.shell-extension.zip\n" + f"gnome-extensions install --force {uuid}.shell-extension.zip" + ) + uninstall = f"gnome-extensions disable {uuid}\ngnome-extensions uninstall {uuid}" + if name == "quickspot": + uninstall = ( + "if systemctl --user cat quickspot-soloist.service >/dev/null 2>&1; then\n" + " systemctl --user disable --now quickspot-soloist.service\nfi\n" + uninstall + ) + return { + "install": [ + ( + "p", + "Requires GNOME Shell " + + " or ".join(metadata["shell-version"]) + + ". " + + project["requirements"], + ), + ("h3", "From a release"), + ( + "p", + ( + "Download the latest release ZIP and install it for your user. xh is a " + "download tool; you can also download the ZIP from GitHub in a browser. " + "Installing compiles the settings schema." + ), + ), + ("code", install), + ("p", "Log out and back in so GNOME discovers the extension, then enable it:"), + ("code", f"gnome-extensions enable {uuid}"), + ("h3", "From a clone"), + ( + "p", + ( + "Install mise and activate it in your shell. Clone the repository, " + "install its pinned tools, and build and install the same ZIP used for " + "releases:" + ), + ), + ( + "code", + f"git clone https://github.com/Ghost-Assembly/{name}.git\ncd {name}\n" + "mise install\nmise exec -- just setup\nmise exec -- just install", + ), + ( + "p", + ( + "Log out and back in, then run just enable. Run just prefs to open " + "preferences. After updating a loaded extension, start a new session to " + "load its new code; opening preferences does not reload GNOME Shell." + ), + ), + ], + "uninstall": [ + ( + "p", + ( + "Disable and uninstall the extension for your user. These commands " + "preserve saved settings and other user data." + ), + ), + ("code", uninstall), + ( + "p", + ( + "From a clone, just uninstall performs the same steps. Disabling with " + "just disable leaves the extension installed." + ), + ), + ], + "testing": [ + ( + "p", + ( + "just test runs the JavaScript suite with Vitest, the shared tooling " + "tests, and any project-specific offline suites. just coverage reports " + "the JavaScript coverage universe, including untested runtime files. Test" + " stubs and generated reports are not runtime source." + ), + ), + ( + "p", + ( + "just test-docs runs Playwright and axe in Chromium and Firefox: dark and" + " light accessibility checks, keyboard navigation, mobile layout, reduced" + " motion, links, metadata, local assets, and no page JavaScript. " + "Automated accessibility checks still require human review of reading and" + " focus order." + ), + ), + ( + "p", + ( + "just test-live checks the package and runs isolated GNOME lifecycle " + "checks. It is a separate local check, not proof of compatibility from a " + "hosted runner. Verify each declared GNOME version and complete the " + "project's manual checks before releasing." + ), + ), + ], + "packaging": [ + ("code", "just build\njust pack-check"), + ( + "p", + f"The output is {uuid}.shell-extension.zip at the repository root, with " + "metadata.json at the archive root. Python's standard library packages " + "the explicit runtimeFiles allowlist in quick-project.json, using stable " + "file order and timestamps.", + ), + ( + "p", + ( + "just pack-check compares both filenames and file contents with GNOME's " + "official packer and validates shipped icons. Docs, tests, dependencies, " + "credentials, downloaded binaries, and development artifacts stay outside" + " the ZIP. Update the runtime allowlist when adding a runtime file." + ), + ), + ], + "releasing": [ + ( + "p", + ( + "Run just ci, just test-live, and the project manual checklist. Set " + "metadata.json version-name and package.json version to the same new " + "version and increment metadata.json version for the GNOME Extension " + "Store. Update the npm lockfile, regenerate the docs, and commit the " + "reviewed changes to main through a passing pull request." + ), + ), + ( + "p", + ( + "Create and push a v-prefixed tag for that version. The release workflow " + "verifies the version, main ancestry, and successful required checks for" + " the tagged commit, then attaches its tested ZIP to a GitHub release. It does" + " not upload to extensions.gnome.org; that submission and its review " + "remain manual." + ), + ), + ], + "development": [ + ( + "p", + ( + "mise.toml pins runtime and CLI versions; justfile owns commands; npm " + "owns development dependencies and the lockfile. GNOME libraries come " + "from the host. On image-based Fedora, use the host's available tools or " + "a toolbox/distrobox for missing system packages; do not layer packages " + "onto the OS." + ), + ), + ("code", COMMANDS), + ( + "p", + ( + "GitHub requires local verification, security analysis, and completed " + "Sonar analysis. The shared Sonar policy requires zero security, " + "reliability, and maintainability issues and zero duplicated lines. " + "Missing configuration fails instead of silently skipping analysis. Pages" + " publishes the tested docs only after the required checks pass on main." + ), + ), + ( + "p", + ( + "Common tooling and these instructions are generated from a pinned " + "canonical template. Change that source and synchronize its approved " + "revision; do not edit generated sections or locally bless drift. " + "Extension-specific behavior belongs in project configuration and " + "project.just." + ), + ), + ], + } + + +def markdown(items: list[tuple[str, str]]) -> str: + """Render the same semantic content for GitHub.""" + return "\n\n".join( + "```sh\n" + text + "\n```" if kind == "code" else "### " + text if kind == "h3" else text + for kind, text in items + ) + + +def markup(items: list[tuple[str, str]]) -> str: + """Escape all generated prose and commands before writing HTML.""" + parts = [] + for kind, text in items: + escaped = html.escape(text) + if kind == "code": + parts.append( + '
Shell
'
+                + escaped
+                + "
" + ) + else: + parts.append(f"<{kind}>{escaped}") + return "\n".join(parts) + + +def replace_block(text: str, name: str, content: str) -> str: + """Replace an explicitly marked generated region; never infer ownership.""" + start = f"" + end = f"" + if text.count(start) != 1 or text.count(end) != 1: + raise ValueError(f"Expected exactly one generated region: {name}") + before, rest = text.split(start) + _, after = rest.split(end) + return before + start + "\n" + content + "\n" + end + after + + +def badges(repo: str, metadata: dict) -> str: + """Link live project status without publishing credential-bearing URLs.""" + github = "https://github.com/Ghost-Assembly/" + repo + sonar = "https://sonarcloud.io/dashboard?id=Ghost-Assembly_" + repo + items = [] + for label, workflow in [("CI", "ci.yml"), ("Security", "security.yml")]: + items.append( + f"[![{label}]({github}/actions/workflows/{workflow}/badge.svg?branch=main)]({github}/actions/workflows/{workflow})" + ) + for label, image, link in [ + ( + "Docs", + "https://img.shields.io/website?" + + urlencode({"url": "https://ghost-assembly.com/" + repo + "/", "label": "docs"}), + "https://ghost-assembly.com/" + repo + "/", + ), + ( + "Release", + f"https://img.shields.io/github/v/release/Ghost-Assembly/{repo}", + github + "/releases/latest", + ), + ( + "License", + f"https://img.shields.io/github/license/Ghost-Assembly/{repo}", + github + "/blob/main/LICENSE", + ), + ( + "GNOME", + "https://img.shields.io/badge/GNOME-" + + quote(" | ".join(metadata["shell-version"]), safe="") + + "-blue", + "https://ghost-assembly.com/" + repo + "/#install", + ), + ]: + items.append(f"[![{label}]({image})]({link})") + for label, metric in [ + ("Security issues", "software_quality_security_issues"), + ("Reliability issues", "software_quality_reliability_issues"), + ("Maintainability issues", "software_quality_maintainability_issues"), + ("Duplication", "duplicated_lines_density"), + ("Coverage", "coverage"), + ]: + api = "https://sonarcloud.io/api/measures/component?" + urlencode( + {"component": "Ghost-Assembly_" + repo, "metricKeys": metric} + ) + image = "https://img.shields.io/badge/dynamic/json?" + urlencode( + {"url": api, "query": "$.component.measures[0].value", "label": label} + ) + items.append(f"[![{label}]({image})]({sonar})") + items.append( + f"[![Sonar policy]({github}/actions/workflows/sonar.yml/badge.svg?branch=main)]({sonar})" + ) + return "\n".join(items) + + +def render(root: Path) -> dict[Path, str]: + """Generate common sections while preserving every unmarked project region.""" + metadata = json.loads((root / "metadata.json").read_text()) + project = json.loads((root / "docs/project.json").read_text()) + readme = (root / "README.md").read_text() + page = (root / "docs/index.html").read_text() + for name, items in blocks(metadata, project).items(): + readme = replace_block(readme, name, markdown(items)) + protected = markup(items).replace( + html.escape(metadata["uuid"]), + "" + html.escape(metadata["uuid"]) + "", + ) + page = replace_block(page, name, protected) + readme = replace_block(readme, "badges", badges(project["repository"], metadata)) + readme = subprocess.run( + ["/usr/bin/env", "node", "node_modules/prettier/bin/prettier.cjs", "--parser", "markdown"], + cwd=root, + input=readme, + text=True, + capture_output=True, + check=True, + timeout=30, + ).stdout + page = subprocess.run( + ["/usr/bin/env", "node", "node_modules/prettier/bin/prettier.cjs", "--parser", "html"], + cwd=root, + input=page, + text=True, + capture_output=True, + check=True, + timeout=30, + ).stdout + return {root / "README.md": readme, root / "docs/index.html": page} + + +def main() -> None: + """Write generated sections or check them without modifying source.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--check", action="store_true") + args = parser.parse_args() + stale = [] + for path, value in render(ROOT).items(): + if args.check: + if path.read_text() != value: + stale.append(str(path.relative_to(ROOT))) + else: + path.write_text(value) + if stale: + raise SystemExit("Stale generated docs: " + ", ".join(stale)) + print("PASS: shared documentation and badges are current") + + +if __name__ == "__main__": + main() diff --git a/scripts/headless-check.sh b/scripts/headless-check.sh index 867e6b3..207eb09 100755 --- a/scripts/headless-check.sh +++ b/scripts/headless-check.sh @@ -10,9 +10,9 @@ # This needs a real gnome-shell and so runs locally only; GitHub's runners have # no GNOME 50. # -# Recommended frame, NOT in template.list: everything outside the marked -# per-repo block is the same in every extension; the block holds what only -# this one checks. Keep the frame in step by hand. +# Project-specific lifecycle checks, called through project.just. +# Keep common lifecycle assertions consistent across the extensions; +# the marked block holds this project's own behavior checks. set -euo pipefail @@ -135,7 +135,6 @@ export G_MESSAGES_DEBUG=all gnome-shell --wayland --headless --virtual-monitor 3840x1600 >"$LOG" 2>&1 & SHELL_PID=$! -# shellcheck disable=SC2317 # invoked via trap cleanup() { # Captured first: this trap's own last command would otherwise become the # script's exit status, which is how a run that printed PASS still exited 1. diff --git a/scripts/pack-check.sh b/scripts/pack-check.sh deleted file mode 100755 index 662e23a..0000000 --- a/scripts/pack-check.sh +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env bash -# Check the zip `just build` produces against the one GNOME's own packer makes. -# -# `just build` uses plain `zip`, because `gnome-extensions` ships inside the -# gnome-shell package and CI has no GNOME — making the build depend on it would -# pull the whole desktop onto every runner. The cost of hand-rolling the archive -# is that nothing checks the layout, and extensions.gnome.org is strict about it: -# metadata.json must sit at the archive root, never nested. -# -# So the official tool is kept as the authority and consulted here instead. This -# needs a real gnome-shell and so runs locally only, via `just test-live`. - -set -euo pipefail - -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -cd "$REPO_ROOT" - -command -v gnome-extensions >/dev/null || { - echo "FAIL: gnome-extensions not found; it ships with the gnome-shell package" >&2 - exit 1 -} - -UUID="$(jq -r .uuid metadata.json)" -ZIP="$UUID.shell-extension.zip" -SCHEMA="schemas/$(jq -r '."settings-schema"' metadata.json).gschema.xml" - -WORK="$(mktemp -d)" -trap 'rm -rf "$WORK"' EXIT - -[[ -f "$SCHEMA" ]] || { - echo "FAIL: metadata.json names settings-schema not found at $SCHEMA" >&2 - exit 1 -} - -# --extra-source takes a directory and recurses, so modules/ and icons/ go in -# whole. That matters: naming files individually would silently drop a newly -# added module. stylesheet.css is a single file, named only when the project -# has one; `just build` ships it on the same condition. -extra=(--extra-source=modules --extra-source=icons) -[[ -f stylesheet.css ]] && extra+=(--extra-source=stylesheet.css) - -gnome-extensions pack --force -o "$WORK" \ - "${extra[@]}" \ - --schema="$SCHEMA" \ - . >/dev/null - -[[ -f "$ZIP" ]] || { - echo "FAIL: $ZIP not found; run 'just build' first" >&2 - exit 1 -} - -# Directory entries only differ in whether each tool records them, which changes -# nothing about what the Shell loads. -listing() { unzip -Z1 "$1" | grep -v '/$' | LC_ALL=C sort; } - -if ! diff -u <(listing "$WORK/$ZIP") <(listing "$ZIP") \ - --label 'gnome-extensions pack' --label 'just build'; then - echo "FAIL: the built zip does not contain what the official packer produces" >&2 - exit 1 -fi - -# The one rule extensions.gnome.org will not bend on. -if [[ "$(unzip -Z1 "$ZIP" | grep -c '^metadata.json$')" -ne 1 ]]; then - echo "FAIL: metadata.json is not at the archive root" >&2 - exit 1 -fi - -# Every shipped icon must actually decode. -# -# This exists because one did not. A long XML comment placed before the -# element defeats gdk-pixbuf's format sniffing, so the file loads as "couldn't -# recognize the image file format" and the extension renders with no icon at -# all — and nothing appears in the shell log to say why. Neither the unit suite -# nor headless-check.sh looks at an icon, so only this notices. -for icon in "$REPO_ROOT"/icons/*.svg; do - [[ -e "$icon" ]] || continue - if ! gjs -m "$REPO_ROOT/scripts/icon-check.js" "$icon" >/dev/null 2>&1; then - echo "FAIL: $(basename "$icon") does not decode as an image" >&2 - exit 1 - fi -done -echo "ok: $(find "$REPO_ROOT/icons" -name '*.svg' | wc -l) icon(s) decode" - -echo "ok: $(listing "$ZIP" | wc -l) files, matching gnome-extensions pack" -echo "ok: metadata.json at the archive root" -echo "PASS" diff --git a/scripts/security_source.py b/scripts/security_source.py new file mode 100644 index 0000000..b12031a --- /dev/null +++ b/scripts/security_source.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Scan publishable Git source, including untracked edits, without private .env files. + +Git's own ignore rules define what can enter source control. Dependencies are +checked through the lockfile; generated output is not copied into this scan. +Full Git history is checked separately, without this source selection. +""" + +import argparse +import shutil +import subprocess +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent + + +def export(root: Path, destination: Path) -> None: + """Copy tracked and untracked source while respecting Git's ignore rules.""" + result = subprocess.run( + ["/usr/bin/env", "git", "ls-files", "--cached", "--others", "--exclude-standard", "-z"], + cwd=root, + check=True, + capture_output=True, + timeout=30, + ) + for entry in result.stdout.split(b"\0"): + if not entry: + continue + name = Path(entry.decode()) + if name.is_absolute() or ".." in name.parts: + raise ValueError("Git source path must remain inside the repository") + source = root / name + if not source.exists(): + continue + if not source.resolve().is_relative_to(root.resolve()): + raise ValueError("Git source symlink escapes the repository") + target = destination / name + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(source, target) + + +def main() -> None: + """Run the same source scanners locally and in GitHub Actions.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("directory", nargs="?", type=Path, default=ROOT) + root = parser.parse_args().directory.resolve() + with tempfile.TemporaryDirectory(prefix="quick-security-") as work: + staging = Path(work) + export(root, staging) + subprocess.run( + ["/usr/bin/env", "gitleaks", "dir", "--redact", "--no-banner", "."], + cwd=staging, + check=True, + timeout=120, + ) + subprocess.run( + [ + "/usr/bin/env", + "trivy", + "fs", + "--scanners", + "vuln,secret,misconfig", + "--exit-code", + "1", + ".", + ], + cwd=staging, + check=True, + timeout=300, + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/sonar_gate.py b/scripts/sonar_gate.py new file mode 100644 index 0000000..0162547 --- /dev/null +++ b/scripts/sonar_gate.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +"""Require fresh Sonar analysis and zero software-quality issues or duplication.""" + +import argparse +import http.client +import json +import os +from urllib.parse import urlencode + +METRICS = ( + "software_quality_security_issues", + "software_quality_reliability_issues", + "software_quality_maintainability_issues", + "duplicated_lines", + "duplicated_lines_density", + "security_hotspots", +) + + +def request(endpoint: str, parameters: dict[str, str]) -> dict: + """Query Sonar without putting the credential in URLs or diagnostics.""" + token = os.environ.get("SONAR_TOKEN") + if not token: + raise ValueError("SONAR_TOKEN is required; analysis cannot be skipped") + connection = http.client.HTTPSConnection("sonarcloud.io", timeout=30) + try: + connection.request( + "GET", + "/api/" + endpoint + "?" + urlencode(parameters), + headers={"Authorization": "Bearer " + token}, + ) + response = connection.getresponse() + if response.status != 200: + raise RuntimeError(f"Sonar {endpoint} failed: HTTP {response.status}") + data = response.read(4 * 1024 * 1024 + 1) + if len(data) > 4 * 1024 * 1024: + raise ValueError("Sonar response exceeds size limit") + return json.loads(data) + finally: + connection.close() + + +def validate(measures: list[dict], analyzed_revision: str, expected_revision: str) -> None: + """Reject stale, incomplete, or nonzero results, including rounded duplication.""" + if analyzed_revision != expected_revision: + raise ValueError("Sonar analysis does not match the checked commit") + values = {measure["metric"]: measure["value"] for measure in measures} + missing = set(METRICS) - values.keys() + if missing: + raise ValueError("Missing Sonar metrics: " + ", ".join(sorted(missing))) + failures = [f"{name}={values[name]}" for name in METRICS if float(values[name]) != 0] + if failures: + raise ValueError("Sonar policy failed: " + ", ".join(failures)) + + +def main() -> None: + """Check completed analysis for the exact branch and commit.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--project", required=True) + parser.add_argument("--revision", required=True) + parser.add_argument("--branch", default="main") + args = parser.parse_args() + analyses = request( + "project_analyses/search", {"project": args.project, "branch": args.branch, "ps": "1"} + )["analyses"] + if not analyses: + raise ValueError("No Sonar analysis exists for this branch") + measures = request( + "measures/component", + {"component": args.project, "branch": args.branch, "metricKeys": ",".join(METRICS)}, + )["component"]["measures"] + validate(measures, analyses[0].get("revision", ""), args.revision) + dismissed = request( + "issues/search", + { + "componentKeys": args.project, + "branch": args.branch, + "issueStatuses": "ACCEPTED,FALSE_POSITIVE", + "ps": "1", + }, + ) + if dismissed["total"]: + raise ValueError("Sonar findings were dismissed instead of fixed") + print( + "PASS: current revision has zero security, reliability, maintainability, " + "hotspots, and duplicated lines" + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/template-check.sh b/scripts/template-check.sh deleted file mode 100755 index fff76a6..0000000 --- a/scripts/template-check.sh +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env bash -# Check the files the Ghost Assembly extensions share against template.sha256. -# -# template.list names the shared files, one path per line. template.sha256 -# holds their checksums, and is identical in every extension that shares the -# same list, so drift shows from the parent directory in one command: -# -# sort quick*/template.sha256 | uniq -c | sort -n -# -# Any line counted fewer times than there are repositories is a file that -# differs somewhere. -# -# To change a shared file: make the same change in every extension, then run -# `just template-check --write` in each. -# -# scripts/template-check.sh verify; exit 1 on any difference -# scripts/template-check.sh --write regenerate template.sha256 - -set -euo pipefail - -cd "$(dirname "${BASH_SOURCE[0]}")/.." - -LIST=template.list -SUMS=template.sha256 - -usage() { - echo "usage: $0 [--write]" >&2 - exit 2 -} - -[[ -f "$LIST" ]] || { - echo "FAIL: $LIST not found" >&2 - exit 1 -} - -# Blank lines and # comments are allowed in the list; everything else is a path. -mapfile -t paths < <(grep -vE '^[[:space:]]*(#|$)' "$LIST") -((${#paths[@]} > 0)) || { - echo "FAIL: $LIST names no files" >&2 - exit 1 -} - -case "${1:-}" in - --write) - (($# == 1)) || usage - sha256sum -- "${paths[@]}" >"$SUMS" - echo "wrote $SUMS for ${#paths[@]} files" - ;; - "") - [[ -f "$SUMS" ]] || { - echo "FAIL: $SUMS not found; run 'just template-check --write'" >&2 - exit 1 - } - - # The manifest must cover exactly the list, or a path added to one and - # not the other would never be checked. - if ! diff -u --label "$LIST" --label "$SUMS" \ - <(printf '%s\n' "${paths[@]}" | LC_ALL=C sort) \ - <(sed -E 's/^[0-9a-f]{64} [ *]//' "$SUMS" | LC_ALL=C sort) >&2; then - echo "FAIL: $LIST and $SUMS name different files" >&2 - exit 1 - fi - - if ! sha256sum --check --strict --quiet "$SUMS"; then - echo "FAIL: a shared file differs from $SUMS." >&2 - echo "Change it in every extension, then run 'just template-check --write' in each." >&2 - exit 1 - fi - echo "ok: ${#paths[@]} shared files match $SUMS" - ;; - *) - usage - ;; -esac diff --git a/scripts/template.py b/scripts/template.py new file mode 100644 index 0000000..cc04d93 --- /dev/null +++ b/scripts/template.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +"""Synchronize and verify the immutable Ghost Assembly tooling template.""" + +import argparse +import hashlib +import http.client +import io +import json +import os +import re +import tempfile +import zipfile +from pathlib import Path, PurePosixPath + +ROOT = Path(__file__).resolve().parent.parent +REPOSITORY = "Ghost-Assembly/quick-template" + + +def revision(value: str) -> str: + """Accept only an immutable Git commit identifier.""" + if not re.fullmatch(r"[0-9a-f]{40}", value): + raise ValueError("Template revision must be a full lowercase commit SHA") + return value + + +def download(host: str, path: str) -> bytes: + """Read a bounded response from a fixed GitHub HTTPS endpoint.""" + if host not in {"codeload.github.com", "api.github.com"}: + raise ValueError("Unsupported template host") + connection = http.client.HTTPSConnection(host, timeout=30) + try: + connection.request("GET", path, headers={"User-Agent": "quick-template"}) + response = connection.getresponse() + if response.status != 200: + raise RuntimeError(f"Template request failed: HTTP {response.status}") + data = response.read(8 * 1024 * 1024 + 1) + if len(data) > 8 * 1024 * 1024: + raise ValueError("Template response exceeds size limit") + return data + finally: + connection.close() + + +def source_files(sha: str) -> dict[str, bytes]: + """Load the canonical files; local source overrides are forbidden in CI.""" + revision(sha) + local = os.environ.get("QUICK_TEMPLATE_SOURCE") + if local: + if os.environ.get("CI"): + raise ValueError("Local template overrides are not allowed in CI") + source = Path(local).resolve() / "template" + return { + p.relative_to(source).as_posix(): p.read_bytes() + for p in source.rglob("*") + if p.is_file() + and not {".ruff_cache", "__pycache__", "node_modules"}.intersection(p.parts) + } + cache = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache")) / "quick-template" + archive = cache / f"{sha}.zip" + data = ( + archive.read_bytes() + if archive.exists() and archive.stat().st_size <= 8 * 1024 * 1024 + else b"" + ) + if not zipfile.is_zipfile(io.BytesIO(data)) or os.environ.get("CI"): + data = download("codeload.github.com", f"/{REPOSITORY}/zip/{sha}") + if not zipfile.is_zipfile(io.BytesIO(data)): + raise ValueError("Canonical template response is not a ZIP archive") + cache.mkdir(parents=True, exist_ok=True) + temporary = None + try: + with tempfile.NamedTemporaryFile( + dir=cache, prefix=".download-", delete=False + ) as stream: + temporary = Path(stream.name) + stream.write(data) + temporary.replace(archive) + finally: + if temporary is not None: + temporary.unlink(missing_ok=True) + prefix = f"quick-template-{sha}/template/" + files = {} + with zipfile.ZipFile(io.BytesIO(data)) as bundle: + if sum(item.file_size for item in bundle.infolist()) > 32 * 1024 * 1024: + raise ValueError("Expanded template exceeds size limit") + for item in bundle.infolist(): + if item.filename.startswith(prefix) and not item.is_dir(): + name = item.filename.removeprefix(prefix) + path = PurePosixPath(name) + if path.is_absolute() or ".." in path.parts or str(path) != name: + raise ValueError("Invalid canonical template path") + files[name] = bundle.read(item) + if "justfile" not in files or "scripts/template.py" not in files: + raise ValueError("Incomplete canonical template archive") + return files + + +def expected_files(root: Path, files: dict[str, bytes]) -> dict[str, bytes]: + """Render project identity into the otherwise identical npm metadata.""" + expected = dict(files) + package = json.loads((root / "package.json").read_text()) + for name in ["package.json", "package-lock.json"]: + document = json.loads(expected[name]) + document["name"] = package["name"] + document["version"] = package["version"] + if name == "package-lock.json": + document["packages"][""]["name"] = package["name"] + document["packages"][""]["version"] = package["version"] + expected[name] = (json.dumps(document, indent=2) + "\n").encode() + return expected + + +def compare(root: Path, expected: dict[str, bytes]) -> list[str]: + """Report every missing or changed managed file without writing.""" + failures = [] + for name, contents in sorted(expected.items()): + path = root / name + if path.is_symlink() or not path.is_file() or path.read_bytes() != contents: + failures.append(name) + return failures + + +def synchronize(root: Path, sha: str, files: dict[str, bytes]) -> None: + """Install reviewed template content and record its immutable revision.""" + for name, contents in expected_files(root, files).items(): + path = root / name + if path.is_symlink() or not path.resolve().is_relative_to(root.resolve()): + raise ValueError("Managed path escapes project") + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(contents) + (root / "quick-template.lock.json").write_text( + json.dumps( + { + "$schema": "quick-template.schema.json", + "repository": REPOSITORY, + "revision": revision(sha), + }, + indent=2, + ) + + "\n" + ) + + +def main() -> None: + """Expose verification, explicit synchronization, and freshness checks.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("command", choices=["check", "sync", "status"]) + parser.add_argument("revision", nargs="?") + args = parser.parse_args() + lock = json.loads((ROOT / "quick-template.lock.json").read_text()) + if lock["repository"] != REPOSITORY: + raise ValueError("Template repository must be Ghost-Assembly/quick-template") + sha = revision(args.revision or lock["revision"]) + if args.command == "status": + release = json.loads(download("api.github.com", f"/repos/{REPOSITORY}/releases/latest")) + latest = revision(release["target_commitish"]) + if latest != sha: + raise SystemExit( + f"Template update available: {sha} -> {latest}; run just template-sync {latest}" + ) + print(f"PASS: latest approved template {sha}") + return + files = source_files(sha) + if args.command == "sync": + synchronize(ROOT, sha, files) + print(f"Synced template {sha}; run npm ci --ignore-scripts and just docs-generate") + return + failures = compare(ROOT, expected_files(ROOT, files)) + if failures: + raise SystemExit("Template drift:\n" + "\n".join(failures)) + digest = hashlib.sha256("\n".join(sorted(files)).encode()).hexdigest()[:12] + print(f"PASS: {len(files)} canonical files match {sha} (inventory {digest})") + + +if __name__ == "__main__": + main() diff --git a/scripts/workflow_lint.py b/scripts/workflow_lint.py new file mode 100644 index 0000000..b8a8183 --- /dev/null +++ b/scripts/workflow_lint.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +"""Lint workflows while adapting GitHub's new self references for actionlint 1.7.12. + +GitHub supports $/ references as of July 2026. This actionlint release predates +that syntax. Normalize only uses values for its parser; zizmor checks originals. +No diagnostic is filtered or suppressed. +""" + +import argparse +import re +import subprocess +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent + + +def normalize(source: str) -> str: + """Translate equivalent local references without changing workflow behavior.""" + return re.sub(r"(?m)^(\s*(?:-\s+)?uses:\s+)\$/", r"\1./", source) + + +def main() -> None: + """Check every workflow and preserve actionlint's exit status and messages.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("directory", nargs="?", type=Path, default=ROOT) + args = parser.parse_args() + paths = sorted((args.directory / ".github/workflows").glob("*.y*ml")) + if not paths: + raise ValueError("No workflows found") + for path in paths: + print(f"Checking {path}", flush=True) + subprocess.run( + ["/usr/bin/env", "actionlint", "-"], + input=normalize(path.read_text()), + cwd=args.directory, + text=True, + check=True, + timeout=60, + ) + + +if __name__ == "__main__": + main() diff --git a/sonar-project.properties b/sonar-project.properties index f423d31..36054eb 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,20 +1,7 @@ -# SonarQube Cloud. projectKey and organization must match what Sonar creates -# when the project is imported; adjust them there rather than guessing here. -sonar.projectKey=Ghost-Assembly_quicktiler sonar.organization=ghost-assembly - -sonar.sources=modules,extension.js,prefs.js +sonar.sources=. sonar.tests=tests -sonar.exclusions=node_modules/**,coverage/** - +sonar.exclusions=tests/**,node_modules/**,coverage/**,test-results/**,playwright-report/**,.scannerwork/** sonar.javascript.lcov.reportPaths=coverage/lcov.info - -# prefs.js holds only Adw/Gtk widget construction; every decision it used to -# make lives in modules/shortcuts.js and is unit-tested there. Testing what -# remains would mean asserting against stubs of the toolkit, which tests the -# stubs. -# -# vitest.config.js excludes tests/** as well, but only to keep a dynamically -# imported stub from being counted; nothing under tests/ is in sonar.sources, -# so there is nothing to exclude here. -sonar.coverage.exclusions=prefs.js +sonar.qualitygate.wait=true +sonar.qualitygate.timeout=600 diff --git a/template.list b/template.list deleted file mode 100644 index 9812a64..0000000 --- a/template.list +++ /dev/null @@ -1,29 +0,0 @@ -.github/dependabot.yml -.github/workflows/ci.yml -.github/workflows/release.yml -.github/workflows/security.yml -.github/workflows/sonar.yml -.gitignore -.gitleaks.toml -.prettierignore -.prettierrc.json -docs/style.css -eslint.config.js -justfile -mise.toml -scripts/icon-check.js -scripts/pack-check.sh -scripts/template-check.sh -template.list -tests/docs.spec.js -tests/stubs/gi-glib.js -tests/stubs/gi-gobject.js -tests/stubs/gi-meta.js -tests/stubs/gi-pango.js -tests/stubs/gi-shell.js -tests/stubs/gi-st.js -tests/stubs/shell-extension.js -tests/stubs/shell-main.js -tests/stubs/shell-popupmenu.js -tests/stubs/shell-quicksettings.js -tests/support/actors.js diff --git a/template.sha256 b/template.sha256 deleted file mode 100644 index 8f92fd9..0000000 --- a/template.sha256 +++ /dev/null @@ -1,29 +0,0 @@ -12415a3cfbbef2613db869ccd9ed15e10dbb2402338105e3c8be109696d6f379 .github/dependabot.yml -59ca7e1a2b184d3e53a662f89d96fa073980a8e33c89b36108c8874ce0e63a40 .github/workflows/ci.yml -ea71be260120d9245df6553528cca2233b5dd21cb9b5015cdb883f75796bb9c1 .github/workflows/release.yml -d199aa2ec6ac57ef6c9ba3caabfbf227602bdc04319a98bd5a6fe7d3d003a64d .github/workflows/security.yml -ee1c07b1a15412996121316e5c15f85721b110c61d335e27f8bf74f85bf7ee21 .github/workflows/sonar.yml -21634e8476b5091a2f848cc0609e224d15aa233db1914b0ecf23a023b018c414 .gitignore -6afe55d45a0b5a2b0657b283a2a6b41a95e935d5c0346ed41be42b06c3e90ead .gitleaks.toml -6df2fb1f584a0130a0b41c5f60f85d1f6b8d940b1d1dcfb30d1f84877e7e98d3 .prettierignore -aa9b2aaa8571afe46ebcfdcdeccb3960f8469cddca366a1b908a427477304821 .prettierrc.json -19157a9ed63cbefee11d6ba3248a606833b89a85b5576b3d0545f4c5bd067e03 docs/style.css -8372f58f1647d10e52216812d45a6c95f629d0663f3aa960f52e668f755aa56e eslint.config.js -7805aaac9c13cf03d33fff8a4decbd179fe7825d05f8659c190c3132e6ca60b1 justfile -cbde6c3009a0b09910a20dc4bcd17bdf19c9174eee6d74c67c825efbfd9c17ca mise.toml -2665c7176c3d98538cb9d2fff77d9c6c8af41c79c0f14a41566ab70a3f68931c scripts/icon-check.js -9f0b7cbd6438b22189e93b0201d3c5d1717709e1cc433472ce942d84d46f854b scripts/pack-check.sh -508b2aabe2c485cc9cdaf51ca7dd1a70e53c70c65c7dd0149dc1458a4f78a679 scripts/template-check.sh -f0a3265f966c1017a80bcba09bff0c465ccec94b7ad9dd6456742f94905b3ae2 template.list -f13c2fca51ef7a2db1755805d5f24a4a55c97605d40610168f1d39d12ad408af tests/docs.spec.js -8b3467177303c86b68bea7532c3bcec8585f5d66d0b7f1d604d22ef5790bea15 tests/stubs/gi-glib.js -3969a5d8b38c607f1f70be1e6e88ce3ff75749710e1591437b511a5fff300df1 tests/stubs/gi-gobject.js -dbebfe1620af3038a86d9454c57778ee4a5885f07748e6acb3f83aa11353dc86 tests/stubs/gi-meta.js -757435b7d2469254f94210744c243fd0dc49983800f2f227fd90c7eb413e385a tests/stubs/gi-pango.js -504707f2bc4516136d5bcd23ecaed1aaeeab81568e4b29daf8dcfc37917a52e3 tests/stubs/gi-shell.js -e28f5f32af5bf490b92e8e1731a79eb59b39bd81078b13414aec34e7a3a4d6c4 tests/stubs/gi-st.js -5b849db4cf452d4e5e1ff9ab8d81c85af6ac0317a2fd8dfd507842b26ce0f629 tests/stubs/shell-extension.js -8972b45189ea7669f975ffd0478401a081ccd0b0db30c956853f68ac2b483aff tests/stubs/shell-main.js -9b60282e2e51220b700a13fe9ba07b223af0e181364c369a6b8b63f23d36f7df tests/stubs/shell-popupmenu.js -879b3b57c24c6b49eb0ea5765d52caab58e04ac690e3be5a098cbb84ca678331 tests/stubs/shell-quicksettings.js -38a2282ebca034f43da8af6785c1fbd386a176c79cad2200163c113deca3a63a tests/support/actors.js diff --git a/tests/docs.config.js b/tests/docs.config.js index 30f4f2c..93e4c4c 100644 --- a/tests/docs.config.js +++ b/tests/docs.config.js @@ -10,6 +10,7 @@ export default { sections: [ ['overview', 'Overview'], ['install', 'Install'], + ['uninstall', 'Uninstall'], ['zones', 'Zones'], ['quick-settings', 'Quick Settings'], ['preferences', 'Preferences'], diff --git a/tests/docs.spec.js b/tests/docs.spec.js index 1df5785..14705de 100644 --- a/tests/docs.spec.js +++ b/tests/docs.spec.js @@ -10,17 +10,12 @@ // tests/docs.config.js. import { readFileSync } from 'node:fs'; -import { fileURLToPath } from 'node:url'; import AxeBuilder from '@axe-core/playwright'; import { expect, test } from '@playwright/test'; import config from './docs.config.js'; - -const METADATA = fileURLToPath(new URL('../metadata.json', import.meta.url)); -// METADATA is a module-relative constant, not input of any kind. -// eslint-disable-next-line security/detect-non-literal-fs-filename -const metadata = JSON.parse(readFileSync(METADATA, 'utf8')); +import metadata from '../metadata.json' with { type: 'json' }; const { site, repo, title, sections } = config; @@ -89,6 +84,21 @@ test('images and social metadata resolve', async ({ page, request }) => { // The page states facts about the project; these tie them to its own files so // a release cannot leave the docs behind. test.describe('agrees with metadata.json', () => { + test('protects bundle names from Cloudflare email obfuscation', async ({ + page, + }) => { + await page.goto('/'); + const names = page.locator('code').filter({ + hasText: `${metadata.uuid}.shell-extension.zip`, + }); + expect(await names.count()).toBeGreaterThan(0); + for (const name of await names.all()) { + const html = await name.innerHTML(); + expect(html).toContain(''); + expect(html).toContain(''); + } + }); + test('installs the real uuid', async ({ page }) => { await page.goto('/'); const install = page.locator('#install'); @@ -127,6 +137,7 @@ test.describe('agrees with metadata.json', () => { if (config.drawing) { test('draws only what the extension shows', async ({ page }) => { await page.goto('/'); + await expect(page.locator('figure.shot')).toBeVisible(); await config.drawing(page.locator('figure.shot'), expect); }); } @@ -157,19 +168,13 @@ test('numbers the sections in the order the contents list gives', async ({ page // docs.config.js opts out with `readmeLinks: false` while the README has none. if (config.readmeLinks !== false) { test('keeps the ids README.md links to', async ({ page }) => { - const README = fileURLToPath(new URL('../README.md', import.meta.url)); - // README is a module-relative constant, not input of any kind. - // eslint-disable-next-line security/detect-non-literal-fs-filename - const readme = readFileSync(README, 'utf8'); - // The site's URL without its scheme, as a literal: a README may link - // with or without https://. - const host = site - .replace(/^https?:\/\//, '') - .replaceAll(/[.*+?^${}()|[\]\\/]/g, '\\$&'); - // Built from docs.config.js, a module-relative constant, not input. - // eslint-disable-next-line security/detect-non-literal-regexp - const pattern = new RegExp(`${host}#([\\w-]+)`, 'g'); - const ids = [...readme.matchAll(pattern)].map(match => match[1]); + // Playwright runs from the repository root, as configured by just. + const readme = readFileSync('README.md', 'utf8'); + const host = site.replace(/^https?:\/\//, ''); + const ids = [...readme.matchAll(/\]\(([^)\s]+)\)/g)] + .map(match => match[1]) + .filter(link => link.startsWith(site + '#') || link.startsWith(host + '#')) + .map(link => link.split('#')[1]); expect(ids.length).toBeGreaterThan(0); await page.goto('/'); @@ -200,23 +205,32 @@ test.describe('contents list', () => { test('is a sticky sidebar on a desktop', async ({ page }) => { await page.goto('/'); await expect(page.locator('nav.toc')).toBeVisible(); - await expect(page.locator('details.toc-m')).toBeHidden(); + await expect(page.locator('.toc-controls')).toBeHidden(); + await expect(page.locator('nav.toc')).toHaveCount(1); await page.locator(`#${sections.at(-1)[0]}`).scrollIntoViewIfNeeded(); await expect(page.locator('nav.toc')).toBeInViewport(); }); - test('folds into a disclosure on a phone', async ({ page }) => { + test('expands the same contents list by keyboard on a phone', async ({ page }) => { await page.setViewportSize(phone); await page.goto('/'); await expect(page.locator('nav.toc')).toBeHidden(); - const details = page.locator('details.toc-m'); - await expect(details).toBeVisible(); - await expect(details.getByRole('link', { name: /Install/ })).toBeHidden(); - - await details.locator('summary').click(); - await expect(details.getByRole('link', { name: /Install/ })).toBeVisible(); + const toggle = page.getByRole('checkbox', { name: 'On this page' }); + await expect(toggle).toBeVisible(); + await expect(toggle).not.toBeChecked(); + await toggle.focus(); + await page.keyboard.press('Space'); + await expect(toggle).toBeChecked(); + await expect( + page.locator('nav.toc').getByRole('link', { name: /Install/ }), + ).toBeVisible(); + await page.keyboard.press('Tab'); + await expect(page.locator('nav.toc a').first()).toBeFocused(); + await toggle.focus(); + await page.keyboard.press('Space'); + await expect(page.locator('nav.toc')).toBeHidden(); }); }); diff --git a/tests/stubs/shell-extension.js b/tests/stubs/shell-extension.js index 74bd8c3..3296ee2 100644 --- a/tests/stubs/shell-extension.js +++ b/tests/stubs/shell-extension.js @@ -5,8 +5,8 @@ export class Extension { /** * @param {object} [metadata] Contents of metadata.json. */ - constructor(metadata = { 'version-name': '0.0.0' }) { - this.metadata = metadata; + constructor(metadata) { + this.metadata = metadata ?? { 'version-name': '0.0.0' }; // The real base class resolves and caches a Gio.Settings from the // gschema. Tests set `settings` on the instance instead, so diff --git a/tests/support/schema.js b/tests/support/schema.js index c3a327b..23430bb 100644 --- a/tests/support/schema.js +++ b/tests/support/schema.js @@ -9,9 +9,12 @@ // others quietly checking less than they claim to. That is the same drift these // suites exist to catch, so it is read in one place instead. -import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; +import xml from '../../schemas/org.gnome.shell.extensions.quicktiler.gschema.xml?raw'; +import readme from '../../README.md?raw'; +import page from '../../docs/index.html?raw'; + /** Absolute path to the shipped gschema. */ export const SCHEMA = fileURLToPath( new URL( @@ -21,16 +24,19 @@ export const SCHEMA = fileURLToPath( ); /** - * Read a file the tests own, by absolute path. + * Read one of the statically imported repository fixtures, by absolute path. * * @param {string} path Absolute path. * @returns {string} File contents. */ export function read(path) { - // Each path is a module-relative constant resolved from import.meta.url, not - // input of any kind; the rule cannot see that it is not a variable path. - // eslint-disable-next-line security/detect-non-literal-fs-filename - return readFileSync(path, 'utf8'); + const fixture = new Map([ + [SCHEMA, xml], + [fileURLToPath(new URL('../../README.md', import.meta.url)), readme], + [fileURLToPath(new URL('../../docs/index.html', import.meta.url)), page], + ]); + if (!fixture.has(path)) throw new Error('Unknown repository fixture.'); + return fixture.get(path); } /** diff --git a/tests/test_tooling.py b/tests/test_tooling.py new file mode 100644 index 0000000..4fe34f5 --- /dev/null +++ b/tests/test_tooling.py @@ -0,0 +1,194 @@ +"""Behavior regressions for canonical tooling and publication boundaries.""" + +import importlib.util +import io +import json +import os +import subprocess +import tempfile +import unittest +import zipfile +from pathlib import Path +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[1] + + +def load(name: str): + """Load the repository helper rather than a separate implementation.""" + spec = importlib.util.spec_from_file_location(name, ROOT / "scripts" / f"{name}.py") + if spec is None or spec.loader is None: + raise RuntimeError("Cannot load shared helper") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +template = load("template") +gate = load("sonar_gate") +bundle = load("build") +docs = load("docs") +workflow = load("workflow_lint") +security = load("security_source") + + +class SecuritySourceTests(unittest.TestCase): + def test_scan_includes_untracked_source_but_not_private_ignored_files(self) -> None: + with tempfile.TemporaryDirectory() as work: + root = Path(work) / "repo" + destination = Path(work) / "scan" + root.mkdir() + destination.mkdir() + subprocess.run( + ["/usr/bin/env", "git", "init", "--quiet"], cwd=root, check=True, timeout=10 + ) + (root / ".gitignore").write_text(".env\ncoverage/\n") + (root / ".env").write_text("private fixture") + (root / "new.py").write_text("new source") + security.export(root, destination) + self.assertEqual((destination / "new.py").read_text(), "new source") + self.assertFalse((destination / ".env").exists()) + + def test_source_symlink_cannot_read_outside_the_repository(self) -> None: + with tempfile.TemporaryDirectory() as work: + root = Path(work) / "repo" + destination = Path(work) / "scan" + root.mkdir() + destination.mkdir() + subprocess.run( + ["/usr/bin/env", "git", "init", "--quiet"], cwd=root, check=True, timeout=10 + ) + private = Path(work) / "private" + private.write_text("private fixture") + (root / "alias.py").symlink_to(private) + with self.assertRaises(ValueError): + security.export(root, destination) + + +class WorkflowTests(unittest.TestCase): + def test_only_self_repository_uses_are_adapted(self) -> None: + source = " uses: $/.github/workflows/ci.yml\n run: echo '$/unchanged'\n" + self.assertEqual( + workflow.normalize(source), + " uses: ./.github/workflows/ci.yml\n run: echo '$/unchanged'\n", + ) + + def test_invalid_workflow_content_is_preserved_for_the_linter(self) -> None: + source = "jobs:\n run: invalid\n uses: remote/repo@main\n" + self.assertEqual(workflow.normalize(source), source) + + +class TemplateTests(unittest.TestCase): + def test_corrupt_archive_cache_is_recovered_and_reused(self) -> None: + sha = "a" * 40 + buffer = io.BytesIO() + expected = {"justfile": b"canonical commands", "scripts/template.py": b"canonical helper"} + with zipfile.ZipFile(buffer, "w") as bundle: + for name, data in expected.items(): + bundle.writestr(f"quick-template-{sha}/template/{name}", data) + with tempfile.TemporaryDirectory() as work: + cache = Path(work) / "quick-template" + cache.mkdir() + archive = cache / f"{sha}.zip" + archive.write_bytes(b"interrupted download") + with ( + patch.dict( + os.environ, {"XDG_CACHE_HOME": work, "QUICK_TEMPLATE_SOURCE": "", "CI": ""} + ), + patch.object(template, "download", return_value=buffer.getvalue()) as download, + ): + self.assertEqual(template.source_files(sha), expected) + self.assertEqual(template.source_files(sha), expected) + download.assert_called_once() + self.assertTrue(zipfile.is_zipfile(archive)) + self.assertEqual(list(cache.glob(".download-*")), []) + + def test_local_manifest_cannot_bless_changed_canonical_content(self) -> None: + with tempfile.TemporaryDirectory() as work: + root = Path(work) + (root / "mise.toml").write_text("node = 'latest'") + (root / "template.sha256").write_text("locally approved checksum") + self.assertEqual( + template.compare(root, {"mise.toml": b"node = '24.21.0'"}), ["mise.toml"] + ) + + def test_missing_files_and_symlinks_fail(self) -> None: + with tempfile.TemporaryDirectory() as work: + root = Path(work) + (root / "alias").symlink_to(root / "missing") + self.assertEqual( + template.compare(root, {"alias": b"x", "missing": b"y"}), ["alias", "missing"] + ) + + def test_template_revision_must_be_immutable(self) -> None: + for value in ["main", "v1.0.0", "a" * 39, "a" * 41, "../main"]: + with self.subTest(value=value), self.assertRaises(ValueError): + template.revision(value) + + +class SonarTests(unittest.TestCase): + def measures(self) -> list[dict]: + return [{"metric": name, "value": "0"} for name in gate.METRICS] + + def test_current_zero_results_pass(self) -> None: + self.assertIsNone(gate.validate(self.measures(), "current", "current")) + + def test_every_quality_category_and_exact_duplication_are_required(self) -> None: + for name in gate.METRICS: + measures = self.measures() + next(item for item in measures if item["metric"] == name)["value"] = "1" + with self.subTest(metric=name), self.assertRaises(ValueError): + gate.validate(measures, "current", "current") + + def test_missing_metrics_and_stale_analysis_fail(self) -> None: + with self.assertRaises(ValueError): + gate.validate(self.measures()[1:], "current", "current") + with self.assertRaises(ValueError): + gate.validate(self.measures(), "previous", "current") + + +class PackagingTests(unittest.TestCase): + def test_unsafe_runtime_paths_are_rejected_before_packaging(self) -> None: + with tempfile.TemporaryDirectory() as work: + root = Path(work) + for name in ["../secret", "/etc/passwd", "scripts/../../secret", ".env"]: + (root / "quick-project.json").write_text(json.dumps({"runtimeFiles": [name]})) + with self.subTest(name=name), self.assertRaises(ValueError): + bundle.runtime_files(root) + + def test_invalid_uuid_cannot_choose_an_output_path(self) -> None: + with tempfile.TemporaryDirectory() as work: + root = Path(work) + (root / "metadata.json").write_text(json.dumps({"uuid": "../../secret"})) + with self.assertRaises(ValueError): + bundle.artifact_name(root) + + +class DocumentationTests(unittest.TestCase): + def test_install_restart_precedes_enable(self) -> None: + sections = docs.blocks( + {"uuid": "fixture@example.test", "shell-version": ["50"]}, + {"repository": "fixture", "requirements": "Fixture dependency."}, + ) + content = docs.markdown(sections["install"]) + self.assertLess( + content.index("Log out and back in"), content.index("gnome-extensions enable") + ) + self.assertIn("Fixture dependency.", content) + + def test_html_escapes_project_content(self) -> None: + self.assertEqual( + docs.markup([("p", "")]), + "

<script>unsafe</script>

", + ) + + def test_missing_or_duplicate_generation_markers_fail(self) -> None: + with self.assertRaises(ValueError): + docs.replace_block("handwritten", "install", "generated") + region = "" + with self.assertRaises(ValueError): + docs.replace_block(region * 2, "install", "generated") + + +if __name__ == "__main__": + unittest.main() diff --git a/vitest.config.js b/vitest.config.js index 7a6de93..9437154 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -10,36 +10,18 @@ export default defineConfig({ include: ['tests/**/*.test.js'], coverage: { provider: 'v8', - reporter: ['text', 'lcov'], - // Everything the extension ships, so the denominator is the real - // one. Listing only the modules that happen to be covered would - // measure coverage against a figure chosen to flatter it. - include: ['modules/**/*.js', 'extension.js', 'prefs.js'], - // prefs.js is the sole exception, and only because every decision - // it used to make now lives in modules/shortcuts.js and is tested - // there. What is left is Adw and Gtk widget construction, which a - // unit test can only assert against stubs of the toolkit — that - // tests the stubs, not the code. Kept identical to - // sonar.coverage.exclusions so the two agree. - // - // tests/** is listed because `include` above did not keep a - // dynamically imported stub out of the report: - // tests/quicktiler-handlers.test.js pulls a stub in through - // vi.doMock and it turned up as production code. A stub counted - // either way is a number that means nothing. - exclude: ['prefs.js', 'tests/**'], + reporter: ['text', 'lcov', 'html'], + include: [ + 'modules/**/*.js', + 'extension.js', + 'prefs.js', + 'scripts/soloist-runner.js', + ], + exclude: ['tests/**'], }, }, - // gnome-shell resolves these at runtime; Node cannot. Pointing them at - // stubs is what makes the Shell layer — and the bugs fixed in it — - // reachable from Vitest at all. The stubs live in tests/, so they are never - // shipped and never counted as covered code. - // - // gi://Adw, gi://Gdk and gi://Gtk are deliberately absent. Only prefs.js - // imports them, and it is excluded below and never imported by a test. The - // day this list needs one of them is the day a decision has leaked into the - // preferences widgets, and the missing alias is how we find out. + // GNOME imports resolve to recording stubs for offline behavior tests. resolve: { alias: [ { find: 'gi://Clutter', replacement: stub('gi-clutter') }, From a64c3e35812f8404fb4a5995f74925bbba4c5b16 Mon Sep 17 00:00:00 2001 From: napalm255 Date: Sat, 3 Oct 2026 15:00:20 -0400 Subject: [PATCH 2/7] fix(ci): install SVG decoding and require full Sonar analysis --- .github/workflows/ci.yml | 2 +- .github/workflows/sonar.yml | 6 +++++- quick-template.lock.json | 2 +- scripts/icon-check.js | 2 +- scripts/sonar_gate.py | 11 +++++++++++ tests/test_tooling.py | 13 +++++++++++++ 6 files changed, 32 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 111813b..080f610 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: - name: Install native test tools run: | sudo apt-get update - sudo apt-get install -y --no-install-recommends gjs gnome-shell libglib2.0-bin gir1.2-soup-3.0 gir1.2-secret-1 dbus-daemon + sudo apt-get install -y --no-install-recommends gjs gnome-shell libglib2.0-bin librsvg2-common gir1.2-soup-3.0 gir1.2-secret-1 dbus-daemon - run: npm ci --ignore-scripts - run: ./node_modules/.bin/playwright install --with-deps chromium firefox - run: just ci diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 516a35b..6cb541c 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -38,7 +38,11 @@ jobs: BRANCH: ${{ github.ref_name }} run: | branch="$BRANCH" - if [ -n "$REVIEW" ]; then branch="review-$REVIEW"; fi + if [ -n "$REVIEW" ]; then + branch="branch-review-$REVIEW" + elif [ "$branch" != main ]; then + branch="branch-$branch" + fi revision="$(git rev-parse HEAD)" project="Ghost-Assembly_$(jq -r .name package.json)" sonar-scanner -Dsonar.projectKey="$project" -Dsonar.branch.name="$branch" -Dsonar.scm.revision="$revision" -Dsonar.qualitygate.wait=true diff --git a/quick-template.lock.json b/quick-template.lock.json index 9267414..3d40bc3 100644 --- a/quick-template.lock.json +++ b/quick-template.lock.json @@ -1,5 +1,5 @@ { "$schema": "quick-template.schema.json", "repository": "Ghost-Assembly/quick-template", - "revision": "effd43bd6c74609886a2574f9a1151eff46b7ff9" + "revision": "2ca95e238eda3966af16ee7c6e876d9618e71696" } diff --git a/scripts/icon-check.js b/scripts/icon-check.js index b0b6ce1..9787e2d 100644 --- a/scripts/icon-check.js +++ b/scripts/icon-check.js @@ -1,6 +1,6 @@ // Assert that one icon file actually decodes. // -// Run by scripts/pack-check.sh over everything in icons/. It exists because an +// Run by just pack-check over everything in icons/. It exists because an // icon that fails to load is completely silent: gnome-shell draws nothing and // logs nothing, so the first report is a person saying "there is no icon". // diff --git a/scripts/sonar_gate.py b/scripts/sonar_gate.py index 0162547..ca39904 100644 --- a/scripts/sonar_gate.py +++ b/scripts/sonar_gate.py @@ -40,6 +40,15 @@ def request(endpoint: str, parameters: dict[str, str]) -> dict: connection.close() +def validate_branch(branches: list[dict], expected_name: str) -> None: + """Require overall-code analysis rather than new-code-only short branches.""" + branch = next((item for item in branches if item.get("name") == expected_name), None) + if branch is None: + raise ValueError("No Sonar analysis exists for the checked branch") + if branch.get("type") != "LONG": + raise ValueError("Sonar must analyze overall code on a long-lived branch") + + def validate(measures: list[dict], analyzed_revision: str, expected_revision: str) -> None: """Reject stale, incomplete, or nonzero results, including rounded duplication.""" if analyzed_revision != expected_revision: @@ -60,6 +69,8 @@ def main() -> None: parser.add_argument("--revision", required=True) parser.add_argument("--branch", default="main") args = parser.parse_args() + branches = request("project_branches/list", {"project": args.project})["branches"] + validate_branch(branches, args.branch) analyses = request( "project_analyses/search", {"project": args.project, "branch": args.branch, "ps": "1"} )["analyses"] diff --git a/tests/test_tooling.py b/tests/test_tooling.py index 4fe34f5..0fb8578 100644 --- a/tests/test_tooling.py +++ b/tests/test_tooling.py @@ -133,6 +133,19 @@ def measures(self) -> list[dict]: def test_current_zero_results_pass(self) -> None: self.assertIsNone(gate.validate(self.measures(), "current", "current")) + def test_short_branch_results_cannot_approve_overall_code(self) -> None: + branches = [ + {"name": "main", "type": "LONG"}, + {"name": "branch-review-1", "type": "LONG"}, + {"name": "review-1", "type": "SHORT"}, + ] + for name in ["main", "branch-review-1"]: + with self.subTest(branch=name): + self.assertIsNone(gate.validate_branch(branches, name)) + for name in ["review-1", "missing"]: + with self.subTest(branch=name), self.assertRaises(ValueError): + gate.validate_branch(branches, name) + def test_every_quality_category_and_exact_duplication_are_required(self) -> None: for name in gate.METRICS: measures = self.measures() From e2f0389566eb2cba89f0b628bb0d9b834411172b Mon Sep 17 00:00:00 2001 From: napalm255 Date: Sat, 3 Oct 2026 15:05:45 -0400 Subject: [PATCH 3/7] fix(ci): scope release artifact access to verification jobs --- .github/workflows/release.yml | 4 +++- quick-template.lock.json | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c62a5fa..27e8b3e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,7 +4,6 @@ on: tags: ['v*'] permissions: contents: read - actions: read # Inspect workflow runs and download their tested artifacts. concurrency: group: release-${{ github.ref }} cancel-in-progress: false @@ -13,6 +12,9 @@ jobs: name: verify runs-on: ubuntu-24.04 timeout-minutes: 10 + permissions: + contents: read + actions: read # Inspect workflow runs and download their tested artifacts. outputs: run: ${{ steps.verify.outputs.run }} steps: diff --git a/quick-template.lock.json b/quick-template.lock.json index 3d40bc3..1b9c5ce 100644 --- a/quick-template.lock.json +++ b/quick-template.lock.json @@ -1,5 +1,5 @@ { "$schema": "quick-template.schema.json", "repository": "Ghost-Assembly/quick-template", - "revision": "2ca95e238eda3966af16ee7c6e876d9618e71696" + "revision": "d6fa407ce8886a5841e6766bef2969c56e222266" } From a755245525fd3ccac808a8df6daa2303dce6c066 Mon Sep 17 00:00:00 2001 From: napalm255 Date: Sat, 3 Oct 2026 15:36:00 -0400 Subject: [PATCH 4/7] docs: leave submission version numbering to GNOME --- README.md | 2 +- docs/index.html | 8 ++++---- quick-template.lock.json | 2 +- scripts/docs.py | 5 +++-- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 5c4c31c..aaaad1a 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,7 @@ just pack-check compares both filenames and file contents with GNOME's official -Run just ci, just test-live, and the project manual checklist. Set metadata.json version-name and package.json version to the same new version and increment metadata.json version for the GNOME Extension Store. Update the npm lockfile, regenerate the docs, and commit the reviewed changes to main through a passing pull request. +Run just ci, just test-live, and the project manual checklist. Set metadata.json version-name and package.json version to the same new version. The GNOME Extensions website assigns the numeric metadata.json version during submission. Update the npm lockfile, regenerate the docs, and commit the reviewed changes to main through a passing pull request. Create and push a v-prefixed tag for that version. The release workflow verifies the version, main ancestry, and successful required checks for the tagged commit, then attaches its tested ZIP to a GitHub release. It does not upload to extensions.gnome.org; that submission and its review remain manual. diff --git a/docs/index.html b/docs/index.html index c4e9491..a7eca88 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1336,10 +1336,10 @@

Releasing

Run just ci, just test-live, and the project manual checklist. Set metadata.json version-name and package.json version to the - same new version and increment metadata.json version for the - GNOME Extension Store. Update the npm lockfile, regenerate the - docs, and commit the reviewed changes to main through a passing - pull request. + same new version. The GNOME Extensions website assigns the + numeric metadata.json version during submission. Update the npm + lockfile, regenerate the docs, and commit the reviewed changes + to main through a passing pull request.

Create and push a v-prefixed tag for that version. The release diff --git a/quick-template.lock.json b/quick-template.lock.json index 1b9c5ce..09995d0 100644 --- a/quick-template.lock.json +++ b/quick-template.lock.json @@ -1,5 +1,5 @@ { "$schema": "quick-template.schema.json", "repository": "Ghost-Assembly/quick-template", - "revision": "d6fa407ce8886a5841e6766bef2969c56e222266" + "revision": "3a46437ad001ece2fdbfcba1b615363331df45fb" } diff --git a/scripts/docs.py b/scripts/docs.py index 552581c..bd49f6c 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -156,8 +156,9 @@ def blocks(metadata: dict, project: dict) -> dict[str, list[tuple[str, str]]]: ( "Run just ci, just test-live, and the project manual checklist. Set " "metadata.json version-name and package.json version to the same new " - "version and increment metadata.json version for the GNOME Extension " - "Store. Update the npm lockfile, regenerate the docs, and commit the " + "version. The GNOME Extensions website assigns the numeric metadata.json " + "version during submission. Update the npm lockfile, regenerate the docs, " + "and commit the " "reviewed changes to main through a passing pull request." ), ), From 822315ca9e445432e0c7d1dc413456de8fad0ccf Mon Sep 17 00:00:00 2001 From: napalm255 Date: Sat, 3 Oct 2026 17:35:56 -0400 Subject: [PATCH 5/7] fix: adopt the public Sonar project identifier correction --- .gitleaks.toml | 10 ++++++++ quick-template.lock.json | 2 +- tests/test_tooling.py | 53 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/.gitleaks.toml b/.gitleaks.toml index 5474857..e6c5ec4 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -1,2 +1,12 @@ [extend] useDefault = true + +[[rules]] +id = "generic-api-key" + +[[rules.allowlists]] +description = "Recognize QuickTiler's historical public Sonar project identifier" +condition = "AND" +regexTarget = "match" +paths = ['''^sonar-project\.properties$'''] +regexes = ['''^sonar\.projectKey=napalm255_tiler$'''] diff --git a/quick-template.lock.json b/quick-template.lock.json index 09995d0..20a37f0 100644 --- a/quick-template.lock.json +++ b/quick-template.lock.json @@ -1,5 +1,5 @@ { "$schema": "quick-template.schema.json", "repository": "Ghost-Assembly/quick-template", - "revision": "3a46437ad001ece2fdbfcba1b615363331df45fb" + "revision": "f05201194d77ce2d4669630d1ae4643b610bac17" } diff --git a/tests/test_tooling.py b/tests/test_tooling.py index 0fb8578..8f30881 100644 --- a/tests/test_tooling.py +++ b/tests/test_tooling.py @@ -1,5 +1,6 @@ """Behavior regressions for canonical tooling and publication boundaries.""" +import hashlib import importlib.util import io import json @@ -32,6 +33,58 @@ def load(name: str): security = load("security_source") +class GitleaksTests(unittest.TestCase): + def test_public_project_identity_cannot_exempt_credentials_or_other_contexts(self) -> None: + public_id = "napalm255_tiler" + fixture = hashlib.sha256(b"nonfunctional scanner regression fixture").hexdigest()[:40] + vendor_fixture = "squ_" + fixture + cases = [ + ("public identity", "sonar-project.properties", "sonar.projectKey", public_id, 0), + ("other identity", "sonar-project.properties", "sonar.projectKey", fixture, 1), + ("credential", "sonar-project.properties", "sonar.token", fixture, 1), + ("public value as credential", "sonar-project.properties", "token", public_id, 1), + ("other file", "other.properties", "sonar.projectKey", public_id, 1), + ( + "vendor credential", + "sonar-project.properties", + "sonar.projectKey", + vendor_fixture, + 1, + ), + ( + "appended credential", + "sonar-project.properties", + "sonar.projectKey", + public_id + " token=" + fixture, + 1, + ), + ] + for name, path, property_name, value, expected in cases: + with self.subTest(case=name), tempfile.TemporaryDirectory() as work: + root = Path(work) + (root / path).write_text(f"{property_name}={value}\n") + + result = subprocess.run( + [ + "/usr/bin/env", + "gitleaks", + "dir", + "--redact", + "--no-banner", + "--no-color", + ".", + ], + cwd=root, + env={**os.environ, "GITLEAKS_CONFIG": str(ROOT / ".gitleaks.toml")}, + capture_output=True, + text=True, + check=False, + timeout=30, + ) + + self.assertEqual(result.returncode, expected, result.stderr) + + class SecuritySourceTests(unittest.TestCase): def test_scan_includes_untracked_source_but_not_private_ignored_files(self) -> None: with tempfile.TemporaryDirectory() as work: From 192ea2cf1cca98a812e06d8c4f6d685616cba4b4 Mon Sep 17 00:00:00 2001 From: napalm255 Date: Sat, 3 Oct 2026 18:18:34 -0400 Subject: [PATCH 6/7] fix: align Sonar checks with the Free plan Analyze PR changes and the full main branch using exact revisions. Report Python tooling coverage without excluding first-party files. --- .github/workflows/sonar.yml | 21 ++++++++------- AGENTS.md | 2 +- README.md | 2 +- docs/index.html | 3 ++- justfile | 4 ++- mise.toml | 2 ++ pyproject.toml | 6 +++++ quick-template.lock.json | 2 +- scripts/docs.py | 1 + scripts/sonar_gate.py | 44 ++++++++++++++++++++++--------- sonar-project.properties | 1 + tests/test_tooling.py | 52 +++++++++++++++++++++++++++++++++++++ 12 files changed, 114 insertions(+), 26 deletions(-) diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 6cb541c..1df5a12 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -31,19 +31,22 @@ jobs: run: test -n "$SONAR_TOKEN" || { echo '::error::SONAR_TOKEN is required; scan cannot be skipped'; exit 1; } - run: npm ci --ignore-scripts - run: just coverage - - name: Analyze the full checked branch + - name: Analyze the checked pull request or main branch env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} REVIEW: ${{ github.event.pull_request.number }} BRANCH: ${{ github.ref_name }} + SOURCE_BRANCH: ${{ github.head_ref }} + TARGET_BRANCH: ${{ github.base_ref }} run: | - branch="$BRANCH" - if [ -n "$REVIEW" ]; then - branch="branch-review-$REVIEW" - elif [ "$branch" != main ]; then - branch="branch-$branch" - fi revision="$(git rev-parse HEAD)" project="Ghost-Assembly_$(jq -r .name package.json)" - sonar-scanner -Dsonar.projectKey="$project" -Dsonar.branch.name="$branch" -Dsonar.scm.revision="$revision" -Dsonar.qualitygate.wait=true - python3 scripts/sonar_gate.py --project "$project" --branch "$branch" --revision "$revision" + if [ -n "$REVIEW" ]; then + test "$TARGET_BRANCH" = main || { echo '::error::Free-plan PR analysis must target main'; exit 1; } + sonar-scanner -Dsonar.projectKey="$project" -Dsonar.pullrequest.key="$REVIEW" -Dsonar.pullrequest.branch="$SOURCE_BRANCH" -Dsonar.pullrequest.base="$TARGET_BRANCH" -Dsonar.scm.revision="$revision" -Dsonar.qualitygate.wait=true + python3 scripts/sonar_gate.py --project "$project" --pull-request "$REVIEW" --revision "$revision" + else + test "$BRANCH" = main || { echo '::error::Dispatch main for overall-code analysis on the Free plan'; exit 1; } + sonar-scanner -Dsonar.projectKey="$project" -Dsonar.branch.name=main -Dsonar.scm.revision="$revision" -Dsonar.qualitygate.wait=true + python3 scripts/sonar_gate.py --project "$project" --branch main --revision "$revision" + fi diff --git a/AGENTS.md b/AGENTS.md index 2972a3c..d440538 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,7 +29,7 @@ Run `just ci` before claiming a change works. | `just template-check` | Compare managed files with the immutable GitHub revision in `quick-template.lock.json` | | `just template-sync SHA` | Synchronize a reviewed canonical revision; then install dependencies and regenerate docs | | `just test` | Run Vitest, Python tooling tests, and project offline integration tests | -| `just coverage` | Measure all runtime JavaScript, including untested files | +| `just coverage` | Measure runtime JavaScript and Python tooling, including untested files | | `just test-docs` | Check docs in Chromium and Firefox, including axe accessibility audits | | `just security` | Run OSV, source and history secret scans, Trivy, actionlint, and Zizmor | | `just build` | Build a deterministic runtime-only ZIP with Python's standard library | diff --git a/README.md b/README.md index aaaad1a..0028e2b 100644 --- a/README.md +++ b/README.md @@ -197,7 +197,7 @@ just template-check # verify the pinned canonical template just template-status # report a newer approved template revision ``` -GitHub requires local verification, security analysis, and completed Sonar analysis. The shared Sonar policy requires zero security, reliability, and maintainability issues and zero duplicated lines. Missing configuration fails instead of silently skipping analysis. Pages publishes the tested docs only after the required checks pass on main. +GitHub requires local verification, security analysis, and completed Sonar analysis. The shared Sonar policy requires zero security, reliability, and maintainability issues and zero duplicated lines. PR checks cover changed code; main checks cover the entire project. Missing configuration fails instead of silently skipping analysis. Pages publishes the tested docs only after the required checks pass on main. Common tooling and these instructions are generated from a pinned canonical template. Change that source and synchronize its approved revision; do not edit generated sections or locally bless drift. Extension-specific behavior belongs in project configuration and project.just. diff --git a/docs/index.html b/docs/index.html index a7eca88..ca9dc87 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1387,7 +1387,8 @@

Development

GitHub requires local verification, security analysis, and completed Sonar analysis. The shared Sonar policy requires zero security, reliability, and maintainability issues and zero - duplicated lines. Missing configuration fails instead of + duplicated lines. PR checks cover changed code; main checks + cover the entire project. Missing configuration fails instead of silently skipping analysis. Pages publishes the tested docs only after the required checks pass on main.

diff --git a/justfile b/justfile index 2ed9e2c..8b0b20e 100644 --- a/justfile +++ b/justfile @@ -58,9 +58,11 @@ test *args: python3 -m unittest discover -s tests -p 'test_*.py' -v just test-extra -# Measure all JavaScript runtime source +# Measure all JavaScript runtime source and Python tooling coverage: ./node_modules/.bin/vitest run --coverage + coverage run -m unittest discover -s tests -p 'test_*.py' -v + coverage xml -o coverage/python.xml # Test static documentation in Chromium and Firefox test-docs *args: diff --git a/mise.toml b/mise.toml index e445bd2..06797f4 100644 --- a/mise.toml +++ b/mise.toml @@ -2,6 +2,8 @@ [tools] node = "24.21.0" python = "3.14.7" +uv = "0.12.9" +"pipx:coverage" = "7.16.2" just = "1.58.0" ruff = "0.16.9" actionlint = "1.7.12" diff --git a/pyproject.toml b/pyproject.toml index bae54d0..396c824 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,3 +4,9 @@ line-length = 100 [tool.ruff.lint] select = ["E", "F", "I", "UP", "B", "S", "SIM", "RUF"] + +[tool.coverage.run] +branch = true +relative_files = true +source = ["scripts"] +data_file = "coverage/.coverage" diff --git a/quick-template.lock.json b/quick-template.lock.json index 20a37f0..e097ffc 100644 --- a/quick-template.lock.json +++ b/quick-template.lock.json @@ -1,5 +1,5 @@ { "$schema": "quick-template.schema.json", "repository": "Ghost-Assembly/quick-template", - "revision": "f05201194d77ce2d4669630d1ae4643b610bac17" + "revision": "8373d5d02b200bc644fa7420f0e5e3de4a0172fb" } diff --git a/scripts/docs.py b/scripts/docs.py index bd49f6c..cf066c4 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -191,6 +191,7 @@ def blocks(metadata: dict, project: dict) -> dict[str, list[tuple[str, str]]]: "GitHub requires local verification, security analysis, and completed " "Sonar analysis. The shared Sonar policy requires zero security, " "reliability, and maintainability issues and zero duplicated lines. " + "PR checks cover changed code; main checks cover the entire project. " "Missing configuration fails instead of silently skipping analysis. Pages" " publishes the tested docs only after the required checks pass on main." ), diff --git a/scripts/sonar_gate.py b/scripts/sonar_gate.py index ca39904..b0da449 100644 --- a/scripts/sonar_gate.py +++ b/scripts/sonar_gate.py @@ -49,6 +49,16 @@ def validate_branch(branches: list[dict], expected_name: str) -> None: raise ValueError("Sonar must analyze overall code on a long-lived branch") +def pull_request_revision(pull_requests: list[dict], key: str) -> str: + """Read the checked PR's analyzed revision, without logging account metadata.""" + review = next((item for item in pull_requests if item.get("key") == key), None) + if review is None: + raise ValueError("No Sonar analysis exists for the checked pull request") + if review.get("base") != "main": + raise ValueError("Sonar pull request analysis must target main") + return review.get("commit", {}).get("sha", "") + + def validate(measures: list[dict], analyzed_revision: str, expected_revision: str) -> None: """Reject stale, incomplete, or nonzero results, including rounded duplication.""" if analyzed_revision != expected_revision: @@ -67,25 +77,34 @@ def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--project", required=True) parser.add_argument("--revision", required=True) - parser.add_argument("--branch", default="main") + scope = parser.add_mutually_exclusive_group() + scope.add_argument("--branch", default="main") + scope.add_argument("--pull-request") args = parser.parse_args() - branches = request("project_branches/list", {"project": args.project})["branches"] - validate_branch(branches, args.branch) - analyses = request( - "project_analyses/search", {"project": args.project, "branch": args.branch, "ps": "1"} - )["analyses"] - if not analyses: - raise ValueError("No Sonar analysis exists for this branch") + if args.pull_request: + selection = {"pullRequest": args.pull_request} + reviews = request("project_pull_requests/list", {"project": args.project})["pullRequests"] + revision = pull_request_revision(reviews, args.pull_request) + else: + selection = {"branch": args.branch} + branches = request("project_branches/list", {"project": args.project})["branches"] + validate_branch(branches, args.branch) + analyses = request( + "project_analyses/search", {"project": args.project, **selection, "ps": "1"} + )["analyses"] + if not analyses: + raise ValueError("No Sonar analysis exists for this branch") + revision = analyses[0].get("revision", "") measures = request( "measures/component", - {"component": args.project, "branch": args.branch, "metricKeys": ",".join(METRICS)}, + {"component": args.project, **selection, "metricKeys": ",".join(METRICS)}, )["component"]["measures"] - validate(measures, analyses[0].get("revision", ""), args.revision) + validate(measures, revision, args.revision) dismissed = request( "issues/search", { "componentKeys": args.project, - "branch": args.branch, + **selection, "issueStatuses": "ACCEPTED,FALSE_POSITIVE", "ps": "1", }, @@ -93,7 +112,8 @@ def main() -> None: if dismissed["total"]: raise ValueError("Sonar findings were dismissed instead of fixed") print( - "PASS: current revision has zero security, reliability, maintainability, " + f"PASS: current {'PR changes' if args.pull_request else 'overall code'} " + "have zero security, reliability, maintainability, " "hotspots, and duplicated lines" ) diff --git a/sonar-project.properties b/sonar-project.properties index 36054eb..1d11226 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -3,5 +3,6 @@ sonar.sources=. sonar.tests=tests sonar.exclusions=tests/**,node_modules/**,coverage/**,test-results/**,playwright-report/**,.scannerwork/** sonar.javascript.lcov.reportPaths=coverage/lcov.info +sonar.python.coverage.reportPaths=coverage/python.xml sonar.qualitygate.wait=true sonar.qualitygate.timeout=600 diff --git a/tests/test_tooling.py b/tests/test_tooling.py index 8f30881..3c9e16e 100644 --- a/tests/test_tooling.py +++ b/tests/test_tooling.py @@ -6,6 +6,7 @@ import json import os import subprocess +import sys import tempfile import unittest import zipfile @@ -186,6 +187,57 @@ def measures(self) -> list[dict]: def test_current_zero_results_pass(self) -> None: self.assertIsNone(gate.validate(self.measures(), "current", "current")) + def test_pull_request_results_must_match_the_review_and_revision(self) -> None: + reviews = [{"key": "3", "base": "main", "commit": {"sha": "current"}}] + revision = gate.pull_request_revision(reviews, "3") + self.assertIsNone(gate.validate(self.measures(), revision, "current")) + with self.assertRaises(ValueError): + gate.validate(self.measures(), revision, "stale") + with self.assertRaises(ValueError): + gate.pull_request_revision(reviews, "4") + with self.assertRaises(ValueError): + gate.pull_request_revision([{"key": "3", "base": "other"}], "3") + + def test_cli_checks_the_requested_scope_and_rejects_dismissed_findings(self) -> None: + def responder(selection: dict[str, str], responses: dict): + def respond(endpoint: str, parameters: dict[str, str]) -> dict: + if endpoint in {"measures/component", "issues/search"}: + self.assertEqual( + {k: v for k, v in parameters.items() if k in {"pullRequest", "branch"}}, + selection, + ) + return responses[endpoint] + + return respond + + for review in [False, True]: + for dismissed in [0, 1]: + selection = {"pullRequest": "3"} if review else {"branch": "main"} + responses = { + "project_pull_requests/list": { + "pullRequests": [{"key": "3", "base": "main", "commit": {"sha": "current"}}] + }, + "project_branches/list": {"branches": [{"name": "main", "type": "LONG"}]}, + "project_analyses/search": {"analyses": [{"revision": "current"}]}, + "measures/component": {"component": {"measures": self.measures()}}, + "issues/search": {"total": dismissed}, + } + + argv = ["sonar_gate.py", "--project", "fixture", "--revision", "current"] + if review: + argv.extend(["--pull-request", "3"]) + with ( + self.subTest(review=review, dismissed=dismissed), + patch.object(gate, "request", side_effect=responder(selection, responses)), + patch.object(sys, "argv", argv), + patch("sys.stdout", new=io.StringIO()), + ): + if dismissed: + with self.assertRaises(ValueError): + gate.main() + else: + gate.main() + def test_short_branch_results_cannot_approve_overall_code(self) -> None: branches = [ {"name": "main", "type": "LONG"}, From 30af27063c9fc401477662812e285a06af3d45cd Mon Sep 17 00:00:00 2001 From: napalm255 Date: Sat, 3 Oct 2026 18:29:45 -0400 Subject: [PATCH 7/7] test: adopt verified shared tooling coverage Pin the merged canonical revision and cover publication boundaries. Report all runtime JavaScript and Python tooling without exclusions. Install native packaging tools for Python tests in the Sonar job. --- .github/workflows/sonar.yml | 4 + README.md | 4 +- docs/index.html | 8 +- quick-template.lock.json | 2 +- scripts/clean.py | 26 ++-- scripts/docs.py | 4 +- tests/test_tooling.py | 270 +++++++++++++++++++++++++++++++++++- 7 files changed, 299 insertions(+), 19 deletions(-) diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 1df5a12..7c0ec87 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -30,6 +30,10 @@ jobs: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} run: test -n "$SONAR_TOKEN" || { echo '::error::SONAR_TOKEN is required; scan cannot be skipped'; exit 1; } - run: npm ci --ignore-scripts + - name: Install native test tools + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends gjs gnome-shell libglib2.0-bin librsvg2-common gir1.2-soup-3.0 gir1.2-secret-1 dbus-daemon - run: just coverage - name: Analyze the checked pull request or main branch env: diff --git a/README.md b/README.md index 0028e2b..f38f8a9 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ From a clone, just uninstall performs the same steps. Disabling with just disabl -just test runs the JavaScript suite with Vitest, the shared tooling tests, and any project-specific offline suites. just coverage reports the JavaScript coverage universe, including untested runtime files. Test stubs and generated reports are not runtime source. +just test runs the JavaScript suite with Vitest, the shared tooling tests, and any project-specific offline suites. just coverage reports runtime JavaScript and Python tooling coverage, including untested files. Test stubs and generated reports are not runtime source. just test-docs runs Playwright and axe in Chromium and Firefox: dark and light accessibility checks, keyboard navigation, mobile layout, reduced motion, links, metadata, local assets, and no page JavaScript. Automated accessibility checks still require human review of reading and focus order. @@ -185,7 +185,7 @@ just setup # install pinned tools, dependencies, and browsers just fmt # format source and configuration just lint # verify template, generated docs, source, and schemas just test # JavaScript, Python, and project offline tests -just coverage # report JavaScript coverage without source exclusions +just coverage # report JavaScript and Python coverage without source exclusions just test-docs # Chromium and Firefox documentation checks just security # dependencies, secrets, and workflow checks just build # build the runtime-only extension ZIP diff --git a/docs/index.html b/docs/index.html index ca9dc87..c84e20e 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1267,9 +1267,9 @@

Testing

just test runs the JavaScript suite with Vitest, the shared tooling tests, and any project-specific offline suites. just - coverage reports the JavaScript coverage universe, including - untested runtime files. Test stubs and generated reports are not - runtime source. + coverage reports runtime JavaScript and Python tooling coverage, + including untested files. Test stubs and generated reports are + not runtime source.

just test-docs runs Playwright and axe in Chromium and Firefox: @@ -1372,7 +1372,7 @@

Development

just fmt # format source and configuration just lint # verify template, generated docs, source, and schemas just test # JavaScript, Python, and project offline tests -just coverage # report JavaScript coverage without source exclusions +just coverage # report JavaScript and Python coverage without source exclusions just test-docs # Chromium and Firefox documentation checks just security # dependencies, secrets, and workflow checks just build # build the runtime-only extension ZIP diff --git a/quick-template.lock.json b/quick-template.lock.json index e097ffc..ab690bc 100644 --- a/quick-template.lock.json +++ b/quick-template.lock.json @@ -1,5 +1,5 @@ { "$schema": "quick-template.schema.json", "repository": "Ghost-Assembly/quick-template", - "revision": "8373d5d02b200bc644fa7420f0e5e3de4a0172fb" + "revision": "40e31d2739e0c94162c707adb6851cc207ce6d2c" } diff --git a/scripts/clean.py b/scripts/clean.py index 71f9e91..1b329b6 100644 --- a/scripts/clean.py +++ b/scripts/clean.py @@ -5,12 +5,20 @@ from pathlib import Path ROOT = Path(__file__).resolve().parent.parent -for name in ["coverage", "test-results", "playwright-report", ".scannerwork"]: - target = ROOT / name - if target.is_symlink(): - target.unlink() - elif target.is_dir(): - shutil.rmtree(target) -for artifact in ROOT.glob("*.shell-extension.zip"): - artifact.unlink() -(ROOT / "schemas/gschemas.compiled").unlink(missing_ok=True) + + +def main(root: Path = ROOT) -> None: + """Keep source and user data while removing the checkout's generated output.""" + for name in ["coverage", "test-results", "playwright-report", ".scannerwork"]: + target = root / name + if target.is_symlink(): + target.unlink() + elif target.is_dir(): + shutil.rmtree(target) + for artifact in root.glob("*.shell-extension.zip"): + artifact.unlink() + (root / "schemas/gschemas.compiled").unlink(missing_ok=True) + + +if __name__ == "__main__": + main() diff --git a/scripts/docs.py b/scripts/docs.py index cf066c4..c014e1b 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -14,7 +14,7 @@ just fmt # format source and configuration just lint # verify template, generated docs, source, and schemas just test # JavaScript, Python, and project offline tests -just coverage # report JavaScript coverage without source exclusions +just coverage # report JavaScript and Python coverage without source exclusions just test-docs # Chromium and Firefox documentation checks just security # dependencies, secrets, and workflow checks just build # build the runtime-only extension ZIP @@ -107,7 +107,7 @@ def blocks(metadata: dict, project: dict) -> dict[str, list[tuple[str, str]]]: ( "just test runs the JavaScript suite with Vitest, the shared tooling " "tests, and any project-specific offline suites. just coverage reports " - "the JavaScript coverage universe, including untested runtime files. Test" + "runtime JavaScript and Python tooling coverage, including untested files. Test" " stubs and generated reports are not runtime source." ), ), diff --git a/tests/test_tooling.py b/tests/test_tooling.py index 3c9e16e..56bea78 100644 --- a/tests/test_tooling.py +++ b/tests/test_tooling.py @@ -11,7 +11,7 @@ import unittest import zipfile from pathlib import Path -from unittest.mock import patch +from unittest.mock import Mock, patch ROOT = Path(__file__).resolve().parents[1] @@ -32,6 +32,7 @@ def load(name: str): docs = load("docs") workflow = load("workflow_lint") security = load("security_source") +clean = load("clean") class GitleaksTests(unittest.TestCase): @@ -120,6 +121,28 @@ def test_source_symlink_cannot_read_outside_the_repository(self) -> None: class WorkflowTests(unittest.TestCase): + def test_cli_preserves_real_actionlint_failures(self) -> None: + with tempfile.TemporaryDirectory() as work: + root = Path(work) + directory = root / ".github/workflows" + directory.mkdir(parents=True) + with ( + patch.object(sys, "argv", ["workflow_lint.py", work]), + patch("sys.stdout", new=io.StringIO()), + ): + with self.assertRaisesRegex(ValueError, "No workflows found"): + workflow.main() + path = directory / "ci.yml" + path.write_text( + "name: Fixture\non: workflow_dispatch\njobs:\n" + " check:\n runs-on: ubuntu-24.04\n steps:\n" + " - run: echo fixture\n" + ) + workflow.main() + path.write_text(path.read_text().replace("runs-on: ubuntu-24.04", "invalid: true")) + with self.assertRaises(subprocess.CalledProcessError): + workflow.main() + def test_only_self_repository_uses_are_adapted(self) -> None: source = " uses: $/.github/workflows/ci.yml\n run: echo '$/unchanged'\n" self.assertEqual( @@ -133,6 +156,104 @@ def test_invalid_workflow_content_is_preserved_for_the_linter(self) -> None: class TemplateTests(unittest.TestCase): + def test_sync_check_and_release_status_preserve_project_identity(self) -> None: + sha = "a" * 40 + files = { + "justfile": b"canonical commands", + "scripts/template.py": b"canonical helper", + "package.json": b'{"name":"template","version":"1.0.0"}', + "package-lock.json": b'{"name":"template","version":"1.0.0","packages":{"":{}}}', + } + with tempfile.TemporaryDirectory() as work: + root = Path(work) + identity = {"name": "quickfixture", "version": "2.0.0"} + (root / "package.json").write_text(json.dumps(identity)) + (root / "quick-template.lock.json").write_text( + json.dumps({"repository": template.REPOSITORY, "revision": sha}) + ) + with ( + patch.object(template, "ROOT", root), + patch.object(template, "source_files", return_value=files), + patch("sys.stdout", new=io.StringIO()), + ): + with patch.object(sys, "argv", ["template.py", "sync", sha]): + template.main() + self.assertEqual(json.loads((root / "package.json").read_text()), identity) + lock = json.loads((root / "package-lock.json").read_text()) + self.assertEqual(lock["packages"][""], identity) + self.assertEqual(template.compare(root, template.expected_files(root, files)), []) + with patch.object(sys, "argv", ["template.py", "check"]): + template.main() + (root / "justfile").write_text("local drift") + with self.assertRaisesRegex(SystemExit, "Template drift"): + template.main() + with ( + patch.object(sys, "argv", ["template.py", "status"]), + patch.object( + template, + "download", + return_value=json.dumps({"target_commitish": sha}).encode(), + ), + ): + template.main() + with ( + patch.object(sys, "argv", ["template.py", "status"]), + patch.object( + template, + "download", + return_value=json.dumps({"target_commitish": "b" * 40}).encode(), + ), + self.assertRaisesRegex(SystemExit, "Template update available"), + ): + template.main() + + def test_sync_cannot_write_through_a_symlink(self) -> None: + with tempfile.TemporaryDirectory() as work: + root = Path(work) / "repo" + root.mkdir() + (root / "package.json").write_text('{"name":"quickfixture","version":"1.0.0"}') + outside = Path(work) / "outside" + outside.write_text("private fixture") + (root / "alias").symlink_to(outside) + files = { + "alias": b"replacement", + "package.json": b'{"name":"template","version":"1.0.0"}', + "package-lock.json": b'{"name":"template","version":"1.0.0","packages":{"":{}}}', + } + with self.assertRaises(ValueError): + template.synchronize(root, "a" * 40, files) + self.assertEqual(outside.read_text(), "private fixture") + + def test_local_source_override_is_rejected_in_ci(self) -> None: + with tempfile.TemporaryDirectory() as work: + payload = Path(work) / "template" + payload.mkdir() + (payload / "justfile").write_text("local commands") + (payload / "__pycache__").mkdir() + (payload / "__pycache__/cache.pyc").write_bytes(b"generated") + with patch.dict(os.environ, {"QUICK_TEMPLATE_SOURCE": work, "CI": ""}): + self.assertEqual(template.source_files("a" * 40), {"justfile": b"local commands"}) + with patch.dict(os.environ, {"CI": "true"}), self.assertRaises(ValueError): + template.source_files("a" * 40) + + def test_archive_rejects_paths_that_escape_the_payload_and_incomplete_content(self) -> None: + sha = "a" * 40 + for names in [["../outside"], ["justfile"]]: + data = io.BytesIO() + with zipfile.ZipFile(data, "w") as archive: + for name in names: + archive.writestr(f"quick-template-{sha}/template/{name}", b"fixture") + with ( + self.subTest(names=names), + tempfile.TemporaryDirectory() as work, + patch.dict( + os.environ, {"XDG_CACHE_HOME": work, "QUICK_TEMPLATE_SOURCE": "", "CI": "true"} + ), + patch.object(template, "download", return_value=data.getvalue()), + self.assertRaises(ValueError), + ): + template.source_files(sha) + def test_corrupt_archive_cache_is_recovered_and_reused(self) -> None: sha = "a" * 40 buffer = io.BytesIO() @@ -265,7 +386,91 @@ def test_missing_metrics_and_stale_analysis_fail(self) -> None: gate.validate(self.measures(), "previous", "current") +class TransportTests(unittest.TestCase): + def test_https_clients_fail_closed_and_close_connections(self) -> None: + def fetch(client): + if client is gate: + return gate.request("fixture", {"project": "fixture with space"}) + return template.download("codeload.github.com", "/fixture") + + credential = hashlib.sha256(b"nonfunctional HTTP credential fixture").hexdigest() + for module, limit in [(gate, 4 * 1024 * 1024), (template, 8 * 1024 * 1024)]: + for status, data, error in [ + (200, b'{"fixture":true}', None), + (403, b"denied", RuntimeError), + (200, b"x" * (limit + 1), ValueError), + ]: + connection = Mock() + response = connection.getresponse.return_value + response.status = status + response.read.return_value = data + with ( + self.subTest( + client=module.__name__, status=status, oversized=len(data) > limit + ), + patch.dict(os.environ, {"SONAR_TOKEN": credential}), + patch.object(module.http.client, "HTTPSConnection", return_value=connection), + ): + if error: + with self.assertRaises(error): + fetch(module) + elif module is gate: + self.assertEqual(fetch(module), {"fixture": True}) + path = connection.request.call_args.args[1] + self.assertIn("project=fixture+with+space", path) + self.assertNotIn(credential, path) + else: + self.assertEqual(fetch(module), data) + connection.close.assert_called_once() + with patch.dict(os.environ, {"SONAR_TOKEN": ""}), self.assertRaises(ValueError): + gate.request("fixture", {}) + with self.assertRaises(ValueError): + template.download("untrusted.example.test", "/fixture") + + class PackagingTests(unittest.TestCase): + def test_packages_are_deterministic_and_detect_content_or_inventory_drift(self) -> None: + with tempfile.TemporaryDirectory() as work: + root = Path(work) + files = { + "metadata.json": json.dumps({"uuid": "fixture@example.test"}).encode(), + "extension.js": b"runtime extension", + "prefs.js": b"runtime preferences", + "LICENSE": b"fixture license", + "modules/model.js": b"runtime model", + } + for name, content in files.items(): + path = root / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(content) + (root / ".env").write_text("private fixture") + (root / "quick-project.json").write_text(json.dumps({"runtimeFiles": list(files)})) + + def pack(command: list[str], **kwargs) -> None: + if command[0] == "/usr/bin/gnome-extensions": + destination = kwargs["cwd"] / "packed" / bundle.artifact_name(root) + with zipfile.ZipFile(destination, "w") as archive: + for name, content in files.items(): + archive.writestr(name, content) + + with patch.object(bundle.subprocess, "run", side_effect=pack): + artifact = bundle.build(root) + first = artifact.read_bytes() + self.assertEqual(bundle.build(root).read_bytes(), first) + with zipfile.ZipFile(artifact) as archive: + self.assertEqual(set(archive.namelist()), set(files)) + self.assertEqual(archive.read("modules/model.js"), files["modules/model.js"]) + with patch("sys.stdout", new=io.StringIO()): + bundle.check(root) + for name in ["modules/model.js", "unexpected.js"]: + with zipfile.ZipFile(artifact, "w") as archive: + for path, content in files.items(): + archive.writestr(path, b"changed" if path == name else content) + if name not in files: + archive.writestr(name, b"unexpected") + with self.subTest(name=name), self.assertRaises(ValueError): + bundle.check(root) + def test_unsafe_runtime_paths_are_rejected_before_packaging(self) -> None: with tempfile.TemporaryDirectory() as work: root = Path(work) @@ -283,6 +488,44 @@ def test_invalid_uuid_cannot_choose_an_output_path(self) -> None: class DocumentationTests(unittest.TestCase): + def test_generation_preserves_project_content_and_detects_stale_docs(self) -> None: + with tempfile.TemporaryDirectory() as work: + root = Path(work) + (root / "docs").mkdir() + modules = ROOT / "node_modules" + if not modules.exists(): + modules = ROOT.parent / "node_modules" + (root / "node_modules").symlink_to(modules, target_is_directory=True) + metadata = {"uuid": "fixture@example.test", "shell-version": ["49", "50"]} + project = {"repository": "quickspot", "requirements": "Fixture ."} + (root / "metadata.json").write_text(json.dumps(metadata)) + (root / "docs/project.json").write_text(json.dumps(project)) + regions = "\n".join( + f"" + for name in [*docs.SECTIONS, "badges"] + ) + (root / "README.md").write_text("# Project prose\n" + regions) + (root / "docs/index.html").write_text( + "
Project prose\n" + regions + "
" + ) + with patch.object(docs, "ROOT", root), patch("sys.stdout", new=io.StringIO()): + with patch.object(sys, "argv", ["docs.py"]): + docs.main() + readme = (root / "README.md").read_text() + page = (root / "docs/index.html").read_text() + self.assertIn("# Project prose", readme) + self.assertIn("Fixture <dependency>.", page) + self.assertIn("fixture@example.test", page) + self.assertIn("systemctl --user disable --now quickspot-soloist.service", readme) + self.assertIn("Security issues", readme) + with patch.object(sys, "argv", ["docs.py", "--check"]): + docs.main() + (root / "README.md").write_text( + readme.replace("just test-docs", "stale command") + ) + with self.assertRaisesRegex(SystemExit, "Stale generated docs"): + docs.main() + def test_install_restart_precedes_enable(self) -> None: sections = docs.blocks( {"uuid": "fixture@example.test", "shell-version": ["50"]}, @@ -308,5 +551,30 @@ def test_missing_or_duplicate_generation_markers_fail(self) -> None: docs.replace_block(region * 2, "install", "generated") +class CleanupTests(unittest.TestCase): + def test_cleanup_preserves_source_and_external_symlink_targets(self) -> None: + with tempfile.TemporaryDirectory() as work: + root = Path(work) / "repo" + root.mkdir() + outside = Path(work) / "outside" + outside.mkdir() + (outside / "private.txt").write_text("private fixture") + (root / "coverage").symlink_to(outside, target_is_directory=True) + (root / "test-results").mkdir() + (root / "test-results/output.json").write_text("generated") + (root / "schemas").mkdir() + (root / "schemas/gschemas.compiled").write_bytes(b"generated") + (root / "fixture.shell-extension.zip").write_bytes(b"generated") + (root / "source.js").write_text("source") + clean.main(root) + clean.main(root) + self.assertEqual((outside / "private.txt").read_text(), "private fixture") + self.assertEqual((root / "source.js").read_text(), "source") + self.assertFalse((root / "coverage").is_symlink()) + self.assertFalse((root / "test-results").exists()) + self.assertFalse((root / "schemas/gschemas.compiled").exists()) + self.assertEqual(list(root.glob("*.shell-extension.zip")), []) + + if __name__ == "__main__": unittest.main()