mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-28 22:56:49 +00:00
Fix non-interactive and wizard onboarding reruns so existing agent lists and bindings are preserved unless the user explicitly resets config. Isolate legacy `plugins.installs` migration into its own write so the config size-drop allowance cannot mask unrelated config loss, while preserving new or repaired install records for the final plugin-index commit. Also keep shrinkwrap generation pinned to pnpm-locked transitive patch versions only when the dependency edge still allows that version, and isolate the tooling Vitest shard that mutates process state. Fixes #84692. Replaces #84748. Co-authored-by: yetval <yetvald@gmail.com>
1287 lines
41 KiB
JavaScript
1287 lines
41 KiB
JavaScript
#!/usr/bin/env node
|
|
import { execFileSync } from "node:child_process";
|
|
import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { parse as parseYaml } from "yaml";
|
|
import { listChangedPathsFromGit, listStagedChangedPaths } from "./changed-lanes.mjs";
|
|
import { resolveNpmRunner } from "./npm-runner.mjs";
|
|
|
|
const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
const EXACT_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/u;
|
|
const STABLE_VERSION_PATTERN = /^(\d+)\.(\d+)\.(\d+)$/u;
|
|
|
|
function usage() {
|
|
return [
|
|
"Usage: node scripts/generate-npm-shrinkwrap.mjs [--check] [--all|--plugins|--changed|--package-dir <dir>] [--base <ref>] [--head <ref>] [--staged]",
|
|
" default: root package only",
|
|
].join("\n");
|
|
}
|
|
|
|
function normalizeOverrideValue(value) {
|
|
if (value === null || value === undefined) {
|
|
return value;
|
|
}
|
|
if (Array.isArray(value)) {
|
|
return value.map((item) => normalizeOverrideValue(item));
|
|
}
|
|
if (typeof value === "object") {
|
|
return Object.fromEntries(
|
|
Object.entries(value).map(([key, nestedValue]) => [key, normalizeOverrideValue(nestedValue)]),
|
|
);
|
|
}
|
|
return String(value);
|
|
}
|
|
|
|
function normalizeOverrides(overrides) {
|
|
if (!overrides || typeof overrides !== "object" || Array.isArray(overrides)) {
|
|
return {};
|
|
}
|
|
return normalizeOverrideValue(overrides);
|
|
}
|
|
|
|
function isPlainObject(value) {
|
|
return value && typeof value === "object" && !Array.isArray(value);
|
|
}
|
|
|
|
function readWorkspaceOverrides() {
|
|
const workspace = parseYaml(readFileSync(path.join(ROOT_DIR, "pnpm-workspace.yaml"), "utf8"));
|
|
return normalizeOverrides(workspace?.overrides);
|
|
}
|
|
|
|
function readWorkspacePackageExtensions() {
|
|
const workspace = parseYaml(readFileSync(path.join(ROOT_DIR, "pnpm-workspace.yaml"), "utf8"));
|
|
return workspace?.packageExtensions && typeof workspace.packageExtensions === "object"
|
|
? workspace.packageExtensions
|
|
: {};
|
|
}
|
|
|
|
function parsePnpmPackageKey(packageKey) {
|
|
if (typeof packageKey !== "string") {
|
|
return null;
|
|
}
|
|
const versionSeparatorIndex = packageKey.startsWith("@")
|
|
? packageKey.indexOf("@", 1)
|
|
: packageKey.indexOf("@");
|
|
if (versionSeparatorIndex <= 0) {
|
|
return null;
|
|
}
|
|
const name = packageKey.slice(0, versionSeparatorIndex);
|
|
const version = packageKey.slice(versionSeparatorIndex + 1).replace(/\(.*/u, "");
|
|
if (!name || !version) {
|
|
return null;
|
|
}
|
|
return { name, version };
|
|
}
|
|
|
|
function readPnpmLockPackages() {
|
|
const lockfile = parseYaml(readFileSync(path.join(ROOT_DIR, "pnpm-lock.yaml"), "utf8"));
|
|
const packages = lockfile?.packages;
|
|
if (!packages || typeof packages !== "object" || Array.isArray(packages)) {
|
|
throw new Error("pnpm-lock.yaml is missing package resolution data.");
|
|
}
|
|
const lockPackages = new Set();
|
|
for (const [packageKey, metadata] of Object.entries(packages)) {
|
|
const parsed = parsePnpmPackageKey(packageKey);
|
|
if (!parsed) {
|
|
continue;
|
|
}
|
|
lockPackages.add(`${parsed.name}@${parsed.version}`);
|
|
if (metadata && typeof metadata === "object" && typeof metadata.version === "string") {
|
|
lockPackages.add(`${parsed.name}@${metadata.version}`);
|
|
}
|
|
}
|
|
return lockPackages;
|
|
}
|
|
|
|
function collectPnpmLockPackageVersions(lockfile) {
|
|
const packages = lockfile?.packages;
|
|
if (!packages || typeof packages !== "object" || Array.isArray(packages)) {
|
|
return new Map();
|
|
}
|
|
const versionsByName = new Map();
|
|
for (const packageKey of Object.keys(packages)) {
|
|
const parsed = parsePnpmPackageKey(packageKey);
|
|
if (!parsed) {
|
|
continue;
|
|
}
|
|
const versions = versionsByName.get(parsed.name) ?? new Set();
|
|
versions.add(parsed.version);
|
|
versionsByName.set(parsed.name, versions);
|
|
}
|
|
return versionsByName;
|
|
}
|
|
|
|
function stableVersionParts(version) {
|
|
const match = version.match(STABLE_VERSION_PATTERN);
|
|
return match
|
|
? {
|
|
major: Number(match[1]),
|
|
minor: Number(match[2]),
|
|
patch: Number(match[3]),
|
|
}
|
|
: null;
|
|
}
|
|
|
|
function pnpmLockOverrideVersionForVersions(versions) {
|
|
const sortedVersions = [...versions].toSorted((left, right) => left.localeCompare(right));
|
|
if (sortedVersions.length === 1) {
|
|
return exactVersionFromOverrideSpec(sortedVersions[0]) === null ? null : sortedVersions[0];
|
|
}
|
|
|
|
const parsedVersions = sortedVersions.map((version) => ({
|
|
version,
|
|
parts: stableVersionParts(version),
|
|
}));
|
|
if (parsedVersions.some(({ parts }) => parts === null)) {
|
|
return null;
|
|
}
|
|
|
|
const [{ parts: firstParts }] = parsedVersions;
|
|
if (
|
|
parsedVersions.some(
|
|
({ parts }) => parts.major !== firstParts.major || parts.minor !== firstParts.minor,
|
|
)
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
// npm patch ranges can float past the pnpm lock. Pin to the newest locked patch
|
|
// when the lock only contains one major/minor line, but keep true version forks free.
|
|
return parsedVersions.toSorted((left, right) => right.parts.patch - left.parts.patch)[0].version;
|
|
}
|
|
|
|
function readPnpmLockVersionOverrides() {
|
|
const lockfile = parseYaml(readFileSync(path.join(ROOT_DIR, "pnpm-lock.yaml"), "utf8"));
|
|
const versionsByName = collectPnpmLockPackageVersions(lockfile);
|
|
if (versionsByName.size === 0) {
|
|
throw new Error("pnpm-lock.yaml is missing package resolution data.");
|
|
}
|
|
return Object.fromEntries(
|
|
[...versionsByName.entries()]
|
|
.map(([name, versions]) => [name, pnpmLockOverrideVersionForVersions(versions)])
|
|
.filter(([, version]) => version !== null)
|
|
.toSorted(([left], [right]) => left.localeCompare(right)),
|
|
);
|
|
}
|
|
|
|
function addNestedOverride(overrides, parentSelector, dependencyName, version, conflicts) {
|
|
const current = overrides[parentSelector];
|
|
if (current !== undefined && !isPlainObject(current)) {
|
|
conflicts.add(parentSelector);
|
|
return;
|
|
}
|
|
const nested = current ?? {};
|
|
const existing = nested[dependencyName];
|
|
if (existing !== undefined && JSON.stringify(existing) !== JSON.stringify(version)) {
|
|
conflicts.add(parentSelector);
|
|
return;
|
|
}
|
|
nested[dependencyName] = version;
|
|
overrides[parentSelector] = nested;
|
|
}
|
|
|
|
function expandScopedOverrideValue(overrides, dependencyName, version, seen = new Set()) {
|
|
const childSelector = `${dependencyName}@${version}`;
|
|
if (seen.has(childSelector)) {
|
|
return version;
|
|
}
|
|
const childOverrides = overrides[childSelector];
|
|
if (!isPlainObject(childOverrides)) {
|
|
return version;
|
|
}
|
|
const childSeen = new Set(seen);
|
|
childSeen.add(childSelector);
|
|
return Object.fromEntries(
|
|
[
|
|
[".", version],
|
|
...Object.entries(childOverrides).map(([nestedName, nestedVersion]) => [
|
|
nestedName,
|
|
typeof nestedVersion === "string"
|
|
? expandScopedOverrideValue(overrides, nestedName, nestedVersion, childSeen)
|
|
: nestedVersion,
|
|
]),
|
|
].toSorted(([left], [right]) => left.localeCompare(right)),
|
|
);
|
|
}
|
|
|
|
function expandScopedOverrideChildren(overrides) {
|
|
return Object.fromEntries(
|
|
Object.entries(overrides)
|
|
.map(([parentSelector, nestedOverrides]) => [
|
|
parentSelector,
|
|
isPlainObject(nestedOverrides)
|
|
? Object.fromEntries(
|
|
Object.entries(nestedOverrides)
|
|
.map(([dependencyName, version]) => [
|
|
dependencyName,
|
|
typeof version === "string"
|
|
? expandScopedOverrideValue(overrides, dependencyName, version)
|
|
: version,
|
|
])
|
|
.toSorted(([left], [right]) => left.localeCompare(right)),
|
|
)
|
|
: typeof nestedOverrides === "string" &&
|
|
exactVersionFromOverrideSpec(nestedOverrides) !== null
|
|
? isPlainObject(
|
|
overrides[`${parentSelector}@${exactVersionFromOverrideSpec(nestedOverrides)}`],
|
|
)
|
|
? expandScopedOverrideValue(
|
|
overrides,
|
|
parentSelector,
|
|
exactVersionFromOverrideSpec(nestedOverrides),
|
|
)
|
|
: nestedOverrides
|
|
: nestedOverrides,
|
|
])
|
|
.toSorted(([left], [right]) => left.localeCompare(right)),
|
|
);
|
|
}
|
|
|
|
function readPnpmLockScopedVersionOverrides() {
|
|
const lockfile = parseYaml(readFileSync(path.join(ROOT_DIR, "pnpm-lock.yaml"), "utf8"));
|
|
const versionsByName = collectPnpmLockPackageVersions(lockfile);
|
|
if (versionsByName.size === 0) {
|
|
throw new Error("pnpm-lock.yaml is missing package resolution data.");
|
|
}
|
|
const forkedPackageNames = new Set(
|
|
[...versionsByName.entries()]
|
|
.filter(
|
|
([, versions]) =>
|
|
versions.size > 1 && pnpmLockOverrideVersionForVersions(versions) === null,
|
|
)
|
|
.map(([name]) => name),
|
|
);
|
|
if (forkedPackageNames.size === 0) {
|
|
return {};
|
|
}
|
|
|
|
const overrides = {};
|
|
const conflicts = new Set();
|
|
for (const [snapshotKey, snapshot] of Object.entries(lockfile?.snapshots ?? {})) {
|
|
const parent = parsePnpmPackageKey(snapshotKey);
|
|
const dependencies = snapshot?.dependencies;
|
|
if (
|
|
!parent ||
|
|
!dependencies ||
|
|
typeof dependencies !== "object" ||
|
|
Array.isArray(dependencies)
|
|
) {
|
|
continue;
|
|
}
|
|
const parentSelector = `${parent.name}@${parent.version}`;
|
|
for (const [dependencyName, dependencySpec] of Object.entries(dependencies)) {
|
|
if (!forkedPackageNames.has(dependencyName)) {
|
|
continue;
|
|
}
|
|
const version = exactVersionFromOverrideSpec(String(dependencySpec));
|
|
if (!version || !versionsByName.get(dependencyName)?.has(version)) {
|
|
continue;
|
|
}
|
|
addNestedOverride(overrides, parentSelector, dependencyName, version, conflicts);
|
|
}
|
|
}
|
|
|
|
for (const parentSelector of conflicts) {
|
|
delete overrides[parentSelector];
|
|
}
|
|
return expandScopedOverrideChildren(overrides);
|
|
}
|
|
|
|
function setKey(values) {
|
|
return [...values].toSorted((left, right) => left.localeCompare(right)).join("\0");
|
|
}
|
|
|
|
function mergeOverrideEntry(merged, name, spec) {
|
|
const current = merged[name];
|
|
if (current === undefined) {
|
|
merged[name] = spec;
|
|
return;
|
|
}
|
|
if (isPlainObject(current) && isPlainObject(spec)) {
|
|
for (const [nestedName, nestedSpec] of Object.entries(spec)) {
|
|
mergeOverrideEntry(current, nestedName, nestedSpec);
|
|
}
|
|
return;
|
|
}
|
|
if (
|
|
typeof current === "string" &&
|
|
isPlainObject(spec) &&
|
|
typeof spec["."] === "string" &&
|
|
exactOverrideVersionsMatch(current, spec["."])
|
|
) {
|
|
merged[name] = { ".": preferredExactOverrideRootSpec(current, spec["."]) };
|
|
for (const [nestedName, nestedSpec] of Object.entries(spec)) {
|
|
if (nestedName === ".") {
|
|
continue;
|
|
}
|
|
mergeOverrideEntry(merged[name], nestedName, nestedSpec);
|
|
}
|
|
return;
|
|
}
|
|
if (
|
|
isPlainObject(current) &&
|
|
typeof spec === "string" &&
|
|
typeof current["."] === "string" &&
|
|
exactOverrideVersionsMatch(current["."], spec)
|
|
) {
|
|
current["."] = preferredExactOverrideRootSpec(current["."], spec);
|
|
return;
|
|
}
|
|
if (JSON.stringify(current) !== JSON.stringify(spec)) {
|
|
throw new Error(`package.json overrides.${name} conflicts with pnpm lock policy for ${name}`);
|
|
}
|
|
}
|
|
|
|
function preferredExactOverrideRootSpec(current, incoming) {
|
|
return incoming.startsWith("npm:") ? incoming : current;
|
|
}
|
|
|
|
function exactOverrideVersionsMatch(left, right) {
|
|
const leftVersion = exactVersionFromOverrideSpec(left);
|
|
if (leftVersion === null || leftVersion !== exactVersionFromOverrideSpec(right)) {
|
|
return false;
|
|
}
|
|
const leftAlias = parseNpmAliasOverrideSpec(left);
|
|
const rightAlias = parseNpmAliasOverrideSpec(right);
|
|
return !leftAlias || !rightAlias || leftAlias.name === rightAlias.name;
|
|
}
|
|
|
|
function parseNpmAliasOverrideSpec(spec) {
|
|
if (!spec.startsWith("npm:")) {
|
|
return null;
|
|
}
|
|
const versionIndex = spec.lastIndexOf("@");
|
|
if (versionIndex <= "npm:".length) {
|
|
return null;
|
|
}
|
|
return { name: spec.slice("npm:".length, versionIndex) };
|
|
}
|
|
|
|
function mergeOverrides(packageOverrides, workspaceOverrides, pnpmLockOverrides) {
|
|
const merged = normalizeOverrides(packageOverrides);
|
|
for (const [name, spec] of [
|
|
...Object.entries(workspaceOverrides),
|
|
...Object.entries(pnpmLockOverrides),
|
|
]) {
|
|
mergeOverrideEntry(merged, name, spec);
|
|
}
|
|
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
}
|
|
|
|
function readShrinkwrapOverrides() {
|
|
return expandScopedOverrideChildren(
|
|
mergeOverrides(
|
|
undefined,
|
|
readWorkspaceOverrides(),
|
|
mergeOverrides(readPnpmLockVersionOverrides(), readPnpmLockScopedVersionOverrides(), {}),
|
|
),
|
|
);
|
|
}
|
|
|
|
function packageJsonForShrinkwrap(packageJson, shrinkwrapOverrides) {
|
|
const normalized = { ...packageJson };
|
|
delete normalized.devDependencies;
|
|
normalized.overrides = mergeOverrides(packageJson.overrides, shrinkwrapOverrides, {});
|
|
return normalized;
|
|
}
|
|
|
|
export function createNpmShrinkwrapCommand(args, options = {}) {
|
|
return resolveNpmRunner({
|
|
comSpec: options.comSpec,
|
|
env: options.env,
|
|
execPath: options.execPath,
|
|
existsSync: options.existsSync,
|
|
npmArgs: args,
|
|
platform: options.platform,
|
|
});
|
|
}
|
|
|
|
function runNpm(args, cwd) {
|
|
const npm = createNpmShrinkwrapCommand(args);
|
|
execFileSync(npm.command, npm.args, {
|
|
cwd,
|
|
env: npm.env ?? process.env,
|
|
shell: npm.shell,
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
windowsVerbatimArguments: npm.windowsVerbatimArguments,
|
|
});
|
|
}
|
|
|
|
function packageExtensionAppliesToDependency(selector, dependencyName) {
|
|
return selector === dependencyName || selector.startsWith(`${dependencyName}@`);
|
|
}
|
|
|
|
function packageExtensionMarksOptionalPeer(packageExtension) {
|
|
const peerDependenciesMeta = packageExtension?.peerDependenciesMeta;
|
|
if (
|
|
!peerDependenciesMeta ||
|
|
typeof peerDependenciesMeta !== "object" ||
|
|
Array.isArray(peerDependenciesMeta)
|
|
) {
|
|
return false;
|
|
}
|
|
return Object.values(peerDependenciesMeta).some((meta) => meta?.optional === true);
|
|
}
|
|
|
|
function shouldUseLegacyPeerDepsForShrinkwrap(
|
|
packageJson,
|
|
packageExtensions = readWorkspacePackageExtensions(),
|
|
) {
|
|
const dependencies = Object.keys(packageJson.dependencies ?? {});
|
|
if (dependencies.length === 0) {
|
|
return false;
|
|
}
|
|
for (const dependencyName of dependencies) {
|
|
for (const [selector, packageExtension] of Object.entries(packageExtensions)) {
|
|
if (
|
|
packageExtensionAppliesToDependency(selector, dependencyName) &&
|
|
packageExtensionMarksOptionalPeer(packageExtension)
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function applyPackageExtensionPeerMetadata(
|
|
lockfile,
|
|
packageExtensions = readWorkspacePackageExtensions(),
|
|
) {
|
|
const packages = lockfile?.packages;
|
|
if (!packages || typeof packages !== "object" || Array.isArray(packages)) {
|
|
return lockfile;
|
|
}
|
|
|
|
for (const [lockPath, metadata] of Object.entries(packages)) {
|
|
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
continue;
|
|
}
|
|
const packageName = metadata.name ?? parseLockPackagePath(lockPath).at(-1)?.name;
|
|
if (!packageName || !metadata.peerDependencies) {
|
|
continue;
|
|
}
|
|
for (const [selector, packageExtension] of Object.entries(packageExtensions)) {
|
|
if (!packageExtensionAppliesToDependency(selector, packageName)) {
|
|
continue;
|
|
}
|
|
const peerDependenciesMeta = packageExtension?.peerDependenciesMeta;
|
|
if (
|
|
!peerDependenciesMeta ||
|
|
typeof peerDependenciesMeta !== "object" ||
|
|
Array.isArray(peerDependenciesMeta)
|
|
) {
|
|
continue;
|
|
}
|
|
for (const [peerName, peerMeta] of Object.entries(peerDependenciesMeta)) {
|
|
if (metadata.peerDependencies[peerName] === undefined) {
|
|
continue;
|
|
}
|
|
metadata.peerDependenciesMeta ??= {};
|
|
const existingPeerMeta = metadata.peerDependenciesMeta[peerName];
|
|
metadata.peerDependenciesMeta[peerName] = existingPeerMeta
|
|
? { ...existingPeerMeta, ...peerMeta }
|
|
: { ...peerMeta };
|
|
}
|
|
}
|
|
}
|
|
|
|
return lockfile;
|
|
}
|
|
|
|
function exactVersionFromOverrideSpec(spec) {
|
|
if (!spec || typeof spec !== "string") {
|
|
return null;
|
|
}
|
|
if (EXACT_VERSION_PATTERN.test(spec)) {
|
|
return spec;
|
|
}
|
|
if (!spec.startsWith("npm:")) {
|
|
return null;
|
|
}
|
|
const versionIndex = spec.lastIndexOf("@");
|
|
if (versionIndex <= "npm:".length) {
|
|
return null;
|
|
}
|
|
const version = spec.slice(versionIndex + 1);
|
|
return EXACT_VERSION_PATTERN.test(version) ? version : null;
|
|
}
|
|
|
|
function exactOverrideRulesFromOverrides(overrides) {
|
|
return Object.fromEntries(
|
|
Object.entries(normalizeOverrides(overrides))
|
|
.map(([name, spec]) => [name, exactVersionFromOverrideSpec(spec)])
|
|
.filter((entry) => entry[1] !== null),
|
|
);
|
|
}
|
|
|
|
function parseLockPackagePath(lockPath) {
|
|
if (!lockPath.startsWith("node_modules/")) {
|
|
return [];
|
|
}
|
|
const packages = [];
|
|
let remaining = lockPath;
|
|
let current = "";
|
|
while (remaining.startsWith("node_modules/")) {
|
|
const withoutPrefix = remaining.slice("node_modules/".length);
|
|
const segments = withoutPrefix.split("/");
|
|
const name = segments[0]?.startsWith("@") ? segments.slice(0, 2).join("/") : segments[0];
|
|
if (!name) {
|
|
return packages;
|
|
}
|
|
current = current ? `${current}/node_modules/${name}` : `node_modules/${name}`;
|
|
packages.push({ name, path: current });
|
|
remaining = withoutPrefix.slice(name.length);
|
|
if (remaining.startsWith("/")) {
|
|
remaining = remaining.slice(1);
|
|
}
|
|
}
|
|
return packages;
|
|
}
|
|
|
|
function collectOverrideViolations(lockfile, overrideRules) {
|
|
const packages = lockfile?.packages;
|
|
if (!packages || typeof packages !== "object") {
|
|
return [];
|
|
}
|
|
const violations = [];
|
|
for (const [lockPath, metadata] of Object.entries(packages)) {
|
|
const packagePath = parseLockPackagePath(lockPath);
|
|
const packageName = packagePath.at(-1)?.name;
|
|
const expectedVersion = packageName ? overrideRules[packageName] : undefined;
|
|
if (!expectedVersion || metadata?.version === expectedVersion) {
|
|
continue;
|
|
}
|
|
violations.push({
|
|
path: lockPath,
|
|
packageName,
|
|
actualVersion: metadata?.version ?? "<missing>",
|
|
expectedVersion,
|
|
packagePath,
|
|
});
|
|
}
|
|
return violations;
|
|
}
|
|
|
|
function disableShrinkwrappedOverrideConflictSources(lockfile, overrideRules) {
|
|
const packages = lockfile?.packages;
|
|
if (!packages || typeof packages !== "object") {
|
|
return [];
|
|
}
|
|
/** @type {Set<string>} */
|
|
const disabled = new Set();
|
|
for (const violation of collectOverrideViolations(lockfile, overrideRules)) {
|
|
const ancestors = violation.packagePath.slice(0, -1).toReversed();
|
|
const shrinkwrappedAncestor = ancestors.find(
|
|
(ancestor) => packages[ancestor.path]?.hasShrinkwrap === true,
|
|
);
|
|
if (!shrinkwrappedAncestor) {
|
|
continue;
|
|
}
|
|
delete packages[shrinkwrappedAncestor.path].hasShrinkwrap;
|
|
disabled.add(shrinkwrappedAncestor.path);
|
|
}
|
|
for (const ancestorPath of disabled) {
|
|
const subtreePrefix = `${ancestorPath}/node_modules/`;
|
|
for (const lockPath of Object.keys(packages)) {
|
|
if (lockPath.startsWith(subtreePrefix)) {
|
|
delete packages[lockPath];
|
|
}
|
|
}
|
|
}
|
|
return [...disabled].toSorted((left, right) => left.localeCompare(right));
|
|
}
|
|
|
|
function describeOverrideViolations(violations) {
|
|
return violations
|
|
.slice(0, 5)
|
|
.map(
|
|
(violation) =>
|
|
`${violation.path} locked ${violation.actualVersion}, expected ${violation.expectedVersion}`,
|
|
)
|
|
.join("; ");
|
|
}
|
|
|
|
function normalizeShrinkwrapOverrides(tempDir, shrinkwrapOverrides, npmInstallArgs) {
|
|
const shrinkwrapPath = path.join(tempDir, "npm-shrinkwrap.json");
|
|
const overrideRules = exactOverrideRulesFromOverrides(shrinkwrapOverrides);
|
|
if (Object.keys(overrideRules).length === 0) {
|
|
return;
|
|
}
|
|
|
|
const shrinkwrap = JSON.parse(readFileSync(shrinkwrapPath, "utf8"));
|
|
const disabled = disableShrinkwrappedOverrideConflictSources(shrinkwrap, overrideRules);
|
|
if (disabled.length === 0) {
|
|
const violations = collectOverrideViolations(shrinkwrap, overrideRules);
|
|
if (violations.length > 0) {
|
|
throw new Error(
|
|
`generated npm-shrinkwrap.json violates workspace overrides: ${describeOverrideViolations(violations)}`,
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// npm ignores root overrides inside dependency-owned shrinkwraps. Mark those embedded
|
|
// shrinkwraps as inactive, drop their cached subtree, then ask npm to recalculate this
|
|
// package's authoritative lock with registry integrity hashes.
|
|
writeFileSync(shrinkwrapPath, `${JSON.stringify(shrinkwrap, null, 2)}\n`);
|
|
runNpm(npmInstallArgs, tempDir);
|
|
|
|
const normalized = JSON.parse(readFileSync(shrinkwrapPath, "utf8"));
|
|
const remaining = collectOverrideViolations(normalized, overrideRules);
|
|
if (remaining.length > 0) {
|
|
throw new Error(
|
|
`generated npm-shrinkwrap.json violates workspace overrides after disabling ${disabled.join(", ")}: ${describeOverrideViolations(remaining)}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function normalizeNpmVersionDrift(lockfile) {
|
|
const packages = lockfile?.packages;
|
|
if (!packages || typeof packages !== "object") {
|
|
return lockfile;
|
|
}
|
|
for (const metadata of Object.values(packages)) {
|
|
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
continue;
|
|
}
|
|
// npm 11 patch releases disagree on these package-lock v3 metadata fields.
|
|
// Keep the shrinkwrap stable across supported Node 24 patch versions.
|
|
delete metadata.libc;
|
|
if (metadata.peer === true) {
|
|
delete metadata.peer;
|
|
}
|
|
}
|
|
return lockfile;
|
|
}
|
|
|
|
function generateShrinkwrap(packageDir, options = {}) {
|
|
const tempDir = mkdtempSync(path.join(tmpdir(), "openclaw-shrinkwrap-"));
|
|
try {
|
|
const packageJson = JSON.parse(readFileSync(path.join(packageDir, "package.json"), "utf8"));
|
|
const currentShrinkwrap = readCurrentShrinkwrap(packageDir);
|
|
const shrinkwrapOverrides = mergeOverrides(
|
|
options.useCurrentShrinkwrapOverrides
|
|
? readCurrentShrinkwrapOverrides(packageDir, declaredPackageDependencies(packageJson))
|
|
: {},
|
|
readShrinkwrapOverrides(),
|
|
{},
|
|
);
|
|
const npmInstallArgs = [
|
|
"install",
|
|
"--package-lock-only",
|
|
"--ignore-scripts",
|
|
"--no-audit",
|
|
"--no-fund",
|
|
...(shouldUseLegacyPeerDepsForShrinkwrap(packageJson) ? ["--legacy-peer-deps"] : []),
|
|
];
|
|
writeFileSync(
|
|
path.join(tempDir, "package.json"),
|
|
`${JSON.stringify(packageJsonForShrinkwrap(packageJson, shrinkwrapOverrides), null, 2)}\n`,
|
|
);
|
|
runNpm(npmInstallArgs, tempDir);
|
|
runNpm(["shrinkwrap", "--ignore-scripts", "--no-audit", "--no-fund"], tempDir);
|
|
normalizeShrinkwrapOverrides(tempDir, shrinkwrapOverrides, npmInstallArgs);
|
|
const generated = restoreCurrentPnpmLockedPackages(
|
|
normalizeNpmVersionDrift(
|
|
applyPackageExtensionPeerMetadata(
|
|
JSON.parse(readFileSync(path.join(tempDir, "npm-shrinkwrap.json"), "utf8")),
|
|
),
|
|
),
|
|
currentShrinkwrap,
|
|
);
|
|
assertShrinkwrapMatchesPnpmLock(generated);
|
|
return `${JSON.stringify(generated, null, 2)}\n`;
|
|
} finally {
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
function collectPnpmLockViolations(shrinkwrap, pnpmLockPackages = readPnpmLockPackages()) {
|
|
const packages = shrinkwrap?.packages;
|
|
if (!packages || typeof packages !== "object") {
|
|
return [];
|
|
}
|
|
const violations = [];
|
|
for (const [lockPath, metadata] of Object.entries(packages)) {
|
|
if (lockPath === "" || !metadata || typeof metadata !== "object" || !metadata.version) {
|
|
continue;
|
|
}
|
|
const packageName = metadata.name ?? parseLockPackagePath(lockPath).at(-1)?.name;
|
|
if (!packageName) {
|
|
continue;
|
|
}
|
|
const packageKey = `${packageName}@${metadata.version}`;
|
|
if (!pnpmLockPackages.has(packageKey)) {
|
|
violations.push({ path: lockPath, packageKey });
|
|
}
|
|
}
|
|
return violations;
|
|
}
|
|
|
|
function declaredPackageDependencies(packageJson) {
|
|
const dependencies = new Set();
|
|
for (const key of ["dependencies", "optionalDependencies", "peerDependencies"]) {
|
|
const values = packageJson?.[key];
|
|
if (!values || typeof values !== "object" || Array.isArray(values)) {
|
|
continue;
|
|
}
|
|
for (const dependencyName of Object.keys(values)) {
|
|
dependencies.add(dependencyName);
|
|
}
|
|
}
|
|
return dependencies;
|
|
}
|
|
|
|
function packageNameForLockPath(lockPath) {
|
|
return parseLockPackagePath(lockPath).at(-1)?.name;
|
|
}
|
|
|
|
function dependencyCandidatePaths(parentLockPath, dependencyName) {
|
|
const candidates = new Set();
|
|
if (parentLockPath) {
|
|
candidates.add(`${parentLockPath}/node_modules/${dependencyName}`);
|
|
}
|
|
|
|
let current = parentLockPath;
|
|
while (current) {
|
|
const nestedNodeModulesIndex = current.lastIndexOf("/node_modules/");
|
|
if (nestedNodeModulesIndex === -1) {
|
|
candidates.add(`node_modules/${dependencyName}`);
|
|
break;
|
|
}
|
|
const ancestorPackagePath = current.slice(0, nestedNodeModulesIndex);
|
|
candidates.add(`${ancestorPackagePath}/node_modules/${dependencyName}`);
|
|
current = ancestorPackagePath;
|
|
}
|
|
if (!parentLockPath) {
|
|
candidates.add(`node_modules/${dependencyName}`);
|
|
}
|
|
return [...candidates];
|
|
}
|
|
|
|
function resolveShrinkwrapDependency(packages, parentLockPath, dependencyName) {
|
|
for (const candidatePath of dependencyCandidatePaths(parentLockPath, dependencyName)) {
|
|
const candidate = packages[candidatePath];
|
|
if (candidate?.version) {
|
|
return {
|
|
path: candidatePath,
|
|
version: candidate.version,
|
|
};
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function collectCurrentShrinkwrapOverrides(
|
|
shrinkwrap,
|
|
declaredDependencies = new Set(),
|
|
pnpmLockPackages = readPnpmLockPackages(),
|
|
) {
|
|
const packages = shrinkwrap?.packages;
|
|
if (!packages || typeof packages !== "object") {
|
|
return {};
|
|
}
|
|
const versionsByName = new Map();
|
|
for (const [lockPath, metadata] of Object.entries(packages)) {
|
|
if (lockPath === "" || !metadata || typeof metadata !== "object" || !metadata.version) {
|
|
continue;
|
|
}
|
|
const packageName = metadata.name ?? packageNameForLockPath(lockPath);
|
|
if (
|
|
!packageName ||
|
|
declaredDependencies.has(packageName) ||
|
|
!pnpmLockPackages.has(`${packageName}@${metadata.version}`)
|
|
) {
|
|
continue;
|
|
}
|
|
const versions = versionsByName.get(packageName) ?? new Set();
|
|
versions.add(metadata.version);
|
|
versionsByName.set(packageName, versions);
|
|
}
|
|
|
|
const overrides = Object.fromEntries(
|
|
[...versionsByName.entries()]
|
|
.filter(([, versions]) => versions.size === 1)
|
|
.map(([name, versions]) => [name, [...versions][0]])
|
|
.toSorted(([left], [right]) => left.localeCompare(right)),
|
|
);
|
|
const forkedPackageNames = new Set(
|
|
[...versionsByName.entries()].filter(([, versions]) => versions.size > 1).map(([name]) => name),
|
|
);
|
|
const conflicts = new Set();
|
|
for (const [lockPath, metadata] of Object.entries(packages)) {
|
|
if (lockPath === "" || !metadata || typeof metadata !== "object" || !metadata.version) {
|
|
continue;
|
|
}
|
|
const parentName = metadata.name ?? packageNameForLockPath(lockPath);
|
|
const dependencies = metadata.dependencies;
|
|
if (
|
|
!parentName ||
|
|
!dependencies ||
|
|
typeof dependencies !== "object" ||
|
|
Array.isArray(dependencies)
|
|
) {
|
|
continue;
|
|
}
|
|
const parentSelector = `${parentName}@${metadata.version}`;
|
|
for (const dependencyName of Object.keys(dependencies)) {
|
|
if (!forkedPackageNames.has(dependencyName)) {
|
|
continue;
|
|
}
|
|
const resolved = resolveShrinkwrapDependency(packages, lockPath, dependencyName);
|
|
if (!resolved || !pnpmLockPackages.has(`${dependencyName}@${resolved.version}`)) {
|
|
continue;
|
|
}
|
|
addNestedOverride(overrides, parentSelector, dependencyName, resolved.version, conflicts);
|
|
}
|
|
}
|
|
for (const parentSelector of conflicts) {
|
|
delete overrides[parentSelector];
|
|
}
|
|
return expandScopedOverrideChildren(overrides);
|
|
}
|
|
|
|
function readCurrentShrinkwrapOverrides(
|
|
packageDir,
|
|
declaredDependencies = new Set(),
|
|
pnpmLockPackages = readPnpmLockPackages(),
|
|
) {
|
|
try {
|
|
return collectCurrentShrinkwrapOverrides(
|
|
JSON.parse(readFileSync(shrinkwrapPathForPackage(packageDir), "utf8")),
|
|
declaredDependencies,
|
|
pnpmLockPackages,
|
|
);
|
|
} catch (error) {
|
|
if (error?.code === "ENOENT") {
|
|
return {};
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function readCurrentShrinkwrap(packageDir) {
|
|
try {
|
|
return JSON.parse(readFileSync(shrinkwrapPathForPackage(packageDir), "utf8"));
|
|
} catch (error) {
|
|
if (error?.code === "ENOENT") {
|
|
return null;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function isStablePatchDrift(generatedVersion, currentVersion) {
|
|
const generatedParts = stableVersionParts(generatedVersion);
|
|
const currentParts = stableVersionParts(currentVersion);
|
|
return (
|
|
generatedParts !== null &&
|
|
currentParts !== null &&
|
|
generatedParts.major === currentParts.major &&
|
|
generatedParts.minor === currentParts.minor &&
|
|
generatedParts.patch !== currentParts.patch
|
|
);
|
|
}
|
|
|
|
function compareStableVersions(leftVersion, rightVersion) {
|
|
const left = stableVersionParts(leftVersion);
|
|
const right = stableVersionParts(rightVersion);
|
|
if (!left || !right) {
|
|
return null;
|
|
}
|
|
return left.major - right.major || left.minor - right.minor || left.patch - right.patch;
|
|
}
|
|
|
|
function versionSatisfiesSimpleSpec(version, spec) {
|
|
const normalized = typeof spec === "string" ? spec.trim() : "";
|
|
if (normalized === "" || normalized === "*") {
|
|
return true;
|
|
}
|
|
const match = normalized.match(/^(?<operator>\^|~|>=)?(?<version>\d+\.\d+\.\d+)$/u);
|
|
if (!match?.groups) {
|
|
return normalized === version;
|
|
}
|
|
const minimumVersion = match.groups.version;
|
|
const comparison = compareStableVersions(version, minimumVersion);
|
|
if (comparison === null || comparison < 0) {
|
|
return false;
|
|
}
|
|
const candidate = stableVersionParts(version);
|
|
const minimum = stableVersionParts(minimumVersion);
|
|
if (!candidate || !minimum) {
|
|
return false;
|
|
}
|
|
switch (match.groups.operator) {
|
|
case "^":
|
|
return minimum.major > 0
|
|
? candidate.major === minimum.major
|
|
: minimum.minor > 0
|
|
? candidate.major === 0 && candidate.minor === minimum.minor
|
|
: candidate.major === 0 && candidate.minor === 0 && candidate.patch === minimum.patch;
|
|
case "~":
|
|
return candidate.major === minimum.major && candidate.minor === minimum.minor;
|
|
case ">=":
|
|
return true;
|
|
default:
|
|
return comparison === 0;
|
|
}
|
|
}
|
|
|
|
function dependencySpecForLockPath(packages, lockPath, dependencyName) {
|
|
const packagePath = parseLockPackagePath(lockPath);
|
|
const parentPath = packagePath.at(-2)?.path ?? "";
|
|
const parent = packages[parentPath];
|
|
return (
|
|
parent?.dependencies?.[dependencyName] ??
|
|
parent?.optionalDependencies?.[dependencyName] ??
|
|
parent?.peerDependencies?.[dependencyName] ??
|
|
null
|
|
);
|
|
}
|
|
|
|
function restoreCurrentPnpmLockedPackages(
|
|
generated,
|
|
current,
|
|
pnpmLockPackages = readPnpmLockPackages(),
|
|
) {
|
|
if (!current) {
|
|
return generated;
|
|
}
|
|
const generatedPackages = generated?.packages;
|
|
const currentPackages = current?.packages;
|
|
if (
|
|
!generatedPackages ||
|
|
typeof generatedPackages !== "object" ||
|
|
!currentPackages ||
|
|
typeof currentPackages !== "object"
|
|
) {
|
|
return generated;
|
|
}
|
|
|
|
for (const [lockPath, metadata] of Object.entries(generatedPackages)) {
|
|
if (lockPath === "" || !metadata || typeof metadata !== "object" || !metadata.version) {
|
|
continue;
|
|
}
|
|
const packageName = metadata.name ?? packageNameForLockPath(lockPath);
|
|
if (!packageName || pnpmLockPackages.has(`${packageName}@${metadata.version}`)) {
|
|
continue;
|
|
}
|
|
|
|
const currentMetadata = currentPackages[lockPath];
|
|
const currentPackageName = currentMetadata?.name ?? packageNameForLockPath(lockPath);
|
|
if (
|
|
!currentMetadata ||
|
|
typeof currentMetadata !== "object" ||
|
|
!currentMetadata.version ||
|
|
currentPackageName !== packageName ||
|
|
!isStablePatchDrift(metadata.version, currentMetadata.version) ||
|
|
!versionSatisfiesSimpleSpec(
|
|
currentMetadata.version,
|
|
dependencySpecForLockPath(generatedPackages, lockPath, packageName),
|
|
) ||
|
|
!pnpmLockPackages.has(`${packageName}@${currentMetadata.version}`)
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
// npm can float transitive patch ranges beyond pnpm's lock when one package
|
|
// name has multiple locked major lines. Keep the existing shrinkwrap entry
|
|
// when it still matches the canonical pnpm lock.
|
|
generatedPackages[lockPath] = currentMetadata;
|
|
}
|
|
|
|
return generated;
|
|
}
|
|
|
|
function assertShrinkwrapMatchesPnpmLock(shrinkwrap) {
|
|
const violations = collectPnpmLockViolations(shrinkwrap);
|
|
if (violations.length === 0) {
|
|
return;
|
|
}
|
|
const examples = violations
|
|
.slice(0, 5)
|
|
.map((violation) => `${violation.path} locked ${violation.packageKey}`)
|
|
.join("; ");
|
|
throw new Error(
|
|
`generated npm-shrinkwrap.json contains package versions absent from pnpm-lock.yaml: ${examples}`,
|
|
);
|
|
}
|
|
|
|
function packageLabel(packageDir) {
|
|
const relative = path.relative(ROOT_DIR, packageDir);
|
|
return relative ? relative.replaceAll(path.sep, "/") : ".";
|
|
}
|
|
|
|
function shrinkwrapPathForPackage(packageDir) {
|
|
return path.join(packageDir, "npm-shrinkwrap.json");
|
|
}
|
|
|
|
function listPublishablePluginPackageDirs() {
|
|
const extensionsDir = path.join(ROOT_DIR, "extensions");
|
|
return readdirSync(extensionsDir, { withFileTypes: true })
|
|
.filter((entry) => entry.isDirectory())
|
|
.map((entry) => path.posix.join("extensions", entry.name))
|
|
.filter((packageDir) => {
|
|
const packageJsonPath = path.join(ROOT_DIR, packageDir, "package.json");
|
|
if (!existsSync(packageJsonPath)) {
|
|
return false;
|
|
}
|
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
return packageJson.openclaw?.release?.publishToNpm === true;
|
|
})
|
|
.toSorted((left, right) => left.localeCompare(right));
|
|
}
|
|
|
|
function shrinkwrapPackageDirsForChangedPaths(changedPaths) {
|
|
const packageDirs = new Set();
|
|
const publishablePluginPackageDirs = new Set(listPublishablePluginPackageDirs());
|
|
let hasAmbiguousDependencyPolicyChange = false;
|
|
let hasLockfileChange = false;
|
|
|
|
for (const rawPath of changedPaths) {
|
|
const changedPath = String(rawPath ?? "")
|
|
.trim()
|
|
.replaceAll("\\", "/")
|
|
.replace(/^\.\/+/u, "");
|
|
if (!changedPath) {
|
|
continue;
|
|
}
|
|
if (changedPath === "package.json" || changedPath === "npm-shrinkwrap.json") {
|
|
packageDirs.add(ROOT_DIR);
|
|
continue;
|
|
}
|
|
const extensionMatch = changedPath.match(
|
|
/^(extensions\/[^/]+)\/(?:package\.json|npm-shrinkwrap\.json)$/u,
|
|
);
|
|
if (extensionMatch && publishablePluginPackageDirs.has(extensionMatch[1])) {
|
|
packageDirs.add(path.resolve(ROOT_DIR, extensionMatch[1]));
|
|
continue;
|
|
}
|
|
if (changedPath === "pnpm-lock.yaml") {
|
|
hasLockfileChange = true;
|
|
continue;
|
|
}
|
|
if (
|
|
changedPath === "pnpm-workspace.yaml" ||
|
|
changedPath === "scripts/generate-npm-shrinkwrap.mjs"
|
|
) {
|
|
hasAmbiguousDependencyPolicyChange = true;
|
|
}
|
|
}
|
|
|
|
if (hasAmbiguousDependencyPolicyChange) {
|
|
return [
|
|
ROOT_DIR,
|
|
...listPublishablePluginPackageDirs().map((dir) => path.resolve(ROOT_DIR, dir)),
|
|
];
|
|
}
|
|
|
|
if (hasLockfileChange) {
|
|
return [
|
|
ROOT_DIR,
|
|
...listPublishablePluginPackageDirs().map((dir) => path.resolve(ROOT_DIR, dir)),
|
|
];
|
|
}
|
|
return [...packageDirs].toSorted((left, right) =>
|
|
packageLabel(left).localeCompare(packageLabel(right)),
|
|
);
|
|
}
|
|
|
|
function normalizeChangedPath(rawPath) {
|
|
return String(rawPath ?? "")
|
|
.trim()
|
|
.replaceAll("\\", "/")
|
|
.replace(/^\.\/+/u, "");
|
|
}
|
|
|
|
function packageDependencyInputsChanged(packageDir, changedPaths) {
|
|
const relativePackageDir = packageLabel(packageDir);
|
|
const packageManifestPath =
|
|
relativePackageDir === "." ? "package.json" : `${relativePackageDir}/package.json`;
|
|
const shrinkwrapPath =
|
|
relativePackageDir === "."
|
|
? "npm-shrinkwrap.json"
|
|
: `${relativePackageDir}/npm-shrinkwrap.json`;
|
|
return changedPaths.some((rawPath) => {
|
|
const changedPath = normalizeChangedPath(rawPath);
|
|
return (
|
|
changedPath === "pnpm-lock.yaml" ||
|
|
changedPath === "pnpm-workspace.yaml" ||
|
|
changedPath === "scripts/generate-npm-shrinkwrap.mjs" ||
|
|
changedPath === packageManifestPath ||
|
|
changedPath === shrinkwrapPath
|
|
);
|
|
});
|
|
}
|
|
|
|
function listCheckChangedPaths() {
|
|
try {
|
|
return listChangedPathsFromGit({ base: "origin/main", head: "HEAD" });
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function resolvePackageDirs(args) {
|
|
const packageDirs = [];
|
|
const check = args.includes("--check");
|
|
const all = args.includes("--all");
|
|
const plugins = args.includes("--plugins");
|
|
const changed = args.includes("--changed");
|
|
const staged = args.includes("--staged");
|
|
const packageDirIndex = args.indexOf("--package-dir");
|
|
const baseIndex = args.indexOf("--base");
|
|
const headIndex = args.indexOf("--head");
|
|
if (packageDirIndex !== -1 && (all || plugins || changed)) {
|
|
throw new Error("--package-dir cannot be combined with --all, --plugins, or --changed.");
|
|
}
|
|
if ([all, plugins, changed].filter(Boolean).length > 1) {
|
|
throw new Error("--all, --plugins, and --changed cannot be combined.");
|
|
}
|
|
for (let index = 0; index < args.length; index += 1) {
|
|
const arg = args[index];
|
|
if (
|
|
arg === "--check" ||
|
|
arg === "--all" ||
|
|
arg === "--plugins" ||
|
|
arg === "--changed" ||
|
|
arg === "--staged"
|
|
) {
|
|
continue;
|
|
}
|
|
if (arg === "--package-dir") {
|
|
const value = args[index + 1];
|
|
if (!value || value.startsWith("--")) {
|
|
throw new Error("--package-dir requires a package directory.");
|
|
}
|
|
packageDirs.push(path.resolve(ROOT_DIR, value));
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if (arg === "--base" || arg === "--head") {
|
|
const value = args[index + 1];
|
|
if (!value || value.startsWith("--")) {
|
|
throw new Error(`${arg} requires a git ref.`);
|
|
}
|
|
index += 1;
|
|
continue;
|
|
}
|
|
throw new Error(usage());
|
|
}
|
|
|
|
if (!changed && (baseIndex !== -1 || headIndex !== -1 || staged)) {
|
|
throw new Error("--base, --head, and --staged require --changed.");
|
|
}
|
|
|
|
if (all) {
|
|
return {
|
|
check,
|
|
changedPaths: check ? listCheckChangedPaths() : [],
|
|
packageDirs: [
|
|
ROOT_DIR,
|
|
...listPublishablePluginPackageDirs().map((dir) => path.resolve(ROOT_DIR, dir)),
|
|
],
|
|
};
|
|
}
|
|
if (plugins) {
|
|
return {
|
|
check,
|
|
changedPaths: check ? listCheckChangedPaths() : [],
|
|
packageDirs: listPublishablePluginPackageDirs().map((dir) => path.resolve(ROOT_DIR, dir)),
|
|
};
|
|
}
|
|
if (changed) {
|
|
const base = baseIndex === -1 ? "origin/main" : args[baseIndex + 1];
|
|
const head = headIndex === -1 ? "HEAD" : args[headIndex + 1];
|
|
const changedPaths = staged
|
|
? listStagedChangedPaths()
|
|
: listChangedPathsFromGit({
|
|
base,
|
|
head,
|
|
});
|
|
return {
|
|
check,
|
|
changedPaths,
|
|
packageDirs: shrinkwrapPackageDirsForChangedPaths(changedPaths),
|
|
};
|
|
}
|
|
return {
|
|
check,
|
|
changedPaths: check ? listCheckChangedPaths() : [],
|
|
packageDirs: packageDirs.length > 0 ? packageDirs : [ROOT_DIR],
|
|
};
|
|
}
|
|
|
|
function updateOrCheckPackage(packageDir, check, changedPaths = []) {
|
|
const generated = generateShrinkwrap(packageDir, {
|
|
useCurrentShrinkwrapOverrides:
|
|
check && !packageDependencyInputsChanged(packageDir, changedPaths),
|
|
});
|
|
const shrinkwrapPath = shrinkwrapPathForPackage(packageDir);
|
|
const label = packageLabel(packageDir);
|
|
if (!check) {
|
|
writeFileSync(shrinkwrapPath, generated);
|
|
process.stdout.write(`${label}: npm-shrinkwrap.json updated.\n`);
|
|
return;
|
|
}
|
|
|
|
let current = "";
|
|
try {
|
|
current = readFileSync(shrinkwrapPath, "utf8");
|
|
} catch {
|
|
throw new Error(
|
|
`${label}: npm-shrinkwrap.json is missing. Run \`pnpm deps:shrinkwrap:generate\`.`,
|
|
);
|
|
}
|
|
if (current !== generated) {
|
|
throw new Error(
|
|
`${label}: npm-shrinkwrap.json is stale. Run \`pnpm deps:shrinkwrap:generate\`.`,
|
|
);
|
|
}
|
|
process.stdout.write(`${label}: npm-shrinkwrap.json is current.\n`);
|
|
}
|
|
|
|
function main() {
|
|
const { check, changedPaths, packageDirs } = resolvePackageDirs(process.argv.slice(2));
|
|
if (packageDirs.length === 0) {
|
|
process.stdout.write("No shrinkwrap-managed package changes detected.\n");
|
|
return;
|
|
}
|
|
for (const packageDir of packageDirs) {
|
|
updateOrCheckPackage(packageDir, check, changedPaths);
|
|
}
|
|
}
|
|
|
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
try {
|
|
main();
|
|
} catch (error) {
|
|
console.error(error instanceof Error ? error.message : String(error));
|
|
process.exitCode = 1;
|
|
}
|
|
}
|
|
|
|
export {
|
|
collectCurrentShrinkwrapOverrides,
|
|
collectOverrideViolations,
|
|
collectPnpmLockViolations,
|
|
disableShrinkwrappedOverrideConflictSources,
|
|
exactOverrideRulesFromOverrides,
|
|
exactVersionFromOverrideSpec,
|
|
mergeOverrides,
|
|
applyPackageExtensionPeerMetadata,
|
|
normalizeNpmVersionDrift,
|
|
packageJsonForShrinkwrap,
|
|
packageDependencyInputsChanged,
|
|
pnpmLockOverrideVersionForVersions,
|
|
parsePnpmPackageKey,
|
|
parseLockPackagePath,
|
|
readShrinkwrapOverrides,
|
|
restoreCurrentPnpmLockedPackages,
|
|
shouldUseLegacyPeerDepsForShrinkwrap,
|
|
shrinkwrapPackageDirsForChangedPaths,
|
|
};
|