From 112924b11394991bba8db447dcd91a9ac478c7e7 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Mon, 4 May 2026 21:24:00 +0100 Subject: [PATCH] fix(update): keep plugin install runtime aliases stable --- scripts/runtime-postbuild.mjs | 173 ++++++++++++++++++++++++- test/scripts/runtime-postbuild.test.ts | 112 ++++++++++++++++ 2 files changed, 278 insertions(+), 7 deletions(-) diff --git a/scripts/runtime-postbuild.mjs b/scripts/runtime-postbuild.mjs index 0d1d1e40437..fae1cf9b920 100644 --- a/scripts/runtime-postbuild.mjs +++ b/scripts/runtime-postbuild.mjs @@ -19,6 +19,7 @@ const ROOT_RUNTIME_ALIAS_PATTERN = /^(?.+\.(?:runtime|contract))-[A-Za-z0- 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 escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/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 @@ -45,6 +46,77 @@ const LEGACY_ROOT_RUNTIME_COMPAT_ALIASES = [ ["provider-dispatcher-BpL2E92x.js", "provider-dispatcher.runtime.js"], ["provider-dispatcher-JG96SkLX.js", "provider-dispatcher.runtime.js"], ]; +const LEGACY_PLUGIN_INSTALL_RUNTIME_MARKERS = [ + "scanPackageInstallSource", + "scanFileInstallSource", + "scanInstalledPackageDependencyTree", + "scanBundleInstallSource", +]; +const PLUGIN_INSTALL_RUNTIME_ALIAS = { + aliasFileName: "install.runtime.js", + sourceIncludes: LEGACY_PLUGIN_INSTALL_RUNTIME_MARKERS, +}; +const LEGACY_PLUGIN_INSTALL_RUNTIME_COMPAT_ALIASES = [ + // Published releases from v2026.3.22 onward. Older updaters could + // overlay package dist instead of swapping it, leaving old install chunks + // that still import these hashed plugin install runtime files. + "install.runtime-D7SL02B2.js", + "install.runtime-Deq6Beal.js", + "install.runtime-Eoq8y3HE.js", + "install.runtime-DDmlaKdG.js", + "install.runtime-ADTafpVD.js", + "install.runtime-v8X-j3Tm.js", + "install.runtime-BLcZ-44g.js", + "install.runtime-vS4aFJvO.js", + "install.runtime-Dm_c092A.js", + "install.runtime-D_7OUvuY.js", + "install.runtime-BLEE0OIk.js", + "install.runtime-3LpjZbr8.js", + "install.runtime-BrsB9OnV.js", + "install.runtime-BEOb-kNW.js", + "install.runtime-Cx_xphd1.js", + "install.runtime-B-MtEMSR.js", + "install.runtime-C-Y4HAqX.js", + "install.runtime-j1SedTZh.js", + "install.runtime-4zsL_8wt.js", + "install.runtime-BhCKlLSJ.js", + "install.runtime-tGJ0KhMF.js", + "install.runtime-DtmATpak.js", + "install.runtime-BzZ38ePb.js", + "install.runtime-DwQr7nEE.js", + "install.runtime-CEIURnUz.js", + "install.runtime-D3EPlM0r.js", + "install.runtime-DIlN5H3O.js", + "install.runtime-DjcOwVH_.js", + "install.runtime-B13jZink.js", + "install.runtime-O8MXNrwm.js", + "install.runtime-Bkf_VMnk.js", + "install.runtime-QOfEzAcZ.js", + "install.runtime-BRVACueI.js", + "install.runtime-DX8jy7tN.js", + "install.runtime-BdfsTamp.js", + "install.runtime-B6OA2_P8.js", + "install.runtime-D9cTH-C0.js", + "install.runtime-OCJULXQo.js", + "install.runtime-9ZXBhZSk.js", + "install.runtime-DlL3C3t_.js", + "install.runtime-TU-jP-TN.js", + "install.runtime-a2FlfOSp.js", + "install.runtime-BwuRABU1.js", + "install.runtime-B3mZL_R2.js", + "install.runtime-CWUzypNQ.js", + "install.runtime-D6FSd9v2.js", + "install.runtime-DQ-ui3nL.js", + "install.runtime-CNHwKOIb.js", + "install.runtime-Dzuj9tSw.js", + "install.runtime-BuF-YAfQ.js", + "install.runtime-Xom5hOHq.js", + "install.runtime-tnhNR9WW.js", +].map((legacyFileName) => ({ + legacyFileName, + aliasFileName: PLUGIN_INSTALL_RUNTIME_ALIAS.aliasFileName, + sourceIncludes: LEGACY_PLUGIN_INSTALL_RUNTIME_MARKERS, +})); const LEGACY_CLI_EXIT_COMPAT_CHUNKS = [ { dest: "dist/memory-state-CcqRgDZU.js", @@ -82,10 +154,18 @@ export function writeStableRootRuntimeAliases(params = {}) { candidatesByAlias.set(aliasFileName, candidates); } - const resolveAliasCandidate = (candidates) => { + const resolveAliasCandidate = (aliasFileName, candidates) => { if (candidates.length === 1) { return candidates[0]; } + if (aliasFileName === PLUGIN_INSTALL_RUNTIME_ALIAS.aliasFileName) { + return resolveRootRuntimeCandidateByMarkers({ + distDir, + fsImpl, + aliasFileName, + sourceIncludes: PLUGIN_INSTALL_RUNTIME_ALIAS.sourceIncludes, + }); + } const candidateSet = new Set(candidates); const wrappers = candidates.filter((candidate) => { const filePath = path.join(distDir, candidate); @@ -108,7 +188,7 @@ export function writeStableRootRuntimeAliases(params = {}) { for (const [aliasFileName, candidates] of candidatesByAlias) { const aliasPath = path.join(distDir, aliasFileName); - const candidate = resolveAliasCandidate(candidates); + const candidate = resolveAliasCandidate(aliasFileName, candidates); if (!candidate) { fsImpl.rmSync?.(aliasPath, { force: true }); continue; @@ -143,10 +223,21 @@ export function rewriteRootRuntimeImportsToStableAliases(params = {}) { } const runtimeAliasFiles = new Map(); for (const [aliasFileName, candidates] of candidatesByAlias) { - if (candidates.length !== 1) { + if (candidates.length === 1) { + runtimeAliasFiles.set(candidates[0], aliasFileName); continue; } - runtimeAliasFiles.set(candidates[0], aliasFileName); + if (aliasFileName === PLUGIN_INSTALL_RUNTIME_ALIAS.aliasFileName) { + const candidate = resolveRootRuntimeCandidateByMarkers({ + distDir, + fsImpl, + aliasFileName, + sourceIncludes: PLUGIN_INSTALL_RUNTIME_ALIAS.sourceIncludes, + }); + if (candidate) { + runtimeAliasFiles.set(candidate, aliasFileName); + } + } } if (runtimeAliasFiles.size === 0) { return; @@ -179,19 +270,87 @@ export function rewriteRootRuntimeImportsToStableAliases(params = {}) { } } +function resolveRootRuntimeCandidateByMarkers(params) { + if (!params.sourceIncludes?.length) { + return null; + } + const match = params.aliasFileName.match(ROOT_STABLE_RUNTIME_ALIAS_PATTERN); + if (!match) { + return null; + } + const aliasBaseFileName = params.aliasFileName.replace(/\.js$/u, ""); + const hashedPattern = new RegExp(`^${escapeRegExp(aliasBaseFileName)}-[A-Za-z0-9_-]+\\.js$`, "u"); + let entries = []; + try { + entries = params.fsImpl.readdirSync(params.distDir, { withFileTypes: true }); + } catch { + return null; + } + const candidates = []; + for (const entry of entries.toSorted((left, right) => left.name.localeCompare(right.name))) { + if (!entry.isFile() || !hashedPattern.test(entry.name)) { + continue; + } + const candidatePath = path.join(params.distDir, entry.name); + let source; + try { + source = params.fsImpl.readFileSync(candidatePath, "utf8"); + } catch { + continue; + } + if (params.sourceIncludes.every((marker) => source.includes(marker))) { + candidates.push(entry.name); + } + } + return candidates.length === 1 ? candidates[0] : null; +} + +function resolveLegacyRootRuntimeCompatTarget(params) { + if ( + params.aliasFileName && + params.fsImpl.existsSync(path.join(params.distDir, params.aliasFileName)) + ) { + return params.aliasFileName; + } + const match = params.legacyFileName.match(ROOT_RUNTIME_ALIAS_PATTERN); + if (!match?.groups?.base) { + return null; + } + return resolveRootRuntimeCandidateByMarkers({ + distDir: params.distDir, + fsImpl: params.fsImpl, + aliasFileName: `${match.groups.base}.js`, + sourceIncludes: params.sourceIncludes, + }); +} + 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) { + for (const entry of [ + ...LEGACY_ROOT_RUNTIME_COMPAT_ALIASES.map(([legacyFileName, aliasFileName]) => ({ + legacyFileName, + aliasFileName, + })), + ...LEGACY_PLUGIN_INSTALL_RUNTIME_COMPAT_ALIASES, + ]) { + const { legacyFileName } = entry; const legacyPath = path.join(distDir, legacyFileName); if (fsImpl.existsSync(legacyPath)) { continue; } - if (!fsImpl.existsSync(path.join(distDir, aliasFileName))) { + const targetFileName = resolveLegacyRootRuntimeCompatTarget({ + distDir, + fsImpl, + legacyFileName, + aliasFileName: entry.aliasFileName, + sourceIncludes: entry.sourceIncludes, + }); + if (!targetFileName) { continue; } - writeTextFileIfChanged(legacyPath, `export * from "./${aliasFileName}";\n`); + writeTextFileIfChanged(legacyPath, `export * from "./${targetFileName}";\n`); } } diff --git a/test/scripts/runtime-postbuild.test.ts b/test/scripts/runtime-postbuild.test.ts index 707b73e3de7..2d09d7527ed 100644 --- a/test/scripts/runtime-postbuild.test.ts +++ b/test/scripts/runtime-postbuild.test.ts @@ -145,6 +145,34 @@ describe("runtime postbuild static assets", () => { await expect(fs.stat(path.join(distDir, "install.runtime.js"))).rejects.toThrow(); }); + it("writes a stable plugin install runtime alias when install runtimes 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 scanPackageInstallSource = true;", + "export const scanFileInstallSource = true;", + "export const scanInstalledPackageDependencyTree = true;", + "export const scanBundleInstallSource = true;", + "", + ].join("\n"), + "utf8", + ); + await fs.writeFile( + path.join(distDir, "install.runtime-Bbb222.js"), + "export const daemonInstall = true;\n", + "utf8", + ); + + writeStableRootRuntimeAliases({ rootDir }); + + expect(await fs.readFile(path.join(distDir, "install.runtime.js"), "utf8")).toBe( + 'export * from "./install.runtime-Aaa111.js";\n', + ); + }); + it("keeps stable aliases when one colliding root runtime chunk re-exports the implementation", async () => { const rootDir = createTempDir("openclaw-runtime-postbuild-"); const distDir = path.join(rootDir, "dist"); @@ -294,6 +322,47 @@ describe("runtime postbuild static assets", () => { ); }); + it("rewrites plugin install runtime imports to stable aliases when install runtimes 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 scanPackageInstallSource = true;", + "export const scanFileInstallSource = true;", + "export const scanInstalledPackageDependencyTree = true;", + "export const scanBundleInstallSource = true;", + "", + ].join("\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.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"); @@ -330,6 +399,22 @@ describe("runtime postbuild static assets", () => { 'export * from "./provider-dispatcher.runtime-NewHash.js";\n', "utf8", ); + await fs.writeFile( + path.join(distDir, "install.runtime-NewPluginHash.js"), + [ + "export const scanPackageInstallSource = true;", + "export const scanFileInstallSource = true;", + "export const scanInstalledPackageDependencyTree = true;", + "export const scanBundleInstallSource = true;", + "", + ].join("\n"), + "utf8", + ); + await fs.writeFile( + path.join(distDir, "install.runtime-OtherHash.js"), + "export const installFromValidatedNpmSpecArchive = true;\n", + "utf8", + ); writeLegacyRootRuntimeCompatAliases({ rootDir }); @@ -342,6 +427,33 @@ describe("runtime postbuild static assets", () => { expect(await fs.readFile(path.join(distDir, "provider-dispatcher-6EQEtc-t.js"), "utf8")).toBe( 'export * from "./provider-dispatcher.runtime.js";\n', ); + expect(await fs.readFile(path.join(distDir, "install.runtime-D7SL02B2.js"), "utf8")).toBe( + 'export * from "./install.runtime-NewPluginHash.js";\n', + ); + expect(await fs.readFile(path.join(distDir, "install.runtime-Deq6Beal.js"), "utf8")).toBe( + 'export * from "./install.runtime-NewPluginHash.js";\n', + ); + expect(await fs.readFile(path.join(distDir, "install.runtime-BRVACueI.js"), "utf8")).toBe( + 'export * from "./install.runtime-NewPluginHash.js";\n', + ); + expect(await fs.readFile(path.join(distDir, "install.runtime-DX8jy7tN.js"), "utf8")).toBe( + 'export * from "./install.runtime-NewPluginHash.js";\n', + ); + expect(await fs.readFile(path.join(distDir, "install.runtime-D6FSd9v2.js"), "utf8")).toBe( + 'export * from "./install.runtime-NewPluginHash.js";\n', + ); + expect(await fs.readFile(path.join(distDir, "install.runtime-DQ-ui3nL.js"), "utf8")).toBe( + 'export * from "./install.runtime-NewPluginHash.js";\n', + ); + expect(await fs.readFile(path.join(distDir, "install.runtime-Xom5hOHq.js"), "utf8")).toBe( + 'export * from "./install.runtime-NewPluginHash.js";\n', + ); + expect(await fs.readFile(path.join(distDir, "install.runtime-tnhNR9WW.js"), "utf8")).toBe( + 'export * from "./install.runtime-NewPluginHash.js";\n', + ); + expect(await fs.readFile(path.join(distDir, "install.runtime-CNHwKOIb.js"), "utf8")).toBe( + 'export * from "./install.runtime-NewPluginHash.js";\n', + ); }); it("writes compatibility aliases for previous gateway shutdown chunk names", async () => {