diff --git a/scripts/tsdown-build.mjs b/scripts/tsdown-build.mjs index 4767ade3bff..0e11c439e44 100644 --- a/scripts/tsdown-build.mjs +++ b/scripts/tsdown-build.mjs @@ -16,6 +16,7 @@ const extraArgs = process.argv.slice(2); const INEFFECTIVE_DYNAMIC_IMPORT_RE = /\[INEFFECTIVE_DYNAMIC_IMPORT\]/; const UNRESOLVED_IMPORT_RE = /\[UNRESOLVED_IMPORT\]/; const ANSI_ESCAPE_RE = new RegExp(String.raw`\u001B\[[0-9;]*m`, "g"); +const HASHED_ROOT_JS_RE = /^(?.+)-[A-Za-z0-9_-]+\.js$/u; function removeDistPluginNodeModulesSymlinks(rootDir) { const extensionsDir = path.join(rootDir, "extensions"); @@ -47,6 +48,34 @@ function pruneStaleRuntimeSymlinks() { removeDistPluginNodeModulesSymlinks(path.join(cwd, "dist-runtime")); } +export function pruneStaleRootChunkFiles(params = {}) { + const cwd = params.cwd ?? process.cwd(); + const fsImpl = params.fs ?? fs; + const roots = [path.join(cwd, "dist"), path.join(cwd, "dist-runtime")]; + for (const root of roots) { + let entries = []; + try { + entries = fsImpl.readdirSync(root, { withFileTypes: true }); + } catch { + continue; + } + + for (const entry of entries) { + if (!entry.isFile()) { + continue; + } + if (!HASHED_ROOT_JS_RE.test(entry.name)) { + continue; + } + try { + fsImpl.rmSync(path.join(root, entry.name), { force: true }); + } catch { + // Best-effort cleanup. The subsequent build will overwrite any stragglers. + } + } + } +} + export function pruneSourceCheckoutBundledPluginNodeModules(params = {}) { const cwd = params.cwd ?? process.cwd(); const logger = params.logger ?? console; @@ -116,6 +145,7 @@ function isMainModule() { if (isMainModule()) { pruneSourceCheckoutBundledPluginNodeModules(); pruneStaleRuntimeSymlinks(); + pruneStaleRootChunkFiles(); const invocation = resolveTsdownBuildInvocation(); const result = spawnSync(invocation.command, invocation.args, invocation.options); diff --git a/test/scripts/tsdown-build.test.ts b/test/scripts/tsdown-build.test.ts index 430230588a5..fe0f0ab5e6d 100644 --- a/test/scripts/tsdown-build.test.ts +++ b/test/scripts/tsdown-build.test.ts @@ -1,9 +1,15 @@ import fs from "node:fs"; +import fsPromises from "node:fs/promises"; +import path from "node:path"; import { describe, expect, it, vi } from "vitest"; import { pruneSourceCheckoutBundledPluginNodeModules, + pruneStaleRootChunkFiles, resolveTsdownBuildInvocation, } from "../../scripts/tsdown-build.mjs"; +import { createScriptTestHarness } from "./test-helpers.js"; + +const { createTempDir } = createScriptTestHarness(); describe("resolveTsdownBuildInvocation", () => { it("routes Windows tsdown builds through the pnpm runner instead of shell=true", () => { @@ -56,4 +62,44 @@ describe("resolveTsdownBuildInvocation", () => { warn.mockRestore(); rmSync.mockRestore(); }); + + it("prunes stale hashed root chunk files but keeps stable aliases and nested assets", async () => { + const rootDir = createTempDir("openclaw-tsdown-build-"); + const distDir = path.join(rootDir, "dist"); + const distRuntimeDir = path.join(rootDir, "dist-runtime"); + await fsPromises.mkdir(path.join(distDir, "control-ui"), { recursive: true }); + await fsPromises.mkdir(distRuntimeDir, { recursive: true }); + await fsPromises.writeFile(path.join(distDir, "delegate-BPjCe4gC.js"), "old delegate\n"); + await fsPromises.writeFile(path.join(distDir, "compact.runtime-2DiEmVcA.js"), "old runtime\n"); + await fsPromises.writeFile(path.join(distDir, "compact.runtime.js"), "stable alias\n"); + await fsPromises.writeFile(path.join(distDir, "entry.js"), "entry\n"); + await fsPromises.writeFile(path.join(distDir, "control-ui", "index.html"), "asset\n"); + await fsPromises.writeFile( + path.join(distRuntimeDir, "heartbeat-runner.runtime-fspOEj_1.js"), + "old runtime\n", + ); + await fsPromises.writeFile(path.join(distRuntimeDir, "heartbeat-runner.runtime.js"), "alias\n"); + + pruneStaleRootChunkFiles({ cwd: rootDir }); + + await expect( + fsPromises.readFile(path.join(distDir, "compact.runtime.js"), "utf8"), + ).resolves.toBe("stable alias\n"); + await expect(fsPromises.readFile(path.join(distDir, "entry.js"), "utf8")).resolves.toBe( + "entry\n", + ); + await expect( + fsPromises.readFile(path.join(distDir, "control-ui", "index.html"), "utf8"), + ).resolves.toBe("asset\n"); + await expect( + fsPromises.readFile(path.join(distRuntimeDir, "heartbeat-runner.runtime.js"), "utf8"), + ).resolves.toBe("alias\n"); + await expect(fsPromises.stat(path.join(distDir, "delegate-BPjCe4gC.js"))).rejects.toThrow(); + await expect( + fsPromises.stat(path.join(distDir, "compact.runtime-2DiEmVcA.js")), + ).rejects.toThrow(); + await expect( + fsPromises.stat(path.join(distRuntimeDir, "heartbeat-runner.runtime-fspOEj_1.js")), + ).rejects.toThrow(); + }); });