mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:20:43 +00:00
refactor(channels): load bundled modules without jiti
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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") {
|
||||
|
||||
Reference in New Issue
Block a user