Spike: npm -> pnpm#339
Conversation
- Add pnpm-workspace.yaml, .npmrc and pnpm-lock.yaml generated via pnpm import - Replace `npm` engine constraint with `pnpm >=9 <10` and packageManager field - Rewrite root scripts: npm run/npx → pnpm/pnpm exec, chakra:typegen via pnpm dlx - Move npm overrides to pnpm.overrides; declare onlyBuiltDependencies allowlist for native modules (bcrypt, @swc/core, esbuild, @nestjs/core, @swc/cli) - Switch internal workspace refs from "@packmind/*: *" to "workspace:*" so pnpm resolves them via the workspace instead of the registry
…tion pnpm no longer hoists transitive dependencies into root node_modules, so several modules that used to be reachable via npm's flat layout now need to be declared explicitly: - Root: add express, body-parser, zod, @types/express, @types/body-parser - apps/frontend: depend on @packmind/assets via workspace protocol - apps/cli: add minimatch, undici, dotenv - packages/ui: depend on @packmind/assets and @zag-js/checkbox - packages/coding-agent: add @types/archiver - packages/node-utils: add @types/express Also widen jest transformIgnorePatterns so they keep matching paths nested under node_modules/.pnpm/<pkg>@<ver>/node_modules/<pkg>/ — the previous patterns assumed the flat npm layout and were skipping ESM-only packages like slug under pnpm. Hoist @types/*, eslint/* and prettier/* via public-hoist-pattern in .npmrc to mirror npm's ergonomics for type/lint resolution without sacrificing pnpm's strictness for runtime code.
- Add pnpm/action-setup@v4 alongside actions/setup-node@v4 in every job - Swap setup-node cache: 'npm' for cache: 'pnpm' so the Node setup primes the pnpm store - Replace `npm ci --ignore-scripts --no-audit --no-fund` with `pnpm install --frozen-lockfile` - Update `npm run <script>` invocations to `pnpm <script>` in quality.yml, build.yml and tmp-cli-lint-windows.yml - Ship pnpm-lock.yaml instead of package-lock.json in the API artifact (build.yml) - Switch the production-CLI smoke install (build.yml) to `pnpm init` + `pnpm add @packmind/cli` End-user release notes in publish-cli-release.yml still reference `npm install -g @packmind/cli`, which is intentional — consumers should keep installing the published CLI with whatever package manager they use.
…ervices - Dockerfile.api/Dockerfile.mcp: activate pnpm 9.15.0 via corepack, swap `npm install --omit=dev` for `pnpm install --prod` plus `pnpm store prune`, and extend the final cleanup step to remove the pnpm/corepack shims along with npm - Dockerfile.mcp: drop the package-lock.json COPY (Nx no longer ships a lockfile next to the dist) and use `pnpm install --no-lockfile --prod` for the standalone install - apps/api/docker-package.json: replace the npm engine constraint with a pnpm range so the runtime image doesn't fail engine checks after npm is removed - docker-compose.yml: install-dependencies/run-migrations/run-e2e-tests services now activate pnpm via corepack and call `pnpm install --frozen-lockfile`, `pnpm typeorm`, and `pnpm e2e` - .husky/pre-commit: switch `npx pretty-quick --staged` to `pnpm exec pretty-quick --staged`
webpack was reaching the root .bin via npm's flat hoisting of webpack-cli's transitive deps. Under pnpm's strict layout the binary is only symlinked when webpack is listed directly, so the api webpack build fails with `webpack: not found`. Listing webpack alongside webpack-cli restores the binary at node_modules/.bin/webpack.
The story files import from '@storybook/react' (Meta, StoryObj types), but it was only reachable as a transitive of @storybook/react-vite. Under pnpm strict mode the package is no longer hoisted to the root, so tsc fails with TS2307. Listing it explicitly at the same version (10.4.0) keeps the types visible during frontend:typecheck.
The node-compile-cache/ directory is materialized at runtime by Node 22+ whenever NODE_COMPILE_CACHE is set; it shouldn't be tracked.
|
@greptile review this |
Greptile SummaryThis PR migrates the monorepo package manager from npm to pnpm 9.15.0, touching CI workflows, Dockerfiles, docker-compose, and all workspace
Confidence Score: 5/5Safe to merge — this is a tooling migration with no changes to application logic; the core migration mechanics (frozen lockfile in CI, corepack shared COREPACK_HOME, workspace:* protocol) are all correct. The npm to pnpm switch is well-executed across CI, Docker, and local dev tooling. The two gaps flagged (API Dockerfile not copying its lockfile unlike MCP, and CLI dist deps installed without version pinning) affect build reproducibility rather than runtime correctness. No application code is touched. dockerfile/Dockerfile.api (missing pnpm-lock.yaml COPY before the first pnpm install) and scripts/install-dist-cli-deps.sh (--no-frozen-lockfile makes CLI artifact deps non-reproducible). Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[git push] --> B[GitHub Actions CI]
B --> C[pnpm action-setup v4]
C --> D[pnpm install frozen-lockfile]
D --> E{Build Jobs}
E --> F[API build]
E --> G[MCP build]
E --> H[CLI build]
F --> F2[cp pnpm-lock.yaml to dist/apps/api]
G --> G2[cp pnpm-lock.yaml to dist/apps/mcp-server]
H --> I[install-dist-cli-deps.sh no-frozen-lockfile]
F2 --> J[Dockerfile.api - no lockfile COPY]
G2 --> K[Dockerfile.mcp - copies pnpm-lock.yaml]
J --> N[API Docker Image - non-reproducible deps]
K --> O[MCP Docker Image - reproducible deps]
Reviews (7): Last reviewed commit: "🐛 fix(scripts): drop frozen-lockfile fr..." | Re-trigger Greptile |
…dled binary The published CLI declares inquirer/zod/semver/which/etc. as runtime dependencies because their pure-ESM nature makes them awkward to bundle into the CJS output. Under npm's flat layout those deps were hoisted to the workspace root, so calling `node dist/apps/cli/main.cjs` from anywhere in the repo just worked. Under pnpm's strict layout they only live inside `apps/cli/node_modules/.pnpm/…`, which the dist binary can't reach. Install the dist's declared deps in place (via `pnpm install --ignore-workspace`) before any step that exercises the binary out of `dist/apps/cli`: - build.yml: new step in build-cli and cli-e2e-tests jobs - tmp-cli-lint-windows.yml: same step before the lint run - precommit-lint.sh: lazy install if `dist/apps/cli/node_modules` is missing - packmind-cli:lint script: chain the install before the lint invocation
Greptile SummaryThis spike migrates the monorepo's package manager from npm to pnpm 9.15.0, replacing
Confidence Score: 4/5The CI and workflow changes are clean and consistent. The Docker-related changes work correctly but have a few inefficiencies worth addressing before this is finalised. The core migration is solid. The remaining concerns are build-time inefficiencies: corepack is prepared as root but pnpm runs as the node user (potential re-download per build), pnpm store prune is called between two sequential installs in Dockerfile.api, and three docker-compose services each call corepack prepare at entrypoint startup. None of these break functionality, but they add fragility in network-restricted environments and slow down Docker builds. dockerfile/Dockerfile.api and docker-compose.yml warrant a second look for the corepack user-context and repeated network-dependent pnpm activation patterns. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Developer pushes code] --> B[GitHub Actions: pnpm/action-setup@v4]
B --> C[actions/setup-node with cache: pnpm]
C --> D[pnpm install --frozen-lockfile]
D --> E{Job type}
E -->|build| F[nx build / test / typecheck]
E -->|quality| G[nx lint + prettier:check]
E -->|docker| H[Build Docker image]
H --> I[Dockerfile.api / Dockerfile.mcp]
I --> J[USER root: corepack enable and prepare pnpm@9.15.0]
J --> K[USER node: pnpm install --prod]
K -->|Dockerfile.api| L[pnpm store prune]
L --> M[migrations: pnpm install --prod]
M --> N[pnpm store prune]
N --> O[Remove npm/pnpm/corepack binaries]
K -->|Dockerfile.mcp| P[pnpm install --prod --no-lockfile]
P --> Q[Remove npm/pnpm/corepack binaries]
Reviews (2): Last reviewed commit: "🐛 fix(cli): install dist runtime depend..." | Re-trigger Greptile |
…_modules layout
The package's jest.config.ts kept its own inline transformIgnorePatterns
('node_modules/(?!(slug)/)') instead of importing the shared helper, so the
pnpm-aware widening done elsewhere didn't reach it. Under pnpm, slug now lives
at node_modules/.pnpm/slug@11.0.1/node_modules/slug/slug.js and the negative
lookahead fails to spot it, marking the ESM-only module as untransformed.
Mirror the pattern used by the other workspaces so the matcher walks deep paths
correctly.
- Dockerfile.mcp (P1): restore a lockfile-driven install so docker builds are reproducible again - build.yml now ships pnpm-lock.yaml inside the mcp-server dist artifact, mirroring the api job - the Dockerfile re-copies the lockfile and drops the --no-lockfile escape hatch; pnpm install --prod now resolves transitive versions from the workspace lockfile instead of picking the latest semver match on every build - .npmrc: scope the types hoist pattern to @types/* so we don't also hoist unrelated packages whose name happens to contain "types" (typeorm, @testing-library/types, custom workspace packages) - chakra:typegen: replace pnpm dlx (which always fetches the latest @chakra-ui/cli from the registry) with pnpm --filter ./packages/ui exec chakra, and pin @chakra-ui/cli as a devDependency of packages/ui — generated types are now identical between devs and CI
…re across boots Greptile flagged three remaining optimisations on the pnpm migration: - Dockerfiles (api + mcp): corepack prepare runs as root while pnpm install runs as the node user. Each user keeps its own COREPACK_HOME, so pnpm was being fetched twice during every image build. Setting COREPACK_HOME=/usr/local/share/node/corepack and chmod a+rX after the prepare step lets both users see the same binary — pnpm is downloaded exactly once. - Dockerfile.api: drop the intermediate `pnpm store prune` between the /app and /app/migrations installs. The two projects share transitive packages; pruning between them forced pnpm to refetch what the migrations install needed. A single prune after the migrations install reaches the same final image size without the extra network round-trips. - docker-compose: the install-dependencies / run-migrations / run-e2e-tests services each ran `corepack prepare pnpm@9.15.0 --activate` at every cold start, hitting the public registry every time (and failing in air-gapped contexts). Two new named volumes (dev-corepack-cache, dev-pnpm-store) now persist the pnpm binary and the content-addressable package store across compose restarts, so subsequent boots are essentially network-free.
…tion Two cheap-but-cumulative speed-ups for the GitHub Actions runs: - prepare hook: extract the husky + chakra typegen logic into scripts/prepare.mjs and short-circuit it when CI is set (GitHub Actions provides CI=true). The hook still runs locally so devs keep their git hooks and chakra types, but each of the ~10 pnpm install invocations in CI now skips the ~8s chakra typegen step. The build-frontend job already calls pnpm chakra:typegen explicitly so it isn't affected. - pnpm install --prefer-offline: tell pnpm to trust the GH Actions pnpm cache and only hit the registry for missing tarballs. Saves a couple of seconds per install on the metadata round-trip when the cache is warm. Roughly 8–10 s shaved off each pnpm install. With ~10 installs per CI run, the gain on the critical path is in the 30–60 s range, with no behavioural change for local dev.
|
@greptile update your review |
The dist CLI install recipe was duplicated across two workflow jobs, the Windows lint workflow, the precommit-lint script, and the packmind-cli:lint npm script. Move it to scripts/install-dist-cli-deps.sh and switch from --no-frozen-lockfile to --frozen-lockfile so the dist install is reproducible. The script copies pnpm-lock.yaml into dist/apps/cli before invoking pnpm so --frozen-lockfile has a lockfile to consult, and short-circuits via cmp -s when the dist already mirrors the workspace lockfile (faster precommit and local lint reruns). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The first pnpm install in Dockerfile.api passes --ignore-scripts but the sibling install for the migrations bundle did not, allowing lifecycle scripts to run during the production-only install. Align both invocations. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The install-dependencies service used to fall through to a plain 'pnpm install' when --frozen-lockfile failed, silently rewriting pnpm-lock.yaml inside the dev container volume. Exit 1 with a clear remediation message instead, so the developer runs the install on the host and commits the updated lockfile. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The workspace pnpm-lock.yaml stores the workspace-root specifiers at its top level, not the CLI's. Running 'pnpm install --frozen-lockfile --ignore-workspace' inside dist/apps/cli therefore fails with ERR_PNPM_OUTDATED_LOCKFILE because the lockfile's root entry doesn't match dist/apps/cli/package.json. Resolve fresh against the CLI's package.json (matching the original PR behavior) and gate on node_modules existence instead of lockfile equality. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|




Explanation
Relates to #
Type of Change
Affected Components
Testing
Test Details:
TODO List
Reviewer Notes