fix: use transpiled jiti for source plugin shims

This commit is contained in:
Peter Steinberger
2026-03-18 16:24:37 +00:00
parent b64f4e313d
commit 3d8afb96bd
4 changed files with 134 additions and 23 deletions

View File

@@ -3,6 +3,7 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { createJiti } from "jiti";
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
import { withEnv } from "../test-utils/env.js";
async function importFreshPluginTestModules() {
@@ -3341,6 +3342,82 @@ module.exports = {
expect("alias" in options).toBe(false);
});
it("uses transpiled Jiti loads for source TypeScript plugin entries", () => {
expect(__testing.shouldPreferNativeJiti("/repo/dist/plugins/runtime/index.js")).toBe(true);
expect(
__testing.shouldPreferNativeJiti("/repo/extensions/discord/src/channel.runtime.ts"),
).toBe(false);
});
it("loads source runtime shims through the non-native Jiti boundary", async () => {
const jiti = createJiti(import.meta.url, {
...__testing.buildPluginLoaderJitiOptions({}),
tryNative: false,
});
const discordChannelRuntime = path.join(
process.cwd(),
"extensions",
"discord",
"src",
"channel.runtime.ts",
);
const discordVoiceRuntime = path.join(
process.cwd(),
"extensions",
"discord",
"src",
"voice",
"manager.runtime.ts",
);
await expect(jiti.import(discordChannelRuntime)).resolves.toMatchObject({
discordSetupWizard: expect.any(Object),
});
await expect(jiti.import(discordVoiceRuntime)).resolves.toMatchObject({
DiscordVoiceManager: expect.any(Function),
DiscordVoiceReadyListener: expect.any(Function),
});
});
it("loads source TypeScript plugins that route through local runtime shims", () => {
const plugin = writePlugin({
id: "source-runtime-shim",
filename: "source-runtime-shim.ts",
body: `import "./runtime-shim.ts";
export default {
id: "source-runtime-shim",
register() {},
};`,
});
fs.writeFileSync(
path.join(plugin.dir, "runtime-shim.ts"),
`import { helperValue } from "./helper.js";
export const runtimeValue = helperValue;`,
"utf-8",
);
fs.writeFileSync(
path.join(plugin.dir, "helper.ts"),
`export const helperValue = "ok";`,
"utf-8",
);
const registry = loadOpenClawPlugins({
cache: false,
workspaceDir: plugin.dir,
config: {
plugins: {
load: { paths: [plugin.file] },
allow: ["source-runtime-shim"],
},
},
});
const record = registry.plugins.find((entry) => entry.id === "source-runtime-shim");
expect(record?.status).toBe("loaded");
});
it.each([
{
name: "prefers dist plugin runtime module when loader runs from dist",

View File

@@ -288,6 +288,18 @@ const resolvePluginSdkScopedAliasMap = (): Record<string, string> => {
return aliasMap;
};
function shouldPreferNativeJiti(modulePath: string): boolean {
switch (path.extname(modulePath).toLowerCase()) {
case ".js":
case ".mjs":
case ".cjs":
case ".json":
return true;
default:
return false;
}
}
export const __testing = {
buildPluginLoaderJitiOptions,
listPluginSdkAliasCandidates,
@@ -295,6 +307,7 @@ export const __testing = {
resolvePluginSdkAliasCandidateOrder,
resolvePluginSdkAliasFile,
resolvePluginRuntimeModulePath,
shouldPreferNativeJiti,
maxPluginRegistryCacheEntries: MAX_PLUGIN_REGISTRY_CACHE_ENTRIES,
};
@@ -849,18 +862,28 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
}
// Lazy: avoid creating the Jiti loader when all plugins are disabled (common in unit tests).
let jitiLoader: ReturnType<typeof createJiti> | null = null;
const getJiti = () => {
if (jitiLoader) {
return jitiLoader;
const jitiLoaders = new Map<boolean, ReturnType<typeof createJiti>>();
const getJiti = (modulePath: string) => {
const tryNative = shouldPreferNativeJiti(modulePath);
const cached = jitiLoaders.get(tryNative);
if (cached) {
return cached;
}
const pluginSdkAlias = resolvePluginSdkAlias();
const aliasMap = {
...(pluginSdkAlias ? { "openclaw/plugin-sdk": pluginSdkAlias } : {}),
...resolvePluginSdkScopedAliasMap(),
};
jitiLoader = createJiti(import.meta.url, buildPluginLoaderJitiOptions(aliasMap));
return jitiLoader;
const loader = createJiti(import.meta.url, {
...buildPluginLoaderJitiOptions(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(tryNative, loader);
return loader;
};
let createPluginRuntimeFactory: ((options?: CreatePluginRuntimeOptions) => PluginRuntime) | null =
@@ -875,7 +898,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
if (!runtimeModulePath) {
throw new Error("Unable to resolve plugin runtime module");
}
const runtimeModule = getJiti()(runtimeModulePath) as {
const runtimeModule = getJiti(runtimeModulePath)(runtimeModulePath) as {
createPluginRuntime?: (options?: CreatePluginRuntimeOptions) => PluginRuntime;
};
if (typeof runtimeModule.createPluginRuntime !== "function") {
@@ -1208,7 +1231,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
let mod: OpenClawPluginModule | null = null;
try {
mod = getJiti()(safeSource) as OpenClawPluginModule;
mod = getJiti(safeSource)(safeSource) as OpenClawPluginModule;
} catch (err) {
recordPluginError({
logger,