fix(plugins): share cached plugin jiti loader config

This commit is contained in:
Vincent Koc
2026-04-14 17:00:10 +01:00
parent 37d5971db3
commit 905b18530f
5 changed files with 124 additions and 68 deletions

View File

@@ -1,19 +1,18 @@
import fs from "node:fs";
import path from "node:path";
import { createJiti } from "jiti";
import { buildChannelConfigSchema } from "../channels/plugins/config-schema.js";
import type { ChannelConfigRuntimeSchema } from "../channels/plugins/types.config.js";
import {
normalizeBundledPluginStringList,
trimBundledPluginString,
} from "./bundled-plugin-scan.js";
import { getCachedPluginJitiLoader, type PluginJitiLoaderCache } from "./jiti-loader-cache.js";
import type { PluginConfigUiHint } from "./manifest-types.js";
import type {
OpenClawPackageManifest,
PluginManifest,
PluginManifestChannelConfig,
} from "./manifest.js";
import { buildPluginLoaderJitiOptions, resolvePluginLoaderJitiConfig } from "./sdk-alias.js";
const PUBLIC_SURFACE_SOURCE_EXTENSIONS = [".ts", ".mts", ".js", ".mjs", ".cts", ".cjs"] as const;
const SOURCE_CONFIG_SCHEMA_CANDIDATES = [
@@ -32,7 +31,7 @@ type ChannelConfigSurface = {
runtime?: ChannelConfigRuntimeSchema;
};
const jitiLoaders = new Map<string, ReturnType<typeof createJiti>>();
const jitiLoaders: PluginJitiLoaderCache = new Map();
function isBuiltChannelConfigSchema(value: unknown): value is ChannelConfigSurface {
if (!value || typeof value !== "object") {
@@ -71,22 +70,13 @@ function resolveConfigSchemaExport(imported: Record<string, unknown>): ChannelCo
}
function getJiti(modulePath: string) {
const { tryNative, aliasMap, cacheKey } = resolvePluginLoaderJitiConfig({
return getCachedPluginJitiLoader({
cache: jitiLoaders,
modulePath,
argv1: process.argv[1],
moduleUrl: import.meta.url,
importerUrl: import.meta.url,
preferBuiltDist: true,
jitiFilename: import.meta.url,
});
const cached = jitiLoaders.get(cacheKey);
if (cached) {
return cached;
}
const loader = createJiti(import.meta.url, {
...buildPluginLoaderJitiOptions(aliasMap),
tryNative,
});
jitiLoaders.set(cacheKey, loader);
return loader;
}
function resolveChannelConfigSchemaModulePath(pluginDir: string): string | undefined {

View File

@@ -0,0 +1,87 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { importFreshModule } from "../../test/helpers/import-fresh.ts";
afterEach(() => {
vi.resetModules();
vi.doUnmock("jiti");
});
describe("getCachedPluginJitiLoader", () => {
it("reuses cached loaders for the same module config and filename", async () => {
const createJiti = vi.fn((filename: string) =>
Object.assign(vi.fn(), {
filename,
}),
);
vi.doMock("jiti", () => ({
createJiti,
}));
const { getCachedPluginJitiLoader } = await importFreshModule<
typeof import("./jiti-loader-cache.js")
>(import.meta.url, "./jiti-loader-cache.js?scope=cached-loader");
const cache = new Map();
const params = {
cache,
modulePath: "/repo/extensions/demo/index.ts",
importerUrl: "file:///repo/src/plugins/setup-registry.ts",
argvEntry: "/repo/openclaw.mjs",
jitiFilename: "file:///repo/src/plugins/source-loader.ts",
} as const;
const first = getCachedPluginJitiLoader(params);
const second = getCachedPluginJitiLoader(params);
expect(second).toBe(first);
expect(createJiti).toHaveBeenCalledTimes(1);
expect(cache.size).toBe(1);
});
it("keeps loader caches scoped by jiti filename and dist preference", async () => {
const createJiti = vi.fn((filename: string, options: Record<string, unknown>) =>
Object.assign(vi.fn(), {
filename,
options,
}),
);
vi.doMock("jiti", () => ({
createJiti,
}));
const { getCachedPluginJitiLoader } = await importFreshModule<
typeof import("./jiti-loader-cache.js")
>(import.meta.url, "./jiti-loader-cache.js?scope=filename-scope");
const cache = new Map();
const first = getCachedPluginJitiLoader({
cache,
modulePath: "/repo/dist/extensions/demo/api.ts",
importerUrl: "file:///repo/src/plugins/public-surface-loader.ts",
argvEntry: "/repo/openclaw.mjs",
preferBuiltDist: true,
jitiFilename: "file:///repo/src/plugins/public-surface-loader.ts",
});
const second = getCachedPluginJitiLoader({
cache,
modulePath: "/repo/dist/extensions/demo/api.ts",
importerUrl: "file:///repo/src/plugins/public-surface-loader.ts",
argvEntry: "/repo/openclaw.mjs",
preferBuiltDist: true,
jitiFilename: "file:///repo/src/plugins/bundled-channel-config-metadata.ts",
});
expect(second).not.toBe(first);
expect(createJiti).toHaveBeenNthCalledWith(
1,
"file:///repo/src/plugins/public-surface-loader.ts",
expect.objectContaining({ tryNative: true }),
);
expect(createJiti).toHaveBeenNthCalledWith(
2,
"file:///repo/src/plugins/bundled-channel-config-metadata.ts",
expect.objectContaining({ tryNative: true }),
);
expect(cache.size).toBe(2);
});
});

View File

@@ -1,9 +1,5 @@
import { createJiti } from "jiti";
import {
buildPluginLoaderAliasMap,
buildPluginLoaderJitiOptions,
shouldPreferNativeJiti,
} from "./sdk-alias.js";
import { buildPluginLoaderJitiOptions, resolvePluginLoaderJitiConfig } from "./sdk-alias.js";
export type PluginJitiLoaderCache = Map<string, ReturnType<typeof createJiti>>;
@@ -12,25 +8,24 @@ export function getCachedPluginJitiLoader(params: {
modulePath: string;
importerUrl: string;
argvEntry?: string;
preferBuiltDist?: boolean;
jitiFilename?: string;
}): ReturnType<typeof createJiti> {
const aliasMap = buildPluginLoaderAliasMap(
params.modulePath,
params.argvEntry ?? process.argv[1],
params.importerUrl,
);
const tryNative = shouldPreferNativeJiti(params.modulePath);
const cacheKey = JSON.stringify({
tryNative,
aliasMap: Object.entries(aliasMap).toSorted(([left], [right]) => left.localeCompare(right)),
const { tryNative, aliasMap, cacheKey } = resolvePluginLoaderJitiConfig({
modulePath: params.modulePath,
argv1: params.argvEntry ?? process.argv[1],
moduleUrl: params.importerUrl,
...(params.preferBuiltDist ? { preferBuiltDist: true } : {}),
});
const cached = params.cache.get(cacheKey);
const scopedCacheKey = `${params.jitiFilename ?? params.modulePath}::${cacheKey}`;
const cached = params.cache.get(scopedCacheKey);
if (cached) {
return cached;
}
const loader = createJiti(params.modulePath, {
const loader = createJiti(params.jitiFilename ?? params.modulePath, {
...buildPluginLoaderJitiOptions(aliasMap),
tryNative,
});
params.cache.set(cacheKey, loader);
params.cache.set(scopedCacheKey, loader);
return loader;
}

View File

@@ -5,6 +5,7 @@ import { fileURLToPath } from "node:url";
import { createJiti } from "jiti";
import { openBoundaryFileSync } from "../infra/boundary-file-read.js";
import { resolveBundledPluginsDir } from "./bundled-dir.js";
import { getCachedPluginJitiLoader, type PluginJitiLoaderCache } from "./jiti-loader-cache.js";
import { resolveBundledPluginPublicSurfacePath } from "./public-surface-runtime.js";
import {
buildPluginLoaderAliasMap,
@@ -28,8 +29,8 @@ const publicSurfaceLocations = new Map<
boundaryRoot: string;
} | null
>();
const jitiLoaders = new Map<string, ReturnType<typeof createJiti>>();
const sharedBundledPublicSurfaceJitiLoaders = new Map<string, ReturnType<typeof createJiti>>();
const jitiLoaders: PluginJitiLoaderCache = new Map();
const sharedBundledPublicSurfaceJitiLoaders: PluginJitiLoaderCache = new Map();
function isSourceArtifactPath(modulePath: string): boolean {
switch (path.extname(modulePath).toLowerCase()) {
@@ -95,7 +96,7 @@ function resolvePublicSurfaceLocation(params: {
}
function getJiti(modulePath: string) {
const { tryNative, aliasMap, cacheKey } = resolvePluginLoaderJitiConfig({
const { tryNative } = resolvePluginLoaderJitiConfig({
modulePath,
argv1: process.argv[1],
moduleUrl: import.meta.url,
@@ -105,15 +106,13 @@ function getJiti(modulePath: string) {
if (sharedLoader) {
return sharedLoader;
}
const cached = jitiLoaders.get(cacheKey);
if (cached) {
return cached;
}
const loader = createJiti(import.meta.url, {
...buildPluginLoaderJitiOptions(aliasMap),
tryNative,
const loader = getCachedPluginJitiLoader({
cache: jitiLoaders,
modulePath,
importerUrl: import.meta.url,
preferBuiltDist: true,
jitiFilename: import.meta.url,
});
jitiLoaders.set(cacheKey, loader);
return loader;
}
@@ -130,10 +129,7 @@ function loadPublicSurfaceModule(modulePath: string): unknown {
return getJiti(modulePath)(modulePath);
}
function getSharedBundledPublicSurfaceJiti(
modulePath: string,
tryNative: boolean,
): ReturnType<typeof createJiti> | null {
function getSharedBundledPublicSurfaceJiti(modulePath: string, tryNative: boolean) {
const bundledPluginsDir = resolveBundledPluginsDir();
if (
!isBundledPluginExtensionPath({

View File

@@ -1,9 +1,5 @@
import { createJiti } from "jiti";
import {
buildPluginLoaderAliasMap,
buildPluginLoaderJitiOptions,
shouldPreferNativeJiti,
} from "./sdk-alias.js";
import type { PluginJitiLoaderCache } from "./jiti-loader-cache.js";
import { getCachedPluginJitiLoader } from "./jiti-loader-cache.js";
export type PluginSourceLoader = (modulePath: string) => unknown;
@@ -12,22 +8,14 @@ function shouldProfilePluginSourceLoader(): boolean {
}
export function createPluginSourceLoader(): PluginSourceLoader {
const loaders = new Map<string, ReturnType<typeof createJiti>>();
const loaders: PluginJitiLoaderCache = new Map();
return (modulePath) => {
const tryNative = shouldPreferNativeJiti(modulePath);
const aliasMap = buildPluginLoaderAliasMap(modulePath, process.argv[1], import.meta.url);
const cacheKey = JSON.stringify({
tryNative,
aliasMap: Object.entries(aliasMap).toSorted(([left], [right]) => left.localeCompare(right)),
const jiti = getCachedPluginJitiLoader({
cache: loaders,
modulePath,
importerUrl: import.meta.url,
jitiFilename: import.meta.url,
});
let jiti = loaders.get(cacheKey);
if (!jiti) {
jiti = createJiti(import.meta.url, {
...buildPluginLoaderJitiOptions(aliasMap),
tryNative,
});
loaders.set(cacheKey, jiti);
}
if (!shouldProfilePluginSourceLoader()) {
return jiti(modulePath);
}