diff --git a/src/plugins/bundled-channel-config-metadata.ts b/src/plugins/bundled-channel-config-metadata.ts index f440c7aad8b..79324d426c1 100644 --- a/src/plugins/bundled-channel-config-metadata.ts +++ b/src/plugins/bundled-channel-config-metadata.ts @@ -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>(); +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): 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 { diff --git a/src/plugins/jiti-loader-cache.test.ts b/src/plugins/jiti-loader-cache.test.ts new file mode 100644 index 00000000000..ab31d5211a0 --- /dev/null +++ b/src/plugins/jiti-loader-cache.test.ts @@ -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) => + 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); + }); +}); diff --git a/src/plugins/jiti-loader-cache.ts b/src/plugins/jiti-loader-cache.ts index 226d18afad5..c6f694a50ac 100644 --- a/src/plugins/jiti-loader-cache.ts +++ b/src/plugins/jiti-loader-cache.ts @@ -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>; @@ -12,25 +8,24 @@ export function getCachedPluginJitiLoader(params: { modulePath: string; importerUrl: string; argvEntry?: string; + preferBuiltDist?: boolean; + jitiFilename?: string; }): ReturnType { - 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; } diff --git a/src/plugins/public-surface-loader.ts b/src/plugins/public-surface-loader.ts index b5bb6ff02df..e84f3255494 100644 --- a/src/plugins/public-surface-loader.ts +++ b/src/plugins/public-surface-loader.ts @@ -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>(); -const sharedBundledPublicSurfaceJitiLoaders = new Map>(); +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 | null { +function getSharedBundledPublicSurfaceJiti(modulePath: string, tryNative: boolean) { const bundledPluginsDir = resolveBundledPluginsDir(); if ( !isBundledPluginExtensionPath({ diff --git a/src/plugins/source-loader.ts b/src/plugins/source-loader.ts index 0e8c150ee5f..f781f1835ec 100644 --- a/src/plugins/source-loader.ts +++ b/src/plugins/source-loader.ts @@ -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>(); + 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); }