diff --git a/.changeset/tsconfig-test-d.md b/.changeset/tsconfig-test-d.md new file mode 100644 index 0000000..cf41f1a --- /dev/null +++ b/.changeset/tsconfig-test-d.md @@ -0,0 +1,31 @@ +--- +"@btravstack/tsconfig": minor +--- + +`test-d.json`: the preset for a type-level test project. + +Type-level tests assert with bindings nothing reads — `type _x = Expect>` +— so `noUnusedLocals` and `noUnusedParameters` must be off for that project and +on everywhere else. Every workspace with a `tsconfig.test-d.json` was writing +those two flags out by hand, along with the reason, or more often without it. + +```json +{ + "extends": ["./tsconfig.json", "@btravstack/tsconfig/test-d.json"], + "include": ["src/**/*.test-d.ts"] +} +``` + +The preset comes **last** in the array so its relaxations win over the strict +config beneath. + +It carries no `include`, and cannot: TypeScript resolves a base config's +`include` / `exclude` / `files` relative to the **base file's** own directory, +so a shipped glob would point inside `node_modules` and match nothing. Measured +— an `include` of `src/**/*.test-d.ts` in a base one directory up resolves to +`../base/src/**/*.test-d.ts` and reports `TS18003`. `scripts/validate.mjs` holds the shape as an +**allow-list** — `$schema` plus exactly those two `compilerOptions` — rather +than forbidding the four keys a reader would reach for. The preset is layered +last in a consumer's `extends` array, so anything it grows silently overrides +the workspace's own config, and only naming what may appear catches a key +nobody thought to forbid. diff --git a/packages/tsconfig/README.md b/packages/tsconfig/README.md index c91c9a3..0511453 100644 --- a/packages/tsconfig/README.md +++ b/packages/tsconfig/README.md @@ -47,6 +47,32 @@ symbols came first, and the sentence naming the missing port came third. With A library keeps `base.json`: there the declaration check is the guarantee that its consumers can build. +## `test-d.json`, for a type-level test project + +Type-level tests assert with bindings nothing reads — `type _x = Expect>` +— so `noUnusedLocals` and `noUnusedParameters` have to be off for that project +and on everywhere else. `test-d.json` sets exactly those two flags — nothing +else, which `scripts/validate.mjs` enforces as an allow-list — and composes +with the workspace's own config through an `extends` array: + +```json +{ + "extends": ["./tsconfig.json", "@btravstack/tsconfig/test-d.json"], + "include": ["src/**/*.test-d.ts"] +} +``` + +Order matters: the preset comes **last**, so its relaxations win over the +strict config it is layered on. + +It carries no `include`, and cannot. TypeScript resolves a base config's +`include` / `exclude` / `files` relative to **the base file's own directory** +(measured: an `include` of `src/**/*.test-d.ts` in a base one directory up +resolves to `../base/src/**/*.test-d.ts` and reports `TS18003`), so a shipped +preset's globs would point inside `node_modules`. Each workspace states its +own — which is also where the answer belongs, since a workspace that keeps its +type tests somewhere else needs a different glob, not a different preset. + It does **not** set `types` — TypeScript auto-includes every reachable `@types/*` package (Node included). This avoids forcing each consuming package to declare a direct `@types/node` just to satisfy a `types: ["node"]` list. diff --git a/packages/tsconfig/package.json b/packages/tsconfig/package.json index 18e43dd..d7f6418 100644 --- a/packages/tsconfig/package.json +++ b/packages/tsconfig/package.json @@ -20,12 +20,14 @@ }, "files": [ "app.json", - "base.json" + "base.json", + "test-d.json" ], "type": "module", "exports": { "./app.json": "./app.json", - "./base.json": "./base.json" + "./base.json": "./base.json", + "./test-d.json": "./test-d.json" }, "publishConfig": { "access": "public" diff --git a/packages/tsconfig/test-d.json b/packages/tsconfig/test-d.json new file mode 100644 index 0000000..ba44a29 --- /dev/null +++ b/packages/tsconfig/test-d.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "noUnusedLocals": false, + "noUnusedParameters": false + } +} diff --git a/scripts/validate.mjs b/scripts/validate.mjs index 0af74be..c684724 100644 --- a/scripts/validate.mjs +++ b/scripts/validate.mjs @@ -22,7 +22,7 @@ const check = (name, fn) => { // Every package ships exactly the files it lists, and those files load. const shipped = { - tsconfig: ["app.json", "base.json"], + tsconfig: ["app.json", "base.json", "test-d.json"], typedoc: ["base.json"], oxlint: ["base.json"], oxfmt: ["base.json"], @@ -58,6 +58,34 @@ check("tsconfig/app.json extends base and emits no declarations", () => { } }); +check("tsconfig/test-d.json is exactly the two unused-check relaxations", () => { + const testD = json("packages/tsconfig/test-d.json"); + // An ALLOW-list, not a deny-list: this preset is layered LAST in a consumer's + // `extends` array, so anything it grows silently overrides the workspace's + // own config. Naming what may appear is the only shape that catches a key + // nobody thought to forbid. + // + // `include` / `exclude` / `files` are the ones a reader will reach for and + // they cannot work here: TypeScript resolves a base config's globs relative + // to the BASE file's own directory, so a shipped glob points inside + // node_modules and matches nothing (measured: TS18003). + const top = Object.keys(testD).sort(); + if (top.join() !== "$schema,compilerOptions") { + throw new Error(`top-level keys must be $schema + compilerOptions, got: ${top.join(", ")}`); + } + const options = Object.keys(testD.compilerOptions ?? {}).sort(); + if (options.join() !== "noUnusedLocals,noUnusedParameters") { + throw new Error( + `compilerOptions must be exactly the two unused checks, got: ${options.join(", ")}`, + ); + } + // An assertion binding is never read, so both must be OFF — the whole reason + // this file exists. + for (const flag of options) { + if (testD.compilerOptions[flag] !== false) throw new Error(`${flag} must be false`); + } +}); + check("typedoc/base.json loads the markdown plugin", () => { const td = json("packages/typedoc/base.json"); if (!td.plugin?.includes("typedoc-plugin-markdown")) {