Skip to content

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

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:

  1. TypeScript 7.0 ships no compiler API. The typescript@7.0.2 package contains the native compiler (tsc) only. Tools that import typescript as 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 .ts file without it.
  2. pnpm 12 is a rewrite with new settings. Build scripts of dependencies are blocked unless approved in allowBuilds (strictDepBuilds is on by default); minimumReleaseAge is in minutes and becomes strict when set explicitly; trustPolicy: no-downgrade rejects a version whose publishing evidence is weaker than an earlier one. It also ships native release management (pnpm change, pnpm version -r).
  3. Node.js 24 runs TypeScript directly by stripping types, as long as the code uses only erasable syntax. Verified on Node 24.19.
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.

  • Type checking a package also checks the sources of the workspace packages it imports, with the consumer’s options: every preset in packages/tsconfig must 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 narrow rootDir.
  • exactOptionalPropertyTypes stays 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, namespace or parameter properties. Biome and the compiler reject them.
  • 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 type statements, which the boundary rules must see.
  • pnpm’s pipeline instead of Turborepo. Promising and native, but new; Turborepo is the tool the analysis chose and is well known to contributors. To be reconsidered.