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..080f610 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 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
+ - 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..27e8b3e 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,84 +1,75 @@
name: Release
-
on:
- push:
- tags: ['v*']
-
+ push:
+ tags: ['v*']
permissions:
- contents: read
-
+ contents: read
+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
+ permissions:
+ contents: read
+ actions: read # Inspect workflow runs and download their tested artifacts.
+ 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..7c0ec87 100644
--- a/.github/workflows/sonar.yml
+++ b/.github/workflows/sonar.yml
@@ -1,53 +1,56 @@
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
+ - 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:
+ 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: |
+ revision="$(git rev-parse HEAD)"
+ project="Ghost-Assembly_$(jq -r .name package.json)"
+ 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/.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..e6c5ec4 100644
--- a/.gitleaks.toml
+++ b/.gitleaks.toml
@@ -1,16 +1,12 @@
-# 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*=.*''']
+[[rules]]
+id = "generic-api-key"
-[[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$''']
+[[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/.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..d440538 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 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 |
+| `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..f38f8a9 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,21 @@
# QuickTiler
+
+
+[](https://github.com/Ghost-Assembly/quicktiler/actions/workflows/ci.yml)
+[](https://github.com/Ghost-Assembly/quicktiler/actions/workflows/security.yml)
+[](https://ghost-assembly.com/quicktiler/)
+[](https://github.com/Ghost-Assembly/quicktiler/releases/latest)
+[](https://github.com/Ghost-Assembly/quicktiler/blob/main/LICENSE)
+[](https://ghost-assembly.com/quicktiler/#install)
+[](https://sonarcloud.io/dashboard?id=Ghost-Assembly_quicktiler)
+[](https://sonarcloud.io/dashboard?id=Ghost-Assembly_quicktiler)
+[](https://sonarcloud.io/dashboard?id=Ghost-Assembly_quicktiler)
+[](https://sonarcloud.io/dashboard?id=Ghost-Assembly_quicktiler)
+[](https://sonarcloud.io/dashboard?id=Ghost-Assembly_quicktiler)
+[](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 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.
+
+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. 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.
+
+
+## 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 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
+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. 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.
+
## License
diff --git a/docs/index.html b/docs/index.html
index 2cb72f2..c84e20e 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -373,7 +373,16 @@
QuickTiler
-
02
Install
- Requires
-
- GNOME Shell 49 or 50.
-
- Nothing else. 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.
+
-
-
$ 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:
+
-
-
$ 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,146 @@
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 runtime JavaScript and Python tooling coverage,
+ including untested files. Test stubs and generated reports are
+ not runtime source.
-
-
- A maximized window reports the work area as its frame,
- and cannot be resized.
-
-
- On request, frames that change only when the client
- commits, as on Wayland, and a minimum size Mutter
- enlarges a window to.
-
-
- Signal handlers tracked per emitter, as gnome-shell
- tracks them, and a Quick Settings menu the Shell never
- destroys — so a leak shows up as a failing test.
-
-
- 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. 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.
-
-
-
$ 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.
+
-
-
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 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
+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. 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/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..8b0b20e 100644
--- a/justfile
+++ b/justfile
@@ -1,138 +1,135 @@
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 and Python tooling
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
+ coverage run -m unittest discover -s tests -p 'test_*.py' -v
+ coverage xml -o coverage/python.xml
-# 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..06797f4 100644
--- a/mise.toml
+++ b/mise.toml
@@ -1,20 +1,19 @@
-# 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"
+uv = "0.12.9"
+"pipx:coverage" = "7.16.2"
+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..396c824
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,12 @@
+[tool.ruff]
+target-version = "py312"
+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-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..ab690bc
--- /dev/null
+++ b/quick-template.lock.json
@@ -0,0 +1,5 @@
+{
+ "$schema": "quick-template.schema.json",
+ "repository": "Ghost-Assembly/quick-template",
+ "revision": "40e31d2739e0c94162c707adb6851cc207ce6d2c"
+}
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..1b329b6
--- /dev/null
+++ b/scripts/clean.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python3
+"""Remove only known generated files from this checkout."""
+
+import shutil
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parent.parent
+
+
+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
new file mode 100644
index 0000000..c014e1b
--- /dev/null
+++ b/scripts/docs.py
@@ -0,0 +1,356 @@
+#!/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 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
+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 "
+ "runtime JavaScript and Python tooling coverage, including untested 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. 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."
+ ),
+ ),
+ (
+ "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. "
+ "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."
+ ),
+ ),
+ (
+ "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(
+ '"
+ )
+ else:
+ parts.append(f"<{kind}>{escaped}{kind}>")
+ 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"[]({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"[]({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"[]({sonar})")
+ items.append(
+ f"[]({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/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/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..b0da449
--- /dev/null
+++ b/scripts/sonar_gate.py
@@ -0,0 +1,122 @@
+#!/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_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 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:
+ 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)
+ scope = parser.add_mutually_exclusive_group()
+ scope.add_argument("--branch", default="main")
+ scope.add_argument("--pull-request")
+ args = parser.parse_args()
+ 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, **selection, "metricKeys": ",".join(METRICS)},
+ )["component"]["measures"]
+ validate(measures, revision, args.revision)
+ dismissed = request(
+ "issues/search",
+ {
+ "componentKeys": args.project,
+ **selection,
+ "issueStatuses": "ACCEPTED,FALSE_POSITIVE",
+ "ps": "1",
+ },
+ )
+ if dismissed["total"]:
+ raise ValueError("Sonar findings were dismissed instead of fixed")
+ print(
+ f"PASS: current {'PR changes' if args.pull_request else 'overall code'} "
+ "have 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..1d11226 100644
--- a/sonar-project.properties
+++ b/sonar-project.properties
@@ -1,20 +1,8 @@
-# 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.python.coverage.reportPaths=coverage/python.xml
+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..56bea78
--- /dev/null
+++ b/tests/test_tooling.py
@@ -0,0 +1,580 @@
+"""Behavior regressions for canonical tooling and publication boundaries."""
+
+import hashlib
+import importlib.util
+import io
+import json
+import os
+import subprocess
+import sys
+import tempfile
+import unittest
+import zipfile
+from pathlib import Path
+from unittest.mock import Mock, 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")
+clean = load("clean")
+
+
+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:
+ 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_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(
+ 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_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()
+ 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_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"},
+ {"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()
+ 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 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)
+ 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_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"]},
+ {"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")
+
+
+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()
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') },