penpot/scripts/clean-node-modules
Andrey Antukh 5c474939ac 🔧 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
2026-09-09 08:04:11 +02:00

92 lines
2.6 KiB
Bash
Executable File

#!/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