diff --git a/scripts/lib/static-extension-assets.mjs b/scripts/lib/static-extension-assets.mjs index 49f1d09b183..3c8797e9d03 100644 --- a/scripts/lib/static-extension-assets.mjs +++ b/scripts/lib/static-extension-assets.mjs @@ -34,6 +34,21 @@ function listExtensionPackageDirs(rootDir, fsImpl) { .toSorted((left, right) => left.dirName.localeCompare(right.dirName)); } +function listDistExtensionPackageDirs(rootDir, fsImpl) { + const extensionsRoot = path.join(rootDir, "dist", "extensions"); + if (!fsImpl.existsSync(extensionsRoot)) { + return []; + } + return fsImpl + .readdirSync(extensionsRoot, { withFileTypes: true }) + .filter((entry) => entry.isDirectory() && entry.name !== "node_modules") + .map((entry) => ({ + dirName: entry.name, + packageDir: path.join(extensionsRoot, entry.name), + })) + .toSorted((left, right) => left.dirName.localeCompare(right.dirName)); +} + function readPackageStaticAssetEntries(packageJson) { const entries = packageJson.openclaw?.build?.staticAssets; return Array.isArray(entries) ? entries : []; @@ -65,6 +80,33 @@ export function discoverStaticExtensionAssets(params = {}) { return assets.toSorted((left, right) => left.dest.localeCompare(right.dest)); } +function discoverStaticExtensionRuntimeOverlayAssets(params = {}) { + const rootDir = params.rootDir ?? process.cwd(); + const fsImpl = params.fs ?? fs; + const assetsByDest = new Map(); + for (const asset of params.assets ?? discoverStaticExtensionAssets({ rootDir, fs: fsImpl })) { + assetsByDest.set(asset.dest, asset); + } + for (const { dirName, packageDir } of listDistExtensionPackageDirs(rootDir, fsImpl)) { + const packageJsonPath = path.join(packageDir, "package.json"); + if (!fsImpl.existsSync(packageJsonPath)) { + continue; + } + const packageJson = readJsonFile(packageJsonPath, fsImpl); + for (const entry of readPackageStaticAssetEntries(packageJson)) { + const output = normalizePackageRelativePath(entry?.output); + if (!output) { + continue; + } + const dest = toPosixPath(path.posix.join("dist", "extensions", dirName, output)); + if (!assetsByDest.has(dest)) { + assetsByDest.set(dest, { pluginDir: dirName, src: dest, dest }); + } + } + } + return [...assetsByDest.values()].toSorted((left, right) => left.dest.localeCompare(right.dest)); +} + export function listStaticExtensionAssetOutputs(params = {}) { const assets = params.assets ?? discoverStaticExtensionAssets(params); return assets @@ -99,7 +141,7 @@ export function copyStaticExtensionAssets(params = {}) { export function copyStaticExtensionAssetsToRuntimeOverlay(params = {}) { const rootDir = params.rootDir ?? process.cwd(); const fsImpl = params.fs ?? fs; - const assets = params.assets ?? discoverStaticExtensionAssets({ rootDir, fs: fsImpl }); + const assets = discoverStaticExtensionRuntimeOverlayAssets({ ...params, rootDir, fs: fsImpl }); const runtimeExtensionsRoot = path.join(rootDir, "dist-runtime", "extensions"); if (!fsImpl.existsSync(runtimeExtensionsRoot)) { return; @@ -111,10 +153,12 @@ export function copyStaticExtensionAssetsToRuntimeOverlay(params = {}) { continue; } const srcPath = path.join(rootDir, src); + const distPath = path.join(rootDir, dest); + const copySourcePath = fsImpl.existsSync(srcPath) ? srcPath : distPath; const destPath = path.join(rootDir, "dist-runtime", normalizedDest.slice("dist/".length)); - if (fsImpl.existsSync(srcPath)) { + if (fsImpl.existsSync(copySourcePath)) { fsImpl.mkdirSync(path.dirname(destPath), { recursive: true }); - fsImpl.copyFileSync(srcPath, destPath); + fsImpl.copyFileSync(copySourcePath, destPath); } else { warn(`[runtime-postbuild] static asset not found, skipping: ${src}`); } diff --git a/src/infra/run-node.test.ts b/src/infra/run-node.test.ts index e1c5fb9d874..63e34fc4f0f 100644 --- a/src/infra/run-node.test.ts +++ b/src/infra/run-node.test.ts @@ -59,16 +59,10 @@ const DIST_RUNTIME_EXTENSION_SKILL = "dist-runtime/extensions/demo/skills/SKILL. const DIST_OPENCLAW_ALIAS_PACKAGE = "dist/extensions/node_modules/openclaw/package.json"; const DIST_OPENCLAW_ALIAS_PLUGIN_SDK_INDEX = "dist/extensions/node_modules/openclaw/plugin-sdk/index.js"; -const ACPX_PACKAGE = "extensions/acpx/package.json"; -const ACPX_MCP_PROXY_SOURCE = "extensions/acpx/src/runtime-internals/mcp-proxy.mjs"; -const DIST_ACPX_MCP_PROXY = "dist/extensions/acpx/mcp-proxy.mjs"; -const ACPX_ERROR_FORMAT_SOURCE = "extensions/acpx/src/runtime-internals/error-format.mjs"; -const DIST_ACPX_ERROR_FORMAT = "dist/extensions/acpx/error-format.mjs"; -const ACPX_MCP_COMMAND_LINE_SOURCE = "extensions/acpx/src/runtime-internals/mcp-command-line.mjs"; -const DIST_ACPX_MCP_COMMAND_LINE = "dist/extensions/acpx/mcp-command-line.mjs"; const DIFFS_PACKAGE = "extensions/diffs/package.json"; const DIFFS_VIEWER_RUNTIME_SOURCE = "extensions/diffs/assets/viewer-runtime.js"; const DIST_DIFFS_VIEWER_RUNTIME = "dist/extensions/diffs/assets/viewer-runtime.js"; +const DIST_RUNTIME_DIFFS_VIEWER_RUNTIME = "dist-runtime/extensions/diffs/assets/viewer-runtime.js"; const DIST_EXTENSION_MANIFEST = bundledDistPluginFile("demo", "openclaw.plugin.json"); const DIST_EXTENSION_PACKAGE = bundledDistPluginFile("demo", "package.json"); @@ -1904,38 +1898,25 @@ describe("run-node script", () => { await setupTrackedProject(tmp, { files: { [ROOT_SRC]: "export const value = 1;\n", - [ACPX_PACKAGE]: - '{"openclaw":{"build":{"staticAssets":[{"source":"./src/runtime-internals/mcp-proxy.mjs","output":"mcp-proxy.mjs"},{"source":"./src/runtime-internals/error-format.mjs","output":"error-format.mjs"},{"source":"./src/runtime-internals/mcp-command-line.mjs","output":"mcp-command-line.mjs"}]}}}\n', - [ACPX_MCP_PROXY_SOURCE]: "export {};\n", - [DIST_ACPX_MCP_PROXY]: "export {};\n", - [ACPX_ERROR_FORMAT_SOURCE]: "export {};\n", - [DIST_ACPX_ERROR_FORMAT]: "export {};\n", - [ACPX_MCP_COMMAND_LINE_SOURCE]: "export {};\n", - [DIST_ACPX_MCP_COMMAND_LINE]: "export {};\n", [DIFFS_PACKAGE]: '{"openclaw":{"build":{"staticAssets":[{"source":"./assets/viewer-runtime.js","output":"assets/viewer-runtime.js"}]}}}\n', [DIFFS_VIEWER_RUNTIME_SOURCE]: "export {};\n", [DIST_DIFFS_VIEWER_RUNTIME]: "export {};\n", + [DIST_RUNTIME_DIFFS_VIEWER_RUNTIME]: "export {};\n", [RUNTIME_POSTBUILD_STAMP]: '{"head":"abc123"}\n', }, buildPaths: [ ROOT_SRC, - ACPX_PACKAGE, - ACPX_MCP_PROXY_SOURCE, - DIST_ACPX_MCP_PROXY, - ACPX_ERROR_FORMAT_SOURCE, - DIST_ACPX_ERROR_FORMAT, - ACPX_MCP_COMMAND_LINE_SOURCE, - DIST_ACPX_MCP_COMMAND_LINE, DIFFS_PACKAGE, DIFFS_VIEWER_RUNTIME_SOURCE, DIST_DIFFS_VIEWER_RUNTIME, + DIST_RUNTIME_DIFFS_VIEWER_RUNTIME, DIST_ENTRY, BUILD_STAMP, RUNTIME_POSTBUILD_STAMP, ], }); - await fs.rm(resolvePath(tmp, DIST_ACPX_MCP_COMMAND_LINE)); + await fs.rm(resolvePath(tmp, DIST_DIFFS_VIEWER_RUNTIME)); const requirement = resolveRuntimePostBuildRequirement( createBuildRequirementDeps(tmp, { @@ -1951,6 +1932,32 @@ describe("run-node script", () => { }); }); + it("does not require static asset outputs when the declared source is absent", async () => { + await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => { + await setupTrackedProject(tmp, { + files: { + [ROOT_SRC]: "export const value = 1;\n", + [DIFFS_PACKAGE]: + '{"openclaw":{"build":{"staticAssets":[{"source":"./assets/viewer-runtime.js","output":"assets/viewer-runtime.js"}]}}}\n', + [RUNTIME_POSTBUILD_STAMP]: '{"head":"abc123"}\n', + }, + buildPaths: [ROOT_SRC, DIFFS_PACKAGE, DIST_ENTRY, BUILD_STAMP, RUNTIME_POSTBUILD_STAMP], + }); + + const requirement = resolveRuntimePostBuildRequirement( + createBuildRequirementDeps(tmp, { + gitHead: "abc123\n", + gitStatus: "", + }), + ); + + expect(requirement).toEqual({ + shouldSync: false, + reason: "clean", + }); + }); + }); + it("reports missing core runtime postbuild outputs when runtime stamps match HEAD", async () => { for (const missingPath of [ DIST_PLUGIN_SDK_ROOT_ALIAS, diff --git a/test/scripts/runtime-postbuild.test.ts b/test/scripts/runtime-postbuild.test.ts index 8812f959663..bd059376aed 100644 --- a/test/scripts/runtime-postbuild.test.ts +++ b/test/scripts/runtime-postbuild.test.ts @@ -139,6 +139,50 @@ describe("runtime postbuild static assets", () => { ); }); + it("preserves restored dist static assets when plugin sources are absent", async () => { + const rootDir = createTempDir("openclaw-runtime-postbuild-"); + const output = "assets/viewer-runtime.js"; + const distPluginDir = path.join(rootDir, "dist", "extensions", "diffs"); + const runtimeAsset = path.join(rootDir, "dist-runtime", "extensions", "diffs", output); + + await fs.mkdir(path.join(rootDir, "src", "plugin-sdk"), { recursive: true }); + await fs.writeFile( + path.join(rootDir, "src", "plugin-sdk", "root-alias.cjs"), + "module.exports = {};\n", + "utf8", + ); + await fs.mkdir(path.join(distPluginDir, "assets"), { recursive: true }); + await fs.writeFile(path.join(distPluginDir, "index.js"), "export default {};\n", "utf8"); + await fs.writeFile( + path.join(distPluginDir, "openclaw.plugin.json"), + '{"id":"diffs"}\n', + "utf8", + ); + await fs.writeFile( + path.join(distPluginDir, "package.json"), + JSON.stringify({ + name: "@openclaw/diffs", + openclaw: { + extensions: ["./index.js"], + build: { + staticAssets: [{ source: `./${output}`, output }], + }, + }, + }), + "utf8", + ); + await fs.writeFile(path.join(distPluginDir, output), "console.log('viewer');\n", "utf8"); + + runRuntimePostBuild({ + cwd: rootDir, + repoRoot: rootDir, + rootDir, + timings: false, + }); + + await expect(fs.readFile(runtimeAsset, "utf8")).resolves.toBe("console.log('viewer');\n"); + }); + it("skips runtime overlay asset copies when the runtime extension root is absent", async () => { const rootDir = createTempDir("openclaw-runtime-postbuild-"); await fs.mkdir(path.join(rootDir, "extensions", "demo", "assets"), { recursive: true });