From 72d55fc12a47d9b181cffd68ecf3db17f7a08a02 Mon Sep 17 00:00:00 2001 From: "Jason (Json)" <263060202+fuller-stack-dev@users.noreply.github.com> Date: Tue, 11 Aug 2026 17:01:06 -0700 Subject: [PATCH] fix(scripts): rewrite transitive OCM workspace packs (#122214) --- scripts/ocm-npm-workspace-deps.mts | 60 ++++++++++++++++----- test/scripts/ocm-npm-workspace-deps.test.ts | 42 ++++++++++++++- 2 files changed, 87 insertions(+), 15 deletions(-) diff --git a/scripts/ocm-npm-workspace-deps.mts b/scripts/ocm-npm-workspace-deps.mts index 65ac913f49a0..3261822b121d 100755 --- a/scripts/ocm-npm-workspace-deps.mts +++ b/scripts/ocm-npm-workspace-deps.mts @@ -15,6 +15,7 @@ const supportedRuntimeBuildProfiles = new Set(["sourcePerformance"]); const fullGitCommitPattern = /^[0-9a-f]{40}$/iu; type WorkspacePackage = { name: string; version: string; tarball: string }; +type WorkspacePackageSource = Omit & { dir: string }; export function parseWorkspaceDependencyDirs( raw: string | undefined = process.env[WORKSPACE_DIRS_ENV], @@ -223,7 +224,7 @@ function packWorkspaceDependencies( workspaceDirs: string[], outputDir: string, ): WorkspacePackage[] { - return workspaceDirs.map((packageDir) => { + const sources: WorkspacePackageSource[] = workspaceDirs.map((packageDir) => { const packageJson = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8")); if (typeof packageJson.name !== "string" || packageJson.name.trim() === "") { throw new Error(`workspace dependency has no package name: ${packageDir}`); @@ -231,28 +232,47 @@ function packWorkspaceDependencies( if (typeof packageJson.version !== "string" || packageJson.version.trim() === "") { throw new Error(`workspace dependency has no package version: ${packageDir}`); } + return { + dir: packageDir, + name: packageJson.name, + version: packageJson.version, + }; + }); + const workspacePackages = sources.map(({ dir, name, version }) => { const before = new Set(readdirSync(outputDir)); - const result = runNpm(npm, ["pack", packageDir, "--pack-destination", outputDir, "--silent"], { + const result = runNpm(npm, ["pack", dir, "--pack-destination", outputDir, "--silent"], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"], }); if (result.status !== 0) { - throw new Error(`npm pack failed for ${packageJson.name} with status ${result.status ?? 1}`); + throw new Error(`npm pack failed for ${name} with status ${result.status ?? 1}`); } const tarballs = readdirSync(outputDir).filter( (entry) => entry.endsWith(".tgz") && !before.has(entry), ); if (tarballs.length !== 1) { throw new Error( - `expected npm pack to create one archive for ${packageJson.name}, found ${tarballs.length}`, + `expected npm pack to create one archive for ${name}, found ${tarballs.length}`, ); } return { - name: packageJson.name, - version: packageJson.version, + name, + version, tarball: join(outputDir, tarballs[0]!), }; }); + return workspacePackages.map((workspacePackage, index) => { + return { + name: workspacePackage.name, + version: workspacePackage.version, + tarball: patchPackageArchiveWorkspaceDependencies( + workspacePackage.tarball, + workspacePackages, + outputDir, + `workspace-${index}`, + ), + }; + }); } export function rewriteWorkspaceDependencyVersions( @@ -277,7 +297,7 @@ export function rewriteWorkspaceDependencyVersions( } const version = workspaceVersions.get(name); if (!version) { - throw new Error(`root archive references unconfigured workspace dependency: ${name}`); + throw new Error(`package archive references unconfigured workspace dependency: ${name}`); } Reflect.set(dependencies, name, version); rewritten += 1; @@ -286,28 +306,42 @@ export function rewriteWorkspaceDependencyVersions( return rewritten; } -function patchRootArchiveWorkspaceDependencies( - rootArchive: string, +function patchPackageArchiveWorkspaceDependencies( + archive: string, workspacePackages: WorkspacePackage[], outputDir: string, + outputStem: string, ): string { - const unpackDir = join(outputDir, "root-archive"); + const unpackDir = join(outputDir, `${outputStem}-archive`); mkdirSync(unpackDir); - runTar(["-xzf", rootArchive, "-C", unpackDir]); + runTar(["-xzf", archive, "-C", unpackDir]); const packageJsonPath = join(unpackDir, "package", "package.json"); const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")); const rewritten = rewriteWorkspaceDependencyVersions(packageJson, workspacePackages); if (rewritten === 0) { - return rootArchive; + return archive; } writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`); - const patchedArchive = join(outputDir, "openclaw-root-patched.tgz"); + const patchedArchive = join(outputDir, `${outputStem}-patched.tgz`); runTar(["-czf", patchedArchive, "-C", unpackDir, "package"]); return patchedArchive; } +function patchRootArchiveWorkspaceDependencies( + rootArchive: string, + workspacePackages: WorkspacePackage[], + outputDir: string, +): string { + return patchPackageArchiveWorkspaceDependencies( + rootArchive, + workspacePackages, + outputDir, + "openclaw-root", + ); +} + function main(): number { const args = process.argv.slice(2); const npm = process.env[REAL_NPM_ENV]?.trim() || "npm"; diff --git a/test/scripts/ocm-npm-workspace-deps.test.ts b/test/scripts/ocm-npm-workspace-deps.test.ts index 34e91972a0f1..871b62894914 100644 --- a/test/scripts/ocm-npm-workspace-deps.test.ts +++ b/test/scripts/ocm-npm-workspace-deps.test.ts @@ -270,16 +270,30 @@ describe("OCM npm workspace dependency adapter", () => { }); }); - it("installs a packed root with a local workspace dependency", () => { + it("rejects package archives with an unconfigured workspace dependency", () => { + const packageJson = { + dependencies: { + "@openclaw/normalization-core": "workspace:*", + }, + }; + + expect(() => rewriteWorkspaceDependencyVersions(packageJson, [])).toThrow( + "package archive references unconfigured workspace dependency: @openclaw/normalization-core", + ); + }); + + it("installs a packed root with transitive local workspace dependencies", () => { const root = mkdtempSync(join(tmpdir(), "openclaw-ocm-adapter-test-")); try { const archiveRoot = join(root, "archive"); const packagedRoot = join(archiveRoot, "package"); const workspaceDir = join(root, "ai"); + const transitiveWorkspaceDir = join(root, "normalization-core"); const installDir = join(root, "install"); const rootArchive = join(root, "openclaw.tgz"); mkdirSync(packagedRoot, { recursive: true }); mkdirSync(workspaceDir, { recursive: true }); + mkdirSync(transitiveWorkspaceDir, { recursive: true }); writeFileSync( join(packagedRoot, "package.json"), `${JSON.stringify({ @@ -294,9 +308,19 @@ describe("OCM npm workspace dependency adapter", () => { name: "@openclaw/ai", version: "1.0.0", main: "index.js", + dependencies: { "@openclaw/normalization-core": "workspace:*" }, })}\n`, ); writeFileSync(join(workspaceDir, "index.js"), "export const ready = true;\n"); + writeFileSync( + join(transitiveWorkspaceDir, "package.json"), + `${JSON.stringify({ + name: "@openclaw/normalization-core", + version: "1.0.0", + main: "index.js", + })}\n`, + ); + writeFileSync(join(transitiveWorkspaceDir, "index.js"), "export const normalized = true;\n"); execFileSync("tar", ["-czf", rootArchive, "-C", archiveRoot, "package"]); execFileSync( @@ -315,7 +339,9 @@ describe("OCM npm workspace dependency adapter", () => { env: { ...process.env, OPENCLAW_OCM_REAL_NPM_BIN: process.platform === "win32" ? "npm.cmd" : "npm", - OPENCLAW_OCM_WORKSPACE_DEPENDENCY_DIRS: workspaceDir, + OPENCLAW_OCM_WORKSPACE_DEPENDENCY_DIRS: [workspaceDir, transitiveWorkspaceDir].join( + delimiter, + ), npm_config_audit: "false", npm_config_cache: join(root, "npm-cache"), npm_config_fund: "false", @@ -332,6 +358,18 @@ describe("OCM npm workspace dependency adapter", () => { JSON.parse(readFileSync(join(installDir, "node_modules/@openclaw/ai/package.json"), "utf8")) .version, ).toBe("1.0.0"); + expect( + JSON.parse( + readFileSync( + join(installDir, "node_modules/@openclaw/normalization-core/package.json"), + "utf8", + ), + ).version, + ).toBe("1.0.0"); + expect( + JSON.parse(readFileSync(join(installDir, "node_modules/@openclaw/ai/package.json"), "utf8")) + .dependencies["@openclaw/normalization-core"], + ).toBe("1.0.0"); } finally { rmSync(root, { force: true, recursive: true }); }