Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .changeset/tsconfig-app-preset.md
Original file line number Diff line number Diff line change
@@ -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.
26 changes: 26 additions & 0 deletions packages/tsconfig/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 8 additions & 0 deletions packages/tsconfig/app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./base.json",
"compilerOptions": {
"declaration": false,
"declarationMap": false
}
}
2 changes: 2 additions & 0 deletions packages/tsconfig/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@
"directory": "packages/tsconfig"
},
"files": [
"app.json",
"base.json"
],
"type": "module",
"exports": {
"./app.json": "./app.json",
"./base.json": "./base.json"
},
"publishConfig": {
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 8 additions & 4 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
13 changes: 12 additions & 1 deletion scripts/validate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand All @@ -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")) {
Expand Down
Loading