diff --git a/docs/gates.md b/docs/gates.md index 8fb4b7cb..e0ca534f 100644 --- a/docs/gates.md +++ b/docs/gates.md @@ -51,6 +51,7 @@ them itself (`no-undef`, `no-redeclare`, `no-dupe-keys`, …), nor the formattin | e2e | `react-hooks/*`, `react-refresh/*`, `local/*`, `no-console`, `no-restricted-globals`, `no-restricted-properties`, `no-restricted-syntax` — not applied | Playwright specs drive a browser from Node: no React, no product UI, and they print to the console on purpose. They get the base + typed rules + the playwright rules. | — | | ts configs | `react-hooks/*`, `react-refresh/*`, `local/*`, `no-console`, `no-restricted-globals`, `no-restricted-properties`, `no-restricted-syntax` — not applied | The build configs (vite, vitest, playwright, reactPlugin, devOnlyGuard) run in Node at build time. | — | | js configs | `@typescript-eslint/*`, `react-hooks/*`, `react-refresh/*`, `local/*`, `no-console`, `no-restricted-globals`, `no-restricted-properties`, `no-restricted-syntax`, `no-var`, `prefer-const`, `prefer-rest-params`, `prefer-spread`, `no-array-constructor`, `no-unused-expressions` — not applied | `eslint.config.js` and `postcss.config.js`: plain-JS build config, linted with `js.configs.recommended` only. | — | +| repo scripts | `scripts/*.mjs` — not in the eslint or prettier lane; `js.configs.recommended` + Prettier via `src/lib/repoScripts.contract.test.ts` | They sit at the repo root, and an ESLint config cannot reach past its own directory. The contract test lints and format-checks them from the repo root, to the same bar as the `js configs` row; no typed rules, as they are in no tsc project. | 0 lint hits; 2 files reformatted (E32 review) | | everywhere | ignored paths: `dist`, `node_modules`, `playwright-report`, `test-results`, `e2e/.auth` | Build output, installed packages, test artefacts, a live Playwright session. Generated, not written. | — | **Not enabled** (decided, not forgotten): `strictTypeChecked` — owner decision E32 D3, ~2000 hits with diff --git a/frontend/src/lib/repoScripts.contract.test.ts b/frontend/src/lib/repoScripts.contract.test.ts new file mode 100644 index 00000000..ad79c1fc --- /dev/null +++ b/frontend/src/lib/repoScripts.contract.test.ts @@ -0,0 +1,57 @@ +import { readdirSync, readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import js from "@eslint/js"; +import { ESLint } from "eslint"; +import globals from "globals"; +import prettier from "prettier"; +import { describe, expect, it } from "vitest"; + +// The repo-root `scripts/*.mjs` (the lock check `siftlode check` runs, the CHANGELOG generator its +// release runs) sit outside every lint lane: the eslint and prettier lanes run from frontend/, and an +// ESLint config cannot reach past its own directory (E32 review). This holds them to the same bar as +// the frontend's plain-JS config files — `js.configs.recommended` under Node, and the frontend's +// Prettier style. Not the typed rules: they are not in a tsc project (docs/gates.md). + +const REPO = fileURLToPath(new URL("../../../", import.meta.url)); +const FRONTEND = `${REPO}frontend/`; +const SCRIPTS = readdirSync(`${REPO}scripts`) + .filter((f) => f.endsWith(".mjs")) + .map((f) => `scripts/${f}`); + +describe("repo-root scripts", () => { + it("are found", () => { + expect(SCRIPTS).toContain("scripts/check-node-modules.mjs"); + }); + + it("pass the plain-JS lint", async () => { + const eslint = new ESLint({ + cwd: REPO, + overrideConfigFile: true, + overrideConfig: [ + { + files: ["scripts/*.mjs"], + ...js.configs.recommended, + languageOptions: { globals: globals.node }, + linterOptions: { reportUnusedDisableDirectives: "error" }, + }, + ], + }); + const hits = (await eslint.lintFiles(SCRIPTS)).flatMap((r) => + r.messages.map((m) => `${r.filePath}:${m.line} ${m.ruleId ?? "parse"}: ${m.message}`), + ); + expect(hits).toEqual([]); + }); + + it("are formatted in the frontend's Prettier style", async () => { + const options = await prettier.resolveConfig(`${FRONTEND}eslint.config.js`); + const unformatted: string[] = []; + for (const f of SCRIPTS) { + const ok = await prettier.check(readFileSync(REPO + f, "utf8"), { + ...options, + filepath: REPO + f, + }); + if (!ok) unformatted.push(f); + } + expect(unformatted).toEqual([]); + }); +}); diff --git a/scripts/check-node-modules.mjs b/scripts/check-node-modules.mjs index 55e23679..b3ced4fc 100644 --- a/scripts/check-node-modules.mjs +++ b/scripts/check-node-modules.mjs @@ -27,7 +27,9 @@ for (const [path, entry] of Object.entries(lock.packages ?? {})) { } const installed = JSON.parse(readFileSync(manifest, "utf8")).version; if (installed !== entry.version) - problems.push(`mismatch ${path.slice(13)}: installed ${installed}, lock pins ${entry.version}`); + problems.push( + `mismatch ${path.slice(13)}: installed ${installed}, lock pins ${entry.version}`, + ); } if (problems.length) { @@ -37,4 +39,6 @@ if (problems.length) { console.error("Fix: cd frontend && npm ci"); process.exit(1); } -console.log(`node_modules matches package-lock.json (${Object.keys(lock.packages).length - 1} entries)`); +console.log( + `node_modules matches package-lock.json (${Object.keys(lock.packages).length - 1} entries)`, +); diff --git a/scripts/gen-changelog.mjs b/scripts/gen-changelog.mjs index 692f33cd..866caf93 100644 --- a/scripts/gen-changelog.mjs +++ b/scripts/gen-changelog.mjs @@ -70,7 +70,9 @@ if (process.argv.includes("--check")) { ); process.exit(1); } - console.log(`CHANGELOG.md is current (${entries.length} releases, newest ${entries[0].version}).`); + console.log( + `CHANGELOG.md is current (${entries.length} releases, newest ${entries[0].version}).`, + ); } else { await writeFile(TARGET, rendered, "utf8"); console.log(`Wrote CHANGELOG.md — ${entries.length} releases, newest ${entries[0].version}.`);