From 55b9969b82776bb1412e29fc5cdc7535bfd69533 Mon Sep 17 00:00:00 2001 From: Eric Fennis Date: Fri, 9 Oct 2026 11:30:00 +0200 Subject: [PATCH] ci(lint): split lint jobs and annotate Prettier and JSON schema errors in PRs (#4982) * ci: split lint-code into ESLint, Prettier and JSON Schema jobs with PR annotations - ESLint: annotations via setup-node's built-in eslint-stylish problem matcher. - Prettier: new scripts/lintFormat.mts formats unformatted files in memory, diffs them and emits line-level `::error` annotations with the expected code. - JSON Schema: replace ajv-cli with a local ESLint rule (Ajv draft 2020-12) that validates icons/categories JSON against their `$schema` and reports at the offending property. eslint-plugin-json-schema-validator was not used because it runs Ajv in draft-07 mode and ignores `dependentRequired`. All annotations use workflow commands, so they work on fork PRs with a read-only token. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FVxEtqNswEDq1N23taZYh8 * Improve root value check and property error handling Enhance error reporting for JSON schema validation. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * style(lint): format jsonSchemaPlugin with Prettier Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FVxEtqNswEDq1N23taZYh8 * test commit * fix(lint): annotate syntax errors from Prettier and ESLint in PRs - ESLint/JSON Schema jobs use a small GitHub formatter that prints `::error` workflow commands. setup-node's eslint-stylish matcher skipped parsing errors because they have no rule id. - lintFormat.mts annotates Prettier syntax errors (exit code 2) at their line and column instead of only printing stderr, and still checks the other files. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FVxEtqNswEDq1N23taZYh8 * ci(lint): spell check icon and category metadata with cspell annotations - New "Spelling" job runs scripts/lintSpelling.mts, which turns cspell findings into `::error` annotations (with cspell's suggested fix when it has one). - cspell accepts both US and British English, ignores contributor usernames and deprecated `alias.typo` aliases. - Add existing search tags and domain terms to .cspell/custom-words.txt so the current metadata passes. - Add `lint:spelling` to `pnpm lint` and lint-staged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FVxEtqNswEDq1N23taZYh8 * fix(lint): pin JSON schemas per file set and drop typo tags - json-schema/no-invalid now takes the schema from config (icon.schema.json for icons, category.schema.json for categories) and reports a `$schema` that points elsewhere, so metadata can't opt out of validation. - Remove the misspelled `frigerator` and `sterisk` tags and their allowlist entries. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FVxEtqNswEDq1N23taZYh8 * fix(icons): split joined-word tags into separate words Tags like `groupchat` and `timemachine` become `group chat` and `time machine` so they read naturally and pass the spell check without allowlisting. Where the spaced tag already existed (`smart home`) the joined one is dropped. cspell now skips all alias names, since they are icon names that can't change (e.g. the deprecated `palmtree` alias). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FVxEtqNswEDq1N23taZYh8 * Revert * Try another error * Revert error --------- Co-authored-by: Claude Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .cspell/custom-words.txt | 39 ++++ .github/workflows/lint-code.yml | 59 ++++- cspell.json | 5 + eslint.config.js | 23 ++ icons/asterisk.json | 1 - icons/bangladeshi-taka.json | 2 +- icons/building-complex-plus.json | 2 +- icons/car-battery.json | 2 +- icons/cloud-backup.json | 2 +- icons/database-backup.json | 2 +- icons/lighthouse.json | 2 +- icons/mail-clock.json | 2 +- icons/messages-circle.json | 2 +- icons/paper-bag.json | 2 +- icons/refrigerator.json | 1 - icons/robot-vacuum.json | 1 - icons/scan-qr-code.json | 2 +- icons/shell.json | 2 +- icons/shield-cog-corner.json | 2 +- icons/spotlight.json | 2 +- icons/toothbrush-sparkles.json | 2 +- icons/toothbrush.json | 2 +- icons/tv-minimal-play.json | 2 +- icons/tv-minimal.json | 2 +- lint-staged.config.mjs | 12 +- package.json | 16 +- pnpm-lock.yaml | 332 ++-------------------------- scripts/eslint/githubFormatter.mjs | 61 +++++ scripts/eslint/jsonSchemaPlugin.mjs | 147 ++++++++++++ scripts/lintFormat.mts | 123 +++++++++++ scripts/lintSpelling.mts | 57 +++++ 31 files changed, 558 insertions(+), 353 deletions(-) create mode 100644 scripts/eslint/githubFormatter.mjs create mode 100644 scripts/eslint/jsonSchemaPlugin.mjs create mode 100644 scripts/lintFormat.mts create mode 100644 scripts/lintSpelling.mts diff --git a/.cspell/custom-words.txt b/.cspell/custom-words.txt index 7418171ad..ddc51fe06 100644 --- a/.cspell/custom-words.txt +++ b/.cspell/custom-words.txt @@ -11,3 +11,42 @@ ungroup toc sunlounger monstera + +# Search tags and terms used in icon and category metadata +ageusia +artboard +barbeque +choy +ecommerce +enby +flexbox +futbol +gddr +glassmorphism +gltf +interpoint +interpunct +middot +mistwarp +moleskine +nonbinary +pantone +phonendoscope +quadcopter +reapproval +rubygems +showreel +solera +timesheets +turbowarp +unbook +unguard +unkeys +unlabel +unprevented +unqueue +unsalvageable +unshield +unstar +vitamizer +wayfinding diff --git a/.github/workflows/lint-code.yml b/.github/workflows/lint-code.yml index ccbb3ad30..0cebc766c 100644 --- a/.github/workflows/lint-code.yml +++ b/.github/workflows/lint-code.yml @@ -7,7 +7,8 @@ permissions: contents: read jobs: - lint-code: + eslint: + name: ESLint runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -20,5 +21,57 @@ jobs: - name: Install Dependencies run: pnpm install --frozen-lockfile - - name: Run Linter - run: pnpm lint + # The formatter prints workflow commands, which GitHub shows as inline annotations. + - name: Run ESLint + run: pnpm lint:es --format ./scripts/eslint/githubFormatter.mjs + + prettier: + name: Prettier + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + cache: 'pnpm' + node-version-file: 'package.json' + + - name: Install Dependencies + run: pnpm install --frozen-lockfile + + - name: Check formatting + run: pnpm lint:format:ci + + json-schema: + name: JSON Schema + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + cache: 'pnpm' + node-version-file: 'package.json' + + - name: Install Dependencies + run: pnpm install --frozen-lockfile + + - name: Validate icon and category JSON + run: pnpm lint:json --format ./scripts/eslint/githubFormatter.mjs + + spelling: + name: Spelling + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + cache: 'pnpm' + node-version-file: 'package.json' + + - name: Install Dependencies + run: pnpm install --frozen-lockfile + + - name: Spell check icon and category metadata + run: pnpm lint:spelling:ci diff --git a/cspell.json b/cspell.json index b98d751c0..10cde6110 100644 --- a/cspell.json +++ b/cspell.json @@ -1,4 +1,5 @@ { + "language": "en,en-GB", "dictionaries": ["en-us", "custom-words"], "dictionaryDefinitions": [ { @@ -6,5 +7,9 @@ "path": "./.cspell/custom-words.txt", "addWords": true } + ], + "ignoreRegExpList": [ + "/\"contributors\"\\s*:\\s*\\[[^\\]]*\\]/g", + "/\"name\"\\s*:\\s*\"[^\"]*\"/g" ] } diff --git a/eslint.config.js b/eslint.config.js index f88f795d6..49aa4a5c7 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -8,6 +8,8 @@ import htmlEslint from '@html-eslint/eslint-plugin'; import htmlParser from '@html-eslint/parser'; import defaultAttrs from './tools/build-icons/render/default-attrs.json' with { type: 'json' }; import tseslint from 'typescript-eslint'; +import jsonSchema from './scripts/eslint/jsonSchemaPlugin.mjs'; +import * as jsoncParser from 'jsonc-eslint-parser'; const gitignorePath = path.join(import.meta.dirname, '.gitignore'); @@ -152,4 +154,25 @@ export default defineConfig([ '@html-eslint/quotes': 'error', }, }, + { + files: ['icons/*.json', 'categories/*.json'], + languageOptions: { + parser: jsoncParser, + }, + plugins: { + 'json-schema': jsonSchema, + }, + }, + { + files: ['icons/*.json'], + rules: { + 'json-schema/no-invalid': ['error', { schema: 'icon.schema.json' }], + }, + }, + { + files: ['categories/*.json'], + rules: { + 'json-schema/no-invalid': ['error', { schema: 'category.schema.json' }], + }, + }, ]); diff --git a/icons/asterisk.json b/icons/asterisk.json index 65a8e1b1c..8705c5696 100644 --- a/icons/asterisk.json +++ b/icons/asterisk.json @@ -8,7 +8,6 @@ "use-cases": [], "tags": [ "symbol", - "sterisk", "mark", "pointer", "pencil", diff --git a/icons/bangladeshi-taka.json b/icons/bangladeshi-taka.json index 3b19ab093..b7a3c5c22 100644 --- a/icons/bangladeshi-taka.json +++ b/icons/bangladeshi-taka.json @@ -16,7 +16,7 @@ "payment", "bdt", "৳", - "currencysymbol", + "currency symbol", "cash", "banknote", "price", diff --git a/icons/building-complex-plus.json b/icons/building-complex-plus.json index 7a24b02c5..ecb8dc16e 100644 --- a/icons/building-complex-plus.json +++ b/icons/building-complex-plus.json @@ -30,7 +30,7 @@ "startup", "registration", "onboarding", - "realestate", + "real estate", "property" ], "categories": [ diff --git a/icons/car-battery.json b/icons/car-battery.json index 9eeaef2fc..8092aa975 100644 --- a/icons/car-battery.json +++ b/icons/car-battery.json @@ -11,7 +11,7 @@ "tags": [ "battery", "automobile", - "powercell", + "power cell", "electric", "power", "electricity", diff --git a/icons/cloud-backup.json b/icons/cloud-backup.json index c1afba8b2..753551f84 100644 --- a/icons/cloud-backup.json +++ b/icons/cloud-backup.json @@ -13,7 +13,7 @@ "bytes", "servers", "backup", - "timemachine", + "time machine", "rotate", "synchronize", "synchronise", diff --git a/icons/database-backup.json b/icons/database-backup.json index 887040443..b41424600 100644 --- a/icons/database-backup.json +++ b/icons/database-backup.json @@ -13,7 +13,7 @@ "bytes", "servers", "backup", - "timemachine", + "time machine", "rotate", "arrow", "left" diff --git a/icons/lighthouse.json b/icons/lighthouse.json index aad0294c8..41cc2604c 100644 --- a/icons/lighthouse.json +++ b/icons/lighthouse.json @@ -30,7 +30,7 @@ "nautical", "harbor", "shore", - "lightbeam", + "light beam", "wayfinding", "guidance", "onboarding", diff --git a/icons/mail-clock.json b/icons/mail-clock.json index 8299dd5d4..51241f599 100644 --- a/icons/mail-clock.json +++ b/icons/mail-clock.json @@ -18,7 +18,7 @@ "unread", "scheduled", "delayed", - "sendlater", + "send later", "delivery", "reminder", "pending", diff --git a/icons/messages-circle.json b/icons/messages-circle.json index adee57e1c..8d8a060c3 100644 --- a/icons/messages-circle.json +++ b/icons/messages-circle.json @@ -26,7 +26,7 @@ "interview", "debate", "group", - "groupchat", + "group chat", "threads", "unread", "bubble", diff --git a/icons/paper-bag.json b/icons/paper-bag.json index d14dd760d..34a2f928d 100644 --- a/icons/paper-bag.json +++ b/icons/paper-bag.json @@ -20,7 +20,7 @@ "eco-friendly", "kraft", "retail", - "doggybag" + "doggy bag" ], "categories": [ "food-beverage", diff --git a/icons/refrigerator.json b/icons/refrigerator.json index 0d20079bf..faa9180a8 100644 --- a/icons/refrigerator.json +++ b/icons/refrigerator.json @@ -5,7 +5,6 @@ ], "use-cases": [], "tags": [ - "frigerator", "fridge", "freezer", "cooler", diff --git a/icons/robot-vacuum.json b/icons/robot-vacuum.json index 41c1b55d5..f119a6163 100644 --- a/icons/robot-vacuum.json +++ b/icons/robot-vacuum.json @@ -18,7 +18,6 @@ "housekeeping", "tool", "maintenance", - "smarthome", "sweeping", "hygiene", "chores", diff --git a/icons/scan-qr-code.json b/icons/scan-qr-code.json index 37556bf2e..da31d6a85 100644 --- a/icons/scan-qr-code.json +++ b/icons/scan-qr-code.json @@ -8,7 +8,7 @@ "tags": [ "barcode", "scan", - "qrcode", + "qr code", "url", "information", "digital", diff --git a/icons/shell.json b/icons/shell.json index 40b9ad130..470c3e135 100644 --- a/icons/shell.json +++ b/icons/shell.json @@ -8,7 +8,7 @@ "beach", "sand", "holiday", - "sealife", + "sea life", "fossil", "ammonite", "biology", diff --git a/icons/shield-cog-corner.json b/icons/shield-cog-corner.json index b6a93f5e7..c3ccdb938 100644 --- a/icons/shield-cog-corner.json +++ b/icons/shield-cog-corner.json @@ -37,7 +37,7 @@ "admin", "verification", "crest", - "shieldcog", + "shield cog", "bravery", "knight", "foot soldier", diff --git a/icons/spotlight.json b/icons/spotlight.json index a49522880..df8141126 100644 --- a/icons/spotlight.json +++ b/icons/spotlight.json @@ -27,7 +27,7 @@ "performance", "lens", "leaderboard", - "followspot", + "follow spot", "best", "highlight" ], diff --git a/icons/toothbrush-sparkles.json b/icons/toothbrush-sparkles.json index 51d9d78d3..196b271b6 100644 --- a/icons/toothbrush-sparkles.json +++ b/icons/toothbrush-sparkles.json @@ -24,7 +24,7 @@ "handle", "plaque", "toothpaste", - "selfcare", + "self care", "grooming", "fresh", "clean" diff --git a/icons/toothbrush.json b/icons/toothbrush.json index 7bce524b5..21c917e63 100644 --- a/icons/toothbrush.json +++ b/icons/toothbrush.json @@ -24,7 +24,7 @@ "handle", "plaque", "toothpaste", - "selfcare", + "self care", "grooming" ], "categories": [ diff --git a/icons/tv-minimal-play.json b/icons/tv-minimal-play.json index ba6cf16c0..2b3237a01 100644 --- a/icons/tv-minimal-play.json +++ b/icons/tv-minimal-play.json @@ -7,7 +7,7 @@ ], "use-cases": [], "tags": [ - "flatscreen", + "flat screen", "television", "stream", "display", diff --git a/icons/tv-minimal.json b/icons/tv-minimal.json index 91541b8ec..474e23a95 100644 --- a/icons/tv-minimal.json +++ b/icons/tv-minimal.json @@ -5,7 +5,7 @@ ], "use-cases": [], "tags": [ - "flatscreen", + "flat screen", "television", "stream", "display", diff --git a/lint-staged.config.mjs b/lint-staged.config.mjs index d1afb395d..f3fc29277 100644 --- a/lint-staged.config.mjs +++ b/lint-staged.config.mjs @@ -1,9 +1,3 @@ -/** - * @param {string[]} filenames - * @returns {string} - */ -const filenamesToAjvOption = (filenames) => filenames.map((filename) => `-d ${filename}`).join(' '); - /** @satisfies {import('lint-staged').Config} */ const config = { 'icons/*.svg': (filenames) => [ @@ -12,11 +6,13 @@ const config = { `prettier --write ${filenames.join(' ')}`, ], 'icons/*.json': (filenames) => [ - `ajv --spec=draft2020 -s icon.schema.json ${filenamesToAjvOption(filenames)}`, + `eslint ${filenames.join(' ')}`, + `cspell --no-progress ${filenames.join(' ')}`, `prettier --write ${filenames.join(' ')}`, ], 'categories/*.json': (filenames) => [ - `ajv --spec=draft2020 -s category.schema.json ${filenamesToAjvOption(filenames)}`, + `eslint ${filenames.join(' ')}`, + `cspell --no-progress ${filenames.join(' ')}`, `prettier --write ${filenames.join(' ')}`, ], }; diff --git a/package.json b/package.json index 96e74b4fe..5c5b66134 100644 --- a/package.json +++ b/package.json @@ -34,16 +34,19 @@ "suggest:metadata:debug": "node --env-file .env ./scripts/suggestMetaData.mts --debug", "suggest:metadata:watch": "node --env-file .env --watch ./scripts/suggestMetaData.mts", "postinstall": "husky", - "lint:es": "eslint .", + "lint:es": "eslint . --ignore-pattern 'icons/*.json' --ignore-pattern 'categories/*.json'", "lint:format": "prettier \"**/*.{js,mjs,ts,jsx,tsx,html,css,scss,json,yml,yaml}\" --check", "lint:format-fix": "prettier \"**/*.{js,mjs,ts,jsx,tsx,html,css,scss,json,yml,yaml}\" --write", "lint:icons": "prettier \"icons/*.svg\" --check", "lint:icons-fix": "prettier \"icons/*.svg\" --write", - "lint:json:icons": "ajv --spec=draft2020 -s icon.schema.json -d 'icons/*.json' > /dev/null", - "lint:json:categories": "ajv --spec=draft2020 -s category.schema.json -d 'categories/*.json' > /dev/null", - "lint:json": "pnpm run lint:json:icons && pnpm run lint:json:categories", + "lint:format:ci": "node ./scripts/lintFormat.mts", + "lint:json:icons": "eslint 'icons/*.json'", + "lint:json:categories": "eslint 'categories/*.json'", + "lint:json": "eslint 'icons/*.json' 'categories/*.json'", + "lint:spelling": "cspell --no-progress 'icons/*.json' 'categories/*.json'", + "lint:spelling:ci": "node ./scripts/lintSpelling.mts", "lint:icons:all": "pnpm run lint:icons && pnpm run lint:json:icons", - "lint": "pnpm lint:es && pnpm lint:format && pnpm lint:json && pnpm lint:icons", + "lint": "pnpm lint:es && pnpm lint:format && pnpm lint:json && pnpm lint:spelling && pnpm lint:icons", "format": "prettier \"**/*.{js,mjs,ts,jsx,tsx,html,css,scss,json,yml,yaml}\" --write", "prepare": "husky", "gi": "node ./scripts/generate/generateIcons.mts", @@ -63,7 +66,7 @@ "@typescript-eslint/parser": "^8.69.0", "@vitest/coverage-v8": "5.0.3", "@vitest/ui": "5.0.3", - "ajv-cli": "^5.0.0", + "ajv": "^8.20.0", "cpy-cli": "^7.0.0", "cspell": "^10.2.0", "dotenv": "^17.4.2", @@ -72,6 +75,7 @@ "eslint-plugin-import-x": "^4.17.1", "husky": "^9.1.7", "jsdom": "^30.0.1", + "jsonc-eslint-parser": "^3.3.0", "lint-staged": "^17.4.1", "minimist": "^1.2.8", "openai": "^7.9.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 02a9123ea..20df442fa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,9 +56,9 @@ importers: '@vitest/ui': specifier: 5.0.3 version: 5.0.3(vitest@5.0.3) - ajv-cli: - specifier: ^5.0.0 - version: 5.0.0(ts-node@10.9.2(@swc/core@1.7.23(@swc/helpers@0.5.23))(@types/node@26.4.1)(typescript@6.0.3)) + ajv: + specifier: ^8.20.0 + version: 8.20.0 cpy-cli: specifier: ^7.0.0 version: 7.0.0 @@ -83,6 +83,9 @@ importers: jsdom: specifier: ^30.0.1 version: 30.0.1 + jsonc-eslint-parser: + specifier: ^3.3.0 + version: 3.3.0 lint-staged: specifier: ^17.4.1 version: 17.4.1 @@ -2816,10 +2819,6 @@ packages: resolution: {integrity: sha512-TIgG/pCkgo3SiayAbV0CorYPdnaNkYWfJFKHhSBTKIdBDwK4YwcbbDlXWGCTJ6gxhhwnrNhcw4jDjz8jejKS4A==} engines: {node: '>=22.18.0'} - '@cspotcode/source-map-support@0.8.1': - resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} - engines: {node: '>=12'} - '@csstools/color-helpers@6.1.1': resolution: {integrity: sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==} engines: {node: '>=20.19.0'} @@ -3767,9 +3766,6 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@jridgewell/trace-mapping@0.3.9': - resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@kwsites/file-exists@1.1.1': resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} @@ -5729,88 +5725,9 @@ packages: svelte: ^5.46.4 vite: ^8.3.2 - '@swc/core-darwin-arm64@1.7.23': - resolution: {integrity: sha512-yyOHPfti6yKlQulfVWMt7BVKst+SyEZYCWuQSGMn1KgmNCH/bYufRWfQXIhkGSj44ZkEepJmsJ8tDyIb4k5WyA==} - engines: {node: '>=10'} - cpu: [arm64] - os: [darwin] - - '@swc/core-darwin-x64@1.7.23': - resolution: {integrity: sha512-GzqHwQ0Y1VyjdI/bBKFX2GKm5HD3PIB6OhuAQtWZMTtEr2yIrlT0YK2T+XKh7oIg31JwxGBeQdBk3KTI7DARmQ==} - engines: {node: '>=10'} - cpu: [x64] - os: [darwin] - - '@swc/core-linux-arm-gnueabihf@1.7.23': - resolution: {integrity: sha512-qwX4gB41OS6/OZkHcpTqLFGsdmvoZyffnJIlgB/kZKwH3lfeJWzv6vx57zXtNpM/t7GoQEe0VZUVdmNjxSxBZw==} - engines: {node: '>=10'} - cpu: [arm] - os: [linux] - - '@swc/core-linux-arm64-gnu@1.7.23': - resolution: {integrity: sha512-TsrbUZdMaUwzI7+g/8rHPLWbntMKYSu5Bn5IBSqVKPeyqaXxNnlIUnWXgXcUcRAc+T+Y8ADfr7EiFz9iz5DuSA==} - engines: {node: '>=10'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@swc/core-linux-arm64-musl@1.7.23': - resolution: {integrity: sha512-JEdtwdthazKq4PBz53KSubwwK8MvqODAihGSAzc8u3Unq4ojcvaS8b0CwLBeD+kTQ78HpxOXTt3DsFIxpgaCAA==} - engines: {node: '>=10'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@swc/core-linux-x64-gnu@1.7.23': - resolution: {integrity: sha512-V51gFPWaVAHbI1yg9ahsoya3aB4uawye3SZ5uQWgcP7wdCdiv60dw4F5nuPJf5Z1oXD3U/BslXuamv8Oh9vXqQ==} - engines: {node: '>=10'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@swc/core-linux-x64-musl@1.7.23': - resolution: {integrity: sha512-BBqQi4+UdeRqag3yM4IJjaHG4yc1o3l9ksENHToE0o/u2DT0FY5+K/DiYGZLC1JHbSFzNqRCYsa7DIzRtZ0A1A==} - engines: {node: '>=10'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@swc/core-win32-arm64-msvc@1.7.23': - resolution: {integrity: sha512-JPk6pvCKncL6bXG7p+NLZf8PWx4FakVvKNdwGeMrYunb+yk1IZf7qf9LJk8+GDGF5QviDXPs8opZrTrfsW80fA==} - engines: {node: '>=10'} - cpu: [arm64] - os: [win32] - - '@swc/core-win32-ia32-msvc@1.7.23': - resolution: {integrity: sha512-2Whxi8d+bLQBzJcQ5qYPHlk02YYVGsMVav0fWk+FnX2z1QRREIu1L1xvrpi7gBpjXp6BIU40ya8GiKeekNT2bg==} - engines: {node: '>=10'} - cpu: [ia32] - os: [win32] - - '@swc/core-win32-x64-msvc@1.7.23': - resolution: {integrity: sha512-82fARk4/yJ40kwWKY/gdKDisPdtgJE9jgpl/vkNG3alyJxrCzuNM7+CtiKoYbXLeqM8GQTS3wlvCaJu9oQ8dag==} - engines: {node: '>=10'} - cpu: [x64] - os: [win32] - - '@swc/core@1.7.23': - resolution: {integrity: sha512-VDNkpDvDlreGh2E3tlDj8B3piiuLhhQA/7rIVZpiLUvG1YpucAa6N7iDXA7Gc/+Hah8spaCg/qvEaBkCmcIYCQ==} - engines: {node: '>=10'} - peerDependencies: - '@swc/helpers': '*' - peerDependenciesMeta: - '@swc/helpers': - optional: true - - '@swc/counter@0.1.3': - resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} - '@swc/helpers@0.5.23': resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} - '@swc/types@0.1.27': - resolution: {integrity: sha512-K6h3iUlqeM946U4sXFYeahefR1YBbXJvko+hv8WS8/0BNJ4OHiHRywMnQUJCqkR7Y9+hqQ1TvEpiKqUhz7NEFg==} - '@tanstack/history@1.162.1': resolution: {integrity: sha512-DR9t6lfLVdrjgCwpglrR9DR7Ok8/HlXjcOE+goWXF3zyuLUO/ug7vMbSFxTqrQTtbRghJfyhmIZ0S6LhPIy44w==} engines: {node: '>=20.19'} @@ -6083,18 +6000,6 @@ packages: '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} - '@tsconfig/node10@1.0.12': - resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} - - '@tsconfig/node12@1.0.11': - resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} - - '@tsconfig/node14@1.0.3': - resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} - - '@tsconfig/node16@1.0.4': - resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} - '@tsconfig/node24@24.0.5': resolution: {integrity: sha512-DWC54fGPuhZgg3Ighg4Y/A/bA3ZdB5hnzAuv/MkQ/X85sVsBsMQxu8t7khutdYeiNksBbFdDyeqv1HSW2Zl8aA==} @@ -7100,10 +7005,6 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn-walk@8.3.5: - resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} - engines: {node: '>=0.4.0'} - acorn@8.18.0: resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} engines: {node: '>=0.4.0'} @@ -7117,15 +7018,6 @@ packages: resolution: {integrity: sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==} engines: {node: '>= 20'} - ajv-cli@5.0.0: - resolution: {integrity: sha512-LY4m6dUv44HTyhV+u2z5uX4EhPYTM38Iv1jdgDJJJCyOOuqB8KtZEGjPZ2T+sh5ZIJrXUfgErYx/j3gLd3+PlQ==} - hasBin: true - peerDependencies: - ts-node: '>=9.0.0' - peerDependenciesMeta: - ts-node: - optional: true - ajv-draft-04@1.0.0: resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} peerDependencies: @@ -7236,15 +7128,9 @@ packages: resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} engines: {node: '>= 14'} - arg@4.1.3: - resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} - arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} - argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -7943,9 +7829,6 @@ packages: resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} engines: {node: '>= 14'} - create-require@1.1.1: - resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - crelt@1.0.7: resolution: {integrity: sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==} @@ -8252,10 +8135,6 @@ packages: devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} - diff@4.0.4: - resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} - engines: {node: '>=0.3.1'} - diff@8.0.4: resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} engines: {node: '>=0.3.1'} @@ -8711,9 +8590,6 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} - fast-json-patch@3.1.1: - resolution: {integrity: sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==} - fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -9668,10 +9544,6 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@3.15.2: - resolution: {integrity: sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==} - hasBin: true - js-yaml@4.3.2: resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} hasBin: true @@ -9696,9 +9568,6 @@ packages: json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - json-schema-migrate@2.0.0: - resolution: {integrity: sha512-r38SVTtojDRp4eD6WsCqiE0eNDt4v1WalBXb9cyZYw9ai5cGtBwzRNWjHzJl38w6TxFkXAIA7h+fyX3tnrAFhQ==} - json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -9720,6 +9589,10 @@ packages: resolution: {integrity: sha512-1e4qoRgnn448pRuMvKGsFFymUCquZV0mpGgOyIKNgD3JVDTsVJyRBGH/Fm0tBb8WsWGgmB1mDe6/yJMQM37DUA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + jsonc-eslint-parser@3.3.0: + resolution: {integrity: sha512-hYTGkHGNRZnXOFZ1urhINADoqDrGfpy53cjw+dxk84QE0pUDujQzeUeamNs6Mz44/TKD49z2x6/GVSu4ZrtA+Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + jsonc-parser@2.3.1: resolution: {integrity: sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==} @@ -9993,9 +9866,6 @@ packages: resolution: {integrity: sha512-IfpFq6UM39dUNiphpA6uDezNx/AvWyhwfICWPR3t1VspkgkMZrL+Rk1RbN1bx+aeNYwOrqGJgEgV3yotk+ZUVw==} engines: {node: '>=18'} - make-error@1.3.6: - resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - make-fetch-happen@14.0.3: resolution: {integrity: sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==} engines: {node: ^18.17.0 || >=20.5.0} @@ -12252,9 +12122,6 @@ packages: resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} engines: {node: '>=0.10.0'} - sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - srvx@0.11.22: resolution: {integrity: sha512-LqZxxBDMKuMAZzFzJnDCkFOrs9MZQZr0LvHiO/SuSZVdQaXD7xQ5UWTUxheJrQPve1qk9MG2B/yttUvJxw8egQ==} engines: {node: '>=20.16.0'} @@ -12703,20 +12570,6 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - ts-node@10.9.2: - resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} - hasBin: true - peerDependencies: - '@swc/core': '>=1.2.50' - '@swc/wasm': '>=1.2.50' - '@types/node': '*' - typescript: '>=2.7' - peerDependenciesMeta: - '@swc/core': - optional: true - '@swc/wasm': - optional: true - tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -13101,9 +12954,6 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} - v8-compile-cache-lib@3.0.1: - resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} - valibot@1.4.2: resolution: {integrity: sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==} peerDependencies: @@ -13803,10 +13653,6 @@ packages: resolution: {integrity: sha512-FemWD5/UqNm8ffj8oZIbjWXIF2KE0mZssggYpdaQkWDDgXBQ/35PNIxEuz6/YLn9o0kOxDBNJe8x8k9ljD7k/g==} engines: {node: '>=18.16.0'} - yn@3.1.1: - resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} - engines: {node: '>=6'} - yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -15637,11 +15483,6 @@ snapshots: '@cspell/url@10.2.0': {} - '@cspotcode/source-map-support@0.8.1': - dependencies: - '@jridgewell/trace-mapping': 0.3.9 - optional: true - '@csstools/color-helpers@6.1.1': {} '@csstools/css-calc@3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': @@ -16559,12 +16400,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.6.0 - '@jridgewell/trace-mapping@0.3.9': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.6.0 - optional: true - '@kwsites/file-exists@1.1.1(supports-color@10.2.2)': dependencies: debug: 4.4.3(supports-color@10.2.2) @@ -18366,66 +18201,10 @@ snapshots: vite: 8.3.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1(supports-color@10.2.2))(sass-embedded@1.105.1)(sass@1.105.1)(stylus@0.56.0(supports-color@10.2.2))(terser@5.51.2)(yaml@2.9.0) vitefu: 1.1.3(vite@8.3.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1(supports-color@10.2.2))(sass-embedded@1.105.1)(sass@1.105.1)(stylus@0.56.0(supports-color@10.2.2))(terser@5.51.2)(yaml@2.9.0)) - '@swc/core-darwin-arm64@1.7.23': - optional: true - - '@swc/core-darwin-x64@1.7.23': - optional: true - - '@swc/core-linux-arm-gnueabihf@1.7.23': - optional: true - - '@swc/core-linux-arm64-gnu@1.7.23': - optional: true - - '@swc/core-linux-arm64-musl@1.7.23': - optional: true - - '@swc/core-linux-x64-gnu@1.7.23': - optional: true - - '@swc/core-linux-x64-musl@1.7.23': - optional: true - - '@swc/core-win32-arm64-msvc@1.7.23': - optional: true - - '@swc/core-win32-ia32-msvc@1.7.23': - optional: true - - '@swc/core-win32-x64-msvc@1.7.23': - optional: true - - '@swc/core@1.7.23(@swc/helpers@0.5.23)': - dependencies: - '@swc/counter': 0.1.3 - '@swc/types': 0.1.27 - optionalDependencies: - '@swc/core-darwin-arm64': 1.7.23 - '@swc/core-darwin-x64': 1.7.23 - '@swc/core-linux-arm-gnueabihf': 1.7.23 - '@swc/core-linux-arm64-gnu': 1.7.23 - '@swc/core-linux-arm64-musl': 1.7.23 - '@swc/core-linux-x64-gnu': 1.7.23 - '@swc/core-linux-x64-musl': 1.7.23 - '@swc/core-win32-arm64-msvc': 1.7.23 - '@swc/core-win32-ia32-msvc': 1.7.23 - '@swc/core-win32-x64-msvc': 1.7.23 - '@swc/helpers': 0.5.23 - optional: true - - '@swc/counter@0.1.3': - optional: true - '@swc/helpers@0.5.23': dependencies: tslib: 2.8.1 - '@swc/types@0.1.27': - dependencies: - '@swc/counter': 0.1.3 - optional: true - '@tanstack/history@1.162.1': {} '@tanstack/history@1.162.4': {} @@ -18817,18 +18596,6 @@ snapshots: '@tokenizer/token@0.3.0': {} - '@tsconfig/node10@1.0.12': - optional: true - - '@tsconfig/node12@1.0.11': - optional: true - - '@tsconfig/node14@1.0.3': - optional: true - - '@tsconfig/node16@1.0.4': - optional: true - '@tsconfig/node24@24.0.5': {} '@tsconfig/svelte@5.0.8': {} @@ -19848,29 +19615,12 @@ snapshots: dependencies: acorn: 8.18.0 - acorn-walk@8.3.5: - dependencies: - acorn: 8.18.0 - optional: true - acorn@8.18.0: {} agent-base@7.1.4: {} agent-base@9.0.0: {} - ajv-cli@5.0.0(ts-node@10.9.2(@swc/core@1.7.23(@swc/helpers@0.5.23))(@types/node@26.4.1)(typescript@6.0.3)): - dependencies: - ajv: 8.20.0 - fast-json-patch: 3.1.1 - glob: 7.2.3 - js-yaml: 3.15.2 - json-schema-migrate: 2.0.0 - json5: 2.2.3 - minimist: 1.2.8 - optionalDependencies: - ts-node: 10.9.2(@swc/core@1.7.23(@swc/helpers@0.5.23))(@types/node@26.4.1)(typescript@6.0.3) - ajv-draft-04@1.0.0(ajv@8.20.0): optionalDependencies: ajv: 8.20.0 @@ -20002,15 +19752,8 @@ snapshots: - bare-buffer - react-native-b4a - arg@4.1.3: - optional: true - arg@5.0.2: {} - argparse@1.0.10: - dependencies: - sprintf-js: 1.0.3 - argparse@2.0.1: {} argparse@3.0.1: {} @@ -20811,9 +20554,6 @@ snapshots: crc-32: 1.2.2 readable-stream: 4.7.0 - create-require@1.1.1: - optional: true - crelt@1.0.7: {} croner@10.0.1: {} @@ -21179,9 +20919,6 @@ snapshots: dependencies: dequal: 2.0.3 - diff@4.0.4: - optional: true - diff@8.0.4: {} diff@9.0.0: {} @@ -21755,8 +21492,6 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 - fast-json-patch@3.1.1: {} - fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} @@ -22753,11 +22488,6 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@3.15.2: - dependencies: - argparse: 1.0.10 - esprima: 4.0.1 - js-yaml@4.3.2: dependencies: argparse: 2.0.1 @@ -22794,10 +22524,6 @@ snapshots: json-buffer@3.0.1: {} - json-schema-migrate@2.0.0: - dependencies: - ajv: 8.20.0 - json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -22815,6 +22541,12 @@ snapshots: espree: 9.6.1 semver: 7.8.5 + jsonc-eslint-parser@3.3.0: + dependencies: + acorn: 8.18.0 + eslint-visitor-keys: 5.0.1 + verkit: 0.3.2 + jsonc-parser@2.3.1: {} jsonc-parser@3.3.1: {} @@ -23147,9 +22879,6 @@ snapshots: make-dir@5.1.0: optional: true - make-error@1.3.6: - optional: true - make-fetch-happen@14.0.3(supports-color@10.2.2): dependencies: '@npmcli/agent': 3.0.0(supports-color@10.2.2) @@ -26123,8 +25852,6 @@ snapshots: speakingurl@14.0.1: {} - sprintf-js@1.0.3: {} - srvx@0.11.22: {} srvx@0.12.8: {} @@ -26603,27 +26330,6 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-node@10.9.2(@swc/core@1.7.23(@swc/helpers@0.5.23))(@types/node@26.4.1)(typescript@6.0.3): - dependencies: - '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.12 - '@tsconfig/node12': 1.0.11 - '@tsconfig/node14': 1.0.3 - '@tsconfig/node16': 1.0.4 - '@types/node': 26.4.1 - acorn: 8.18.0 - acorn-walk: 8.3.5 - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.4 - make-error: 1.3.6 - typescript: 6.0.3 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 - optionalDependencies: - '@swc/core': 1.7.23(@swc/helpers@0.5.23) - optional: true - tslib@2.8.1: {} ttf2eot@3.1.0: @@ -27028,9 +26734,6 @@ snapshots: utils-merge@1.0.1: {} - v8-compile-cache-lib@3.0.1: - optional: true - valibot@1.4.2(typescript@6.0.3): optionalDependencies: typescript: 6.0.3 @@ -27872,9 +27575,6 @@ snapshots: yerror@8.0.0: {} - yn@3.1.1: - optional: true - yocto-queue@0.1.0: {} yocto-queue@1.2.2: {} diff --git a/scripts/eslint/githubFormatter.mjs b/scripts/eslint/githubFormatter.mjs new file mode 100644 index 000000000..e4eb1cc82 --- /dev/null +++ b/scripts/eslint/githubFormatter.mjs @@ -0,0 +1,61 @@ +import path from 'node:path'; + +/** + * ESLint formatter for GitHub Actions. Prints each problem as a readable line plus a workflow + * command (`::error file=…`), so it shows up as an inline annotation in the PR diff. + * + * Unlike setup-node's `eslint-stylish` problem matcher, this also annotates parsing errors, which + * have no rule id. + * + * Usage: eslint --format ./scripts/eslint/githubFormatter.mjs + */ + +/** Escape data for a workflow command, see https://github.com/actions/toolkit/blob/main/packages/core/src/command.ts */ +const escapeData = (/** @type {string} */ value) => + value.replaceAll('%', '%25').replaceAll('\r', '%0D').replaceAll('\n', '%0A'); +const escapeProperty = (/** @type {string} */ value) => + escapeData(value).replaceAll(':', '%3A').replaceAll(',', '%2C'); + +/** @type {import('eslint').ESLint.FormatterFunction} */ +const formatter = (results) => { + const lines = []; + let errors = 0; + let warnings = 0; + + for (const result of results) { + const file = path.relative(process.cwd(), result.filePath); + + for (const message of result.messages) { + const severity = message.severity === 2 ? 'error' : 'warning'; + if (severity === 'error') errors++; + else warnings++; + + const rule = message.ruleId ?? 'parse'; + const line = message.line ?? 1; + const column = message.column ?? 1; + lines.push(`${file}:${line}:${column} ${severity} ${message.message} (${rule})`); + + const properties = [ + `file=${escapeProperty(file)}`, + `line=${line}`, + `col=${column}`, + message.endLine ? `endLine=${message.endLine}` : '', + message.endColumn ? `endColumn=${message.endColumn}` : '', + `title=${escapeProperty(`ESLint (${rule})`)}`, + ].filter(Boolean); + lines.push(`::${severity} ${properties.join(',')}::${escapeData(message.message)}`); + } + } + + if (errors + warnings > 0) { + const plural = (/** @type {number} */ count, /** @type {string} */ word) => + `${count} ${word}${count === 1 ? '' : 's'}`; + lines.push( + '', + `✖ ${plural(errors + warnings, 'problem')} (${plural(errors, 'error')}, ${plural(warnings, 'warning')})`, + ); + } + return lines.join('\n'); +}; + +export default formatter; diff --git a/scripts/eslint/jsonSchemaPlugin.mjs b/scripts/eslint/jsonSchemaPlugin.mjs new file mode 100644 index 000000000..7a317fea9 --- /dev/null +++ b/scripts/eslint/jsonSchemaPlugin.mjs @@ -0,0 +1,147 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { Ajv2020 } from 'ajv/dist/2020.js'; +import { getStaticJSONValue } from 'jsonc-eslint-parser'; + +/** + * ESLint plugin that validates JSON files against a configured JSON Schema (draft 2020-12), + * reporting each error at the offending property. It also requires the file's `$schema` field to + * point at that schema. + * + * Off-the-shelf plugins (e.g. eslint-plugin-json-schema-validator) only run Ajv in draft-07 mode, + * which silently skips 2020-12 keywords such as `dependentRequired`. + */ + +const ajv = new Ajv2020({ allErrors: true }); +/** @type {Map} */ +const validators = new Map(); + +/** @param {string} schemaPath */ +const getValidator = (schemaPath) => { + let validate = validators.get(schemaPath); + if (!validate) { + validate = ajv.compile(JSON.parse(fs.readFileSync(schemaPath, 'utf-8'))); + validators.set(schemaPath, validate); + } + return validate; +}; + +/** + * Find the AST node for a JSON pointer (e.g. `/aliases/0/name`). Returns the property node for + * object members so the report points at the key, falling back to the closest existing parent. + * + * @param {any} node + * @param {string} pointer + */ +const findNode = (node, pointer) => { + const segments = pointer + .split('/') + .slice(1) + .map((segment) => segment.replaceAll('~1', '/').replaceAll('~0', '~')); + let current = node; + let reportNode = node; + for (const segment of segments) { + if (current?.type === 'JSONObjectExpression') { + const property = current.properties.find( + (/** @type {any} */ prop) => (prop.key.value ?? prop.key.name) === segment, + ); + if (!property) break; + reportNode = property; + current = property.value; + } else if (current?.type === 'JSONArrayExpression') { + const element = current.elements[Number(segment)]; + if (!element) break; + reportNode = element; + current = element; + } else { + break; + } + } + return { node: current, reportNode }; +}; + +/** @type {import('eslint').Rule.RuleModule} */ +const noInvalid = { + meta: { + type: 'problem', + docs: { + description: 'Validate JSON files against a JSON Schema and require `$schema` to point at it', + }, + schema: [ + { + type: 'object', + properties: { + // Path to the schema, relative to the ESLint working directory (the repo root). + schema: { type: 'string' }, + }, + required: ['schema'], + additionalProperties: false, + }, + ], + }, + create(context) { + return { + /** @param {any} program */ + Program(program) { + const root = program.body[0]?.expression; + if (root?.type !== 'JSONObjectExpression') { + context.report({ node: root ?? program, message: 'Root value must be an object.' }); + return; + } + + const data = getStaticJSONValue(root); + if (typeof data?.$schema !== 'string') { + context.report({ node: root, message: 'Missing `$schema` field.' }); + return; + } + + // Validate against the configured schema, not whatever `$schema` points at, so a file can't + // opt out of validation by referencing a more permissive schema. + const schemaPath = path.resolve(context.cwd, context.options[0].schema); + const expectedRef = path + .relative(path.dirname(context.filename), schemaPath) + .split(path.sep) + .join('/'); + if (path.resolve(path.dirname(context.filename), data.$schema) !== schemaPath) { + context.report({ + node: findNode(root, '/$schema').reportNode, + message: `\`$schema\` must be "${expectedRef}".`, + }); + } + + const validate = getValidator(schemaPath); + if (validate(data)) return; + + for (const error of validate.errors ?? []) { + const { node, reportNode } = findNode(root, error.instancePath); + const location = error.instancePath || '(root)'; + + if (error.keyword === 'additionalProperties' && node?.type === 'JSONObjectExpression') { + const property = findNode( + node, + `/${error.params.additionalProperty.replaceAll('~', '~0').replaceAll('/', '~1')}`, + ).reportNode; + context.report({ + node: property, + message: `${location} has unexpected property "${error.params.additionalProperty}".`, + }); + continue; + } + + const isContainer = + reportNode.type === 'JSONObjectExpression' || reportNode.type === 'JSONArrayExpression'; + context.report({ + // Report objects and arrays at their opening bracket only, not their whole range. + loc: isContainer ? reportNode.loc.start : reportNode.loc, + message: `${location} ${error.message}.`, + }); + } + }, + }; + }, +}; + +export default { + meta: { name: 'json-schema' }, + rules: { 'no-invalid': noInvalid }, +}; diff --git a/scripts/lintFormat.mts b/scripts/lintFormat.mts new file mode 100644 index 000000000..faaa37a80 --- /dev/null +++ b/scripts/lintFormat.mts @@ -0,0 +1,123 @@ +/** + * Checks formatting with Prettier and reports every unformatted range as a GitHub Actions + * annotation (`::error file=…,line=…`), so it shows up inline in the PR diff. + * + * `prettier --check` only reports file names, so this formats each listed file in memory and + * diffs it against the original to find the exact lines. + */ +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import process from 'process'; +import { spawnSync } from 'child_process'; +import { createRequire } from 'module'; +import * as prettier from 'prettier'; + +const PATTERNS = ['**/*.{js,mjs,ts,jsx,tsx,html,css,scss,json,yml,yaml}', 'icons/*.svg']; + +const prettierBin = createRequire(import.meta.url).resolve('prettier/bin/prettier.cjs'); + +const listDifferent = spawnSync( + process.execPath, + [prettierBin, '--list-different', '--no-color', ...PATTERNS], + { encoding: 'utf-8', maxBuffer: 64 * 1024 * 1024 }, +); + +/** Escape data for a workflow command, see https://github.com/actions/toolkit/blob/main/packages/core/src/command.ts */ +const escapeData = (value: string) => + value.replaceAll('%', '%25').replaceAll('\r', '%0D').replaceAll('\n', '%0A'); +const escapeProperty = (value: string) => + escapeData(value).replaceAll(':', '%3A').replaceAll(',', '%2C'); + +let problems = 0; + +// Exit code 2 means Prettier could not process a file, e.g. because of a syntax error. +if (listDifferent.status !== 0 && listDifferent.status !== 1) { + console.log('::group::Prettier errors'); + console.log(listDifferent.stderr); + console.log('::endgroup::'); + + const syntaxError = /^\[error\] (.+?): (\w*Error: .+) \((\d+):(\d+)\)$/; + for (const line of listDifferent.stderr.split('\n')) { + const match = line.match(syntaxError); + if (!match) continue; + const [, file, message, errorLine, column] = match; + console.log( + `::error file=${escapeProperty(file)},line=${errorLine},col=${column},title=Prettier::${escapeData(message)}`, + ); + problems++; + } + + if (problems === 0) { + console.log( + `::error title=Prettier::${escapeData(listDifferent.stderr.trim() || 'Prettier failed.')}`, + ); + problems++; + } +} + +const files = listDifferent.stdout.split('\n').filter(Boolean); + +if (files.length === 0 && problems === 0) { + console.log('All matched files use Prettier code style!'); + process.exit(0); +} + +const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lint-format-')); +const hunkHeader = /^@@ -(\d+)(?:,(\d+))? \+\d+(?:,\d+)? @@/; + +for (const file of files) { + const source = fs.readFileSync(file, 'utf-8'); + const options = await prettier.resolveConfig(file); + const formatted = await prettier.format(source, { ...options, filepath: file }); + + const formattedFile = path.join(tmpDir, 'formatted'); + fs.writeFileSync(formattedFile, formatted); + + const { stdout: diff } = spawnSync( + 'git', + ['diff', '--no-index', '--no-color', '-U0', '--', file, formattedFile], + { encoding: 'utf-8', maxBuffer: 64 * 1024 * 1024 }, + ); + + console.log(`::group::${file}`); + console.log(diff); + console.log('::endgroup::'); + + const lines = diff.split('\n'); + for (let index = 0; index < lines.length; index++) { + const match = lines[index].match(hunkHeader); + if (!match) continue; + + const start = Number(match[1]); + const count = match[2] === undefined ? 1 : Number(match[2]); + + const expected: string[] = []; + while (index + 1 < lines.length && /^[-+\\ ]/.test(lines[index + 1])) { + index++; + if (lines[index].startsWith('+')) expected.push(lines[index].slice(1)); + } + + // A pure insertion (count 0) happens after `start`, so point at that line. + const line = Math.max(start, 1); + const endLine = count === 0 ? line : start + count - 1; + + const message = + expected.length > 0 + ? `Code is not formatted with Prettier. Expected:\n${expected.join('\n')}` + : 'Code is not formatted with Prettier. Remove these lines.'; + + console.log( + `::error file=${escapeProperty(file)},line=${line},endLine=${endLine},title=Prettier::${escapeData(message)}`, + ); + } +} + +fs.rmSync(tmpDir, { recursive: true, force: true }); + +if (files.length > 0) { + console.log( + `\n${files.length} file(s) not formatted with Prettier. Run \`pnpm lint:format-fix\` and \`pnpm lint:icons-fix\` to fix.`, + ); +} +process.exit(1); diff --git a/scripts/lintSpelling.mts b/scripts/lintSpelling.mts new file mode 100644 index 000000000..512522e9b --- /dev/null +++ b/scripts/lintSpelling.mts @@ -0,0 +1,57 @@ +/** + * Spell checks icon and category metadata with cspell and reports every unknown word as a GitHub + * Actions annotation (`::error file=…,line=…`), so it shows up inline in the PR diff. + * + * Words that are correct but unknown to cspell go in `.cspell/custom-words.txt`. + */ +import process from 'process'; +import { spawnSync } from 'child_process'; +import { fileURLToPath } from 'url'; + +const PATTERNS = ['icons/*.json', 'categories/*.json']; + +// cspell only exports its bin for `import`, so resolve it the ESM way. +const cspellBin = fileURLToPath(import.meta.resolve('cspell/bin.mjs')); + +const cspell = spawnSync( + process.execPath, + [cspellBin, '--no-color', '--no-progress', '--no-summary', ...PATTERNS], + { encoding: 'utf-8', maxBuffer: 64 * 1024 * 1024 }, +); + +/** Escape data for a workflow command, see https://github.com/actions/toolkit/blob/main/packages/core/src/command.ts */ +const escapeData = (value: string) => + value.replaceAll('%', '%25').replaceAll('\r', '%0D').replaceAll('\n', '%0A'); +const escapeProperty = (value: string) => + escapeData(value).replaceAll(':', '%3A').replaceAll(',', '%2C'); + +const issue = /^(.+?):(\d+):(\d+) - Unknown word \((.+?)\)(?: fix: \((.+?)\))?/; +let problems = 0; + +for (const line of cspell.stdout.split('\n')) { + const match = line.match(issue); + if (!match) continue; + + const [, file, lineNumber, column, word, fix] = match; + const suggestion = fix ? ` Did you mean "${fix}"?` : ''; + const message = `Unknown word "${word}".${suggestion} Fix the typo or add the word to .cspell/custom-words.txt.`; + + console.log(line); + console.log( + `::error file=${escapeProperty(file)},line=${lineNumber},col=${column},endColumn=${Number(column) + word.length},title=cspell::${escapeData(message)}`, + ); + problems++; +} + +if (cspell.status !== 0 && problems === 0) { + process.stderr.write(cspell.stderr); + console.log(`::error title=cspell::${escapeData(cspell.stderr.trim() || 'cspell failed.')}`); + process.exit(cspell.status ?? 1); +} + +if (problems > 0) { + console.log(`\n${problems} spelling issue(s) found.`); + process.exit(1); +} + +console.log('No spelling issues found.');