From 5c474939ace68ff4e28c5b15aaa5131dd3c7fbcb Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 7 Sep 2026 21:04:44 +0000 Subject: [PATCH] :wrench: Pin all pnpm workspaces to one shared pnpm store Set storeDir in every pnpm-workspace.yaml: `.pnpm-store` at the repo root and `../.pnpm-store` in the ten module workspaces, so all of them resolve to /.pnpm-store. pnpm resolves the value against the workspace root, and nested workspaces do not inherit settings, which had left the root workspace and the modules on two different stores. Add scripts/clean-node-modules: removes every workspace node_modules in one pass (ignores external/ and .opencode/), keeps the shared store unless --store removes it too. Verified: every workspace resolves the same store path; reinstalls after a full clean reuse the cache with zero downloads; frozen-lockfile installs pass in all 11 workspaces with no lockfile changes; the frontend storybook suite stays green. AI-assisted-by: omen-alpha --- .serena/memories/critical-info.md | 4 + .serena/memories/workflow/updating-pnpm.md | 24 ++++++ backend/pnpm-workspace.yaml | 2 + common/pnpm-workspace.yaml | 2 + docs/pnpm-workspace.yaml | 2 + exporter/pnpm-workspace.yaml | 2 + frontend/pnpm-workspace.yaml | 2 + library/pnpm-workspace.yaml | 2 + mcp/pnpm-workspace.yaml | 2 + media-processor/pnpm-workspace.yaml | 2 + plugins/pnpm-workspace.yaml | 2 + pnpm-workspace.yaml | 2 + render-wasm/pnpm-workspace.yaml | 2 + scripts/clean-node-modules | 91 ++++++++++++++++++++++ 14 files changed, 141 insertions(+) create mode 100755 scripts/clean-node-modules diff --git a/.serena/memories/critical-info.md b/.serena/memories/critical-info.md index f7b1754377..0854fe28ee 100644 --- a/.serena/memories/critical-info.md +++ b/.serena/memories/critical-info.md @@ -74,6 +74,10 @@ module. You can read it from `mem:/core` - `scripts/error-reports.mjs` — Query error reports via RPC API with token authentication. Supports list/get operations with filtering and pagination. See `mem:scripts/error-reports`. +- `scripts/clean-node-modules` — Remove stale `node_modules` from all pnpm + workspaces (root, modules, member packages). Keeps the shared pnpm store + at `/.pnpm-store` unless `--store`; ignores `external/` and + `.opencode/`. Usage and reinstall steps: `mem:workflow/updating-pnpm`. # Dependency graph diff --git a/.serena/memories/workflow/updating-pnpm.md b/.serena/memories/workflow/updating-pnpm.md index 72f727a979..9f4ba133df 100644 --- a/.serena/memories/workflow/updating-pnpm.md +++ b/.serena/memories/workflow/updating-pnpm.md @@ -14,6 +14,15 @@ file (never pipe tool output through filters). workspace. Members must not carry their own `pnpm-workspace.yaml` or `pnpm-lock.yaml`; their dependencies resolve through the parent workspace's lockfile. +- One shared pnpm store for the whole repo: `/.pnpm-store`. Every + workspace yaml sets it explicitly: `storeDir: .pnpm-store` at the root, + `storeDir: ../.pnpm-store` in each module. pnpm resolves the value + against the workspace root, so all workspaces land on the same store. + Do not remove these lines: nested workspaces do not inherit settings, + and without them each workspace may resolve a different store. +- The store survives `node_modules` cleans. It is content-addressed and + integrity-verified, so it cannot go stale; staleness lives in + node_modules. Only `scripts/clean-node-modules --store` removes it. - Every `package.json` (about 35 of them) must carry a `packageManager` field with the identical `pnpm@+sha512.` value. Do not let them drift. - CI pins no pnpm version; workflows rely on corepack reading @@ -63,3 +72,18 @@ file (never pipe tool output through filters). - `pnpm --version` in each workspace prints the target version. - `pnpm install --frozen-lockfile` succeeds in each of the 11 workspaces. - `git diff` on lockfiles matches the expectations above. + +## Cleaning stale node_modules + +- `scripts/clean-node-modules` removes every workspace `node_modules`: the + repo root, all module workspaces, and all member packages. Use it when + installs misbehave after dependency changes: clean, reinstall, done. +- Flags: `-n/--dry-run` lists without deleting; `--store` also removes the + shared pnpm store at `/.pnpm-store` (the next install re-downloads + what it held). `external/` (vendored dependency trees with their own + lifecycles) and `.opencode/` are always ignored. +- The script never touches the pnpm store by default, so the reinstall + after cleaning reuses cached packages (zero downloads). +- After cleaning, run `pnpm install` in each workspace root to restore the + development environment; `frontend` postinstall also reinstalls and + builds `plugins-runtime`. diff --git a/backend/pnpm-workspace.yaml b/backend/pnpm-workspace.yaml index b3fbd9192b..3ebe73f82d 100644 --- a/backend/pnpm-workspace.yaml +++ b/backend/pnpm-workspace.yaml @@ -1,2 +1,4 @@ +storeDir: ../.pnpm-store + minimumReleaseAgeExclude: - brace-expansion@5.0.8 || 5.0.9 diff --git a/common/pnpm-workspace.yaml b/common/pnpm-workspace.yaml index a1c031fc33..7b3a5d3edf 100644 --- a/common/pnpm-workspace.yaml +++ b/common/pnpm-workspace.yaml @@ -1 +1,3 @@ +storeDir: ../.pnpm-store + minimumReleaseAge: 0 diff --git a/docs/pnpm-workspace.yaml b/docs/pnpm-workspace.yaml index bd2aa47620..b9b659dcc5 100644 --- a/docs/pnpm-workspace.yaml +++ b/docs/pnpm-workspace.yaml @@ -1,3 +1,5 @@ +storeDir: ../.pnpm-store + minimumReleaseAgeExclude: - undici@7.28.0 || 7.29.0 - js-yaml@3.15.0 || 4.3.0 diff --git a/exporter/pnpm-workspace.yaml b/exporter/pnpm-workspace.yaml index ac94ff8d53..3a65caa2ac 100644 --- a/exporter/pnpm-workspace.yaml +++ b/exporter/pnpm-workspace.yaml @@ -1,3 +1,5 @@ +storeDir: ../.pnpm-store + allowBuilds: core-js-pure: false minimumReleaseAgeExclude: diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml index 2c3a2c026f..c30ce8c1ea 100644 --- a/frontend/pnpm-workspace.yaml +++ b/frontend/pnpm-workspace.yaml @@ -1,3 +1,5 @@ +storeDir: ../.pnpm-store + packages: - "packages/draft-js" - "packages/mousetrap" diff --git a/library/pnpm-workspace.yaml b/library/pnpm-workspace.yaml index b5e864413d..528f811e1a 100644 --- a/library/pnpm-workspace.yaml +++ b/library/pnpm-workspace.yaml @@ -1,3 +1,5 @@ +storeDir: ../.pnpm-store + minimumReleaseAgeExclude: - brace-expansion@5.0.8 || 5.0.9 patchedDependencies: diff --git a/mcp/pnpm-workspace.yaml b/mcp/pnpm-workspace.yaml index eb1c390c2f..13e111b1dc 100644 --- a/mcp/pnpm-workspace.yaml +++ b/mcp/pnpm-workspace.yaml @@ -1,3 +1,5 @@ +storeDir: ../.pnpm-store + allowBuilds: esbuild: true sharp: false diff --git a/media-processor/pnpm-workspace.yaml b/media-processor/pnpm-workspace.yaml index 5ed0b5af0d..f355f8d1a4 100644 --- a/media-processor/pnpm-workspace.yaml +++ b/media-processor/pnpm-workspace.yaml @@ -1,2 +1,4 @@ +storeDir: ../.pnpm-store + allowBuilds: esbuild: true diff --git a/plugins/pnpm-workspace.yaml b/plugins/pnpm-workspace.yaml index a0099791fc..864c77bb38 100644 --- a/plugins/pnpm-workspace.yaml +++ b/plugins/pnpm-workspace.yaml @@ -1,3 +1,5 @@ +storeDir: ../.pnpm-store + packages: - 'apps/**' - 'libs/**' diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 7a28fcc29c..612f46bca4 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,5 @@ +storeDir: .pnpm-store + allowBuilds: esbuild: true diff --git a/render-wasm/pnpm-workspace.yaml b/render-wasm/pnpm-workspace.yaml index 09a02ca1c8..cad1aaed6b 100644 --- a/render-wasm/pnpm-workspace.yaml +++ b/render-wasm/pnpm-workspace.yaml @@ -1,3 +1,5 @@ +storeDir: ../.pnpm-store + allowBuilds: esbuild: true onlyBuiltDependencies: diff --git a/scripts/clean-node-modules b/scripts/clean-node-modules new file mode 100755 index 0000000000..8d17b5f213 --- /dev/null +++ b/scripts/clean-node-modules @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# Remove node_modules directories across the monorepo's pnpm workspaces: +# the repo root, all module workspaces, and all workspace member packages. +# +# Use it when node_modules are stale or corrupted and a clean reinstall is +# cheaper than debugging. Reinstall afterwards with `pnpm install` in each +# workspace root (root, backend, common, docs, exporter, frontend, library, +# mcp, media-processor, plugins, render-wasm). +# +# external/ (vendored dependency trees) and .opencode/ are always ignored. +# The pnpm content-addressable store lives at /.pnpm-store, outside +# node_modules, and survives a default clean. `--store` removes it too; +# the next install re-downloads what it held. + +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: scripts/clean-node-modules [options] + +Remove every node_modules directory in the pnpm workspaces: the repo root, +all module workspaces, and all workspace member packages. + +Options: + -n, --dry-run Print what would be removed without deleting anything. + --store Also delete the shared pnpm store at /.pnpm-store. + The next install re-downloads everything it held. + -h, --help Show this help. + +external/ (vendored dependency trees) and .opencode/ are always ignored. +EOF +} + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +DRY_RUN=0 +WITH_STORE=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + -n | --dry-run) DRY_RUN=1 ;; + --store) WITH_STORE=1 ;; + -h | --help) usage; exit 0 ;; + *) { + echo "error: unknown option: $1" >&2 + usage >&2 + exit 64 + } ;; + esac + shift +done + +cd "$ROOT" + +if [[ $WITH_STORE -eq 1 && -d .pnpm-store ]]; then + size="$(du -sh .pnpm-store | cut -f1)" + if [[ $DRY_RUN -eq 1 ]]; then + echo "would remove: ./.pnpm-store (pnpm store, $size)" + else + echo "removing pnpm store: $ROOT/.pnpm-store ($size); the next install re-downloads it" + rm -rf .pnpm-store + fi +fi + +# -prune stops the descent, so only the top-most node_modules of each tree +# matches; nested dependency copies inside it die with their parent. +mapfile -t dirs < <( + find . \( -name .git -o -name external -o -name .opencode \) -type d -prune \ + -o \( -name node_modules -type d -prune -print \) +) + +if [[ ${#dirs[@]} -eq 0 ]]; then + echo "no node_modules directories found" + exit 0 +fi + +cleaned=0 +for dir in "${dirs[@]}"; do + if [[ $DRY_RUN -eq 1 ]]; then + echo "would remove: $dir" + else + rm -rf "$dir" + echo "removed: $dir" + fi + cleaned=$((cleaned + 1)) +done + +if [[ $DRY_RUN -eq 1 ]]; then + echo "$cleaned node_modules directories found" +else + echo "$cleaned node_modules directories cleaned" +fi