Skip to content

chore: move the workspace from bun to pnpm and upgrade Nx to 23 - #45

Merged
codemancers-bot merged 2 commits into
mainfrom
chore/migrate-bun-to-pnpm-and-nx-23
Sep 20, 2026
Merged

codemancers-bot merged 2 commits into
mainfrom
chore/migrate-bun-to-pnpm-and-nx-23

Conversation

@codemancers-bot

Copy link
Copy Markdown
Contributor

Switches the workspace from bun to pnpm, and upgrades Nx along with it.

Package manager

pnpm 11.26.0, pinned via packageManager and installed through Corepack. .npmrc is gone; pnpm settings now live in pnpm-workspace.yaml, each with a comment explaining why:

Setting Why
nodeLinker: hoisted Keeps bun's flat layout. apps/meeting-bot and apps/backend-e2e are not workspace members and resolve from the root install.
autoInstallPeers: false + strictPeerDependencies: false Stands in for legacy-peer-deps=true. Auto-installing peers also pulled @better-auth/core 1.7.x alongside 1.6.22 and blew up resolution.
verifyDepsBeforeRun: false Otherwise every pnpm exec nx … shells out to a full install.
allowBuilds pnpm blocks dependency build scripts unless approved. @scarf/scarf is denied on purpose — analytics, not a build.
overrides Was overrides + a duplicate resolutions in package.json.

Internal @zuko/* deps now use workspace:*. Besides being the pnpm convention, @nx/js:prune-lockfile rewrites that protocol to file: paths itself — so the fix-lockfile-symlinks sed workaround is deleted.

⚠️ Dependency drift — the part worth reviewing

The bun lockfile could not be carried over. pnpm import only preserves direct dependency resolutions; regenerating re-resolved every caret range to today's newest match. 494 packages moved. Two broke the workspace and are pinned back to what bun.lock held:

  • better-auth1.6.22 (was ^1.6.22). 1.7.x drops the verifyAccessToken export mcp-bearer.guard.ts imports — the same breakage a6a925a fixed.
  • oxlint1.71.0 (was ^1.62.0). 1.83 adds React rules that fail on 25 pre-existing violations in web, ui-kit and meeting-bot.

Both are one-line reverts when you want those upgrades. The other 492 moves are untested beyond the checks below.

Nx 22.4.3 → 23.2.1

Nx 22 cannot parse a pnpm 12 lockfile, which is what capped us at pnpm 11.

nx migrate also wanted TypeScript 6, Vite 8, @vitejs/plugin-react 6 and webpack-cli 7. Nx 23 requires none of them (vite ^5–^8, webpack-cli ^5–^7, no TS peer), so those ranges are unchanged and left for their own upgrade.

33 of 41 migrations applied. Six skipped as inapplicable: two TypeScript 6 tsconfig preps, the Vite 8 rollupOptionsrolldownOptions rename, two "write an AI prompt file" migrations for Vite 8 and a React 18→19 upgrade already done, and the interactive analytics prompt (nx.json sets neverConnectToCloud: true).

The Vitest 4 migration found one real hit: apps/harness/vitest.config.mts used poolOptions.forks.{min,max}Forks, which Vitest 4 flattened to a top-level maxWorkers. Vitest 4 was already installed, so that CI serialization gate had silently stopped taking effect.

Backend image

Nx 23's prune-lockfile emits a dist/pnpm-workspace.yaml carrying the workspace's build-script approvals, so the image inherits exactly those rather than allowing every build script. It's declared in the target's outputs — Nx's own migration skipped that because our outputs list was hand-edited, and without it a cache replay ships a dist with no approvals and prisma generate fails in the image.

Verification

Clean install, --frozen-lockfile, nx affected, typecheck, build, lint, knip across all 11 projects, fmt:check, 857 unit tests, the prune pipeline both fresh and cache-replayed, and the backend image path simulated end to end — install → prisma generate → Nest boots.

Not run: the Playwright e2e suite (needs a chromium download and the full stack).

Follow-ups, deliberately not in this PR

  • @nx/vitest:test is deprecated and removed in Nx 24; nx g @nx/vitest:convert-to-inferred rewrites every project's test target and risks dropping per-project options like the harness's parallelism: false.
  • @berenddeboer/nx-knip@1.1.0 declares @nx/devkit peers of ^20 || ^21 || ^22. Knip runs fine on 23, but it's unsupported until they widen the range.
  • Adding apps/meeting-bot and apps/backend-e2e to the workspace would let nodeLinker: hoisted go and restore pnpm's phantom-dependency protection.
  • pnpm 12 is unblocked by the Nx upgrade.

🤖 Generated with Claude Code

codemancers-bot and others added 2 commits September 20, 2026 13:23
bun was not buying us anything here, so the package manager is now pnpm
11.26.0, pinned via `packageManager` and installed through Corepack.

The lockfile could not be carried over. `pnpm import` only preserves
direct dependency resolutions, so regenerating re-resolved every caret
range to today's newest match -- 494 packages moved. Two of those bumps
broke the workspace and are pinned to what bun.lock held:

  - better-auth 1.6.22. 1.7.x drops the `verifyAccessToken` export that
    mcp-bearer.guard.ts imports -- the same breakage a6a925a fixed.
  - oxlint 1.71.0. 1.83 adds React rules that fail on 25 pre-existing
    violations in web, ui-kit and meeting-bot.

pnpm settings live in pnpm-workspace.yaml, replacing .npmrc's
legacy-peer-deps. `nodeLinker: hoisted` keeps bun's flat layout, which
apps/meeting-bot and apps/backend-e2e depend on -- neither is a
workspace member, so both eat from the root install.

Internal @zuko/* deps now use the `workspace:*` protocol. That is the
pnpm convention, and it lets @nx/js:prune-lockfile rewrite them to
`file:` paths itself, so the fix-lockfile-symlinks sed workaround is
gone.

Nx 23.2.1 comes along because Nx 22 cannot parse a pnpm 12 lockfile.
`nx migrate` also wanted TypeScript 6, Vite 8, @vitejs/plugin-react 6
and webpack-cli 7; Nx 23 requires none of them, so those ranges are
unchanged and left for their own upgrade. Of 41 migrations, 33 applied;
the six skipped were TypeScript 6 and Vite 8 preparation, an AI-prompt
migration for a React 19 upgrade already done, and the interactive
analytics prompt this workspace opts out of. The Vitest 4 migration
found one real hit: the harness CI gate used poolOptions.forks
min/maxForks, which Vitest 4 flattened to a top-level maxWorkers, so
that serialization had stopped taking effect.

Nx 23's prune-lockfile emits a dist/pnpm-workspace.yaml carrying the
workspace's build-script approvals, so the backend image inherits those
instead of allowing every build script. It is declared in the target's
outputs, otherwise a cache replay ships a dist with no approvals and
`prisma generate` fails in the image.

Verified: clean install, --frozen-lockfile, nx affected, typecheck,
build, lint, knip across 11 projects, fmt:check, 857 unit tests, the
prune pipeline fresh and cache-replayed, and the backend image path
simulated end to end (install, prisma generate, Nest boots). The
Playwright e2e suite was not run locally.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
setupSprite clones the repo into the sprite and ran `bun install`. After
the pnpm migration that clone carries a pnpm-lock.yaml and a
`packageManager: pnpm` field and no bun.lock, so the next provision
would have installed from nothing deterministic -- a path no build or
test in this repo exercises, since it only runs against a live sprite.

Corepack resolves pnpm from the cloned repo's `packageManager` field, so
nothing needs baking into the sprite image. `corepack enable` symlinks
into Node's bin directory, which an unprivileged sprite user cannot
write, hence the user-local fallback. Each exec is its own shell, so
startServer prepends the same prelude.

The install is now `--frozen-lockfile`: a fresh clone of a committed
lockfile should fail loudly on drift rather than silently resolve
something new.

@langchain/langgraph-cli is not a workspace dependency, so `bunx` was
fetching it on demand -- that is `pnpm dlx`, not `pnpm exec`.

Also converts the two app READMEs the earlier docs pass missed
(apps/harness, apps/web-e2e), and rewords the shiki/Turbopack note in
next.config.ts, which blamed bun's node_modules layout for a workaround
that is still needed under pnpm's hoisted layout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codemancers-bot

Copy link
Copy Markdown
Contributor Author

Follow-up in cffa850 — two misses in the first pass, both real:

sprites.service.ts ran bun install inside provisioned sprites (and bunx @langchain/langgraph-cli). Nothing in CI reaches that code path — it only executes against a live sprite — so it would have broken on the next provision regardless of a green build. Now uses Corepack to resolve pnpm from the cloned repo's packageManager field, with a user-local fallback because corepack enable needs a writable Node bin dir. The install is --frozen-lockfile now; a fresh clone of a committed lockfile should fail loudly on drift. @langchain/langgraph-cli isn't a workspace dep, so bunx was fetching on demand — that maps to pnpm dlx, not pnpm exec.

apps/harness/README.md and apps/web-e2e/README.md — my docs pass only covered the root README and docs/.

Also reworded the shiki/Turbopack note in next.config.ts, which blamed bun's node_modules layout; the workaround is still needed under pnpm's hoisted layout, so only the comment changed.

⚠️ The sprite path is the one thing in this PR I could not verify end to end — it needs the Fly sprites API and credentials. I tested the Corepack prelude locally, including the permission fallback, and it resolves pnpm 11.26.0 from the pinned field. What's untested is whether the sprite image has Node/Corepack available at all. Worth one manual provision before relying on it.

@codemancers-bot
codemancers-bot merged commit bd3c17b into main Sep 20, 2026
1 check passed
@codemancers-bot
codemancers-bot deleted the chore/migrate-bun-to-pnpm-and-nx-23 branch September 20, 2026 09:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant