From c96e62d5abc3dca0e74e7334409d480d76a28439 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 2 May 2026 21:26:27 +0100 Subject: [PATCH] build: avoid ambiguous runtime aliases --- scripts/runtime-postbuild.mjs | 34 ++++++++++++--- test/scripts/runtime-postbuild.test.ts | 60 ++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 6 deletions(-) diff --git a/scripts/runtime-postbuild.mjs b/scripts/runtime-postbuild.mjs index c865ed7ca1e..ec7a723a4a4 100644 --- a/scripts/runtime-postbuild.mjs +++ b/scripts/runtime-postbuild.mjs @@ -107,7 +107,8 @@ export function writeStableRootRuntimeAliases(params = {}) { return; } - for (const entry of entries) { + const candidatesByAlias = new Map(); + for (const entry of entries.toSorted((left, right) => left.name.localeCompare(right.name))) { if (!entry.isFile()) { continue; } @@ -115,8 +116,19 @@ export function writeStableRootRuntimeAliases(params = {}) { if (!match?.groups?.base) { continue; } - const aliasPath = path.join(distDir, `${match.groups.base}.js`); - writeTextFileIfChanged(aliasPath, `export * from "./${entry.name}";\n`); + const aliasFileName = `${match.groups.base}.js`; + const candidates = candidatesByAlias.get(aliasFileName) ?? []; + candidates.push(entry.name); + candidatesByAlias.set(aliasFileName, candidates); + } + + for (const [aliasFileName, candidates] of candidatesByAlias) { + const aliasPath = path.join(distDir, aliasFileName); + if (candidates.length !== 1) { + fsImpl.rmSync?.(aliasPath, { force: true }); + continue; + } + writeTextFileIfChanged(aliasPath, `export * from "./${candidates[0]}";\n`); } } @@ -131,16 +143,26 @@ export function rewriteRootRuntimeImportsToStableAliases(params = {}) { return; } - const runtimeAliasFiles = new Map(); - for (const entry of entries) { + const candidatesByAlias = new Map(); + for (const entry of entries.toSorted((left, right) => left.name.localeCompare(right.name))) { if (!entry.isFile()) { continue; } const match = entry.name.match(ROOT_RUNTIME_ALIAS_PATTERN); if (match?.groups?.base) { - runtimeAliasFiles.set(entry.name, `${match.groups.base}.js`); + const aliasFileName = `${match.groups.base}.js`; + const candidates = candidatesByAlias.get(aliasFileName) ?? []; + candidates.push(entry.name); + candidatesByAlias.set(aliasFileName, candidates); } } + const runtimeAliasFiles = new Map(); + for (const [aliasFileName, candidates] of candidatesByAlias) { + if (candidates.length !== 1) { + continue; + } + runtimeAliasFiles.set(candidates[0], aliasFileName); + } if (runtimeAliasFiles.size === 0) { return; } diff --git a/test/scripts/runtime-postbuild.test.ts b/test/scripts/runtime-postbuild.test.ts index 9cd58bdf05c..f2c05df07a3 100644 --- a/test/scripts/runtime-postbuild.test.ts +++ b/test/scripts/runtime-postbuild.test.ts @@ -88,6 +88,31 @@ describe("runtime postbuild static assets", () => { await expect(fs.stat(path.join(distDir, "library.js"))).rejects.toThrow(); }); + it("does not write ambiguous stable aliases for colliding root runtime chunks", async () => { + const rootDir = createTempDir("openclaw-runtime-postbuild-"); + const distDir = path.join(rootDir, "dist"); + await fs.mkdir(distDir, { recursive: true }); + await fs.writeFile( + path.join(distDir, "install.runtime-Aaa111.js"), + "export const pluginInstall = true;\n", + "utf8", + ); + await fs.writeFile( + path.join(distDir, "install.runtime-Bbb222.js"), + "export const daemonInstall = true;\n", + "utf8", + ); + await fs.writeFile( + path.join(distDir, "install.runtime.js"), + 'export * from "./install.runtime-Stale.js";\n', + "utf8", + ); + + writeStableRootRuntimeAliases({ rootDir }); + + await expect(fs.stat(path.join(distDir, "install.runtime.js"))).rejects.toThrow(); + }); + it("rewrites root runtime imports to stable aliases", async () => { const rootDir = createTempDir("openclaw-runtime-postbuild-"); const distDir = path.join(rootDir, "dist"); @@ -118,6 +143,41 @@ describe("runtime postbuild static assets", () => { ); }); + it("keeps hashed imports when a stable runtime alias would collide", async () => { + const rootDir = createTempDir("openclaw-runtime-postbuild-"); + const distDir = path.join(rootDir, "dist"); + await fs.mkdir(distDir, { recursive: true }); + await fs.writeFile( + path.join(distDir, "install.runtime-Aaa111.js"), + "export const pluginInstall = true;\n", + "utf8", + ); + await fs.writeFile( + path.join(distDir, "install.runtime-Bbb222.js"), + "export const daemonInstall = true;\n", + "utf8", + ); + await fs.writeFile( + path.join(distDir, "install-OldHash.js"), + [ + 'const pluginRuntime = () => import("./install.runtime-Aaa111.js");', + 'const daemonRuntime = () => import("./install.runtime-Bbb222.js");', + "", + ].join("\n"), + "utf8", + ); + + rewriteRootRuntimeImportsToStableAliases({ rootDir }); + + expect(await fs.readFile(path.join(distDir, "install-OldHash.js"), "utf8")).toBe( + [ + 'const pluginRuntime = () => import("./install.runtime-Aaa111.js");', + 'const daemonRuntime = () => import("./install.runtime-Bbb222.js");', + "", + ].join("\n"), + ); + }); + it("leaves stable alias files pointing at their hashed runtime chunks", async () => { const rootDir = createTempDir("openclaw-runtime-postbuild-"); const distDir = path.join(rootDir, "dist");