refactor: collapse plugin loader native fallbacks

This commit is contained in:
Peter Steinberger
2026-05-01 22:03:18 +01:00
parent 82e8518bd7
commit dabddb2165
5 changed files with 22 additions and 97 deletions

View File

@@ -123,7 +123,7 @@ OpenClaw's plugin system has four layers:
Core decides whether a discovered plugin is enabled, disabled, blocked, or selected for an exclusive slot such as memory.
</Step>
<Step title="Runtime loading">
Native OpenClaw plugins are loaded in-process via jiti and register capabilities into a central registry. Compatible bundles are normalized into registry records without importing runtime code.
Native OpenClaw plugins are loaded in-process and register capabilities into a central registry. Packaged JavaScript loads through native `require`; source TypeScript falls back to Jiti. Compatible bundles are normalized into registry records without importing runtime code.
</Step>
<Step title="Surface consumption">
The rest of OpenClaw reads the registry to expose tools, channels, provider setup, hooks, HTTP routes, CLI commands, and services.

View File

@@ -5,27 +5,22 @@ import {
getCachedPluginJitiLoader,
type PluginJitiLoaderCache,
} from "../../plugins/jiti-loader-cache.js";
import { tryNativeRequireJavaScriptModule } from "../../plugins/native-module-require.js";
export { isJavaScriptModulePath } from "../../plugins/native-module-require.js";
function createModuleLoader() {
const jitiLoaders: PluginJitiLoaderCache = new Map();
const jitiLoaders: PluginJitiLoaderCache = new Map();
return (modulePath: string, tryNative?: boolean) => {
return getCachedPluginJitiLoader({
cache: jitiLoaders,
modulePath,
importerUrl: import.meta.url,
argvEntry: process.argv[1],
preferBuiltDist: true,
jitiFilename: import.meta.url,
tryNative,
});
};
function loadModule(modulePath: string, tryNative?: boolean) {
return getCachedPluginJitiLoader({
cache: jitiLoaders,
modulePath,
importerUrl: import.meta.url,
argvEntry: process.argv[1],
preferBuiltDist: true,
jitiFilename: import.meta.url,
tryNative,
});
}
let loadModule = createModuleLoader();
export function resolveCompiledBundledModulePath(modulePath: string): string {
const compiledDistModulePath = modulePath.replace(
`${path.sep}dist-runtime${path.sep}`,
@@ -84,14 +79,5 @@ export function loadChannelPluginModule(params: {
}
const safePath = opened.path;
fs.closeSync(opened.fd);
const shouldTryNative = params.shouldTryNativeRequire?.(safePath);
if (shouldTryNative) {
const nativeModule = tryNativeRequireJavaScriptModule(safePath, {
allowWindows: true,
});
if (nativeModule.ok) {
return nativeModule.moduleExport;
}
}
return loadModule(safePath, shouldTryNative)(safePath);
return loadModule(safePath, params.shouldTryNativeRequire?.(safePath))(safePath);
}

View File

@@ -6,7 +6,6 @@ import type { OpenClawConfig } from "../config/types.js";
import { asNullableRecord } from "../shared/record-coerce.js";
import { getCachedPluginJitiLoader, type PluginJitiLoaderCache } from "./jiti-loader-cache.js";
import type { PluginManifestRegistry } from "./manifest-registry.js";
import { tryNativeRequireJavaScriptModule } from "./native-module-require.js";
import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js";
const CONTRACT_API_EXTENSIONS = [".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"] as const;
@@ -39,20 +38,12 @@ type PluginManifestRegistryRecord = PluginManifestRegistry["plugins"][number];
const jitiLoaders: PluginJitiLoaderCache = new Map();
function getJiti(modulePath: string) {
function loadPluginDoctorContractModule(modulePath: string): PluginDoctorContractModule {
return getCachedPluginJitiLoader({
cache: jitiLoaders,
modulePath,
importerUrl: import.meta.url,
});
}
function loadPluginDoctorContractModule(modulePath: string): PluginDoctorContractModule {
const nativeModule = tryNativeRequireJavaScriptModule(modulePath, { allowWindows: true });
if (nativeModule.ok) {
return nativeModule.moduleExport as PluginDoctorContractModule;
}
return getJiti(modulePath)(modulePath) as PluginDoctorContractModule;
})(modulePath) as PluginDoctorContractModule;
}
function resolveContractApiPath(rootDir: string): string | null {

View File

@@ -36,8 +36,6 @@ describe("bundled plugin public surface loader", () => {
createJiti,
}));
vi.doMock("./native-module-require.js", () => ({
isJavaScriptModulePath: (modulePath: string) =>
modulePath.endsWith(".js") || modulePath.endsWith(".mjs") || modulePath.endsWith(".cjs"),
tryNativeRequireJavaScriptModule: () => ({
ok: true,
moduleExport: { marker: "windows-dist-ok" },
@@ -118,8 +116,6 @@ describe("bundled plugin public surface loader", () => {
createJiti,
}));
vi.doMock("./native-module-require.js", () => ({
isJavaScriptModulePath: (modulePath: string) =>
modulePath.endsWith(".js") || modulePath.endsWith(".mjs") || modulePath.endsWith(".cjs"),
tryNativeRequireJavaScriptModule: (modulePath: string) => ({
ok: true,
moduleExport: { marker: path.basename(path.dirname(modulePath)) },
@@ -129,7 +125,7 @@ describe("bundled plugin public surface loader", () => {
const publicSurfaceLoader = await importFreshModule<
typeof import("./public-surface-loader.js")
>(import.meta.url, "./public-surface-loader.js?scope=shared-bundled-jiti");
>(import.meta.url, "./public-surface-loader.js?scope=bundled-native-public-artifacts");
const tempRoot = createTempDir();
const bundledPluginsDir = path.join(tempRoot, "dist");
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledPluginsDir;

View File

@@ -6,13 +6,8 @@ import { openBoundaryFileSync } from "../infra/boundary-file-read.js";
import { sameFileIdentity } from "../infra/file-identity.js";
import { resolveBundledPluginsDir } from "./bundled-dir.js";
import { getCachedPluginJitiLoader, type PluginJitiLoaderCache } from "./jiti-loader-cache.js";
import { tryNativeRequireJavaScriptModule } from "./native-module-require.js";
import { resolveBundledPluginPublicSurfacePath } from "./public-surface-runtime.js";
import {
isBundledPluginExtensionPath,
resolvePluginLoaderJitiTryNative,
resolveLoaderPackageRoot,
} from "./sdk-alias.js";
import { resolvePluginLoaderJitiTryNative, resolveLoaderPackageRoot } from "./sdk-alias.js";
const OPENCLAW_PACKAGE_ROOT =
resolveLoaderPackageRoot({
@@ -29,7 +24,6 @@ const publicSurfaceLocations = new Map<
} | null
>();
const jitiLoaders: PluginJitiLoaderCache = new Map();
const sharedBundledPublicSurfaceJitiLoaders: PluginJitiLoaderCache = new Map();
function isSourceArtifactPath(modulePath: string): boolean {
switch (path.extname(modulePath).toLowerCase()) {
@@ -95,57 +89,23 @@ function resolvePublicSurfaceLocation(params: {
}
function getJiti(modulePath: string) {
const tryNative = resolvePluginLoaderJitiTryNative(modulePath, { preferBuiltDist: true });
const sharedLoader = getSharedBundledPublicSurfaceJiti(modulePath, tryNative);
if (sharedLoader) {
return sharedLoader;
}
const loader = getCachedPluginJitiLoader({
return getCachedPluginJitiLoader({
cache: jitiLoaders,
modulePath,
importerUrl: import.meta.url,
preferBuiltDist: true,
jitiFilename: import.meta.url,
});
return loader;
}
function loadPublicSurfaceModule(modulePath: string): unknown {
const tryNative = resolvePluginLoaderJitiTryNative(modulePath, { preferBuiltDist: true });
if (tryNative) {
const nativeModule = tryNativeRequireJavaScriptModule(modulePath, { allowWindows: true });
if (nativeModule.ok) {
return nativeModule.moduleExport;
}
}
if (canUseSourceArtifactRequire({ modulePath, tryNative })) {
return sourceArtifactRequire(modulePath);
}
return getJiti(modulePath)(modulePath);
}
function getSharedBundledPublicSurfaceJiti(modulePath: string, tryNative: boolean) {
const bundledPluginsDir = resolveBundledPluginsDir();
if (
!isBundledPluginExtensionPath({
modulePath,
openClawPackageRoot: OPENCLAW_PACKAGE_ROOT,
...(bundledPluginsDir ? { bundledPluginsDir } : {}),
})
) {
return null;
}
const cacheKey = tryNative ? "bundled:native" : "bundled:source";
return getCachedPluginJitiLoader({
cache: sharedBundledPublicSurfaceJitiLoaders,
modulePath,
importerUrl: import.meta.url,
jitiFilename: import.meta.url,
cacheScopeKey: cacheKey,
tryNative,
});
}
// oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- Dynamic public artifact loaders use caller-supplied module surface types.
export function loadBundledPluginPublicArtifactModuleSync<T extends object>(params: {
dirName: string;
@@ -157,21 +117,16 @@ export function loadBundledPluginPublicArtifactModuleSync<T extends object>(para
`Unable to resolve bundled plugin public surface ${params.dirName}/${params.artifactBasename}`,
);
}
const preparedLocation = location;
const cached =
loadedPublicSurfaceModules.get(location.modulePath) ??
loadedPublicSurfaceModules.get(preparedLocation.modulePath);
const cached = loadedPublicSurfaceModules.get(location.modulePath);
if (cached) {
return cached as T;
}
const opened = openBoundaryFileSync({
absolutePath: preparedLocation.modulePath,
rootPath: preparedLocation.boundaryRoot,
absolutePath: location.modulePath,
rootPath: location.boundaryRoot,
boundaryLabel:
preparedLocation.boundaryRoot === OPENCLAW_PACKAGE_ROOT
? "OpenClaw package root"
: "plugin root",
location.boundaryRoot === OPENCLAW_PACKAGE_ROOT ? "OpenClaw package root" : "plugin root",
rejectHardlinks: true,
});
if (!opened.ok) {
@@ -193,7 +148,6 @@ export function loadBundledPluginPublicArtifactModuleSync<T extends object>(para
const sentinel = {} as T;
loadedPublicSurfaceModules.set(location.modulePath, sentinel);
loadedPublicSurfaceModules.set(preparedLocation.modulePath, sentinel);
loadedPublicSurfaceModules.set(validatedPath, sentinel);
try {
const loaded = loadPublicSurfaceModule(validatedPath) as T;
@@ -201,7 +155,6 @@ export function loadBundledPluginPublicArtifactModuleSync<T extends object>(para
return sentinel;
} catch (error) {
loadedPublicSurfaceModules.delete(location.modulePath);
loadedPublicSurfaceModules.delete(preparedLocation.modulePath);
loadedPublicSurfaceModules.delete(validatedPath);
throw error;
}
@@ -218,5 +171,4 @@ export function resetBundledPluginPublicArtifactLoaderForTest(): void {
loadedPublicSurfaceModules.clear();
publicSurfaceLocations.clear();
jitiLoaders.clear();
sharedBundledPublicSurfaceJitiLoaders.clear();
}