Files
penpot/scripts/sync-pnpm-version
Andrey Antukh 183e31f18f 🔥 Remove corepack, use system pnpm everywhere
Corepack is gone from Node 25+, so every `corepack enable` call
fails. pnpm now ships as a system binary (devenv, CI runners and
Docker images install it directly) and auto-downloads the version
pinned in `packageManager` on mismatch.

Scripts, workflows and Dockerfiles call `pnpm` straight away; the
three deploy workflows use a single `pnpm/setup@v2` step; and the
new `scripts/sync-pnpm-version` stamps all 35 `packageManager`
fields from the system pnpm, replacing the `corepack use` sweep.

AI-assisted-by: muse-spark-1.3-contributor
2026-09-20 11:42:37 +02:00

186 lines
5.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# Sync the `packageManager` field of every first-party package.json to the
# system pnpm version.
#
# Only `pnpm` and `node` are required (plus registry access to resolve the
# integrity hash, unless --field is given).
#
# Run it from the repo root with the log redirected to a file (never pipe
# tool output through filters).
set -euo pipefail
usage() {
cat <<'EOF'
Usage: scripts/sync-pnpm-version [options] [version]
Stamp every first-party package.json's `packageManager` field with the
system pnpm version (default: `pnpm --version`).
Options:
--field FIELD Use FIELD verbatim, skip the registry lookup.
For tests and offline use.
--check Verify all fields match; do not write. Exit 1 on drift.
--install After stamping, run `pnpm install` in each workspace
root to refresh lockfile metadata.
--root DIR Scan DIR instead of the repo root. For tests.
-h, --help Show this help.
external/ (vendored trees with their own lifecycles), .opencode/,
.pnpm-store/, bundles/ and docker build-context copies are never stamped.
EOF
}
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
FIELD=""
CHECK=0
INSTALL=0
VERSION=""
while [[ $# -gt 0 ]]; do
case "$1" in
--field) FIELD="$2"; shift 2 ;;
--field=*) FIELD="${1#--field=}"; shift ;;
--check) CHECK=1; shift ;;
--install) INSTALL=1; shift ;;
--root) ROOT="$2"; shift 2 ;;
--root=*) ROOT="${1#--root=}"; shift ;;
-h | --help) usage; exit 0 ;;
-*) {
echo "error: unknown option: $1" >&2
usage >&2
exit 64
} ;;
*)
if [[ -n "$VERSION" ]]; then
echo "error: only one version argument allowed" >&2
usage >&2
exit 64
fi
VERSION="$1"; shift ;;
esac
done
if [[ -z "$FIELD" ]]; then
if [[ -z "$VERSION" ]]; then
VERSION="$(pnpm --version | tr -d '[:space:]')"
fi
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-.+)?$ ]]; then
echo "error: invalid pnpm version: $VERSION" >&2
exit 64
fi
# Same value `npm view pnpm@<v> dist.integrity` returns; pnpm is the one
# tool guaranteed to exist everywhere, so no npm needed.
integrity="$(pnpm view "pnpm@${VERSION}" dist.integrity | tr -d '[:space:]')"
b64="${integrity#sha512-}"
if ! [[ "$b64" =~ ^[A-Za-z0-9+/=]+$ ]]; then
echo "error: unexpected integrity for pnpm@${VERSION}: ${integrity}" >&2
exit 1
fi
# node ships with every pnpm, so no python3 needed for base64 -> hex.
hex="$(node -p "Buffer.from('${b64}','base64').toString('hex')")"
FIELD="pnpm@${VERSION}+sha512.${hex}"
fi
cd "$ROOT"
# Same exclusions as scripts/clean-node-modules, plus build-context copies
# (bundles/, docker/images/bundle-*) which builds regenerate from the
# stamped sources.
mapfile -t files < <(
find . \
\( -name .git -o -name node_modules -o -name .pnpm-store \
-o -name external -o -name .opencode -o -name bundles \
-o -name .angular \) -type d -prune \
-o -path './docker/images/bundle-*' -prune \
-o -name package.json -type f -print | sort
)
if [[ ${#files[@]} -eq 0 ]]; then
echo "error: no package.json files found under $ROOT" >&2
exit 1
fi
# Stamp one file with node: replaces only the packageManager value when the
# key exists (indent, key order and the rest stay byte-identical), else
# inserts the key after "name" using the file's own indent. Prints
# "updated <rel>" / "unchanged <rel>" / "missing <rel>" / "mismatch <rel>".
stamp_one() {
node -e '
const fs = require("fs");
const [file, field, check] = process.argv.slice(1);
const raw = fs.readFileSync(file, "utf8");
const data = JSON.parse(raw);
const rel = file.replace(/^\.\//, "");
if (data.packageManager === field) {
console.log(`unchanged ${rel}`);
} else if (check === "1") {
console.log(`${data.packageManager === undefined ? "missing" : "mismatch"} ${rel}`);
process.exit(3);
} else {
let out;
const valueRe = /("packageManager"\s*:\s*")[^"]*(")/;
if (valueRe.test(raw)) {
out = raw.replace(valueRe, `$1${field}$2`);
} else {
const anchor = raw.match(/^(\s*)"(name|version)"\s*:\s*"[^"]*",\s*$/m);
if (!anchor) {
console.error(`error: cannot find insertion point in ${rel}`);
process.exit(1);
}
out = raw.replace(
anchor[0],
`${anchor[0]}\n${anchor[1]}"packageManager": "${field}",`
);
}
JSON.parse(out); // refuse to write invalid JSON
fs.writeFileSync(file, out);
console.log(`updated ${rel}`);
}
' "$1" "$FIELD" "$CHECK"
}
failures=0
updated=0
unchanged=0
for file in "${files[@]}"; do
if [[ "$CHECK" -eq 1 ]]; then
if stamp_one "$file"; then
unchanged=$((unchanged + 1))
else
rc=$?
if [[ "$rc" -eq 3 ]]; then
failures=$((failures + 1))
else
exit "$rc"
fi
fi
else
if [[ "$(stamp_one "$file")" == updated* ]]; then
updated=$((updated + 1))
else
unchanged=$((unchanged + 1))
fi
fi
done
if [[ "$CHECK" -eq 1 ]]; then
if [[ "$failures" -gt 0 ]]; then
echo "check failed: ${failures} file(s) differ from ${FIELD}" >&2
exit 1
fi
echo "all ${unchanged} package.json files match ${FIELD}"
exit 0
fi
echo "stamped ${FIELD}: ${updated} updated, ${unchanged} unchanged"
if [[ "$INSTALL" -eq 1 ]]; then
# The 11 pnpm workspaces: repo root plus one per module. Members resolve
# through their parent workspace lockfile, so no per-member install.
for dir in . backend common docs exporter frontend library mcp media-processor plugins render-wasm; do
echo "--- pnpm install in ${dir}"
(cd "$dir" && pnpm install)
done
fi