diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f08e1e121df9..3fc10614fbd6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a7e805f45051..e8e310c53f2f 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -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 diff --git a/scripts/generate-npm-shrinkwrap.mjs b/scripts/generate-npm-shrinkwrap.mjs index 8cb036802146..8cd59a3e8841 100644 --- a/scripts/generate-npm-shrinkwrap.mjs +++ b/scripts/generate-npm-shrinkwrap.mjs @@ -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, diff --git a/test/scripts/generate-npm-shrinkwrap.test.ts b/test/scripts/generate-npm-shrinkwrap.test.ts index faf559dec4e9..1d9e502c1af1 100644 --- a/test/scripts/generate-npm-shrinkwrap.test.ts +++ b/test/scripts/generate-npm-shrinkwrap.test.ts @@ -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); + }); });