diff --git a/src/plugins/jiti-loader-cache.test.ts b/src/plugins/jiti-loader-cache.test.ts index ab31d5211a0..4008a8ba218 100644 --- a/src/plugins/jiti-loader-cache.test.ts +++ b/src/plugins/jiti-loader-cache.test.ts @@ -84,4 +84,57 @@ describe("getCachedPluginJitiLoader", () => { ); expect(cache.size).toBe(2); }); + + it("lets callers override alias maps and tryNative while keeping cache keys stable", 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=overrides"); + + const cache = new Map(); + const first = getCachedPluginJitiLoader({ + cache, + modulePath: "/repo/extensions/demo/index.ts", + importerUrl: "file:///repo/src/plugins/loader.ts", + jitiFilename: "file:///repo/src/plugins/loader.ts", + aliasMap: { + alpha: "/repo/alpha.js", + zeta: "/repo/zeta.js", + }, + tryNative: false, + }); + const second = getCachedPluginJitiLoader({ + cache, + modulePath: "/repo/extensions/demo/index.ts", + importerUrl: "file:///repo/src/plugins/loader.ts", + jitiFilename: "file:///repo/src/plugins/loader.ts", + aliasMap: { + zeta: "/repo/zeta.js", + alpha: "/repo/alpha.js", + }, + tryNative: false, + }); + + expect(second).toBe(first); + expect(createJiti).toHaveBeenCalledTimes(1); + expect(createJiti).toHaveBeenCalledWith( + "file:///repo/src/plugins/loader.ts", + expect.objectContaining({ + tryNative: false, + alias: { + alpha: "/repo/alpha.js", + zeta: "/repo/zeta.js", + }, + }), + ); + }); }); diff --git a/src/plugins/jiti-loader-cache.ts b/src/plugins/jiti-loader-cache.ts index fa0735fe79a..40909665e7e 100644 --- a/src/plugins/jiti-loader-cache.ts +++ b/src/plugins/jiti-loader-cache.ts @@ -1,5 +1,9 @@ import { createJiti } from "jiti"; -import { buildPluginLoaderJitiOptions, resolvePluginLoaderJitiConfig } from "./sdk-alias.js"; +import { + buildPluginLoaderJitiOptions, + createPluginLoaderJitiCacheKey, + resolvePluginLoaderJitiConfig, +} from "./sdk-alias.js"; export type PluginJitiLoader = ReturnType; export type PluginJitiLoaderFactory = typeof createJiti; @@ -13,12 +17,33 @@ export function getCachedPluginJitiLoader(params: { preferBuiltDist?: boolean; jitiFilename?: string; createLoader?: PluginJitiLoaderFactory; + aliasMap?: Record; + tryNative?: boolean; }): PluginJitiLoader { - const { tryNative, aliasMap, cacheKey } = resolvePluginLoaderJitiConfig({ - modulePath: params.modulePath, - argv1: params.argvEntry ?? process.argv[1], - moduleUrl: params.importerUrl, - ...(params.preferBuiltDist ? { preferBuiltDist: true } : {}), + const defaultConfig = + params.aliasMap || typeof params.tryNative === "boolean" + ? resolvePluginLoaderJitiConfig({ + modulePath: params.modulePath, + argv1: params.argvEntry ?? process.argv[1], + moduleUrl: params.importerUrl, + ...(params.preferBuiltDist ? { preferBuiltDist: true } : {}), + }) + : null; + const resolved = defaultConfig + ? { + tryNative: params.tryNative ?? defaultConfig.tryNative, + aliasMap: params.aliasMap ?? defaultConfig.aliasMap, + } + : resolvePluginLoaderJitiConfig({ + modulePath: params.modulePath, + argv1: params.argvEntry ?? process.argv[1], + moduleUrl: params.importerUrl, + ...(params.preferBuiltDist ? { preferBuiltDist: true } : {}), + }); + const { tryNative, aliasMap } = resolved; + const cacheKey = createPluginLoaderJitiCacheKey({ + tryNative, + aliasMap, }); const scopedCacheKey = `${params.jitiFilename ?? params.modulePath}::${cacheKey}`; const cached = params.cache.get(scopedCacheKey); diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index f714118dc25..c821f7ede85 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -1,7 +1,6 @@ import { createHash } from "node:crypto"; import fs from "node:fs"; import path from "node:path"; -import { createJiti } from "jiti"; import { clearAgentHarnesses, listRegisteredAgentHarnesses, @@ -46,6 +45,7 @@ import { import { discoverOpenClawPlugins } from "./discovery.js"; import { initializeGlobalHookRunner } from "./hook-runner-global.js"; import { clearPluginInteractiveHandlers } from "./interactive-registry.js"; +import { getCachedPluginJitiLoader, type PluginJitiLoaderCache } from "./jiti-loader-cache.js"; import { loadPluginManifestRegistry } from "./manifest-registry.js"; import type { PluginBundleFormat, PluginDiagnostic, PluginFormat } from "./manifest-types.js"; import type { PluginManifestContracts } from "./manifest.js"; @@ -277,7 +277,7 @@ function toSafeImportPath(specifier: string): string { } function createPluginJitiLoader(options: Pick) { - const jitiLoaders = new Map>(); + const jitiLoaders: PluginJitiLoaderCache = new Map(); return (modulePath: string) => { const tryNative = shouldPreferNativeJiti(modulePath); const aliasMap = buildPluginLoaderAliasMap( @@ -286,24 +286,18 @@ function createPluginJitiLoader(options: Pick left.localeCompare(right)), - }); - const cached = jitiLoaders.get(cacheKey); - if (cached) { - return cached; - } - const loader = createJiti(import.meta.url, { - ...buildPluginLoaderJitiOptions(aliasMap), + return getCachedPluginJitiLoader({ + cache: jitiLoaders, + modulePath, + importerUrl: import.meta.url, + jitiFilename: import.meta.url, + aliasMap, // Source .ts runtime shims import sibling ".js" specifiers that only exist // after build. Disable native loading for source entries so Jiti rewrites // those imports against the source graph, while keeping native dist/*.js // loading for the canonical built module graph. tryNative, }); - jitiLoaders.set(cacheKey, loader); - return loader; }; }