From 82dd68d2995088bd30b09dbe7d425f99fd6c4689 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 (cherry picked from commit c96e62d5abc3dca0e74e7334409d480d76a28439) --- scripts/runtime-postbuild.mjs | 119 +++++++++++++++++++++- test/scripts/runtime-postbuild.test.ts | 134 +++++++++++++++++++++++++ 2 files changed, 250 insertions(+), 3 deletions(-) diff --git a/scripts/runtime-postbuild.mjs b/scripts/runtime-postbuild.mjs index 548c70e889e..6e10973579e 100644 --- a/scripts/runtime-postbuild.mjs +++ b/scripts/runtime-postbuild.mjs @@ -16,6 +16,27 @@ export { copyStaticExtensionAssets, listStaticExtensionAssetOutputs }; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const ROOT_RUNTIME_ALIAS_PATTERN = /^(?.+\.(?:runtime|contract))-[A-Za-z0-9_-]+\.js$/u; +const ROOT_STABLE_RUNTIME_ALIAS_PATTERN = /^.+\.(?:runtime|contract)\.js$/u; +const ROOT_RUNTIME_IMPORT_SPECIFIER_PATTERN = + /(["'])\.\/([^"']+\.(?:runtime|contract)-[A-Za-z0-9_-]+\.js)\1/gu; +const LEGACY_ROOT_RUNTIME_COMPAT_ALIASES = [ + // v2026.4.29 dispatch lazy chunks. Package updates used to replace the + // dist tree before the live gateway had restarted, so an already-loaded old + // dispatch chunk could still resolve these names after the swap. + ["abort.runtime-DX6vo4yJ.js", "abort.runtime.js"], + ["get-reply-from-config.runtime-uABrvCZ-.js", "get-reply-from-config.runtime.js"], + ["reply-media-paths.runtime-C5UnVaLF.js", "reply-media-paths.runtime.js"], + ["route-reply.runtime-D4PGzijU.js", "route-reply.runtime.js"], + ["runtime-plugins.runtime-fLHuT7Vs.js", "runtime-plugins.runtime.js"], + ["tts.runtime-66taD50M.js", "tts.runtime.js"], + // v2026.5.2-beta.1 dispatch lazy chunks. + ["abort.runtime-CKviLU0L.js", "abort.runtime.js"], + ["get-reply-from-config.runtime-BzFAggVK.js", "get-reply-from-config.runtime.js"], + ["reply-media-paths.runtime-ZpULeITb.js", "reply-media-paths.runtime.js"], + ["route-reply.runtime-uzaOjbd1.js", "route-reply.runtime.js"], + ["runtime-plugins.runtime-CNAfmQRG.js", "runtime-plugins.runtime.js"], + ["tts.runtime-D-THXDsp.js", "tts.runtime.js"], +]; const LEGACY_CLI_EXIT_COMPAT_CHUNKS = [ { dest: "dist/memory-state-CcqRgDZU.js", @@ -38,7 +59,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; } @@ -46,8 +68,97 @@ 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`); + } +} + +export function rewriteRootRuntimeImportsToStableAliases(params = {}) { + const rootDir = params.rootDir ?? ROOT; + const distDir = path.join(rootDir, "dist"); + const fsImpl = params.fs ?? fs; + let entries = []; + try { + entries = fsImpl.readdirSync(distDir, { withFileTypes: true }); + } catch { + return; + } + + 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) { + 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; + } + + for (const entry of entries) { + if (!entry.isFile() || !entry.name.endsWith(".js")) { + continue; + } + if (ROOT_STABLE_RUNTIME_ALIAS_PATTERN.test(entry.name)) { + continue; + } + const filePath = path.join(distDir, entry.name); + let source; + try { + source = fsImpl.readFileSync(filePath, "utf8"); + } catch { + continue; + } + const rewritten = source.replace( + ROOT_RUNTIME_IMPORT_SPECIFIER_PATTERN, + (specifier, quote, fileName) => { + const aliasFileName = runtimeAliasFiles.get(fileName); + return aliasFileName ? `${quote}./${aliasFileName}${quote}` : specifier; + }, + ); + if (rewritten !== source) { + writeTextFileIfChanged(filePath, rewritten); + } + } +} + +export function writeLegacyRootRuntimeCompatAliases(params = {}) { + const rootDir = params.rootDir ?? ROOT; + const distDir = path.join(rootDir, "dist"); + const fsImpl = params.fs ?? fs; + for (const [legacyFileName, aliasFileName] of LEGACY_ROOT_RUNTIME_COMPAT_ALIASES) { + const legacyPath = path.join(distDir, legacyFileName); + if (fsImpl.existsSync(legacyPath)) { + continue; + } + if (!fsImpl.existsSync(path.join(distDir, aliasFileName))) { + continue; + } + writeTextFileIfChanged(legacyPath, `export * from "./${aliasFileName}";\n`); } } @@ -76,7 +187,9 @@ export function runRuntimePostBuild(params = {}) { runPhase("bundled plugin metadata", () => copyBundledPluginMetadata(params)); runPhase("official channel catalog", () => writeOfficialChannelCatalog(params)); runPhase("bundled plugin runtime overlay", () => stageBundledPluginRuntime(params)); + runPhase("stable root runtime imports", () => rewriteRootRuntimeImportsToStableAliases(params)); runPhase("stable root runtime aliases", () => writeStableRootRuntimeAliases(params)); + runPhase("legacy root runtime compat aliases", () => writeLegacyRootRuntimeCompatAliases(params)); runPhase("legacy CLI exit compat chunks", () => writeLegacyCliExitCompatChunks(params)); runPhase("static extension assets", () => copyStaticExtensionAssets({ diff --git a/test/scripts/runtime-postbuild.test.ts b/test/scripts/runtime-postbuild.test.ts index 61d97a5b1a2..cf29584c37f 100644 --- a/test/scripts/runtime-postbuild.test.ts +++ b/test/scripts/runtime-postbuild.test.ts @@ -5,7 +5,9 @@ import { discoverStaticExtensionAssets } from "../../scripts/lib/static-extensio import { copyStaticExtensionAssets, listStaticExtensionAssetOutputs, + rewriteRootRuntimeImportsToStableAliases, writeLegacyCliExitCompatChunks, + writeLegacyRootRuntimeCompatAliases, writeStableRootRuntimeAliases, } from "../../scripts/runtime-postbuild.mjs"; import { createScriptTestHarness } from "./test-helpers.js"; @@ -118,6 +120,138 @@ 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"); + await fs.mkdir(distDir, { recursive: true }); + await fs.writeFile( + path.join(distDir, "runtime-plugins.runtime-AbCd1234.js"), + "export const ready = true;\n", + "utf8", + ); + await fs.writeFile( + path.join(distDir, "dispatch-OldHash.js"), + [ + 'const lazy = () => import("./runtime-plugins.runtime-AbCd1234.js");', + 'import "./missing.runtime-Nope.js";', + "", + ].join("\n"), + "utf8", + ); + + rewriteRootRuntimeImportsToStableAliases({ rootDir }); + + expect(await fs.readFile(path.join(distDir, "dispatch-OldHash.js"), "utf8")).toBe( + [ + 'const lazy = () => import("./runtime-plugins.runtime.js");', + 'import "./missing.runtime-Nope.js";', + "", + ].join("\n"), + ); + }); + + 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"); + await fs.mkdir(distDir, { recursive: true }); + await fs.writeFile( + path.join(distDir, "runtime-plugins.runtime-AbCd1234.js"), + "export const ready = true;\n", + "utf8", + ); + await fs.writeFile( + path.join(distDir, "runtime-plugins.runtime.js"), + 'export * from "./runtime-plugins.runtime-AbCd1234.js";\n', + "utf8", + ); + + rewriteRootRuntimeImportsToStableAliases({ rootDir }); + + expect(await fs.readFile(path.join(distDir, "runtime-plugins.runtime.js"), "utf8")).toBe( + 'export * from "./runtime-plugins.runtime-AbCd1234.js";\n', + ); + }); + + it("writes compatibility aliases for previous release runtime chunk names", 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, "runtime-plugins.runtime.js"), + 'export * from "./runtime-plugins.runtime-NewHash.js";\n', + "utf8", + ); + + writeLegacyRootRuntimeCompatAliases({ rootDir }); + + expect( + await fs.readFile(path.join(distDir, "runtime-plugins.runtime-fLHuT7Vs.js"), "utf8"), + ).toBe('export * from "./runtime-plugins.runtime.js";\n'); + expect( + await fs.readFile(path.join(distDir, "runtime-plugins.runtime-CNAfmQRG.js"), "utf8"), + ).toBe('export * from "./runtime-plugins.runtime.js";\n'); + }); + it("writes legacy CLI exit compatibility chunks", async () => { const rootDir = createTempDir("openclaw-runtime-postbuild-");