0003. Use Node.js 24, pnpm 12, TypeScript 7 and source-first packages
- Status: accepted, revised after the structure review (round 1)
- Date: 2026-10-26
Context
Section titled “Context”The analysis fixes the language and the main tools (chapters 7 and 18); this ADR records the versions and the behaviours verified on 25–26 October 2026 while preparing the first scaffold. Three facts shape it:
- TypeScript 7.0 ships no compiler API. The
typescript@7.0.2package contains the native compiler (tsc) only. Tools that importtypescriptas a library need the TypeScript 6 API, published as@typescript/typescript6. Verified: dependency-cruiser 18.6 refuses TypeScript 7 (it accepts>=2 <7) and parses no.tsfile without it. - pnpm 12 is a rewrite with new settings. Build scripts of dependencies are blocked unless approved in
allowBuilds(strictDepBuildsis on by default);minimumReleaseAgeis in minutes and becomes strict when set explicitly;trustPolicy: no-downgraderejects a version whose publishing evidence is weaker than an earlier one. It also ships native release management (pnpm change,pnpm version -r). - Node.js 24 runs TypeScript directly by stripping types, as long as the code uses only erasable syntax. Verified on Node 24.19.
Decision
Section titled “Decision”| Area | Choice |
|---|---|
| Runtime | Node.js 24 LTS (.node-version 24.22.1; engines >=24.11). Node.js 26 becomes LTS on 28 October 2026 but Vercel Functions do not offer it yet: it is added to CI as a second version when they do. |
| Package manager | pnpm 12.15.0, pinned in packageManager. Workspace settings: minimumReleaseAge: 10080 (seven days, transitive dependencies included), trustPolicy: no-downgrade, build scripts allowed one by one in allowBuilds, shared versions in pnpm catalogs. |
| Task runner | Turborepo 2.11 for typecheck, test and build, with local cache only. |
| Compiler | TypeScript 7.0 for type checking, strict, with erasableSyntaxOnly, verbatimModuleSyntax and rewriteRelativeImportExtensions. Presets per environment (Node.js, Next.js, published libraries, later Expo) share the same strictness. |
| Compiler API | TypeScript 6 (npm:@typescript/typescript6@6.0.2) given only to the tools that need it, through packageExtensions. |
| Lint and format | Biome 2.5 (2.6 was younger than seven days on the decision date). |
| Boundaries and dead code | dependency-cruiser 18.6 (with the TypeScript 6 API) and Knip 6. |
| Tests | Vitest 5 with the V8 coverage provider (ADR 0005). |
| Bundling | tsdown, only for publishable packages and for the worker and migrator artifacts. |
| Git hooks | Lefthook, for fast local feedback; CI repeats every check. |
Source-first packages. Internal packages export their TypeScript sources ("exports": { "./x": "./src/x/index.ts" }). Nothing is built to run tests, type checks or the development servers: Vitest and Next.js transpile the sources, Node.js strips the types (verified: it works through pnpm’s workspace symlinks, whose real path is outside node_modules). Publishable packages add a tsdown build and switch their entry points in publishConfig; pnpm pack was verified to replace the source exports with the dist ones.
Production artifacts never rely on source resolution. The worker and migrator bundles inline every private @guestcraft/* package (tsdown deps.alwaysBundle), because Node.js refuses to strip types from files copied under node_modules; SQL files and other runtime assets are packaged explicitly. The Next.js standalone output declares the workspace root for file tracing. CI runs each artifact from an isolated directory without the checkout.
Cache correctness. Type checks and tests read the sources of upstream workspace packages, so their Turborepo hash must include them: both tasks depend on a transit task that chains through every workspace dependency, and shared inputs outside the packages (presets, the root Vitest configuration, shared test setup) are global dependencies. The authoritative gate in CI, pnpm test:coverage, is not cached.
No isolatedDeclarations by default. The first draft of this ADR applied it everywhere. The structure review (docs/architecture/reviews/structure-r1.md, finding R01) showed that TypeScript rejects ordinary inferred Zod schemas, Drizzle tables and Hono route chains under it, and that a package’s setting does not protect it when a consumer re-checks its sources with the flag on. Explicit types on public functions remain a design guideline; the flag can be enabled for a small published library whose whole source graph supports it.
Version policy. New dependencies are pinned to exact versions that are at least seven days old, so that the initial selection follows the same rule as the updates Renovate will propose. An exception names the exact version and the reason, and expires.
Release management of the product (one version, changelog, images) uses release-please from the Conventional Commits (ADR 0039). Whether the packages published on npm (SDK and clients) keep that single version or get their own is decided before their first publication.
Consequences
Section titled “Consequences”- Type checking a package also checks the sources of the workspace packages it imports, with the consumer’s options: every preset in
packages/tsconfigmust accept the whole internal source graph, so presets differ only in environment (libraries, JSX, module resolution), never in strictness. Private source consumers do not set a narrowrootDir. exactOptionalPropertyTypesstays on. Wire formats parsed with Zod (where optional means “absent or undefined”) are normalized at the boundary into domain types, instead of weakening the domain types.- The TypeScript 6 API is a second compiler in the dependency tree, used only by tools. A canary check in CI makes sure each of those tools still finds it.
- Seven days of delay on new versions also delays security fixes; urgent ones go through an explicit, expiring exception.
- Only erasable syntax is allowed: no
enum,namespaceor parameter properties. Biome and the compiler reject them.
Alternatives considered
Section titled “Alternatives considered”- Compiled internal packages (each one built to
dist/). Rejected for now: a build step for every change and every test run, and source maps to debug, without benefits inside the monorepo. - TypeScript 6 everywhere. Rejected: TypeScript 7 is the supported line and type checking is an order of magnitude faster.
- swc as dependency-cruiser’s parser. It works without the compiler API, but it drops
import typestatements, which the boundary rules must see. - pnpm’s
pipelineinstead of Turborepo. Promising and native, but new; Turborepo is the tool the analysis chose and is well known to contributors. To be reconsidered.