diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index fc0f6c2f208..c99ccbf41f0 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -3462,6 +3462,47 @@ module.exports = { expect(subpaths).toEqual(["channel-runtime", "core"]); }); + it("builds plugin-sdk aliases from the module being loaded, not the loader location", () => { + const fixture = createPluginSdkAliasFixture({ + srcFile: "channel-runtime.ts", + distFile: "channel-runtime.js", + packageExports: { + "./plugin-sdk/channel-runtime": { default: "./dist/plugin-sdk/channel-runtime.js" }, + }, + }); + const sourceRootAlias = path.join(fixture.root, "src", "plugin-sdk", "root-alias.cjs"); + const distRootAlias = path.join(fixture.root, "dist", "plugin-sdk", "root-alias.cjs"); + fs.writeFileSync(sourceRootAlias, "module.exports = {};\n", "utf-8"); + fs.writeFileSync(distRootAlias, "module.exports = {};\n", "utf-8"); + const sourcePluginEntry = path.join(fixture.root, "extensions", "demo", "src", "index.ts"); + fs.mkdirSync(path.dirname(sourcePluginEntry), { recursive: true }); + fs.writeFileSync(sourcePluginEntry, 'export const plugin = "demo";\n', "utf-8"); + + const sourceAliases = withEnv({ NODE_ENV: undefined }, () => + __testing.buildPluginLoaderAliasMap(sourcePluginEntry), + ); + expect(fs.realpathSync(sourceAliases["openclaw/plugin-sdk"] ?? "")).toBe( + fs.realpathSync(sourceRootAlias), + ); + expect(fs.realpathSync(sourceAliases["openclaw/plugin-sdk/channel-runtime"] ?? "")).toBe( + fs.realpathSync(path.join(fixture.root, "src", "plugin-sdk", "channel-runtime.ts")), + ); + + const distPluginEntry = path.join(fixture.root, "dist", "extensions", "demo", "index.js"); + fs.mkdirSync(path.dirname(distPluginEntry), { recursive: true }); + fs.writeFileSync(distPluginEntry, 'export const plugin = "demo";\n', "utf-8"); + + const distAliases = withEnv({ NODE_ENV: undefined }, () => + __testing.buildPluginLoaderAliasMap(distPluginEntry), + ); + expect(fs.realpathSync(distAliases["openclaw/plugin-sdk"] ?? "")).toBe( + fs.realpathSync(distRootAlias), + ); + expect(fs.realpathSync(distAliases["openclaw/plugin-sdk/channel-runtime"] ?? "")).toBe( + fs.realpathSync(path.join(fixture.root, "dist", "plugin-sdk", "channel-runtime.js")), + ); + }); + it("does not resolve plugin-sdk alias files from cwd fallback when package root is not an OpenClaw root", () => { const fixture = createPluginSdkAliasFixture({ srcFile: "channel-runtime.ts", diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index b1aff47073c..68ba5b8403a 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -104,8 +104,20 @@ function resolveLoaderModulePath(params: LoaderModuleResolveParams = {}): string return params.modulePath ?? fileURLToPath(params.moduleUrl ?? import.meta.url); } -const resolvePluginSdkAlias = (): string | null => - resolvePluginSdkAliasFile({ srcFile: "root-alias.cjs", distFile: "root-alias.cjs" }); +const resolvePluginSdkAlias = (params: LoaderModuleResolveParams = {}): string | null => + resolvePluginSdkAliasFile({ + srcFile: "root-alias.cjs", + distFile: "root-alias.cjs", + ...params, + }); + +function buildPluginLoaderAliasMap(modulePath: string): Record { + const pluginSdkAlias = resolvePluginSdkAlias({ modulePath }); + return { + ...(pluginSdkAlias ? { "openclaw/plugin-sdk": pluginSdkAlias } : {}), + ...resolvePluginSdkScopedAliasMap({ modulePath }), + }; +} function resolvePluginRuntimeModulePath(params: LoaderModuleResolveParams = {}): string | null { try { @@ -138,6 +150,7 @@ function resolvePluginRuntimeModulePath(params: LoaderModuleResolveParams = {}): export const __testing = { buildPluginLoaderJitiOptions, + buildPluginLoaderAliasMap, listPluginSdkAliasCandidates, listPluginSdkExportedSubpaths, resolvePluginSdkScopedAliasMap, @@ -704,18 +717,18 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi } // Lazy: avoid creating the Jiti loader when all plugins are disabled (common in unit tests). - const jitiLoaders = new Map>(); + const jitiLoaders = new Map>(); const getJiti = (modulePath: string) => { const tryNative = shouldPreferNativeJiti(modulePath); - const cached = jitiLoaders.get(tryNative); + const aliasMap = buildPluginLoaderAliasMap(modulePath); + const cacheKey = JSON.stringify({ + tryNative, + aliasMap: Object.entries(aliasMap).toSorted(([left], [right]) => left.localeCompare(right)), + }); + const cached = jitiLoaders.get(cacheKey); if (cached) { return cached; } - const pluginSdkAlias = resolvePluginSdkAlias(); - const aliasMap = { - ...(pluginSdkAlias ? { "openclaw/plugin-sdk": pluginSdkAlias } : {}), - ...resolvePluginSdkScopedAliasMap(), - }; const loader = createJiti(import.meta.url, { ...buildPluginLoaderJitiOptions(aliasMap), // Source .ts runtime shims import sibling ".js" specifiers that only exist @@ -724,7 +737,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi // loading for the canonical built module graph. tryNative, }); - jitiLoaders.set(tryNative, loader); + jitiLoaders.set(cacheKey, loader); return loader; };