mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-03 15:00:20 +00:00
fix(plugins): keep built plugin loading on one module graph (#48595)
This commit is contained in:
@@ -3211,6 +3211,16 @@ module.exports = {
|
||||
expect(resolved).toBe(distFile);
|
||||
});
|
||||
|
||||
it("configures the plugin loader jiti boundary to prefer native dist modules", () => {
|
||||
const options = __testing.buildPluginLoaderJitiOptions({});
|
||||
|
||||
expect(options.tryNative).toBe(true);
|
||||
expect(options.interopDefault).toBe(true);
|
||||
expect(options.extensions).toContain(".js");
|
||||
expect(options.extensions).toContain(".ts");
|
||||
expect("alias" in options).toBe(false);
|
||||
});
|
||||
|
||||
it("prefers src root-alias shim when loader runs from src in non-production", () => {
|
||||
const { root, srcFile } = createPluginSdkAliasFixture({
|
||||
srcFile: "root-alias.cjs",
|
||||
@@ -3243,6 +3253,15 @@ module.exports = {
|
||||
expect(resolved).toBe(srcFile);
|
||||
});
|
||||
|
||||
it("prefers dist plugin runtime module when loader runs from dist", () => {
|
||||
const { root, distFile } = createPluginRuntimeAliasFixture();
|
||||
|
||||
const resolved = __testing.resolvePluginRuntimeModulePath({
|
||||
modulePath: path.join(root, "dist", "plugins", "loader.js"),
|
||||
});
|
||||
expect(resolved).toBe(distFile);
|
||||
});
|
||||
|
||||
it("resolves plugin runtime module from package root when loader runs from transpiler cache path", () => {
|
||||
const { root, srcFile } = createPluginRuntimeAliasFixture();
|
||||
|
||||
|
||||
@@ -198,6 +198,21 @@ const resolvePluginSdkAliasFile = (params: {
|
||||
const resolvePluginSdkAlias = (): string | null =>
|
||||
resolvePluginSdkAliasFile({ srcFile: "root-alias.cjs", distFile: "root-alias.cjs" });
|
||||
|
||||
function buildPluginLoaderJitiOptions(aliasMap: Record<string, string>) {
|
||||
return {
|
||||
interopDefault: true,
|
||||
// Prefer Node's native sync ESM loader for built dist/*.js modules so
|
||||
// bundled plugins and plugin-sdk subpaths stay on the canonical module graph.
|
||||
tryNative: true,
|
||||
extensions: [".ts", ".tsx", ".mts", ".cts", ".mtsx", ".ctsx", ".js", ".mjs", ".cjs", ".json"],
|
||||
...(Object.keys(aliasMap).length > 0
|
||||
? {
|
||||
alias: aliasMap,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
function resolvePluginRuntimeModulePath(params: LoaderModuleResolveParams = {}): string | null {
|
||||
try {
|
||||
const modulePath = resolveLoaderModulePath(params);
|
||||
@@ -273,6 +288,7 @@ const resolvePluginSdkScopedAliasMap = (): Record<string, string> => {
|
||||
};
|
||||
|
||||
export const __testing = {
|
||||
buildPluginLoaderJitiOptions,
|
||||
listPluginSdkAliasCandidates,
|
||||
listPluginSdkExportedSubpaths,
|
||||
resolvePluginSdkAliasCandidateOrder,
|
||||
@@ -839,15 +855,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
...(pluginSdkAlias ? { "openclaw/plugin-sdk": pluginSdkAlias } : {}),
|
||||
...resolvePluginSdkScopedAliasMap(),
|
||||
};
|
||||
jitiLoader = createJiti(import.meta.url, {
|
||||
interopDefault: true,
|
||||
extensions: [".ts", ".tsx", ".mts", ".cts", ".mtsx", ".ctsx", ".js", ".mjs", ".cjs", ".json"],
|
||||
...(Object.keys(aliasMap).length > 0
|
||||
? {
|
||||
alias: aliasMap,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
jitiLoader = createJiti(import.meta.url, buildPluginLoaderJitiOptions(aliasMap));
|
||||
return jitiLoader;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { stageBundledPluginRuntime } from "../../scripts/stage-bundled-plugin-runtime.mjs";
|
||||
import { discoverOpenClawPlugins } from "./discovery.js";
|
||||
import { loadPluginManifestRegistry } from "./manifest-registry.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
@@ -19,7 +22,7 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("stageBundledPluginRuntime", () => {
|
||||
it("hard-links bundled dist plugins into dist-runtime and links plugin-local node_modules", () => {
|
||||
it("stages bundled dist plugins as runtime wrappers and links plugin-local node_modules", () => {
|
||||
const repoRoot = makeRepoRoot("openclaw-stage-bundled-runtime-");
|
||||
const distPluginDir = path.join(repoRoot, "dist", "extensions", "diffs");
|
||||
fs.mkdirSync(path.join(repoRoot, "dist"), { recursive: true });
|
||||
@@ -39,14 +42,16 @@ describe("stageBundledPluginRuntime", () => {
|
||||
|
||||
const runtimePluginDir = path.join(repoRoot, "dist-runtime", "extensions", "diffs");
|
||||
expect(fs.existsSync(path.join(runtimePluginDir, "index.js"))).toBe(true);
|
||||
expect(fs.statSync(path.join(runtimePluginDir, "index.js")).nlink).toBeGreaterThan(1);
|
||||
expect(fs.readFileSync(path.join(runtimePluginDir, "index.js"), "utf8")).toContain(
|
||||
"../../../dist/extensions/diffs/index.js",
|
||||
);
|
||||
expect(fs.lstatSync(path.join(runtimePluginDir, "node_modules")).isSymbolicLink()).toBe(true);
|
||||
expect(fs.realpathSync(path.join(runtimePluginDir, "node_modules"))).toBe(
|
||||
fs.realpathSync(sourcePluginNodeModulesDir),
|
||||
);
|
||||
});
|
||||
|
||||
it("hard-links top-level dist chunks so staged bundled plugins keep relative imports working", () => {
|
||||
it("writes wrappers that forward plugin entry imports into canonical dist files", async () => {
|
||||
const repoRoot = makeRepoRoot("openclaw-stage-bundled-runtime-chunks-");
|
||||
fs.mkdirSync(path.join(repoRoot, "dist", "extensions", "diffs"), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
@@ -62,19 +67,138 @@ describe("stageBundledPluginRuntime", () => {
|
||||
|
||||
stageBundledPluginRuntime({ repoRoot });
|
||||
|
||||
const runtimeChunkPath = path.join(repoRoot, "dist-runtime", "chunk-abc.js");
|
||||
expect(fs.readFileSync(runtimeChunkPath, "utf8")).toContain("value = 1");
|
||||
expect(fs.statSync(runtimeChunkPath).nlink).toBeGreaterThan(1);
|
||||
expect(
|
||||
fs.readFileSync(
|
||||
path.join(repoRoot, "dist-runtime", "extensions", "diffs", "index.js"),
|
||||
"utf8",
|
||||
const runtimeEntryPath = path.join(repoRoot, "dist-runtime", "extensions", "diffs", "index.js");
|
||||
expect(fs.readFileSync(runtimeEntryPath, "utf8")).toContain(
|
||||
"../../../dist/extensions/diffs/index.js",
|
||||
);
|
||||
expect(fs.existsSync(path.join(repoRoot, "dist-runtime", "chunk-abc.js"))).toBe(false);
|
||||
|
||||
const runtimeModule = await import(`${pathToFileURL(runtimeEntryPath).href}?t=${Date.now()}`);
|
||||
expect(runtimeModule.value).toBe(1);
|
||||
});
|
||||
|
||||
it("copies package metadata files but symlinks other non-js plugin artifacts into the runtime overlay", () => {
|
||||
const repoRoot = makeRepoRoot("openclaw-stage-bundled-runtime-assets-");
|
||||
const distPluginDir = path.join(repoRoot, "dist", "extensions", "diffs");
|
||||
fs.mkdirSync(path.join(distPluginDir, "assets"), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(distPluginDir, "package.json"),
|
||||
JSON.stringify(
|
||||
{ name: "@openclaw/diffs", openclaw: { extensions: ["./index.js"] } },
|
||||
null,
|
||||
2,
|
||||
),
|
||||
).toContain("../../chunk-abc.js");
|
||||
const distChunkStats = fs.statSync(path.join(repoRoot, "dist", "chunk-abc.js"));
|
||||
const runtimeChunkStats = fs.statSync(runtimeChunkPath);
|
||||
expect(runtimeChunkStats.ino).toBe(distChunkStats.ino);
|
||||
expect(runtimeChunkStats.dev).toBe(distChunkStats.dev);
|
||||
"utf8",
|
||||
);
|
||||
fs.writeFileSync(path.join(distPluginDir, "openclaw.plugin.json"), "{}\n", "utf8");
|
||||
fs.writeFileSync(path.join(distPluginDir, "assets", "info.txt"), "ok\n", "utf8");
|
||||
|
||||
stageBundledPluginRuntime({ repoRoot });
|
||||
|
||||
const runtimePackagePath = path.join(
|
||||
repoRoot,
|
||||
"dist-runtime",
|
||||
"extensions",
|
||||
"diffs",
|
||||
"package.json",
|
||||
);
|
||||
const runtimeManifestPath = path.join(
|
||||
repoRoot,
|
||||
"dist-runtime",
|
||||
"extensions",
|
||||
"diffs",
|
||||
"openclaw.plugin.json",
|
||||
);
|
||||
const runtimeAssetPath = path.join(
|
||||
repoRoot,
|
||||
"dist-runtime",
|
||||
"extensions",
|
||||
"diffs",
|
||||
"assets",
|
||||
"info.txt",
|
||||
);
|
||||
|
||||
expect(fs.lstatSync(runtimePackagePath).isSymbolicLink()).toBe(false);
|
||||
expect(fs.readFileSync(runtimePackagePath, "utf8")).toContain('"extensions": [');
|
||||
expect(fs.lstatSync(runtimeManifestPath).isSymbolicLink()).toBe(false);
|
||||
expect(fs.readFileSync(runtimeManifestPath, "utf8")).toBe("{}\n");
|
||||
expect(fs.lstatSync(runtimeAssetPath).isSymbolicLink()).toBe(true);
|
||||
expect(fs.readFileSync(runtimeAssetPath, "utf8")).toBe("ok\n");
|
||||
});
|
||||
|
||||
it("preserves package metadata needed for bundled plugin discovery from dist-runtime", () => {
|
||||
const repoRoot = makeRepoRoot("openclaw-stage-bundled-runtime-discovery-");
|
||||
const distPluginDir = path.join(repoRoot, "dist", "extensions", "demo");
|
||||
const runtimeExtensionsDir = path.join(repoRoot, "dist-runtime", "extensions");
|
||||
fs.mkdirSync(distPluginDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(distPluginDir, "package.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "@openclaw/demo",
|
||||
openclaw: {
|
||||
extensions: ["./main.js"],
|
||||
setupEntry: "./setup.js",
|
||||
startup: {
|
||||
deferConfiguredChannelFullLoadUntilAfterListen: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(distPluginDir, "openclaw.plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
id: "demo",
|
||||
channels: ["demo"],
|
||||
configSchema: { type: "object" },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
fs.writeFileSync(path.join(distPluginDir, "main.js"), "export default {};\n", "utf8");
|
||||
fs.writeFileSync(path.join(distPluginDir, "setup.js"), "export default {};\n", "utf8");
|
||||
|
||||
stageBundledPluginRuntime({ repoRoot });
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
OPENCLAW_BUNDLED_PLUGINS_DIR: runtimeExtensionsDir,
|
||||
};
|
||||
const discovery = discoverOpenClawPlugins({
|
||||
env,
|
||||
cache: false,
|
||||
});
|
||||
const manifestRegistry = loadPluginManifestRegistry({
|
||||
env,
|
||||
cache: false,
|
||||
candidates: discovery.candidates,
|
||||
diagnostics: discovery.diagnostics,
|
||||
});
|
||||
const expectedRuntimeMainPath = fs.realpathSync(
|
||||
path.join(runtimeExtensionsDir, "demo", "main.js"),
|
||||
);
|
||||
const expectedRuntimeSetupPath = fs.realpathSync(
|
||||
path.join(runtimeExtensionsDir, "demo", "setup.js"),
|
||||
);
|
||||
|
||||
expect(discovery.candidates).toHaveLength(1);
|
||||
expect(fs.realpathSync(discovery.candidates[0]?.source ?? "")).toBe(expectedRuntimeMainPath);
|
||||
expect(fs.realpathSync(discovery.candidates[0]?.setupSource ?? "")).toBe(
|
||||
expectedRuntimeSetupPath,
|
||||
);
|
||||
expect(fs.realpathSync(manifestRegistry.plugins[0]?.setupSource ?? "")).toBe(
|
||||
expectedRuntimeSetupPath,
|
||||
);
|
||||
expect(manifestRegistry.plugins[0]?.startupDeferConfiguredChannelFullLoadUntilAfterListen).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("removes stale runtime plugin directories that are no longer in dist", () => {
|
||||
|
||||
Reference in New Issue
Block a user