🔧 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 <repo>/.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
This commit is contained in:
Andrey Antukh 2026-09-07 21:04:44 +00:00
parent f68c266380
commit 5c474939ac
14 changed files with 141 additions and 0 deletions

View File

@ -74,6 +74,10 @@ module. You can read it from `mem:<MODULE>/core`
- `scripts/error-reports.mjs` — Query error reports via RPC API with token - `scripts/error-reports.mjs` — Query error reports via RPC API with token
authentication. Supports list/get operations with filtering and pagination. authentication. Supports list/get operations with filtering and pagination.
See `mem:scripts/error-reports`. 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 `<repo>/.pnpm-store` unless `--store`; ignores `external/` and
`.opencode/`. Usage and reinstall steps: `mem:workflow/updating-pnpm`.
# Dependency graph # Dependency graph

View File

@ -14,6 +14,15 @@ file (never pipe tool output through filters).
workspace. Members must not carry their own `pnpm-workspace.yaml` or workspace. Members must not carry their own `pnpm-workspace.yaml` or
`pnpm-lock.yaml`; their dependencies resolve through the parent `pnpm-lock.yaml`; their dependencies resolve through the parent
workspace's lockfile. workspace's lockfile.
- One shared pnpm store for the whole repo: `<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 - Every `package.json` (about 35 of them) must carry a `packageManager` field
with the identical `pnpm@<version>+sha512.<hash>` value. Do not let them drift. with the identical `pnpm@<version>+sha512.<hash>` value. Do not let them drift.
- CI pins no pnpm version; workflows rely on corepack reading - 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 --version` in each workspace prints the target version.
- `pnpm install --frozen-lockfile` succeeds in each of the 11 workspaces. - `pnpm install --frozen-lockfile` succeeds in each of the 11 workspaces.
- `git diff` on lockfiles matches the expectations above. - `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 `<repo>/.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`.

View File

@ -1,2 +1,4 @@
storeDir: ../.pnpm-store
minimumReleaseAgeExclude: minimumReleaseAgeExclude:
- brace-expansion@5.0.8 || 5.0.9 - brace-expansion@5.0.8 || 5.0.9

View File

@ -1 +1,3 @@
storeDir: ../.pnpm-store
minimumReleaseAge: 0 minimumReleaseAge: 0

View File

@ -1,3 +1,5 @@
storeDir: ../.pnpm-store
minimumReleaseAgeExclude: minimumReleaseAgeExclude:
- undici@7.28.0 || 7.29.0 - undici@7.28.0 || 7.29.0
- js-yaml@3.15.0 || 4.3.0 - js-yaml@3.15.0 || 4.3.0

View File

@ -1,3 +1,5 @@
storeDir: ../.pnpm-store
allowBuilds: allowBuilds:
core-js-pure: false core-js-pure: false
minimumReleaseAgeExclude: minimumReleaseAgeExclude:

View File

@ -1,3 +1,5 @@
storeDir: ../.pnpm-store
packages: packages:
- "packages/draft-js" - "packages/draft-js"
- "packages/mousetrap" - "packages/mousetrap"

View File

@ -1,3 +1,5 @@
storeDir: ../.pnpm-store
minimumReleaseAgeExclude: minimumReleaseAgeExclude:
- brace-expansion@5.0.8 || 5.0.9 - brace-expansion@5.0.8 || 5.0.9
patchedDependencies: patchedDependencies:

View File

@ -1,3 +1,5 @@
storeDir: ../.pnpm-store
allowBuilds: allowBuilds:
esbuild: true esbuild: true
sharp: false sharp: false

View File

@ -1,2 +1,4 @@
storeDir: ../.pnpm-store
allowBuilds: allowBuilds:
esbuild: true esbuild: true

View File

@ -1,3 +1,5 @@
storeDir: ../.pnpm-store
packages: packages:
- 'apps/**' - 'apps/**'
- 'libs/**' - 'libs/**'

View File

@ -1,3 +1,5 @@
storeDir: .pnpm-store
allowBuilds: allowBuilds:
esbuild: true esbuild: true

View File

@ -1,3 +1,5 @@
storeDir: ../.pnpm-store
allowBuilds: allowBuilds:
esbuild: true esbuild: true
onlyBuiltDependencies: onlyBuiltDependencies:

91
scripts/clean-node-modules Executable file
View File

@ -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 <repo>/.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 <repo>/.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