diff --git a/src/plugins/runtime-plugin-boundary.whatsapp.test.ts b/src/plugins/runtime-plugin-boundary.whatsapp.test.ts index 2a392ded435..d73b480cf66 100644 --- a/src/plugins/runtime-plugin-boundary.whatsapp.test.ts +++ b/src/plugins/runtime-plugin-boundary.whatsapp.test.ts @@ -4,7 +4,7 @@ import { bundledDistPluginFile } from "openclaw/plugin-sdk/test-fixtures"; import { afterEach, describe, expect, it } from "vitest"; import { stageBundledPluginRuntime } from "../../scripts/stage-bundled-plugin-runtime.mjs"; import type { PluginJitiLoaderCache } from "./jiti-loader-cache.js"; -import { loadPluginBoundaryModuleWithJiti } from "./runtime/runtime-plugin-boundary.js"; +import { loadPluginBoundaryModule } from "./runtime/runtime-plugin-boundary.js"; import { cleanupTrackedTempDirs, makeTrackedTempDir } from "./test-helpers/fs-fixtures.js"; type LightModule = { @@ -92,13 +92,15 @@ function createBundledWhatsAppRuntimeFixture() { function loadWhatsAppBoundaryModules(runtimePluginDir: string) { const loaders: PluginJitiLoaderCache = new Map(); return { - light: loadPluginBoundaryModuleWithJiti( + light: loadPluginBoundaryModule( path.join(runtimePluginDir, "light-runtime-api.js"), loaders, + { origin: "bundled" }, ), - heavy: loadPluginBoundaryModuleWithJiti( + heavy: loadPluginBoundaryModule( path.join(runtimePluginDir, "runtime-api.js"), loaders, + { origin: "bundled" }, ), }; } @@ -126,4 +128,28 @@ describe("runtime plugin boundary whatsapp seam", () => { it("shares listener state between staged light and heavy runtime modules", () => { expectSharedWhatsAppListenerState(createBundledWhatsAppRuntimeFixture(), "work"); }); + + it("rejects bundled TypeScript runtime modules instead of using the source loader", () => { + const rootDir = makeTrackedTempDir("openclaw-bundled-boundary-ts", tempDirs); + const modulePath = path.join(rootDir, "runtime-api.ts"); + writeRuntimeFixtureText(rootDir, "runtime-api.ts", "export const ok = true;\n"); + const loaders: PluginJitiLoaderCache = new Map(); + + expect(() => + loadPluginBoundaryModule<{ ok: boolean }>(modulePath, loaders, { origin: "bundled" }), + ).toThrow(/must be built JavaScript/u); + expect(loaders.size).toBe(0); + }); + + it("keeps TypeScript source fallback available for non-bundled plugins", () => { + const rootDir = makeTrackedTempDir("openclaw-external-boundary-ts", tempDirs); + const modulePath = path.join(rootDir, "runtime-api.ts"); + writeRuntimeFixtureText(rootDir, "runtime-api.ts", "export const ok = true;\n"); + const loaders: PluginJitiLoaderCache = new Map(); + + expect( + loadPluginBoundaryModule<{ ok: boolean }>(modulePath, loaders, { origin: "workspace" }), + ).toMatchObject({ ok: true }); + expect(loaders.size).toBe(1); + }); }); diff --git a/src/plugins/runtime/runtime-plugin-boundary.ts b/src/plugins/runtime/runtime-plugin-boundary.ts index 13fdb81cefe..0eee570f6aa 100644 --- a/src/plugins/runtime/runtime-plugin-boundary.ts +++ b/src/plugins/runtime/runtime-plugin-boundary.ts @@ -3,10 +3,14 @@ import path from "node:path"; import { getRuntimeConfig } from "../../config/config.js"; import { getCachedPluginJitiLoader, type PluginJitiLoaderCache } from "../jiti-loader-cache.js"; import { loadPluginManifestRegistry } from "../manifest-registry.js"; -import { shouldPreferNativeJiti } from "../sdk-alias.js"; +import { + isJavaScriptModulePath, + tryNativeRequireJavaScriptModule, +} from "../native-module-require.js"; +import type { PluginOrigin } from "../plugin-origin.types.js"; type PluginRuntimeRecord = { - origin?: string; + origin?: PluginOrigin; rootDir?: string; source: string; }; @@ -105,21 +109,33 @@ export function resolvePluginRuntimeModulePath( return null; } -export function getPluginBoundaryJiti(modulePath: string, loaders: PluginJitiLoaderCache) { - const tryNative = shouldPreferNativeJiti(modulePath); +function getPluginBoundarySourceLoader(modulePath: string, loaders: PluginJitiLoaderCache) { return getCachedPluginJitiLoader({ cache: loaders, modulePath, importerUrl: import.meta.url, jitiFilename: import.meta.url, - tryNative, + tryNative: false, }); } // oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- Dynamic plugin boundary loaders use caller-supplied module types. -export function loadPluginBoundaryModuleWithJiti( +export function loadPluginBoundaryModule( modulePath: string, loaders: PluginJitiLoaderCache, + options: { origin?: PluginOrigin } = {}, ): TModule { - return getPluginBoundaryJiti(modulePath, loaders)(modulePath) as TModule; + if (isJavaScriptModulePath(modulePath)) { + const native = tryNativeRequireJavaScriptModule(modulePath, { allowWindows: true }); + if (native.ok) { + return native.moduleExport as TModule; + } + if (options.origin === "bundled") { + throw new Error(`bundled plugin runtime module must load natively: ${modulePath}`); + } + } else if (options.origin === "bundled") { + throw new Error(`bundled plugin runtime module must be built JavaScript: ${modulePath}`); + } + + return getPluginBoundarySourceLoader(modulePath, loaders)(modulePath) as TModule; } diff --git a/src/plugins/runtime/runtime-web-channel-plugin.ts b/src/plugins/runtime/runtime-web-channel-plugin.ts index 2f0bf526ad4..40dadfc9edd 100644 --- a/src/plugins/runtime/runtime-web-channel-plugin.ts +++ b/src/plugins/runtime/runtime-web-channel-plugin.ts @@ -10,7 +10,7 @@ import { import type { PollInput } from "../../polls.js"; import type { PluginJitiLoaderCache } from "../jiti-loader-cache.js"; import { - loadPluginBoundaryModuleWithJiti, + loadPluginBoundaryModule, resolvePluginRuntimeRecordByEntryBaseNames, resolvePluginRuntimeModulePath, } from "./runtime-plugin-boundary.js"; @@ -132,25 +132,22 @@ function resolveWebChannelRuntimeModulePath( } function loadCurrentHeavyModuleSync(): WebChannelHeavyRuntimeModule { - const modulePath = resolveWebChannelRuntimeModulePath( - resolveWebChannelPluginRecord(), - "runtime-api", - ); - return loadPluginBoundaryModuleWithJiti(modulePath, jitiLoaders); + const record = resolveWebChannelPluginRecord(); + const modulePath = resolveWebChannelRuntimeModulePath(record, "runtime-api"); + return loadPluginBoundaryModule(modulePath, jitiLoaders, { + origin: record.origin, + }); } function loadWebChannelLightModule(): WebChannelLightRuntimeModule { - const modulePath = resolveWebChannelRuntimeModulePath( - resolveWebChannelPluginRecord(), - "light-runtime-api", - ); + const record = resolveWebChannelPluginRecord(); + const modulePath = resolveWebChannelRuntimeModulePath(record, "light-runtime-api"); if (cachedLightModule && cachedLightModulePath === modulePath) { return cachedLightModule; } - const loaded = loadPluginBoundaryModuleWithJiti( - modulePath, - jitiLoaders, - ); + const loaded = loadPluginBoundaryModule(modulePath, jitiLoaders, { + origin: record.origin, + }); cachedLightModulePath = modulePath; cachedLightModule = loaded; return loaded; @@ -162,10 +159,9 @@ async function loadWebChannelHeavyModule(): Promise( - modulePath, - jitiLoaders, - ); + const loaded = loadPluginBoundaryModule(modulePath, jitiLoaders, { + origin: record.origin, + }); cachedHeavyModulePath = modulePath; cachedHeavyModule = loaded; return loaded;