Skip to content

Research 002 — Monorepo wiring ​

Status: complete

Step 1.5 output, written between the spec draft and the approval gate. Both [NEEDS VERIFICATION] markers in spec.md (V1 on R1, V2 on R5) have a verdict here, so this file is complete. No [NEEDS CLARIFICATION] markers were open.

Verified against, on 2026-07-23: Node v26.5.0 (≥22.18), pnpm 11.15.1, TypeScript 7.0.2, Vitest 4.1.10, @biomejs/biome 2.5.5, @types/node 26.1.1. Evidence comes from a throwaway spike workspace built outside the repo (scratchpad/spike-002/, since discarded per the constitution's second exception) that mirrored the repo's tsconfig shape — nodenext, allowImportingTsExtensions, verbatimModuleSyntax, noEmit — with a package @gw2priory/domain exporting { ".": "./src/index.ts" } and a root test importing it by name.

V1 — Does a workspace package whose exports points at raw .ts resolve under both tsc (TS7, nodenext) and Vitest 4, with no build? ​

Question. R1 assumes a package can be consumed as TypeScript source — exports → ./src/index.ts, no dist/ — and still resolve by name at both typecheck (tsc --noEmit) and test (Vitest) time.

Verdict. Confirmed, conditional on one thing: the consumer must declare the package as a workspace:* dependency (see F3). With that declaration, source packages need no build step and R1 holds as written.

Evidence.

  • Without a workspace:* dependency on the consumer: no symlink is created (ls node_modules/@gw2priory/ → "No such file or directory"), tsc --noEmit fails with TS2307: Cannot find module '@gw2priory/domain', and Vitest fails with Cannot find package '@gw2priory/domain'. The co-located test (relative ./index.ts import) passed regardless.
  • After adding "@gw2priory/domain": "workspace:*" to the consumer's devDependencies and re-running pnpm install: the symlink appears (node_modules/@gw2priory/domain -> ../../packages/domain), tsc --noEmit exits 0, and pnpm exec vitest run reports 2 files / 3 tests passed, including the by-name cross-workspace import in tests/resolution.test.ts.

Caveat. The two resolvers succeed by different routes. tsc's nodenext resolution reads the package exports and accepts the .ts target because allowImportingTsExtensions + noEmit are set. Vitest resolves through Vite's own resolver (esbuild transform), independent of Node's runtime type stripping — so this result does not by itself prove node packages/.../foo.ts at runtime, only typecheck and Vitest, which is all R1/R3/R4 require. The fallback named in R1 (exports conditions) was not needed.

V2 — Does Biome's current release run cleanly on Node ≥22.18 with one config, staying independent of the tsc API (TS7-safe)? ​

Question. R5 makes Biome the sole linter/formatter and rests on it being immune to the TypeScript 7 compatibility risk that couples typescript-eslint to the tsc API.

Verdict. Confirmed. Biome runs cleanly, from one config, and never touches tsc — so TS7 poses no risk to it.

Evidence.

  • @biomejs/biome 2.5.5 installed and ran on Node v26.5.0; biome --version → 2.5.5.
  • biome init generated a single biome.json (schema 2.5.5); biome check . checked 9 files in 6 ms.
  • Independence from tsc: the installed @biomejs/biome package.json reports empty dependencies and peerDependencies — it is a bundled Rust binary that parses TypeScript itself. There is no path by which a TypeScript 7 change could break it.

Caveat. biome check . exited non-zero, but every diagnostic was a style default (tab indent, double quotes, import sorting) differing from the repo's current 2-space / single-quote / no-semicolon style — Biome working, not failing. The plan must (a) choose the format config and (b) run one biome check --write pass; expect it to reformat existing files (scripts/new-spec.ts, .vitepress/config.mts, tests/). This is an adoption decision, not a blocker, and it refutes nothing in the spec.

F3 — Cross-workspace consumption requires an explicit workspace:* dependency in the consumer ​

Not asked about directly, but load-bearing. The apps/* / packages/* glob in pnpm-workspace.yaml makes a package part of the workspace; it does not make it importable elsewhere. pnpm only symlinks a workspace package into a consumer that declares a dependency on it. Importing by name without that declaration fails identically at typecheck (TS2307) and at test time (Cannot find package) — see V1's "without" evidence.

Why it matters. This is the mechanism behind SC1 ("importable by its @gw2priory/* name") and R8 (the root tests/ importing @gw2priory/domain). The plan must add "@gw2priory/domain": "workspace:*" to the root package.json, and later each app that consumes a package must do the same. Nothing in the spec is refuted — R8 stands — but the plan carries this as an explicit step rather than assuming the glob is enough.

Refuted claims ​

None. Both spec claims were confirmed; F3 adds an implementation requirement for the plan without contradicting any spec statement. The spec does not return to step 1.

Graduation ​

Graduated at step 6 into a new docs/architecture/monorepo.md (a dedicated doc rather than stack.md, so the additions do not collide with spec 001's R8 verbatim check on stack.md):

  • The source-package convention — packages/* consumed as TypeScript source via exports → ./src/index.ts, no build, private: true — and its precondition that every consumer declares the package workspace:* (V1 + F3). ✓ moved.
  • Biome as the project's linter/formatter, TS7-agnostic, one biome.json, with the chosen style config (V2). ✓ moved.

CLAUDE.md's Reference section links the new doc.