diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index ed053ac395e..167889b61b4 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -1351,6 +1351,20 @@ describe("loadOpenClawPlugins", () => { expect(resolved).toBe(distFile); }); + it("prefers dist candidates first for production src runtime", () => { + const { root, srcFile, distFile } = createPluginSdkAliasFixture(); + + const candidates = withEnv({ NODE_ENV: "production", VITEST: undefined }, () => + __testing.listPluginSdkAliasCandidates({ + srcFile: "index.ts", + distFile: "index.js", + modulePath: path.join(root, "src", "plugins", "loader.ts"), + }), + ); + + expect(candidates.indexOf(distFile)).toBeLessThan(candidates.indexOf(srcFile)); + }); + it("prefers src plugin-sdk alias when loader runs from src in non-production", () => { const { root, srcFile } = createPluginSdkAliasFixture(); @@ -1364,6 +1378,20 @@ describe("loadOpenClawPlugins", () => { expect(resolved).toBe(srcFile); }); + it("prefers src candidates first for non-production src runtime", () => { + const { root, srcFile, distFile } = createPluginSdkAliasFixture(); + + const candidates = withEnv({ NODE_ENV: undefined }, () => + __testing.listPluginSdkAliasCandidates({ + srcFile: "index.ts", + distFile: "index.js", + modulePath: path.join(root, "src", "plugins", "loader.ts"), + }), + ); + + expect(candidates.indexOf(srcFile)).toBeLessThan(candidates.indexOf(distFile)); + }); + it("falls back to src plugin-sdk alias when dist is missing in production", () => { const { root, srcFile, distFile } = createPluginSdkAliasFixture(); fs.rmSync(distFile); diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index f112fcb815c..6460b45c945 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -47,6 +47,43 @@ const registryCache = new Map(); const defaultLogger = () => createSubsystemLogger("plugins"); +function resolvePluginSdkAliasCandidateOrder(params: { + modulePath: string; + isProduction: boolean; +}) { + const normalizedModulePath = params.modulePath.replace(/\\/g, "/"); + const isDistRuntime = normalizedModulePath.includes("/dist/"); + return isDistRuntime || params.isProduction ? ["dist", "src"] : ["src", "dist"]; +} + +function listPluginSdkAliasCandidates(params: { + srcFile: string; + distFile: string; + modulePath: string; +}) { + const orderedKinds = resolvePluginSdkAliasCandidateOrder({ + modulePath: params.modulePath, + isProduction: process.env.NODE_ENV === "production", + }); + let cursor = path.dirname(params.modulePath); + const candidates: string[] = []; + for (let i = 0; i < 6; i += 1) { + const candidateMap = { + src: path.join(cursor, "src", "plugin-sdk", params.srcFile), + dist: path.join(cursor, "dist", "plugin-sdk", params.distFile), + } as const; + for (const kind of orderedKinds) { + candidates.push(candidateMap[kind]); + } + const parent = path.dirname(cursor); + if (parent === cursor) { + break; + } + cursor = parent; + } + return candidates; +} + const resolvePluginSdkAliasFile = (params: { srcFile: string; distFile: string; @@ -54,27 +91,14 @@ const resolvePluginSdkAliasFile = (params: { }): string | null => { try { const modulePath = params.modulePath ?? fileURLToPath(import.meta.url); - const isProduction = process.env.NODE_ENV === "production"; - const normalizedModulePath = modulePath.replace(/\\/g, "/"); - const isDistRuntime = normalizedModulePath.includes("/dist/"); - let cursor = path.dirname(modulePath); - for (let i = 0; i < 6; i += 1) { - const srcCandidate = path.join(cursor, "src", "plugin-sdk", params.srcFile); - const distCandidate = path.join(cursor, "dist", "plugin-sdk", params.distFile); - const orderedCandidates = - isDistRuntime || isProduction - ? [distCandidate, srcCandidate] - : [srcCandidate, distCandidate]; - for (const candidate of orderedCandidates) { - if (fs.existsSync(candidate)) { - return candidate; - } + for (const candidate of listPluginSdkAliasCandidates({ + srcFile: params.srcFile, + distFile: params.distFile, + modulePath, + })) { + if (fs.existsSync(candidate)) { + return candidate; } - const parent = path.dirname(cursor); - if (parent === cursor) { - break; - } - cursor = parent; } } catch { // ignore @@ -190,6 +214,8 @@ const resolvePluginSdkScopedAliasMap = (): Record => { }; export const __testing = { + listPluginSdkAliasCandidates, + resolvePluginSdkAliasCandidateOrder, resolvePluginSdkAliasFile, };