diff --git a/.changeset/tsconfig-app-preset.md b/.changeset/tsconfig-app-preset.md new file mode 100644 index 0000000..c131351 --- /dev/null +++ b/.changeset/tsconfig-app-preset.md @@ -0,0 +1,16 @@ +--- +"@btravstack/tsconfig": minor +--- + +`app.json`: `base.json` with `declaration` and `declarationMap` off, for a +workspace that emits no declarations. + +Declaration emit is type-checked even under `noEmit`, so `declaration: true` +costs an application the errors it buys a library. Measured on a DI composition +root with one unmet dependency: two `TS4023` lines about a library's internal +`ID` / `SERVICE` brand symbols printed **first**, and the sentence naming the +missing port printed third. An application that ships no `.d.ts` has nothing to +gain from that check and pays for it on every mistake, internals-first. + +A library keeps `base.json`, where the check is the guarantee that its consumers +can build. diff --git a/packages/tsconfig/README.md b/packages/tsconfig/README.md index c502eb2..c91c9a3 100644 --- a/packages/tsconfig/README.md +++ b/packages/tsconfig/README.md @@ -21,6 +21,32 @@ pnpm add -D @btravstack/tsconfig `moduleDetection: "force"`, and friends, on `NodeNext` / `ES2022`. Override anything in your own `compilerOptions`. +## `app.json`, for a package that emits no declarations + +An application — a deployment, an example, a test workspace — extends +`app.json` instead, which is `base.json` with `declaration` and +`declarationMap` off: + +```jsonc +// tsconfig.json +{ + "extends": "@btravstack/tsconfig/app.json", + "compilerOptions": { "noEmit": true }, + "include": ["src/**/*"], +} +``` + +The flag is not free when nothing consumes the declarations it type-checks: +declaration emit is checked even under `noEmit`, so an exported value whose +type reaches a library's unexported brand symbols reports `TS4023` — **before** +the error the developer actually made. Measured on a DI composition root with +one unmet dependency: two `TS4023` lines about internal `ID` / `SERVICE` +symbols came first, and the sentence naming the missing port came third. With +`app.json` the actionable diagnostic is the only one. + +A library keeps `base.json`: there the declaration check is the guarantee that +its consumers can build. + 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/app.json b/packages/tsconfig/app.json new file mode 100644 index 0000000..e9e246b --- /dev/null +++ b/packages/tsconfig/app.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "./base.json", + "compilerOptions": { + "declaration": false, + "declarationMap": false + } +} diff --git a/packages/tsconfig/package.json b/packages/tsconfig/package.json index a5f0598..cbc787b 100644 --- a/packages/tsconfig/package.json +++ b/packages/tsconfig/package.json @@ -19,10 +19,12 @@ "directory": "packages/tsconfig" }, "files": [ + "app.json", "base.json" ], "type": "module", "exports": { + "./app.json": "./app.json", "./base.json": "./base.json" }, "publishConfig": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 00ab12c..6616bf2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,7 +26,7 @@ catalogs: version: 1.73.0 overrides: - fast-uri@<3.1.5: 3.1.5 + fast-uri@<3.1.6: 3.1.6 importers: @@ -639,8 +639,8 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} - fast-uri@3.1.5: - resolution: {integrity: sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==} + fast-uri@3.1.6: + resolution: {integrity: sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==} fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -1472,7 +1472,7 @@ snapshots: ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.5 + fast-uri: 3.1.6 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -1581,7 +1581,7 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 - fast-uri@3.1.5: {} + fast-uri@3.1.6: {} fastq@1.20.1: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c932f56..5eaeed1 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -20,10 +20,14 @@ allowBuilds: lefthook: true overrides: - # GHSA-7p8r-x3mc-p8w7 (High, supersedes GHSA-v2hh-gcrm-f6hx): fast-uri, - # patched in 3.1.5. Transitive only (via @commitlint/cli > ajv); dev-tooling - # only. 3.1.5 published 2026-07-31, past the 7-day cutoff. - "fast-uri@<3.1.5": "3.1.5" + # GHSA-7p8r-x3mc-p8w7 (High, supersedes GHSA-v2hh-gcrm-f6hx), then four more + # in the same parser — GHSA-5jgf-p345-68v8, GHSA-f65p-4m7j-42xc, + # GHSA-fph4-wmhf-6fwf, GHSA-jqff-g426-hqxp — all ReDoS or parsing confusion + # below 3.1.6. Transitive only (via @commitlint/cli > ajv); dev-tooling only. + # The floor moves rather than a second entry being added: two overlapping + # ranges for one package is how a floor stops applying unnoticed. 3.1.6 + # published 2026-08-23, past the 7-day cutoff. + "fast-uri@<3.1.6": "3.1.6" # Supply-chain maturity delay: mirror the consumer repos. Don't adopt a freshly # published version until it has been on the registry for 7 days (10080 minutes). diff --git a/scripts/validate.mjs b/scripts/validate.mjs index b0b5410..0af74be 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: ["base.json"], + tsconfig: ["app.json", "base.json"], typedoc: ["base.json"], oxlint: ["base.json"], oxfmt: ["base.json"], @@ -47,6 +47,17 @@ check("tsconfig/base.json is strict", () => { if (tsc.compilerOptions?.strict !== true) throw new Error("strict must be true"); }); +check("tsconfig/app.json extends base and emits no declarations", () => { + const app = json("packages/tsconfig/app.json"); + if (app.extends !== "./base.json") throw new Error("app.json must extend ./base.json"); + // Both, because the preset promises both: `declarationMap: true` under + // `declaration: false` emits nothing, so a regression there is silent — the + // exact shape this file exists to catch. + for (const flag of ["declaration", "declarationMap"]) { + if (app.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")) {