Skip to content

Latest commit

 

History

History
45 lines (30 loc) · 6.04 KB

File metadata and controls

45 lines (30 loc) · 6.04 KB

Agent Note: Solution root over two aggregate programs

Status: implemented

English | 中文

Problem

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-goal was in the typecheck graph but missing from the build graph).
  • The lefthook pre-push hook ran tsc -b tsconfig.json only, 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).

Decision

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.

Alternatives considered

  • Rename tsconfig.build.json to tsconfig.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 is tsconfig.host.json, and the build graph itself is subsumed by the solution.
  • Point vitest at the root solution — rejected: a solution has neither paths nor include, 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 to exports and loading a second copy of module singletons.
  • Keep tsconfig.vitest.json as 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.

Consequences

  • docs/development.md#typescript-project-layout is the authoritative description; root AGENTS.md carries the two disciplines as conventions.
  • The ts-build-config note keeps ownership of the tsc-first build pipeline (tsc emits, tsdown bundles, .ts specifiers with rewriteRelativeImportExtensions); 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.json and Client packages in tsconfig.client.json. api/remotes is 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.