refactor(plugins): declare static runtime assets in package metadata

This commit is contained in:
Vincent Koc
2026-05-02 23:29:22 -07:00
parent 188c3b74ba
commit c7bbb3f9af
7 changed files with 136 additions and 33 deletions

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 = [];

View File

@@ -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"]);

View File

@@ -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]);

View File

@@ -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<T extends string>(
values: readonly T[],
label: string,

View File

@@ -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";