Skip to content

Latest commit

 

History

History
80 lines (49 loc) · 7.8 KB

File metadata and controls

80 lines (49 loc) · 7.8 KB

Agent Note: Ordered Build for API Remotes Generated Contracts

Status: implemented

English | 中文

Problem

Typert must generate /remote declarations and runtime contributions from the Host's @Remote methods before the Client's api-remotes/src/client/index.ts can typecheck and bundle those contributions. If the root build hands both the Host and Client Project Reference graphs to tsc together, the Client compiles before the generated artifacts exist. Adding a separate contracts preprocessing step would instead compile the generator again outside the normal Host graph and let stale artifacts hide incorrect dependencies.

This ordering dependency must not change the repository's ordinary package rule. A normal package belongs to exactly one TypeScript face: Host packages are registered in tsconfig.host.json, and Client packages in tsconfig.client.json. A Client plugin having both a Node loader entry and a browser entry describes its bundled artifact shapes, not a reason to split its TypeScript project.

Decision

The root build completes Host tsc and Host tsdown first, with Host tsdown running Typert and generating the Remote Client contract. It then completes Client tsc, Client tsdown, and the Web build:

tsc -b tsconfig.host.json
tsdown --env.DSH_BUILD_FACE host
tsc -b tsconfig.client.json
tsdown --env.DSH_BUILD_FACE client
Vite Web build

build:lib:host owns the first two steps, build:lib:client owns the middle two, and build:web runs last. typecheck must also run the complete Host lib phase first because Client tsc requires declarations generated by Host tsdown; it does not need Client tsdown or the Web build.

Each tsc phase is the sole TypeScript compiler path and emits JavaScript, declarations, and incremental state to lib/types. Tsdown reads only that JavaScript and produces published bundles; it neither reads source nor emits declarations.

The sole package exception

api/remotes is the only package with both Host and Client composite projects. The Host project contains the Agent/Session lookup policy, Host plugin entry, and invariant; the Client project contains only src/client/index.ts, which must wait for the generated contract:

packages/api/remotes/
├─ tsconfig.json
├─ tsconfig.host.json
├─ tsconfig.client.json
└─ src/
   ├─ index.ts
   ├─ agent-lookup.ts
   ├─ invariant.ts
   └─ client/
      └─ index.ts

The package-root tsconfig.json is a solution that only references the two concrete projects; it enters neither aggregate nor any direct consumer's dependency graph. The root Host aggregate and host/apiproxy reference api/remotes/tsconfig.host.json, while the root Client aggregate and client/ui-goal reference api/remotes/tsconfig.client.json. ui-goal itself remains an ordinary single Client project. The workspace constraints gate walks the reachable Project Reference graph and rejects any face-declared project that references a split package's solution root or opposite leaf; targets with only tsconfig.json remain valid from either face.

The two projects use disjoint files and separate .tsbuildinfo files, so they can share lib/types without emitting any source file twice. If both sides later need a shared implementation, move that implementation into a neutral package instead of giving the same source to two emitting projects.

This exception follows from the real generated-contract ordering and is not a template available to ordinary packages. New packages remain restricted to one aggregate; adding another exception requires changing this decision and proving another generated dependency that cannot be eliminated.

Typert and tsdown

Host tsdown enables typertPlugin({ mode: 'workspace', faces: ['host'] }) in the normal root config. The generator uses only tsconfig.host.json as its program seed and produces both typert.host.* and the typert.remote-client.* projection of Host contracts; Client tsdown neither starts Typert nor analyzes the Client aggregate.

The Typert analyzer distinguishes compiler faces from runtime faces. Direct Project References in the aggregate determine which compiler face analyzes a project; only a split project explicitly referenced through tsconfig.host.json or tsconfig.client.json is restricted to that corresponding face. Runtime models follow package subpath contributions instead, so an ordinary single-project dshClient package may contribute both Host and Client runtime models. Consequently, Host analysis of api-remotes does not also register its Client entry, while an ordinary dual-entry package does not lose its Host model.

Both the Host and Client tsdown passes receive the same complete workspace of vendor/*, packages/*/*, and apps/cli. The root config does not scan lib/types/client/index.js, maintain a package classification table, or use a tsdown filter; package-local configs return entries for the current phase according to DSH_BUILD_FACE.

An ordinary Client plugin returns an empty config during the Host pass and produces both its Node loader entry and browser bundle during the Client pass. The clientBundle(..., { hostPhase: true }) used by api-remotes is the only phase exception: the Host pass produces its Host entry, and the Client pass produces only its browser bundle. Package-local tsdown without DSH_BUILD_FACE still returns that package's normal entries together for local single-package development.

Alternatives considered

Keep a separate contracts preprocessing step. This would compile the generator again outside the normal Host Project Reference graph and let residual generated artifacts hide the Client entering the Host graph too early.

Run the root tsc -b tsconfig.json once before tsdown. Client tsc would run before Host tsdown and could not obtain /remote declarations from a clean worktree.

Split every package containing src/client/index.ts. Separate Node and browser entries are the normal Client plugin bundling convention and do not create a compilation ordering dependency; splitting them universally would only increase the maintenance cost of references and incremental state.

Scan Client compilation artifacts or maintain two workspace lists. Artifact scanning would make package participation depend on residual files, while hand-maintained lists and package-name filters would drift as directories change. A complete workspace with package-local face selection already provides deterministic behavior.

Run Typert again during the Client pass. Remote Client is a projection of the Host contract and has no independent Client reflection source; a second Typert program would only duplicate work and increase the risk of mixing both sides' declarations into one analysis.

Consequences

A clean build is the authoritative check of ordering correctness: with no existing /remote artifacts, Host tsc must succeed first, Host tsdown must generate the contract, and then Client tsc, Client tsdown, and the Web build must succeed. No phase may write artifacts into src.

The tsc-first ownership established by the TypeScript build config note remains unchanged, but this note replaces its command shape of one whole-graph tsc pass followed by bundling with ordered phases. The ordinary-package single-aggregate rule established by the two-aggregate solution note also remains unchanged; this note creates one explicit exception for api/remotes.

An independent Client build is no longer a self-contained entry on a clean worktree; repository commands, CI, and release flows must run the Host lib phase first. Developers of ordinary packages do not need to understand or copy this exception and continue to choose one aggregate according to the package's runtime environment.