From d2cd3a9dd392428c5ad3051f0904ea6327b0928d Mon Sep 17 00:00:00 2001 From: Benoit TRAVERS Date: Wed, 2 Sep 2026 17:13:38 +0200 Subject: [PATCH 1/3] feat(tsconfig): app.json, for a workspace that emits no declarations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `base.json` with `declaration` and `declarationMap` off. Declaration emit is type-checked even under `noEmit`, so an application pays two `TS4023` lines about a library's internal brand symbols before every mistake it actually made, and gains nothing — it ships no `.d.ts`. A library keeps `base.json`. Claude-Session: https://claude.ai/code/session_01GGixjxi5AQ2cNK62bBymfF --- .changeset/tsconfig-app-preset.md | 16 ++++++++++++++++ packages/tsconfig/README.md | 26 ++++++++++++++++++++++++++ packages/tsconfig/app.json | 8 ++++++++ packages/tsconfig/package.json | 2 ++ scripts/validate.mjs | 8 +++++++- 5 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 .changeset/tsconfig-app-preset.md create mode 100644 packages/tsconfig/app.json 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/scripts/validate.mjs b/scripts/validate.mjs index b0b5410..ab631d8 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,12 @@ 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"); + if (app.compilerOptions?.declaration !== false) throw new Error("declaration 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")) { From 592123d2164a9293aa78bcf68fd54cfa98ee9349 Mon Sep 17 00:00:00 2001 From: Benoit TRAVERS Date: Wed, 2 Sep 2026 21:45:48 +0200 Subject: [PATCH 2/3] test(tsconfig): assert declarationMap too, since the preset promises it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review on #8: the check asserted `declaration: false` and left the second flag unguarded — and `declarationMap: true` under `declaration: false` emits nothing either way, so a regression there is silent, which is exactly the shape this file exists to catch. Verified to bite: flipping the flag fails the check naming it. Claude-Session: https://claude.ai/code/session_01GGixjxi5AQ2cNK62bBymfF --- scripts/validate.mjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/validate.mjs b/scripts/validate.mjs index ab631d8..0af74be 100644 --- a/scripts/validate.mjs +++ b/scripts/validate.mjs @@ -50,7 +50,12 @@ check("tsconfig/base.json is strict", () => { 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"); - if (app.compilerOptions?.declaration !== false) throw new Error("declaration must be false"); + // 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", () => { From eaab57c1cd62aeee126294eb8366a63e50a8eb4b Mon Sep 17 00:00:00 2001 From: Benoit TRAVERS Date: Wed, 2 Sep 2026 21:48:10 +0200 Subject: [PATCH 3/3] chore(deps): move the fast-uri floor to 3.1.6 for four more advisories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Security Audit` is red on `main`, and on every open PR with it: four advisories landed in the same parser after the last bump — GHSA-5jgf-p345-68v8, GHSA-f65p-4m7j-42xc, GHSA-fph4-wmhf-6fwf and GHSA-jqff-g426-hqxp, all ReDoS or parsing confusion in fast-uri below 3.1.6. The existing entry's floor moves rather than a second one being added: two overlapping ranges for one package is how a floor stops applying without anybody noticing. Same path as before — `@commitlint/cli` > `ajv`, dev tooling only — and 3.1.6 published 2026-08-23, well past `minimumReleaseAge`. Claude-Session: https://claude.ai/code/session_01GGixjxi5AQ2cNK62bBymfF --- pnpm-lock.yaml | 10 +++++----- pnpm-workspace.yaml | 12 ++++++++---- 2 files changed, 13 insertions(+), 9 deletions(-) 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).