mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:40:44 +00:00
fix(plugins): load packaged runtime mirrors from canonical sources
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
existsSync,
|
||||
mkdtempSync,
|
||||
mkdirSync,
|
||||
realpathSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
@@ -17,6 +18,10 @@ import {
|
||||
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
|
||||
writePackageDistInventory,
|
||||
} from "../src/infra/package-dist-inventory.ts";
|
||||
import {
|
||||
resolveBundledRuntimeDependencyInstallRoot,
|
||||
resolveBundledRuntimeDependencyPackageInstallRoot,
|
||||
} from "../src/plugins/bundled-runtime-deps.ts";
|
||||
import {
|
||||
collectBundledExtensionManifestErrors,
|
||||
type BundledExtension,
|
||||
@@ -317,28 +322,48 @@ function bundledRuntimeDependencySentinelPath(
|
||||
);
|
||||
}
|
||||
|
||||
function bundledRuntimeDependencySentinelCandidates(
|
||||
export function bundledRuntimeDependencySentinelCandidates(
|
||||
packageRoot: string,
|
||||
pluginId: string,
|
||||
dependencyName: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string[] {
|
||||
const dependencyParts = dependencyName.split("/");
|
||||
const packageRoots = [
|
||||
packageRoot,
|
||||
(() => {
|
||||
try {
|
||||
return realpathSync(packageRoot);
|
||||
} catch {
|
||||
return packageRoot;
|
||||
}
|
||||
})(),
|
||||
];
|
||||
const runtimeRoots = packageRoots.flatMap((root) => [
|
||||
resolveBundledRuntimeDependencyPackageInstallRoot(root, { env }),
|
||||
resolveBundledRuntimeDependencyInstallRoot(join(root, "dist", "extensions", pluginId), {
|
||||
env,
|
||||
}),
|
||||
]);
|
||||
return [
|
||||
bundledRuntimeDependencySentinelPath(packageRoot, pluginId, dependencyName),
|
||||
join(packageRoot, "dist", "extensions", "node_modules", ...dependencyParts, "package.json"),
|
||||
join(packageRoot, "node_modules", ...dependencyParts, "package.json"),
|
||||
];
|
||||
...runtimeRoots.map((root) => join(root, "node_modules", ...dependencyParts, "package.json")),
|
||||
].filter((candidate, index, candidates) => candidates.indexOf(candidate) === index);
|
||||
}
|
||||
|
||||
function assertBundledRuntimeDependencyAbsent(params: {
|
||||
packageRoot: string;
|
||||
pluginId: string;
|
||||
dependencyName: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): void {
|
||||
const sentinelPath = bundledRuntimeDependencySentinelCandidates(
|
||||
params.packageRoot,
|
||||
params.pluginId,
|
||||
params.dependencyName,
|
||||
params.env,
|
||||
).find((candidate) => existsSync(candidate));
|
||||
if (sentinelPath) {
|
||||
throw new Error(
|
||||
@@ -351,11 +376,13 @@ function assertBundledRuntimeDependencyPresent(params: {
|
||||
packageRoot: string;
|
||||
pluginId: string;
|
||||
dependencyName: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): void {
|
||||
const sentinelPath = bundledRuntimeDependencySentinelCandidates(
|
||||
params.packageRoot,
|
||||
params.pluginId,
|
||||
params.dependencyName,
|
||||
params.env,
|
||||
).find((candidate) => existsSync(candidate));
|
||||
if (sentinelPath) {
|
||||
return;
|
||||
@@ -413,24 +440,25 @@ function runPackedBundledPluginActivationSmoke(packageRoot: string, tmpRoot: str
|
||||
{ pluginId: "feishu", dependencyName: "@larksuiteoapi/node-sdk" },
|
||||
] as const;
|
||||
|
||||
for (const dep of lazyDeps) {
|
||||
assertBundledRuntimeDependencyAbsent({ packageRoot, ...dep });
|
||||
}
|
||||
|
||||
const homeDir = join(tmpRoot, "activation-home");
|
||||
mkdirSync(homeDir, { recursive: true });
|
||||
const env = createPackedCliSmokeEnv(process.env, {
|
||||
HOME: homeDir,
|
||||
OPENAI_API_KEY: "sk-openclaw-release-check",
|
||||
});
|
||||
for (const dep of lazyDeps) {
|
||||
assertBundledRuntimeDependencyAbsent({ packageRoot, env, ...dep });
|
||||
}
|
||||
|
||||
writePackedBundledPluginActivationConfig(homeDir);
|
||||
execFileSync(process.execPath, [join(packageRoot, "openclaw.mjs"), "plugins", "doctor"], {
|
||||
cwd: packageRoot,
|
||||
stdio: "inherit",
|
||||
env: createPackedCliSmokeEnv(process.env, {
|
||||
HOME: homeDir,
|
||||
OPENAI_API_KEY: "sk-openclaw-release-check",
|
||||
}),
|
||||
env,
|
||||
});
|
||||
|
||||
for (const dep of lazyDeps) {
|
||||
assertBundledRuntimeDependencyPresent({ packageRoot, ...dep });
|
||||
assertBundledRuntimeDependencyPresent({ packageRoot, env, ...dep });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1504,6 +1504,60 @@ module.exports = {
|
||||
expect(registry.plugins.find((entry) => entry.id === "alpha")?.status).toBe("loaded");
|
||||
});
|
||||
|
||||
it("loads bundled plugins from symlinked package roots with an external stage dir", () => {
|
||||
const packageRoot = makeTempDir();
|
||||
const stageDir = makeTempDir();
|
||||
const aliasRoot = path.join(makeTempDir(), "openclaw-alias");
|
||||
const bundledDir = path.join(packageRoot, "dist", "extensions");
|
||||
const plugin = writePlugin({
|
||||
id: "alpha",
|
||||
dir: path.join(bundledDir, "alpha"),
|
||||
filename: "index.cjs",
|
||||
body: `module.exports = { id: "alpha", register(api) { api.registerCommand({ name: "alpha", handler: () => "ok" }); } };`,
|
||||
});
|
||||
fs.writeFileSync(
|
||||
path.join(packageRoot, "package.json"),
|
||||
JSON.stringify({ name: "openclaw", version: "2026.4.24", type: "module" }),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(plugin.dir, "package.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "@openclaw/alpha",
|
||||
version: "1.0.0",
|
||||
openclaw: { extensions: ["./index.cjs"] },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(plugin.dir, "openclaw.plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
id: "alpha",
|
||||
enabledByDefault: true,
|
||||
configSchema: EMPTY_PLUGIN_SCHEMA,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
fs.symlinkSync(packageRoot, aliasRoot, "dir");
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = path.join(aliasRoot, "dist", "extensions");
|
||||
process.env.OPENCLAW_PLUGIN_STAGE_DIR = stageDir;
|
||||
|
||||
const registry = loadOpenClawPlugins({
|
||||
cache: false,
|
||||
config: { plugins: { enabled: true } },
|
||||
});
|
||||
|
||||
expect(registry.plugins.find((entry) => entry.id === "alpha")?.status).toBe("loaded");
|
||||
});
|
||||
|
||||
it("loads bundled plugins with plugin-sdk imports from an external stage dir", () => {
|
||||
const packageRoot = makeTempDir();
|
||||
const stageDir = makeTempDir();
|
||||
|
||||
@@ -2276,8 +2276,12 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
};
|
||||
const pluginRoot = safeRealpathOrResolve(candidate.rootDir);
|
||||
let runtimePluginRoot = pluginRoot;
|
||||
let runtimeCandidateSource = candidate.source;
|
||||
let runtimeSetupSource = manifestRecord.setupSource;
|
||||
let runtimeCandidateSource =
|
||||
candidate.origin === "bundled" ? safeRealpathOrResolve(candidate.source) : candidate.source;
|
||||
let runtimeSetupSource =
|
||||
candidate.origin === "bundled" && manifestRecord.setupSource
|
||||
? safeRealpathOrResolve(manifestRecord.setupSource)
|
||||
: manifestRecord.setupSource;
|
||||
|
||||
const scopedSetupOnlyChannelPluginRequested =
|
||||
includeSetupOnlyChannelPlugins &&
|
||||
@@ -2381,12 +2385,12 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
});
|
||||
runtimeCandidateSource =
|
||||
remapBundledPluginRuntimePath({
|
||||
source: candidate.source,
|
||||
source: runtimeCandidateSource,
|
||||
pluginRoot,
|
||||
mirroredRoot: runtimePluginRoot,
|
||||
}) ?? candidate.source;
|
||||
}) ?? runtimeCandidateSource;
|
||||
runtimeSetupSource = remapBundledPluginRuntimePath({
|
||||
source: manifestRecord.setupSource,
|
||||
source: runtimeSetupSource,
|
||||
pluginRoot,
|
||||
mirroredRoot: runtimePluginRoot,
|
||||
});
|
||||
@@ -3186,7 +3190,11 @@ export async function loadOpenClawPluginCliRegistry(
|
||||
const pluginRoot = safeRealpathOrResolve(candidate.rootDir);
|
||||
const cliMetadataSource = resolveCliMetadataEntrySource(candidate.rootDir);
|
||||
const sourceForCliMetadata =
|
||||
candidate.origin === "bundled" ? cliMetadataSource : (cliMetadataSource ?? candidate.source);
|
||||
candidate.origin === "bundled"
|
||||
? cliMetadataSource
|
||||
? safeRealpathOrResolve(cliMetadataSource)
|
||||
: safeRealpathOrResolve(candidate.source)
|
||||
: (cliMetadataSource ?? candidate.source);
|
||||
if (!sourceForCliMetadata) {
|
||||
record.status = "loaded";
|
||||
registry.plugins.push(record);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { mkdtempSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
collectBundledPluginRootRuntimeMirrorErrors,
|
||||
collectForbiddenPackContentPaths,
|
||||
collectInstalledBundledPluginRuntimeDepErrors,
|
||||
bundledRuntimeDependencySentinelCandidates,
|
||||
collectRootDistBundledRuntimeMirrors,
|
||||
collectForbiddenPackPaths,
|
||||
collectMissingPackPaths,
|
||||
@@ -673,3 +674,36 @@ describe("collectInstalledBundledPluginRuntimeDepErrors", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("bundledRuntimeDependencySentinelCandidates", () => {
|
||||
it("checks canonical external runtime-deps roots for packed installs", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "release-check-runtime-candidates-"));
|
||||
const packageRoot = join(root, "package");
|
||||
const aliasRoot = join(root, "package-alias");
|
||||
const homeRoot = join(root, "home");
|
||||
try {
|
||||
mkdirSync(join(packageRoot, "dist", "extensions", "browser"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(packageRoot, "package.json"),
|
||||
JSON.stringify({ name: "openclaw", version: "2026.4.24-beta.1" }, null, 2),
|
||||
);
|
||||
symlinkSync(packageRoot, aliasRoot, "dir");
|
||||
|
||||
const candidates = bundledRuntimeDependencySentinelCandidates(
|
||||
aliasRoot,
|
||||
"browser",
|
||||
"playwright-core",
|
||||
{ HOME: homeRoot } as NodeJS.ProcessEnv,
|
||||
);
|
||||
const externalCandidates = candidates.filter(
|
||||
(candidate) =>
|
||||
candidate.startsWith(join(homeRoot, ".openclaw", "plugin-runtime-deps")) &&
|
||||
candidate.endsWith(join("node_modules", "playwright-core", "package.json")),
|
||||
);
|
||||
|
||||
expect(externalCandidates.length).toBeGreaterThanOrEqual(2);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user