mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 15:40:44 +00:00
fix(plugins): load mirrored runtime deps through ESM-safe aliases
This commit is contained in:
@@ -92,7 +92,7 @@ describe("channel plugin module loader helpers", () => {
|
||||
expect(createJiti).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps Windows dist loads off Jiti native import", async () => {
|
||||
it("uses native Jiti import for Windows dist loads", async () => {
|
||||
const createJiti = vi.fn(() => vi.fn(() => ({ ok: true })));
|
||||
vi.doMock("jiti", () => ({
|
||||
createJiti,
|
||||
@@ -119,7 +119,7 @@ describe("channel plugin module loader helpers", () => {
|
||||
expect(createJiti).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
tryNative: false,
|
||||
tryNative: true,
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
|
||||
@@ -108,7 +108,7 @@ describe("plugin-sdk facade loader", () => {
|
||||
expect(listImportedFacadeRuntimeIds()).toEqual(["demo"]);
|
||||
});
|
||||
|
||||
it("keeps Windows dist facade loads off Jiti native import", () => {
|
||||
it("uses native Jiti import for Windows dist facade loads", () => {
|
||||
const dir = createTempDirSync("openclaw-facade-loader-windows-dist-");
|
||||
const bundledPluginsDir = path.join(dir, "dist");
|
||||
fs.mkdirSync(path.join(bundledPluginsDir, "demo"), { recursive: true });
|
||||
@@ -139,7 +139,7 @@ describe("plugin-sdk facade loader", () => {
|
||||
expect(createJitiCalls[0]?.[0]).toEqual(expect.any(String));
|
||||
expect(createJitiCalls[0]?.[1]).toEqual(
|
||||
expect.objectContaining({
|
||||
tryNative: false,
|
||||
tryNative: true,
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
|
||||
@@ -122,6 +122,7 @@ function prepareBundledPluginRuntimeDistMirror(params: {
|
||||
const mirrorDistRoot = path.join(params.installRoot, "dist");
|
||||
const mirrorExtensionsRoot = path.join(mirrorDistRoot, "extensions");
|
||||
fs.mkdirSync(mirrorExtensionsRoot, { recursive: true, mode: 0o755 });
|
||||
ensureBundledRuntimeDistPackageJson(mirrorDistRoot);
|
||||
for (const entry of fs.readdirSync(sourceDistRoot, { withFileTypes: true })) {
|
||||
if (entry.name === "extensions") {
|
||||
continue;
|
||||
@@ -145,6 +146,14 @@ function prepareBundledPluginRuntimeDistMirror(params: {
|
||||
return mirrorExtensionsRoot;
|
||||
}
|
||||
|
||||
function ensureBundledRuntimeDistPackageJson(mirrorDistRoot: string): void {
|
||||
const packageJsonPath = path.join(mirrorDistRoot, "package.json");
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
return;
|
||||
}
|
||||
writeRuntimeJsonFile(packageJsonPath, { type: "module" });
|
||||
}
|
||||
|
||||
function copyBundledPluginRuntimeRoot(sourceRoot: string, targetRoot: string): void {
|
||||
fs.mkdirSync(targetRoot, { recursive: true, mode: 0o755 });
|
||||
for (const entry of fs.readdirSync(sourceRoot, { withFileTypes: true })) {
|
||||
|
||||
@@ -34,7 +34,7 @@ describe("doctor-contract-registry getJiti", () => {
|
||||
clearPluginDoctorContractRegistryCache();
|
||||
});
|
||||
|
||||
it("disables native jiti loading on Windows for contract-api modules", () => {
|
||||
it("uses native jiti loading on Windows for contract-api modules", () => {
|
||||
const pluginRoot = makeTempDir();
|
||||
fs.writeFileSync(path.join(pluginRoot, "contract-api.js"), "export default {};\n", "utf-8");
|
||||
mocks.loadPluginManifestRegistry.mockReturnValue({
|
||||
@@ -56,7 +56,7 @@ describe("doctor-contract-registry getJiti", () => {
|
||||
expect(mocks.createJiti.mock.calls[0]?.[0]).toBe(path.join(pluginRoot, "contract-api.js"));
|
||||
expect(mocks.createJiti.mock.calls[0]?.[1]).toEqual(
|
||||
expect.objectContaining({
|
||||
tryNative: false,
|
||||
tryNative: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1582,7 +1582,11 @@ module.exports = {
|
||||
name: "root-support",
|
||||
version: "1.0.0",
|
||||
type: "module",
|
||||
exports: "./index.js",
|
||||
exports: {
|
||||
".": {
|
||||
import: "./index.js",
|
||||
},
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
@@ -1602,10 +1606,11 @@ module.exports = {
|
||||
path.join(pluginRoot, "index.js"),
|
||||
[
|
||||
`import { marker } from "../../manifest-support.js";`,
|
||||
`import externalRuntime from "external-runtime";`,
|
||||
`export default {`,
|
||||
` id: "alpha",`,
|
||||
` register(api) {`,
|
||||
` api.registerCommand({ name: "root-support", handler: () => marker });`,
|
||||
` api.registerCommand({ name: "root-support", handler: () => [marker, externalRuntime.marker].join(":") });`,
|
||||
` },`,
|
||||
`};`,
|
||||
"",
|
||||
@@ -1619,6 +1624,9 @@ module.exports = {
|
||||
name: "@openclaw/alpha",
|
||||
version: "1.0.0",
|
||||
type: "module",
|
||||
dependencies: {
|
||||
"external-runtime": "1.0.0",
|
||||
},
|
||||
openclaw: { extensions: ["./index.js"] },
|
||||
},
|
||||
null,
|
||||
@@ -1650,6 +1658,29 @@ module.exports = {
|
||||
registry = loadOpenClawPlugins({
|
||||
cache: false,
|
||||
config: { plugins: { enabled: true } },
|
||||
bundledRuntimeDepsInstaller: ({ installRoot }) => {
|
||||
const depRoot = path.join(installRoot, "node_modules", "external-runtime");
|
||||
fs.mkdirSync(depRoot, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(depRoot, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "external-runtime",
|
||||
version: "1.0.0",
|
||||
type: "module",
|
||||
exports: {
|
||||
".": {
|
||||
import: "./index.js",
|
||||
},
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(depRoot, "index.js"),
|
||||
"export default { marker: 'external-ok' };\n",
|
||||
"utf-8",
|
||||
);
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
symlinkSync.mockRestore();
|
||||
|
||||
@@ -120,6 +120,7 @@ import {
|
||||
resolvePluginSdkAliasFile,
|
||||
resolvePluginRuntimeModulePath,
|
||||
resolvePluginSdkScopedAliasMap,
|
||||
normalizeJitiAliasTargetPath,
|
||||
shouldPreferNativeJiti,
|
||||
} from "./sdk-alias.js";
|
||||
import { hasKind, kindsEqual } from "./slots.js";
|
||||
@@ -257,7 +258,7 @@ export function clearPluginLoaderCache(): void {
|
||||
inFlightPluginRegistryLoads.clear();
|
||||
openAllowlistWarningCache.clear();
|
||||
clearBundledRuntimeDependencyNodePaths();
|
||||
registeredBundledRuntimeDepMirrorRoots.clear();
|
||||
bundledRuntimeDependencyJitiAliases.clear();
|
||||
clearAgentHarnesses();
|
||||
clearPluginCommands();
|
||||
clearCompactionProviders();
|
||||
@@ -457,16 +458,133 @@ function toSafeImportPath(specifier: string): string {
|
||||
return specifier;
|
||||
}
|
||||
|
||||
type RuntimeDependencyPackageJson = {
|
||||
dependencies?: Record<string, unknown>;
|
||||
optionalDependencies?: Record<string, unknown>;
|
||||
peerDependencies?: Record<string, unknown>;
|
||||
exports?: unknown;
|
||||
module?: string;
|
||||
main?: string;
|
||||
};
|
||||
|
||||
const bundledRuntimeDependencyJitiAliases = new Map<string, string>();
|
||||
|
||||
function readRuntimeDependencyPackageJson(
|
||||
packageJsonPath: string,
|
||||
): RuntimeDependencyPackageJson | null {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) as RuntimeDependencyPackageJson;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function collectRuntimeDependencyNames(pkg: RuntimeDependencyPackageJson): string[] {
|
||||
return [
|
||||
...Object.keys(pkg.dependencies ?? {}),
|
||||
...Object.keys(pkg.optionalDependencies ?? {}),
|
||||
...Object.keys(pkg.peerDependencies ?? {}),
|
||||
].toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
function resolveRuntimePackageImportTarget(exportsField: unknown): string | null {
|
||||
if (typeof exportsField === "string") {
|
||||
return exportsField;
|
||||
}
|
||||
if (Array.isArray(exportsField)) {
|
||||
for (const entry of exportsField) {
|
||||
const resolved = resolveRuntimePackageImportTarget(entry);
|
||||
if (resolved) {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (!exportsField || typeof exportsField !== "object" || Array.isArray(exportsField)) {
|
||||
return null;
|
||||
}
|
||||
const record = exportsField as Record<string, unknown>;
|
||||
if (Object.prototype.hasOwnProperty.call(record, ".")) {
|
||||
return resolveRuntimePackageImportTarget(record["."]);
|
||||
}
|
||||
for (const condition of ["import", "node", "default"] as const) {
|
||||
const resolved = resolveRuntimePackageImportTarget(record[condition]);
|
||||
if (resolved) {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function registerBundledRuntimeDependencyJitiAliases(rootDir: string): void {
|
||||
const rootPackageJson = readRuntimeDependencyPackageJson(path.join(rootDir, "package.json"));
|
||||
if (!rootPackageJson) {
|
||||
return;
|
||||
}
|
||||
for (const dependencyName of collectRuntimeDependencyNames(rootPackageJson)) {
|
||||
const dependencyPackageJsonPath = path.join(
|
||||
rootDir,
|
||||
"node_modules",
|
||||
...dependencyName.split("/"),
|
||||
"package.json",
|
||||
);
|
||||
const dependencyPackageJson = readRuntimeDependencyPackageJson(dependencyPackageJsonPath);
|
||||
if (!dependencyPackageJson) {
|
||||
continue;
|
||||
}
|
||||
const entry =
|
||||
resolveRuntimePackageImportTarget(dependencyPackageJson.exports) ??
|
||||
dependencyPackageJson.module ??
|
||||
dependencyPackageJson.main;
|
||||
if (!entry || entry.startsWith("#")) {
|
||||
continue;
|
||||
}
|
||||
const dependencyRoot = path.dirname(dependencyPackageJsonPath);
|
||||
const targetPath = path.resolve(dependencyRoot, entry);
|
||||
if (!isPathInside(dependencyRoot, targetPath) || !fs.existsSync(targetPath)) {
|
||||
continue;
|
||||
}
|
||||
bundledRuntimeDependencyJitiAliases.set(
|
||||
dependencyName,
|
||||
normalizeJitiAliasTargetPath(targetPath),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveBundledRuntimeDependencyJitiAliasMap(): Record<string, string> | undefined {
|
||||
if (bundledRuntimeDependencyJitiAliases.size === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return Object.fromEntries(
|
||||
[...bundledRuntimeDependencyJitiAliases.entries()].toSorted(([left], [right]) =>
|
||||
left.localeCompare(right),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function createPluginJitiLoader(options: Pick<PluginLoadOptions, "pluginSdkResolution">) {
|
||||
const jitiLoaders: PluginJitiLoaderCache = new Map();
|
||||
return (modulePath: string) => {
|
||||
const tryNative =
|
||||
shouldPreferNativeJiti(modulePath) && !isBundledRuntimeDependencyMirrorPath(modulePath);
|
||||
const tryNative = shouldPreferNativeJiti(modulePath);
|
||||
const runtimeAliasMap = resolveBundledRuntimeDependencyJitiAliasMap();
|
||||
return getCachedPluginJitiLoader({
|
||||
cache: jitiLoaders,
|
||||
modulePath,
|
||||
importerUrl: import.meta.url,
|
||||
jitiFilename: modulePath,
|
||||
...(runtimeAliasMap
|
||||
? {
|
||||
aliasMap: {
|
||||
...buildPluginLoaderAliasMap(
|
||||
modulePath,
|
||||
process.argv[1],
|
||||
import.meta.url,
|
||||
options.pluginSdkResolution,
|
||||
),
|
||||
...runtimeAliasMap,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
pluginSdkResolution: options.pluginSdkResolution,
|
||||
// Source .ts runtime shims import sibling ".js" specifiers that only exist
|
||||
// after build. Disable native loading for source entries so Jiti rewrites
|
||||
@@ -487,25 +605,6 @@ function resolveCanonicalDistRuntimeSource(source: string): string {
|
||||
return fs.existsSync(candidate) ? candidate : source;
|
||||
}
|
||||
|
||||
const registeredBundledRuntimeDepMirrorRoots = new Set<string>();
|
||||
|
||||
function isBundledRuntimeDependencyMirrorPath(modulePath: string): boolean {
|
||||
const resolvedModulePath = path.resolve(modulePath);
|
||||
for (const installRoot of registeredBundledRuntimeDepMirrorRoots) {
|
||||
if (
|
||||
resolvedModulePath === installRoot ||
|
||||
resolvedModulePath.startsWith(`${installRoot}${path.sep}`)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function registerBundledRuntimeDependencyMirrorRoot(installRoot: string): void {
|
||||
registeredBundledRuntimeDepMirrorRoots.add(path.resolve(installRoot));
|
||||
}
|
||||
|
||||
function mirrorBundledPluginRuntimeRoot(params: {
|
||||
pluginId: string;
|
||||
pluginRoot: string;
|
||||
@@ -551,6 +650,7 @@ function prepareBundledPluginRuntimeDistMirror(params: {
|
||||
const mirrorDistRoot = path.join(params.installRoot, sourceDistRootName);
|
||||
const mirrorExtensionsRoot = path.join(mirrorDistRoot, "extensions");
|
||||
fs.mkdirSync(mirrorExtensionsRoot, { recursive: true, mode: 0o755 });
|
||||
ensureBundledRuntimeDistPackageJson(mirrorDistRoot);
|
||||
for (const entry of fs.readdirSync(sourceDistRoot, { withFileTypes: true })) {
|
||||
if (entry.name === "extensions") {
|
||||
continue;
|
||||
@@ -592,6 +692,14 @@ function prepareBundledPluginRuntimeDistMirror(params: {
|
||||
return mirrorExtensionsRoot;
|
||||
}
|
||||
|
||||
function ensureBundledRuntimeDistPackageJson(mirrorDistRoot: string): void {
|
||||
const packageJsonPath = path.join(mirrorDistRoot, "package.json");
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
return;
|
||||
}
|
||||
writeRuntimeJsonFile(packageJsonPath, { type: "module" });
|
||||
}
|
||||
|
||||
function copyBundledPluginRuntimeRoot(sourceRoot: string, targetRoot: string): void {
|
||||
fs.mkdirSync(targetRoot, { recursive: true, mode: 0o755 });
|
||||
for (const entry of fs.readdirSync(sourceRoot, { withFileTypes: true })) {
|
||||
@@ -2358,9 +2466,10 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
const packageRoot = resolveBundledRuntimeDependencyPackageRoot(pluginRoot);
|
||||
if (packageRoot) {
|
||||
registerBundledRuntimeDependencyNodePath(packageRoot);
|
||||
registerBundledRuntimeDependencyJitiAliases(packageRoot);
|
||||
}
|
||||
registerBundledRuntimeDependencyNodePath(installRoot);
|
||||
registerBundledRuntimeDependencyMirrorRoot(installRoot);
|
||||
registerBundledRuntimeDependencyJitiAliases(installRoot);
|
||||
runtimePluginRoot = mirrorBundledPluginRuntimeRoot({
|
||||
pluginId: record.id,
|
||||
pluginRoot,
|
||||
|
||||
@@ -28,7 +28,7 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("bundled plugin public surface loader", () => {
|
||||
it("keeps Windows dist public artifact loads off Jiti native import", async () => {
|
||||
it("uses native Jiti import for Windows dist public artifact loads", async () => {
|
||||
const createJiti = vi.fn(() => vi.fn(() => ({ marker: "windows-dist-ok" })));
|
||||
vi.doMock("jiti", () => ({
|
||||
createJiti,
|
||||
@@ -56,7 +56,7 @@ describe("bundled plugin public surface loader", () => {
|
||||
expect(createJiti).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
tryNative: false,
|
||||
tryNative: true,
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
|
||||
@@ -870,7 +870,7 @@ describe("plugin sdk alias helpers", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("disables native Jiti loads on Windows even for built JavaScript entries", () => {
|
||||
it("prefers native Jiti loads on Windows for built JavaScript entries", () => {
|
||||
const originalPlatform = process.platform;
|
||||
Object.defineProperty(process, "platform", {
|
||||
configurable: true,
|
||||
@@ -878,9 +878,9 @@ describe("plugin sdk alias helpers", () => {
|
||||
});
|
||||
|
||||
try {
|
||||
expect(shouldPreferNativeJiti("/repo/dist/plugins/runtime/index.js")).toBe(false);
|
||||
expect(shouldPreferNativeJiti("/repo/dist/plugins/runtime/index.js")).toBe(true);
|
||||
expect(shouldPreferNativeJiti(`/repo/${bundledDistPluginFile("browser", "index.js")}`)).toBe(
|
||||
false,
|
||||
true,
|
||||
);
|
||||
} finally {
|
||||
Object.defineProperty(process, "platform", {
|
||||
@@ -890,7 +890,7 @@ describe("plugin sdk alias helpers", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps plugin loader dist shortcuts off on Windows", () => {
|
||||
it("keeps plugin loader dist shortcuts native on Windows", () => {
|
||||
const originalPlatform = process.platform;
|
||||
Object.defineProperty(process, "platform", {
|
||||
configurable: true,
|
||||
@@ -902,7 +902,7 @@ describe("plugin sdk alias helpers", () => {
|
||||
resolvePluginLoaderJitiTryNative(`/repo/${bundledDistPluginFile("browser", "index.js")}`, {
|
||||
preferBuiltDist: true,
|
||||
}),
|
||||
).toBe(false);
|
||||
).toBe(true);
|
||||
expect(
|
||||
resolvePluginLoaderJitiTryNative(`/repo/${bundledDistPluginFile("browser", "helper.ts")}`, {
|
||||
preferBuiltDist: true,
|
||||
@@ -918,7 +918,7 @@ describe("plugin sdk alias helpers", () => {
|
||||
|
||||
it("prefers native jiti for bundled plugin dist .js modules, keeps .ts on aliased path", () => {
|
||||
// Built .js/.mjs/.cjs files under dist/extensions/ should now delegate
|
||||
// to shouldPreferNativeJiti() — which returns true on Linux/macOS for
|
||||
// to shouldPreferNativeJiti() — which returns true on Node for
|
||||
// compiled artifacts, avoiding the slow jiti transform path.
|
||||
expect(
|
||||
resolvePluginLoaderJitiTryNative(`/repo/${bundledDistPluginFile("browser", "index.js")}`, {
|
||||
|
||||
@@ -695,7 +695,7 @@ export function buildPluginLoaderJitiOptions(aliasMap: Record<string, string>) {
|
||||
|
||||
function supportsNativeJitiRuntime(): boolean {
|
||||
const versions = process.versions as { bun?: string };
|
||||
return typeof versions.bun !== "string" && process.platform !== "win32";
|
||||
return typeof versions.bun !== "string";
|
||||
}
|
||||
|
||||
function isBundledPluginDistModulePath(modulePath: string): boolean {
|
||||
|
||||
@@ -158,7 +158,7 @@ describe("setup-registry getJiti", () => {
|
||||
clearPluginSetupRegistryCache();
|
||||
});
|
||||
|
||||
it("disables native jiti loading on Windows for setup-api modules", () => {
|
||||
it("uses native jiti loading on Windows for setup-api modules", () => {
|
||||
const pluginRoot = makeTempDir();
|
||||
fs.writeFileSync(path.join(pluginRoot, "setup-api.js"), "export default {};\n", "utf-8");
|
||||
mocks.loadPluginManifestRegistry.mockReturnValue({
|
||||
@@ -180,7 +180,7 @@ describe("setup-registry getJiti", () => {
|
||||
expect(mocks.createJiti.mock.calls[0]?.[0]).toBe(path.join(pluginRoot, "setup-api.js"));
|
||||
expect(mocks.createJiti.mock.calls[0]?.[1]).toEqual(
|
||||
expect.objectContaining({
|
||||
tryNative: false,
|
||||
tryNative: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user