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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FVxEtqNswEDq1N23taZYh8

* Revert

* Try another error

* Revert error

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Eric Fennis
2026-10-09 11:30:00 +02:00
committed by GitHub
co-authored by Claude Opus 5.5 Copilot Autofix powered by AI
parent 62c584a5f2
commit 55b9969b82
31 changed files with 558 additions and 353 deletions
+39
View File
@@ -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
+56 -3
View File
@@ -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
+5
View File
@@ -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"
]
}
+23
View File
@@ -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' }],
},
},
]);
-1
View File
@@ -8,7 +8,6 @@
"use-cases": [],
"tags": [
"symbol",
"sterisk",
"mark",
"pointer",
"pencil",
+1 -1
View File
@@ -16,7 +16,7 @@
"payment",
"bdt",
"৳",
"currencysymbol",
"currency symbol",
"cash",
"banknote",
"price",
+1 -1
View File
@@ -30,7 +30,7 @@
"startup",
"registration",
"onboarding",
"realestate",
"real estate",
"property"
],
"categories": [
+1 -1
View File
@@ -11,7 +11,7 @@
"tags": [
"battery",
"automobile",
"powercell",
"power cell",
"electric",
"power",
"electricity",
+1 -1
View File
@@ -13,7 +13,7 @@
"bytes",
"servers",
"backup",
"timemachine",
"time machine",
"rotate",
"synchronize",
"synchronise",
+1 -1
View File
@@ -13,7 +13,7 @@
"bytes",
"servers",
"backup",
"timemachine",
"time machine",
"rotate",
"arrow",
"left"
+1 -1
View File
@@ -30,7 +30,7 @@
"nautical",
"harbor",
"shore",
"lightbeam",
"light beam",
"wayfinding",
"guidance",
"onboarding",
+1 -1
View File
@@ -18,7 +18,7 @@
"unread",
"scheduled",
"delayed",
"sendlater",
"send later",
"delivery",
"reminder",
"pending",
+1 -1
View File
@@ -26,7 +26,7 @@
"interview",
"debate",
"group",
"groupchat",
"group chat",
"threads",
"unread",
"bubble",
+1 -1
View File
@@ -20,7 +20,7 @@
"eco-friendly",
"kraft",
"retail",
"doggybag"
"doggy bag"
],
"categories": [
"food-beverage",
-1
View File
@@ -5,7 +5,6 @@
],
"use-cases": [],
"tags": [
"frigerator",
"fridge",
"freezer",
"cooler",
-1
View File
@@ -18,7 +18,6 @@
"housekeeping",
"tool",
"maintenance",
"smarthome",
"sweeping",
"hygiene",
"chores",
+1 -1
View File
@@ -8,7 +8,7 @@
"tags": [
"barcode",
"scan",
"qrcode",
"qr code",
"url",
"information",
"digital",
+1 -1
View File
@@ -8,7 +8,7 @@
"beach",
"sand",
"holiday",
"sealife",
"sea life",
"fossil",
"ammonite",
"biology",
+1 -1
View File
@@ -37,7 +37,7 @@
"admin",
"verification",
"crest",
"shieldcog",
"shield cog",
"bravery",
"knight",
"foot soldier",
+1 -1
View File
@@ -27,7 +27,7 @@
"performance",
"lens",
"leaderboard",
"followspot",
"follow spot",
"best",
"highlight"
],
+1 -1
View File
@@ -24,7 +24,7 @@
"handle",
"plaque",
"toothpaste",
"selfcare",
"self care",
"grooming",
"fresh",
"clean"
+1 -1
View File
@@ -24,7 +24,7 @@
"handle",
"plaque",
"toothpaste",
"selfcare",
"self care",
"grooming"
],
"categories": [
+1 -1
View File
@@ -7,7 +7,7 @@
],
"use-cases": [],
"tags": [
"flatscreen",
"flat screen",
"television",
"stream",
"display",
+1 -1
View File
@@ -5,7 +5,7 @@
],
"use-cases": [],
"tags": [
"flatscreen",
"flat screen",
"television",
"stream",
"display",
+4 -8
View File
@@ -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(' ')}`,
],
};
+10 -6
View File
@@ -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",
+16 -316
View File
@@ -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: {}
+61
View File
@@ -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;
+147
View File
@@ -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<string, import('ajv').ValidateFunction>} */
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 },
};
+123
View File
@@ -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);
+57
View File
@@ -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.');