Generate the TypeScript client at build time, not committed (T3) - #135
Merged
Conversation
|
4 tasks
stephane-segning
force-pushed
the
claude/dashboard-t3-client
branch
from
August 8, 2026 13:12
c9ca85f to
004ceb0
Compare
Auto-generated code should not be in version control, so `packages/sms-client` is now produced by `just client-gen` rather than checked in. Two files stay tracked, each for a concrete reason: - `package.json`, because pnpm needs a manifest on disk to resolve the workspace member at all — `pnpm install --frozen-lockfile` has to succeed *before* generation can run. Its content is schema-invariant boilerplate and byte-identical to what the generator writes, so it is not a hand-forked copy that can drift. - `GENERATING.md`, hand-written, because in a fresh un-generated clone it is the only thing telling you to run `just client-gen` first. The generated README that would say so does not exist yet at that point. Gate A (`generate-typescript --check`) is deleted rather than kept: it existed to prove the *committed* client matched the schema, and with nothing committed there is no drift to detect. A check that asserts nothing is worse than no check. Gate B — `ci/assert-client-routes-match-server.mjs`, comparing every route the client calls against the pinned `sms-gateway routes` output — is now the load-bearing one and is verified to fail when a route is broken. CI generates before typechecking, installing the CLI from crates.io version-locked to the library pin. A client generated by a locally-built binary is reproducible on exactly one machine, which would defeat the point. Two real problems found while doing this, neither predictable from reading: - **Turborepo's default hashing ignores gitignored files.** Once the generated sources became untracked, `turbo run build` happily served a stale cache after they changed. Fixed with an explicit `inputs` override on `@vsms/sms-client#build`, reverified with a miss/hit/miss sequence. - **The TypeScript override was declared in two places.** T1 put it in `.pnpmrc`; this change needs it in `pnpm-workspace.yaml` because the generated `package.json` asks for `typescript@^7.0.2` and Next 15.5 predates TS 7. Both present produced `ERR_PNPM_LOCKFILE_CONFIG_MISMATCH` on a clean clone — invisible to any existing checkout with a warm `node_modules`. Consolidated on `pnpm-workspace.yaml`, pnpm 11's documented home for settings. Extends the existing `js` CI job rather than adding a second one, since #133 landed that job while this branch was open. Verified from a real `git clone` into a temp directory, not a wiped `node_modules`: `pnpm install --frozen-lockfile` succeeds, `just client-gen` generates, `just client-check` reports all 102 client calls matching 102 served routes, and `turbo run typecheck build` reports 8/8. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
stephane-segning
force-pushed
the
claude/dashboard-t3-client
branch
from
August 8, 2026 13:17
004ceb0 to
84a6479
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Generates
packages/sms-clientfromschema/schema.cstackwithcratestack generate-typescript(T3), and adds a CI drift gate over it. Reworked after this PR's first commit landed the generated client as tracked files — the owner's standing rule is that auto-generated code is never committed to version control, so this now generates the client at build/CI time instead. The first commit (2562529) is left in history rather than squashed; the second commit (c9ca85f) is the actual shape this PR ships.Intent
Source of truth: the T3 task brief in the dashboard architecture plan (DECISIONS §2, "Depend on
packages/sms-client? — YES, generate it"), M4 issue #47 (client drift-detection gate), and a direct owner correction mid-PR: generated code must never be committed.T3 was previously blocked on cratestack#455/#456 (the
Decimalscalar never emitting a TS type — vsms uses it on three money fields), fixed and released in cratestack 0.7.8. This PR generates with that release (a locally built~/dev/cratestackbinary for local verification; CI installs the published 0.7.8 from crates.io — see below).Scope
packages/sms-client/— generated bycratestack generate-typescript, not committed..gitignorenow readspackages/sms-client/*with two explicit exceptions:package.json— tracked deliberately. pnpm needs apackage.jsonon disk to recognise this as a workspace member and validatepnpm install --frozen-lockfileagainst the committedpnpm-lock.yaml, and that install has to succeed before generation is even possible to run (generation itself needs nonode_modules, but everything downstream of install — typecheck, build — does). Its content is exactly what generation writes (the generator's dependency list is schema-invariant boilerplate, not derived from model content) — confirmed byte-identical across repeated regenerations, not a hand-maintained fork.GENERATING.md— a short, hand-written, tracked note. It's the one thing that exists inpackages/sms-client/in a fresh, ungenerated clone (beforejust client-genhas ever run), specifically so "run this one command first" is discoverable — the real generatedREADME.mdthat would normally say that doesn't exist yet at that point.ci/assert-sms-client-matches-schema.mjs— deleted. This was "Gate A" (does the committed client match the schema?) from the PR's first commit. With nothing committed, there's nothing to diff against; it would assert nothing, so it's removed rather than kept as decoration.ci/assert-client-routes-match-server.mjs("Gate B") — kept, and is now the only gate: does every route the freshly generated client calls exist on the pinned server's real route table (sms-gateway routes)? This is what actually catches acratestack-CLI-vs-pinned-library (=0.6.7) skew, independent of whether anything's committed.justfile—client-genunchanged in shape;client-checknow depends onclient-gen(client-check: client-gen) and only runs Gate B.turbo.json— a package-specific task override,"@vsms/sms-client#build": { "inputs": ["src/**", "package.json", "tsconfig.json"] }. See "the caching bug" below — this is not decorative..github/workflows/ci.yml— newjsjob (see CI wiring below).Untouched, same as before:
packages/gateway/src/messages.ts(hand-writtenpreviewMessage/sendMessage) — this PR is "the client exists, is generated correctly, and stays provably in sync," not "the gateway now uses it."The pnpm chicken-and-egg problem, and how it's resolved
With the client untracked,
pnpm install --frozen-lockfilestill has to succeed on a clean checkout, and it does — becausepackage.json(and only that file) stays tracked. I considered the alternative (generate the whole package, includingpackage.json, beforepnpm installruns) and rejected it: it would meanpnpm-lock.yaml's existing importer entry forpackages/sms-clienthas nothing to validate against until generation has already run once per checkout, which is a worse ordering constraint than "one boilerplate file stays tracked." Trackingpackage.json— confirmed to be exactly the generator's own deterministic output, never hand-edited — was the more honest fix. Full reasoning is inline in.gitignore's comment on this package and inpackages/sms-client/GENERATING.md.Verified from a genuinely clean
git clone(notrm -rf node_modules, per the instruction to actually test this path — a local clone into a fresh temp directory, matching what CI itself does):git clone→ onlypackage.jsonandGENERATING.mdexist underpackages/sms-client/.pnpm install --frozen-lockfile→ succeeds, resolves@vsms/sms-client@0.0.0as a real workspace member (pnpm -r list --depth -1shows it), installs its owndevDependencies(typescript@5.9.3via the workspace override,@tanstack/react-query) intopackages/sms-client/node_modules.just client-gen→ generatessrc/,tsconfig.json,README.md.just client-check→ buildssms-gateway, Gate B passes (102/102 routes match).SKIP_ENV_VALIDATION=1 pnpm turbo run typecheck build→ 8/8 tasks succeed, all cache misses (correct — nothing was cached yet).pnpm turbo run test→ 3/3 succeed.pnpm biome ci .→ clean, 84 files.packages/sms-client/node_modules/.bin/tsc -p tsconfig.json --noEmit→ clean (the exact check that was broken before the upstreamDecimalfix).The caching bug found while wiring this up
Not assumed — found live. Turborepo's default task-hashing only considers git-tracked (or untracked-but-not-gitignored) files. Since
packages/sms-client/srcis now gitignored,turbo run buildserved a stale cache hit after mutating generated source content:Fixed with an explicit per-package task override in
turbo.json:which forces turbo to hash those globs regardless of
.gitignore. Re-verified with a clean three-step sequence: miss on first build → hit on an identical rebuild (proving caching still works when nothing changed) → miss again after mutating generated content (proving the fix). All three behaved correctly.CI wiring
.github/workflows/ci.ymlgains ajsjob. PR #133 (which independently adds a job with the same name) had not landed onmainas of this branch, so there was nothing existing to attach to. The new job carries a comment for whoever merges both: if #133 lands too, keep onejsjob and fold this PR's two new steps (installing cratestack, generating the client) into it, in the position noted (after install, before typecheck/build).Job order, and why it's exactly this order:
actions/checkout→actions/setup-node→corepack enablepnpm install --frozen-lockfile— succeeds becausepackage.jsonis tracked (see above).pnpm biome ci .dtolnay/rust-toolchain@stable+Swatinem/rust-cache@v2→cargo install cratestack-cli --version 0.7.8 --locked— the published crate from crates.io, never a locally built binary, so this gate proves something on every CI run, not just one developer's machine.>=0.7.8is required for theDecimalfix.packages/sms-client(the same two commandsjust client-genwraps).node ci/assert-client-routes-match-server.mjs(Gate B).pnpm turbo run typecheck build, thenpnpm turbo run test.Could not verify by actually running GitHub Actions — no network access to trigger a live CI run from this environment. Verified every individual piece of this ordering by hand instead (see the clean-clone steps above), using the locally-built 0.7.8 binary as a stand-in for what
cargo install cratestack-cli --version 0.7.8 --lockedwill produce. Two things about the CI-specific step I could not verify directly and are worth a reviewer's second look: (a) whethercargo install cratestack-cli --version 0.7.8 --lockedcompiles in acceptable CI time with no prior cache (it's a real compile from source, not a prebuilt binary —cargo-binstallwould be faster sincecratestack-cli's ownCargo.tomlships[package.metadata.binstall]pointing at GitHub Release assets, but I chose the slower, dependency-freecargo installpath because I could verify its mechanism — plain crates.io resolution — with higher confidence than binstall's asset-matching from this sandbox); (b)Swatinem/rust-cache@v2's actual speedup for a barecargo installof a crate outside this repo's own workspace, since its cache-keying is normally driven by this repo's ownCargo.lock.Verification
Exact generation command:
--base-path ''confirmed load-bearing:sms-gateway routesreports 102 routes served at/. GeneratedREADME.mdsays so explicitly.Decimaltypes; the three money fields arestring.just client-check(client-gen + Gate B) passes.cargo check --workspace— clean, unaffected (no Rust/schema files touched).Screenshots/Evidence
N/A — generated TypeScript package, CI config, and gitignore/turbo config. No UI surface in this PR.
Risk Assessment
Low-to-moderate. The main residual risk is the two things flagged above as unverifiable from this sandbox: the CI job's actual runtime behavior (install speed, action references) has only been verified piece-by-piece by hand, never as one continuous GitHub Actions run. Everything else — the untracked-generation model itself, the turbo caching fix, Gate B — was verified against real command execution, including a genuinely clean
git clone, not assumed.AI Usage Declaration
Written and verified by Claude (Sonnet 5). This PR was reworked mid-flight after the owner corrected the original approach (committing the generated client) — the rework is a new commit, not a squash, so both approaches are visible in history.
git cloneinto a fresh temp directory (notrm -rf node_modules) to test the install-before-generation ordering for real.Reviewer Focus
cargo install cratestack-cli --version 0.7.8 --locked(correct but slow, no caching benefit demonstrated) is the right call for thejsjob versuscargo-binstall(faster, but its GitHub-Release-asset-matching mechanism is what I could least verify from this sandbox)..gitignore/tracked-package.jsondesign: confirm the reasoning (pnpm needs a manifest on disk before install, and before generation is even runnable) matches your own understanding of pnpm workspace resolution.js-job duplication risk with unmerged Add a CI job for the TypeScript half, and correct a stale security comment #133 — the in-file comment is there, but you'll need to actually reconcile it when both land.turbo.jsoninputsoverride — confirm this is the right fix shape versus, say, disabling turbo caching entirely for this one task.