Skip to content

Monorepo conventions ​

How the pnpm workspace is wired, established by spec 002 and verified rather than assumed. Graduated from specs/002-monorepo-wiring/research.md so the next spec does not re-derive it.

Verified 2026-07-24 against pnpm 11.15.1, Node 26.5.0 (≥22.18), TypeScript 7.0.2, Vitest 4.1.10, Biome 2.5.5.

Packages are TypeScript source, not builds ​

packages/* are consumed as source. Each package resolves its public entry through exports to a .ts file, emits no dist/, and is private: true (nothing is published):

jsonc
// packages/domain/package.json
{ "name": "@gw2priory/domain", "private": true, "type": "module",
  "exports": { ".": "./src/index.ts" } }

This extends the existing "Node runs TypeScript directly, tsconfig is noEmit" model (workflow-tooling.md) to packages rather than introducing a build step. Both tsc (TS 7, nodenext) and Vitest 4 resolve such a package by name with no build — confirmed in spec 002's research.md (V1).

The precondition — a consumer must declare the dependency. The packages/* glob makes a package part of the workspace; it does not make it importable elsewhere. pnpm only links a package into a consumer that declares it. A consumer therefore adds:

jsonc
"devDependencies": { "@gw2priory/domain": "workspace:*" }

Without it, an import by name fails identically at typecheck (TS2307) and at test time (Cannot find package). This is spec 002's F3 finding; the root — and later each app — carries the declaration.

Package scope is @gw2priory/*.

But the SWC-built api cannot consume a source-.ts package at runtime. tsc and Vitest resolve a source package fine (they follow the symlink to the real path and transform it), but the compiled api runs as plain node dist/main.js, and Node refuses to type-strip files under node_modules — where pnpm links workspace packages: require('@gw2priory/foo') / import() of a package whose exports point to ./src/index.ts fails with "Stripping types is currently unsupported for files under node_modules" (measured, Node 26, spec 006 research.md F12). So an api service must consume a package's data as a JSON subpath (exports: { "./data": "./data/x.json" } → require('@gw2priory/foo/data'), which loads at runtime because JSON is not type-stripped) and import its types only (import type, erased by SWC) — never a runtime value from the package root. Any future api-consumes-package case takes this shape (or a built artifact); packages/legendary-recipes is the first instance.

Update (spec 008, Node 26) — F12 is narrower than stated above; re-verify before relying on it. 008's price engine imports a runtime value (netSellPrice) from the package root of @gw2priory/domain (a source-.ts workspace package, exports: "./src/index.ts"), and it loads fine in the SWC-built api at runtime. Measured: after pnpm --filter @gw2priory/api build, node -e "require('@gw2priory/domain').netSellPrice(1000)" returns 850 with no type-strip error, because pnpm symlinks the package and require.resolve returns its realpath (packages/domain/src/index.ts) — which is outside node_modules, so Node 26 strips its types. This directly contradicts the "never a runtime value from the package root" rule as written. The rule may still hold for legendary-recipes under different conditions (e.g. its import … with { type: 'json' } attribute, or a hoisted/non-symlinked layout), so CuratedRecipeService's JSON-data pattern is left unchanged — but the blanket claim needs re-measuring and either narrowing or removal. Tracked as a follow-up; do not treat F12 as an absolute blocker without re-testing your specific case.

One config, one typecheck, one test run ​

  • Typecheck: tsconfig.base.json holds environment-agnostic strictness/syntax plus a pinned non-DOM lib floor (["ES2023"]) — added by spec 004 (research.md F8, reconciling the original "only strictness/syntax" statement here: TypeScript's default lib includes DOM, so leaving it unset would let DOM globals leak into every profile, including apps/api). The root tsconfig.json extends it for packages/**/tests/**/scripts/**.
  • When apps arrived (spec 004: apps/web React, apps/api NestJS), each brought its owntsconfig.json extending the same base with profile overrides (DOM+JSX for web; decorators for api) — options that cannot coexist in one compilerOptions. The one-command guarantee is a root script chaining a tsc --noEmit per project, not project references: tsc --noEmit && tsc --noEmit -p apps/api/tsconfig.json && tsc --noEmit -p apps/web/tsconfig.json. Project references were tried and rejected (research.md V5): they work under TS 7 but force emit (.d.ts + .tsbuildinfo) and shift a referenced package's resolution from source to built declarations, breaking the "packages are source, not builds" model above. packages/** itself still needs no per-package config — only the apps, once genuinely divergent, needed their own.
  • Test: a single vitest.config.ts uses the test.projects key (spec 004, research.md V6) — one node project covering packages/**/tests/** (include: ['packages/**/*.test.ts', 'tests/**/*.test.ts']), plus one project per app in its own environment (apps/api node, apps/web jsdom). jsdom is a root dev dependency; the older vitest.workspace file is deprecated in favour of test.projects. The api's Vitest project additionally needs unplugin-swc to emit decorator metadata under test, mirroring the SWC build step (stack.md).

Lint and format: Biome only ​

Biome is the sole linter and formatter — no ESLint, no Prettier. One biome.json; scripts are lint = biome check . and format = biome check --write .. Biome honours .gitignore (vcs.useIgnoreFile). It is a bundled Rust binary independent of the tsc API, so TypeScript 7 poses no compatibility risk (spec 002 research.md V2). House style: 2-space indent, single quotes, semicolons.

Orchestration: plain pnpm ​

Task orchestration is plain pnpm scripts. No Turborepo, Nx, Moon, or other build/cache tool — mirroring "no Redis until the caching story earns it": no cache/orchestration layer until build or CI time earns it. Turborepo is the intended drop-in upgrade, deferred to its own spec.

Reference ​

  • specs/002-monorepo-wiring/ — the spec, discovery, plan, and tasks that established all of the above.
  • specs/004-app-foundation/ — graduated the one-typecheck/one-test topology once apps arrived (the per-project tsc --noEmit script, the lib floor, and Vitest test.projects).
  • docs/architecture/stack.md — the higher-level stack decision this refines.