diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..23b48fc --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,2 @@ +cd agiledancers-fe || exit 1 +node node_modules/lint-staged/bin/lint-staged.js --concurrent false diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..004bd71 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["angular.ng-template", "dbaeumer.vscode-eslint"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..21e7bf9 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "eslint.workingDirectories": ["./agiledancers-fe"] +} diff --git a/README.md b/README.md index bc53d4c..910e4ee 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,53 @@ backend on port 8080. Frontend builds and tests run independently of Maven: `npm run build` and `npm test`. See the frontend README for browser tests and production hosting requirements. +## Frontend lint and formatting + +Run these commands from `agiledancers-fe/`: + +```sh +npm run lint +npm run lint:fix +``` + +`lint` checks maintained application code, inline and external Angular templates, +unit tests, Playwright tests and JavaScript/TypeScript configuration. It uses the +recommended JavaScript, Angular and type-aware TypeScript rules. It changes no +files; both errors and warnings cause failure. Dependencies, generated output +and `package-lock.json` are excluded. `lint:fix` applies ESLint's available fixes; +review the diff and resolve any remaining findings manually. Prettier retains +responsibility for formatting with the existing configuration. + +Every branch pipeline runs `npm run lint` immediately after frontend `npm ci`, +before frontend tests/builds and container E2E. Failure prevents image publication; +findings are in the job log and retained `ci/artifacts/verify.log`. CI only checks +and never fixes or stages source files. + +Local `npm ci` automatically installs Husky at the repository root. Its commit +hook runs **Prettier only**, through lint-staged with backup, rollback and hiding +of unstaged changes in partially staged files enabled. It formats applicable +staged frontend TypeScript, JavaScript, HTML, CSS and JSON files, then stages the +formatting. Unrelated unstaged edits and files remain outside the commit. A +formatter error or a conflict restoring unstaged edits aborts the commit and +restores the original staged and unstaged state. Generated files are excluded; +commits without applicable frontend changes skip these checks. + +The complete Prettier-plus-ESLint hook exceeds the agreed three-second budget +when all maintained files are staged. The selected Prettier-only hook meets the +budget for single-file, mixed and all-file commits. ESLint is therefore run +explicitly during development and always by CI. See [S2 validation](docs/s2-validation.md) +for the measured environment, timings and preservation checks. + +Hook installation is skipped when `CI` is set (including Docker builds) or +`HUSKY=0`. Use `HUSKY=0 npm ci` to skip local installation, `HUSKY=0 git commit` +to skip one hook execution, or `git config --local core.hooksPath /dev/null` to +disable hooks for this clone. A subsequent normal `npm ci` enables them again. + +VS Code recommends the ESLint extension in both root and frontend workspaces. +With it installed, findings appear in the editor and Problems panel; the root +workspace selects `agiledancers-fe/` as ESLint's working directory. No automatic +fixing or formatting on save is required. `npm run lint` uses the same rules as CI. + ## Gitea CI Every branch push builds and tests both applications and their Docker images. diff --git a/agiledancers-fe/.prettierignore b/agiledancers-fe/.prettierignore new file mode 100644 index 0000000..f4b692b --- /dev/null +++ b/agiledancers-fe/.prettierignore @@ -0,0 +1,14 @@ +node_modules/ +dist/ +.angular/ +coverage/ +test-results/ +unit-results/ +playwright-report/ +package-lock.json +out-tsc/ +bazel-out/ +tmp/ +typings/ +__screenshots__/ +.history/ diff --git a/agiledancers-fe/.vscode/extensions.json b/agiledancers-fe/.vscode/extensions.json index 77b3745..b7cd262 100644 --- a/agiledancers-fe/.vscode/extensions.json +++ b/agiledancers-fe/.vscode/extensions.json @@ -1,4 +1,4 @@ { // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 - "recommendations": ["angular.ng-template"] + "recommendations": ["angular.ng-template", "dbaeumer.vscode-eslint"] } diff --git a/agiledancers-fe/Dockerfile b/agiledancers-fe/Dockerfile index d3a0391..b709919 100644 --- a/agiledancers-fe/Dockerfile +++ b/agiledancers-fe/Dockerfile @@ -1,9 +1,10 @@ FROM node:24.15.0-bookworm AS build WORKDIR /workspace COPY package.json package-lock.json ./ -RUN npm ci +COPY tools/install-hooks.mjs tools/install-hooks.mjs +RUN CI=true npm ci COPY . . -RUN npm test && npm run build +RUN npm run lint && npm test && npm run build FROM nginx:1.28-alpine AS runtime ENV BACKEND_UPSTREAM=backend:8080 diff --git a/agiledancers-fe/README.md b/agiledancers-fe/README.md index 7eaf866..62503ee 100644 --- a/agiledancers-fe/README.md +++ b/agiledancers-fe/README.md @@ -84,3 +84,9 @@ PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/path/to/chrome npm run e2e Browser test screenshots and failure traces are written to the ignored `test-results` directory. + +## Lint and commit formatting + +See the [root README](../README.md#frontend-lint-and-formatting) for `npm run lint`, +`npm run lint:fix`, editor support, CI enforcement and the measured Prettier-only +commit hook, including installation and local opt-out. diff --git a/agiledancers-fe/e2e/school-management.spec.ts b/agiledancers-fe/e2e/school-management.spec.ts index 2405a05..e1efe94 100644 --- a/agiledancers-fe/e2e/school-management.spec.ts +++ b/agiledancers-fe/e2e/school-management.spec.ts @@ -1,4 +1,5 @@ import { test, expect, Page } from '@playwright/test'; +import type { School } from '../src/app/core/api-models'; const backend = process.env['PLAYWRIGHT_BASE_URL'] || 'http://127.0.0.1:8080'; let schoolToDelete: number | undefined; @@ -32,7 +33,6 @@ test('manages a school, shared memberships and shows through the real REST API', page, }) => { const schoolName = `E2E Studio ${Date.now()}`; - let schoolId: number | undefined; const browserErrors: string[] = []; page.on('pageerror', (error) => browserErrors.push(error.message)); await page.goto('/'); @@ -43,8 +43,9 @@ test('manages a school, shared memberships and shows through the real REST API', response.url().endsWith('/api/dance-schools') && response.request().method() === 'POST', ); await save(page); - schoolId = (await (await created).json()).id; - schoolToDelete = schoolId; + const school: unknown = await (await created).json(); + expect(school).toEqual(expect.objectContaining({ id: expect.any(Number) })); + schoolToDelete = (school as School).id; await page.getByRole('link', { name: new RegExp(schoolName) }).click(); await expect(page.getByRole('heading', { name: schoolName, exact: true })).toBeVisible(); diff --git a/agiledancers-fe/eslint.config.mjs b/agiledancers-fe/eslint.config.mjs new file mode 100644 index 0000000..bc17003 --- /dev/null +++ b/agiledancers-fe/eslint.config.mjs @@ -0,0 +1,47 @@ +import js from '@eslint/js'; +import { defineConfig, globalIgnores } from 'eslint/config'; +import tseslint from 'typescript-eslint'; +import angular from 'angular-eslint'; +import prettier from 'eslint-config-prettier'; +import globals from 'globals'; + +export default defineConfig( + globalIgnores([ + '**/node_modules/**', + '**/dist/**', + '**/out-tsc/**', + '**/bazel-out/**', + '**/tmp/**', + '**/typings/**', + '**/__screenshots__/**', + '**/.history/**', + '**/.angular/**', + '**/coverage/**', + '**/test-results/**', + '**/unit-results/**', + '**/playwright-report/**', + 'package-lock.json', + ]), + { + files: ['**/*.{js,mjs,cjs}'], + extends: [js.configs.recommended], + languageOptions: { globals: globals.node }, + }, + { + files: ['**/*.ts'], + extends: [ + js.configs.recommended, + tseslint.configs.recommendedTypeChecked, + angular.configs.tsRecommended, + ], + languageOptions: { + parserOptions: { + project: './tsconfig.eslint.json', + tsconfigRootDir: import.meta.dirname, + }, + }, + processor: angular.processInlineTemplates, + }, + { files: ['**/*.html'], extends: [angular.configs.templateRecommended] }, + prettier, +); diff --git a/agiledancers-fe/lint-staged.config.mjs b/agiledancers-fe/lint-staged.config.mjs new file mode 100644 index 0000000..1465a0d --- /dev/null +++ b/agiledancers-fe/lint-staged.config.mjs @@ -0,0 +1,21 @@ +import path from 'node:path'; + +export default { + '*.{ts,js,mjs,cjs,html,css,json,prettierrc}': (files) => { + const maintained = files.filter((file) => { + const relative = path + .relative(import.meta.dirname, file) + .split(path.sep) + .join('/'); + return ( + !relative.startsWith('../') && + !/^(node_modules|dist|out-tsc|bazel-out|tmp|typings|__screenshots__|\.history|\.angular|coverage|test-results|unit-results|playwright-report)\//.test( + relative, + ) && + relative !== 'package-lock.json' + ); + }); + if (!maintained.length) return []; + return [`prettier --write ${maintained.map((file) => JSON.stringify(file)).join(' ')}`]; + }, +}; diff --git a/agiledancers-fe/package-lock.json b/agiledancers-fe/package-lock.json index e2b4568..8750064 100644 --- a/agiledancers-fe/package-lock.json +++ b/agiledancers-fe/package-lock.json @@ -21,10 +21,19 @@ "@angular/build": "22.2.4", "@angular/cli": "22.2.4", "@angular/compiler-cli": "22.2.3", + "@eslint/js": "10.0.1", "@playwright/test": "1.64.1", + "@types/node": "22.20.5", + "angular-eslint": "22.6.0", + "eslint": "10.13.0", + "eslint-config-prettier": "10.1.8", + "globals": "17.13.0", + "husky": "9.1.7", "jsdom": "30.1.3", + "lint-staged": "17.6.0", "prettier": "3.9.10", "typescript": "6.0.3", + "typescript-eslint": "8.72.1", "vitest": "5.0.5" }, "engines": { @@ -111,6 +120,111 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@angular-eslint/builder": { + "version": "22.6.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-22.6.0.tgz", + "integrity": "sha512-Zn9yjLtUYKd9HF5UZXKqLKuvdYFejVuTJHQ0gFt4pjWDRvb16Lgc8EzeGyYTsc9MNnyFAmHeDTs2dtlFITSDBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": ">= 0.2200.0 < 0.2300.0", + "@angular-devkit/core": ">= 22.0.0 < 23.0.0" + }, + "peerDependencies": { + "eslint": "^9.0.0 || ^10.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/bundled-angular-compiler": { + "version": "22.6.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-22.6.0.tgz", + "integrity": "sha512-OOoWbFG3JOsdY9/qiY2YdXJFZNVg3jPb09OTHQvBir0CLIdEnudKePVs0JxlKL1Yl05ibvwolo+/Ewfmp5Gqvg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular-eslint/eslint-plugin": { + "version": "22.6.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-22.6.0.tgz", + "integrity": "sha512-zLQSfwNiOaWYp1IRQh4TlgBbUmaGm0OWNEO98E2U9DstHJTPTghRwG4wZX4Ly/YOYsb3WU06pD1/cCRUYHRgmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "22.6.0", + "@angular-eslint/utils": "22.6.0", + "ts-api-utils": "^2.1.0" + }, + "peerDependencies": { + "@typescript-eslint/utils": "^8.0.0", + "eslint": "^9.0.0 || ^10.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/eslint-plugin-template": { + "version": "22.6.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-22.6.0.tgz", + "integrity": "sha512-e3ueYzr1ARovQtoBaiAeYtLR3MLIeJNKA46LBwKbRhVl1S8L1FiwYsWJvx0lRvcgdFWFt+WrSXeTxuc0ITW9eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "22.6.0", + "@angular-eslint/utils": "22.6.0", + "aria-query": "5.3.2", + "axobject-query": "4.1.0" + }, + "peerDependencies": { + "@angular-eslint/template-parser": "22.6.0", + "@typescript-eslint/types": "^8.0.0", + "@typescript-eslint/utils": "^8.0.0", + "eslint": "^9.0.0 || ^10.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/schematics": { + "version": "22.6.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-22.6.0.tgz", + "integrity": "sha512-rLZkMWuUxIc8IZlf3TDPkLreVAmPDAJmzqziwnBzXM8slb1xumU4yXsNk8A8nQAua4Sr1IcBKBRJKydSg8fHFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": ">= 22.0.0 < 23.0.0", + "@angular-devkit/schematics": ">= 22.0.0 < 23.0.0", + "@angular-eslint/eslint-plugin": "22.6.0", + "@angular-eslint/eslint-plugin-template": "22.6.0", + "ignore": "7.0.12", + "semver": "7.8.5", + "strip-json-comments": "3.1.1" + } + }, + "node_modules/@angular-eslint/template-parser": { + "version": "22.6.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-22.6.0.tgz", + "integrity": "sha512-QmrW5T9Sb8ve2cgEGtkhDujp7ENRnXkqT+nCxLK0rZkbPXET+i1svLxuc4tzyVJpceYo772EzMyQuJbNqiH/cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "22.6.0", + "eslint-scope": "9.1.2" + }, + "peerDependencies": { + "eslint": "^9.0.0 || ^10.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/utils": { + "version": "22.6.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-22.6.0.tgz", + "integrity": "sha512-Nd5pZGgkBTvcCNMBt88iG6r/Tzk1XTWHpQQccwYUWYhz4PMim0yyXAEvqAabxag8S9iLPUEufpaOw20OOxNJ3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "22.6.0" + }, + "peerDependencies": { + "@typescript-eslint/utils": "^8.0.0", + "eslint": "^9.0.0 || ^10.0.0", + "typescript": "*" + } + }, "node_modules/@angular/build": { "version": "22.2.4", "resolved": "https://registry.npmjs.org/@angular/build/-/build-22.2.4.tgz", @@ -820,6 +934,30 @@ "dev": true, "license": "(Apache-2.0 AND BSD-3-Clause)" }, + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, "node_modules/@csstools/color-helpers": { "version": "6.1.2", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.2.tgz", @@ -1402,6 +1540,121 @@ "node": ">=18" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, "node_modules/@exodus/bytes": { "version": "1.16.0", "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.16.0.tgz", @@ -1428,6 +1681,72 @@ "license": "Apache-2.0", "optional": true }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@inquirer/ansi": { "version": "2.0.10", "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.10.tgz", @@ -1991,6 +2310,30 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, "node_modules/@listr2/prompt-adapter-inquirer": { "version": "4.2.7", "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-4.2.7.tgz", @@ -3481,6 +3824,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -3502,6 +3852,256 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz", + "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.1.tgz", + "integrity": "sha512-5CokTWXpmbylfFabsbvHC0CAw3XLaGnoa+nFJX5FR592h/LoZsENUE6p/rg5xvkKrFEXz6pYmgI29AgXc2N3BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.72.1", + "@typescript-eslint/type-utils": "8.72.1", + "@typescript-eslint/utils": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.72.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.1.tgz", + "integrity": "sha512-ZGwZP4OCfyA0AQTpEAMWz+rtX01DRhQWW5hB0ucKDEIX5U7jdmPSaMx94rQfaPAQzIYz2BimOgKOJtjfpRFQ9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.72.1", + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/typescript-estree": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.1.tgz", + "integrity": "sha512-fAuVMkwdO/huI8nyo/WKnNKf6Gag93S56Rh3Ts5mGcavwSdaN8tRg1gCeIGX8PF+nzp1BjGfO4gHhGzW1J6jTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.72.1", + "@typescript-eslint/types": "^8.72.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.1.tgz", + "integrity": "sha512-lC/t6eMCZZQAAKZEYGL6RDVSuFehpdmMT6h+nJIjIa6oylN/PwDgDVtEIbB7O9xFfZYtZAYLoLlDNWKJhtF+uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.1.tgz", + "integrity": "sha512-e4D5R5vpeAEPBxT2BAzsFQL3sbwyeeeo5+aczgNSRK1V734jE7pTqaGFU+fXapWEQwnZbpFaTn32+Z8DwS6OTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.72.1.tgz", + "integrity": "sha512-iY2wlmyrXJmx4yxUGP3aYCFF3cA/6ShddOKmLU2dx7LnEykbAM8WThlxZEfd1NddqceF6eJmZUayxx9oKIYNlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/typescript-estree": "8.72.1", + "@typescript-eslint/utils": "8.72.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.1.tgz", + "integrity": "sha512-LmE5IhQet1BWOWEXqBrVtBPsMAOQ6Nzk35d+WLh3mk4EUF33sqlfxjio1MdYeSvKSG5skactKKuixBIKXhL4Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.1.tgz", + "integrity": "sha512-DAlUoUV93hIvrtIQLwCbmZQYVZwOMhiHJrOVUUIWicFZaIYjTRqlY3I/NZevfkLmSqkNHRu2qYDcq9lrn16oOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.72.1", + "@typescript-eslint/tsconfig-utils": "8.72.1", + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.1.tgz", + "integrity": "sha512-5v5liypurJBFHCBNqh/RjsymAD+VWcE0djuGY/BUqp5fC3Qn+luWDEksjuTtWjPn6i5zQlUtkZCRG/CYG0yCkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.72.1", + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/typescript-estree": "8.72.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.1.tgz", + "integrity": "sha512-SB8xqipSzZHUePCCLNeYxqnxq83X61/qzNhjQGbTfjxGZIeOEP7tBgWYX05Tcd6CgE02WdTJnh5b8kMZJ/+bnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@vitejs/plugin-basic-ssl": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.3.0.tgz", @@ -3563,6 +4163,29 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/agent-base": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", @@ -3608,6 +4231,29 @@ } } }, + "node_modules/angular-eslint": { + "version": "22.6.0", + "resolved": "https://registry.npmjs.org/angular-eslint/-/angular-eslint-22.6.0.tgz", + "integrity": "sha512-nHjnadgrM54bz6tq/1EJqAg1mZCjJLyunitINdpPk+07Iz5MkJc4F1smeYshqfhH0i9+HOyJ4c7eA419O970Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": ">= 22.0.0 < 23.0.0", + "@angular-devkit/schematics": ">= 22.0.0 < 23.0.0", + "@angular-eslint/builder": "22.6.0", + "@angular-eslint/eslint-plugin": "22.6.0", + "@angular-eslint/eslint-plugin-template": "22.6.0", + "@angular-eslint/schematics": "22.6.0", + "@angular-eslint/template-parser": "22.6.0", + "@typescript-eslint/types": "^8.0.0", + "@typescript-eslint/utils": "^8.0.0" + }, + "peerDependencies": { + "eslint": "^9.0.0 || ^10.0.0", + "typescript": "*", + "typescript-eslint": "^8.0.0" + } + }, "node_modules/ansi-escapes": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", @@ -3650,6 +4296,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -3660,6 +4316,26 @@ "node": ">=12" } }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.11.33", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.33.tgz", @@ -3718,6 +4394,19 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/browserslist": { "version": "4.29.5", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.5.tgz", @@ -3759,6 +4448,20 @@ "dev": true, "license": "MIT" }, + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001819", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001819.tgz", @@ -3951,6 +4654,21 @@ "dev": true, "license": "MIT" }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/css-select": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-7.0.0.tgz", @@ -4039,6 +4757,13 @@ "dev": true, "license": "MIT" }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -4229,6 +4954,240 @@ "node": ">=6" } }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.13.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.13.0.tgz", + "integrity": "sha512-UohDjD4zSzEwhf58M3O7vc/ycc8O1I7dNlhtD0WxE8Px8EHNJKtUwzBqrzeGhisRuv2ag4sAYswszCJRgKXYUg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.3", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "11.1.5 || >11.1.6 <12", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -4239,6 +5198,16 @@ "@types/estree": "^1.0.0" } }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/expect-type": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", @@ -4256,6 +5225,20 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-string-truncated-width": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", @@ -4318,6 +5301,52 @@ } } }, + "node_modules/file-entry-cache": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^6.1.23" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, "node_modules/flru": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/flru/-/flru-1.0.2.tgz", @@ -4376,6 +5405,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.13.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.13.0.tgz", + "integrity": "sha512-RwMTC61u7hrG3ZJMkZQOK4JLJrKS8QtoTii63EmWvFXHqycY9FObBJQCbsLxSO8kjKhWE5kV1GC+Vb1g3RI0Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -4386,6 +5441,26 @@ "node": ">=8" } }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true, + "license": "MIT" + }, "node_modules/hosted-git-info": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-10.1.1.tgz", @@ -4450,6 +5525,22 @@ "node": ">= 20" } }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-lite": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", @@ -4467,6 +5558,16 @@ "url": "https://opencollective.com/express" } }, + "node_modules/ignore": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.12.tgz", + "integrity": "sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/immutable": { "version": "5.1.9", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz", @@ -4485,6 +5586,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -4557,6 +5668,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, "node_modules/js-tokens": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", @@ -4624,6 +5742,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -4644,6 +5769,30 @@ "dev": true, "license": "MIT" }, + "node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lightningcss": { "version": "1.33.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", @@ -4905,6 +6054,30 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lint-staged": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-17.6.0.tgz", + "integrity": "sha512-nOhBfYkZEwkFtNzD5pK9s9LkcWoxJ+whLzEnhqi9nl7zTB5n7tg8+9iYgMGx2Wm3VlLu8cbNj0RACzY+nWi5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^4.0.7", + "string-argv": "^0.3.2", + "tinyexec": "^1.3.1" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=22.22.1" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + }, + "optionalDependencies": { + "yaml": "^2.9.1" + } + }, "node_modules/listr2": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/listr2/-/listr2-11.1.0.tgz", @@ -4957,6 +6130,22 @@ "license": "MIT", "optional": true }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/log-symbols": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", @@ -5035,6 +6224,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -5115,6 +6320,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/node-addon-api": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", @@ -5211,6 +6423,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/ora": { "version": "9.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-9.4.1.tgz", @@ -5279,6 +6509,38 @@ "@oxc-parser/binding-win32-x64-msvc": "0.150.0" } }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse5": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", @@ -5320,6 +6582,26 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5445,6 +6727,16 @@ "postcss": "^8.4.31" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/prettier": { "version": "3.9.10", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz", @@ -5499,6 +6791,26 @@ "node": ">=6" } }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "dev": true, + "license": "MIT" + }, "node_modules/readdirp": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", @@ -6002,6 +7314,29 @@ "node": ">=10" } }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -6093,6 +7428,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-width": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", @@ -6126,6 +7471,19 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -6248,12 +7606,38 @@ "node": "^22.14.0 || >=24.0.0" } }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/typescript": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", @@ -6268,6 +7652,30 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.72.1.tgz", + "integrity": "sha512-3Li9FfPmQp45nxEtFD1ZCrYFI3cYK5kpyNtmm1H7lMagm8u9sy6PTjy3ytwQZs7KNukOyKFHAZ6z6GjYFeaeXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.72.1", + "@typescript-eslint/parser": "8.72.1", + "@typescript-eslint/typescript-estree": "8.72.1", + "@typescript-eslint/utils": "8.72.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, "node_modules/undici": { "version": "8.12.3", "resolved": "https://registry.npmjs.org/undici/-/undici-8.12.3.tgz", @@ -6278,6 +7686,13 @@ "node": ">=22.19.0" } }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "node_modules/update-browserslist-db": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", @@ -6309,6 +7724,16 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/validate-npm-package-name": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-8.0.0.tgz", @@ -6897,6 +8322,22 @@ "node": "^22.14.0 || >=24.0.0" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/why-is-node-running": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz", @@ -6910,6 +8351,16 @@ "node": ">=20.11" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "10.0.2", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-10.0.2.tgz", @@ -6961,6 +8412,23 @@ "node": ">=10" } }, + "node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yargs": { "version": "18.1.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.1.0.tgz", @@ -6989,6 +8457,19 @@ "node": "^20.19.0 || ^22.12.0 || >=23" } }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/yoctocolors": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz", diff --git a/agiledancers-fe/package.json b/agiledancers-fe/package.json index d82989e..93bd8b9 100644 --- a/agiledancers-fe/package.json +++ b/agiledancers-fe/package.json @@ -7,7 +7,10 @@ "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test --watch=false", - "e2e": "playwright test" + "e2e": "playwright test", + "lint": "eslint . --max-warnings 0", + "lint:fix": "eslint . --fix --max-warnings 0", + "prepare": "node tools/install-hooks.mjs" }, "private": true, "dependencies": { @@ -24,10 +27,19 @@ "@angular/build": "22.2.4", "@angular/cli": "22.2.4", "@angular/compiler-cli": "22.2.3", + "@eslint/js": "10.0.1", "@playwright/test": "1.64.1", + "@types/node": "22.20.5", + "angular-eslint": "22.6.0", + "eslint": "10.13.0", + "eslint-config-prettier": "10.1.8", + "globals": "17.13.0", + "husky": "9.1.7", "jsdom": "30.1.3", + "lint-staged": "17.6.0", "prettier": "3.9.10", "typescript": "6.0.3", + "typescript-eslint": "8.72.1", "vitest": "5.0.5" }, "engines": { diff --git a/agiledancers-fe/src/app/shared/school-transfer.spec.ts b/agiledancers-fe/src/app/shared/school-transfer.spec.ts index dedcb01..bebd7c5 100644 --- a/agiledancers-fe/src/app/shared/school-transfer.spec.ts +++ b/agiledancers-fe/src/app/shared/school-transfer.spec.ts @@ -16,7 +16,10 @@ describe('School JSON import', () => { afterEach(() => http.verify()); async function select() { const selected = component.select({ - target: { files: [{ text: async () => '{"formatVersion":1}' }], value: 'file.json' }, + target: { + files: [{ text: () => Promise.resolve('{"formatVersion":1}') }], + value: 'file.json', + }, } as unknown as Event); await Promise.resolve(); return { selected, request: http.expectOne('/api/dance-schools/import/preview') }; diff --git a/agiledancers-fe/tools/check-hook.mjs b/agiledancers-fe/tools/check-hook.mjs new file mode 100644 index 0000000..225f679 --- /dev/null +++ b/agiledancers-fe/tools/check-hook.mjs @@ -0,0 +1,245 @@ +/** Isolated hook acceptance checks and full-process timing; never stages this checkout. */ +import assert from 'node:assert/strict'; +import { execFileSync, spawnSync } from 'node:child_process'; +import { cpSync, mkdtempSync, readFileSync, writeFileSync, symlinkSync } from 'node:fs'; +import { tmpdir, cpus, platform, release } from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { performance } from 'node:perf_hooks'; + +const source = fileURLToPath(new URL('../../', import.meta.url)); +const root = mkdtempSync(path.join(tmpdir(), 's2-hook-acceptance-')); +const frontend = path.join(root, 'agiledancers-fe'); +const excluded = new Set([ + 'node_modules', + 'dist', + '.angular', + 'coverage', + 'test-results', + 'unit-results', + 'playwright-report', + '_', +]); +cpSync(path.join(source, 'agiledancers-fe'), frontend, { + recursive: true, + filter: (file) => !excluded.has(path.basename(file)), +}); +cpSync(path.join(source, '.husky'), path.join(root, '.husky'), { + recursive: true, + filter: (file) => !excluded.has(path.basename(file)), +}); +symlinkSync( + path.join(source, 'agiledancers-fe/node_modules'), + path.join(frontend, 'node_modules'), + 'dir', +); +const env = { ...process.env }; +delete env.CI; +delete env.HUSKY; +function run(command, args, cwd = root, environment = env) { + return execFileSync(command, args, { + cwd, + env: environment, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }).trimEnd(); +} +const git = (...args) => run('git', args); +const write = (file, content) => writeFileSync(path.join(root, file), content); +const read = (file) => readFileSync(path.join(root, file), 'utf8'); +const fixture = 'agiledancers-fe/src/hook-fixture.ts'; +const baseline = + 'export const hookValue = { value: 1 };\n\n// Independent edits below.\n\nexport const otherValue = 1;\n'; +write(fixture, baseline); +write('unrelated.txt', 'baseline\n'); +git('init', '-q'); +git('config', 'user.name', 'S2 validation'); +git('config', 'user.email', 's2@example.invalid'); +git('add', '.'); +git('-c', 'core.hooksPath=/dev/null', 'commit', '-qm', 'Fixture baseline'); +git('config', 'core.hooksPath', '/dev/null'); +run('node', ['tools/install-hooks.mjs'], frontend, { ...env, CI: 'true' }); +assert.equal(git('config', '--get', 'core.hooksPath'), '/dev/null'); +run('node', ['tools/install-hooks.mjs'], frontend, { ...env, HUSKY: '0' }); +assert.equal(git('config', '--get', 'core.hooksPath'), '/dev/null'); +console.log('PASS CI and HUSKY=0 skip hook installation'); +run('node', ['tools/install-hooks.mjs'], frontend); +assert.equal(git('config', '--get', 'core.hooksPath'), '.husky/_'); +const reset = () => git('reset', '--hard', '-q', 'HEAD'); +const snapshot = () => + JSON.stringify([ + git('diff', '--cached', '--binary'), + git('diff', '--binary'), + git('status', '--porcelain'), + git('stash', 'list'), + ]); +function hook() { + const start = performance.now(); + const result = spawnSync('sh', ['.husky/_/pre-commit'], { cwd: root, env, encoding: 'utf8' }); + return { + seconds: Number(((performance.now() - start) / 1000).toFixed(3)), + exit: result.status, + output: result.stdout + result.stderr, + }; +} +function passed(name) { + console.log(`PASS ${name}`); +} + +// Format only the index version while independently staged/unstaged edits survive. +write(fixture, baseline.replace('{ value: 1 }', '{value:2}')); +git('add', fixture); +write(fixture, read(fixture).replace('otherValue = 1', 'otherValue = 2')); +write('unrelated.txt', 'staged\n'); +git('add', 'unrelated.txt'); +write('unrelated.txt', 'staged\nunstaged\n'); +const result = hook(); +assert.equal(result.exit, 0, result.output); +assert.match(git('show', `:${fixture}`), /hookValue = \{ value: 2 \}/); +assert.match(git('show', `:${fixture}`), /otherValue = 1/); +assert.match(read(fixture), /otherValue = 2/); +assert.equal(git('show', ':unrelated.txt'), 'staged'); +assert.equal(read('unrelated.txt'), 'staged\nunstaged\n'); +assert.equal(git('stash', 'list'), ''); +passed('partially staged formatting preserves unrelated staged and unstaged changes'); +reset(); + +write('unrelated.txt', 'only backend/documentation\n'); +git('add', 'unrelated.txt'); +let before = snapshot(); +assert.equal(hook().exit, 0); +assert.equal(snapshot(), before); +passed('commits without frontend changes leave state unchanged'); +reset(); + +write('agiledancers-fe/package-lock.json', read('agiledancers-fe/package-lock.json') + '\n\n'); +git('add', 'agiledancers-fe/package-lock.json'); +before = snapshot(); +assert.equal(hook().exit, 0); +assert.equal(snapshot(), before); +passed('generated lockfile is excluded'); +reset(); + +write( + 'agiledancers-fe/src/app/app.ts', + read('agiledancers-fe/src/app/app.ts') + '\nexport const formatterCheck={value:2};\n', +); +write(fixture, 'export const broken = ;\n'); +git('add', 'agiledancers-fe/src/app/app.ts', fixture); +write('unrelated.txt', 'unstaged survives failures\n'); +before = snapshot(); +const failed = hook(); +assert.notEqual(failed.exit, 0); +assert.match(failed.output, /SyntaxError|Unexpected token/); +assert.equal(snapshot(), before); +passed('formatter failure restores index, worktree and stash'); +reset(); + +write(fixture, baseline.replace('{ value: 1 }', '{value:2}')); +git('add', fixture); +write(fixture, read(fixture).replace('{value:2}', '{value:3}')); +before = snapshot(); +const conflict = hook(); +assert.notEqual(conflict.exit, 0, conflict.output); +assert.match(conflict.output, /conflict|Failed to restore unstaged/i); +assert.equal(snapshot(), before); +passed('unstaged reconciliation conflict restores original state'); +reset(); + +// Benchmark the rejected combined candidate using the same hook and state protections. +const selectedConfig = read('agiledancers-fe/lint-staged.config.mjs'); +write( + 'agiledancers-fe/lint-staged.combined.mjs', + `import base from './lint-staged.config.mjs'; +export default Object.fromEntries(Object.entries(base).map(([pattern, task]) => [pattern, files => { + const commands = task(files); + const lintable = files.filter(file => /\\.(ts|js|mjs|cjs|html)$/.test(file)); + return commands.length && lintable.length + ? [...commands, 'eslint --max-warnings 0 ' + lintable.map(file => JSON.stringify(file)).join(' ')] : commands; +}]));\n`, +); +const selectedHook = read('.husky/pre-commit'); +write( + '.husky/pre-commit', + selectedHook.replace( + '--concurrent false', + '--concurrent false --config lint-staged.combined.mjs', + ), +); +write(fixture, baseline.replace('{ value: 1 }', '{value:2}')); +write( + 'agiledancers-fe/src/app/app.ts', + read('agiledancers-fe/src/app/app.ts') + '\nexport const badLint: any = 1;\n', +); +git('add', fixture, 'agiledancers-fe/src/app/app.ts'); +before = snapshot(); +const lintFailure = hook(); +assert.notEqual(lintFailure.exit, 0); +assert.match(lintFailure.output, /no-explicit-any/); +assert.equal(snapshot(), before); +passed('combined candidate restores formatting after ESLint failure'); +reset(); + +const all = git('ls-files', 'agiledancers-fe') + .split('\n') + .filter((file) => !file.endsWith('/package-lock.json')); +const scopes = { + single: ['agiledancers-fe/src/app/app.ts'], + mixed: [ + 'agiledancers-fe/src/app/app.ts', + 'agiledancers-fe/src/app/app.html', + 'agiledancers-fe/src/app/app.css', + 'agiledancers-fe/angular.json', + 'agiledancers-fe/eslint.config.mjs', + 'agiledancers-fe/e2e/school-management.spec.ts', + ], + all, +}; +const timings = []; +for (const mode of ['combined', 'prettier-only']) { + for (const [scope, files] of Object.entries(scopes)) { + for (let repeat = 1; repeat <= 3; repeat++) { + reset(); + write( + '.husky/pre-commit', + mode === 'combined' + ? selectedHook.replace( + '--concurrent false', + '--concurrent false --config lint-staged.combined.mjs', + ) + : selectedHook, + ); + for (const file of files) { + const addition = /\.(ts|js|mjs|cjs)$/.test(file) + ? '\n// Hook timing fixture.\n' + : file.endsWith('.html') + ? '\n\n' + : file.endsWith('.css') + ? '\n/* Hook timing fixture. */\n' + : '\n'; + write(file, read(file) + addition); + } + git('add', '--', ...files); + const measurement = hook(); + assert.equal(measurement.exit, 0, measurement.output); + timings.push({ mode, scope, repeat, files, seconds: measurement.seconds }); + console.log(`TIMING ${mode} ${scope} ${repeat}: ${measurement.seconds}s`); + } + } +} +assert.equal(read('agiledancers-fe/lint-staged.config.mjs').includes(selectedConfig.trim()), true); +const report = { + machine: cpus()[0].model, + system: `${platform()} ${release()}`, + node: process.version, + npm: run('npm', ['--version']), + root, + timings, +}; +writeFileSync(path.join(root, 'report.json'), JSON.stringify(report, null, 2) + '\n'); +console.log(`Evidence: ${path.join(root, 'report.json')}`); +const selected = timings.filter((result) => result.mode === 'prettier-only'); +assert.ok( + selected.every((result) => result.seconds <= 3), + 'Prettier-only exceeds the three-second budget; do not enable the hook.', +); diff --git a/agiledancers-fe/tools/install-hooks.mjs b/agiledancers-fe/tools/install-hooks.mjs new file mode 100644 index 0000000..ac874fa --- /dev/null +++ b/agiledancers-fe/tools/install-hooks.mjs @@ -0,0 +1,13 @@ +import { existsSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import husky from 'husky'; + +// Docker uses CI=true; CI and local opt-out must never change Git configuration. +if (!process.env.CI && process.env.HUSKY !== '0') { + const root = fileURLToPath(new URL('../../', import.meta.url)); + if (existsSync(new URL('../../.git', import.meta.url))) { + process.chdir(root); + const error = husky('.husky'); + if (error) throw new Error(error); + } +} diff --git a/agiledancers-fe/tsconfig.eslint.json b/agiledancers-fe/tsconfig.eslint.json new file mode 100644 index 0000000..53f3d76 --- /dev/null +++ b/agiledancers-fe/tsconfig.eslint.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { "types": ["vitest/globals", "node"], "noEmit": true }, + "include": ["src/**/*.ts", "e2e/**/*.ts", "*.ts"], + "exclude": [ + "node_modules", + "dist", + ".angular", + "coverage", + "test-results", + "unit-results", + "playwright-report" + ] +} diff --git a/ci/CiTests.java b/ci/CiTests.java index ae2a798..f1be880 100644 --- a/ci/CiTests.java +++ b/ci/CiTests.java @@ -102,6 +102,21 @@ class CiTests { equal(0L,f.calls("build")); equal(0L,f.calls("push")); } }); + test("Frontend lint runs after install and blocks tests and build on failure", () -> { + try (Fixture f = new Fixture()) { + f.fake.lintFailure = true; + try { f.pipeline.verifyFrontend(); throw new AssertionError("Expected failure"); } + catch (Commands.Failure error) { equal(1,error.exitCode); } + equal(List.of(List.of("npm", "ci"), List.of("npm", "run", "lint")), f.fake.calls); + } + }); + test("Frontend verification checks before testing and building", () -> { + try (Fixture f = new Fixture()) { + f.pipeline.verifyFrontend(); + equal(List.of("ci", "run lint", "test -- --reporters=default --reporters=junit --output-file=unit-results/junit.xml", "run build"), + f.fake.calls.stream().map(a -> String.join(" ", a.subList(1,a.size()))).toList()); + } + }); test("Process execution enforces its deadline", () -> { Path dir = Files.createTempDirectory("ci-process-test-"); try { @@ -135,13 +150,14 @@ class CiTests { final List> calls = new ArrayList<>(); final Map images = new HashMap<>(), tags = new HashMap<>(); String head = SHA, pushFailure = "", pullFailure = ""; - boolean manifestError, mavenFailure; + boolean manifestError, mavenFailure, lintFailure; boolean dockerAvailable = true; FakeCommands() { super(Duration.ofMinutes(1)); } @Override Result execute(List args, Path cwd, Map env, String input, boolean echo) { calls.add(List.copyOf(args)); if (args.getFirst().equals("git")) return new Result(0,SHA); if (args.getFirst().equals("mvn") && mavenFailure) return new Result(42,"deliberate failure"); + if (args.equals(List.of("npm", "run", "lint")) && lintFailure) return new Result(1,"deliberate lint failure"); if (!args.getFirst().equals("docker")) return new Result(0,""); String command = args.get(1), ref = args.getLast(); if (command.equals("info")) return new Result(dockerAvailable ? 0 : 1,args.contains("{{.Architecture}}") ? "amd64" : "linux"); diff --git a/ci/Pipeline.java b/ci/Pipeline.java index 7e212a3..f265fe8 100644 --- a/ci/Pipeline.java +++ b/ci/Pipeline.java @@ -69,10 +69,7 @@ class Pipeline { run("java", "-version"); run("node", "--version"); run("mvn", "-B", "-ntp", "clean", "verify"); CiTests.run(); Path frontend = root.resolve("agiledancers-fe"); - commands.execute(List.of("npm", "ci"), frontend, env, null, true).checked(); - commands.execute(List.of("npm", "test", "--", "--reporters=default", "--reporters=junit", - "--output-file=unit-results/junit.xml"), frontend, env, null, true).checked(); - commands.execute(List.of("npm", "run", "build"), frontend, env, null, true).checked(); + verifyFrontend(); String platform = env.get("TARGET_PLATFORM"); run("docker", "build", "--platform", platform, "--target", "prebuilt", "--label", "org.opencontainers.image.revision=" + commit, "-t", backendImage, "."); @@ -101,6 +98,14 @@ class Pipeline { } } } + void verifyFrontend() throws Exception { + Path frontend = root.resolve("agiledancers-fe"); + commands.execute(List.of("npm", "ci"), frontend, env, null, true).checked(); + commands.execute(List.of("npm", "run", "lint"), frontend, env, null, true).checked(); + commands.execute(List.of("npm", "test", "--", "--reporters=default", "--reporters=junit", + "--output-file=unit-results/junit.xml"), frontend, env, null, true).checked(); + commands.execute(List.of("npm", "run", "build"), frontend, env, null, true).checked(); + } void waitForApplication() throws Exception { for (int i = 0; i < 60; i++) { if (result(false,"docker", "exec", runName + "-frontend", "wget", "-q", "-O", "-", diff --git a/docs/s2-hook-timings.json b/docs/s2-hook-timings.json new file mode 100644 index 0000000..90ad326 --- /dev/null +++ b/docs/s2-hook-timings.json @@ -0,0 +1,184 @@ +{ + "date": "2026-10-29", + "machine": "AMD Ryzen 7 7730U with Radeon Graphics", + "system": "linux 7.0.0-38-generic", + "node": "v22.22.3", + "npm": "10.9.8", + "scopes": { + "single": [ + "agiledancers-fe/src/app/app.ts" + ], + "mixed": [ + "agiledancers-fe/src/app/app.ts", + "agiledancers-fe/src/app/app.html", + "agiledancers-fe/src/app/app.css", + "agiledancers-fe/angular.json", + "agiledancers-fe/eslint.config.mjs", + "agiledancers-fe/e2e/school-management.spec.ts" + ], + "all": [ + "agiledancers-fe/.dockerignore", + "agiledancers-fe/.editorconfig", + "agiledancers-fe/.gitignore", + "agiledancers-fe/.nvmrc", + "agiledancers-fe/.prettierignore", + "agiledancers-fe/.prettierrc", + "agiledancers-fe/.vscode/extensions.json", + "agiledancers-fe/.vscode/launch.json", + "agiledancers-fe/.vscode/tasks.json", + "agiledancers-fe/Dockerfile", + "agiledancers-fe/README.md", + "agiledancers-fe/angular.json", + "agiledancers-fe/e2e/school-management.spec.ts", + "agiledancers-fe/eslint.config.mjs", + "agiledancers-fe/lint-staged.config.mjs", + "agiledancers-fe/nginx/default.conf.template", + "agiledancers-fe/package.json", + "agiledancers-fe/playwright.config.ts", + "agiledancers-fe/proxy.conf.json", + "agiledancers-fe/public/favicon.svg", + "agiledancers-fe/src/app/app.config.ts", + "agiledancers-fe/src/app/app.css", + "agiledancers-fe/src/app/app.html", + "agiledancers-fe/src/app/app.routes.ts", + "agiledancers-fe/src/app/app.spec.ts", + "agiledancers-fe/src/app/app.ts", + "agiledancers-fe/src/app/core/api-error.ts", + "agiledancers-fe/src/app/core/api-models.ts", + "agiledancers-fe/src/app/core/school-api.spec.ts", + "agiledancers-fe/src/app/core/school-api.ts", + "agiledancers-fe/src/app/schools/school-page.html", + "agiledancers-fe/src/app/schools/school-page.ts", + "agiledancers-fe/src/app/schools/schools-page.html", + "agiledancers-fe/src/app/schools/schools-page.ts", + "agiledancers-fe/src/app/shared/confirm-dialog.ts", + "agiledancers-fe/src/app/shared/dialog-frame.ts", + "agiledancers-fe/src/app/shared/entity-editor.spec.ts", + "agiledancers-fe/src/app/shared/entity-editor.ts", + "agiledancers-fe/src/app/shared/group-members.spec.ts", + "agiledancers-fe/src/app/shared/group-members.ts", + "agiledancers-fe/src/app/shared/school-transfer.spec.ts", + "agiledancers-fe/src/app/shared/school-transfer.ts", + "agiledancers-fe/src/hook-fixture.ts", + "agiledancers-fe/src/index.html", + "agiledancers-fe/src/main.ts", + "agiledancers-fe/src/styles.css", + "agiledancers-fe/tools/check-hook.mjs", + "agiledancers-fe/tools/install-hooks.mjs", + "agiledancers-fe/tsconfig.app.json", + "agiledancers-fe/tsconfig.eslint.json", + "agiledancers-fe/tsconfig.json", + "agiledancers-fe/tsconfig.spec.json" + ] + }, + "timings": [ + { + "mode": "combined", + "scope": "single", + "repeat": 1, + "seconds": 2.875 + }, + { + "mode": "combined", + "scope": "single", + "repeat": 2, + "seconds": 2.776 + }, + { + "mode": "combined", + "scope": "single", + "repeat": 3, + "seconds": 2.894 + }, + { + "mode": "combined", + "scope": "mixed", + "repeat": 1, + "seconds": 3.779 + }, + { + "mode": "combined", + "scope": "mixed", + "repeat": 2, + "seconds": 4.099 + }, + { + "mode": "combined", + "scope": "mixed", + "repeat": 3, + "seconds": 4.478 + }, + { + "mode": "combined", + "scope": "all", + "repeat": 1, + "seconds": 5.916 + }, + { + "mode": "combined", + "scope": "all", + "repeat": 2, + "seconds": 6.137 + }, + { + "mode": "combined", + "scope": "all", + "repeat": 3, + "seconds": 4.728 + }, + { + "mode": "prettier-only", + "scope": "single", + "repeat": 1, + "seconds": 0.313 + }, + { + "mode": "prettier-only", + "scope": "single", + "repeat": 2, + "seconds": 0.338 + }, + { + "mode": "prettier-only", + "scope": "single", + "repeat": 3, + "seconds": 0.401 + }, + { + "mode": "prettier-only", + "scope": "mixed", + "repeat": 1, + "seconds": 0.648 + }, + { + "mode": "prettier-only", + "scope": "mixed", + "repeat": 2, + "seconds": 0.72 + }, + { + "mode": "prettier-only", + "scope": "mixed", + "repeat": 3, + "seconds": 0.647 + }, + { + "mode": "prettier-only", + "scope": "all", + "repeat": 1, + "seconds": 1.573 + }, + { + "mode": "prettier-only", + "scope": "all", + "repeat": 2, + "seconds": 1.441 + }, + { + "mode": "prettier-only", + "scope": "all", + "repeat": 3, + "seconds": 1.276 + } + ] +} diff --git a/docs/s2-validation.md b/docs/s2-validation.md new file mode 100644 index 0000000..7b2a6df --- /dev/null +++ b/docs/s2-validation.md @@ -0,0 +1,126 @@ +# S2 implementation evidence + +Validated on the development mini-PC on 2026-10-29: AMD Ryzen 7 7730U (8 cores, +16 threads), Ubuntu 26.04.1 LTS, Linux 7.0.0-38-generic x86_64, approximately +27 GiB RAM, Node v22.22.3, npm 10.9.8. The standalone Docker build uses the +existing Node 24.15.0 image. + +## Lint scope and rules + +ESLint 10.13.0, typescript-eslint 8.72.1 and angular-eslint 22.6.0 support the +existing Angular 22 and TypeScript 6.0.3 versions. Framework versions are unchanged. +The flat configuration applies recommended JavaScript, recommended type-checked +TypeScript and recommended Angular rules, plus the recommended template rules +for both inline and external templates. No rule suppressions were needed. +`eslint-config-prettier` prevents conflicts with the existing formatting settings. + +`tsconfig.eslint.json` supplies one explicit type-checking project covering +application files, unit tests, Playwright tests and root TypeScript configuration. +This includes E2E and Playwright configuration outside Angular's app/test +compilation projects, without changing the application compilation scope. +Node types resolve `process.env` in those files. The existing test findings were +fixed by treating Playwright response JSON as unknown before checking its shape, +and returning an explicit resolved promise from the fake file reader. + +Injected probes confirmed `no-floating-promises` for application, unit-test, +Playwright and configuration TypeScript; `no-unused-vars` for JavaScript; +and `template/no-negated-async` in both external and inline Angular templates. +Generated files are excluded. A warning-only CLI probe returned exit status 1 +with `--max-warnings 0`. + +## Complete hook measurements + +Run from the repository root, with dependencies installed: + +```sh +node agiledancers-fe/tools/check-hook.mjs +``` + +This creates an isolated temporary Git repository from the current frontend and +hook sources, links the installed dependency directory, makes an initial fixture +commit and installs the real Husky wrapper. The real checkout and its index are +not staged or committed by the checks. The temporary repository is retained and +its report path is printed so failures can be inspected. + +Each timed invocation runs `sh .husky/_/pre-commit` in a new process. Timing +starts before Husky and includes file selection, lint-staged backup, hiding +unstaged edits, fresh tool startup, formatting/checking, restaging and cleanup. +No ESLint or Prettier cache is enabled. The final measurement ran while the +standalone Docker build was also running; all selected-hook runs still passed. Three fresh executions per scenario and +candidate are recorded; there is no runtime timeout interrupting writers. + +The combined candidate uses the same hook, adding type-aware ESLint after +Prettier. The selected configuration uses only Prettier. Staged scopes are: + +- Single: `src/app/app.ts`. +- Mixed: application TS, HTML and CSS, `angular.json`, `eslint.config.mjs` and + the Playwright spec (six files). +- All: all 52 maintained frontend files, including non-applicable files that the + hook must skip, plus a small TS preservation-test fixture; generated lockfile + excluded. Exact paths and raw measurements are in + [the timing report](s2-hook-timings.json). + +| Candidate | Staged scope | Run 1 | Run 2 | Run 3 | +| --- | --- | --- | --- | --- | +| combined | single | 2.875 s | 2.776 s | 2.894 s | +| combined | mixed | 3.779 s | 4.099 s | 4.478 s | +| combined | all | 5.916 s | 6.137 s | 4.728 s | +| prettier-only | single | 0.313 s | 0.338 s | 0.401 s | +| prettier-only | mixed | 0.648 s | 0.720 s | 0.647 s | +| prettier-only | all | 1.573 s | 1.441 s | 1.276 s | + +The combined candidate exceeds three seconds, so it is rejected. Every measured +Prettier-only execution is below three seconds, so that hook is installed. These +are measurements on this mini-PC, not a guarantee for other machines or future +repository sizes. Rerun the checks when the tooling or scope changes. + +## Preservation and installation checks + +The isolated checks assert all of the following against the real hook, comparing +both cached and unstaged binary diffs, status and stash state where appropriate: + +- Partially staged formatting updates only the intended index version and + retains independent unstaged changes in the same file. +- An unrelated file with both staged and unstaged edits remains intact. +- A commit without frontend changes leaves the state unchanged. +- `package-lock.json` is not formatted or restaged. +- A formatter syntax error after another file has been formatted aborts and + restores the original index and worktree. +- A conflict restoring an unstaged change on the formatted line aborts and + restores the original index and worktree. +- The rejected combined candidate also restores all changes when ESLint fails + after successful formatting. +- No backup stash remains after successful recovery. +- `CI=true` and `HUSKY=0` leave an existing hooks configuration untouched; + normal local installation selects `.husky/_`. + +A real local `npm ci` successfully installed `.husky/_` in this checkout. +Docker sets `CI=true` for installation and copies the prepare entry point before +`npm ci`, so the lifecycle script can safely skip hook installation. The +standalone build runs lint, tests and production compilation after copying sources. + +## Build and pipeline verification + +The full Java-based pipeline was executed locally without invoking publication: + +```sh +JAVA_HOME=/usr/lib/jvm/java-25-openjdk-amd64 \ +PATH=/usr/lib/jvm/java-25-openjdk-amd64/bin:/home/wanja-krah/.nvm/versions/node/v22.22.3/bin:/usr/local/bin:/usr/bin:/bin \ +TARGET_PLATFORM=linux/amd64 CI_RUN_ID=s2-local CI_RUN_ATTEMPT=2 \ +java ci/Ci.java verify +``` + +Results: 20 backend tests, 17 CI-helper tests, clean frontend lint, 14 frontend +unit tests, production build, both application images and both container E2E +tests passed. Diagnostics remain in the ignored `ci/artifacts/` directory. +A Gitea workflow run for these uncommitted changes has not been executed. + +The CI-helper regression checks assert that lint immediately follows `npm ci` +and precedes tests/build; a lint failure stops frontend verification with its +nonzero status. Existing failed-verification and publication guards ensure only +a completely verified commit can publish. The shared command runner writes lint +output to the job log and retained `verify.log`. CI never invokes `lint:fix`. + +`npm run lint:fix` followed by `npm run lint` also passed without further changes. +The standalone Docker build was checked separately with an isolated Docker +configuration to avoid the desktop credential helper; no images were published. diff --git a/specs/refined/S2_implement-eslint-for-fe b/specs/in_review/S2_implement-eslint-for-fe similarity index 87% rename from specs/refined/S2_implement-eslint-for-fe rename to specs/in_review/S2_implement-eslint-for-fe index eef877f..b37bf46 100644 --- a/specs/refined/S2_implement-eslint-for-fe +++ b/specs/in_review/S2_implement-eslint-for-fe @@ -1,6 +1,6 @@ # S2 — ESLint for the frontend -Status: REFINED +Status: IN_REVIEW Agreed after the grill-me review; shared understanding confirmed. @@ -183,36 +183,43 @@ consistent. Record the performance evidence with the implementation documentatio ## Acceptance criteria -- [ ] Compatible tooling checks maintained application TypeScript/JavaScript, +- [x] Compatible tooling checks maintained application TypeScript/JavaScript, Angular templates, unit tests, Playwright tests and applicable frontend configuration using the agreed recommended and type-aware rules. -- [ ] All existing violations are fixed; there is no legacy baseline allowance. +- [x] All existing violations are fixed; there is no legacy baseline allowance. Frontend builds, unit tests and container end-to-end tests still pass. -- [ ] `npm run lint` checks the full scope without modifications and fails on +- [x] `npm run lint` checks the full scope without modifications and fails on either errors or warnings. -- [ ] `npm run lint:fix` offers explicit local autofixes; CI does not modify source. -- [ ] Formatting remains under Prettier, without conflicting ESLint rules. -- [ ] Any necessary rule suppressions are narrow and concretely justified. -- [ ] Every branch pipeline runs lint immediately after frontend `npm ci` and +- [x] `npm run lint:fix` offers explicit local autofixes; CI does not modify source. +- [x] Formatting remains under Prettier, without conflicting ESLint rules. +- [x] Any necessary rule suppressions are narrow and concretely justified. +- [x] Every branch pipeline runs lint immediately after frontend `npm ci` and before frontend tests/builds and container E2E; lint failure prevents publication and leaves actionable diagnostic output. -- [ ] VS Code recommends ESLint without mandatory fix/format-on-save settings. -- [ ] Complete-hook measurements cover the agreed single-file, mixed-file and +- [x] VS Code recommends ESLint without mandatory fix/format-on-save settings. +- [x] Complete-hook measurements cover the agreed single-file, mixed-file and all-maintained-files cases with fresh processes and no tool cache. -- [ ] The measured three-second budget determines combined tools, Prettier-only +- [x] The measured three-second budget determines combined tools, Prettier-only or no active hook; no writing formatter is forcibly interrupted to meet it. -- [ ] If installed, the hook processes only applicable staged frontend files, +- [x] If installed, the hook processes only applicable staged frontend files, applies Prettier formatting before any ESLint check, and preserves unrelated files and unstaged edits, including partially staged files. -- [ ] A failed installed hook restores the original staged and unstaged state and +- [x] A failed installed hook restores the original staged and unstaged state and blocks the commit with a concrete explanation. -- [ ] Hook installation is automatic for local `npm ci` when applicable, skipped +- [x] Hook installation is automatic for local `npm ci` when applicable, skipped in CI/Docker, and has documented local opt-out instructions. -- [ ] Root `README.md` explains `npm run lint`, `npm run lint:fix`, CI enforcement +- [x] Root `README.md` explains `npm run lint`, `npm run lint:fix`, CI enforcement and the selected hook behavior; measurement evidence is included. ## Implementation handoff -This specification records agreed behavior. It does not install ESLint, change -application code, modify the pipeline, install a hook or update command usage in -the README yet. Those changes and the timing measurements are implementation work. +The agreed behavior has been implemented and is ready for review. See the +implementation evidence below. + +## Implementation evidence + +Implemented with ESLint flat configuration, type-aware TypeScript and Angular +template checks. The measured three-second budget selects a Prettier-only +Husky/lint-staged hook; ESLint remains mandatory in the Java-based pipeline. +See [validation and timings](../../docs/s2-validation.md) for scope, preservation +checks, local installation and successful builds/unit/container E2E tests.