From d6c76eb5bf00481fca9d2badea7b5e209c9c52eb Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 29 May 2026 02:47:12 +0100 Subject: [PATCH] perf: prefer bundled plugin dist entries --- .../plugins/bundled.shape-guard.test.ts | 121 ++++++++++++++++++ src/channels/plugins/bundled.ts | 26 +++- src/plugins/bundled-channel-runtime.test.ts | 43 +++++++ src/plugins/bundled-plugin-metadata.ts | 68 +++++++++- 4 files changed, 252 insertions(+), 6 deletions(-) diff --git a/src/channels/plugins/bundled.shape-guard.test.ts b/src/channels/plugins/bundled.shape-guard.test.ts index 0004b7530d4..f1008408d55 100644 --- a/src/channels/plugins/bundled.shape-guard.test.ts +++ b/src/channels/plugins/bundled.shape-guard.test.ts @@ -390,6 +390,127 @@ describe("bundled channel entry shape guards", () => { } }); + it("falls back through the cached loader for package-local dist entries needing SDK aliases", async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-bundled-package-dist-")); + const pluginDir = path.join(root, "extensions", "alpha", "dist"); + fs.mkdirSync(pluginDir, { recursive: true }); + fs.writeFileSync(path.join(root, "package.json"), '{"type":"module"}\n', "utf8"); + fs.writeFileSync( + path.join(pluginDir, "index.js"), + [ + 'import { defineBundledChannelEntry } from "openclaw/plugin-sdk/channel-entry-contract";', + "export default defineBundledChannelEntry({", + " id: 'alpha',", + " name: 'Alpha',", + " description: 'Alpha',", + " importMetaUrl: import.meta.url,", + " plugin: { specifier: './plugin.js', exportName: 'plugin' },", + "});", + "", + ].join("\n"), + "utf8", + ); + fs.writeFileSync( + path.join(pluginDir, "plugin.js"), + [ + "export const plugin = {", + " id: 'alpha',", + " meta: { id: 'alpha', label: 'Package dist Alpha' },", + " capabilities: {},", + " config: {},", + "};", + "", + ].join("\n"), + "utf8", + ); + + vi.doMock("./bundled-root.js", () => ({ + resolveBundledChannelRootScope: () => ({ + packageRoot: root, + cacheKey: `${root}:package-local-dist`, + }), + })); + vi.doMock("../../plugins/bundled-channel-runtime.js", () => ({ + listBundledChannelPluginMetadata: () => [ + { + ...alphaChannelMetadata(), + source: { + source: path.join(root, "extensions", "alpha", "index.ts"), + built: path.join(root, "extensions", "alpha", "index.ts"), + }, + }, + ], + resolveBundledChannelGeneratedPath: () => path.join(pluginDir, "index.js"), + })); + + try { + const bundled = await importFreshModule( + import.meta.url, + "./bundled.js?scope=bundled-package-local-dist-sdk-alias", + ); + + expect(bundled.requireBundledChannelPlugin("alpha").meta.label).toBe("Package dist Alpha"); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } + }); + + it("falls back through the cached loader for direct override dist entries needing SDK aliases", async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-bundled-direct-dist-")); + const previousBundledPluginsDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR; + const pluginsRoot = path.join(root, "bundled-plugins"); + const pluginDir = path.join(pluginsRoot, "alpha", "dist"); + fs.mkdirSync(pluginDir, { recursive: true }); + fs.writeFileSync(path.join(pluginsRoot, "package.json"), '{"type":"module"}\n', "utf8"); + fs.writeFileSync( + path.join(pluginDir, "index.js"), + [ + 'import { defineBundledChannelEntry } from "openclaw/plugin-sdk/channel-entry-contract";', + "export default defineBundledChannelEntry({", + " id: 'alpha',", + " name: 'Alpha',", + " description: 'Alpha',", + " importMetaUrl: import.meta.url,", + " plugin: { specifier: './plugin.js', exportName: 'plugin' },", + "});", + "", + ].join("\n"), + "utf8", + ); + fs.writeFileSync( + path.join(pluginDir, "plugin.js"), + [ + "export const plugin = {", + " id: 'alpha',", + " meta: { id: 'alpha', label: 'Direct dist Alpha' },", + " capabilities: {},", + " config: {},", + "};", + "", + ].join("\n"), + "utf8", + ); + + vi.doMock("../../plugins/bundled-channel-runtime.js", () => ({ + listBundledChannelPluginMetadata: () => [alphaChannelMetadata()], + resolveBundledChannelGeneratedPath: () => path.join(pluginDir, "index.js"), + })); + + try { + process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = pluginsRoot; + + const bundled = await importFreshModule( + import.meta.url, + "./bundled.js?scope=bundled-direct-dist-sdk-alias", + ); + + expect(bundled.requireBundledChannelPlugin("alpha").meta.label).toBe("Direct dist Alpha"); + } finally { + restoreBundledPluginsDir(previousBundledPluginsDir); + fs.rmSync(root, { recursive: true, force: true }); + } + }); + it("treats direct bundled plugin-tree overrides as scan roots", async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-bundled-direct-override-")); const previousBundledPluginsDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR; diff --git a/src/channels/plugins/bundled.ts b/src/channels/plugins/bundled.ts index d8e58e004eb..bc4abc20844 100644 --- a/src/channels/plugins/bundled.ts +++ b/src/channels/plugins/bundled.ts @@ -105,6 +105,20 @@ function isSourceModulePath(modulePath: string): boolean { return /\.(?:c|m)?tsx?$/iu.test(modulePath); } +function isPackageLocalBundledDistModulePath(params: { + rootScope: BundledChannelRootScope; + metadata: BundledChannelPluginMetadata; + modulePath: string; +}): boolean { + const distRoots = [ + ...(params.rootScope.pluginsDir + ? [path.join(params.rootScope.pluginsDir, params.metadata.dirName, "dist")] + : []), + path.join(params.rootScope.packageRoot, "extensions", params.metadata.dirName, "dist"), + ]; + return distRoots.some((root) => isPathInside(root, params.modulePath)); +} + function resolveChannelPluginModuleEntry( moduleExport: unknown, ): BundledChannelEntryRuntimeContract | null { @@ -259,7 +273,15 @@ function loadGeneratedBundledChannelModule(params: { boundaryRootDir: boundaryRoot, }); } catch (error) { - if (!isSourceModulePath(modulePath)) { + const canRetryWithCachedLoader = + isSourceModulePath(modulePath) || + (isPackageLocalBundledDistModulePath({ + rootScope: params.rootScope, + metadata: params.metadata, + modulePath, + }) && + findMissingModuleCodeInChain(error) !== undefined); + if (!canRetryWithCachedLoader) { throw error; } const loader = getCachedPluginModuleLoader({ @@ -267,7 +289,7 @@ function loadGeneratedBundledChannelModule(params: { modulePath, importerUrl: import.meta.url, preferBuiltDist: true, - cacheScopeKey: "bundled-channel-source-entry", + cacheScopeKey: "bundled-channel-entry", }); return loader(modulePath); } diff --git a/src/plugins/bundled-channel-runtime.test.ts b/src/plugins/bundled-channel-runtime.test.ts index 8bf1e820384..31cc2abc7a8 100644 --- a/src/plugins/bundled-channel-runtime.test.ts +++ b/src/plugins/bundled-channel-runtime.test.ts @@ -4,6 +4,7 @@ import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { listBundledChannelPluginMetadata, + resolveBundledChannelGeneratedPath, resolveBundledChannelWorkspacePath, } from "./bundled-channel-runtime.js"; @@ -39,4 +40,46 @@ describe("bundled channel runtime metadata", () => { listBundledChannelPluginMetadata({ rootDir: tempRoot, scanDir: missingScanDir }), ).toStrictEqual([]); }); + + it("prefers package-local dist entries over source checkout channel entries", () => { + const tempRoot = createTempRoot(); + const pluginRoot = path.join(tempRoot, "extensions", "slack"); + fs.mkdirSync(path.join(pluginRoot, "dist"), { recursive: true }); + fs.writeFileSync(path.join(pluginRoot, "index.ts"), "export default {};\n", "utf8"); + fs.writeFileSync(path.join(pluginRoot, "dist", "index.js"), "export default {};\n", "utf8"); + + expect( + resolveBundledChannelGeneratedPath( + tempRoot, + { + source: "./index.ts", + built: "index.js", + }, + "slack", + path.join(tempRoot, "extensions"), + ), + ).toBe(path.join(pluginRoot, "dist", "index.js")); + }); + + it("prefers package-local dist entries for absolute installed registry sources", () => { + const tempRoot = createTempRoot(); + const pluginRoot = path.join(tempRoot, "extensions", "slack"); + const builtScanRoot = path.join(tempRoot, "dist", "extensions"); + fs.mkdirSync(path.join(pluginRoot, "dist"), { recursive: true }); + fs.mkdirSync(path.join(builtScanRoot, "slack"), { recursive: true }); + fs.writeFileSync(path.join(pluginRoot, "index.ts"), "export default {};\n", "utf8"); + fs.writeFileSync(path.join(pluginRoot, "dist", "index.js"), "export default {};\n", "utf8"); + + expect( + resolveBundledChannelGeneratedPath( + tempRoot, + { + source: path.join(pluginRoot, "index.ts"), + built: path.join(pluginRoot, "index.ts"), + }, + "slack", + builtScanRoot, + ), + ).toBe(path.join(pluginRoot, "dist", "index.js")); + }); }); diff --git a/src/plugins/bundled-plugin-metadata.ts b/src/plugins/bundled-plugin-metadata.ts index 20b610ebf13..3ed764729b8 100644 --- a/src/plugins/bundled-plugin-metadata.ts +++ b/src/plugins/bundled-plugin-metadata.ts @@ -229,15 +229,73 @@ function listBundledPluginEntryBaseDirs(params: { pluginDirName?: string; scanDir?: string; }): string[] { + const scanPluginRoot = params.scanDir + ? path.resolve(params.scanDir, params.pluginDirName ?? "") + : undefined; const baseDirs = [ - ...(params.scanDir ? [path.resolve(params.scanDir, params.pluginDirName ?? "")] : []), + ...(scanPluginRoot ? [path.resolve(scanPluginRoot, "dist")] : []), + ...(scanPluginRoot ? [scanPluginRoot] : []), path.resolve(params.rootDir, "dist", "extensions", params.pluginDirName ?? ""), path.resolve(params.rootDir, "dist-runtime", "extensions", params.pluginDirName ?? ""), + path.resolve(params.rootDir, "extensions", params.pluginDirName ?? "", "dist"), path.resolve(params.rootDir, "extensions", params.pluginDirName ?? ""), ]; return uniqueStrings(baseDirs); } +function isPathInsideRoot(rootDir: string, targetPath: string): boolean { + const relative = path.relative(rootDir, targetPath); + return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative); +} + +function listBundledPluginEntryRoots(params: { + rootDir: string; + pluginDirName?: string; + scanDir?: string; +}): string[] { + const roots = [ + ...(params.scanDir ? [path.resolve(params.scanDir, params.pluginDirName ?? "")] : []), + path.resolve(params.rootDir, "extensions", params.pluginDirName ?? ""), + path.resolve(params.rootDir, "dist", "extensions", params.pluginDirName ?? ""), + path.resolve(params.rootDir, "dist-runtime", "extensions", params.pluginDirName ?? ""), + ]; + return uniqueStrings(roots); +} + +function listBundledPluginEntrySearchPaths( + entry: BundledPluginPathPair, + params: { + rootDir: string; + pluginDirName?: string; + scanDir?: string; + }, +): string[] { + const paths: string[] = []; + const roots = listBundledPluginEntryRoots(params); + for (const rawEntry of [entry.built, entry.source]) { + if (typeof rawEntry !== "string" || rawEntry.length === 0) { + continue; + } + if (!path.isAbsolute(rawEntry)) { + paths.push(rawEntry); + continue; + } + const normalizedEntry = path.normalize(rawEntry); + for (const root of roots) { + if (!isPathInsideRoot(root, normalizedEntry)) { + continue; + } + const relativeEntry = path.relative(root, normalizedEntry); + const builtEntry = rewriteBundledPluginEntryToBuiltPath(relativeEntry); + if (builtEntry) { + paths.push(builtEntry); + } + paths.push(relativeEntry); + } + } + return uniqueStrings(paths); +} + export function resolveBundledPluginGeneratedPath( rootDir: string, entry: BundledPluginPathPair | undefined, @@ -247,9 +305,11 @@ export function resolveBundledPluginGeneratedPath( if (!entry) { return null; } - const entryOrder = [entry.built, entry.source].filter( - (candidate): candidate is string => typeof candidate === "string" && candidate.length > 0, - ); + const entryOrder = listBundledPluginEntrySearchPaths(entry, { + rootDir, + pluginDirName, + ...(scanDir ? { scanDir } : {}), + }); const baseDirs = listBundledPluginEntryBaseDirs({ rootDir, pluginDirName,