diff --git a/Dockerfile b/Dockerfile index ee10f64f346..288e6ef9e58 100644 --- a/Dockerfile +++ b/Dockerfile @@ -138,7 +138,7 @@ ARG OPENCLAW_BUNDLED_PLUGIN_DIR # BuildKit cache mounts are not part of cached layers; seed tarballs for the # installed prod graph in the same step that runs offline prune. RUN --mount=type=cache,id=openclaw-pnpm-store,target=/root/.local/share/pnpm/store,sharing=locked \ - pnpm list --prod --depth Infinity --json | node scripts/list-prod-store-packages.mjs | xargs -r pnpm store add && \ + node scripts/list-prod-store-packages.mjs | xargs -r pnpm store add && \ CI=true pnpm prune --prod \ --config.offline=true \ --config.supportedArchitectures.os=linux \ diff --git a/scripts/list-prod-store-packages.mjs b/scripts/list-prod-store-packages.mjs index e1e49772ffc..4f5117e8463 100644 --- a/scripts/list-prod-store-packages.mjs +++ b/scripts/list-prod-store-packages.mjs @@ -3,8 +3,6 @@ import fs from "node:fs"; import path from "node:path"; import { parse } from "yaml"; -const parsed = JSON.parse(fs.readFileSync(0, "utf8")); -const roots = Array.isArray(parsed) ? parsed : [parsed]; const specs = new Set(); const target = { cpu: process.arch, @@ -24,6 +22,12 @@ function packageSpec(name, version) { ) { return undefined; } + if (normalizedVersion.startsWith("npm:")) { + return normalizedVersion.slice("npm:".length); + } + if (normalizedVersion.startsWith("@")) { + return normalizedVersion; + } return `${name}@${normalizedVersion}`; } @@ -85,6 +89,15 @@ function addSpec(lockfile, spec) { } } +function parseListRoots() { + const input = fs.readFileSync(0, "utf8").trim(); + if (!input) { + return []; + } + const parsed = JSON.parse(input); + return Array.isArray(parsed) ? parsed : [parsed]; +} + function visitListNode(lockfile, node) { for (const dep of Object.values(node.dependencies ?? {})) { const name = dep.from || dep.name; @@ -96,6 +109,16 @@ function visitListNode(lockfile, node) { } } +function addImporterRoots(lockfile) { + for (const importer of Object.values(lockfile?.importers ?? {})) { + for (const deps of [importer.dependencies, importer.optionalDependencies]) { + for (const [name, dep] of Object.entries(deps ?? {})) { + addSpec(lockfile, packageSpec(name, dep?.version)); + } + } + } +} + function readLockfile() { const lockfilePath = path.join(process.cwd(), "pnpm-lock.yaml"); if (!fs.existsSync(lockfilePath)) { @@ -145,9 +168,10 @@ function addSnapshotClosure(lockfile) { } const lockfile = readLockfile(); -for (const root of roots) { +for (const root of parseListRoots()) { visitListNode(lockfile, root); } +addImporterRoots(lockfile); addSnapshotClosure(lockfile); process.stdout.write([...specs].toSorted((a, b) => a.localeCompare(b)).join("\n")); diff --git a/src/dockerfile.test.ts b/src/dockerfile.test.ts index e5711a9d984..d27c8da82cc 100644 --- a/src/dockerfile.test.ts +++ b/src/dockerfile.test.ts @@ -101,7 +101,7 @@ describe("Dockerfile", () => { const dockerfile = await readFile(dockerfilePath, "utf8"); const installIndex = dockerfile.indexOf("pnpm install --frozen-lockfile \\"); const storeSeedIndex = dockerfile.indexOf( - "pnpm list --prod --depth Infinity --json | node scripts/list-prod-store-packages.mjs | xargs -r pnpm store add", + "node scripts/list-prod-store-packages.mjs | xargs -r pnpm store add", ); const pruneIndex = dockerfile.indexOf("CI=true pnpm prune --prod \\"); diff --git a/test/scripts/list-prod-store-packages.test.ts b/test/scripts/list-prod-store-packages.test.ts index 63db5d6d93a..caa55ff9348 100644 --- a/test/scripts/list-prod-store-packages.test.ts +++ b/test/scripts/list-prod-store-packages.test.ts @@ -16,6 +16,14 @@ function runListProdStorePackages(input: unknown, cwd = process.cwd()) { }); } +function runListProdStorePackagesRaw(input: string, cwd = process.cwd()) { + return spawnSync(process.execPath, [scriptPath], { + cwd, + encoding: "utf8", + input, + }); +} + describe("list-prod-store-packages", () => { afterEach(() => { cleanupTempDirs(tempDirs); @@ -101,6 +109,64 @@ describe("list-prod-store-packages", () => { expect(result.stdout).toBe("source-map-support@0.5.21\nsource-map@0.6.1"); }); + it("adds production importer dependency closures without pnpm list input", () => { + const cwd = makeTempRepoRoot(tempDirs, "openclaw-prod-store-packages-"); + writeFileSync( + join(cwd, "pnpm-lock.yaml"), + [ + "lockfileVersion: '10.0'", + "", + "importers:", + " .:", + " dependencies:", + " '@homebridge/ciao':", + " specifier: 1.3.9", + " version: 1.3.9", + " fetch-blob:", + " specifier: 3.2.0", + " version: 3.2.0", + "", + "packages:", + " '@homebridge/ciao@1.3.9':", + " resolution: {integrity: sha512-test}", + " source-map-support@0.5.21:", + " resolution: {integrity: sha512-test}", + " source-map@0.6.1:", + " resolution: {integrity: sha512-test}", + " fetch-blob@3.2.0:", + " resolution: {integrity: sha512-test}", + " '@nolyfill/domexception@1.0.28':", + " resolution: {integrity: sha512-test}", + "", + "snapshots:", + " '@homebridge/ciao@1.3.9':", + " dependencies:", + " source-map-support: 0.5.21", + " source-map-support@0.5.21:", + " dependencies:", + " source-map: 0.6.1", + " source-map@0.6.1: {}", + " fetch-blob@3.2.0:", + " dependencies:", + " node-domexception: '@nolyfill/domexception@1.0.28'", + " '@nolyfill/domexception@1.0.28': {}", + "", + ].join("\n"), + ); + const result = runListProdStorePackagesRaw("", cwd); + + expect(result.status).toBe(0); + expect(result.stdout.split("\n")).toEqual( + [ + "@homebridge/ciao@1.3.9", + "fetch-blob@3.2.0", + "@nolyfill/domexception@1.0.28", + "source-map-support@0.5.21", + "source-map@0.6.1", + ].toSorted((a, b) => a.localeCompare(b)), + ); + }); + it("adds target optional dependencies from peer-resolved lockfile snapshots", () => { const cwd = makeTempRepoRoot(tempDirs, "openclaw-prod-store-packages-"); const platformPackages = [