diff --git a/extensions/acpx/package.json b/extensions/acpx/package.json index 053cd39345b..a57deb9a344 100644 --- a/extensions/acpx/package.json +++ b/extensions/acpx/package.json @@ -28,7 +28,21 @@ "pluginApi": ">=2026.5.3" }, "build": { - "openclawVersion": "2026.5.3" + "openclawVersion": "2026.5.3", + "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" + } + ] }, "release": { "publishToClawHub": true, diff --git a/extensions/diffs/package.json b/extensions/diffs/package.json index 7b45fb6551a..0436576e524 100644 --- a/extensions/diffs/package.json +++ b/extensions/diffs/package.json @@ -33,7 +33,13 @@ "pluginApi": ">=2026.5.3" }, "build": { - "openclawVersion": "2026.5.3" + "openclawVersion": "2026.5.3", + "staticAssets": [ + { + "source": "./assets/viewer-runtime.js", + "output": "assets/viewer-runtime.js" + } + ] }, "release": { "publishToClawHub": true, diff --git a/scripts/lib/static-extension-assets.mjs b/scripts/lib/static-extension-assets.mjs index 8933d1b7efa..2e7a7a4e16a 100644 --- a/scripts/lib/static-extension-assets.mjs +++ b/scripts/lib/static-extension-assets.mjs @@ -1,42 +1,88 @@ import fs from "node:fs"; import path from "node:path"; -/** - * Static, non-transpiled runtime assets referenced by built extension code. - * - * `dest` is the root-package dist path. Package-local runtime builds rewrite it - * under the plugin package's own dist directory. - */ -export const STATIC_EXTENSION_ASSETS = [ - { - src: "extensions/acpx/src/runtime-internals/mcp-proxy.mjs", - dest: "dist/extensions/acpx/mcp-proxy.mjs", - }, - { - src: "extensions/acpx/src/runtime-internals/error-format.mjs", - dest: "dist/extensions/acpx/error-format.mjs", - }, - { - src: "extensions/acpx/src/runtime-internals/mcp-command-line.mjs", - dest: "dist/extensions/acpx/mcp-command-line.mjs", - }, - { - src: "extensions/diffs/assets/viewer-runtime.js", - dest: "dist/extensions/diffs/assets/viewer-runtime.js", - }, -]; +function toPosixPath(value) { + return String(value ?? "").replaceAll("\\", "/"); +} + +function readJsonFile(filePath, fsImpl) { + return JSON.parse(fsImpl.readFileSync(filePath, "utf8")); +} + +function normalizePackageRelativePath(value) { + const normalized = toPosixPath(value) + .trim() + .replace(/^\.\/+/u, ""); + if (!normalized || normalized.startsWith("../") || normalized.includes("/../")) { + return ""; + } + return normalized; +} + +function listExtensionPackageDirs(rootDir, fsImpl) { + const extensionsRoot = path.join(rootDir, "extensions"); + if (!fsImpl.existsSync(extensionsRoot)) { + return []; + } + return fsImpl + .readdirSync(extensionsRoot, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .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 : []; +} + +export function discoverStaticExtensionAssets(params = {}) { + const rootDir = params.rootDir ?? process.cwd(); + const fsImpl = params.fs ?? fs; + const assets = []; + for (const { dirName, packageDir } of listExtensionPackageDirs(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 source = normalizePackageRelativePath(entry?.source); + const output = normalizePackageRelativePath(entry?.output); + if (!source || !output) { + continue; + } + assets.push({ + pluginDir: dirName, + src: toPosixPath(path.posix.join("extensions", dirName, source)), + dest: toPosixPath(path.posix.join("dist", "extensions", dirName, output)), + }); + } + } + return assets.toSorted((left, right) => left.dest.localeCompare(right.dest)); +} export function listStaticExtensionAssetOutputs(params = {}) { - const assets = params.assets ?? STATIC_EXTENSION_ASSETS; + const assets = params.assets ?? discoverStaticExtensionAssets(params); return assets .map(({ dest }) => dest.replace(/\\/g, "/")) .toSorted((left, right) => left.localeCompare(right)); } +export function listStaticExtensionAssetSources(params = {}) { + const assets = params.assets ?? discoverStaticExtensionAssets(params); + return assets + .map(({ src }) => src.replace(/\\/g, "/")) + .toSorted((left, right) => left.localeCompare(right)); +} + export function copyStaticExtensionAssets(params = {}) { const rootDir = params.rootDir ?? process.cwd(); - const assets = params.assets ?? STATIC_EXTENSION_ASSETS; const fsImpl = params.fs ?? fs; + const assets = params.assets ?? discoverStaticExtensionAssets({ rootDir, fs: fsImpl }); const warn = params.warn ?? console.warn; for (const { src, dest } of assets) { const srcPath = path.join(rootDir, src); @@ -52,8 +98,8 @@ export function copyStaticExtensionAssets(params = {}) { export function copyStaticExtensionAssetsForPackage(params) { const rootDir = params.rootDir ?? process.cwd(); - const assets = params.assets ?? STATIC_EXTENSION_ASSETS; const fsImpl = params.fs ?? fs; + const assets = params.assets ?? discoverStaticExtensionAssets({ rootDir, fs: fsImpl }); const packagePrefix = `extensions/${params.pluginDir}/`; const rootDistPrefix = `dist/extensions/${params.pluginDir}/`; const copied = []; diff --git a/scripts/run-node.mjs b/scripts/run-node.mjs index 655311a0406..0f536e3772a 100644 --- a/scripts/run-node.mjs +++ b/scripts/run-node.mjs @@ -15,6 +15,7 @@ import { writeBuildStamp as writeDistBuildStamp, writeRuntimePostBuildStamp as writeDistRuntimePostBuildStamp, } from "./lib/local-build-metadata.mjs"; +import { listStaticExtensionAssetSources } from "./lib/static-extension-assets.mjs"; import { runRuntimePostBuild } from "./runtime-postbuild.mjs"; const buildScript = "scripts/tsdown-build.mjs"; @@ -46,10 +47,7 @@ const ignoredRunNodeRepoPaths = new Set([ const runtimePostBuildScriptPaths = new Set( runtimePostBuildWatchedPaths.filter((entry) => entry.startsWith("scripts/")), ); -const runtimePostBuildStaticAssetPaths = new Set([ - "extensions/acpx/src/runtime-internals/mcp-proxy.mjs", - "extensions/diffs/assets/viewer-runtime.js", -]); +const runtimePostBuildStaticAssetPaths = new Set(listStaticExtensionAssetSources()); const extensionSourceFilePattern = /\.(?:[cm]?[jt]sx?)$/; const extensionRestartMetadataFiles = new Set(["openclaw.plugin.json", "package.json"]); diff --git a/src/plugins/runtime-sidecar-paths-baseline.ts b/src/plugins/runtime-sidecar-paths-baseline.ts index 771c06a8375..44cabe6f802 100644 --- a/src/plugins/runtime-sidecar-paths-baseline.ts +++ b/src/plugins/runtime-sidecar-paths-baseline.ts @@ -24,6 +24,10 @@ function collectRootPackageExcludedRuntimeSidecarPluginDirs(rootDir: string): Se if (typeof entry !== "string") { continue; } + // The root package intentionally excludes externalized official plugin + // runtime trees. Do not put their runtime sidecars in the root package + // baseline: packaged installs must load those files from the plugin's own + // npm package-local dist directory instead. const match = /^!dist\/extensions\/([^/]+)\/\*\*$/u.exec(entry); if (match?.[1]) { excluded.add(match[1]); diff --git a/src/plugins/runtime-sidecar-paths.ts b/src/plugins/runtime-sidecar-paths.ts index 065d617cd2c..2b596ade4eb 100644 --- a/src/plugins/runtime-sidecar-paths.ts +++ b/src/plugins/runtime-sidecar-paths.ts @@ -1,5 +1,8 @@ import bundledRuntimeSidecarPaths from "../../scripts/lib/bundled-runtime-sidecar-paths.json" with { type: "json" }; +// Keep this JSON as the root package's runtime sidecar inventory only. Official +// plugin packages that are excluded from root package files must ship their +// sidecars from their own npm package-local dist directory instead. export function assertUniqueValues( values: readonly T[], label: string, diff --git a/test/scripts/runtime-postbuild.test.ts b/test/scripts/runtime-postbuild.test.ts index 8636bfee2b4..61d97a5b1a2 100644 --- a/test/scripts/runtime-postbuild.test.ts +++ b/test/scripts/runtime-postbuild.test.ts @@ -1,6 +1,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { describe, expect, it, vi } from "vitest"; +import { discoverStaticExtensionAssets } from "../../scripts/lib/static-extension-assets.mjs"; import { copyStaticExtensionAssets, listStaticExtensionAssetOutputs, @@ -23,6 +24,37 @@ describe("runtime postbuild static assets", () => { ); }); + it("discovers static assets from plugin package metadata", async () => { + const rootDir = createTempDir("openclaw-runtime-postbuild-"); + const packageDir = path.join(rootDir, "extensions", "demo"); + await fs.mkdir(packageDir, { recursive: true }); + await fs.writeFile( + path.join(packageDir, "package.json"), + JSON.stringify({ + name: "@openclaw/demo", + openclaw: { + build: { + staticAssets: [ + { + source: "./assets/runtime.js", + output: "assets/runtime.js", + }, + ], + }, + }, + }), + "utf8", + ); + + expect(discoverStaticExtensionAssets({ rootDir })).toEqual([ + { + pluginDir: "demo", + src: "extensions/demo/assets/runtime.js", + dest: "dist/extensions/demo/assets/runtime.js", + }, + ]); + }); + it("copies declared static assets into dist", async () => { const rootDir = createTempDir("openclaw-runtime-postbuild-"); const src = "extensions/acpx/src/runtime-internals/mcp-proxy.mjs";