Skip to content

Adopt Bun as the package manager and script runner #16

Description

@AlaskanTuna

The owner has decided to adopt Bun and has reaffirmed it after being warned that it reverses this repo's own Critical Do-Not ("Do not use npm, npx, or bun in this repo — pnpm only") and their personal global preference file, which currently carves this repo out as pnpm. That reversal is settled; this task plans how to land it safely, not whether to land it. The real engineering content is that "adopt Bun" is three different changes with three very different risk profiles, and the owner's brief ("Bun runtime", listed under "workspace tooling") does not say which is meant:

Scope What changes Risk
S1 — package manager + script runner bun install / bun run, bun.lock, workspaces move into root package.json, allowBuildstrustedDependencies Low. Node still executes everything
S2 — backend runtime bun dist/server.js (or bun src/server.ts) replaces node dist/server.js and tsx Medium–high. Prisma engine, sharp N-API addon, deploy target
S3 — test runner bun test replaces Vitest across ~260 tests High. Reject

Scope: package.json (root, backend/, frontend/, shared/), pnpm-workspace.yaml (delete), pnpm-lock.yaml (delete), bun.lock (new), .npmrc (delete), .bun-version (new), bunfig.toml (new, only if needed), Procfile / heroku.yml, Dockerfile (from task 21), .github/workflows/ci-cd.yml, README.md, RUNBOOK.md

Depends on: #8, #15, 20, 21 — and not services/pdf-service, which stays on npm (see below)

Checklist

  • Record the exact baseline first, on main: pnpm install wall-clock from a cold store, pnpm build wall-clock, and the exact passing test counts for backend and frontend. Every later comparison is against these numbers, not against memory
  • On branch spike/bun, convert the workspace: move packages: from pnpm-workspace.yaml into root package.json "workspaces": ["shared", "backend", "frontend"] (keeping services/pdf-service out, exactly as today), transcribe allowBuilds into "trustedDependencies" in full (a partial list silently disables the default allowlist), delete .npmrc, and replace "packageManager": "pnpm@…" with a .bun-version file — leave engines.node: 24.x alone, Node still runs the toolchain
  • bun install, then confirm bun.lock is text and committed, and that bun install --frozen-lockfile on a clean clone reproduces the tree without rewriting the lockfile
  • Rewrite the eleven root scripts from pnpm --filter X to the Bun equivalents. Consider dropping the concurrently dependency in favour of bun run --filter '*' dev, but only if it actually reproduces the current three-watcher behaviour
  • Run the four gates under Bun with everything still executing on Node: bun run typecheck, bun run lint, bun run test, bun run build. Test counts must match the Phase A baseline exactly — a drop is a silently skipped suite, not a win
  • Exercise the two native/foreign-toolchain paths explicitly, because they are the ones that break: bunx --bun prisma generate (note whether the npm-alongside-Bun requirement bites; if it does, document that the Prisma CLI runs through Node and move on — it is not a blocker for S1), and a sharp code path (run the RGB-conversion-touching backend tests, or resolve one location locally) to prove the N-API addon loads under a Bun-installed tree
  • Record install and build wall-clock under Bun and write both numbers into the issue. If the win is not material, that is worth knowing before the deploy work
  • With task 21's container in place, rebuild the image with Bun as the installer and deploy it to a Heroku staging app, not production. The lockfile swap does not merge until a real deploy of that image has served GET /api/health
  • If the container route is not chosen, evaluate a community Bun buildpack (jmlow/heroku-buildpack-bun, kolide/heroku-buildpack-bun, or the Fir CNB) as the cheap escape hatch — and record the trade-off honestly: an unmaintained third-party buildpack on the only live production host is a worse dependency than a Dockerfile the project controls
  • Update .github/workflows/ci-cd.yml: pnpm/action-setupoven-sh/setup-bun with bun-version-file, cache-dependency-pathbun.lock, and every pnpm … step to its Bun form. Keep task 16's four gates (build, typecheck, lint, test) intact — this task must not quietly reduce CI coverage
  • Fix the services/pdf-service install leg while in this file. Its committed lockfile is package-lock.json, so task 16's planned pnpm --dir services/pdf-service install --frozen-lockfile cannot work as written — --frozen-lockfile wants a pnpm-lock.yaml that does not exist. Use npm ci in that directory, which matches what Vercel actually does for that project. This is a pre-existing bug in task 16, not something Bun introduces — flag it there too
  • Update README.md and RUNBOOK.md end to end: prerequisite list, install command, every pnpm invocation, and the CI/deploy description
  • Only after S1 is merged and stable: on a second spike branch, switch the backend start to Bun and run the backend suite under bun --bun. Keep it in its own commit so it can be reverted independently of S1
  • Evaluate whether engineType = "client" + runtime = "bun" + a driver adapter is required. If it is, stop and re-plan — that change belongs beside task 5's Neon datasource decision, not bolted onto a runtime swap, and it is a Prisma-behaviour change, not a tooling change
  • Abort conditions (any one is sufficient to ship S1 only and record why): sharp fails to load or produces different output; Prisma needs schema changes that contradict task 5's settled Neon configuration; any currently-passing backend test fails under bun --bun; idle RSS regresses (Render free is 512 MB); the Solar API pipeline wall-clock regresses
  • Whatever the outcome, write one line into docs/decisions.md at Gate 2 recording the adopted scope and the reason — this reverses a Critical Do-Not and must be legible to the next planning session
  • Tag the last pnpm commit pre-bun before merging, so restore is one command. Do not keep both lockfiles in the tree — two lockfiles drift and confuse both CI and any buildpack
  • Name the cheap-reversal points explicitly in the issue: (1) spike branch — free; (2) after S1 merge, before the deploy switch — git checkout pre-bun -- package.json pnpm-workspace.yaml .npmrc pnpm-lock.yaml && rm bun.lock && pnpm install --frozen-lockfile, one revert commit; (3) after the deploy/stack switch — reversal now also means reverting the Heroku stack and redeploying, so this is where rollback stops being cheap; (4) after S2 — cheap again only if the runtime switch stayed in its own commit
  • verify: bun install --frozen-lockfile on a fresh clone, then bun run build && bun run typecheck && bun run lint && bun run test all green, with test counts identical to the Phase A baseline
  • verify: the staging deploy serves GET /api/health, and a cached project loads end-to-end (workbench opens, a panel drag recomputes flux) with zero outbound calls to solar.googleapis.com — the project's core cost constraint must survive a toolchain change
  • verify: git grep -nE '\bpnpm\b' returns nothing in README.md, RUNBOOK.md, Procfile/heroku.yml, Dockerfile, or .github/workflows/ci-cd.yml
  • verify: the rollback is actually rehearsed on a scratch branch — check out the pre-bun files, pnpm install --frozen-lockfile, pnpm build succeeds. A documented rollback that has never been run is not a rollback

Priority: Medium · Phase 12 task 18 of the maintainer’s migration plan.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2-mediumShould fix, not blockinginfraHosting, DNS, CI/CDtoolingBuild, lint, monorepo, developer workflow

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions