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's package manager from npm to pnpm 11, replacing
Confidence Score: 5/5Safe to merge — all CI, Docker, and compose flows use the pinned lockfile; only the bundled CLI dist install runs without a lockfile due to a pnpm workspace constraint, which is documented in the script. The migration is comprehensive and internally consistent: all production Docker builds copy pnpm-lock.yaml for reproducibility, CI workflows use --frozen-lockfile, and previous review feedback (shared COREPACK_HOME, deferred store prune, MCP lockfile copy) has been addressed. The only unresolved reproducibility gap is the CLI dist dep install, which is explicitly acknowledged in the code. scripts/install-dist-cli-deps.sh — non-deterministic dep resolution for the bundled CLI production artifact. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[pnpm install --frozen-lockfile] -->|CI / local dev| B[Workspace node_modules
pnpm-lock.yaml pinned]
B --> C{Build target}
C -->|API / MCP-Server| D[nx build to dist/]
C -->|CLI| E[nx build to dist/apps/cli/]
D -->|copy pnpm-lock.yaml to dist| F[Docker COPY package.json + pnpm-lock.yaml]
F --> G[pnpm install --prod --ignore-scripts
reproducible]
E --> H[install-dist-cli-deps.sh]
H -->|--no-frozen-lockfile --ignore-workspace| I[pnpm install --prod
non-deterministic]
B -->|docker-compose dev| J[install-dependencies service
pnpm install --frozen-lockfile]
J --> K[run-migrations
pnpm typeorm migration:run]
J --> L[e2e-tests
pnpm e2e]
Reviews (15): Last reviewed commit: "📝 docs(pnpm): soften workspace config r..." | 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>
API image and its migrations install ran 'pnpm install --prod' with no lockfile present, leaving transitive versions unpinned and the image non-reproducible. Copy pnpm-lock.yaml into both install dirs so versions are pinned, matching the mcp-server image. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The short-circuit only checked whether node_modules existed, so once installed the dist CLI dependency tree was never refreshed even after apps/cli/package.json changed. Fingerprint the manifest (sha256) and reinstall when it differs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add pnpm_migrate.sh: installs pnpm (corepack with npm fallback), wipes all node_modules and package-lock.json, reinstalls against the lockfile, and regenerates the tsconfig. Update CONTRIBUTING.md setup to use pnpm and document the migration script. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bundled main.js directly requires transitive deps that pnpm's strict node_modules layout does not expose at top level, crash-looping the containers with MODULE_NOT_FOUND (exit 1): - api: body-parser, express, semver - mcp-server: semver Declare them as explicit deps so pnpm installs them at the package root. For mcp-server, add an app-level package.json so @nx/esbuild generatePackageJson merges semver into the generated dist package.json. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pnpm 11 no longer reads overrides/onlyBuiltDependencies from package.json or behavior settings from .npmrc (silently ignored). Move overrides, convert onlyBuiltDependencies array to the new allowBuilds map, and move .npmrc settings into pnpm-workspace.yaml. nx left disabled in allowBuilds (absent from the prior onlyBuiltDependencies, so never built under v9). Bump engines.pnpm to >=11.0.0 <12.0.0 and packageManager to pnpm@11.5.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bump every corepack/action-setup pin and the api docker-package.json engines constraint from 9.15.0 to 11.5.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the hardcoded `version: 11.5.0` from every pnpm/action-setup@v4 step. With no version specified, the action reads the pnpm version from the root package.json `packageManager` field, making it the single source of truth. Bumping pnpm now only requires editing package.json instead of ~10 CI steps. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rsnA # Conflicts: # package-lock.json
pnpm 11 auto-added 6 ignored build deps to allowBuilds with the placeholder "set this to true or false". That placeholder is not a boolean, so strictDepBuilds (default true in v11) kept treating them as unreviewed and failed install with ERR_PNPM_IGNORED_BUILDS. These packages were not built under pnpm 9 (absent from onlyBuiltDependencies) and ship prebuilt native binaries, so set them to false to match prior behavior and clear the error. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve conflict: keep package-lock.json deleted (branch migrates npm→pnpm). main moved class-transformer/class-validator to runtime dependencies; regenerate pnpm-lock.yaml so --frozen-lockfile passes. Unblocks CI: pull_request workflows could not run while the PR had merge conflicts (GitHub cannot build the merge ref). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The cli-e2e prod-CLI step installs @packmind/cli into a fresh mktemp dir with no allowBuilds config. pnpm 11 defaults strictDepBuilds=true, so ignored build scripts (bcrypt, msgpackr-extract) failed install with ERR_PNPM_IGNORED_BUILDS. Those packages ship prebuilt binaries and the step only needs the CLI binary resolvable to run --version, so pass --config.strictDepBuilds=false to downgrade the error to a warning. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Builds leaf libs (types, logger) after install so a broken pnpm migration surfaces during the script run instead of on the developer's next workday. Skippable via SKIP_BUILD_CHECK=1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pnpm 11 runs lockfile supply-chain verification on every install when a release-age/trust policy is active (~5.6s for 2671 entries). Result is cached in ~/.cache/pnpm/lockfile-verified.jsonl, but setup-node's cache: 'pnpm' only persists the store (~/.pnpm-store), not the cache dir, so every runner re-scans cold. Add an actions/cache step keyed on pnpm-lock.yaml before each install to persist ~/.cache/pnpm. Cache hit skips the rescan; lockfile changes rotate the key and re-verify once. Keeps the supply-chain policy enabled. Applied to all ubuntu install sites (build x6, quality x2, docker x1). Skipped tmp-cli-lint-windows.yml (Windows cacheDir differs). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
onlyBuiltDependencies list-form still works in pnpm 11; allowBuilds map is the alternative, not a forced replacement. Reword to avoid overstating "removed"/"no longer reads". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|

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