diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6249de341..9b8bbd9b4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -355,6 +355,14 @@ jobs: # unknown, passthrough, any) in shared's request schemas. run: cd shared && npm run contracts:open + - name: Unused code and dependencies check + # knip (knip.jsonc) over client, server and shared: unused files, + # exports, types and dependencies per file may not rise above + # scripts/ci/knip-baseline.json (scripts/ci/knip-ratchet.mjs). Here + # rather than in client-quality because it needs every workspace's + # dependencies, which only this job installs. + run: npm run lint:knip + client-quality: name: Client Types & Lint needs: changes diff --git a/CLAUDE.md b/CLAUDE.md index 67e55294d..0a0ddcda0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,6 +22,7 @@ npm run dev # build shared, then watch shared + server + c npm run build # build shared → server → client (order matters) npm run lint # lint all three workspaces, check-only (npm run lint:fix applies the fixes in shared and server) npm run format # prettier --write all three +npm run lint:knip # knip over client/server/shared (knip.jsonc): unused files, exports, types, deps may only shrink against scripts/ci/knip-baseline.json (CI gate; --update lowers it) ``` Tests: @@ -133,7 +134,7 @@ These principles come out of a full-repo audit and shape all new code: ## Quality gates in CI -`.github/workflows/test.yml` runs, per package: typecheck (server also `typecheck:tests`, client also the strict-mode ratchet `lint:strict`), lint (client and server through `lint:warnings`, which also holds the warning count per rule), `lint:pages`, `lint:rtl`, `lint:size` (client and server), the server's `lint:strict`, `lint:boundaries`, `lint:tx`, `lint:dialect`, `lint:test-sql`, `lint:test-mocks`, `lint:test-new-service`, `lint:query-api`, `lint:mcp-zod`, `lint:service-http` and `lint:response-contracts`, the client's and server's `lint:format`, `lint:layers`, `lint:offline`, `lint:dup`, `lint:pairs`, `lint:skips`, `lint:i18n-keys` and `theme:lint`, shared's `contracts:open`, `check:plugin-facts`, `i18n:parity:strict` (with the untranslated-strings ratchet), the S3 contract suite, the Postgres probe (`postgres-probe`: every dialect helper on SQLite and Postgres, and the statements every repository method reaches with sample arguments against a Postgres service; the SQL errors per method and the list of methods it cannot measure are held to a baseline that only shrinks and fails while it is still unmeasured; see `server/CLAUDE.md`), the browser end-to-end flows (Chromium and WebKit), a client build with `check:gl-split`, coverage for all four packages, then a SonarCloud scan. It also builds the Docker image and boots it until `/api/health/ready` answers (`docker-image`), and lints and renders the Helm chart against the Kubernetes schemas and runs `docker compose config` on every compose file (`deploy-manifests`). `lint-prettier.yml` additionally runs `lint` + `lint:format` on `shared/`, both check-only. `plugin-sdk` has no lint script and is outside the eslint gates. **Not in CI**: the screenshot and help-media Playwright projects; run them locally. There are no git hooks; nothing runs before a commit except you. Other workflows build Docker images, scan them, and publish `trek-plugin-sdk` on `plugin-sdk-v*` tags. +`.github/workflows/test.yml` runs, per package: typecheck (server also `typecheck:tests`, client also the strict-mode ratchet `lint:strict`), lint (client and server through `lint:warnings`, which also holds the warning count per rule), `lint:pages`, `lint:rtl`, `lint:size` (client and server), the server's `lint:strict`, `lint:boundaries`, `lint:tx`, `lint:dialect`, `lint:test-sql`, `lint:test-mocks`, `lint:test-new-service`, `lint:query-api`, `lint:mcp-zod`, `lint:service-http` and `lint:response-contracts`, the client's and server's `lint:format`, `lint:layers`, `lint:offline`, `lint:dup`, `lint:pairs`, `lint:skips`, `lint:i18n-keys` and `theme:lint`, shared's `contracts:open`, the repo-wide `lint:knip` (in server-quality), `check:plugin-facts`, `i18n:parity:strict` (with the untranslated-strings ratchet), the S3 contract suite, the Postgres probe (`postgres-probe`: every dialect helper on SQLite and Postgres, and the statements every repository method reaches with sample arguments against a Postgres service; the SQL errors per method and the list of methods it cannot measure are held to a baseline that only shrinks and fails while it is still unmeasured; see `server/CLAUDE.md`), the browser end-to-end flows (Chromium and WebKit), a client build with `check:gl-split`, coverage for all four packages, then a SonarCloud scan. It also builds the Docker image and boots it until `/api/health/ready` answers (`docker-image`), and lints and renders the Helm chart against the Kubernetes schemas and runs `docker compose config` on every compose file (`deploy-manifests`). `lint-prettier.yml` additionally runs `lint` + `lint:format` on `shared/`, both check-only. `plugin-sdk` has no lint script and is outside the eslint gates. **Not in CI**: the screenshot and help-media Playwright projects; run them locally. There are no git hooks; nothing runs before a commit except you. Other workflows build Docker images, scan them, and publish `trek-plugin-sdk` on `plugin-sdk-v*` tags. - **`ci-ok` is the required check.** The branch ruleset requires `ci-ok` (plus `lint` and `check-target` from their own workflows), and `ci-ok` needs every job above except the scan. It fails if any of them failed or was cancelled and passes if they passed or were skipped. A new gate job counts only once it is listed in `ci-ok`'s `needs:`. - **Guides name paths that exist**: the `changes` job runs `scripts/ci/doc-paths.mjs`, which resolves every backtick path in the CLAUDE.md files, `README.md`, `server/src/nest/README.md` and the wiki against the tracked files; the missing ones are listed in `scripts/ci/doc-paths-baseline.json`, which only shrinks (`--update` after a fix). diff --git a/knip.jsonc b/knip.jsonc new file mode 100644 index 000000000..c79a82181 --- /dev/null +++ b/knip.jsonc @@ -0,0 +1,70 @@ +{ + // knip over the three workspaces: unused files, exports, types and + // dependencies. scripts/ci/knip-ratchet.mjs (npm run lint:knip) holds what it + // finds today in scripts/ci/knip-baseline.json and fails on anything new. + // A false positive is fixed here, with the reason, never in the baseline. + "$schema": "https://unpkg.com/knip@6/schema.json", + // plugin-sdk is published on its own with its own lockfile and is not a + // root workspace; its test files would otherwise read as unlisted vitest. + "ignore": ["plugin-sdk/**"], + // test.yml runs `cd server && node --require tsconfig-paths/register`; knip + // resolves the workflow from the root, where server's dependency is not. + "ignoreUnresolved": ["tsconfig-paths/register"], + "workspaces": { + ".": { + "entry": ["scripts/ci/*.mjs", "scripts/*.{cjs,mts}"], + "project": ["scripts/**/*.{mjs,cjs,mts}"], + // An optional peer of tsdown, which loads shared/tsdown.config.ts through + // it; nothing imports it by name. + "ignoreDependencies": ["unrun"] + }, + "client": { + // The app, its scripts, the Playwright specs and the server launcher + // playwright.config.ts starts (`node e2e/server-launch.mjs`), and the tests. + "entry": [ + "src/main.tsx", + "scripts/*.mjs", + "e2e/**/*.{ts,mjs}", + "src/**/*.test.{ts,tsx}", + "tests/**/*.{ts,tsx}" + ], + "project": ["src/**/*.{ts,tsx}", "scripts/**/*.mjs", "tests/**/*.{ts,tsx}", "e2e/**/*.{ts,mjs}"] + }, + "server": { + "entry": [ + "src/index.ts", + "src/bootstrap.ts", + // Read by the MikroORM CLI and at boot, by path. + "src/mikro-orm.config.ts", + // Started with child_process.fork() by the plugin supervisor (nest/plugins/paths.ts). + "src/nest/plugins/runtime/plugin-host-entry.ts", + // The plugin-author-facing API the forked child exposes; its exported + // types are the contract for plugin code outside this repository. + "src/nest/plugins/runtime/plugin-sdk.ts", + // Found by the migrator and the seeder through a glob, never imported. + "src/db/migrations/*.ts", + "src/db/seeders/*.ts", + "scripts/**/*.{mjs,cjs,ts}", + "reset-admin.js", + "tests/**/*.ts" + ], + "project": ["src/**/*.ts", "scripts/**/*.{mjs,cjs,ts}", "tests/**/*.ts"], + // The MikroORM command line (npx mikro-orm), configured by the + // "mikro-orm" block in server/package.json and never imported. + "ignoreDependencies": ["@mikro-orm/cli"] + }, + "shared": { + "entry": [ + // The package entry points tsdown builds, one per locale for the lazy chunks. + "src/index.ts", + "src/roadtrip/planning.ts", + "src/i18n/index.ts", + "src/i18n/*/index.ts", + // open-request-schemas.d.mts types the .mjs script beside it. + "scripts/*.{mjs,d.mts}", + "src/**/*.spec.ts" + ], + "project": ["src/**/*.ts", "scripts/**/*.{mjs,mts}"] + } + } +} diff --git a/package-lock.json b/package-lock.json index c7c414a27..e84b24115 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ ], "devDependencies": { "concurrently": "^10.0.4", + "knip": "^6.38.0", "unrun": "^0.3.1" }, "engines": { @@ -5612,22 +5613,25 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", - "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.5.tgz", + "integrity": "sha512-HshSvXzmBxNzqejd3k/KemgQ3hdREUYFRRrgxOZ+gPljDIsO2SDZrVZMd4Z/HsxudQbE62OVyVFE8z9BCurfjA==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.2" + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/Brooooooklyn" }, "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, "node_modules/@nestjs/common": { @@ -6336,6 +6340,329 @@ "@otplib/plugin-thirty-two": "^12.0.1" } }, + "node_modules/@oxc-parser/binding-android-arm-eabi": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.150.0.tgz", + "integrity": "sha512-oQef2Zu4Prz1KLKznz3HqZzU9uVoA5PMoDZuuLmqms7hKmKSAPzlaMnLllJq3t+rgKmfJJ2siPrpZfFrW06btw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.150.0.tgz", + "integrity": "sha512-B6ofpoFiAUwIZ0MJ2IgHPvZK8FAtL4qzSZRwOkAKIfxEobE1mQC8nDdiFZj7pUaJiVUVCsfkMsBJKedzO8rZvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.150.0.tgz", + "integrity": "sha512-J+9IHKzx/bSz1JetOfD4zKXSK9sOm4/a7+0qomJODcTL6JsRXGeV/ZdkAPSIDydfFmWzYCraMlQEosSZEyBYkQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.150.0.tgz", + "integrity": "sha512-v6IPfcAcSYrBWXV5Tce1DmxDMXLhEYpIIWiRFPJopgCVscZdLaV4MRQOUxvL3usQlw+yrwYOjBFB9IwBx+jUiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.150.0.tgz", + "integrity": "sha512-AoR/4jD02HET0KO0yNT4nbTE+XJUxiO9jB1X/ycEjUE3WrIJ3IjV/Vf+gskhWLcqqeXfvNnkDtBR7epllAm88A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.150.0.tgz", + "integrity": "sha512-A/hycCFjLUrCmLoL5O/vBp40ohlaXO1Ta3v5hqYZxicYFs129wZmgZJggcW4mYGYbZebQLhup+N8JjeQl8qwyw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.150.0.tgz", + "integrity": "sha512-pbqahg1Pkz7J4RKmXm30s/iQu99hv64ayXCu8P4p75HcEH5azOdR4eGqiB6lFGkFR3mMpzaYsSKLkS8oJbjmeQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.150.0.tgz", + "integrity": "sha512-HV11aRbBQGwqv8bEo+K/6qr88uB4fEe1mhXncMhlVCrj+WpBSraxrUzHaPVmeQRU6x6x8v/3DuCbbo93rpp+fw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.150.0.tgz", + "integrity": "sha512-k6pVkJqALwtEuP4zukVhGRhdIy4+ofChUIUUsAHHyqDZlNsknmel3JjbggrJiWGaes6h/dIcWmEXsKW4SmK9oQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.150.0.tgz", + "integrity": "sha512-tEFg39mw/rHO5n8GK2DR4ZMFfsPQZtnQIPbsIpjoQRomJc/2tRfsCSmCT6gNit+NZGoeJG5aH9ATDH5bf0WnoQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.150.0.tgz", + "integrity": "sha512-0JO7IFkoek6HqV589Menx3hJySNXi6quRhO4tq2P7kpEHKEXoDATnoPVUpn5B7gDH2KLkdo751N0uIrGJFCTCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-musl": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.150.0.tgz", + "integrity": "sha512-7XPzREnyAS5wHU9aB+49Uaqgoo1gVCklHc3DRlc3fJy2tXD/eAJFN1QFz/crj+Ulup5P1+axNREF+/RGaB/IRA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.150.0.tgz", + "integrity": "sha512-7n7ZxcbRWDFaTdyj8M8p0O9OfzoMKm8O6syBAIgcSutbOALq0Bb9G52Me+XH0anMLZV+NgXc726gqgG2q39vcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.150.0.tgz", + "integrity": "sha512-Vx0GSA9ZCRTUiczoEQnIIyXkMvtEY8uc6IiwLQtMe5ci2sXPqjrry0Ek5fsPP1k2kCzkML3ow9UOOEgnEDi7Bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.150.0.tgz", + "integrity": "sha512-ii4/9m3viDLssMnfXU7/Pni/3nYplApuGayceX4qsVGaqqQJCx3tHIH9M/HWIeSty5i8LjS21zPYT6lVIkwLxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-openharmony-arm64": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.150.0.tgz", + "integrity": "sha512-ZtoxX5rez36ZWkwtiEhjkCPnlTPoQ2I7lIL0IYZ1yjxMYohbvoOjBmUIZzrf9W/HouONq0omIfTeCWEY+R380w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.150.0.tgz", + "integrity": "sha512-VqeRb5JX/bKYBrff7TUDvJyKY0914UzuCkuXIGxJS4FUmvMNfqkCbc7Sq90iMz9DlmAtlggwaBYQ9eg3h3ltiw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-ia32-msvc": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.150.0.tgz", + "integrity": "sha512-EPqJfeZ4Pgg2BJSsCV8GozxV0FDltRzpVtGVa1r2noJu1iu+opOw3gtBWXwIo9odCT/MdFfyHxdy0/CRqs3OqA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.150.0.tgz", + "integrity": "sha512-n5YMzbqwPQqozbkrexi0lNZrUz5cnPHalYpFWe6Dau7AmKIWNo+y24IwzDuZEQtEdUHuKhXmMMih/MPjOI+CMw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/@oxc-project/types": { "version": "0.137.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.137.0.tgz", @@ -6346,6 +6673,300 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@oxc-resolver/binding-android-arm-eabi": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.24.2.tgz", + "integrity": "sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-android-arm64": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.24.2.tgz", + "integrity": "sha512-cl4icWaZFnLdg8m6qtnh5rBMuGbxc/ptStFHLeCNwr+2cZjkjNwQu/jYRS0CHlnPecOJMpuS5M6/BH+0J/YkEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-arm64": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.24.2.tgz", + "integrity": "sha512-At29QEMF6HajbQvgY8K6OXnHD1x9rad74xBEfmCB6ZqCGsdq75aK7tOYcTbOanMy8qdIBrfL3SMr3p/lfSlb9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-x64": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.24.2.tgz", + "integrity": "sha512-A5Kqr1EUj4oIL5CF4WRssq/o5P0Y11cwoFouMRmQ7YnC/A8V93nv1nb7aSU8HwcgmXropjLNkVTl4MN87cu28Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-freebsd-x64": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.24.2.tgz", + "integrity": "sha512-R5xkRBRRz7ceH/P5Jrc6G7FmdUdgpLYyESFAUDVTNQ9K0sGPxcp4ljiwEwEqsvNcQ4sYbMRrWcHHBCu7ksAJVw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.24.2.tgz", + "integrity": "sha512-k/RuYL4L/R58IBn3wT5ma3Wh4k62bp1eYCFRWCmMsasUOqL+H6sW0VGFadEzKWXFFlz+2uIMoeMk9ySSZJHgbg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-musleabihf": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.24.2.tgz", + "integrity": "sha512-bnHAak3ujYfH5pKk4NieFNbvYvernfoQDgwLddbZ3OtMYrem87/qjlA+u+aKG0oZcqSLGCful/6/CEA+aeAgaA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-gnu": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.24.2.tgz", + "integrity": "sha512-vDT3KHgzYp47gmtNOqL2VNhCyl5Zv643eyxm//A68J8DeUGXrvD1pZFiaT4jSfe+RInfnn1R2yVHye4enx6RnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-musl": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.24.2.tgz", + "integrity": "sha512-+kMlQvbzfyEYtu5FcjE4p+ttBLpKW4d/AsAsuE69BxV6V4twZJeIQZFfD8gh/wqglY0MkPSezWXQH0jBV13MUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-ppc64-gnu": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.24.2.tgz", + "integrity": "sha512-shjfMhmZ3gq9fv/w7bi3PnZlgOPG+2QAOFf0BJF0EgBSIGZ6PMLN2zbGEblTUYB/NKVDRyYhE2ff3dJ1QqNPkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-gnu": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.24.2.tgz", + "integrity": "sha512-zGelwFR5oRo+b69k8Lrzun86DyUHzfKN6cnjbR9l7Z7NIRznOE/2ZvPa1IUKqAL2PzAXOdwkfVqNvO1H2RlpAw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-musl": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.24.2.tgz", + "integrity": "sha512-qxZ1SWCXJY0eyhAlP6Lmo9F2Nrtx7EkYj9oCgL8apDPCwXwCEDA2U697bbT81JIc2IrVjxO4KX6WU2N+oN9Z4w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-s390x-gnu": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.24.2.tgz", + "integrity": "sha512-sGCecF3cx2DFlH4t/z7ApnOnXqN48p5p5mlHDEnHTAukQa2P+qMVE4CwyWE9W+q/m3QJ7kKfGrIjax31f44oFQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-gnu": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.24.2.tgz", + "integrity": "sha512-k/VlMMcSzMlahb3/fENM4rTlsJ0s3fFROA0KXPBmKggqmTSaE383sl8F3KCOXPLmVsYfW6hCitMhXCEtNeZxxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-musl": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.24.2.tgz", + "integrity": "sha512-8hbnZyNi97b/8wapYaIF9+t9GmZKBW2vunaOc3h9HGJptH7b7XpvZqOTBSm/MpTjr7H497BlgOaSfLUdhmy2bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-openharmony-arm64": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.24.2.tgz", + "integrity": "sha512-MvyGik3a6pVgZ0t/kWlbmFxFLmXQJwgLsY2eYFHLpy0wGwRbfzeIGgDwQ3kXqE30z+kSXennRkCrT7TUvkptNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.24.2.tgz", + "integrity": "sha512-vHcssMPwO08RTvj/c0iOBz90attxyG3wQJ0dTcyEQK43LRpcdLWZlV5feBhv6Isn6ahbQIzHbCgfa81+RiML0Q==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.2", + "@emnapi/runtime": "1.11.2", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz", + "integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@oxc-resolver/binding-win32-arm64-msvc": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.24.2.tgz", + "integrity": "sha512-uokJqro2iBqkFvJdKQLP7d8/BUmFwESQFVmIJUQKj1Xn1a/LysJoe1vmeECLF5b3jsV8CAL5sEMJXX6SdK9Nhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxc-resolver/binding-win32-x64-msvc": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.24.2.tgz", + "integrity": "sha512-UqGPmo56KDfLlfXFAFIrNflHT8tFxWGEivWg3Zeyp4Uy2NlKN1FGPr6/BxcLGG3+kZ6Wp14g5Uj+n71boqZfiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@paralleldrive/cuid2": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", @@ -7012,7 +7633,8 @@ "optional": true, "os": [ "android" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-android-arm64": { "version": "4.62.0", @@ -7026,7 +7648,8 @@ "optional": true, "os": [ "android" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.62.0", @@ -7040,7 +7663,8 @@ "optional": true, "os": [ "darwin" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-darwin-x64": { "version": "4.62.0", @@ -7054,7 +7678,8 @@ "optional": true, "os": [ "darwin" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-freebsd-arm64": { "version": "4.62.0", @@ -7068,7 +7693,8 @@ "optional": true, "os": [ "freebsd" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-freebsd-x64": { "version": "4.62.0", @@ -7082,7 +7708,8 @@ "optional": true, "os": [ "freebsd" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { "version": "4.62.0", @@ -7096,7 +7723,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { "version": "4.62.0", @@ -7110,7 +7738,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-arm64-gnu": { "version": "4.62.0", @@ -7124,7 +7753,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-arm64-musl": { "version": "4.62.0", @@ -7151,7 +7781,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-loong64-musl": { "version": "4.62.0", @@ -7165,7 +7796,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { "version": "4.62.0", @@ -7179,7 +7811,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-ppc64-musl": { "version": "4.62.0", @@ -7193,7 +7826,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { "version": "4.62.0", @@ -7207,7 +7841,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-riscv64-musl": { "version": "4.62.0", @@ -7221,7 +7856,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-s390x-gnu": { "version": "4.62.0", @@ -7235,7 +7871,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.62.0", @@ -7249,7 +7886,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-x64-musl": { "version": "4.62.0", @@ -7276,7 +7914,8 @@ "optional": true, "os": [ "openbsd" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-openharmony-arm64": { "version": "4.62.0", @@ -7290,7 +7929,8 @@ "optional": true, "os": [ "openharmony" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-win32-arm64-msvc": { "version": "4.62.0", @@ -7304,7 +7944,8 @@ "optional": true, "os": [ "win32" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-win32-ia32-msvc": { "version": "4.62.0", @@ -7318,7 +7959,8 @@ "optional": true, "os": [ "win32" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-win32-x64-gnu": { "version": "4.62.0", @@ -7332,7 +7974,8 @@ "optional": true, "os": [ "win32" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-win32-x64-msvc": { "version": "4.62.0", @@ -7346,7 +7989,8 @@ "optional": true, "os": [ "win32" - ] + ], + "peer": true }, "node_modules/@scarf/scarf": { "version": "1.4.0", @@ -7923,9 +8567,9 @@ } }, "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz", + "integrity": "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==", "dev": true, "license": "MIT", "optional": true, @@ -12108,6 +12752,16 @@ "reusify": "^1.0.4" } }, + "node_modules/fd-package-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fd-package-json/-/fd-package-json-2.0.0.tgz", + "integrity": "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "walk-up-path": "^4.0.0" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -12327,6 +12981,23 @@ "node": ">= 6" } }, + "node_modules/formatly": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/formatly/-/formatly-0.7.1.tgz", + "integrity": "sha512-ESuCEJE0SGgabl9q+zABI8LhJDmF+8nhpuqDzuM48HfJ9QTD5o6PqFJDFZY7w6f8xsh5lixuj5nJsQNvYOWvKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fd-package-json": "^2.0.0", + "package-manager-detector": "^1.8.0" + }, + "bin": { + "formatly": "bin/index.mjs" + }, + "engines": { + "node": ">=18.3.0" + } + }, "node_modules/formidable": { "version": "3.5.4", "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", @@ -12558,6 +13229,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.3.tgz", + "integrity": "sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/gifwrap": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/gifwrap/-/gifwrap-0.10.1.tgz", @@ -14275,6 +14959,86 @@ "json-buffer": "3.0.1" } }, + "node_modules/knip": { + "version": "6.38.0", + "resolved": "https://registry.npmjs.org/knip/-/knip-6.38.0.tgz", + "integrity": "sha512-umytiMCaZf02MV9SOO1FEDgbDf6h20VZk3ruJE0kh52kkGjYZFRUAXHJWYoLhvhoHGINO/7GXVJmQ2vxTBvl6A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/webpro" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/knip" + } + ], + "license": "ISC", + "dependencies": { + "fdir": "^6.5.0", + "formatly": "^0.7.0", + "get-tsconfig": "4.14.3", + "jiti": "^2.7.0", + "oxc-parser": "^0.150.0", + "oxc-resolver": "11.24.2", + "picomatch": "^4.0.7", + "smol-toml": "^1.8.0", + "strip-json-comments": "5.0.3", + "tinyglobby": "^0.2.17", + "unbash": "^4.0.11", + "yaml": "^2.9.1", + "zod": "^4.4.3" + }, + "bin": { + "knip": "bin/knip.js", + "knip-bun": "bin/knip-bun.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/knip/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/knip/node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/knip/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/kysely": { "version": "0.29.6", "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.29.6.tgz", @@ -16464,6 +17228,84 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/oxc-parser": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.150.0.tgz", + "integrity": "sha512-zwajKw1GUa57IOdafIS7/yOObGF2NihwIfZfs5yJTRprMSkiwhWOc/k+n8bwnUPbHr2/XsjfBO8KyTPaW3VpWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "^0.150.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + }, + "optionalDependencies": { + "@oxc-parser/binding-android-arm-eabi": "0.150.0", + "@oxc-parser/binding-android-arm64": "0.150.0", + "@oxc-parser/binding-darwin-arm64": "0.150.0", + "@oxc-parser/binding-darwin-x64": "0.150.0", + "@oxc-parser/binding-freebsd-x64": "0.150.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.150.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.150.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.150.0", + "@oxc-parser/binding-linux-arm64-musl": "0.150.0", + "@oxc-parser/binding-linux-ppc64-gnu": "0.150.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.150.0", + "@oxc-parser/binding-linux-riscv64-musl": "0.150.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.150.0", + "@oxc-parser/binding-linux-x64-gnu": "0.150.0", + "@oxc-parser/binding-linux-x64-musl": "0.150.0", + "@oxc-parser/binding-openharmony-arm64": "0.150.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.150.0", + "@oxc-parser/binding-win32-ia32-msvc": "0.150.0", + "@oxc-parser/binding-win32-x64-msvc": "0.150.0" + } + }, + "node_modules/oxc-parser/node_modules/@oxc-project/types": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.150.0.tgz", + "integrity": "sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/oxc-resolver": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.24.2.tgz", + "integrity": "sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-resolver/binding-android-arm-eabi": "11.24.2", + "@oxc-resolver/binding-android-arm64": "11.24.2", + "@oxc-resolver/binding-darwin-arm64": "11.24.2", + "@oxc-resolver/binding-darwin-x64": "11.24.2", + "@oxc-resolver/binding-freebsd-x64": "11.24.2", + "@oxc-resolver/binding-linux-arm-gnueabihf": "11.24.2", + "@oxc-resolver/binding-linux-arm-musleabihf": "11.24.2", + "@oxc-resolver/binding-linux-arm64-gnu": "11.24.2", + "@oxc-resolver/binding-linux-arm64-musl": "11.24.2", + "@oxc-resolver/binding-linux-ppc64-gnu": "11.24.2", + "@oxc-resolver/binding-linux-riscv64-gnu": "11.24.2", + "@oxc-resolver/binding-linux-riscv64-musl": "11.24.2", + "@oxc-resolver/binding-linux-s390x-gnu": "11.24.2", + "@oxc-resolver/binding-linux-x64-gnu": "11.24.2", + "@oxc-resolver/binding-linux-x64-musl": "11.24.2", + "@oxc-resolver/binding-openharmony-arm64": "11.24.2", + "@oxc-resolver/binding-wasm32-wasi": "11.24.2", + "@oxc-resolver/binding-win32-arm64-msvc": "11.24.2", + "@oxc-resolver/binding-win32-x64-msvc": "11.24.2" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -16512,6 +17354,13 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/package-manager-detector": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.9.0.tgz", + "integrity": "sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA==", + "dev": true, + "license": "MIT" + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -19022,6 +19871,19 @@ "node": ">=20.0.0" } }, + "node_modules/smol-toml": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.9.1.tgz", + "integrity": "sha512-j7WyPdf0CJoutMzMoPLF/bVXHtKZIT9Rb4eGeEbWw8X2t5oirjPgpL+OpaENZrILYaXqYwnVH+qgUwVbl+pUJQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, "node_modules/source-map": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0.tgz", @@ -19344,6 +20206,19 @@ "node": ">=8" } }, + "node_modules/strip-json-comments": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strnum": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.2.tgz", @@ -20177,6 +21052,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/unbash": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/unbash/-/unbash-4.0.12.tgz", + "integrity": "sha512-PnKvRX+Fbxl/usVlTr/31tXIOVAH+8OdC9qaxUQcvurwEnoc3Vk/ipi2Ce8IjMxzK9byfSGdsUBjqqJGc1o5lw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -21189,6 +22074,16 @@ "node": ">=18" } }, + "node_modules/walk-up-path": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz", + "integrity": "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/webidl-conversions": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", @@ -21811,6 +22706,22 @@ "dev": true, "license": "ISC" }, + "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", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yargs": { "version": "18.0.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", diff --git a/package.json b/package.json index 048e4ab46..e7d391275 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "test:e2e": "npm run test:e2e --workspace=server", "test:ci-scripts": "node --test scripts/ci/*.test.mjs", "test:lib-scripts": "node --test scripts/lib/*.test.mjs", + "lint:knip": "node scripts/ci/knip-ratchet.mjs", "lint": "npm run lint --workspace=shared && npm run lint --workspace=server && npm run lint --workspace=client", "lint:fix": "npm run lint:fix --workspace=shared && npm run lint:fix --workspace=server", "format": "npm run format --workspace=shared && npm run format --workspace=server && npm run format --workspace=client", @@ -32,6 +33,7 @@ }, "devDependencies": { "concurrently": "^10.0.4", + "knip": "^6.38.0", "unrun": "^0.3.1" }, "comment:scarfSettings": "swagger-ui-dist ships @scarf/scarf install-time analytics; TREK sends no telemetry, so it stays off.", diff --git a/scripts/ci/changed-areas.mjs b/scripts/ci/changed-areas.mjs index 0b7858f41..386ebea96 100644 --- a/scripts/ci/changed-areas.mjs +++ b/scripts/ci/changed-areas.mjs @@ -49,6 +49,8 @@ export const AREAS = { 'package-lock.json', '.nvmrc', 'sonar-project.properties', + // lint:knip in server-quality reads it. + 'knip.jsonc', 'Dockerfile', ...READ_BY_TESTS, ...ALWAYS, diff --git a/scripts/ci/changed-areas.test.mjs b/scripts/ci/changed-areas.test.mjs index 6d1d2ce08..48f6f66d2 100644 --- a/scripts/ci/changed-areas.test.mjs +++ b/scripts/ci/changed-areas.test.mjs @@ -52,6 +52,10 @@ describe('classify', () => { assert.deepEqual(classify(['scripts/lib/ratchet.mjs']), { code: true, image: false, deploy: false }); }); + it('runs the tests but not the image for a knip config change', () => { + assert.deepEqual(classify(['knip.jsonc']), { code: true, image: false, deploy: false }); + }); + it('runs the image but not the tests for a .dockerignore change', () => { assert.deepEqual(classify(['.dockerignore']), { code: false, image: true, deploy: false }); }); diff --git a/scripts/ci/knip-baseline.json b/scripts/ci/knip-baseline.json new file mode 100644 index 000000000..a9e2ab069 --- /dev/null +++ b/scripts/ci/knip-baseline.json @@ -0,0 +1,249 @@ +{ + "devDependencies": { + "server/package.json": 3 + }, + "duplicates": { + "client/src/api/client.ts": 1, + "client/src/components/Map/VectorBasemap.tsx": 1, + "client/src/components/shared/Skeleton.tsx": 1, + "client/src/components/shared/SlidingTabs.tsx": 1, + "client/src/components/shared/Toast.tsx": 1, + "client/src/components/shared/Tooltip.tsx": 1, + "shared/src/docsync/docsync.schema.ts": 1, + "shared/src/tours/tours.schema.ts": 1 + }, + "exports": { + "client/scripts/lib/dup.mjs": 6, + "client/scripts/lib/eslint-warnings.mjs": 1, + "client/scripts/lib/format.mjs": 3, + "client/scripts/lib/offline.mjs": 1, + "client/scripts/lib/pairs.mjs": 1, + "client/scripts/lib/rtl.mjs": 2, + "client/scripts/lib/size.mjs": 3, + "client/scripts/lib/skips.mjs": 2, + "client/scripts/lib/strict.mjs": 1, + "client/scripts/lib/theme.mjs": 4, + "client/src/api/pluginPois.ts": 1, + "client/src/components/Admin/storage/storageModel.ts": 1, + "client/src/components/Admin/storage/useStorageAdmin.ts": 1, + "client/src/components/Budget/BudgetPanel.constants.ts": 1, + "client/src/components/Budget/BudgetPanel.helpers.ts": 5, + "client/src/components/Collab/useChatImages.ts": 2, + "client/src/components/Files/docsync/useConnectForm.ts": 2, + "client/src/components/Journey/journeyPhotoLayer.ts": 1, + "client/src/components/Journey/moodConfig.ts": 1, + "client/src/components/Journey/photoZoom.ts": 2, + "client/src/components/Layout/navItems.ts": 1, + "client/src/components/Map/MapViewGL.tsx": 1, + "client/src/components/Map/glProviders.ts": 2, + "client/src/components/Map/poiClusters.ts": 2, + "client/src/components/Packing/packingListPanel.helpers.ts": 1, + "client/src/components/Planner/DayPlanSidebar.constants.ts": 2, + "client/src/components/Planner/PlaceFormModal.helpers.ts": 1, + "client/src/components/Planner/transportEndpoints.ts": 1, + "client/src/components/Roadtrip/corridor.ts": 2, + "client/src/components/Roadtrip/dayWindow.ts": 1, + "client/src/components/Roadtrip/followTrack.ts": 1, + "client/src/components/Roadtrip/roadtripModel.ts": 6, + "client/src/components/Roadtrip/stopKinds.ts": 1, + "client/src/components/Roadtrip/useFollowTrack.ts": 2, + "client/src/components/Roadtrip/useHotelBookends.ts": 1, + "client/src/components/Studio/countryShapes.ts": 1, + "client/src/components/Tours/planner/useTourPlanner.ts": 3, + "client/src/components/shared/ErrorBoundary.tsx": 1, + "client/src/components/shared/Toast.tsx": 1, + "client/src/constants/mapDefaults.ts": 4, + "client/src/help/registry.ts": 1, + "client/src/i18n/supportedLanguages.ts": 1, + "client/src/mobile/screens/trip/roadtrip/useMRoadtrip.ts": 2, + "client/src/pages/dashboard/useTripSearch.ts": 1, + "client/src/pages/journeyDetail/JourneyDetailPage.helpers.ts": 3, + "client/src/pages/journeyStudio/useJourneyStudio.ts": 2, + "client/src/pages/sharedTrip/SharedPlaceDetails.tsx": 1, + "client/src/push/webPush.ts": 2, + "client/src/repo/docSyncRepo.ts": 1, + "client/src/utils/dayImpactLines.ts": 1, + "client/src/utils/dayMerge.ts": 1, + "client/src/utils/flightLegs.ts": 3, + "client/src/utils/formatters.ts": 1, + "client/src/utils/geo.ts": 1, + "client/src/utils/placeSource.ts": 1, + "client/src/utils/placesSession.ts": 1, + "client/src/vacay/yearWindow.ts": 1, + "server/src/app-config/boot-derive.ts": 1, + "server/src/app-config/derive.ts": 2, + "server/src/app-config/index.ts": 24, + "server/src/app-config/parsers.ts": 1, + "server/src/db/dialect/platform.ts": 1, + "server/src/mcp/index.ts": 2, + "server/src/mcp/nest-mcp-policy.ts": 1, + "server/src/mcp/scopes.ts": 3, + "server/src/mcp/tools/_shared.ts": 8, + "server/src/nest-mcp/index.ts": 1, + "server/src/nest/accommodations/night-seat.ts": 1, + "server/src/nest/app-config/index.ts": 6, + "server/src/nest/auth/auth.helpers.ts": 1, + "server/src/nest/auth/public.decorator.ts": 1, + "server/src/nest/backup/backup-archive.ts": 1, + "server/src/nest/backup/backup.impl.ts": 3, + "server/src/nest/budget/budget.service.ts": 1, + "server/src/nest/common/validate-managed-routes.ts": 1, + "server/src/nest/day-notes/day-notes.service.ts": 1, + "server/src/nest/doc-sync/providers/paperless.client.ts": 7, + "server/src/nest/doc-sync/providers/paperless.provider.ts": 1, + "server/src/nest/doc-sync/providers/papra.client.ts": 2, + "server/src/nest/doc-sync/providers/synology-drive.client.ts": 1, + "server/src/nest/doc-sync/providers/webdav.client.ts": 2, + "server/src/nest/doc-sync/providers/webdav.provider.ts": 1, + "server/src/nest/files/files.constants.ts": 1, + "server/src/nest/integrations/airtrail.mapper.ts": 1, + "server/src/nest/llm-parse/image-input.ts": 2, + "server/src/nest/llm-parse/llm-config.ts": 1, + "server/src/nest/llm-parse/llm-prompt.ts": 1, + "server/src/nest/maps/maps.service.ts": 2, + "server/src/nest/maps/providers/amap.provider.ts": 2, + "server/src/nest/maps/providers/places-provider.ts": 1, + "server/src/nest/notifications/channel-registry.ts": 1, + "server/src/nest/notifications/push/push-subscription.helpers.ts": 2, + "server/src/nest/notifications/transports/ntfy.service.ts": 1, + "server/src/nest/oauth/oauth.helpers.ts": 1, + "server/src/nest/place-enrichment/place-enrichment.service.ts": 1, + "server/src/nest/place-shadow/place-shadow.service.ts": 1, + "server/src/nest/places/kml-import.helpers.ts": 1, + "server/src/nest/places/places.helpers.ts": 1, + "server/src/nest/plugins/contributions/plugin-search.helpers.ts": 2, + "server/src/nest/plugins/host/plugin-data.service.ts": 1, + "server/src/nest/plugins/host/plugin-jobs.ts": 1, + "server/src/nest/plugins/mcp-tool-schema.ts": 5, + "server/src/nest/plugins/protocol/envelope.ts": 1, + "server/src/nest/public-api/public-api-request.ts": 2, + "server/src/nest/roadtrip/charging.helpers.ts": 1, + "server/src/nest/route-usage/route-usage.service.ts": 1, + "server/src/nest/share/share.service.ts": 1, + "server/src/nest/storage/storage-stats.service.ts": 1, + "server/src/nest/transit/transit-provider.ts": 2, + "shared/src/roadtrip/accessSpur.ts": 3 + }, + "types": { + "client/src/components/Admin/storage/BackendForm.tsx": 1, + "client/src/components/Admin/storage/storageModel.ts": 1, + "client/src/components/Admin/storage/useStorageAdmin.ts": 1, + "client/src/components/Budget/CostsToolbar.tsx": 1, + "client/src/components/Budget/costsModel.ts": 2, + "client/src/components/Collab/CollabChat.types.ts": 1, + "client/src/components/Collab/collabModel.ts": 2, + "client/src/components/Collections/useCollectionPlaceForm.ts": 1, + "client/src/components/Collections/useTripCopyPicker.ts": 1, + "client/src/components/Files/docsync/useDocSync.ts": 1, + "client/src/components/Journey/JourneyEntryCover.tsx": 1, + "client/src/components/Journey/JourneyMap.tsx": 2, + "client/src/components/Map/mapViewContract.ts": 1, + "client/src/components/Map/useTripRouteOverview.ts": 2, + "client/src/components/Planner/PlaceFormModal.tsx": 1, + "client/src/components/Planner/placeOpenState.ts": 1, + "client/src/components/Planner/transportEndpoints.ts": 1, + "client/src/components/Planner/useTripExport.ts": 1, + "client/src/components/Plugins/PluginFrame.tsx": 1, + "client/src/components/Roadtrip/alternativePins.ts": 1, + "client/src/components/Roadtrip/carrierRide.ts": 1, + "client/src/components/Roadtrip/corridor.ts": 2, + "client/src/components/Roadtrip/dayWindow.ts": 3, + "client/src/components/Roadtrip/followTrack.ts": 2, + "client/src/components/Roadtrip/manualStop.ts": 2, + "client/src/components/Roadtrip/nightSpill.ts": 1, + "client/src/components/Roadtrip/refuelSuggestion.ts": 1, + "client/src/components/Roadtrip/roadtripModel.ts": 4, + "client/src/components/Roadtrip/useFollowTrack.ts": 1, + "client/src/components/Roadtrip/useRoadtripRoutes.ts": 3, + "client/src/components/Studio/StudioCanvas.tsx": 1, + "client/src/components/Studio/grids.ts": 1, + "client/src/components/Studio/mapTiles.ts": 1, + "client/src/components/Studio/templates.ts": 1, + "client/src/components/Trips/useTripShare.ts": 3, + "client/src/components/shared/DialogShell.tsx": 1, + "client/src/components/shared/SlidingTabs.tsx": 1, + "client/src/components/shared/elevationProfileModel.ts": 1, + "client/src/db/offlineDb.ts": 1, + "client/src/help/defineScreen.ts": 1, + "client/src/help/types.ts": 1, + "client/src/hooks/useDawarichConnection.ts": 1, + "client/src/hooks/useDawarichSuggestions.ts": 1, + "client/src/i18n/supportedLanguages.ts": 1, + "client/src/mobile/components/MSegmented.tsx": 1, + "client/src/mobile/components/MSheet.tsx": 2, + "client/src/mobile/screens/collections/MCollLabelsSheet.tsx": 1, + "client/src/mobile/screens/journey/MJourneyCreateSheet.tsx": 1, + "client/src/mobile/screens/settings/MSetPickerSheet.tsx": 1, + "client/src/mobile/screens/settings/MSettingsUi.tsx": 1, + "client/src/mobile/screens/trip/MTripShell.tsx": 1, + "client/src/mobile/screens/trip/plan/planTimelineModel.ts": 1, + "client/src/mobile/screens/trip/tabs/transportsModel.ts": 1, + "client/src/mobile/screens/vacay/useMVacay.ts": 1, + "client/src/pages/dashboard/dashboardModel.ts": 2, + "client/src/pages/journeyPublic/journeyPublicModel.ts": 1, + "client/src/pages/tripPlanner/plannerTypes.ts": 1, + "client/src/pages/tripPlanner/useBookingEdits.ts": 1, + "client/src/pages/tripPlanner/useDayPlanEdits.ts": 1, + "client/src/pages/tripPlanner/usePlaceFormOpeners.ts": 1, + "client/src/pages/tripPlanner/useRoadtripAlternatives.ts": 1, + "client/src/pages/tripPlanner/useRoadtripStopEdits.ts": 1, + "client/src/push/webPush.ts": 1, + "client/src/store/backgroundTasksStore.ts": 1, + "client/src/store/helpStore.ts": 2, + "client/src/theme/applyAppearance.ts": 1, + "client/src/types.ts": 7, + "client/src/utils/connectionsVisibility.ts": 1, + "client/src/utils/dayDeleteImpact.ts": 3, + "client/src/utils/journeyLifecycle.ts": 1, + "client/src/utils/tripRangeImpact.ts": 1, + "server/src/app-config/index.ts": 2, + "server/src/db/repositories/AuditLog.repository.ts": 1, + "server/src/db/repositories/BudgetSettlements.repository.ts": 1, + "server/src/db/repositories/DocumentProviderFields.repository.ts": 1, + "server/src/db/repositories/GoogleApiUsage.repository.ts": 1, + "server/src/db/repositories/McpTokens.repository.ts": 1, + "server/src/db/repositories/PasswordResetTokens.repository.ts": 1, + "server/src/db/repositories/Places.repository.ts": 1, + "server/src/db/repositories/UserSessions.repository.ts": 1, + "server/src/db/repositories/_shared/rows.ts": 1, + "server/src/mcp/nest-mcp-policy.ts": 1, + "server/src/nest-mcp/index.ts": 13, + "server/src/nest-mcp/types.ts": 4, + "server/src/nest/booking-import/kitinerary.types.ts": 6, + "server/src/nest/collab/collab.service.ts": 1, + "server/src/nest/files/files.service.ts": 1, + "server/src/nest/integrations/airtrail.mapper.ts": 1, + "server/src/nest/llm-parse/llm-config.ts": 1, + "server/src/nest/maps/maps.helpers.ts": 2, + "server/src/nest/maps/providers/places-provider.ts": 1, + "server/src/nest/maps/trek-places.client.ts": 1, + "server/src/nest/memories/memories.helpers.ts": 1, + "server/src/nest/notifications/notifications.service.ts": 1, + "server/src/nest/places/places.helpers.ts": 1, + "server/src/nest/plugins/contributions/map-layers.controller.ts": 1, + "server/src/nest/plugins/host/rpc-kit/types.ts": 2, + "server/src/nest/plugins/install/manifest.ts": 7, + "server/src/nest/plugins/plugins.service.ts": 1, + "server/src/nest/plugins/protocol/envelope.ts": 2, + "server/src/nest/storage/drivers/mirror.driver.ts": 1, + "server/src/nest/storage/storage-registry.service.ts": 1, + "server/src/nest/tours/tours.helpers.ts": 1, + "server/src/nest/trips/trips.service.ts": 3, + "server/src/nest/unsplash/unsplash.service.ts": 1, + "server/src/nest/vacay/vacay.service.ts": 4, + "server/src/nest/weather/weather.impl.ts": 1, + "server/src/shutdown.ts": 1, + "server/src/systemNotices/types.ts": 7, + "server/src/types.ts": 14 + }, + "unlisted": { + "client/src/components/Map/MapView.tsx": 1, + "client/src/components/Map/hazardPopup.ts": 1, + "client/src/components/Map/mapZoomCeiling.test.ts": 1, + "client/src/components/Map/markerCluster.test.ts": 1, + "client/src/pages/sharedTrip/SharedMap.tsx": 1, + "server/tests/unit/eslint-rules/no-promise-as-value.test.ts": 1, + "server/tests/unit/eslint-rules/restricted-syntax-selectors.test.ts": 1, + "shared/src/i18n/tours-locales.spec.ts": 1 + } +} diff --git a/scripts/ci/knip-ratchet.mjs b/scripts/ci/knip-ratchet.mjs new file mode 100644 index 000000000..1c599870d --- /dev/null +++ b/scripts/ci/knip-ratchet.mjs @@ -0,0 +1,27 @@ +#!/usr/bin/env node +/* + * lint:knip: unused files, exports, types and dependencies may only go down. + * + * knip (configured in knip.jsonc at the repo root) follows the imports of the + * client, server and shared workspaces from their entry points and reports + * what nothing reaches: a file, an export, a type, a dependency in a + * package.json, an import of a package no package.json lists. Dead code + * costs every sweep that edits it and every reader who wonders who calls it. + * The findings per category and file are held in scripts/ci/knip-baseline.json: + * a file may not gain one, and an entry above what knip finds now, or for a + * file that is gone, fails until --update lowers it. A false positive (a + * file loaded by path, a public API surface) is fixed in knip.jsonc, with the + * reason, never by raising the baseline. + * + * npm run lint:knip check against the baseline (CI) + * npm run lint:knip -- --update lower the baseline to today's findings; + * it never raises an entry + * + * The comparison lives in scripts/ci/lib/knip.mjs. + */ +import { fileURLToPath } from 'node:url'; +import { runCli } from '../../client/scripts/lib/ratchet.mjs'; +import { check } from './lib/knip.mjs'; + +const root = fileURLToPath(new URL('../..', import.meta.url)); +await runCli('lint:knip', (args) => check({ root, update: args.includes('--update') })); diff --git a/scripts/ci/knip-ratchet.test.mjs b/scripts/ci/knip-ratchet.test.mjs new file mode 100644 index 000000000..3897d0a1d --- /dev/null +++ b/scripts/ci/knip-ratchet.test.mjs @@ -0,0 +1,86 @@ +import assert from 'node:assert/strict'; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { after, describe, it } from 'node:test'; +import { RatchetError } from '../../client/scripts/lib/ratchet.mjs'; +import { categoryCounts, check, tally } from './lib/knip.mjs'; + +const scratch = mkdtempSync(join(tmpdir(), 'knip-ratchet-')); +after(() => rmSync(scratch, { recursive: true, force: true })); + +const REPORT = { + issues: [ + { + file: 'client/src/a.ts', + exports: [{ name: 'unusedA' }, { name: 'unusedB' }], + types: [{ name: 'OldType' }], + files: [], + }, + { file: 'server/package.json', devDependencies: [{ name: 'nodemon' }] }, + { file: 'client/src/dead.ts', files: [{ name: 'client/src/dead.ts' }] }, + { file: 'client/src/toast.tsx', duplicates: [[{ name: 'useToast' }, { name: 'default' }]] }, + ], +}; + +let n = 0; +function run(baseline, report = REPORT, update = false) { + const path = join(scratch, `baseline-${n++}.json`); + writeFileSync(path, JSON.stringify(baseline)); + const out = { log: [], error: [] }; + const code = check({ + root: scratch, + baselinePath: path, + update, + knip: () => report, + log: (l) => out.log.push(l), + error: (l) => out.error.push(l), + }); + return { code, out, path }; +} + +const FULL = { + exports: { 'client/src/a.ts': 2 }, + types: { 'client/src/a.ts': 1 }, + devDependencies: { 'server/package.json': 1 }, + files: { 'client/src/dead.ts': 1 }, + duplicates: { 'client/src/toast.tsx': 1 }, +}; + +describe('lint:knip', () => { + it('KNIP-001: counts each category per file with the names behind it', () => { + const { counts, names } = tally(REPORT); + assert.deepEqual(counts.exports, { 'client/src/a.ts': 2 }); + assert.deepEqual(counts.files, { 'client/src/dead.ts': 1 }); + assert.deepEqual(names.duplicates['client/src/toast.tsx'], ['useToast = default']); + assert.deepEqual(counts.dependencies, {}); + }); + + it('KNIP-002: passes findings within the baseline', () => { + const { code, out } = run(FULL); + assert.equal(code, 0, out.error.join('\n')); + assert.match(out.log.join('\n'), /knip: 6 finding\(s\), baseline allows 6/); + }); + + it('KNIP-003: fails a new finding, by category, and names it', () => { + const { code, out } = run({ ...FULL, exports: { 'client/src/a.ts': 1 } }); + assert.equal(code, 1); + assert.match(out.error.join('\n'), /FAIL {2}exports: client\/src\/a\.ts has 2, baseline 1: unusedA, unusedB/); + }); + + it('KNIP-004: fails an entry above its count until --update lowers it, and never raises one', () => { + const { code, out } = run({ ...FULL, types: { 'client/src/a.ts': 3, 'client/src/gone.ts': 1 } }); + assert.equal(code, 1); + assert.match(out.error.join('\n'), /types: client\/src\/a\.ts is held at 3 in scripts\/ci\/knip-baseline\.json/); + const updated = run({ ...FULL, types: { 'client/src/a.ts': 3, 'client/src/gone.ts': 1 }, exports: {} }, REPORT, true); + const written = JSON.parse(readFileSync(updated.path, 'utf8')); + assert.deepEqual(written.types, { 'client/src/a.ts': 1 }); + assert.equal(written.exports, undefined); + }); + + it('KNIP-005: a broken baseline or a report without issues stops the check', () => { + assert.match(categoryCounts({ nonsense: {} }), /not one of/); + assert.throws(() => run({ exports: { a: 0 } }), RatchetError); + assert.throws(() => run(FULL, { files: [] }), /no issues array/); + }); +}); diff --git a/scripts/ci/lib/knip.mjs b/scripts/ci/lib/knip.mjs new file mode 100644 index 000000000..2d1c6b0bc --- /dev/null +++ b/scripts/ci/lib/knip.mjs @@ -0,0 +1,163 @@ +/* + * The tally and the comparison behind lint:knip (scripts/ci/knip-ratchet.mjs). + * + * Built on the client's ratchet helpers, which hold the baseline rules every + * ratchet in the repo shares: fail closed on a broken baseline, lower only, + * fail on an entry above its count. + */ +import { spawnSync } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; +import { + countMap, + lowerCounts, + RatchetError, + readBaseline, + reportStale, + staleCounts, + writeBaseline, +} from '../../../client/scripts/lib/ratchet.mjs'; + +/** + * The kinds of finding knip reports per file, as its JSON reporter names them. + * Each is held apart in the baseline, so an unused export removed does not pay + * for an unused dependency added. + */ +const CATEGORIES = [ + 'files', + 'dependencies', + 'devDependencies', + 'optionalPeerDependencies', + 'unlisted', + 'binaries', + 'unresolved', + 'exports', + 'types', + 'enumMembers', + 'namespaceMembers', + 'duplicates', + 'catalog', +]; + +/** A baseline: each category an object of file → positive count. */ +export function categoryCounts(value) { + if (!value || typeof value !== 'object' || Array.isArray(value)) return 'it is not an object of category → file counts'; + for (const [category, counts] of Object.entries(value)) { + if (!CATEGORIES.includes(category)) return `"${category}" is not one of ${CATEGORIES.join(', ')}`; + const problem = countMap(counts); + if (problem) return `in "${category}", ${problem}`; + } + return null; +} + +const entries = (value) => (Array.isArray(value) ? value : value && typeof value === 'object' ? Object.values(value) : []); +const nameOf = (item) => + Array.isArray(item) ? item.map(nameOf).join(' = ') : typeof item === 'string' ? item : (item?.name ?? '?'); + +/** + * knip's JSON report as counts per category and file, with the names behind + * each count. A report knip did not produce (no `issues` array) is an error of + * the check, not an empty result. + */ +export function tally(report) { + if (!report || !Array.isArray(report.issues)) throw new RatchetError('knip printed no issues array; see its output above'); + const counts = Object.fromEntries(CATEGORIES.map((c) => [c, {}])); + const names = Object.fromEntries(CATEGORIES.map((c) => [c, {}])); + for (const issue of report.issues) { + for (const category of CATEGORIES) { + const found = entries(issue[category]); + if (!found.length) continue; + counts[category][issue.file] = found.length; + names[category][issue.file] = found.map(nameOf); + } + } + return { counts, names }; +} + +/** knip over the repository, as its parsed JSON report. */ +function runKnip(root) { + // knip is a root devDependency, so npm installs it at the top of node_modules. + const bin = join(root, 'node_modules', 'knip', 'bin', 'knip.js'); + if (!existsSync(bin)) throw new RatchetError(`knip is not installed at ${bin}; run npm ci at the repo root`); + const result = spawnSync(process.execPath, [bin, '--reporter', 'json', '--no-exit-code', '--no-progress'], { + cwd: root, + encoding: 'utf8', + maxBuffer: 256 * 1024 * 1024, + }); + if (result.error) throw new RatchetError(`knip did not run: ${result.error.message}`); + if (result.status !== 0) throw new RatchetError(`knip stopped with exit code ${result.status}: ${result.stderr.slice(0, 800)}`); + try { + return JSON.parse(result.stdout); + } catch { + throw new RatchetError(`knip's output is not JSON: ${result.stdout.slice(0, 300)} ${result.stderr.slice(0, 300)}`); + } +} + +/** The baseline with each category's files in order too, so a diff of it shows only what changed. */ +const sortedFiles = (baseline) => + Object.fromEntries( + Object.entries(baseline).map(([category, counts]) => [ + category, + Object.fromEntries(Object.entries(counts).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))), + ]) + ); + +const sum = (counts) => Object.values(counts).reduce((a, b) => a + b, 0); + +/** + * Runs knip (or knip, in a test) and compares its findings with the baseline + * at baselinePath. Returns the exit code. + */ +export function check({ + root, + baselinePath = join(root, 'scripts/ci/knip-baseline.json'), + update = false, + knip = runKnip, + log = console.log, + error = console.error, +}) { + let baseline = readBaseline(baselinePath, categoryCounts); + const { counts, names } = tally(knip(root)); + + if (update) { + baseline = Object.fromEntries( + Object.entries(baseline) + .map(([category, allowed]) => [category, lowerCounts(allowed, counts[category] ?? {})]) + .filter(([, allowed]) => Object.keys(allowed).length) + ); + writeBaseline(baselinePath, sortedFiles(baseline)); + } + + let grown = 0; + let stale = 0; + for (const category of CATEGORIES) { + const allowed = baseline[category] ?? {}; + for (const [file, n] of Object.entries(counts[category])) { + if (n <= (allowed[file] ?? 0)) continue; + grown++; + error( + `FAIL ${category}: ${file} has ${n}, baseline ${allowed[file] ?? 0}: ${names[category][file].slice(0, 8).join(', ')}` + ); + } + const fallen = staleCounts(allowed, counts[category]); + reportStale(fallen, { + file: 'ci/knip-baseline.json', + command: 'lint:knip', + label: (file) => `${category}: ${file}`, + error, + }); + stale += fallen.length; + } + if (grown) { + error( + 'Delete what nothing uses, or list the dependency it needs. If knip is wrong (a file loaded by path, ' + + 'a public API), say so in knip.jsonc with the reason; the baseline is not the place for it.' + ); + } + const now = CATEGORIES.map((c) => sum(counts[c])).reduce((a, b) => a + b, 0); + const held = Object.values(baseline) + .map(sum) + .reduce((a, b) => a + b, 0); + log(`knip: ${now} finding(s), baseline allows ${held}`); + return grown || stale ? 1 : 0; +}