tsconfig conventions
Every package here is source-only .ts: exports point at ./src/*.ts,
there is no build step, and consumers (Bun, Vite, Tauri, the Cloudflare Worker)
operate on raw .ts. That single fact decides the whole config.
The exceptions are the audience, not the package. packages/workspace and
packages/field are published for toolchains we do not control, so shipping
source would subject them to a stranger's compiler settings. They emit .js and
.d.ts, and their exports point at ./dist. Each leaf tsconfig.json is an
ordinary tier from the table below; the emit lives in a sibling
tsconfig.build.json that extends the leaf and turns on exactly what emitting
needs (noEmit: false, outDir, rootDir, declaration, types: []). Do not
add outDir, declaration, or noEmit: false to any leaf. If another package
earns a build, copy that split.
The one rule
A leaf tsconfig.json may set only: types, library-only strictness
(noUnusedLocals/noUnusedParameters), checkJs (SvelteKit), and genuinely
package-specific options (jsx, paths, customConditions, include).
Anything else belongs in a base. If a leaf option repeats a base value or a TypeScript default, delete it.
Two base files
tsconfig.base.json universal: target/lib[ESNext], module preserve, strict,
noEmit, isolatedModules, verbatimModuleSyntax, types []
tsconfig.dom.json extends base; its ONLY job is lib [ESNext, DOM, DOM.Iterable]
There is no tsconfig.base.lib.json and no project references. Source-only
packages never emit, so composite/declaration/outDir have no place in a
leaf. The two published packages that do emit put them in their own
tsconfig.build.json (see above), never in the leaf or a base.
Module strategy: one, repo-wide
module: "preserve" + moduleResolution: "bundler". preserve implies
bundler (TS 5.4+); the explicit pair is kept for legibility. Never use
NodeNext anywhere: nothing in this repo is published as emitted Node ESM,
and bundler resolution already reads package.json imports/exports.
The seven leaf tiers
Pick the tier, copy the shape, change nothing else.
| Tier | extends | Leaf adds |
| --- | --- | --- |
| Bun library | "../../tsconfig.base.json" | types:["bun"], noUnusedLocals, noUnusedParameters |
| Node library | "../../tsconfig.base.json" | types:["node"], noUnusedLocals, noUnusedParameters |
| Svelte library | "../../tsconfig.dom.json" | types:["bun"], noUnusedLocals, noUnusedParameters |
| SvelteKit app | ["../../tsconfig.base.json", "./.svelte-kit/tsconfig.json"] | checkJs:true, types:["bun"] |
| Cloudflare Worker | "../../tsconfig.base.json" | jsx, jsxImportSource, types, include |
| Bun app | "../../tsconfig.base.json" | types:["bun"], include |
| Astro app | ["../../tsconfig.base.json", "astro/tsconfigs/strict"] | astro include/exclude |
A generated config (./.svelte-kit/tsconfig.json) goes last in the array so
its lib/module win where they must. Never hand-edit a generated config.
The two canonical library shapes in full:
// bun library: packages/data, workspace, field, sqlite, sync, identity, ...
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"types": ["bun"],
"noUnusedLocals": true,
"noUnusedParameters": true
}
}
// svelte library: packages/ui, svelte-utils
{
"extends": "../../tsconfig.dom.json",
"compilerOptions": {
"types": ["bun"],
"noUnusedLocals": true,
"noUnusedParameters": true
}
}
Never redeclare these (base value or TS default)
Putting any of these in a leaf is dead weight. Delete on sight.
| Do not write in a leaf | Why |
| --- | --- |
| module: "preserve" | already in tsconfig.base.json |
| moduleResolution, target, noEmit, strict, isolatedModules | already in the base |
| lib: ["ESNext"] | the base default; use tsconfig.dom.json if you need DOM |
| noPropertyAccessFromIndexSignature: false | that is already the TS default |
| useDefineForClassFields: true | default when target >= ES2022 |
| forceConsistentCasingInFileNames | default true since TS 5.0 |
| resolvePackageJsonExports | default true under moduleResolution: bundler |
| sourceMap | no-op under noEmit |
| single-element extends array | use the string form: "extends": "../../tsconfig.base.json" |
types is opt-in, on purpose
The base sets types: [] to disable auto-inclusion of every node_modules/@types
package. Each leaf opts in: ["bun"] for almost everything, ["node"] for the
one package that uses @types/node. Do not hoist ["bun"] to a base: a package
without @types/bun installed would then fail with Cannot find type definition.
include rules
- TypeScript does not merge
includeacrossextends. A leafincludefully replaces the inherited one. For SvelteKit apps this means: do not set your owninclude, or you drop the generatedambient.d.ts/$typesglobs. - Only set
includewhen you must narrow scope for a real reason; comment why.
Adding a new package
- Scaffold via the
monoreposkill's boilerplate. - Pick a tier from the table above; copy that exact shape.
package.jsonusesexportsonly, nomain/types. Entry point is./src/index.ts.bun installat the repo root, thenbun typecheck.
Background
The full rationale, the migration that established this layout, and the baseline
typecheck state are in specs/20260522T190000-modernize-monorepo-tsconfig.md.