Move from npm to pnpm across the workspace, CI and the image
CI / verify (push) Successful in 3m23s

The monorepo was on npm workspaces. pnpm gives it a content-addressed store
shared between the eight packages, a lockfile that records the whole graph
rather than a flattened view of it, and — the reason this mattered in practice —
`workspace:*`, which makes an internal dependency unambiguous instead of a
version range that npm may satisfy from the registry.

Mechanics:

  - `packageManager: pnpm@11.21.0` pins the version; corepack installs it in CI
    and in the image, so all three environments resolve identically.
  - The npm `workspaces` array is replaced by `pnpm-workspace.yaml`. pnpm
    ignores the former, and keeping both would leave two sources of truth.
  - All six internal dependencies moved to `workspace:*`.
  - Root scripts use `pnpm -r --if-present` and `pnpm -F <pkg>`.

Two findings worth recording, both from running it rather than reading it:

`tsx` was a devDependency, but the server runs TypeScript directly in
production — the container's command is `pnpm exec tsx apps/api/src/server.ts`.
Under npm this was concealed by the runtime stage re-installing tsx by hand
after pruning dev dependencies. Under `pnpm install --prod` that sleight of
hand stops working and the image simply fails to start. tsx is now declared in
`dependencies`, which is what it has always actually been.

The first image build failed with ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY.
That is not a pnpm bug: it had decided the modules directory was stale and
wanted confirmation before deleting it, which a non-interactive build cannot
give. The trigger was the host's `node_modules` reaching the build context —
there was no `.dockerignore` at all. pnpm's tree is symlinks into a
content-addressed store, so copying it into an image produces dangling links
and a directory pnpm rightly considers corrupt. Fixed by adding
`.dockerignore` and setting `CI=true`, which is required in any non-interactive
pnpm build.

`esbuild` is denied install scripts via `allowBuilds`. Its platform binary
arrives through the optional dependency `@esbuild/linux-x64` and the postinstall
only verifies it; confirmed by running the binary directly, which reports
0.25.12.

Verified under pnpm: typecheck clean, 150 tests / 0 failures, e2e passes, web
builds. The image was built and booted against a real Postgres — health ok,
`/api/dashboard` 401 with an issuer configured, `/` and `/capacity` serve the
SPA, `/og.png` serves as image/png, and the migrator runs from the pruned
runtime stage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-13 04:15:54 -07:00
parent 2b50797349
commit a6167629cc
19 changed files with 5270 additions and 7578 deletions
+29 -7
View File
@@ -52,8 +52,15 @@ docs/ ontology.md, build-plan.md, agents.md, deploy.md, seed-data.md
## 3. Running it
**PIG uses pnpm**, pinned by the `packageManager` field. Do not run `npm
install` — it will write a `package-lock.json` that nothing reads and resolve a
dependency tree that neither CI nor the image uses. Corepack ships with Node and
installs the pinned version for you:
```bash
npm install
corepack enable
pnpm install
# Postgres. PIG needs its own database — never point it at a shared one.
docker run -d --name pig-dev -p 5432:5432 \
@@ -61,12 +68,12 @@ docker run -d --name pig-dev -p 5432:5432 \
postgres:16-alpine
export DATABASE_URL=postgres://pig:pig@localhost:5432/pig
npm run db:migrate
npm run db:seed # sourced, cited people — optional
npm run db:demo # a plausible demo book — optional, prefixed "DEMO — "
pnpm run db:migrate
pnpm run db:seed # sourced, cited people — optional
pnpm run db:demo # a plausible demo book — optional, prefixed "DEMO — "
npm run dev:api # :8920
npm run dev:web # :5173, proxies /api to 8920
pnpm run dev:api # :8920
pnpm run dev:web # :5173, proxies /api to 8920
```
With no `SUPABASE_URL` set, **authentication is disabled in development** and
@@ -76,7 +83,7 @@ in production without it, so this cannot leak.
Before pushing:
```bash
npm run typecheck && npm test
pnpm run typecheck && pnpm test
```
---
@@ -169,6 +176,21 @@ document.documentElement.scrollWidth - document.documentElement.clientWidth
It should be 0 on every route at 393px wide.
**pnpm needs `CI=true` in any non-interactive build.** When it decides a
modules directory is stale it asks before removing it; with no TTY it cannot
ask, so it aborts with `ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY`. This reads
like a pnpm bug and is not — it is pnpm refusing to delete files nobody
confirmed. Both the Dockerfile and CI set it. The usual trigger is a host
`node_modules` reaching the build context, which is why `.dockerignore` exists:
pnpm's tree is symlinks into a content-addressed store, so copying it into an
image yields dangling links and a modules directory pnpm considers corrupt.
**`tsx` is a production dependency, not a dev one.** The server runs TypeScript
directly — `pnpm exec tsx apps/api/src/server.ts` is the container's command —
so pruning it away breaks the image. It lives in `dependencies` deliberately;
moving it back to `devDependencies` because "it's a build tool" makes
`pnpm install --prod` produce an image that cannot start.
**Drizzle-generated migrations are not always valid SQL.** A `jsonb → integer`
cast was emitted without the `USING` clause Postgres requires. Always apply a
new migration to a real empty database before pushing — CI does this, but find