Skip to content

Spike: npm -> pnpm#339

Open
cteyton wants to merge 16 commits into
mainfrom
claude/upbeat-gates-orsnA
Open

Spike: npm -> pnpm#339
cteyton wants to merge 16 commits into
mainfrom
claude/upbeat-gates-orsnA

Conversation

@cteyton
Copy link
Copy Markdown
Contributor

@cteyton cteyton commented May 28, 2026

Explanation

Relates to #

Type of Change

  • Bug fix
  • New feature
  • Improvement/Enhancement
  • Refactoring
  • Documentation
  • Breaking change

Affected Components

  • Domain packages affected:
  • Frontend / Backend / Both:
  • Breaking changes (if any):

Testing

  • Unit tests added/updated
  • Integration tests added/updated
  • Manual testing completed
  • Test coverage maintained or improved

Test Details:

TODO List

  • CHANGELOG Updated
  • Documentation Updated

Reviewer Notes

claude added 7 commits May 28, 2026 10:19
- 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.
@cteyton cteyton changed the title Claude/upbeat gates orsn a Spike: npm -> pnpm May 28, 2026
@cteyton
Copy link
Copy Markdown
Contributor Author

cteyton commented May 28, 2026

@greptile review this

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 28, 2026

Greptile Summary

This PR migrates the monorepo package manager from npm to pnpm 9.15.0, touching CI workflows, Dockerfiles, docker-compose, and all workspace package.json files. The change replaces npm ci with pnpm install --frozen-lockfile, swaps npm workspaces for pnpm-workspace.yaml, converts internal workspace references from \"*\" to \"workspace:*\", and wires pnpm into Husky, pre-commit scripts, and the Docker build chain via corepack.

  • CI/CD: All GitHub Actions jobs now use pnpm/action-setup@v4 + pnpm install --frozen-lockfile --prefer-offline; build artifacts include pnpm-lock.yaml copies for Docker; a new install-dist-cli-deps.sh script installs CLI runtime deps post-build with --no-frozen-lockfile because the workspace lockfile can't be reused there.
  • Docker: Both API and MCP Dockerfiles activate pnpm via corepack under a shared COREPACK_HOME to solve a root-vs-node user binary visibility problem; MCP correctly copies its pnpm-lock.yaml before installing, but the API Dockerfile's equivalent step is missing.
  • docker-compose: Replaces npm-based install logic with corepack-activated pnpm; adds persistent volumes (dev-corepack-cache, dev-pnpm-store) to cache the pnpm store across container restarts; the install-dependencies entrypoint now exits with an error on lockfile drift instead of silently upgrading."

Confidence Score: 5/5

Safe 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

Filename Overview
.github/workflows/build.yml Consistently migrated all jobs from npm ci to pnpm install --frozen-lockfile --prefer-offline with pnpm/action-setup@v4; adds the new install-dist-cli-deps.sh step for CLI builds, and copies pnpm-lock.yaml to dist artifacts for both API and MCP.
dockerfile/Dockerfile.api Adds corepack/pnpm setup with shared COREPACK_HOME to fix root vs node user binary visibility; replaces npm install with pnpm install --prod, but does not copy pnpm-lock.yaml before installing (unlike Dockerfile.mcp which does), leaving the API image install non-reproducible.
dockerfile/Dockerfile.mcp Correctly copies dist/apps/mcp-server/pnpm-lock.yaml before pnpm install --prod, giving pinned reproducible resolution; shared COREPACK_HOME pattern matches Dockerfile.api.
docker-compose.yml Replaces npm-based install logic with pnpm via corepack; adds dev-corepack-cache and dev-pnpm-store volumes to persist the pnpm store across container restarts; install-dependencies now exits with an error on lockfile drift instead of silently upgrading.
scripts/install-dist-cli-deps.sh New script installs CLI runtime deps in dist/apps/cli; uses --no-frozen-lockfile so resolution is non-reproducible across builds; short-circuits if node_modules already exists.
package.json Replaces npm workspaces with pnpm-workspace.yaml; migrates all script invocations from npm run to pnpm; moves npm overrides to pnpm.overrides; adds packageManager field pinned to pnpm@9.15.0.
pnpm-workspace.yaml New file declaring workspace packages (packages/* and apps/*), replacing the npm workspaces array that was in package.json.
.npmrc New pnpm configuration file; sets public-hoist-pattern for @types/*, eslint, and prettier; disables shamefully-hoist and strict-peer-dependencies.
scripts/prepare.mjs New prepare lifecycle script; exits immediately in CI, otherwise runs pnpm husky and pnpm chakra:typegen for local developer setup.

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]
Loading

Reviews (7): Last reviewed commit: "🐛 fix(scripts): drop frozen-lockfile fr..." | Re-trigger Greptile

Comment thread dockerfile/Dockerfile.mcp Outdated
Comment thread package.json Outdated
…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-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 28, 2026

Greptile Summary

This spike migrates the monorepo's package manager from npm to pnpm 9.15.0, replacing package-lock.json with pnpm-lock.yaml, adding pnpm-workspace.yaml, updating all CI workflows to use pnpm/action-setup, and converting Dockerfiles to activate pnpm via corepack.

  • All GitHub Actions workflows gain a pnpm/action-setup@v4 step before setup-node, and npm ci is replaced with pnpm install --frozen-lockfile; the package-lock.json artifact copy is replaced with pnpm-lock.yaml.
  • Jest transformIgnorePatterns are updated from Unix-only leading-slash patterns to portable patterns that correctly traverse pnpm's nested symlink structure on both Unix and Windows.
  • Both Dockerfiles activate pnpm via corepack prepare (run as root) then invoke pnpm install as the node user, and all three docker-compose service entrypoints call corepack prepare at startup, creating redundant network calls on cold starts.

Confidence Score: 4/5

The 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

Filename Overview
.github/workflows/build.yml All npm commands replaced with pnpm equivalents; pnpm/action-setup@v4 added to every job; pnpm-lock.yaml replaces package-lock.json in the API artifact copy.
dockerfile/Dockerfile.api corepack enables pnpm as root, but both pnpm install steps execute as the node user — COREPACK_HOME is user-specific, which may trigger a re-download during build; two sequential pnpm install + store prune pairs further interact with the store.
dockerfile/Dockerfile.mcp Same root-vs-node corepack pattern as Dockerfile.api; installs without a lockfile (--no-lockfile) intentionally documented.
docker-compose.yml install-dependencies, run-migrations, and e2e-tests service entrypoints all call corepack prepare at container startup, requiring internet access on every cold start.
.npmrc New pnpm config; public-hoist-pattern[]=types is broader than @types/* and could inadvertently hoist packages that aren't TypeScript declarations.
package.json Switched from npm workspaces to pnpm; added explicit root-level deps (express, body-parser, webpack, etc.) that were previously hoisted; zod v3 added at root while CLI uses v4.
pnpm-workspace.yaml New workspace definition file equivalent to the removed npm workspaces field in package.json.
jest-utils.ts transformIgnorePatterns updated from leading-slash Unix-only patterns to portable patterns that handle pnpm's nested node_modules symlink paths on both Unix and Windows.

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]
Loading

Reviews (2): Last reviewed commit: "🐛 fix(cli): install dist runtime depend..." | Re-trigger Greptile

Comment thread .npmrc Outdated
Comment thread dockerfile/Dockerfile.api Outdated
Comment thread docker-compose.yml
Comment thread dockerfile/Dockerfile.api Outdated
claude added 4 commits May 28, 2026 11:11
…_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.
@cteyton
Copy link
Copy Markdown
Contributor Author

cteyton commented May 28, 2026

@greptile update your review

cteyton and others added 4 commits May 28, 2026 17:35
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>
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
12 Security Hotspots
C Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

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.

2 participants