mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-27 00:52:05 +00:00
Plugins: extract loader runtime factories
This commit is contained in:
@@ -32,7 +32,9 @@ This is an implementation checklist, not a future-design spec.
|
||||
| Resolved static registry | flat rows in `src/plugins/manifest-registry.ts` | `src/extension-host/resolved-registry.ts` | `partial` | Manifest records now carry `resolvedExtension`; a host-owned resolved registry view exists for static consumers. |
|
||||
| Manifest/package metadata loading | `src/plugins/manifest.ts`, `src/plugins/discovery.ts`, `src/plugins/install.ts` | `src/extension-host/schema.ts` and `src/extension-host/manifest-registry.ts` | `partial` | Package metadata parsing is routed through host schema helpers; legacy loader flow still supplies the source manifests. |
|
||||
| Loader SDK alias compatibility | `src/plugins/loader.ts` | `src/extension-host/loader-compat.ts` | `partial` | Plugin-SDK alias candidate ordering, alias-file resolution, and scoped alias-map construction now live in host-owned loader compatibility helpers. |
|
||||
| Loader alias-wired module loader creation | `src/plugins/loader.ts` | `src/extension-host/loader-module-loader.ts` | `partial` | Lazy Jiti creation and SDK-alias-wired module loading now delegate through a host-owned loader-module-loader helper. |
|
||||
| Loader cache key and registry cache control | `src/plugins/loader.ts` | `src/extension-host/loader-cache.ts` | `partial` | Cache-key construction, LRU registry cache reads and writes, and cache clearing now delegate through host-owned loader-cache helpers while preserving the current cache shape and cap. |
|
||||
| Loader lazy runtime proxy creation | `src/plugins/loader.ts` | `src/extension-host/loader-runtime-proxy.ts` | `partial` | Lazy plugin runtime creation now delegates through a host-owned loader-runtime-proxy helper instead of remaining inline in the orchestrator. |
|
||||
| Loader provenance and duplicate-order policy | `src/plugins/loader.ts` | `src/extension-host/loader-policy.ts` | `partial` | Plugin-record creation, duplicate precedence, and provenance indexing now live in host-owned loader-policy helpers. |
|
||||
| Loader discovery policy results | mixed inside `src/extension-host/loader-policy.ts` and `src/extension-host/loader-orchestrator.ts` | `src/extension-host/loader-discovery-policy.ts` | `partial` | Open-allowlist discovery warnings now resolve through explicit host-owned discovery-policy results before the orchestrator logs them. |
|
||||
| Loader initial candidate planning and record creation | `src/plugins/loader.ts` | `src/extension-host/loader-records.ts` | `partial` | Duplicate detection, initial record creation, manifest metadata attachment, and first-pass enable-state planning now delegate through host-owned loader-records helpers. |
|
||||
|
||||
48
src/extension-host/loader-module-loader.test.ts
Normal file
48
src/extension-host/loader-module-loader.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createExtensionHostModuleLoader } from "./loader-module-loader.js";
|
||||
|
||||
describe("extension host module loader", () => {
|
||||
it("creates the jiti loader lazily and reuses it", () => {
|
||||
let createCount = 0;
|
||||
const loadedSources: string[] = [];
|
||||
|
||||
const loadModule = createExtensionHostModuleLoader({
|
||||
importMetaUrl: "file:///test-loader.ts",
|
||||
createJitiLoader: (_url, options) => {
|
||||
createCount += 1;
|
||||
expect(options.alias).toEqual({
|
||||
"openclaw/plugin-sdk": "/sdk/index.ts",
|
||||
"openclaw/plugin-sdk/telegram": "/sdk/telegram.ts",
|
||||
});
|
||||
return ((safeSource: string) => {
|
||||
loadedSources.push(safeSource);
|
||||
return { safeSource };
|
||||
}) as never;
|
||||
},
|
||||
resolvePluginSdkAliasFn: () => "/sdk/index.ts",
|
||||
resolvePluginSdkScopedAliasMapFn: () => ({
|
||||
"openclaw/plugin-sdk/telegram": "/sdk/telegram.ts",
|
||||
}),
|
||||
});
|
||||
|
||||
expect(createCount).toBe(0);
|
||||
expect(loadModule("/plugins/one.ts")).toEqual({ safeSource: "/plugins/one.ts" });
|
||||
expect(loadModule("/plugins/two.ts")).toEqual({ safeSource: "/plugins/two.ts" });
|
||||
expect(createCount).toBe(1);
|
||||
expect(loadedSources).toEqual(["/plugins/one.ts", "/plugins/two.ts"]);
|
||||
});
|
||||
|
||||
it("omits alias config when no aliases resolve", () => {
|
||||
const loadModule = createExtensionHostModuleLoader({
|
||||
importMetaUrl: "file:///test-loader.ts",
|
||||
createJitiLoader: (_url, options) => {
|
||||
expect(options.alias).toBeUndefined();
|
||||
return ((safeSource: string) => ({ safeSource })) as never;
|
||||
},
|
||||
resolvePluginSdkAliasFn: () => null,
|
||||
resolvePluginSdkScopedAliasMapFn: () => ({}),
|
||||
});
|
||||
|
||||
expect(loadModule("/plugins/demo.ts")).toEqual({ safeSource: "/plugins/demo.ts" });
|
||||
});
|
||||
});
|
||||
44
src/extension-host/loader-module-loader.ts
Normal file
44
src/extension-host/loader-module-loader.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { createJiti } from "jiti";
|
||||
import type { OpenClawPluginModule } from "../plugins/types.js";
|
||||
import { resolvePluginSdkAlias, resolvePluginSdkScopedAliasMap } from "./loader-compat.js";
|
||||
|
||||
type JitiLoaderFactory = typeof createJiti;
|
||||
type JitiLoader = ReturnType<JitiLoaderFactory>;
|
||||
|
||||
export function createExtensionHostModuleLoader(
|
||||
params: {
|
||||
createJitiLoader?: JitiLoaderFactory;
|
||||
importMetaUrl?: string;
|
||||
resolvePluginSdkAliasFn?: typeof resolvePluginSdkAlias;
|
||||
resolvePluginSdkScopedAliasMapFn?: typeof resolvePluginSdkScopedAliasMap;
|
||||
} = {},
|
||||
): (safeSource: string) => OpenClawPluginModule {
|
||||
const createJitiLoader = params.createJitiLoader ?? createJiti;
|
||||
const importMetaUrl = params.importMetaUrl ?? import.meta.url;
|
||||
const resolvePluginSdkAliasFn = params.resolvePluginSdkAliasFn ?? resolvePluginSdkAlias;
|
||||
const resolvePluginSdkScopedAliasMapFn =
|
||||
params.resolvePluginSdkScopedAliasMapFn ?? resolvePluginSdkScopedAliasMap;
|
||||
|
||||
let jitiLoader: JitiLoader | null = null;
|
||||
|
||||
const getJiti = (): JitiLoader => {
|
||||
if (jitiLoader) {
|
||||
return jitiLoader;
|
||||
}
|
||||
const pluginSdkAlias = resolvePluginSdkAliasFn();
|
||||
const aliasMap = {
|
||||
...(pluginSdkAlias ? { "openclaw/plugin-sdk": pluginSdkAlias } : {}),
|
||||
...resolvePluginSdkScopedAliasMapFn(),
|
||||
};
|
||||
jitiLoader = createJitiLoader(importMetaUrl, {
|
||||
interopDefault: true,
|
||||
extensions: [".ts", ".tsx", ".mts", ".cts", ".mtsx", ".ctsx", ".js", ".mjs", ".cjs", ".json"],
|
||||
...(Object.keys(aliasMap).length > 0 ? { alias: aliasMap } : {}),
|
||||
});
|
||||
return jitiLoader;
|
||||
};
|
||||
|
||||
return (safeSource: string): OpenClawPluginModule => {
|
||||
return getJiti()(safeSource) as OpenClawPluginModule;
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { createJiti } from "jiti";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { activateExtensionHostRegistry } from "../extension-host/activation.js";
|
||||
import {
|
||||
@@ -20,10 +19,10 @@ import { discoverOpenClawPlugins } from "../plugins/discovery.js";
|
||||
import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js";
|
||||
import { createPluginRegistry, type PluginRegistry } from "../plugins/registry.js";
|
||||
import { createPluginRuntime, type CreatePluginRuntimeOptions } from "../plugins/runtime/index.js";
|
||||
import type { PluginRuntime } from "../plugins/runtime/types.js";
|
||||
import type { OpenClawPluginModule, PluginLogger } from "../plugins/types.js";
|
||||
import { resolvePluginSdkAlias, resolvePluginSdkScopedAliasMap } from "./loader-compat.js";
|
||||
import type { PluginLogger } from "../plugins/types.js";
|
||||
import { resolveExtensionHostDiscoveryPolicy } from "./loader-discovery-policy.js";
|
||||
import { createExtensionHostModuleLoader } from "./loader-module-loader.js";
|
||||
import { createExtensionHostLazyRuntime } from "./loader-runtime-proxy.js";
|
||||
import {
|
||||
createExtensionHostLoaderSession,
|
||||
finalizeExtensionHostLoaderSession,
|
||||
@@ -78,38 +77,9 @@ export function loadExtensionHostPluginRegistry(
|
||||
// Clear previously registered plugin commands before reloading.
|
||||
clearPluginCommands();
|
||||
|
||||
// Lazily initialize the runtime so startup paths that discover/skip plugins do
|
||||
// not eagerly load every channel runtime dependency.
|
||||
let resolvedRuntime: PluginRuntime | null = null;
|
||||
const resolveRuntime = (): PluginRuntime => {
|
||||
resolvedRuntime ??= createPluginRuntime(options.runtimeOptions);
|
||||
return resolvedRuntime;
|
||||
};
|
||||
const runtime = new Proxy({} as PluginRuntime, {
|
||||
get(_target, prop, receiver) {
|
||||
return Reflect.get(resolveRuntime(), prop, receiver);
|
||||
},
|
||||
set(_target, prop, value, receiver) {
|
||||
return Reflect.set(resolveRuntime(), prop, value, receiver);
|
||||
},
|
||||
has(_target, prop) {
|
||||
return Reflect.has(resolveRuntime(), prop);
|
||||
},
|
||||
ownKeys() {
|
||||
return Reflect.ownKeys(resolveRuntime() as object);
|
||||
},
|
||||
getOwnPropertyDescriptor(_target, prop) {
|
||||
return Reflect.getOwnPropertyDescriptor(resolveRuntime() as object, prop);
|
||||
},
|
||||
defineProperty(_target, prop, attributes) {
|
||||
return Reflect.defineProperty(resolveRuntime() as object, prop, attributes);
|
||||
},
|
||||
deleteProperty(_target, prop) {
|
||||
return Reflect.deleteProperty(resolveRuntime() as object, prop);
|
||||
},
|
||||
getPrototypeOf() {
|
||||
return Reflect.getPrototypeOf(resolveRuntime() as object);
|
||||
},
|
||||
const runtime = createExtensionHostLazyRuntime({
|
||||
runtimeOptions: options.runtimeOptions,
|
||||
createRuntime: createPluginRuntime,
|
||||
});
|
||||
const { registry, createApi } = createPluginRegistry({
|
||||
logger,
|
||||
@@ -152,28 +122,7 @@ export function loadExtensionHostPluginRegistry(
|
||||
env,
|
||||
});
|
||||
|
||||
// Lazy: avoid creating the Jiti loader when all plugins are disabled (common in unit tests).
|
||||
let jitiLoader: ReturnType<typeof createJiti> | null = null;
|
||||
const getJiti = () => {
|
||||
if (jitiLoader) {
|
||||
return jitiLoader;
|
||||
}
|
||||
const pluginSdkAlias = resolvePluginSdkAlias();
|
||||
const aliasMap = {
|
||||
...(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,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
return jitiLoader;
|
||||
};
|
||||
const loadModule = createExtensionHostModuleLoader();
|
||||
|
||||
const manifestByRoot = new Map(
|
||||
manifestRegistry.plugins.map((record) => [record.rootDir, record]),
|
||||
@@ -213,7 +162,7 @@ export function loadExtensionHostPluginRegistry(
|
||||
rootConfig: cfg,
|
||||
validateOnly,
|
||||
createApi,
|
||||
loadModule: (safeSource) => getJiti()(safeSource) as OpenClawPluginModule,
|
||||
loadModule,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
33
src/extension-host/loader-runtime-proxy.test.ts
Normal file
33
src/extension-host/loader-runtime-proxy.test.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createExtensionHostLazyRuntime } from "./loader-runtime-proxy.js";
|
||||
|
||||
describe("extension host loader runtime proxy", () => {
|
||||
it("creates the runtime lazily on first access", () => {
|
||||
let createCount = 0;
|
||||
const runtime = createExtensionHostLazyRuntime({
|
||||
createRuntime: () => {
|
||||
createCount += 1;
|
||||
return { value: 1 } as never;
|
||||
},
|
||||
});
|
||||
|
||||
expect(createCount).toBe(0);
|
||||
expect((runtime as never as { value: number }).value).toBe(1);
|
||||
expect(createCount).toBe(1);
|
||||
});
|
||||
|
||||
it("reuses the same runtime instance across proxy operations", () => {
|
||||
let createCount = 0;
|
||||
const runtime = createExtensionHostLazyRuntime({
|
||||
createRuntime: () => {
|
||||
createCount += 1;
|
||||
return { value: 1 } as never;
|
||||
},
|
||||
});
|
||||
|
||||
expect("value" in (runtime as object)).toBe(true);
|
||||
expect(Object.keys(runtime as object)).toEqual(["value"]);
|
||||
expect((runtime as never as { value: number }).value).toBe(1);
|
||||
expect(createCount).toBe(1);
|
||||
});
|
||||
});
|
||||
39
src/extension-host/loader-runtime-proxy.ts
Normal file
39
src/extension-host/loader-runtime-proxy.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { PluginRuntime } from "../plugins/runtime/types.js";
|
||||
|
||||
export function createExtensionHostLazyRuntime<TOptions>(params: {
|
||||
runtimeOptions?: TOptions;
|
||||
createRuntime: (runtimeOptions?: TOptions) => PluginRuntime;
|
||||
}): PluginRuntime {
|
||||
let resolvedRuntime: PluginRuntime | null = null;
|
||||
const resolveRuntime = (): PluginRuntime => {
|
||||
resolvedRuntime ??= params.createRuntime(params.runtimeOptions);
|
||||
return resolvedRuntime;
|
||||
};
|
||||
|
||||
return new Proxy({} as PluginRuntime, {
|
||||
get(_target, prop, receiver) {
|
||||
return Reflect.get(resolveRuntime(), prop, receiver);
|
||||
},
|
||||
set(_target, prop, value, receiver) {
|
||||
return Reflect.set(resolveRuntime(), prop, value, receiver);
|
||||
},
|
||||
has(_target, prop) {
|
||||
return Reflect.has(resolveRuntime(), prop);
|
||||
},
|
||||
ownKeys() {
|
||||
return Reflect.ownKeys(resolveRuntime() as object);
|
||||
},
|
||||
getOwnPropertyDescriptor(_target, prop) {
|
||||
return Reflect.getOwnPropertyDescriptor(resolveRuntime() as object, prop);
|
||||
},
|
||||
defineProperty(_target, prop, attributes) {
|
||||
return Reflect.defineProperty(resolveRuntime() as object, prop, attributes);
|
||||
},
|
||||
deleteProperty(_target, prop) {
|
||||
return Reflect.deleteProperty(resolveRuntime() as object, prop);
|
||||
},
|
||||
getPrototypeOf() {
|
||||
return Reflect.getPrototypeOf(resolveRuntime() as object);
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user