Agent Skills: tsconfig conventions

TypeScript config conventions for this monorepo: the two-base layering, the eight leaf tiers, and the never-redeclare list. Use when adding a package, editing any tsconfig.json, picking a tier for a new app, or debugging module resolution.

UncategorizedID: EpicenterHQ/epicenter/tsconfig

Repository

EpicenterHQLicense: NOASSERTION
4,678359

Install this agent skill to your local

pnpm dlx add-skill https://github.com/EpicenterHQ/epicenter/tree/HEAD/.agents/skills/tsconfig

Skill Files

Browse the full folder contents for tsconfig.

Download Skill

Loading file tree…

.agents/skills/tsconfig/SKILL.md

Skill Metadata

Name
tsconfig
Description
'TypeScript config conventions for this monorepo: the two-base layering, the eight leaf tiers, and the never-redeclare list. Use when adding a package, editing any tsconfig.json, picking a tier for a new app, or debugging module resolution.'

tsconfig conventions

Every package here is source-only .ts: exports point at ./src/*.ts, there is no build step, and consumers (Bun, Vite, WXT, Tauri, the Cloudflare Worker) operate on raw .ts. That single fact decides the whole config.

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 here.

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 eight 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 | | WXT extension | ["../../tsconfig.dom.json", "./.wxt/tsconfig.json"] | customConditions, paths, include |

A generated config (./.svelte-kit/tsconfig.json, ./.wxt/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/workspace, filesystem, util, sync, cli, ...
{
	"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 include across extends. A leaf include fully replaces the inherited one. For SvelteKit apps this means: do not set your own include, or you drop the generated ambient.d.ts/$types globs.
  • Only set include when you must narrow scope for a real reason; comment why.

Adding a new package

  1. Scaffold via the monorepo skill's boilerplate.
  2. Pick a tier from the table above; copy that exact shape.
  3. package.json uses exports only, no main/types. Entry point is ./src/index.ts.
  4. bun install at the repo root, then bun typecheck.

Background

The full rationale, the migration that established this layout, and the baseline typecheck state are in specs/20260522T190000-modernize-monorepo-tsconfig.md.