refactor(channels): load bundled modules without jiti

This commit is contained in:
Peter Steinberger
2026-05-02 00:03:56 +01:00
parent 890a053062
commit 42773cb89f
5 changed files with 87 additions and 96 deletions

View File

@@ -105,6 +105,8 @@ function listSourceBundledPluginRoots(): string[] {
}
afterEach(() => {
delete (globalThis as { __openclawBundledChannelReenter?: () => void })
.__openclawBundledChannelReenter;
vi.resetModules();
vi.doUnmock("../../plugins/bundled-channel-runtime.js");
vi.doUnmock("../../plugins/bundled-plugin-metadata.js");
@@ -835,21 +837,48 @@ describe("bundled channel entry shape guards", () => {
it("breaks reentrant bundled channel discovery cycles with an empty fallback", async () => {
const pluginDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-bundled-reentrant-"));
const modulePath = path.join(pluginDir, "index.js");
fs.writeFileSync(modulePath, "export {};\n", "utf8");
const modulePath = path.join(pluginDir, "index.cjs");
fs.writeFileSync(
modulePath,
`
const reenter = globalThis.__openclawBundledChannelReenter;
if (typeof reenter === "function") {
reenter();
}
module.exports = {
default: {
kind: "bundled-channel-entry",
id: "alpha",
name: "Alpha",
description: "Alpha",
configSchema: {},
register() {},
loadChannelPlugin() {
return {
id: "alpha",
meta: {},
capabilities: {},
config: {},
};
},
},
};
`,
"utf8",
);
vi.doMock("../../plugins/bundled-plugin-metadata.js", async (importOriginal) => {
vi.doMock("../../plugins/bundled-channel-runtime.js", async (importOriginal) => {
const actual =
await importOriginal<typeof import("../../plugins/bundled-plugin-metadata.js")>();
await importOriginal<typeof import("../../plugins/bundled-channel-runtime.js")>();
return {
...actual,
listBundledPluginMetadata: () => [
listBundledChannelPluginMetadata: () => [
{
dirName: "alpha",
idHint: "alpha",
source: {
source: "./index.js",
built: "./index.js",
source: "./index.cjs",
built: "./index.cjs",
},
manifest: {
id: "alpha",
@@ -857,7 +886,7 @@ describe("bundled channel entry shape guards", () => {
},
},
],
resolveBundledPluginGeneratedPath: () => modulePath,
resolveBundledChannelGeneratedPath: () => modulePath,
};
});
vi.doMock("../../infra/boundary-file-read.js", () => ({
@@ -870,44 +899,16 @@ describe("bundled channel entry shape guards", () => {
vi.doMock("../../plugins/channel-catalog-registry.js", () => ({
listChannelCatalogEntries: () => [],
}));
// jiti-loader-cache prefers native require() for compiled .js before
// falling back to jiti. This test drives plugin loading via the jiti
// mock — disable the native-require fast path so the mocked jiti loader
// is exercised instead of loading the on-disk fixture directly.
vi.doMock("../../plugins/native-module-require.js", () => ({
isJavaScriptModulePath: () => false,
tryNativeRequireJavaScriptModule: () => ({ ok: false }),
}));
let reentered = false;
vi.doMock("jiti", () => ({
createJiti: () => {
return () => {
if (!reentered) {
reentered = true;
expect(bundled.listBundledChannelPlugins()).toEqual([]);
}
return {
default: {
kind: "bundled-channel-entry",
id: "alpha",
name: "Alpha",
description: "Alpha",
configSchema: {},
register() {},
loadChannelPlugin() {
return {
id: "alpha",
meta: {},
capabilities: {},
config: {},
};
},
},
};
};
},
}));
(
globalThis as { __openclawBundledChannelReenter?: () => void }
).__openclawBundledChannelReenter = () => {
if (!reentered) {
reentered = true;
expect(bundled.listBundledChannelPlugins()).toEqual([]);
}
};
const bundled = await importFreshModule<typeof import("./bundled.js")>(
import.meta.url,

View File

@@ -15,7 +15,6 @@ import {
import { normalizePluginsConfig } from "../../plugins/config-state.js";
import { passesManifestOwnerBasePolicy } from "../../plugins/manifest-owner-policy.js";
import { unwrapDefaultModuleExport } from "../../plugins/module-export.js";
import { isJavaScriptModulePath } from "../../plugins/native-module-require.js";
import type { PluginRuntime } from "../../plugins/runtime/types.js";
import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js";
import { resolveBundledChannelRootScope, type BundledChannelRootScope } from "./bundled-root.js";
@@ -209,8 +208,6 @@ function loadGeneratedBundledChannelModule(params: {
modulePath,
rootDir: boundaryRoot,
boundaryRootDir: boundaryRoot,
shouldTryNativeRequire: (safePath) =>
safePath.includes(`${path.sep}dist${path.sep}`) && isJavaScriptModulePath(safePath),
});
}

View File

@@ -13,7 +13,6 @@ afterEach(() => {
fs.rmSync(tempDir, { recursive: true, force: true });
}
vi.resetModules();
vi.doUnmock("jiti");
});
function createTempDir(): string {
@@ -38,7 +37,7 @@ describe("channel plugin module loader helpers", () => {
expect(isJavaScriptModulePath("/tmp/entry.ts")).toBe(false);
});
it("uses native require for eligible JavaScript modules before falling back to Jiti", async () => {
it("uses native require for eligible JavaScript modules without creating Jiti", async () => {
const createJiti = vi.fn(() => vi.fn(() => ({ ok: false })));
vi.resetModules();
vi.doMock("jiti", () => ({
@@ -57,45 +56,34 @@ describe("channel plugin module loader helpers", () => {
loaderModule.loadChannelPluginModule({
modulePath,
rootDir,
shouldTryNativeRequire: () => true,
}),
).toEqual({ ok: true });
expect(createJiti).not.toHaveBeenCalled();
});
it("creates the runtime-supported Jiti boundary for Windows dist loads", async () => {
const createJiti = vi.fn(() => vi.fn(() => ({ ok: true })));
it("rejects TypeScript modules without creating Jiti", async () => {
const createJiti = vi.fn(() => {
throw new Error("channel module loader must not create jiti");
});
vi.resetModules();
vi.doMock("jiti", () => ({
createJiti,
}));
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
const loaderModule = await importFreshModule<typeof import("./module-loader.js")>(
import.meta.url,
"./module-loader.js?scope=source-ts-native-hook",
);
const rootDir = createTempDir();
const modulePath = path.join(rootDir, "extensions", "demo", "index.ts");
fs.mkdirSync(path.dirname(modulePath), { recursive: true });
fs.writeFileSync(modulePath, "export const ok = true;\n", "utf8");
try {
const loaderModule = await importFreshModule<typeof import("./module-loader.js")>(
import.meta.url,
"./module-loader.js?scope=windows-dist-jiti",
);
const rootDir = createTempDir();
const modulePath = path.join(rootDir, "dist", "extensions", "demo", "index.js");
fs.mkdirSync(path.dirname(modulePath), { recursive: true });
fs.writeFileSync(modulePath, "export const ok = true;\n", "utf8");
const loaded = loaderModule.loadChannelPluginModule({
expect(() =>
loaderModule.loadChannelPluginModule({
modulePath,
rootDir,
shouldTryNativeRequire: () => false,
});
expect(loaded).toMatchObject({ ok: true });
expect(createJiti).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
tryNative: false,
}),
);
} finally {
platformSpy.mockRestore();
}
}),
).toThrow(/must be built JavaScript/u);
expect(createJiti).not.toHaveBeenCalled();
});
});

View File

@@ -1,23 +1,31 @@
import fs from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import { openBoundaryFileSync } from "../../infra/boundary-file-read.js";
import {
getCachedPluginJitiLoader,
type PluginJitiLoaderCache,
} from "../../plugins/jiti-loader-cache.js";
import { isJavaScriptModulePath } from "../../plugins/native-module-require.js";
const jitiLoaders: PluginJitiLoaderCache = new Map();
const nodeRequire = createRequire(import.meta.url);
const SOURCE_MODULE_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".cts"]);
function loadModule(modulePath: string, tryNative?: boolean) {
return getCachedPluginJitiLoader({
cache: jitiLoaders,
modulePath,
importerUrl: import.meta.url,
argvEntry: process.argv[1],
preferBuiltDist: true,
jitiFilename: import.meta.url,
tryNative,
});
function hasNativeSourceRequireHook(modulePath: string): boolean {
const extension = path.extname(modulePath).toLowerCase();
return (
SOURCE_MODULE_EXTENSIONS.has(extension) &&
typeof nodeRequire.extensions?.[extension] === "function"
);
}
function loadModule(modulePath: string): unknown {
if (!isJavaScriptModulePath(modulePath) && !hasNativeSourceRequireHook(modulePath)) {
throw new Error(`channel plugin module must be built JavaScript: ${modulePath}`);
}
try {
return nodeRequire(modulePath);
} catch (error) {
throw new Error(`failed to load channel plugin module with native require: ${modulePath}`, {
cause: error,
});
}
}
function resolvePluginModuleCandidates(rootDir: string, specifier: string): string[] {
@@ -52,7 +60,6 @@ export function loadChannelPluginModule(params: {
rootDir: string;
boundaryRootDir?: string;
boundaryLabel?: string;
shouldTryNativeRequire?: (safePath: string) => boolean;
}): unknown {
const opened = openBoundaryFileSync({
absolutePath: params.modulePath,
@@ -68,5 +75,5 @@ export function loadChannelPluginModule(params: {
}
const safePath = opened.path;
fs.closeSync(opened.fd);
return loadModule(safePath, params.shouldTryNativeRequire?.(safePath))(safePath);
return loadModule(safePath);
}

View File

@@ -5,7 +5,6 @@ import {
listChannelCatalogEntries,
type PluginChannelCatalogEntry,
} from "../../plugins/channel-catalog-registry.js";
import { isJavaScriptModulePath } from "../../plugins/native-module-require.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { loadChannelPluginModule, resolveExistingPluginModulePath } from "./module-loader.js";
@@ -60,7 +59,6 @@ function resolveChannelPackageStateChecker(params: {
const moduleExport = loadChannelPluginModule({
modulePath: resolveExistingPluginModulePath(params.entry.rootDir, metadata.specifier!),
rootDir: params.entry.rootDir,
shouldTryNativeRequire: isJavaScriptModulePath,
}) as Record<string, unknown>;
const checker = moduleExport[metadata.exportName!] as ChannelPackageStateChecker | undefined;
if (typeof checker !== "function") {