Status: implemented
English | 中文
The GUI split introduced a second aggregate program (tsconfig.client.json, layering RFC) while the root tsconfig.json kept doubling as the host aggregate, and tsconfig.build.json remained a third, hand-maintained full emit graph. That triple bookkeeping produced four concrete asymmetries:
- The typecheck and build references lists drifted apart (
packages/goal/command-goalwas in the typecheck graph but missing from the build graph). - The lefthook pre-push hook ran
tsc -b tsconfig.jsononly, so client-side type breakage passed the local checkpoint and surfaced in CI. - tsserver discovers only configs named
tsconfig.json, so client test files sat on no discoverable config chain and fell back to inferred projects (no paths, wrong lib/jsx). - The vitest configs pointed at three different resolution sources (
tsconfig.vitest.json, the root config, and one hand-written alias).
One solution root, two check units, one shared base pair, no separate build or vitest config:
| File | Role | Forms a program? |
|---|---|---|
tsconfig.json |
Solution root: extends base, files: [], two references; the whole-repo tsc -b tsconfig.json graph, the tsserver entry, and the nearest config for get-tsconfig consumers (tsx running examples/, scripts/, doc fences) whose bare workspace imports resolve through the inherited paths |
No |
tsconfig.base.json |
Shared compilerOptions and the source paths map; doubles as the resolution facade for vite-tsconfig-paths (no include, so it applies to every importer) |
No |
tsconfig.base.client.json |
Browser compiler shape (jsx: react-jsx, DOM libs, types: []) shared by the client aggregate and every packages/client/* package |
No |
tsconfig.host.json |
The former root aggregate, moved verbatim: host packages, examples, tests, scripts, website; excludes packages/client |
Yes |
tsconfig.client.json |
Client packages and their tests; extends tsconfig.base.client.json |
Yes |
The load-bearing principle: cordis Context declaration-merge collisions exist only inside a ts.Program, never in module resolution. A solution file forms no program, so referencing both aggregates from one root cannot collide the merges; vite-tsconfig-paths reads only paths and include and discards types, so one facade may span both sides. The only way to explode is to flatten both sides into a single program — hence two derived disciplines: tsconfig.base.json never gains include/files (it would leak into every extending package and narrow the facade), and every repo-wide ts.Program consumer (scripts/ts-project.ts, doc-typecheck standalone mode) seeds tsconfig.host.json or tsconfig.client.json explicitly, never the root solution. Program-backed generators and semantic gates intentionally stay host-only; the client side gets program-backed gates only when a real need arrives.
The root tsconfig.json remains the solution entry for explicitly running the complete Project Reference graph, and lefthook pre-push incrementally covers both sides through tsc -b tsconfig.json --pretty false. Because the Client depends on Remote contracts generated by Host tsdown, the repository's build and typecheck commands run the Host and Client in order; the API Remotes build note owns the exact orchestration. tsconfig.build.json and tsconfig.vitest.json are deleted; all vitest configs point vite-tsconfig-paths at tsconfig.base.json.
The solution root extends the base deliberately: examples/ and scripts/ have no nearer tsconfig, so tsx (get-tsconfig) resolves their workspace imports through the root file. extends restores the paths map there while files: [] keeps the file program-less. Their type checking is unaffected by this: examples, scripts, and website files are included by the host aggregate.
- Rename
tsconfig.build.jsontotsconfig.host.json— rejected: the build graph was the full emit graph including all client packages, not a host graph; the name that fits the former root aggregate istsconfig.host.json, and the build graph itself is subsumed by the solution. - Point vitest at the root solution — rejected: a solution has neither
pathsnorinclude, so resolution would become a function of how far the plugin walks references, and the client aggregate's include (tests only, no src) would leave transitive src→src imports unmapped, falling through toexportsand loading a second copy of module singletons. - Keep
tsconfig.vitest.jsonas a dedicated facade — retained only as the fallback if vite-tsconfig-paths mishandles an include-less config; the base file already carries the paths map, and an include-less config applies everywhere, which is strictly wider than the facade's hand-kept include list.
docs/development.md#typescript-project-layoutis the authoritative description; rootAGENTS.mdcarries the two disciplines as conventions.- The ts-build-config note keeps ownership of the tsc-first build pipeline (tsc emits, tsdown bundles,
.tsspecifiers withrewriteRelativeImportExtensions); its former "one root typecheck project" shape is superseded by this note. - Adding an ordinary package registers it in exactly one aggregate's references: Host packages in
tsconfig.host.jsonand Client packages intsconfig.client.json.api/remotesis the only explicit split exception because the Host generates a contract that the Client consumes later; its two concrete projects are registered separately, while its package-root solution enters neither aggregate. - The Host and Client build phases must run serially: Client tsc cannot begin until Host tsdown has generated the contract. Each phase reuses its projects' incremental state instead of processing the same graph concurrently.