fix: repair packaged plugin runtime mirrors

This commit is contained in:
Peter Steinberger
2026-04-28 00:23:38 +01:00
parent 152e30935f
commit f71f5bc586
5 changed files with 161 additions and 8 deletions

View File

@@ -1072,7 +1072,7 @@ describe("scanBundledPluginRuntimeDeps config policy", () => {
JSON.stringify({
name: "openclaw",
version: "2026.4.25",
dependencies: { tslog: "^4.10.2" },
dependencies: { semver: "7.7.4", tslog: "^4.10.2" },
}),
);
writeBundledPluginPackage({
@@ -1090,10 +1090,12 @@ describe("scanBundledPluginRuntimeDeps config policy", () => {
expect(result.deps.map((dep) => `${dep.name}@${dep.version}`)).toEqual([
"discord-runtime@1.0.0",
"semver@7.7.4",
"tslog@^4.10.2",
]);
expect(result.missing.map((dep) => `${dep.name}@${dep.version}`)).toEqual([
"discord-runtime@1.0.0",
"semver@7.7.4",
"tslog@^4.10.2",
]);
});

View File

@@ -69,7 +69,7 @@ const BUNDLED_RUNTIME_DEPS_OWNERLESS_LOCK_STALE_MS = 30_000;
const BUNDLED_RUNTIME_DEPS_INSTALL_PROGRESS_INTERVAL_MS = 5_000;
const BUNDLED_RUNTIME_MIRROR_MATERIALIZED_EXTENSIONS = new Set([".cjs", ".js", ".mjs"]);
const BUNDLED_EXTENSION_DIST_DIR = "extensions";
const MIRRORED_CORE_RUNTIME_DEP_NAMES = ["tslog"] as const;
const MIRRORED_CORE_RUNTIME_DEP_NAMES = ["semver", "tslog"] as const;
const MIRRORED_PACKAGE_RUNTIME_DEP_PLUGIN_ID = "openclaw-core";
const BUNDLED_RUNTIME_MIRROR_PLUGIN_REGION_RE = /(?:^|\n)\/\/#region extensions\/[^/\s]+(?:\/|$)/u;
const BUNDLED_RUNTIME_MIRROR_IMPORT_SPECIFIER_RE =

View File

@@ -180,6 +180,86 @@ describe("prepareBundledPluginRuntimeRoot", () => {
expect(fs.readFileSync(distChunk, "utf8")).toContain("same-root");
});
it("mirrors canonical dist chunks when loading from dist-runtime", () => {
const packageRoot = makeTempRoot();
const stageDir = makeTempRoot();
const canonicalPluginRoot = path.join(packageRoot, "dist", "extensions", "qqbot");
const runtimePluginRoot = path.join(packageRoot, "dist-runtime", "extensions", "qqbot");
const env = { ...process.env, OPENCLAW_PLUGIN_STAGE_DIR: stageDir };
fs.mkdirSync(canonicalPluginRoot, { recursive: true });
fs.mkdirSync(runtimePluginRoot, { recursive: true });
fs.writeFileSync(
path.join(packageRoot, "package.json"),
JSON.stringify({ name: "openclaw", version: "2026.4.27", type: "module" }),
"utf8",
);
fs.writeFileSync(
path.join(packageRoot, "dist", "onboard-abc123.js"),
"export const setup = 'canonical-setup';\n",
"utf8",
);
fs.writeFileSync(
path.join(canonicalPluginRoot, "index.js"),
`import { setup } from "../../onboard-abc123.js"; export default { id: "qqbot", setup };\n`,
"utf8",
);
fs.writeFileSync(
path.join(canonicalPluginRoot, "package.json"),
JSON.stringify(
{
name: "@openclaw/qqbot",
version: "1.0.0",
type: "module",
dependencies: { "qqbot-runtime": "1.0.0" },
openclaw: { extensions: ["./index.js"] },
},
null,
2,
),
"utf8",
);
fs.writeFileSync(
path.join(runtimePluginRoot, "index.js"),
`export { default } from "../../../dist/extensions/qqbot/index.js";\n`,
"utf8",
);
fs.writeFileSync(
path.join(runtimePluginRoot, "package.json"),
JSON.stringify(
{
name: "@openclaw/qqbot",
version: "1.0.0",
type: "module",
dependencies: { "qqbot-runtime": "1.0.0" },
openclaw: { extensions: ["./index.js"] },
},
null,
2,
),
"utf8",
);
const installRoot = resolveBundledRuntimeDependencyInstallRoot(runtimePluginRoot, { env });
fs.mkdirSync(path.join(installRoot, "node_modules", "qqbot-runtime"), { recursive: true });
fs.writeFileSync(
path.join(installRoot, "node_modules", "qqbot-runtime", "package.json"),
JSON.stringify({ name: "qqbot-runtime", version: "1.0.0", type: "module" }),
"utf8",
);
const prepared = prepareBundledPluginRuntimeRoot({
pluginId: "qqbot",
pluginRoot: runtimePluginRoot,
modulePath: path.join(runtimePluginRoot, "index.js"),
env,
});
expect(prepared.pluginRoot).toBe(path.join(installRoot, "dist-runtime", "extensions", "qqbot"));
expect(fs.existsSync(path.join(installRoot, "dist", "onboard-abc123.js"))).toBe(true);
expect(
fs.readFileSync(path.join(installRoot, "dist", "extensions", "qqbot", "index.js"), "utf8"),
).toContain("onboard-abc123");
});
it("reuses unchanged external runtime mirrors from the original plugin root", async () => {
const packageRoot = makeTempRoot();
const stageDir = makeTempRoot();

View File

@@ -138,16 +138,52 @@ function prepareBundledPluginRuntimeDistMirror(params: {
}): string {
const sourceExtensionsRoot = path.dirname(params.pluginRoot);
const sourceDistRoot = path.dirname(sourceExtensionsRoot);
const mirrorDistRoot = path.join(params.installRoot, "dist");
const sourceDistRootName = path.basename(sourceDistRoot);
const mirrorDistRoot = path.join(params.installRoot, sourceDistRootName);
const mirrorExtensionsRoot = path.join(mirrorDistRoot, "extensions");
ensureBundledRuntimeMirrorDirectory(mirrorDistRoot);
fs.mkdirSync(mirrorExtensionsRoot, { recursive: true, mode: 0o755 });
ensureBundledRuntimeDistPackageJson(mirrorDistRoot);
for (const entry of fs.readdirSync(sourceDistRoot, { withFileTypes: true })) {
mirrorBundledRuntimeDistRootEntries({
sourceDistRoot,
mirrorDistRoot,
});
if (sourceDistRootName === "dist-runtime") {
mirrorCanonicalBundledRuntimeDistRoot({
installRoot: params.installRoot,
pluginRoot: params.pluginRoot,
sourceRuntimeDistRoot: sourceDistRoot,
});
}
ensureOpenClawPluginSdkAlias(mirrorDistRoot);
return mirrorExtensionsRoot;
}
function ensureBundledRuntimeMirrorDirectory(targetRoot: string): void {
try {
const stat = fs.lstatSync(targetRoot);
if (stat.isDirectory() && !stat.isSymbolicLink()) {
return;
}
fs.rmSync(targetRoot, { recursive: true, force: true });
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
throw error;
}
}
fs.mkdirSync(targetRoot, { recursive: true, mode: 0o755 });
}
function mirrorBundledRuntimeDistRootEntries(params: {
sourceDistRoot: string;
mirrorDistRoot: string;
}): void {
for (const entry of fs.readdirSync(params.sourceDistRoot, { withFileTypes: true })) {
if (entry.name === "extensions") {
continue;
}
const sourcePath = path.join(sourceDistRoot, entry.name);
const targetPath = path.join(mirrorDistRoot, entry.name);
const sourcePath = path.join(params.sourceDistRoot, entry.name);
const targetPath = path.join(params.mirrorDistRoot, entry.name);
if (path.resolve(sourcePath) === path.resolve(targetPath)) {
continue;
}
@@ -171,8 +207,39 @@ function prepareBundledPluginRuntimeDistMirror(params: {
}
}
}
ensureOpenClawPluginSdkAlias(mirrorDistRoot);
return mirrorExtensionsRoot;
}
function mirrorCanonicalBundledRuntimeDistRoot(params: {
installRoot: string;
pluginRoot: string;
sourceRuntimeDistRoot: string;
}): void {
const sourceCanonicalDistRoot = path.join(path.dirname(params.sourceRuntimeDistRoot), "dist");
if (!fs.existsSync(sourceCanonicalDistRoot)) {
return;
}
const targetCanonicalDistRoot = path.join(params.installRoot, "dist");
ensureBundledRuntimeMirrorDirectory(targetCanonicalDistRoot);
fs.mkdirSync(path.join(targetCanonicalDistRoot, "extensions"), { recursive: true, mode: 0o755 });
ensureBundledRuntimeDistPackageJson(targetCanonicalDistRoot);
mirrorBundledRuntimeDistRootEntries({
sourceDistRoot: sourceCanonicalDistRoot,
mirrorDistRoot: targetCanonicalDistRoot,
});
ensureOpenClawPluginSdkAlias(targetCanonicalDistRoot);
const pluginId = path.basename(params.pluginRoot);
const sourceCanonicalPluginRoot = path.join(sourceCanonicalDistRoot, "extensions", pluginId);
if (!fs.existsSync(sourceCanonicalPluginRoot)) {
return;
}
const targetCanonicalPluginRoot = path.join(targetCanonicalDistRoot, "extensions", pluginId);
refreshBundledPluginRuntimeMirrorRoot({
pluginId,
sourceRoot: sourceCanonicalPluginRoot,
targetRoot: targetCanonicalPluginRoot,
tempDirParent: path.dirname(targetCanonicalPluginRoot),
});
}
function ensureBundledRuntimeDistPackageJson(mirrorDistRoot: string): void {