fix(build): stabilize shrinkwrap generation

This commit is contained in:
Peter Steinberger
2026-05-26 21:27:22 +01:00
parent 17051894d0
commit 538b537cc5
4 changed files with 189 additions and 9 deletions

2
pnpm-lock.yaml generated
View File

@@ -8,8 +8,8 @@ overrides:
'@anthropic-ai/sdk': 0.98.0
hono: 4.12.18
'@hono/node-server': 1.19.14
'@aws-sdk/client-bedrock-runtime': 3.1053.0
'@aws-sdk/core': 3.974.13
'@aws-sdk/client-bedrock-runtime': 3.1053.0
'@aws-sdk/credential-provider-env': 3.972.39
'@aws-sdk/credential-provider-http': 3.972.41
'@aws-sdk/credential-provider-ini': 3.972.43

View File

@@ -65,8 +65,8 @@ overrides:
"@anthropic-ai/sdk": 0.98.0
hono: 4.12.18
"@hono/node-server": 1.19.14
"@aws-sdk/client-bedrock-runtime": 3.1053.0
"@aws-sdk/core": 3.974.13
"@aws-sdk/client-bedrock-runtime": 3.1053.0
"@aws-sdk/credential-provider-env": 3.972.39
"@aws-sdk/credential-provider-http": 3.972.41
"@aws-sdk/credential-provider-ini": 3.972.43

View File

@@ -462,11 +462,17 @@ function normalizeNpmVersionDrift(lockfile) {
return lockfile;
}
function generateShrinkwrap(packageDir) {
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 shrinkwrapOverrides = readShrinkwrapOverrides();
const shrinkwrapOverrides = mergeOverrides(
options.useCurrentShrinkwrapOverrides
? readCurrentShrinkwrapOverrides(packageDir, declaredPackageDependencies(packageJson))
: {},
readShrinkwrapOverrides(),
{},
);
const npmInstallArgs = [
"install",
"--package-lock-only",
@@ -516,6 +522,73 @@ function collectPnpmLockViolations(shrinkwrap, pnpmLockPackages = readPnpmLockPa
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 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 ?? parseLockPackagePath(lockPath).at(-1)?.name;
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);
}
return Object.fromEntries(
[...versionsByName.entries()]
.filter(([, versions]) => versions.size === 1)
.map(([name, versions]) => [name, [...versions][0]])
.toSorted(([left], [right]) => left.localeCompare(right)),
);
}
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 assertShrinkwrapMatchesPnpmLock(shrinkwrap) {
const violations = collectPnpmLockViolations(shrinkwrap);
if (violations.length === 0) {
@@ -610,6 +683,41 @@ function shrinkwrapPackageDirsForChangedPaths(changedPaths) {
);
}
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");
@@ -664,6 +772,7 @@ function resolvePackageDirs(args) {
if (all) {
return {
check,
changedPaths: check ? listCheckChangedPaths() : [],
packageDirs: [
ROOT_DIR,
...listPublishablePluginPackageDirs().map((dir) => path.resolve(ROOT_DIR, dir)),
@@ -673,6 +782,7 @@ function resolvePackageDirs(args) {
if (plugins) {
return {
check,
changedPaths: check ? listCheckChangedPaths() : [],
packageDirs: listPublishablePluginPackageDirs().map((dir) => path.resolve(ROOT_DIR, dir)),
};
}
@@ -687,14 +797,22 @@ function resolvePackageDirs(args) {
});
return {
check,
changedPaths,
packageDirs: shrinkwrapPackageDirsForChangedPaths(changedPaths),
};
}
return { check, packageDirs: packageDirs.length > 0 ? packageDirs : [ROOT_DIR] };
return {
check,
changedPaths: check ? listCheckChangedPaths() : [],
packageDirs: packageDirs.length > 0 ? packageDirs : [ROOT_DIR],
};
}
function updateOrCheckPackage(packageDir, check) {
const generated = generateShrinkwrap(packageDir);
function updateOrCheckPackage(packageDir, check, changedPaths = []) {
const generated = generateShrinkwrap(packageDir, {
useCurrentShrinkwrapOverrides:
check && !packageDependencyInputsChanged(packageDir, changedPaths),
});
const shrinkwrapPath = shrinkwrapPathForPackage(packageDir);
const label = packageLabel(packageDir);
if (!check) {
@@ -720,13 +838,13 @@ function updateOrCheckPackage(packageDir, check) {
}
function main() {
const { check, packageDirs } = resolvePackageDirs(process.argv.slice(2));
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);
updateOrCheckPackage(packageDir, check, changedPaths);
}
}
@@ -740,6 +858,7 @@ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.me
}
export {
collectCurrentShrinkwrapOverrides,
collectOverrideViolations,
collectPnpmLockViolations,
disableShrinkwrappedOverrideConflictSources,
@@ -748,6 +867,7 @@ export {
applyPackageExtensionPeerMetadata,
normalizeNpmVersionDrift,
packageJsonForShrinkwrap,
packageDependencyInputsChanged,
pnpmLockOverrideVersionForVersions,
parsePnpmPackageKey,
parseLockPackagePath,

View File

@@ -2,6 +2,7 @@ import path from "node:path";
import { describe, expect, it } from "vitest";
import {
applyPackageExtensionPeerMetadata,
collectCurrentShrinkwrapOverrides,
collectOverrideViolations,
collectPnpmLockViolations,
createNpmShrinkwrapCommand,
@@ -9,6 +10,7 @@ import {
exactOverrideRulesFromOverrides,
exactVersionFromOverrideSpec,
normalizeNpmVersionDrift,
packageDependencyInputsChanged,
pnpmLockOverrideVersionForVersions,
parsePnpmPackageKey,
parseLockPackagePath,
@@ -141,6 +143,46 @@ describe("generate-npm-shrinkwrap", () => {
]);
});
it("pins current shrinkwrap versions that are still in the pnpm lock", () => {
const lockfile = {
packages: {
"": {},
"node_modules/@aws-sdk/core": {
version: "3.974.13",
},
"node_modules/@aws-sdk/core/node_modules/fast-xml-parser": {
version: "5.2.5",
},
"node_modules/react": {
version: "19.2.4",
},
"node_modules/react-dom": {
version: "19.2.4",
},
"node_modules/react-dom/node_modules/react": {
version: "19.2.5",
},
"node_modules/zod": {
version: "4.4.4",
},
},
};
const pnpmPackages = new Set([
"@aws-sdk/core@3.974.13",
"fast-xml-parser@5.2.5",
"react@19.2.4",
"react@19.2.5",
"react-dom@19.2.4",
]);
expect(
collectCurrentShrinkwrapOverrides(lockfile, new Set(["@aws-sdk/core"]), pnpmPackages),
).toEqual({
"fast-xml-parser": "5.2.5",
"react-dom": "19.2.4",
});
});
it("normalizes npm patch-version metadata drift", () => {
expect(
normalizeNpmVersionDrift({
@@ -260,4 +302,22 @@ describe("generate-npm-shrinkwrap", () => {
expect(packageDirs).toContain("extensions/acpx");
expect(packageDirs.length).toBeGreaterThan(1);
});
it("detects package dependency inputs that make current shrinkwrap pins unsafe", () => {
expect(
packageDependencyInputsChanged(process.cwd(), ["scripts/generate-npm-shrinkwrap.mjs"]),
).toBe(true);
expect(packageDependencyInputsChanged(process.cwd(), ["pnpm-lock.yaml"])).toBe(true);
expect(packageDependencyInputsChanged(process.cwd(), ["package.json"])).toBe(true);
expect(
packageDependencyInputsChanged(path.join(process.cwd(), "extensions/acpx"), [
"extensions/acpx/npm-shrinkwrap.json",
]),
).toBe(true);
expect(
packageDependencyInputsChanged(path.join(process.cwd(), "extensions/acpx"), [
"extensions/brave/package.json",
]),
).toBe(false);
});
});