mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
refactor: simplify plugin module loading
This commit is contained in:
@@ -66,6 +66,7 @@ vi.mock("../../channels/plugins/config-writes.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../../channels/registry.js", () => ({
|
||||
normalizeAnyChannelId: vi.fn((value?: string) => value),
|
||||
normalizeChannelId: vi.fn((value?: string) => value),
|
||||
}));
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ export function listPotentialConfiguredChannelPresenceSignals(
|
||||
signals.push({ channelId, source });
|
||||
};
|
||||
const configuredChannelIds = new Set<string>();
|
||||
const channelIds = listBundledChannelPluginIds();
|
||||
const channelIds = listBundledChannelPluginIds(env);
|
||||
const channelEnvPrefixes = listChannelEnvPrefixes(channelIds);
|
||||
const channels = isRecord(cfg.channels) ? cfg.channels : null;
|
||||
if (channels) {
|
||||
@@ -164,7 +164,7 @@ function hasEnvConfiguredChannel(
|
||||
env: NodeJS.ProcessEnv,
|
||||
options: ChannelPresenceOptions = {},
|
||||
): boolean {
|
||||
const channelIds = listBundledChannelPluginIds();
|
||||
const channelIds = listBundledChannelPluginIds(env);
|
||||
const channelEnvPrefixes = listChannelEnvPrefixes(channelIds);
|
||||
for (const [key, value] of Object.entries(env)) {
|
||||
if (!hasNonEmptyString(value)) {
|
||||
|
||||
@@ -10,6 +10,6 @@ export function listBundledChannelPluginIdsForRoot(
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
export function listBundledChannelPluginIds(): string[] {
|
||||
return listBundledChannelPluginIdsForRoot(resolveBundledChannelRootScope().cacheKey);
|
||||
export function listBundledChannelPluginIds(env: NodeJS.ProcessEnv = process.env): string[] {
|
||||
return listBundledChannelPluginIdsForRoot(resolveBundledChannelRootScope(env).cacheKey, env);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
listReadOnlyChannelPluginsForConfig,
|
||||
} from "./read-only.js";
|
||||
|
||||
const jitiLoaderParams = vi.hoisted(
|
||||
const moduleLoaderParams = vi.hoisted(
|
||||
() =>
|
||||
[] as Array<{
|
||||
modulePath: string;
|
||||
@@ -31,8 +31,9 @@ vi.mock("../../plugins/bundled-dir.js", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../plugins/jiti-loader-cache.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../plugins/jiti-loader-cache.js")>();
|
||||
vi.mock("../../plugins/plugin-module-loader-cache.js", async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import("../../plugins/plugin-module-loader-cache.js")>();
|
||||
const { createRequire } = await import("node:module");
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
@@ -105,12 +106,12 @@ vi.mock("../../plugins/jiti-loader-cache.js", async (importOriginal) => {
|
||||
|
||||
return {
|
||||
...actual,
|
||||
getCachedPluginJitiLoader: ((params) => {
|
||||
jitiLoaderParams.push({
|
||||
getCachedPluginModuleLoader: ((params) => {
|
||||
moduleLoaderParams.push({
|
||||
modulePath: params.modulePath,
|
||||
tryNative: params.tryNative,
|
||||
});
|
||||
const actualLoader = actual.getCachedPluginJitiLoader(params);
|
||||
const actualLoader = actual.getCachedPluginModuleLoader(params);
|
||||
return ((modulePath: string) => {
|
||||
if (
|
||||
modulePath.endsWith("/plugins/loader.js") ||
|
||||
@@ -119,8 +120,8 @@ vi.mock("../../plugins/jiti-loader-cache.js", async (importOriginal) => {
|
||||
return { loadOpenClawPlugins };
|
||||
}
|
||||
return actualLoader(modulePath);
|
||||
}) as ReturnType<typeof actual.getCachedPluginJitiLoader>;
|
||||
}) satisfies typeof actual.getCachedPluginJitiLoader,
|
||||
}) as ReturnType<typeof actual.getCachedPluginModuleLoader>;
|
||||
}) satisfies typeof actual.getCachedPluginModuleLoader,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -431,7 +432,7 @@ function expectExternalChatSetupOnlyPluginLoaded(params: {
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
jitiLoaderParams.length = 0;
|
||||
moduleLoaderParams.length = 0;
|
||||
resetPluginLoaderTestStateForTest();
|
||||
});
|
||||
|
||||
@@ -498,7 +499,7 @@ describe("listReadOnlyChannelPluginsForConfig", () => {
|
||||
|
||||
expectExternalChatSetupOnlyPluginLoaded({ plugins, setupMarker, fullMarker });
|
||||
expect(
|
||||
jitiLoaderParams.some(
|
||||
moduleLoaderParams.some(
|
||||
(entry) =>
|
||||
entry.tryNative === true &&
|
||||
(entry.modulePath.endsWith("/plugins/loader.js") ||
|
||||
|
||||
@@ -8,11 +8,11 @@ import {
|
||||
listConfiguredChannelIdsForReadOnlyScope,
|
||||
resolveDiscoverableScopedChannelPluginIds,
|
||||
} from "../../plugins/channel-plugin-ids.js";
|
||||
import {
|
||||
getCachedPluginJitiLoader,
|
||||
type PluginJitiLoaderCache,
|
||||
} from "../../plugins/jiti-loader-cache.js";
|
||||
import type { PluginManifestRecord } from "../../plugins/manifest-registry.js";
|
||||
import {
|
||||
getCachedPluginModuleLoader,
|
||||
type PluginModuleLoaderCache,
|
||||
} from "../../plugins/plugin-module-loader-cache.js";
|
||||
import { loadPluginManifestRegistryForPluginRegistry } from "../../plugins/plugin-registry.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
|
||||
import { sanitizeForLog } from "../../terminal/ansi.js";
|
||||
@@ -34,7 +34,7 @@ const BUILT_PLUGIN_LOADER_MODULE_CANDIDATES = [
|
||||
"plugins/loader.js",
|
||||
"plugins/build-smoke-entry.js",
|
||||
] as const;
|
||||
const jitiLoaders: PluginJitiLoaderCache = new Map();
|
||||
const moduleLoaders: PluginModuleLoaderCache = new Map();
|
||||
|
||||
type PluginLoaderModule = {
|
||||
loadOpenClawPlugins: (params: {
|
||||
@@ -93,15 +93,15 @@ function loadPluginLoaderModule(): PluginLoaderModule {
|
||||
for (const candidate of listPluginLoaderModuleCandidateUrls()) {
|
||||
const modulePath = fileURLToPath(candidate);
|
||||
try {
|
||||
const jiti = getCachedPluginJitiLoader({
|
||||
cache: jitiLoaders,
|
||||
const moduleLoader = getCachedPluginModuleLoader({
|
||||
cache: moduleLoaders,
|
||||
modulePath,
|
||||
importerUrl: import.meta.url,
|
||||
preferBuiltDist: true,
|
||||
jitiFilename: import.meta.url,
|
||||
loaderFilename: import.meta.url,
|
||||
tryNative: true,
|
||||
});
|
||||
pluginLoaderModule = jiti(modulePath) as PluginLoaderModule;
|
||||
pluginLoaderModule = moduleLoader(modulePath) as PluginLoaderModule;
|
||||
return pluginLoaderModule;
|
||||
} catch {
|
||||
// Try built/runtime source candidates in order.
|
||||
|
||||
@@ -35,6 +35,7 @@ vi.mock("../../channels/plugins/catalog.js", () => ({
|
||||
}));
|
||||
vi.mock("../../channels/registry.js", () => ({
|
||||
listChatChannels: () => listChatChannels(),
|
||||
normalizeAnyChannelId: (channelId?: string) => channelId?.trim().toLowerCase() ?? null,
|
||||
}));
|
||||
vi.mock("../../plugins/manifest-registry.js", () => ({
|
||||
loadPluginManifestRegistry: (...a: unknown[]) => loadPluginManifestRegistry(...a),
|
||||
|
||||
@@ -12,8 +12,31 @@ import {
|
||||
makeRegistry,
|
||||
resetPluginAutoEnableTestState,
|
||||
} from "./plugin-auto-enable.test-helpers.js";
|
||||
import type { OpenClawConfig } from "./types.openclaw.js";
|
||||
import { validateConfigObject } from "./validation.js";
|
||||
|
||||
vi.mock("../channels/plugins/configured-state.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../channels/plugins/configured-state.js")>();
|
||||
return {
|
||||
...actual,
|
||||
hasBundledChannelConfiguredState: (params: {
|
||||
channelId: string;
|
||||
cfg: OpenClawConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}) => {
|
||||
if (params.channelId === "irc") {
|
||||
return Boolean(params.env?.IRC_HOST?.trim() && params.env?.IRC_NICK?.trim());
|
||||
}
|
||||
if (params.channelId === "slack") {
|
||||
return ["SLACK_APP_TOKEN", "SLACK_BOT_TOKEN", "SLACK_USER_TOKEN"].some((key) =>
|
||||
Boolean(params.env?.[key]?.trim()),
|
||||
);
|
||||
}
|
||||
return actual.hasBundledChannelConfiguredState(params);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const env = makeIsolatedEnv();
|
||||
|
||||
afterAll(() => {
|
||||
@@ -93,6 +116,19 @@ describe("applyPluginAutoEnable core", () => {
|
||||
expect(result.config.plugins?.allow).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not auto-enable Slack from unrelated Slack-prefixed env vars", () => {
|
||||
const result = applyPluginAutoEnable({
|
||||
config: {},
|
||||
env: makeIsolatedEnv({
|
||||
SLACK_WEBHOOK_URL: "https://hooks.slack.com/services/T000/B000/XXX",
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result.config.channels?.slack).toBeUndefined();
|
||||
expect(result.config.plugins?.entries?.slack).toBeUndefined();
|
||||
expect(result.changes).toEqual([]);
|
||||
});
|
||||
|
||||
it("stores auto-enable reasons in a null-prototype dictionary", () => {
|
||||
const result = applyPluginAutoEnable({
|
||||
config: {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { collectConfiguredAgentHarnessRuntimes } from "../agents/harness-runtime
|
||||
import { normalizeProviderId } from "../agents/provider-id.js";
|
||||
import {
|
||||
hasPotentialConfiguredChannels,
|
||||
listPotentialConfiguredChannelIds,
|
||||
listPotentialConfiguredChannelPresenceSignals,
|
||||
} from "../channels/config-presence.js";
|
||||
import { getChatChannelMeta, normalizeChatChannelId } from "../channels/registry.js";
|
||||
import {
|
||||
@@ -294,10 +294,12 @@ function collectPluginIdsForConfiguredChannel(
|
||||
return [builtInId ?? claims[0]?.plugin.id ?? normalizedChannelId];
|
||||
}
|
||||
|
||||
function collectCandidateChannelIds(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): string[] {
|
||||
return listPotentialConfiguredChannelIds(cfg, env, { includePersistedAuthState: false }).map(
|
||||
(channelId) => normalizeChatChannelId(channelId) ?? channelId,
|
||||
);
|
||||
function collectConfiguredChannelIds(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): string[] {
|
||||
return listPotentialConfiguredChannelPresenceSignals(cfg, env, {
|
||||
includePersistedAuthState: false,
|
||||
})
|
||||
.map((signal) => normalizeChatChannelId(signal.channelId) ?? signal.channelId)
|
||||
.filter((channelId) => isChannelConfigured(cfg, channelId, env));
|
||||
}
|
||||
|
||||
function hasConfiguredWebSearchPluginEntry(cfg: OpenClawConfig): boolean {
|
||||
@@ -574,11 +576,9 @@ export function resolveConfiguredPluginAutoEnableCandidates(params: {
|
||||
registry: PluginManifestRegistry;
|
||||
}): PluginAutoEnableCandidate[] {
|
||||
const changes: PluginAutoEnableCandidate[] = [];
|
||||
for (const channelId of collectCandidateChannelIds(params.config, params.env)) {
|
||||
if (isChannelConfigured(params.config, channelId, params.env)) {
|
||||
for (const pluginId of collectPluginIdsForConfiguredChannel(channelId, params.registry)) {
|
||||
changes.push({ pluginId, kind: "channel-configured", channelId });
|
||||
}
|
||||
for (const channelId of collectConfiguredChannelIds(params.config, params.env)) {
|
||||
for (const pluginId of collectPluginIdsForConfiguredChannel(channelId, params.registry)) {
|
||||
changes.push({ pluginId, kind: "channel-configured", channelId });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ export function makeIsolatedEnv(overrides: NodeJS.ProcessEnv = {}): NodeJS.Proce
|
||||
return {
|
||||
OPENCLAW_STATE_DIR: path.join(rootDir, "state"),
|
||||
OPENCLAW_BUNDLED_PLUGINS_DIR: path.join(process.cwd(), "extensions"),
|
||||
OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR: "1",
|
||||
VITEST: "true",
|
||||
...overrides,
|
||||
};
|
||||
|
||||
@@ -45,6 +45,7 @@ vi.mock("../channels/registry.js", () => ({
|
||||
meta: Parameters<FormatChannelSelectionLine>[0],
|
||||
docsLink: Parameters<FormatChannelSelectionLine>[1],
|
||||
) => formatChannelSelectionLine(meta, docsLink),
|
||||
normalizeAnyChannelId: (channelId?: string) => channelId?.trim().toLowerCase() ?? null,
|
||||
}));
|
||||
|
||||
vi.mock("../commands/channel-setup/discovery.js", () => ({
|
||||
|
||||
@@ -134,6 +134,8 @@ vi.mock("../channels/plugins/setup-registry.js", () => ({
|
||||
vi.mock("../channels/registry.js", () => ({
|
||||
getChatChannelMeta: (channelId: string) => ({ id: channelId, label: channelId }),
|
||||
listChatChannels: () => [],
|
||||
normalizeAnyChannelId: (channelId?: unknown) =>
|
||||
typeof channelId === "string" ? channelId.trim().toLowerCase() || null : null,
|
||||
normalizeChatChannelId: (channelId?: unknown) =>
|
||||
typeof channelId === "string" ? channelId.trim().toLowerCase() || null : null,
|
||||
}));
|
||||
|
||||
@@ -213,7 +213,7 @@ async function expectBuiltArtifactNodeRequireFastPath(
|
||||
const importerPath = path.join(pluginRoot, "index.js");
|
||||
const sidecarPath = path.join(pluginRoot, "fast-path-sidecar.cjs");
|
||||
fs.writeFileSync(importerPath, "export default {};\n", "utf8");
|
||||
// CommonJS so `nodeRequire` succeeds without falling back to jiti, even
|
||||
// CommonJS so `nodeRequire` succeeds without falling back to the source loader, even
|
||||
// inside built plugin artifacts with a `type: "module"` package boundary.
|
||||
fs.writeFileSync(sidecarPath, "module.exports = { sentinel: 7 };\n", "utf8");
|
||||
|
||||
@@ -228,10 +228,10 @@ async function expectBuiltArtifactNodeRequireFastPath(
|
||||
.map((args) => String(args[0] ?? ""))
|
||||
.find((line) => line.startsWith("[plugin-load-profile] phase=bundled-entry-module-load"));
|
||||
expect(profileLine, "expected a bundled-entry-module-load profile line").toBeDefined();
|
||||
expect(profileLine).toMatch(/getJitiMs=\d/u);
|
||||
expect(profileLine).toMatch(/jitiCallMs=\d/u);
|
||||
expect(profileLine).not.toMatch(/getJitiMs=-/);
|
||||
expect(profileLine).not.toMatch(/jitiCallMs=-/);
|
||||
expect(profileLine).toMatch(/sourceLoaderCreateMs=\d/u);
|
||||
expect(profileLine).toMatch(/sourceLoaderCallMs=\d/u);
|
||||
expect(profileLine).not.toMatch(/sourceLoaderCreateMs=-/);
|
||||
expect(profileLine).not.toMatch(/sourceLoaderCallMs=-/);
|
||||
} finally {
|
||||
errorSpy.mockRestore();
|
||||
}
|
||||
@@ -267,7 +267,7 @@ describe("loadBundledEntryExportSync", () => {
|
||||
expect(message).toContain("ENOENT");
|
||||
});
|
||||
|
||||
it("keeps Windows dist sidecar loads off Jiti native import", async () => {
|
||||
it("keeps Windows dist sidecar loads off source-transform loading", async () => {
|
||||
const createJiti = vi.fn(() => vi.fn(() => ({ load: 42 })));
|
||||
vi.doMock("jiti", () => ({
|
||||
createJiti,
|
||||
@@ -306,7 +306,7 @@ describe("loadBundledEntryExportSync", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("normalizes Windows absolute sidecar paths before Jiti loads them", async () => {
|
||||
it("normalizes Windows absolute sidecar paths before module loads them", async () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-channel-entry-contract-"));
|
||||
tempDirs.push(tempRoot);
|
||||
const openedFdPath = path.join(tempRoot, "opened");
|
||||
@@ -396,10 +396,10 @@ describe("loadBundledEntryExportSync", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("emits non-negative jiti sub-step timings on the built-artifact load path", async () => {
|
||||
it("emits non-negative source-loader sub-step timings on the built-artifact load path", async () => {
|
||||
// Built artifacts prefer `nodeRequire`, but Node can still reject a sidecar
|
||||
// and fall back through jiti. The profile line must never report negative
|
||||
// or missing jiti sub-step timings either way.
|
||||
// or missing source-loader sub-step timings either way.
|
||||
await expectBuiltArtifactNodeRequireFastPath("built-artifact-profile-fast-path");
|
||||
});
|
||||
|
||||
|
||||
@@ -8,15 +8,15 @@ import type { ChannelLegacyStateMigrationPlan } from "../channels/plugins/types.
|
||||
import type { ChannelPlugin } from "../channels/plugins/types.plugin.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { openBoundaryFileSync } from "../infra/boundary-file-read.js";
|
||||
import {
|
||||
getCachedPluginJitiLoader,
|
||||
type PluginJitiLoaderCache,
|
||||
} from "../plugins/jiti-loader-cache.js";
|
||||
import {
|
||||
createProfiler,
|
||||
formatPluginLoadProfileLine,
|
||||
shouldProfilePluginLoader,
|
||||
} from "../plugins/plugin-load-profile.js";
|
||||
import {
|
||||
getCachedPluginSourceModuleLoader,
|
||||
type PluginModuleLoaderCache,
|
||||
} from "../plugins/plugin-module-loader-cache.js";
|
||||
import type { PluginRuntime } from "../plugins/runtime/types.js";
|
||||
import { resolveLoaderPackageRoot } from "../plugins/sdk-alias.js";
|
||||
import type { AnyAgentTool, OpenClawPluginApi, PluginCommandContext } from "../plugins/types.js";
|
||||
@@ -125,7 +125,7 @@ export type BundledChannelSetupEntryContract<TPlugin = ChannelPlugin> = {
|
||||
export type BundledEntryModuleLoadOptions = Record<string, never>;
|
||||
|
||||
const nodeRequire = createRequire(import.meta.url);
|
||||
const jitiLoaders: PluginJitiLoaderCache = new Map();
|
||||
const moduleLoaders: PluginModuleLoaderCache = new Map();
|
||||
const loadedModuleExports = new Map<string, unknown>();
|
||||
const disableBundledEntrySourceFallbackEnv = "OPENCLAW_DISABLE_BUNDLED_ENTRY_SOURCE_FALLBACK";
|
||||
|
||||
@@ -319,13 +319,13 @@ function resolveBundledEntryModulePath(importMetaUrl: string, specifier: string)
|
||||
);
|
||||
}
|
||||
|
||||
function getJiti(modulePath: string) {
|
||||
return getCachedPluginJitiLoader({
|
||||
cache: jitiLoaders,
|
||||
function getSourceModuleLoader(modulePath: string) {
|
||||
return getCachedPluginSourceModuleLoader({
|
||||
cache: moduleLoaders,
|
||||
modulePath,
|
||||
importerUrl: import.meta.url,
|
||||
preferBuiltDist: true,
|
||||
jitiFilename: import.meta.url,
|
||||
loaderFilename: import.meta.url,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -352,25 +352,25 @@ function loadBundledEntryModuleSync(
|
||||
let loaded: unknown;
|
||||
const profile = shouldProfilePluginLoader();
|
||||
const loadStartMs = profile ? performance.now() : 0;
|
||||
let getJitiEndMs = 0;
|
||||
let sourceLoaderReadyMs = 0;
|
||||
if (canTryNodeRequireBuiltModule(modulePath)) {
|
||||
try {
|
||||
loaded = nodeRequire(modulePath);
|
||||
} catch {
|
||||
const jiti = getJiti(modulePath);
|
||||
getJitiEndMs = profile ? performance.now() : 0;
|
||||
loaded = jiti(toSafeImportPath(modulePath));
|
||||
const moduleLoader = getSourceModuleLoader(modulePath);
|
||||
sourceLoaderReadyMs = profile ? performance.now() : 0;
|
||||
loaded = moduleLoader(toSafeImportPath(modulePath));
|
||||
}
|
||||
} else {
|
||||
const jiti = getJiti(modulePath);
|
||||
getJitiEndMs = profile ? performance.now() : 0;
|
||||
loaded = jiti(toSafeImportPath(modulePath));
|
||||
const moduleLoader = getSourceModuleLoader(modulePath);
|
||||
sourceLoaderReadyMs = profile ? performance.now() : 0;
|
||||
loaded = moduleLoader(toSafeImportPath(modulePath));
|
||||
}
|
||||
if (profile) {
|
||||
const endMs = performance.now();
|
||||
// Use shared formatter — but split timing fields ourselves so we can
|
||||
// attribute time spent in `getJiti(...)` factory creation vs the actual
|
||||
// graph-walking `__j(modulePath)` call. Both are emitted as extras
|
||||
// attribute time spent in source-loader creation vs the actual graph load.
|
||||
// Both are emitted as extras
|
||||
// alongside the canonical `elapsedMs=<total>` field.
|
||||
console.error(
|
||||
formatPluginLoadProfileLine({
|
||||
@@ -378,15 +378,12 @@ function loadBundledEntryModuleSync(
|
||||
pluginId: "(bundled-entry)",
|
||||
source: modulePath,
|
||||
elapsedMs: endMs - loadStartMs,
|
||||
// When the built-artifact fast-path resolves the module via `nodeRequire`,
|
||||
// `getJitiEndMs` stays `0` because the `catch` block (the only place
|
||||
// it gets stamped) never runs. Reporting `getJitiMs` /
|
||||
// `jitiCallMs` as `0` for that path keeps the breakdown honest:
|
||||
// `elapsedMs=` already captures the nodeRequire time, and we don't
|
||||
// want to mis-attribute it to jiti sub-steps.
|
||||
// When the built-artifact fast path resolves via `nodeRequire`, the
|
||||
// source-loader timestamp stays `0`; keep its breakdown at zero so
|
||||
// `elapsedMs=` owns the native load time.
|
||||
extras: [
|
||||
["getJitiMs", getJitiEndMs ? getJitiEndMs - loadStartMs : 0],
|
||||
["jitiCallMs", getJitiEndMs ? endMs - getJitiEndMs : 0],
|
||||
["sourceLoaderCreateMs", sourceLoaderReadyMs ? sourceLoaderReadyMs - loadStartMs : 0],
|
||||
["sourceLoaderCallMs", sourceLoaderReadyMs ? endMs - sourceLoaderReadyMs : 0],
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
listImportedBundledPluginFacadeIds,
|
||||
loadBundledPluginPublicSurfaceModuleSync,
|
||||
resetFacadeLoaderStateForTest,
|
||||
setFacadeLoaderJitiFactoryForTest,
|
||||
setFacadeLoaderSourceTransformFactoryForTest,
|
||||
} from "./facade-loader.js";
|
||||
import { listImportedBundledPluginFacadeIds as listImportedFacadeRuntimeIds } from "./facade-runtime.js";
|
||||
import { createPluginSdkTestHarness } from "./test-helpers.js";
|
||||
@@ -15,7 +15,9 @@ const { createTempDirSync } = createPluginSdkTestHarness();
|
||||
const originalBundledPluginsDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
const originalDisableBundledPlugins = process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS;
|
||||
const FACADE_LOADER_GLOBAL = "__openclawTestLoadBundledPluginPublicSurfaceModuleSync";
|
||||
type FacadeLoaderJitiFactory = NonNullable<Parameters<typeof setFacadeLoaderJitiFactoryForTest>[0]>;
|
||||
type FacadeLoaderSourceTransformFactory = NonNullable<
|
||||
Parameters<typeof setFacadeLoaderSourceTransformFactoryForTest>[0]
|
||||
>;
|
||||
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const trustedBundledPluginFixtureRoots: string[] = [];
|
||||
let trustedPluginIdCounter = 0;
|
||||
@@ -159,7 +161,7 @@ function writeJsonFile(filePath: string, value: unknown): void {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
resetFacadeLoaderStateForTest();
|
||||
setFacadeLoaderJitiFactoryForTest(undefined);
|
||||
setFacadeLoaderSourceTransformFactoryForTest(undefined);
|
||||
for (const dir of trustedBundledPluginFixtureRoots.splice(0)) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
@@ -265,13 +267,13 @@ describe("plugin-sdk facade loader", () => {
|
||||
});
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = fixture.bundledPluginsDir;
|
||||
|
||||
const createJitiCalls: Parameters<FacadeLoaderJitiFactory>[] = [];
|
||||
setFacadeLoaderJitiFactoryForTest(((...args) => {
|
||||
const createJitiCalls: Parameters<FacadeLoaderSourceTransformFactory>[] = [];
|
||||
setFacadeLoaderSourceTransformFactoryForTest(((...args) => {
|
||||
createJitiCalls.push(args);
|
||||
return vi.fn(() => ({
|
||||
marker: "jiti-fallback",
|
||||
})) as unknown as ReturnType<FacadeLoaderJitiFactory>;
|
||||
}) as FacadeLoaderJitiFactory);
|
||||
})) as unknown as ReturnType<FacadeLoaderSourceTransformFactory>;
|
||||
}) as FacadeLoaderSourceTransformFactory);
|
||||
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
||||
const restoreVersions = forceNodeRuntimeVersionsForTest();
|
||||
|
||||
|
||||
@@ -5,29 +5,29 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { openBoundaryFileSync } from "../infra/boundary-file-read.js";
|
||||
import { resolveBundledPluginsDir } from "../plugins/bundled-dir.js";
|
||||
import {
|
||||
getCachedPluginJitiLoader,
|
||||
type PluginJitiLoaderCache,
|
||||
type PluginJitiLoaderFactory,
|
||||
} from "../plugins/jiti-loader-cache.js";
|
||||
getCachedPluginModuleLoader,
|
||||
type PluginModuleLoaderCache,
|
||||
type PluginModuleLoaderFactory,
|
||||
} from "../plugins/plugin-module-loader-cache.js";
|
||||
import { resolveLoaderPackageRoot } from "../plugins/sdk-alias.js";
|
||||
import { resolveBundledFacadeModuleLocation } from "./facade-resolution-shared.js";
|
||||
|
||||
const CURRENT_MODULE_PATH = fileURLToPath(import.meta.url);
|
||||
|
||||
const nodeRequire = createRequire(import.meta.url);
|
||||
const jitiLoaders: PluginJitiLoaderCache = new Map();
|
||||
const moduleLoaders: PluginModuleLoaderCache = new Map();
|
||||
const loadedFacadeModules = new Map<string, unknown>();
|
||||
const loadedFacadePluginIds = new Set<string>();
|
||||
let facadeLoaderJitiFactory: PluginJitiLoaderFactory | undefined;
|
||||
let facadeLoaderSourceTransformFactory: PluginModuleLoaderFactory | undefined;
|
||||
let cachedOpenClawPackageRoot: string | undefined;
|
||||
|
||||
function getJitiFactory() {
|
||||
if (facadeLoaderJitiFactory) {
|
||||
return facadeLoaderJitiFactory;
|
||||
function getSourceTransformFactory() {
|
||||
if (facadeLoaderSourceTransformFactory) {
|
||||
return facadeLoaderSourceTransformFactory;
|
||||
}
|
||||
const { createJiti } = nodeRequire("jiti") as typeof import("jiti");
|
||||
facadeLoaderJitiFactory = createJiti;
|
||||
return facadeLoaderJitiFactory;
|
||||
facadeLoaderSourceTransformFactory = createJiti;
|
||||
return facadeLoaderSourceTransformFactory;
|
||||
}
|
||||
|
||||
function getOpenClawPackageRoot() {
|
||||
@@ -56,14 +56,14 @@ function resolveFacadeModuleLocation(params: {
|
||||
});
|
||||
}
|
||||
|
||||
function getJiti(modulePath: string) {
|
||||
return getCachedPluginJitiLoader({
|
||||
cache: jitiLoaders,
|
||||
function getModuleLoader(modulePath: string) {
|
||||
return getCachedPluginModuleLoader({
|
||||
cache: moduleLoaders,
|
||||
modulePath,
|
||||
importerUrl: import.meta.url,
|
||||
preferBuiltDist: true,
|
||||
jitiFilename: import.meta.url,
|
||||
createLoader: getJitiFactory(),
|
||||
loaderFilename: import.meta.url,
|
||||
createLoader: getSourceTransformFactory(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ export function loadFacadeModuleAtLocationSync<T extends object>(params: {
|
||||
try {
|
||||
loaded =
|
||||
params.loadModule?.(location.modulePath) ??
|
||||
(getJiti(location.modulePath)(location.modulePath) as T);
|
||||
(getModuleLoader(location.modulePath)(location.modulePath) as T);
|
||||
Object.assign(sentinel, loaded);
|
||||
loadedFacadePluginIds.add(
|
||||
typeof params.trackedPluginId === "function"
|
||||
@@ -264,13 +264,13 @@ export function listImportedBundledPluginFacadeIds(): string[] {
|
||||
export function resetFacadeLoaderStateForTest(): void {
|
||||
loadedFacadeModules.clear();
|
||||
loadedFacadePluginIds.clear();
|
||||
jitiLoaders.clear();
|
||||
facadeLoaderJitiFactory = undefined;
|
||||
moduleLoaders.clear();
|
||||
facadeLoaderSourceTransformFactory = undefined;
|
||||
cachedOpenClawPackageRoot = undefined;
|
||||
}
|
||||
|
||||
export function setFacadeLoaderJitiFactoryForTest(
|
||||
factory: PluginJitiLoaderFactory | undefined,
|
||||
export function setFacadeLoaderSourceTransformFactoryForTest(
|
||||
factory: PluginModuleLoaderFactory | undefined,
|
||||
): void {
|
||||
facadeLoaderJitiFactory = factory;
|
||||
facadeLoaderSourceTransformFactory = factory;
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { resolveBundledPluginsDir } from "../plugins/bundled-dir.js";
|
||||
import {
|
||||
getCachedPluginJitiLoader,
|
||||
type PluginJitiLoaderCache,
|
||||
} from "../plugins/jiti-loader-cache.js";
|
||||
getCachedPluginSourceModuleLoader,
|
||||
type PluginModuleLoaderCache,
|
||||
} from "../plugins/plugin-module-loader-cache.js";
|
||||
import { resolveLoaderPackageRoot } from "../plugins/sdk-alias.js";
|
||||
import {
|
||||
loadBundledPluginPublicSurfaceModuleSync as loadBundledPluginPublicSurfaceModuleSyncLight,
|
||||
@@ -110,16 +110,15 @@ const FACADE_ACTIVATION_CHECK_RUNTIME_CANDIDATES = [
|
||||
] as const;
|
||||
|
||||
let facadeActivationCheckRuntimeModule: FacadeActivationCheckRuntimeModule | undefined;
|
||||
const facadeActivationCheckRuntimeJitiLoaders: PluginJitiLoaderCache = new Map();
|
||||
const facadeActivationCheckRuntimeLoaders: PluginModuleLoaderCache = new Map();
|
||||
|
||||
function getFacadeActivationCheckRuntimeJiti(modulePath: string) {
|
||||
return getCachedPluginJitiLoader({
|
||||
cache: facadeActivationCheckRuntimeJitiLoaders,
|
||||
function getFacadeActivationCheckRuntimeSourceLoader(modulePath: string) {
|
||||
return getCachedPluginSourceModuleLoader({
|
||||
cache: facadeActivationCheckRuntimeLoaders,
|
||||
modulePath,
|
||||
importerUrl: import.meta.url,
|
||||
jitiFilename: import.meta.url,
|
||||
loaderFilename: import.meta.url,
|
||||
aliasMap: {},
|
||||
tryNative: false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -149,7 +148,7 @@ function loadFacadeActivationCheckRuntime(): FacadeActivationCheckRuntimeModule
|
||||
return facadeActivationCheckRuntimeModule;
|
||||
}
|
||||
facadeActivationCheckRuntimeModule = loadFacadeActivationCheckRuntimeFromCandidates((candidate) =>
|
||||
getFacadeActivationCheckRuntimeJiti(candidate)(candidate),
|
||||
getFacadeActivationCheckRuntimeSourceLoader(candidate)(candidate),
|
||||
);
|
||||
if (facadeActivationCheckRuntimeModule) {
|
||||
return facadeActivationCheckRuntimeModule;
|
||||
@@ -246,7 +245,7 @@ export function tryLoadActivatedBundledPluginPublicSurfaceModuleSync<T extends o
|
||||
export function resetFacadeRuntimeStateForTest(): void {
|
||||
resetFacadeLoaderStateForTest();
|
||||
facadeActivationCheckRuntimeModule = undefined;
|
||||
facadeActivationCheckRuntimeJitiLoaders.clear();
|
||||
facadeActivationCheckRuntimeLoaders.clear();
|
||||
}
|
||||
|
||||
export const __testing = {
|
||||
|
||||
@@ -5,7 +5,7 @@ const fs = require("node:fs");
|
||||
|
||||
let monolithicSdk = null;
|
||||
let diagnosticEventsModule = null;
|
||||
const jitiLoaders = new Map();
|
||||
const moduleLoaders = new Map();
|
||||
const pluginSdkSubpathsCache = new Map();
|
||||
const pluginSdkPackageNames = ["openclaw/plugin-sdk", "@openclaw/plugin-sdk"];
|
||||
const pluginSdkSourceExtensions = [".ts", ".mts", ".js", ".mjs", ".cts", ".cjs"];
|
||||
@@ -194,15 +194,15 @@ function buildPluginSdkAliasMap(useDist) {
|
||||
return aliasMap;
|
||||
}
|
||||
|
||||
function getJiti(tryNative) {
|
||||
function getModuleLoader(tryNative) {
|
||||
const effectiveTryNative = process.platform === "win32" ? false : tryNative;
|
||||
|
||||
if (jitiLoaders.has(effectiveTryNative)) {
|
||||
return jitiLoaders.get(effectiveTryNative);
|
||||
if (moduleLoaders.has(effectiveTryNative)) {
|
||||
return moduleLoaders.get(effectiveTryNative);
|
||||
}
|
||||
|
||||
const { createJiti } = require("jiti");
|
||||
const jitiLoader = createJiti(__filename, {
|
||||
const moduleLoader = createJiti(__filename, {
|
||||
alias: buildPluginSdkAliasMap(effectiveTryNative),
|
||||
interopDefault: true,
|
||||
// Prefer Node's native sync ESM loader for built dist/plugin-sdk/*.js files
|
||||
@@ -210,8 +210,8 @@ function getJiti(tryNative) {
|
||||
tryNative: effectiveTryNative,
|
||||
extensions: [".ts", ".tsx", ".mts", ".cts", ".mtsx", ".ctsx", ".js", ".mjs", ".cjs", ".json"],
|
||||
});
|
||||
jitiLoaders.set(effectiveTryNative, jitiLoader);
|
||||
return jitiLoader;
|
||||
moduleLoaders.set(effectiveTryNative, moduleLoader);
|
||||
return moduleLoader;
|
||||
}
|
||||
|
||||
function loadMonolithicSdk() {
|
||||
@@ -222,14 +222,16 @@ function loadMonolithicSdk() {
|
||||
const distCandidate = path.resolve(__dirname, "..", "..", "dist", "plugin-sdk", "compat.js");
|
||||
if (!shouldPreferSourceGraph && fs.existsSync(distCandidate)) {
|
||||
try {
|
||||
monolithicSdk = getJiti(true)(distCandidate);
|
||||
monolithicSdk = getModuleLoader(true)(distCandidate);
|
||||
return monolithicSdk;
|
||||
} catch {
|
||||
// Fall through to source alias if dist is unavailable or stale.
|
||||
}
|
||||
}
|
||||
|
||||
monolithicSdk = getJiti(false)(path.join(getPackageRoot(), "src", "plugin-sdk", "compat.ts"));
|
||||
monolithicSdk = getModuleLoader(false)(
|
||||
path.join(getPackageRoot(), "src", "plugin-sdk", "compat.ts"),
|
||||
);
|
||||
return monolithicSdk;
|
||||
}
|
||||
|
||||
@@ -252,7 +254,9 @@ function loadDiagnosticEventsModule() {
|
||||
findDistChunkByPrefix("diagnostic-events");
|
||||
if (distCandidate) {
|
||||
try {
|
||||
diagnosticEventsModule = normalizeDiagnosticEventsModule(getJiti(true)(distCandidate));
|
||||
diagnosticEventsModule = normalizeDiagnosticEventsModule(
|
||||
getModuleLoader(true)(distCandidate),
|
||||
);
|
||||
return diagnosticEventsModule;
|
||||
} catch {
|
||||
// Fall through to source path if dist is unavailable or stale.
|
||||
@@ -261,7 +265,7 @@ function loadDiagnosticEventsModule() {
|
||||
}
|
||||
|
||||
diagnosticEventsModule = normalizeDiagnosticEventsModule(
|
||||
getJiti(false)(path.join(getPackageRoot(), "src", "infra", "diagnostic-events.ts")),
|
||||
getModuleLoader(false)(path.join(getPackageRoot(), "src", "infra", "diagnostic-events.ts")),
|
||||
);
|
||||
return diagnosticEventsModule;
|
||||
}
|
||||
|
||||
@@ -9,15 +9,18 @@ import {
|
||||
import { resolveBundledPluginRepoEntryPath } from "./bundled-plugin-metadata.js";
|
||||
import { createCapturedPluginRegistration } from "./captured-registration.js";
|
||||
import { discoverOpenClawPlugins } from "./discovery.js";
|
||||
import { getCachedPluginJitiLoader, type PluginJitiLoaderCache } from "./jiti-loader-cache.js";
|
||||
import type { PluginLoadOptions } from "./loader.js";
|
||||
import { loadPluginManifestRegistry } from "./manifest-registry.js";
|
||||
import { unwrapDefaultModuleExport } from "./module-export.js";
|
||||
import {
|
||||
getCachedPluginModuleLoader,
|
||||
type PluginModuleLoaderCache,
|
||||
} from "./plugin-module-loader-cache.js";
|
||||
import { createEmptyPluginRegistry } from "./registry-empty.js";
|
||||
import type { PluginRecord, PluginRegistry } from "./registry.js";
|
||||
import {
|
||||
buildPluginLoaderAliasMap,
|
||||
shouldPreferNativeJiti,
|
||||
shouldPreferNativeModuleLoad,
|
||||
type PluginSdkResolutionPreference,
|
||||
} from "./sdk-alias.js";
|
||||
import type { OpenClawPluginDefinition, OpenClawPluginModule } from "./types.js";
|
||||
@@ -193,11 +196,12 @@ export function loadBundledCapabilityRuntimeRegistry(params: {
|
||||
const env = params.env ?? process.env;
|
||||
const pluginIds = new Set(params.pluginIds);
|
||||
const registry = createEmptyPluginRegistry();
|
||||
const jitiLoaders: PluginJitiLoaderCache = new Map();
|
||||
const moduleLoaders: PluginModuleLoaderCache = new Map();
|
||||
|
||||
const getJiti = (modulePath: string) => {
|
||||
const getModuleLoader = (modulePath: string) => {
|
||||
const tryNative =
|
||||
shouldPreferNativeJiti(modulePath) && !(env?.VITEST && params.pluginSdkResolution === "dist");
|
||||
shouldPreferNativeModuleLoad(modulePath) &&
|
||||
!(env?.VITEST && params.pluginSdkResolution === "dist");
|
||||
const aliasMap = shouldApplyVitestCapabilityAliasOverrides({
|
||||
pluginSdkResolution: params.pluginSdkResolution,
|
||||
env,
|
||||
@@ -213,11 +217,11 @@ export function loadBundledCapabilityRuntimeRegistry(params: {
|
||||
env,
|
||||
})
|
||||
: undefined;
|
||||
return getCachedPluginJitiLoader({
|
||||
cache: jitiLoaders,
|
||||
return getCachedPluginModuleLoader({
|
||||
cache: moduleLoaders,
|
||||
modulePath,
|
||||
importerUrl: import.meta.url,
|
||||
jitiFilename: import.meta.url,
|
||||
loaderFilename: import.meta.url,
|
||||
...(aliasMap ? { aliasMap } : {}),
|
||||
pluginSdkResolution: params.pluginSdkResolution,
|
||||
tryNative,
|
||||
@@ -289,7 +293,7 @@ export function loadBundledCapabilityRuntimeRegistry(params: {
|
||||
|
||||
let mod: OpenClawPluginModule | null = null;
|
||||
try {
|
||||
mod = getJiti(safeSource)(safeSource) as OpenClawPluginModule;
|
||||
mod = getModuleLoader(safeSource)(safeSource) as OpenClawPluginModule;
|
||||
} catch (error) {
|
||||
recordCapabilityLoadError(registry, record, String(error));
|
||||
continue;
|
||||
|
||||
@@ -7,13 +7,16 @@ import {
|
||||
normalizeBundledPluginStringList,
|
||||
trimBundledPluginString,
|
||||
} from "./bundled-plugin-scan.js";
|
||||
import { getCachedPluginJitiLoader, type PluginJitiLoaderCache } from "./jiti-loader-cache.js";
|
||||
import type { PluginConfigUiHint } from "./manifest-types.js";
|
||||
import type {
|
||||
OpenClawPackageManifest,
|
||||
PluginManifest,
|
||||
PluginManifestChannelConfig,
|
||||
} from "./manifest.js";
|
||||
import {
|
||||
getCachedPluginModuleLoader,
|
||||
type PluginModuleLoaderCache,
|
||||
} from "./plugin-module-loader-cache.js";
|
||||
import { PUBLIC_SURFACE_SOURCE_EXTENSIONS } from "./public-surface-runtime.js";
|
||||
|
||||
const SOURCE_CONFIG_SCHEMA_CANDIDATES = [
|
||||
@@ -32,7 +35,7 @@ type ChannelConfigSurface = {
|
||||
runtime?: ChannelConfigRuntimeSchema;
|
||||
};
|
||||
|
||||
const jitiLoaders: PluginJitiLoaderCache = new Map();
|
||||
const moduleLoaders: PluginModuleLoaderCache = new Map();
|
||||
|
||||
function isBuiltChannelConfigSchema(value: unknown): value is ChannelConfigSurface {
|
||||
if (!value || typeof value !== "object") {
|
||||
@@ -70,13 +73,13 @@ function resolveConfigSchemaExport(imported: Record<string, unknown>): ChannelCo
|
||||
return null;
|
||||
}
|
||||
|
||||
function getJiti(modulePath: string) {
|
||||
return getCachedPluginJitiLoader({
|
||||
cache: jitiLoaders,
|
||||
function getModuleLoader(modulePath: string) {
|
||||
return getCachedPluginModuleLoader({
|
||||
cache: moduleLoaders,
|
||||
modulePath,
|
||||
importerUrl: import.meta.url,
|
||||
preferBuiltDist: true,
|
||||
jitiFilename: import.meta.url,
|
||||
loaderFilename: import.meta.url,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -100,7 +103,7 @@ function resolveChannelConfigSchemaModulePath(pluginDir: string): string | undef
|
||||
|
||||
function loadChannelConfigSurfaceModuleSync(modulePath: string): ChannelConfigSurface | null {
|
||||
try {
|
||||
const imported = getJiti(modulePath)(modulePath) as Record<string, unknown>;
|
||||
const imported = getModuleLoader(modulePath)(modulePath) as Record<string, unknown>;
|
||||
return resolveConfigSchemaExport(imported);
|
||||
} catch {
|
||||
return null;
|
||||
|
||||
@@ -23,7 +23,7 @@ afterEach(() => {
|
||||
cleanupTrackedTempDirs(tempDirs);
|
||||
});
|
||||
|
||||
describe("doctor-contract-registry getJiti", () => {
|
||||
describe("doctor-contract-registry module loader", () => {
|
||||
beforeEach(async () => {
|
||||
resetRegistryJitiMocks();
|
||||
vi.resetModules();
|
||||
@@ -67,7 +67,7 @@ describe("doctor-contract-registry getJiti", () => {
|
||||
expect(mocks.createJiti).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to the Jiti boundary on Windows for TypeScript contract-api modules", () => {
|
||||
it("falls back to the source-transform boundary on Windows for TypeScript contract-api modules", () => {
|
||||
const pluginRoot = makeTempDir();
|
||||
const contractApiPath = path.join(pluginRoot, "contract-api.ts");
|
||||
fs.writeFileSync(
|
||||
|
||||
@@ -4,8 +4,11 @@ import { fileURLToPath } from "node:url";
|
||||
import type { LegacyConfigRule } from "../config/legacy.shared.js";
|
||||
import type { OpenClawConfig } from "../config/types.js";
|
||||
import { asNullableRecord } from "../shared/record-coerce.js";
|
||||
import { getCachedPluginJitiLoader, type PluginJitiLoaderCache } from "./jiti-loader-cache.js";
|
||||
import type { PluginManifestRegistry } from "./manifest-registry.js";
|
||||
import {
|
||||
getCachedPluginModuleLoader,
|
||||
type PluginModuleLoaderCache,
|
||||
} from "./plugin-module-loader-cache.js";
|
||||
import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js";
|
||||
|
||||
const CONTRACT_API_EXTENSIONS = [".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"] as const;
|
||||
@@ -36,11 +39,11 @@ type PluginDoctorContractEntry = {
|
||||
|
||||
type PluginManifestRegistryRecord = PluginManifestRegistry["plugins"][number];
|
||||
|
||||
const jitiLoaders: PluginJitiLoaderCache = new Map();
|
||||
const moduleLoaders: PluginModuleLoaderCache = new Map();
|
||||
|
||||
function loadPluginDoctorContractModule(modulePath: string): PluginDoctorContractModule {
|
||||
return getCachedPluginJitiLoader({
|
||||
cache: jitiLoaders,
|
||||
return getCachedPluginModuleLoader({
|
||||
cache: moduleLoaders,
|
||||
modulePath,
|
||||
importerUrl: import.meta.url,
|
||||
})(modulePath) as PluginDoctorContractModule;
|
||||
@@ -228,7 +231,7 @@ function resolvePluginDoctorContracts(params?: {
|
||||
}
|
||||
|
||||
export function clearPluginDoctorContractRegistryCache(): void {
|
||||
jitiLoaders.clear();
|
||||
moduleLoaders.clear();
|
||||
}
|
||||
|
||||
export function listPluginDoctorLegacyConfigRules(params?: {
|
||||
|
||||
@@ -1872,7 +1872,14 @@ describe("installPluginFromArchive", () => {
|
||||
});
|
||||
|
||||
it("does not flag the real qa-matrix plugin as dangerous install code", async () => {
|
||||
const pluginDir = path.resolve(process.cwd(), "extensions", "qa-matrix");
|
||||
const sourcePluginDir = path.resolve(process.cwd(), "extensions", "qa-matrix");
|
||||
const pluginDir = path.join(suiteTempRootTracker.makeTempDir(), "qa-matrix");
|
||||
fs.cpSync(sourcePluginDir, pluginDir, {
|
||||
recursive: true,
|
||||
filter: (entryPath) =>
|
||||
!path.relative(sourcePluginDir, entryPath).split(path.sep).includes("node_modules"),
|
||||
});
|
||||
vi.mocked(resolveOpenClawPackageRootSync).mockReturnValue(process.cwd());
|
||||
|
||||
const scanResult = await installSecurityScan.scanPackageInstallSource({
|
||||
extensions: ["./index.ts"],
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
isPrereleaseResolutionAllowed,
|
||||
parseRegistryNpmSpec,
|
||||
} from "../infra/npm-registry-spec.js";
|
||||
import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js";
|
||||
import {
|
||||
createSafeNpmInstallArgs,
|
||||
createSafeNpmInstallEnv,
|
||||
@@ -35,6 +34,7 @@ import {
|
||||
type PackageManifest as PluginPackageManifest,
|
||||
} from "./manifest.js";
|
||||
import { validatePackageExtensionEntriesForInstall } from "./package-entry-resolution.js";
|
||||
import { linkOpenClawPeerDependencies } from "./plugin-peer-link.js";
|
||||
|
||||
export { resolvePluginInstallDir } from "./install-paths.js";
|
||||
|
||||
@@ -552,50 +552,6 @@ async function detectNativePackageInstallSource(packageDir: string): Promise<boo
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* After the installed package tree has been scanned, symlink the host openclaw
|
||||
* package for plugins that declare it as a peer dependency.
|
||||
*/
|
||||
async function linkOpenClawPeerDependencies(params: {
|
||||
installedDir: string;
|
||||
peerDependencies: Record<string, string>;
|
||||
logger: PluginInstallLogger;
|
||||
}): Promise<void> {
|
||||
const peers = Object.keys(params.peerDependencies).filter((name) => name === "openclaw");
|
||||
if (peers.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hostRoot = resolveOpenClawPackageRootSync({
|
||||
argv1: process.argv[1],
|
||||
moduleUrl: import.meta.url,
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
if (!hostRoot) {
|
||||
params.logger.warn?.(
|
||||
"Could not locate openclaw package root to symlink peerDependencies; plugin may fail to resolve openclaw at runtime.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeModulesDir = path.join(params.installedDir, "node_modules");
|
||||
await fs.mkdir(nodeModulesDir, { recursive: true });
|
||||
|
||||
for (const peerName of peers) {
|
||||
const linkPath = path.join(nodeModulesDir, peerName);
|
||||
|
||||
try {
|
||||
// Remove any existing entry (broken link or stale directory) before
|
||||
// creating the new symlink so re-installs are idempotent.
|
||||
await fs.rm(linkPath, { recursive: true, force: true });
|
||||
await fs.symlink(hostRoot, linkPath, "junction");
|
||||
params.logger.info?.(`Linked peerDependency "${peerName}" -> ${hostRoot}`);
|
||||
} catch (err) {
|
||||
params.logger.warn?.(`Failed to symlink peerDependency "${peerName}": ${String(err)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type ValidatedPackagePlugin = {
|
||||
manifest: PackageManifest;
|
||||
pluginId: string;
|
||||
|
||||
@@ -27,8 +27,8 @@ describe("plugin loader git path regression", () => {
|
||||
const copiedPluginSdkDir = path.join(copiedExtensionRoot, "plugin-sdk");
|
||||
mkdirSafe(copiedSourceDir);
|
||||
mkdirSafe(copiedPluginSdkDir);
|
||||
const jitiBaseFile = path.join(copiedSourceDir, "__jiti-base__.mjs");
|
||||
fs.writeFileSync(jitiBaseFile, "export {};\n", "utf-8");
|
||||
const sourceLoaderBaseFile = path.join(copiedSourceDir, "__jiti-base__.mjs");
|
||||
fs.writeFileSync(sourceLoaderBaseFile, "export {};\n", "utf-8");
|
||||
fs.writeFileSync(
|
||||
path.join(copiedSourceDir, "channel.runtime.ts"),
|
||||
`import { resolveOutboundSendDep } from "openclaw/plugin-sdk/outbound-send-deps";
|
||||
@@ -59,7 +59,7 @@ export const copiedRuntimeMarker = {
|
||||
const copiedChannelRuntime = path.join(copiedExtensionRoot, "src", "channel.runtime.ts");
|
||||
const script = `
|
||||
import { createJiti } from "jiti";
|
||||
const withoutAlias = createJiti(${JSON.stringify(jitiBaseFile)}, {
|
||||
const withoutAlias = createJiti(${JSON.stringify(sourceLoaderBaseFile)}, {
|
||||
interopDefault: true,
|
||||
tryNative: false,
|
||||
extensions: [".ts", ".tsx", ".mts", ".cts", ".mtsx", ".ctsx", ".js", ".mjs", ".cjs", ".json"],
|
||||
@@ -70,7 +70,7 @@ export const copiedRuntimeMarker = {
|
||||
} catch {
|
||||
withoutAliasThrew = true;
|
||||
}
|
||||
const withAlias = createJiti(${JSON.stringify(jitiBaseFile)}, {
|
||||
const withAlias = createJiti(${JSON.stringify(sourceLoaderBaseFile)}, {
|
||||
interopDefault: true,
|
||||
tryNative: false,
|
||||
extensions: [".ts", ".tsx", ".mts", ".cts", ".mtsx", ".ctsx", ".js", ".mjs", ".cjs", ".json"],
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { importFreshModule } from "openclaw/plugin-sdk/test-fixtures";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
function makeTempDir() {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-plugin-loader-"));
|
||||
tempDirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
function writeBundledPluginFixture(id: string) {
|
||||
const pluginRoot = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(pluginRoot, "openclaw.plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
id,
|
||||
configSchema: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginRoot, "index.cjs"),
|
||||
`module.exports = { id: ${JSON.stringify(id)}, register() {} };`,
|
||||
"utf-8",
|
||||
);
|
||||
return pluginRoot;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetModules();
|
||||
vi.doUnmock("./jiti-loader-cache.js");
|
||||
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe("createPluginModuleLoader", () => {
|
||||
it("loads bundled JavaScript without creating a jiti loader", async () => {
|
||||
const jitiLoaderCalls: Array<{ modulePath: string; jitiFilename?: string }> = [];
|
||||
vi.doMock("./jiti-loader-cache.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./jiti-loader-cache.js")>();
|
||||
return {
|
||||
...actual,
|
||||
getCachedPluginJitiLoader: vi.fn((params) => {
|
||||
jitiLoaderCalls.push({
|
||||
modulePath: params.modulePath,
|
||||
jitiFilename: params.jitiFilename,
|
||||
});
|
||||
return vi.fn(() => ({
|
||||
default: {
|
||||
id: "demo",
|
||||
register() {},
|
||||
},
|
||||
}));
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const { loadOpenClawPlugins } = await importFreshModule<typeof import("./loader.js")>(
|
||||
import.meta.url,
|
||||
"./loader.js?scope=jiti-filename",
|
||||
);
|
||||
|
||||
const pluginRoot = writeBundledPluginFixture("demo");
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = pluginRoot;
|
||||
|
||||
loadOpenClawPlugins({
|
||||
cache: false,
|
||||
workspaceDir: pluginRoot,
|
||||
onlyPluginIds: ["demo"],
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
demo: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(jitiLoaderCalls).toEqual([]);
|
||||
});
|
||||
});
|
||||
176
src/plugins/loader.native-module-loader.test.ts
Normal file
176
src/plugins/loader.native-module-loader.test.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { importFreshModule } from "openclaw/plugin-sdk/test-fixtures";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
function makeTempDir() {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-plugin-loader-"));
|
||||
tempDirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
function writeBundledPluginFixture(id: string) {
|
||||
const pluginRoot = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(pluginRoot, "openclaw.plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
id,
|
||||
configSchema: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginRoot, "index.cjs"),
|
||||
`module.exports = { id: ${JSON.stringify(id)}, register() {} };`,
|
||||
"utf-8",
|
||||
);
|
||||
return pluginRoot;
|
||||
}
|
||||
|
||||
function writePackagedPluginFixture(id: string) {
|
||||
const pluginRoot = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(pluginRoot, "package.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: id,
|
||||
type: "commonjs",
|
||||
openclaw: {
|
||||
extensions: ["./index.cjs"],
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginRoot, "openclaw.plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
id,
|
||||
configSchema: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginRoot, "index.cjs"),
|
||||
`module.exports = { id: ${JSON.stringify(id)}, register() {} };`,
|
||||
"utf-8",
|
||||
);
|
||||
return pluginRoot;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetModules();
|
||||
vi.doUnmock("./plugin-module-loader-cache.js");
|
||||
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
function mockSourceLoaderCalls() {
|
||||
const sourceLoaderCalls: Array<{ modulePath: string; loaderFilename?: string }> = [];
|
||||
vi.doMock("./plugin-module-loader-cache.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./plugin-module-loader-cache.js")>();
|
||||
return {
|
||||
...actual,
|
||||
getCachedPluginSourceModuleLoader: vi.fn((params) => {
|
||||
sourceLoaderCalls.push({
|
||||
modulePath: params.modulePath,
|
||||
loaderFilename: params.loaderFilename,
|
||||
});
|
||||
return vi.fn(() => ({
|
||||
default: {
|
||||
id: "source-fallback",
|
||||
register() {},
|
||||
},
|
||||
}));
|
||||
}),
|
||||
};
|
||||
});
|
||||
return sourceLoaderCalls;
|
||||
}
|
||||
|
||||
describe("createPluginModuleLoader", () => {
|
||||
it("loads bundled JavaScript without creating a module loader", async () => {
|
||||
const sourceLoaderCalls = mockSourceLoaderCalls();
|
||||
|
||||
const { loadOpenClawPlugins } = await importFreshModule<typeof import("./loader.js")>(
|
||||
import.meta.url,
|
||||
"./loader.js?scope=native-module-loader",
|
||||
);
|
||||
|
||||
const pluginRoot = writeBundledPluginFixture("demo");
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = pluginRoot;
|
||||
|
||||
loadOpenClawPlugins({
|
||||
cache: false,
|
||||
workspaceDir: pluginRoot,
|
||||
onlyPluginIds: ["demo"],
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
demo: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(sourceLoaderCalls).toEqual([]);
|
||||
});
|
||||
|
||||
it("loads packaged JavaScript without creating a module loader", async () => {
|
||||
const sourceLoaderCalls = mockSourceLoaderCalls();
|
||||
|
||||
const { loadOpenClawPlugins } = await importFreshModule<typeof import("./loader.js")>(
|
||||
import.meta.url,
|
||||
"./loader.js?scope=packaged-native-module-loader",
|
||||
);
|
||||
|
||||
const pluginRoot = writePackagedPluginFixture("npm-demo");
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = makeTempDir();
|
||||
|
||||
const registry = loadOpenClawPlugins({
|
||||
cache: false,
|
||||
config: {
|
||||
plugins: {
|
||||
enabled: true,
|
||||
load: {
|
||||
paths: [pluginRoot],
|
||||
},
|
||||
allow: ["npm-demo"],
|
||||
entries: {
|
||||
"npm-demo": {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(registry.plugins.find((plugin) => plugin.id === "npm-demo")?.status).toBe("loaded");
|
||||
expect(sourceLoaderCalls).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -56,7 +56,6 @@ import {
|
||||
listPluginInteractiveHandlers,
|
||||
restorePluginInteractiveHandlers,
|
||||
} from "./interactive-registry.js";
|
||||
import { getCachedPluginJitiLoader, type PluginJitiLoaderCache } from "./jiti-loader-cache.js";
|
||||
import { PluginLoaderCacheState } from "./loader-cache-state.js";
|
||||
import {
|
||||
channelPluginIdBelongsToManifest,
|
||||
@@ -104,6 +103,10 @@ import {
|
||||
import { unwrapDefaultModuleExport } from "./module-export.js";
|
||||
import { tryNativeRequireJavaScriptModule } from "./native-module-require.js";
|
||||
import { withProfile } from "./plugin-load-profile.js";
|
||||
import {
|
||||
getCachedPluginSourceModuleLoader,
|
||||
type PluginModuleLoaderCache,
|
||||
} from "./plugin-module-loader-cache.js";
|
||||
import {
|
||||
createPluginIdScopeSet,
|
||||
hasExplicitPluginIdScope,
|
||||
@@ -135,7 +138,7 @@ import {
|
||||
resolvePluginSdkAliasFile,
|
||||
resolvePluginRuntimeModulePath,
|
||||
resolvePluginSdkScopedAliasMap,
|
||||
shouldPreferNativeJiti,
|
||||
shouldPreferNativeModuleLoad,
|
||||
} from "./sdk-alias.js";
|
||||
import { hasKind, kindsEqual } from "./slots.js";
|
||||
import type {
|
||||
@@ -457,13 +460,13 @@ function runPluginRegisterSync(
|
||||
}
|
||||
|
||||
function createPluginModuleLoader(options: Pick<PluginLoadOptions, "pluginSdkResolution">) {
|
||||
const jitiLoaders: PluginJitiLoaderCache = new Map();
|
||||
const loadWithJiti = (modulePath: string) => {
|
||||
return getCachedPluginJitiLoader({
|
||||
cache: jitiLoaders,
|
||||
const moduleLoaders: PluginModuleLoaderCache = new Map();
|
||||
const loadSourceModule = (modulePath: string) => {
|
||||
return getCachedPluginSourceModuleLoader({
|
||||
cache: moduleLoaders,
|
||||
modulePath,
|
||||
importerUrl: import.meta.url,
|
||||
jitiFilename: modulePath,
|
||||
loaderFilename: modulePath,
|
||||
aliasMap: buildPluginLoaderAliasMap(
|
||||
modulePath,
|
||||
process.argv[1],
|
||||
@@ -471,11 +474,10 @@ function createPluginModuleLoader(options: Pick<PluginLoadOptions, "pluginSdkRes
|
||||
options.pluginSdkResolution,
|
||||
),
|
||||
pluginSdkResolution: options.pluginSdkResolution,
|
||||
tryNative: false,
|
||||
});
|
||||
};
|
||||
return (modulePath: string): unknown => {
|
||||
if (shouldPreferNativeJiti(modulePath)) {
|
||||
if (shouldPreferNativeModuleLoad(modulePath)) {
|
||||
const native = tryNativeRequireJavaScriptModule(modulePath, { allowWindows: true });
|
||||
if (native.ok) {
|
||||
return native.moduleExport;
|
||||
@@ -484,7 +486,7 @@ function createPluginModuleLoader(options: Pick<PluginLoadOptions, "pluginSdkRes
|
||||
// Source .ts runtime shims import sibling ".js" specifiers that only exist
|
||||
// after build. Jiti remains the dev/source fallback because it rewrites those
|
||||
// imports against the source graph and applies SDK aliases.
|
||||
return loadWithJiti(modulePath)(toSafeImportPath(modulePath));
|
||||
return loadSourceModule(modulePath)(toSafeImportPath(modulePath));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -510,7 +512,7 @@ export const __testing = {
|
||||
resolvePluginRuntimeModulePath,
|
||||
ensureOpenClawPluginSdkAlias,
|
||||
shouldLoadChannelPluginInSetupRuntime,
|
||||
shouldPreferNativeJiti,
|
||||
shouldPreferNativeModuleLoad,
|
||||
toSafeImportPath,
|
||||
getCompatibleActivePluginRegistry,
|
||||
resolvePluginLoadCacheContext,
|
||||
|
||||
@@ -7,7 +7,7 @@ afterEach(() => {
|
||||
vi.doUnmock("jiti");
|
||||
});
|
||||
|
||||
async function loadCachedPluginJitiLoader(scope: string) {
|
||||
async function loadCachedPluginModuleLoader(scope: string) {
|
||||
const createJiti = vi.fn((filename: string, options?: Record<string, unknown>) =>
|
||||
Object.assign(vi.fn(), {
|
||||
filename,
|
||||
@@ -18,17 +18,17 @@ async function loadCachedPluginJitiLoader(scope: string) {
|
||||
createJiti,
|
||||
}));
|
||||
|
||||
const { getCachedPluginJitiLoader } = await importFreshModule<
|
||||
typeof import("./jiti-loader-cache.js")
|
||||
>(import.meta.url, `./jiti-loader-cache.js?scope=${scope}`);
|
||||
const { getCachedPluginModuleLoader } = await importFreshModule<
|
||||
typeof import("./plugin-module-loader-cache.js")
|
||||
>(import.meta.url, `./plugin-module-loader-cache.js?scope=${scope}`);
|
||||
|
||||
return { createJiti, getCachedPluginJitiLoader };
|
||||
return { createJiti, getCachedPluginModuleLoader };
|
||||
}
|
||||
|
||||
describe("getCachedPluginJitiLoader", () => {
|
||||
describe("getCachedPluginModuleLoader", () => {
|
||||
it("reuses cached loaders for the same module config and filename", async () => {
|
||||
const { createJiti, getCachedPluginJitiLoader } =
|
||||
await loadCachedPluginJitiLoader("cached-loader");
|
||||
const { createJiti, getCachedPluginModuleLoader } =
|
||||
await loadCachedPluginModuleLoader("cached-loader");
|
||||
|
||||
const cache = new Map();
|
||||
const params = {
|
||||
@@ -36,11 +36,11 @@ describe("getCachedPluginJitiLoader", () => {
|
||||
modulePath: "/repo/extensions/demo/index.ts",
|
||||
importerUrl: "file:///repo/src/plugins/setup-registry.ts",
|
||||
argvEntry: "/repo/openclaw.mjs",
|
||||
jitiFilename: "file:///repo/src/plugins/source-loader.ts",
|
||||
loaderFilename: "file:///repo/src/plugins/source-loader.ts",
|
||||
} as const;
|
||||
|
||||
const first = getCachedPluginJitiLoader(params);
|
||||
const second = getCachedPluginJitiLoader(params);
|
||||
const first = getCachedPluginModuleLoader(params);
|
||||
const second = getCachedPluginModuleLoader(params);
|
||||
|
||||
expect(second).toBe(first);
|
||||
first("/repo/extensions/demo/index.ts");
|
||||
@@ -48,26 +48,26 @@ describe("getCachedPluginJitiLoader", () => {
|
||||
expect(cache.size).toBe(1);
|
||||
});
|
||||
|
||||
it("keeps loader caches scoped by jiti filename and dist preference", async () => {
|
||||
const { createJiti, getCachedPluginJitiLoader } =
|
||||
await loadCachedPluginJitiLoader("filename-scope");
|
||||
it("keeps loader caches scoped by loader filename and dist preference", async () => {
|
||||
const { createJiti, getCachedPluginModuleLoader } =
|
||||
await loadCachedPluginModuleLoader("filename-scope");
|
||||
|
||||
const cache = new Map();
|
||||
const first = getCachedPluginJitiLoader({
|
||||
const first = getCachedPluginModuleLoader({
|
||||
cache,
|
||||
modulePath: "/repo/dist/extensions/demo/api.ts",
|
||||
importerUrl: "file:///repo/src/plugins/public-surface-loader.ts",
|
||||
argvEntry: "/repo/openclaw.mjs",
|
||||
preferBuiltDist: true,
|
||||
jitiFilename: "file:///repo/src/plugins/public-surface-loader.ts",
|
||||
loaderFilename: "file:///repo/src/plugins/public-surface-loader.ts",
|
||||
});
|
||||
const second = getCachedPluginJitiLoader({
|
||||
const second = getCachedPluginModuleLoader({
|
||||
cache,
|
||||
modulePath: "/repo/dist/extensions/demo/api.ts",
|
||||
importerUrl: "file:///repo/src/plugins/public-surface-loader.ts",
|
||||
argvEntry: "/repo/openclaw.mjs",
|
||||
preferBuiltDist: true,
|
||||
jitiFilename: "file:///repo/src/plugins/bundled-channel-config-metadata.ts",
|
||||
loaderFilename: "file:///repo/src/plugins/bundled-channel-config-metadata.ts",
|
||||
});
|
||||
|
||||
expect(second).not.toBe(first);
|
||||
@@ -95,25 +95,26 @@ describe("getCachedPluginJitiLoader", () => {
|
||||
});
|
||||
|
||||
it("lets callers override alias maps and tryNative while keeping cache keys stable", async () => {
|
||||
const { createJiti, getCachedPluginJitiLoader } = await loadCachedPluginJitiLoader("overrides");
|
||||
const { createJiti, getCachedPluginModuleLoader } =
|
||||
await loadCachedPluginModuleLoader("overrides");
|
||||
|
||||
const cache = new Map();
|
||||
const first = getCachedPluginJitiLoader({
|
||||
const first = getCachedPluginModuleLoader({
|
||||
cache,
|
||||
modulePath: "/repo/extensions/demo/index.ts",
|
||||
importerUrl: "file:///repo/src/plugins/loader.ts",
|
||||
jitiFilename: "file:///repo/src/plugins/loader.ts",
|
||||
loaderFilename: "file:///repo/src/plugins/loader.ts",
|
||||
aliasMap: {
|
||||
alpha: "/repo/alpha.js",
|
||||
zeta: "/repo/zeta.js",
|
||||
},
|
||||
tryNative: false,
|
||||
});
|
||||
const second = getCachedPluginJitiLoader({
|
||||
const second = getCachedPluginModuleLoader({
|
||||
cache,
|
||||
modulePath: "/repo/extensions/demo/index.ts",
|
||||
importerUrl: "file:///repo/src/plugins/loader.ts",
|
||||
jitiFilename: "file:///repo/src/plugins/loader.ts",
|
||||
loaderFilename: "file:///repo/src/plugins/loader.ts",
|
||||
aliasMap: {
|
||||
zeta: "/repo/zeta.js",
|
||||
alpha: "/repo/alpha.js",
|
||||
@@ -137,26 +138,26 @@ describe("getCachedPluginJitiLoader", () => {
|
||||
});
|
||||
|
||||
it("lets callers intentionally share loaders behind a custom cache scope key", async () => {
|
||||
const { createJiti, getCachedPluginJitiLoader } =
|
||||
await loadCachedPluginJitiLoader("cache-scope-key");
|
||||
const { createJiti, getCachedPluginModuleLoader } =
|
||||
await loadCachedPluginModuleLoader("cache-scope-key");
|
||||
|
||||
const cache = new Map();
|
||||
const first = getCachedPluginJitiLoader({
|
||||
const first = getCachedPluginModuleLoader({
|
||||
cache,
|
||||
modulePath: "/repo/dist/extensions/demo-a/api.js",
|
||||
importerUrl: "file:///repo/src/plugins/public-surface-loader.ts",
|
||||
jitiFilename: "file:///repo/src/plugins/public-surface-loader.ts",
|
||||
loaderFilename: "file:///repo/src/plugins/public-surface-loader.ts",
|
||||
aliasMap: {
|
||||
demo: "/repo/demo-a.js",
|
||||
},
|
||||
tryNative: true,
|
||||
cacheScopeKey: "bundled:native",
|
||||
});
|
||||
const second = getCachedPluginJitiLoader({
|
||||
const second = getCachedPluginModuleLoader({
|
||||
cache,
|
||||
modulePath: "/repo/dist/extensions/demo-b/api.js",
|
||||
importerUrl: "file:///repo/src/plugins/public-surface-loader.ts",
|
||||
jitiFilename: "file:///repo/src/plugins/public-surface-loader.ts",
|
||||
loaderFilename: "file:///repo/src/plugins/public-surface-loader.ts",
|
||||
aliasMap: {
|
||||
demo: "/repo/demo-b.js",
|
||||
},
|
||||
@@ -171,26 +172,26 @@ describe("getCachedPluginJitiLoader", () => {
|
||||
});
|
||||
|
||||
it("reuses pre-normalized alias options across module-scoped loader filenames", async () => {
|
||||
const { createJiti, getCachedPluginJitiLoader } =
|
||||
await loadCachedPluginJitiLoader("module-filename-aliases");
|
||||
const { createJiti, getCachedPluginModuleLoader } =
|
||||
await loadCachedPluginModuleLoader("module-filename-aliases");
|
||||
|
||||
const cache = new Map();
|
||||
getCachedPluginJitiLoader({
|
||||
getCachedPluginModuleLoader({
|
||||
cache,
|
||||
modulePath: "/repo/extensions/demo-a/index.ts",
|
||||
importerUrl: "file:///repo/src/plugins/loader.ts",
|
||||
jitiFilename: "/repo/extensions/demo-a/index.ts",
|
||||
loaderFilename: "/repo/extensions/demo-a/index.ts",
|
||||
aliasMap: {
|
||||
alpha: "/repo/alpha",
|
||||
beta: "alpha/sub",
|
||||
},
|
||||
tryNative: false,
|
||||
});
|
||||
getCachedPluginJitiLoader({
|
||||
getCachedPluginModuleLoader({
|
||||
cache,
|
||||
modulePath: "/repo/extensions/demo-b/index.ts",
|
||||
importerUrl: "file:///repo/src/plugins/loader.ts",
|
||||
jitiFilename: "/repo/extensions/demo-b/index.ts",
|
||||
loaderFilename: "/repo/extensions/demo-b/index.ts",
|
||||
aliasMap: {
|
||||
beta: "alpha/sub",
|
||||
alpha: "/repo/alpha",
|
||||
@@ -198,22 +199,22 @@ describe("getCachedPluginJitiLoader", () => {
|
||||
tryNative: false,
|
||||
});
|
||||
|
||||
getCachedPluginJitiLoader({
|
||||
getCachedPluginModuleLoader({
|
||||
cache,
|
||||
modulePath: "/repo/extensions/demo-a/index.ts",
|
||||
importerUrl: "file:///repo/src/plugins/loader.ts",
|
||||
jitiFilename: "/repo/extensions/demo-a/index.ts",
|
||||
loaderFilename: "/repo/extensions/demo-a/index.ts",
|
||||
aliasMap: {
|
||||
alpha: "/repo/alpha",
|
||||
beta: "alpha/sub",
|
||||
},
|
||||
tryNative: false,
|
||||
})("/repo/extensions/demo-a/index.ts");
|
||||
getCachedPluginJitiLoader({
|
||||
getCachedPluginModuleLoader({
|
||||
cache,
|
||||
modulePath: "/repo/extensions/demo-b/index.ts",
|
||||
importerUrl: "file:///repo/src/plugins/loader.ts",
|
||||
jitiFilename: "/repo/extensions/demo-b/index.ts",
|
||||
loaderFilename: "/repo/extensions/demo-b/index.ts",
|
||||
aliasMap: {
|
||||
beta: "alpha/sub",
|
||||
alpha: "/repo/alpha",
|
||||
@@ -232,9 +233,9 @@ describe("getCachedPluginJitiLoader", () => {
|
||||
expect((firstAlias as Record<symbol, unknown>)[marker]).toBe(true);
|
||||
});
|
||||
|
||||
it("serves compiled .js targets from native require without invoking the jiti loader", async () => {
|
||||
const jitiLoader = vi.fn();
|
||||
const createJiti = vi.fn(() => jitiLoader);
|
||||
it("serves compiled .js targets from native require without invoking the module loader", async () => {
|
||||
const fromSourceTransformer = vi.fn();
|
||||
const createJiti = vi.fn(() => fromSourceTransformer);
|
||||
vi.doMock("jiti", () => ({ createJiti }));
|
||||
const nativeStub = vi.fn((target: string) => ({
|
||||
ok: true as const,
|
||||
@@ -245,16 +246,16 @@ describe("getCachedPluginJitiLoader", () => {
|
||||
p.endsWith(".js") || p.endsWith(".mjs") || p.endsWith(".cjs"),
|
||||
tryNativeRequireJavaScriptModule: nativeStub,
|
||||
}));
|
||||
const { getCachedPluginJitiLoader } = await importFreshModule<
|
||||
typeof import("./jiti-loader-cache.js")
|
||||
>(import.meta.url, "./jiti-loader-cache.js?scope=native-require-fastpath");
|
||||
const { getCachedPluginModuleLoader } = await importFreshModule<
|
||||
typeof import("./plugin-module-loader-cache.js")
|
||||
>(import.meta.url, "./plugin-module-loader-cache.js?scope=native-require-fastpath");
|
||||
|
||||
const cache = new Map();
|
||||
const loader = getCachedPluginJitiLoader({
|
||||
const loader = getCachedPluginModuleLoader({
|
||||
cache,
|
||||
modulePath: "/repo/dist/extensions/demo/api.js",
|
||||
importerUrl: "file:///repo/src/plugins/public-surface-loader.ts",
|
||||
jitiFilename: "file:///repo/src/plugins/public-surface-loader.ts",
|
||||
loaderFilename: "file:///repo/src/plugins/public-surface-loader.ts",
|
||||
});
|
||||
|
||||
const result = loader("/repo/dist/extensions/demo/api.js") as { loadedFrom: string };
|
||||
@@ -262,57 +263,57 @@ describe("getCachedPluginJitiLoader", () => {
|
||||
// Jiti should not be constructed or invoked for .js targets that
|
||||
// `tryNativeRequireJavaScriptModule` resolves.
|
||||
expect(createJiti).not.toHaveBeenCalled();
|
||||
expect(jitiLoader).not.toHaveBeenCalled();
|
||||
expect(fromSourceTransformer).not.toHaveBeenCalled();
|
||||
// allowWindows must be passed so the native fast path works on Windows too.
|
||||
expect(nativeStub).toHaveBeenCalledWith("/repo/dist/extensions/demo/api.js", {
|
||||
allowWindows: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to jiti when the native-require helper declines", async () => {
|
||||
const jitiLoader = vi.fn(() => ({ fromJiti: true }));
|
||||
const createJiti = vi.fn(() => jitiLoader);
|
||||
it("falls back to source transform when the native-require helper declines", async () => {
|
||||
const fromSourceTransformer = vi.fn(() => ({ fromSourceTransform: true }));
|
||||
const createJiti = vi.fn(() => fromSourceTransformer);
|
||||
vi.doMock("jiti", () => ({ createJiti }));
|
||||
vi.doMock("./native-module-require.js", () => ({
|
||||
isJavaScriptModulePath: () => true,
|
||||
tryNativeRequireJavaScriptModule: () => ({ ok: false }),
|
||||
}));
|
||||
const { getCachedPluginJitiLoader } = await importFreshModule<
|
||||
typeof import("./jiti-loader-cache.js")
|
||||
>(import.meta.url, "./jiti-loader-cache.js?scope=native-require-fallback");
|
||||
const { getCachedPluginModuleLoader } = await importFreshModule<
|
||||
typeof import("./plugin-module-loader-cache.js")
|
||||
>(import.meta.url, "./plugin-module-loader-cache.js?scope=native-require-fallback");
|
||||
|
||||
const cache = new Map();
|
||||
const loader = getCachedPluginJitiLoader({
|
||||
const loader = getCachedPluginModuleLoader({
|
||||
cache,
|
||||
modulePath: "/repo/dist/extensions/demo/api.js",
|
||||
importerUrl: "file:///repo/src/plugins/public-surface-loader.ts",
|
||||
jitiFilename: "file:///repo/src/plugins/public-surface-loader.ts",
|
||||
loaderFilename: "file:///repo/src/plugins/public-surface-loader.ts",
|
||||
});
|
||||
|
||||
const result = loader("/repo/dist/extensions/demo/api.js") as { fromJiti: boolean };
|
||||
expect(result.fromJiti).toBe(true);
|
||||
expect(jitiLoader).toHaveBeenCalledWith("/repo/dist/extensions/demo/api.js");
|
||||
const result = loader("/repo/dist/extensions/demo/api.js") as { fromSourceTransform: boolean };
|
||||
expect(result.fromSourceTransform).toBe(true);
|
||||
expect(fromSourceTransformer).toHaveBeenCalledWith("/repo/dist/extensions/demo/api.js");
|
||||
});
|
||||
|
||||
it("normalizes Windows absolute paths before creating and calling jiti", async () => {
|
||||
it("normalizes Windows absolute paths before creating and calling the source transformer", async () => {
|
||||
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
||||
const jitiLoader = vi.fn(() => ({ fromJiti: true }));
|
||||
const createJiti = vi.fn(() => jitiLoader);
|
||||
const fromSourceTransformer = vi.fn(() => ({ fromSourceTransform: true }));
|
||||
const createJiti = vi.fn(() => fromSourceTransformer);
|
||||
vi.doMock("jiti", () => ({ createJiti }));
|
||||
vi.doMock("./native-module-require.js", () => ({
|
||||
isJavaScriptModulePath: () => true,
|
||||
tryNativeRequireJavaScriptModule: () => ({ ok: false }),
|
||||
}));
|
||||
const { getCachedPluginJitiLoader } = await importFreshModule<
|
||||
typeof import("./jiti-loader-cache.js")
|
||||
>(import.meta.url, "./jiti-loader-cache.js?scope=windows-jiti-paths");
|
||||
const { getCachedPluginModuleLoader } = await importFreshModule<
|
||||
typeof import("./plugin-module-loader-cache.js")
|
||||
>(import.meta.url, "./plugin-module-loader-cache.js?scope=windows-jiti-paths");
|
||||
|
||||
const cache = new Map();
|
||||
const loader = getCachedPluginJitiLoader({
|
||||
const loader = getCachedPluginModuleLoader({
|
||||
cache,
|
||||
modulePath: "C:\\Users\\alice\\openclaw\\dist\\extensions\\feishu\\api.js",
|
||||
importerUrl: "file:///C:/Users/alice/openclaw/dist/src/plugins/public-surface-loader.js",
|
||||
jitiFilename: "C:\\Users\\alice\\openclaw\\dist\\extensions\\feishu\\api.js",
|
||||
loaderFilename: "C:\\Users\\alice\\openclaw\\dist\\extensions\\feishu\\api.js",
|
||||
tryNative: true,
|
||||
});
|
||||
|
||||
@@ -322,62 +323,62 @@ describe("getCachedPluginJitiLoader", () => {
|
||||
"file:///C:/Users/alice/openclaw/dist/extensions/feishu/api.js",
|
||||
expect.objectContaining({ tryNative: true }),
|
||||
);
|
||||
expect(jitiLoader).toHaveBeenCalledWith(
|
||||
expect(fromSourceTransformer).toHaveBeenCalledWith(
|
||||
"file:///C:/Users/alice/openclaw/dist/extensions/feishu/api.js",
|
||||
);
|
||||
});
|
||||
|
||||
it("skips the native-require fast path when tryNative is explicitly false", async () => {
|
||||
const jitiLoader = vi.fn(() => ({ fromJiti: true }));
|
||||
const createJiti = vi.fn(() => jitiLoader);
|
||||
const fromSourceTransformer = vi.fn(() => ({ fromSourceTransform: true }));
|
||||
const createJiti = vi.fn(() => fromSourceTransformer);
|
||||
vi.doMock("jiti", () => ({ createJiti }));
|
||||
const nativeStub = vi.fn(() => ({ ok: true, moduleExport: { fromNative: true } }));
|
||||
vi.doMock("./native-module-require.js", () => ({
|
||||
isJavaScriptModulePath: () => true,
|
||||
tryNativeRequireJavaScriptModule: nativeStub,
|
||||
}));
|
||||
const { getCachedPluginJitiLoader } = await importFreshModule<
|
||||
typeof import("./jiti-loader-cache.js")
|
||||
>(import.meta.url, "./jiti-loader-cache.js?scope=native-require-opt-out");
|
||||
const { getCachedPluginModuleLoader } = await importFreshModule<
|
||||
typeof import("./plugin-module-loader-cache.js")
|
||||
>(import.meta.url, "./plugin-module-loader-cache.js?scope=native-require-opt-out");
|
||||
|
||||
const cache = new Map();
|
||||
const loader = getCachedPluginJitiLoader({
|
||||
const loader = getCachedPluginModuleLoader({
|
||||
cache,
|
||||
modulePath: "/repo/dist/extensions/demo/api.js",
|
||||
importerUrl: "file:///repo/src/plugins/bundled-capability-runtime.ts",
|
||||
jitiFilename: "file:///repo/src/plugins/bundled-capability-runtime.ts",
|
||||
loaderFilename: "file:///repo/src/plugins/bundled-capability-runtime.ts",
|
||||
aliasMap: { "openclaw/plugin-sdk": "/repo/shim.js" },
|
||||
tryNative: false,
|
||||
});
|
||||
|
||||
const result = loader("/repo/dist/extensions/demo/api.js") as { fromJiti: boolean };
|
||||
expect(result.fromJiti).toBe(true);
|
||||
// With tryNative: false the wrapper must route every target through jiti
|
||||
const result = loader("/repo/dist/extensions/demo/api.js") as { fromSourceTransform: boolean };
|
||||
expect(result.fromSourceTransform).toBe(true);
|
||||
// With tryNative: false the wrapper must route every target through the source transformer
|
||||
// so its alias rewrites still apply; native require must not be consulted.
|
||||
expect(nativeStub).not.toHaveBeenCalled();
|
||||
expect(jitiLoader).toHaveBeenCalledWith("/repo/dist/extensions/demo/api.js");
|
||||
expect(fromSourceTransformer).toHaveBeenCalledWith("/repo/dist/extensions/demo/api.js");
|
||||
});
|
||||
|
||||
it("normalizes Windows absolute paths when native loading is disabled", async () => {
|
||||
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
||||
const jitiLoader = vi.fn(() => ({ fromJiti: true }));
|
||||
const createJiti = vi.fn(() => jitiLoader);
|
||||
const fromSourceTransformer = vi.fn(() => ({ fromSourceTransform: true }));
|
||||
const createJiti = vi.fn(() => fromSourceTransformer);
|
||||
vi.doMock("jiti", () => ({ createJiti }));
|
||||
const nativeStub = vi.fn(() => ({ ok: true, moduleExport: { fromNative: true } }));
|
||||
vi.doMock("./native-module-require.js", () => ({
|
||||
isJavaScriptModulePath: () => true,
|
||||
tryNativeRequireJavaScriptModule: nativeStub,
|
||||
}));
|
||||
const { getCachedPluginJitiLoader } = await importFreshModule<
|
||||
typeof import("./jiti-loader-cache.js")
|
||||
>(import.meta.url, "./jiti-loader-cache.js?scope=windows-jiti-no-native");
|
||||
const { getCachedPluginModuleLoader } = await importFreshModule<
|
||||
typeof import("./plugin-module-loader-cache.js")
|
||||
>(import.meta.url, "./plugin-module-loader-cache.js?scope=windows-jiti-no-native");
|
||||
|
||||
const cache = new Map();
|
||||
const loader = getCachedPluginJitiLoader({
|
||||
const loader = getCachedPluginModuleLoader({
|
||||
cache,
|
||||
modulePath: "C:\\Users\\alice\\openclaw\\extensions\\feishu\\api.ts",
|
||||
importerUrl: "file:///C:/Users/alice/openclaw/src/plugins/loader.ts",
|
||||
jitiFilename: "C:\\Users\\alice\\openclaw\\extensions\\feishu\\api.ts",
|
||||
loaderFilename: "C:\\Users\\alice\\openclaw\\extensions\\feishu\\api.ts",
|
||||
tryNative: false,
|
||||
});
|
||||
|
||||
@@ -388,33 +389,37 @@ describe("getCachedPluginJitiLoader", () => {
|
||||
"file:///C:/Users/alice/openclaw/extensions/feishu/api.ts",
|
||||
expect.objectContaining({ tryNative: false }),
|
||||
);
|
||||
expect(jitiLoader).toHaveBeenCalledWith(
|
||||
expect(fromSourceTransformer).toHaveBeenCalledWith(
|
||||
"file:///C:/Users/alice/openclaw/extensions/feishu/api.ts",
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards extra loader arguments through to the jiti fallback", async () => {
|
||||
const jitiLoader = vi.fn(() => ({ fromJiti: true }));
|
||||
const createJiti = vi.fn(() => jitiLoader);
|
||||
it("forwards extra loader arguments through to the source-transform fallback", async () => {
|
||||
const fromSourceTransformer = vi.fn(() => ({ fromSourceTransform: true }));
|
||||
const createJiti = vi.fn(() => fromSourceTransformer);
|
||||
vi.doMock("jiti", () => ({ createJiti }));
|
||||
vi.doMock("./native-module-require.js", () => ({
|
||||
isJavaScriptModulePath: () => true,
|
||||
tryNativeRequireJavaScriptModule: () => ({ ok: false }),
|
||||
}));
|
||||
const { getCachedPluginJitiLoader } = await importFreshModule<
|
||||
typeof import("./jiti-loader-cache.js")
|
||||
>(import.meta.url, "./jiti-loader-cache.js?scope=native-require-rest-args");
|
||||
const { getCachedPluginModuleLoader } = await importFreshModule<
|
||||
typeof import("./plugin-module-loader-cache.js")
|
||||
>(import.meta.url, "./plugin-module-loader-cache.js?scope=native-require-rest-args");
|
||||
|
||||
const cache = new Map();
|
||||
const loader = getCachedPluginJitiLoader({
|
||||
const loader = getCachedPluginModuleLoader({
|
||||
cache,
|
||||
modulePath: "/repo/dist/extensions/demo/api.js",
|
||||
importerUrl: "file:///repo/src/plugins/public-surface-loader.ts",
|
||||
jitiFilename: "file:///repo/src/plugins/public-surface-loader.ts",
|
||||
loaderFilename: "file:///repo/src/plugins/public-surface-loader.ts",
|
||||
});
|
||||
|
||||
const loose = loader as unknown as (t: string, ...a: unknown[]) => unknown;
|
||||
loose("/repo/dist/extensions/demo/api.js", { hint: "x" }, 42);
|
||||
expect(jitiLoader).toHaveBeenCalledWith("/repo/dist/extensions/demo/api.js", { hint: "x" }, 42);
|
||||
expect(fromSourceTransformer).toHaveBeenCalledWith(
|
||||
"/repo/dist/extensions/demo/api.js",
|
||||
{ hint: "x" },
|
||||
42,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -3,31 +3,31 @@ import { toSafeImportPath } from "../shared/import-specifier.js";
|
||||
import { tryNativeRequireJavaScriptModule } from "./native-module-require.js";
|
||||
import {
|
||||
buildPluginLoaderJitiOptions,
|
||||
createPluginLoaderJitiCacheKey,
|
||||
resolvePluginLoaderJitiConfig,
|
||||
createPluginLoaderModuleCacheKey,
|
||||
resolvePluginLoaderModuleConfig,
|
||||
type PluginSdkResolutionPreference,
|
||||
} from "./sdk-alias.js";
|
||||
|
||||
export type PluginJitiLoader = ReturnType<typeof createJiti>;
|
||||
export type PluginJitiLoaderFactory = typeof createJiti;
|
||||
export type PluginJitiLoaderCache = Map<string, PluginJitiLoader>;
|
||||
export type PluginModuleLoader = ReturnType<typeof createJiti>;
|
||||
export type PluginModuleLoaderFactory = typeof createJiti;
|
||||
export type PluginModuleLoaderCache = Map<string, PluginModuleLoader>;
|
||||
|
||||
export function getCachedPluginJitiLoader(params: {
|
||||
cache: PluginJitiLoaderCache;
|
||||
export function getCachedPluginModuleLoader(params: {
|
||||
cache: PluginModuleLoaderCache;
|
||||
modulePath: string;
|
||||
importerUrl: string;
|
||||
argvEntry?: string;
|
||||
preferBuiltDist?: boolean;
|
||||
jitiFilename?: string;
|
||||
createLoader?: PluginJitiLoaderFactory;
|
||||
loaderFilename?: string;
|
||||
createLoader?: PluginModuleLoaderFactory;
|
||||
aliasMap?: Record<string, string>;
|
||||
tryNative?: boolean;
|
||||
pluginSdkResolution?: PluginSdkResolutionPreference;
|
||||
cacheScopeKey?: string;
|
||||
}): PluginJitiLoader {
|
||||
const jitiFilename = toSafeImportPath(params.jitiFilename ?? params.modulePath);
|
||||
}): PluginModuleLoader {
|
||||
const loaderFilename = toSafeImportPath(params.loaderFilename ?? params.modulePath);
|
||||
if (params.cacheScopeKey) {
|
||||
const scopedCacheKey = `${jitiFilename}::${params.cacheScopeKey}`;
|
||||
const scopedCacheKey = `${loaderFilename}::${params.cacheScopeKey}`;
|
||||
const cached = params.cache.get(scopedCacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
@@ -37,7 +37,7 @@ export function getCachedPluginJitiLoader(params: {
|
||||
const hasTryNativeOverride = typeof params.tryNative === "boolean";
|
||||
const defaultConfig =
|
||||
hasAliasOverride || hasTryNativeOverride
|
||||
? resolvePluginLoaderJitiConfig({
|
||||
? resolvePluginLoaderModuleConfig({
|
||||
modulePath: params.modulePath,
|
||||
argv1: params.argvEntry ?? process.argv[1],
|
||||
moduleUrl: params.importerUrl,
|
||||
@@ -57,7 +57,7 @@ export function getCachedPluginJitiLoader(params: {
|
||||
aliasMap: params.aliasMap ?? defaultConfig.aliasMap,
|
||||
cacheKey: canReuseDefaultCacheKey ? defaultConfig.cacheKey : undefined,
|
||||
}
|
||||
: resolvePluginLoaderJitiConfig({
|
||||
: resolvePluginLoaderModuleConfig({
|
||||
modulePath: params.modulePath,
|
||||
argv1: params.argvEntry ?? process.argv[1],
|
||||
moduleUrl: params.importerUrl,
|
||||
@@ -67,25 +67,25 @@ export function getCachedPluginJitiLoader(params: {
|
||||
const { tryNative, aliasMap } = resolved;
|
||||
const cacheKey =
|
||||
resolved.cacheKey ??
|
||||
createPluginLoaderJitiCacheKey({
|
||||
createPluginLoaderModuleCacheKey({
|
||||
tryNative,
|
||||
aliasMap,
|
||||
});
|
||||
const scopedCacheKey = `${jitiFilename}::${params.cacheScopeKey ?? cacheKey}`;
|
||||
const scopedCacheKey = `${loaderFilename}::${params.cacheScopeKey ?? cacheKey}`;
|
||||
const cached = params.cache.get(scopedCacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
let loadWithJiti: PluginJitiLoader | undefined;
|
||||
const getLoadWithJiti = (): PluginJitiLoader => {
|
||||
if (loadWithJiti) {
|
||||
return loadWithJiti;
|
||||
let loadWithSourceTransform: PluginModuleLoader | undefined;
|
||||
const getLoadWithSourceTransform = (): PluginModuleLoader => {
|
||||
if (loadWithSourceTransform) {
|
||||
return loadWithSourceTransform;
|
||||
}
|
||||
const jitiLoader = (params.createLoader ?? createJiti)(jitiFilename, {
|
||||
const jitiLoader = (params.createLoader ?? createJiti)(loaderFilename, {
|
||||
...buildPluginLoaderJitiOptions(aliasMap),
|
||||
tryNative,
|
||||
});
|
||||
loadWithJiti = new Proxy(jitiLoader, {
|
||||
loadWithSourceTransform = new Proxy(jitiLoader, {
|
||||
apply(target, thisArg, argArray) {
|
||||
const [first, ...rest] = argArray as [unknown, ...unknown[]];
|
||||
if (typeof first === "string") {
|
||||
@@ -97,7 +97,7 @@ export function getCachedPluginJitiLoader(params: {
|
||||
return Reflect.apply(target, thisArg, argArray as never) as never;
|
||||
},
|
||||
});
|
||||
return loadWithJiti;
|
||||
return loadWithSourceTransform;
|
||||
};
|
||||
// When the caller has explicitly opted out of native loading (for example
|
||||
// `bundled-capability-runtime` in Vitest+dist mode, which depends on
|
||||
@@ -105,10 +105,10 @@ export function getCachedPluginJitiLoader(params: {
|
||||
// target through jiti so those alias rewrites still apply.
|
||||
if (!tryNative) {
|
||||
const loader = ((target: string, ...rest: unknown[]) =>
|
||||
(getLoadWithJiti() as (t: string, ...a: unknown[]) => unknown)(
|
||||
(getLoadWithSourceTransform() as (t: string, ...a: unknown[]) => unknown)(
|
||||
target,
|
||||
...rest,
|
||||
)) as PluginJitiLoader;
|
||||
)) as PluginModuleLoader;
|
||||
params.cache.set(scopedCacheKey, loader);
|
||||
return loader;
|
||||
}
|
||||
@@ -124,8 +124,20 @@ export function getCachedPluginJitiLoader(params: {
|
||||
if (native.ok) {
|
||||
return native.moduleExport;
|
||||
}
|
||||
return (getLoadWithJiti() as (t: string, ...a: unknown[]) => unknown)(target, ...rest);
|
||||
}) as PluginJitiLoader;
|
||||
return (getLoadWithSourceTransform() as (t: string, ...a: unknown[]) => unknown)(
|
||||
target,
|
||||
...rest,
|
||||
);
|
||||
}) as PluginModuleLoader;
|
||||
params.cache.set(scopedCacheKey, loader);
|
||||
return loader;
|
||||
}
|
||||
|
||||
export function getCachedPluginSourceModuleLoader(
|
||||
params: Omit<Parameters<typeof getCachedPluginModuleLoader>[0], "tryNative">,
|
||||
): PluginModuleLoader {
|
||||
return getCachedPluginModuleLoader({
|
||||
...params,
|
||||
tryNative: false,
|
||||
});
|
||||
}
|
||||
51
src/plugins/plugin-peer-link.ts
Normal file
51
src/plugins/plugin-peer-link.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js";
|
||||
|
||||
type PluginPeerLinkLogger = {
|
||||
info?: (message: string) => void;
|
||||
warn?: (message: string) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Symlink the host openclaw package for plugins that declare it as a peer.
|
||||
* Plugin package managers still own third-party dependencies; this only wires
|
||||
* the host SDK package into the plugin-local Node graph.
|
||||
*/
|
||||
export async function linkOpenClawPeerDependencies(params: {
|
||||
installedDir: string;
|
||||
peerDependencies: Record<string, string>;
|
||||
logger: PluginPeerLinkLogger;
|
||||
}): Promise<void> {
|
||||
const peers = Object.keys(params.peerDependencies).filter((name) => name === "openclaw");
|
||||
if (peers.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hostRoot = resolveOpenClawPackageRootSync({
|
||||
argv1: process.argv[1],
|
||||
moduleUrl: import.meta.url,
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
if (!hostRoot) {
|
||||
params.logger.warn?.(
|
||||
"Could not locate openclaw package root to symlink peerDependencies; plugin may fail to resolve openclaw at runtime.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeModulesDir = path.join(params.installedDir, "node_modules");
|
||||
await fs.mkdir(nodeModulesDir, { recursive: true });
|
||||
|
||||
for (const peerName of peers) {
|
||||
const linkPath = path.join(nodeModulesDir, peerName);
|
||||
|
||||
try {
|
||||
await fs.rm(linkPath, { recursive: true, force: true });
|
||||
await fs.symlink(hostRoot, linkPath, "junction");
|
||||
params.logger.info?.(`Linked peerDependency "${peerName}" -> ${hostRoot}`);
|
||||
} catch (err) {
|
||||
params.logger.warn?.(`Failed to symlink peerDependency "${peerName}": ${String(err)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,12 @@ import { fileURLToPath } from "node:url";
|
||||
import { openBoundaryFileSync } from "../infra/boundary-file-read.js";
|
||||
import { sameFileIdentity } from "../infra/file-identity.js";
|
||||
import { resolveBundledPluginsDir } from "./bundled-dir.js";
|
||||
import { getCachedPluginJitiLoader, type PluginJitiLoaderCache } from "./jiti-loader-cache.js";
|
||||
import {
|
||||
getCachedPluginModuleLoader,
|
||||
type PluginModuleLoaderCache,
|
||||
} from "./plugin-module-loader-cache.js";
|
||||
import { resolveBundledPluginPublicSurfacePath } from "./public-surface-runtime.js";
|
||||
import { resolvePluginLoaderJitiTryNative, resolveLoaderPackageRoot } from "./sdk-alias.js";
|
||||
import { resolvePluginLoaderTryNative, resolveLoaderPackageRoot } from "./sdk-alias.js";
|
||||
|
||||
const OPENCLAW_PACKAGE_ROOT =
|
||||
resolveLoaderPackageRoot({
|
||||
@@ -23,7 +26,7 @@ const publicSurfaceLocations = new Map<
|
||||
boundaryRoot: string;
|
||||
} | null
|
||||
>();
|
||||
const jitiLoaders: PluginJitiLoaderCache = new Map();
|
||||
const moduleLoaders: PluginModuleLoaderCache = new Map();
|
||||
|
||||
function isSourceArtifactPath(modulePath: string): boolean {
|
||||
switch (path.extname(modulePath).toLowerCase()) {
|
||||
@@ -88,22 +91,22 @@ function resolvePublicSurfaceLocation(params: {
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function getJiti(modulePath: string) {
|
||||
return getCachedPluginJitiLoader({
|
||||
cache: jitiLoaders,
|
||||
function getModuleLoader(modulePath: string) {
|
||||
return getCachedPluginModuleLoader({
|
||||
cache: moduleLoaders,
|
||||
modulePath,
|
||||
importerUrl: import.meta.url,
|
||||
preferBuiltDist: true,
|
||||
jitiFilename: import.meta.url,
|
||||
loaderFilename: import.meta.url,
|
||||
});
|
||||
}
|
||||
|
||||
function loadPublicSurfaceModule(modulePath: string): unknown {
|
||||
const tryNative = resolvePluginLoaderJitiTryNative(modulePath, { preferBuiltDist: true });
|
||||
const tryNative = resolvePluginLoaderTryNative(modulePath, { preferBuiltDist: true });
|
||||
if (canUseSourceArtifactRequire({ modulePath, tryNative })) {
|
||||
return sourceArtifactRequire(modulePath);
|
||||
}
|
||||
return getJiti(modulePath)(modulePath);
|
||||
return getModuleLoader(modulePath)(modulePath);
|
||||
}
|
||||
|
||||
// oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- Dynamic public artifact loaders use caller-supplied module surface types.
|
||||
@@ -170,5 +173,5 @@ export function resolveBundledPluginPublicArtifactPath(params: {
|
||||
export function resetBundledPluginPublicArtifactLoaderForTest(): void {
|
||||
loadedPublicSurfaceModules.clear();
|
||||
publicSurfaceLocations.clear();
|
||||
jitiLoaders.clear();
|
||||
moduleLoaders.clear();
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import path from "node:path";
|
||||
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 type { PluginModuleLoaderCache } from "./plugin-module-loader-cache.js";
|
||||
import { loadPluginBoundaryModule } from "./runtime/runtime-plugin-boundary.js";
|
||||
import { cleanupTrackedTempDirs, makeTrackedTempDir } from "./test-helpers/fs-fixtures.js";
|
||||
|
||||
@@ -122,7 +122,7 @@ function createExternalTypeScriptRuntimePackageFixture() {
|
||||
}
|
||||
|
||||
function loadWhatsAppBoundaryModules(runtimePluginDir: string) {
|
||||
const loaders: PluginJitiLoaderCache = new Map();
|
||||
const loaders: PluginModuleLoaderCache = new Map();
|
||||
return {
|
||||
light: loadPluginBoundaryModule<LightModule>(
|
||||
path.join(runtimePluginDir, "light-runtime-api.js"),
|
||||
@@ -165,7 +165,7 @@ describe("runtime plugin boundary whatsapp seam", () => {
|
||||
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();
|
||||
const loaders: PluginModuleLoaderCache = new Map();
|
||||
|
||||
expect(() =>
|
||||
loadPluginBoundaryModule<{ ok: boolean }>(modulePath, loaders, { origin: "bundled" }),
|
||||
@@ -173,9 +173,9 @@ describe("runtime plugin boundary whatsapp seam", () => {
|
||||
expect(loaders.size).toBe(0);
|
||||
});
|
||||
|
||||
it("keeps the Jiti TypeScript package fallback available for non-bundled plugins", () => {
|
||||
it("keeps the TypeScript source package fallback available for non-bundled plugins", () => {
|
||||
const modulePath = createExternalTypeScriptRuntimePackageFixture();
|
||||
const loaders: PluginJitiLoaderCache = new Map();
|
||||
const loaders: PluginModuleLoaderCache = new Map();
|
||||
|
||||
expect(
|
||||
loadPluginBoundaryModule<{ ok: boolean; loadedVia: string }>(modulePath, loaders, {
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import fs from "node:fs";
|
||||
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 {
|
||||
isJavaScriptModulePath,
|
||||
tryNativeRequireJavaScriptModule,
|
||||
} from "../native-module-require.js";
|
||||
import {
|
||||
getCachedPluginSourceModuleLoader,
|
||||
type PluginModuleLoaderCache,
|
||||
} from "../plugin-module-loader-cache.js";
|
||||
import type { PluginOrigin } from "../plugin-origin.types.js";
|
||||
|
||||
type PluginRuntimeRecord = {
|
||||
@@ -109,20 +112,19 @@ export function resolvePluginRuntimeModulePath(
|
||||
return null;
|
||||
}
|
||||
|
||||
function getPluginBoundarySourceLoader(modulePath: string, loaders: PluginJitiLoaderCache) {
|
||||
return getCachedPluginJitiLoader({
|
||||
function getPluginBoundarySourceLoader(modulePath: string, loaders: PluginModuleLoaderCache) {
|
||||
return getCachedPluginSourceModuleLoader({
|
||||
cache: loaders,
|
||||
modulePath,
|
||||
importerUrl: import.meta.url,
|
||||
jitiFilename: import.meta.url,
|
||||
tryNative: false,
|
||||
loaderFilename: import.meta.url,
|
||||
});
|
||||
}
|
||||
|
||||
// oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- Dynamic plugin boundary loaders use caller-supplied module types.
|
||||
export function loadPluginBoundaryModule<TModule>(
|
||||
modulePath: string,
|
||||
loaders: PluginJitiLoaderCache,
|
||||
loaders: PluginModuleLoaderCache,
|
||||
options: { origin?: PluginOrigin } = {},
|
||||
): TModule {
|
||||
if (isJavaScriptModulePath(modulePath)) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
optimizeImageToJpeg as optimizeImageToJpegImpl,
|
||||
} from "../../media/web-media.js";
|
||||
import type { PollInput } from "../../polls.js";
|
||||
import type { PluginJitiLoaderCache } from "../jiti-loader-cache.js";
|
||||
import type { PluginModuleLoaderCache } from "../plugin-module-loader-cache.js";
|
||||
import type { PluginOrigin } from "../plugin-origin.types.js";
|
||||
import {
|
||||
loadPluginBoundaryModule,
|
||||
@@ -109,7 +109,7 @@ let cachedHeavyModule: WebChannelHeavyRuntimeModule | null = null;
|
||||
let cachedLightModulePath: string | null = null;
|
||||
let cachedLightModule: WebChannelLightRuntimeModule | null = null;
|
||||
|
||||
const jitiLoaders: PluginJitiLoaderCache = new Map();
|
||||
const moduleLoaders: PluginModuleLoaderCache = new Map();
|
||||
|
||||
function resolveWebChannelPluginRecord(): WebChannelPluginRecord {
|
||||
return resolvePluginRuntimeRecordByEntryBaseNames(["light-runtime-api", "runtime-api"], () => {
|
||||
@@ -135,7 +135,7 @@ function resolveWebChannelRuntimeModulePath(
|
||||
function loadCurrentHeavyModuleSync(): WebChannelHeavyRuntimeModule {
|
||||
const record = resolveWebChannelPluginRecord();
|
||||
const modulePath = resolveWebChannelRuntimeModulePath(record, "runtime-api");
|
||||
return loadPluginBoundaryModule<WebChannelHeavyRuntimeModule>(modulePath, jitiLoaders, {
|
||||
return loadPluginBoundaryModule<WebChannelHeavyRuntimeModule>(modulePath, moduleLoaders, {
|
||||
origin: record.origin,
|
||||
});
|
||||
}
|
||||
@@ -146,7 +146,7 @@ function loadWebChannelLightModule(): WebChannelLightRuntimeModule {
|
||||
if (cachedLightModule && cachedLightModulePath === modulePath) {
|
||||
return cachedLightModule;
|
||||
}
|
||||
const loaded = loadPluginBoundaryModule<WebChannelLightRuntimeModule>(modulePath, jitiLoaders, {
|
||||
const loaded = loadPluginBoundaryModule<WebChannelLightRuntimeModule>(modulePath, moduleLoaders, {
|
||||
origin: record.origin,
|
||||
});
|
||||
cachedLightModulePath = modulePath;
|
||||
@@ -160,7 +160,7 @@ async function loadWebChannelHeavyModule(): Promise<WebChannelHeavyRuntimeModule
|
||||
if (cachedHeavyModule && cachedHeavyModulePath === modulePath) {
|
||||
return cachedHeavyModule;
|
||||
}
|
||||
const loaded = loadPluginBoundaryModule<WebChannelHeavyRuntimeModule>(modulePath, jitiLoaders, {
|
||||
const loaded = loadPluginBoundaryModule<WebChannelHeavyRuntimeModule>(modulePath, moduleLoaders, {
|
||||
origin: record.origin,
|
||||
});
|
||||
cachedHeavyModulePath = modulePath;
|
||||
|
||||
@@ -10,18 +10,18 @@ import { afterAll, describe, expect, it, vi } from "vitest";
|
||||
import { withEnv } from "../test-utils/env.js";
|
||||
import {
|
||||
buildPluginLoaderAliasMap,
|
||||
createPluginLoaderJitiCacheKey,
|
||||
createPluginLoaderModuleCacheKey,
|
||||
buildPluginLoaderJitiOptions,
|
||||
isBundledPluginExtensionPath,
|
||||
listPluginSdkAliasCandidates,
|
||||
listPluginSdkExportedSubpaths,
|
||||
normalizeJitiAliasTargetPath,
|
||||
resolvePluginLoaderJitiConfig,
|
||||
resolvePluginLoaderJitiTryNative,
|
||||
resolvePluginLoaderModuleConfig,
|
||||
resolvePluginLoaderTryNative,
|
||||
resolveExtensionApiAlias,
|
||||
resolvePluginRuntimeModulePath,
|
||||
resolvePluginSdkAliasFile,
|
||||
shouldPreferNativeJiti,
|
||||
shouldPreferNativeModuleLoad,
|
||||
} from "./sdk-alias.js";
|
||||
import {
|
||||
cleanupTrackedTempDirs,
|
||||
@@ -912,7 +912,7 @@ describe("plugin sdk alias helpers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("configures the plugin loader jiti boundary to prefer native dist modules", () => {
|
||||
it("configures the plugin loader native-first boundary to prefer native dist modules", () => {
|
||||
const options = buildPluginLoaderJitiOptions({});
|
||||
|
||||
expect(options.tryNative).toBe(true);
|
||||
@@ -922,14 +922,16 @@ describe("plugin sdk alias helpers", () => {
|
||||
expect("alias" in options).toBe(false);
|
||||
});
|
||||
|
||||
it("uses transpiled Jiti loads for source TypeScript plugin entries", () => {
|
||||
expect(shouldPreferNativeJiti("/repo/dist/plugins/runtime/index.js")).toBe(true);
|
||||
it("uses transpiled module loads for source TypeScript plugin entries", () => {
|
||||
expect(shouldPreferNativeModuleLoad("/repo/dist/plugins/runtime/index.js")).toBe(true);
|
||||
expect(
|
||||
shouldPreferNativeJiti(`/repo/${bundledPluginFile("discord", "src/channel.runtime.ts")}`),
|
||||
shouldPreferNativeModuleLoad(
|
||||
`/repo/${bundledPluginFile("discord", "src/channel.runtime.ts")}`,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("disables native Jiti loads under Bun even for built JavaScript entries", () => {
|
||||
it("disables native module loads under Bun even for built JavaScript entries", () => {
|
||||
const originalVersions = process.versions;
|
||||
Object.defineProperty(process, "versions", {
|
||||
configurable: true,
|
||||
@@ -940,10 +942,10 @@ describe("plugin sdk alias helpers", () => {
|
||||
});
|
||||
|
||||
try {
|
||||
expect(shouldPreferNativeJiti("/repo/dist/plugins/runtime/index.js")).toBe(false);
|
||||
expect(shouldPreferNativeJiti(`/repo/${bundledDistPluginFile("browser", "index.js")}`)).toBe(
|
||||
false,
|
||||
);
|
||||
expect(shouldPreferNativeModuleLoad("/repo/dist/plugins/runtime/index.js")).toBe(false);
|
||||
expect(
|
||||
shouldPreferNativeModuleLoad(`/repo/${bundledDistPluginFile("browser", "index.js")}`),
|
||||
).toBe(false);
|
||||
} finally {
|
||||
Object.defineProperty(process, "versions", {
|
||||
configurable: true,
|
||||
@@ -952,7 +954,7 @@ describe("plugin sdk alias helpers", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("enables native Jiti loads on Windows for built JavaScript entries", () => {
|
||||
it("enables native module loads on Windows for built JavaScript entries", () => {
|
||||
const originalPlatform = process.platform;
|
||||
Object.defineProperty(process, "platform", {
|
||||
configurable: true,
|
||||
@@ -960,10 +962,10 @@ describe("plugin sdk alias helpers", () => {
|
||||
});
|
||||
|
||||
try {
|
||||
expect(shouldPreferNativeJiti("/repo/dist/plugins/runtime/index.js")).toBe(true);
|
||||
expect(shouldPreferNativeJiti(`/repo/${bundledDistPluginFile("browser", "index.js")}`)).toBe(
|
||||
true,
|
||||
);
|
||||
expect(shouldPreferNativeModuleLoad("/repo/dist/plugins/runtime/index.js")).toBe(true);
|
||||
expect(
|
||||
shouldPreferNativeModuleLoad(`/repo/${bundledDistPluginFile("browser", "index.js")}`),
|
||||
).toBe(true);
|
||||
} finally {
|
||||
Object.defineProperty(process, "platform", {
|
||||
configurable: true,
|
||||
@@ -972,7 +974,7 @@ describe("plugin sdk alias helpers", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps plugin loader dist shortcuts on native Jiti on Windows for JS entries", () => {
|
||||
it("keeps plugin loader dist shortcuts on native module loading on Windows for JS entries", () => {
|
||||
const originalPlatform = process.platform;
|
||||
Object.defineProperty(process, "platform", {
|
||||
configurable: true,
|
||||
@@ -981,12 +983,12 @@ describe("plugin sdk alias helpers", () => {
|
||||
|
||||
try {
|
||||
expect(
|
||||
resolvePluginLoaderJitiTryNative(`/repo/${bundledDistPluginFile("browser", "index.js")}`, {
|
||||
resolvePluginLoaderTryNative(`/repo/${bundledDistPluginFile("browser", "index.js")}`, {
|
||||
preferBuiltDist: true,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
resolvePluginLoaderJitiTryNative(`/repo/${bundledDistPluginFile("browser", "helper.ts")}`, {
|
||||
resolvePluginLoaderTryNative(`/repo/${bundledDistPluginFile("browser", "helper.ts")}`, {
|
||||
preferBuiltDist: true,
|
||||
}),
|
||||
).toBe(false);
|
||||
@@ -998,31 +1000,31 @@ describe("plugin sdk alias helpers", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("prefers native jiti for bundled plugin dist .js modules, keeps .ts on aliased path", () => {
|
||||
it("prefers native module loading for bundled plugin dist .js modules, keeps .ts on aliased path", () => {
|
||||
// Built .js/.mjs/.cjs files under dist/extensions/ should now delegate
|
||||
// to shouldPreferNativeJiti() — which returns true on Node for
|
||||
// to shouldPreferNativeModuleLoad() — which returns true on Node for
|
||||
// compiled artifacts, avoiding the slow jiti transform path.
|
||||
expect(
|
||||
resolvePluginLoaderJitiTryNative(`/repo/${bundledDistPluginFile("browser", "index.js")}`, {
|
||||
resolvePluginLoaderTryNative(`/repo/${bundledDistPluginFile("browser", "index.js")}`, {
|
||||
preferBuiltDist: true,
|
||||
}),
|
||||
).toBe(true);
|
||||
// TypeScript source files still need jiti's transform pipeline.
|
||||
expect(
|
||||
resolvePluginLoaderJitiTryNative(`/repo/${bundledDistPluginFile("browser", "helper.ts")}`, {
|
||||
resolvePluginLoaderTryNative(`/repo/${bundledDistPluginFile("browser", "helper.ts")}`, {
|
||||
preferBuiltDist: true,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
resolvePluginLoaderJitiTryNative("/repo/dist/plugins/runtime/index.js", {
|
||||
resolvePluginLoaderTryNative("/repo/dist/plugins/runtime/index.js", {
|
||||
preferBuiltDist: true,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps plugin loader Jiti cache keys stable across alias insertion order", () => {
|
||||
it("keeps plugin loader module cache keys stable across alias insertion order", () => {
|
||||
expect(
|
||||
createPluginLoaderJitiCacheKey({
|
||||
createPluginLoaderModuleCacheKey({
|
||||
tryNative: true,
|
||||
aliasMap: {
|
||||
zeta: "/repo/zeta.js",
|
||||
@@ -1030,7 +1032,7 @@ describe("plugin sdk alias helpers", () => {
|
||||
},
|
||||
}),
|
||||
).toBe(
|
||||
createPluginLoaderJitiCacheKey({
|
||||
createPluginLoaderModuleCacheKey({
|
||||
tryNative: true,
|
||||
aliasMap: {
|
||||
alpha: "/repo/alpha.js",
|
||||
@@ -1040,14 +1042,14 @@ describe("plugin sdk alias helpers", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("returns plugin loader Jiti config with stable cache keys", () => {
|
||||
const first = resolvePluginLoaderJitiConfig({
|
||||
it("returns plugin loader module config with stable cache keys", () => {
|
||||
const first = resolvePluginLoaderModuleConfig({
|
||||
modulePath: `/repo/${bundledDistPluginFile("browser", "index.js")}`,
|
||||
argv1: "/repo/openclaw.mjs",
|
||||
moduleUrl: "file:///repo/src/plugins/public-surface-loader.ts",
|
||||
preferBuiltDist: true,
|
||||
});
|
||||
const second = resolvePluginLoaderJitiConfig({
|
||||
const second = resolvePluginLoaderModuleConfig({
|
||||
modulePath: `/repo/${bundledDistPluginFile("browser", "index.js")}`,
|
||||
argv1: "/repo/openclaw.mjs",
|
||||
moduleUrl: "file:///repo/src/plugins/public-surface-loader.ts",
|
||||
@@ -1057,7 +1059,7 @@ describe("plugin sdk alias helpers", () => {
|
||||
expect(second).toBe(first);
|
||||
});
|
||||
|
||||
it("scopes plugin loader Jiti config by plugin-sdk resolution", () => {
|
||||
it("scopes plugin loader module config by plugin-sdk resolution", () => {
|
||||
const { fixture, sourceRootAlias, distRootAlias } = createPluginSdkAliasTargetFixture();
|
||||
const sourcePluginEntry = writePluginEntry(
|
||||
fixture.root,
|
||||
@@ -1065,19 +1067,19 @@ describe("plugin sdk alias helpers", () => {
|
||||
);
|
||||
|
||||
const { auto, dist, distAgain } = withEnv({ NODE_ENV: undefined }, () => ({
|
||||
auto: resolvePluginLoaderJitiConfig({
|
||||
auto: resolvePluginLoaderModuleConfig({
|
||||
modulePath: sourcePluginEntry,
|
||||
argv1: path.join(fixture.root, "openclaw.mjs"),
|
||||
moduleUrl: pathToFileURL(path.join(fixture.root, "src/plugins/loader.ts")).href,
|
||||
pluginSdkResolution: "auto",
|
||||
}),
|
||||
dist: resolvePluginLoaderJitiConfig({
|
||||
dist: resolvePluginLoaderModuleConfig({
|
||||
modulePath: sourcePluginEntry,
|
||||
argv1: path.join(fixture.root, "openclaw.mjs"),
|
||||
moduleUrl: pathToFileURL(path.join(fixture.root, "src/plugins/loader.ts")).href,
|
||||
pluginSdkResolution: "dist",
|
||||
}),
|
||||
distAgain: resolvePluginLoaderJitiConfig({
|
||||
distAgain: resolvePluginLoaderModuleConfig({
|
||||
modulePath: sourcePluginEntry,
|
||||
argv1: path.join(fixture.root, "openclaw.mjs"),
|
||||
moduleUrl: pathToFileURL(path.join(fixture.root, "src/plugins/loader.ts")).href,
|
||||
@@ -1116,7 +1118,7 @@ describe("plugin sdk alias helpers", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("normalizes Windows alias targets before handing them to Jiti", () => {
|
||||
it("normalizes Windows alias targets before handing them to the source transformer", () => {
|
||||
const originalPlatform = process.platform;
|
||||
Object.defineProperty(process, "platform", {
|
||||
configurable: true,
|
||||
@@ -1135,14 +1137,14 @@ describe("plugin sdk alias helpers", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("loads source runtime shims through the non-native Jiti boundary", async () => {
|
||||
it("loads source runtime shims through the non-native module loading boundary", async () => {
|
||||
const copiedExtensionRoot = path.join(makeTempDir(), bundledPluginRoot("discord"));
|
||||
const copiedSourceDir = path.join(copiedExtensionRoot, "src");
|
||||
const copiedPluginSdkDir = path.join(copiedExtensionRoot, "plugin-sdk");
|
||||
mkdirSafeDir(copiedSourceDir);
|
||||
mkdirSafeDir(copiedPluginSdkDir);
|
||||
const jitiBaseFile = path.join(copiedSourceDir, "__jiti-base__.mjs");
|
||||
fs.writeFileSync(jitiBaseFile, "export {};\n", "utf-8");
|
||||
const sourceLoaderBaseFile = path.join(copiedSourceDir, "__jiti-base__.mjs");
|
||||
fs.writeFileSync(sourceLoaderBaseFile, "export {};\n", "utf-8");
|
||||
fs.writeFileSync(
|
||||
path.join(copiedSourceDir, "channel.runtime.ts"),
|
||||
`import { resolveOutboundSendDep } from "@openclaw/plugin-sdk/outbound-send-deps";
|
||||
@@ -1163,16 +1165,16 @@ export const syntheticRuntimeMarker = {
|
||||
"utf-8",
|
||||
);
|
||||
const copiedChannelRuntime = path.join(copiedExtensionRoot, "src", "channel.runtime.ts");
|
||||
const jitiBaseUrl = pathToFileURL(jitiBaseFile).href;
|
||||
const sourceLoaderBaseUrl = pathToFileURL(sourceLoaderBaseFile).href;
|
||||
|
||||
const createJiti = await getCreateJiti();
|
||||
const withoutAlias = createJiti(jitiBaseUrl, {
|
||||
const withoutAlias = createJiti(sourceLoaderBaseUrl, {
|
||||
...buildPluginLoaderJitiOptions({}),
|
||||
tryNative: false,
|
||||
});
|
||||
expect(() => withoutAlias(copiedChannelRuntime)).toThrow();
|
||||
|
||||
const withAlias = createJiti(jitiBaseUrl, {
|
||||
const withAlias = createJiti(sourceLoaderBaseUrl, {
|
||||
...buildPluginLoaderJitiOptions({
|
||||
"openclaw/plugin-sdk/outbound-send-deps": copiedChannelRuntimeShim,
|
||||
"@openclaw/plugin-sdk/outbound-send-deps": copiedChannelRuntimeShim,
|
||||
@@ -1351,7 +1353,7 @@ describe("buildPluginLoaderAliasMap memoization", () => {
|
||||
});
|
||||
|
||||
describe("buildPluginLoaderJitiOptions", () => {
|
||||
it("pre-normalizes and marks alias maps for Jiti", () => {
|
||||
it("pre-normalizes and marks alias maps for source transforms", () => {
|
||||
const marker = Symbol.for("pathe:normalizedAlias");
|
||||
const aliasMap = {
|
||||
"openclaw/plugin-sdk/core": "/repo/src/plugin-sdk/core.ts",
|
||||
@@ -1367,7 +1369,7 @@ describe("buildPluginLoaderJitiOptions", () => {
|
||||
expect(Object.prototype.propertyIsEnumerable.call(first, marker)).toBe(false);
|
||||
});
|
||||
|
||||
it("applies Jiti alias-target normalization before caching", () => {
|
||||
it("applies source-transform alias-target normalization before caching", () => {
|
||||
const aliasMap = {
|
||||
alpha: "/repo/alpha",
|
||||
beta: "alpha/sub",
|
||||
|
||||
@@ -490,7 +490,7 @@ const JITI_ALIAS_ROOT_SENTINELS = new Set<string | undefined>(["/", "\\", undefi
|
||||
// surfaces depend on them.
|
||||
const aliasMapCache = new Map<string, Record<string, string>>();
|
||||
const normalizedJitiAliasMapCache = new Map<string, Record<string, string>>();
|
||||
const pluginLoaderJitiConfigCache = new Map<
|
||||
const pluginLoaderModuleConfigCache = new Map<
|
||||
string,
|
||||
{
|
||||
tryNative: boolean;
|
||||
@@ -578,7 +578,7 @@ function buildPluginLoaderAliasMapCacheKey(params: {
|
||||
].join("\0");
|
||||
}
|
||||
|
||||
function buildPluginLoaderJitiConfigCacheKey(params: {
|
||||
function buildPluginLoaderModuleConfigCacheKey(params: {
|
||||
modulePath: string;
|
||||
argv1?: string;
|
||||
moduleUrl: string;
|
||||
@@ -693,7 +693,7 @@ export function buildPluginLoaderJitiOptions(aliasMap: Record<string, string>) {
|
||||
};
|
||||
}
|
||||
|
||||
function supportsNativeJitiRuntime(): boolean {
|
||||
function supportsNativeModuleRuntime(): boolean {
|
||||
const versions = process.versions as { bun?: string };
|
||||
return typeof versions.bun !== "string";
|
||||
}
|
||||
@@ -702,8 +702,8 @@ function isBundledPluginDistModulePath(modulePath: string): boolean {
|
||||
return modulePath.replace(/\\/g, "/").includes("/dist/extensions/");
|
||||
}
|
||||
|
||||
export function shouldPreferNativeJiti(modulePath: string): boolean {
|
||||
if (!supportsNativeJitiRuntime()) {
|
||||
export function shouldPreferNativeModuleLoad(modulePath: string): boolean {
|
||||
if (!supportsNativeModuleRuntime()) {
|
||||
return false;
|
||||
}
|
||||
switch (normalizeLowercaseStringOrEmpty(path.extname(modulePath))) {
|
||||
@@ -717,24 +717,24 @@ export function shouldPreferNativeJiti(modulePath: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
export function resolvePluginLoaderJitiTryNative(
|
||||
export function resolvePluginLoaderTryNative(
|
||||
modulePath: string,
|
||||
options?: {
|
||||
preferBuiltDist?: boolean;
|
||||
},
|
||||
): boolean {
|
||||
if (isBundledPluginDistModulePath(modulePath)) {
|
||||
return shouldPreferNativeJiti(modulePath);
|
||||
return shouldPreferNativeModuleLoad(modulePath);
|
||||
}
|
||||
return (
|
||||
shouldPreferNativeJiti(modulePath) ||
|
||||
(supportsNativeJitiRuntime() &&
|
||||
shouldPreferNativeModuleLoad(modulePath) ||
|
||||
(supportsNativeModuleRuntime() &&
|
||||
options?.preferBuiltDist === true &&
|
||||
modulePath.includes(`${path.sep}dist${path.sep}`))
|
||||
);
|
||||
}
|
||||
|
||||
export function createPluginLoaderJitiCacheKey(params: {
|
||||
export function createPluginLoaderModuleCacheKey(params: {
|
||||
tryNative: boolean;
|
||||
aliasMap: Record<string, string>;
|
||||
}): string {
|
||||
@@ -746,7 +746,7 @@ export function createPluginLoaderJitiCacheKey(params: {
|
||||
});
|
||||
}
|
||||
|
||||
export function resolvePluginLoaderJitiConfig(params: {
|
||||
export function resolvePluginLoaderModuleConfig(params: {
|
||||
modulePath: string;
|
||||
argv1?: string;
|
||||
moduleUrl: string;
|
||||
@@ -757,13 +757,13 @@ export function resolvePluginLoaderJitiConfig(params: {
|
||||
aliasMap: Record<string, string>;
|
||||
cacheKey: string;
|
||||
} {
|
||||
const configCacheKey = buildPluginLoaderJitiConfigCacheKey(params);
|
||||
const cached = pluginLoaderJitiConfigCache.get(configCacheKey);
|
||||
const configCacheKey = buildPluginLoaderModuleConfigCacheKey(params);
|
||||
const cached = pluginLoaderModuleConfigCache.get(configCacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const tryNative = resolvePluginLoaderJitiTryNative(
|
||||
const tryNative = resolvePluginLoaderTryNative(
|
||||
params.modulePath,
|
||||
params.preferBuiltDist ? { preferBuiltDist: true } : {},
|
||||
);
|
||||
@@ -776,12 +776,12 @@ export function resolvePluginLoaderJitiConfig(params: {
|
||||
const result = {
|
||||
tryNative,
|
||||
aliasMap,
|
||||
cacheKey: createPluginLoaderJitiCacheKey({
|
||||
cacheKey: createPluginLoaderModuleCacheKey({
|
||||
tryNative,
|
||||
aliasMap,
|
||||
}),
|
||||
};
|
||||
setBoundedCacheValue(pluginLoaderJitiConfigCache, configCacheKey, result);
|
||||
setBoundedCacheValue(pluginLoaderModuleConfigCache, configCacheKey, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ import {
|
||||
resetRegistryJitiMocks,
|
||||
} from "./test-helpers/registry-jiti-mocks.js";
|
||||
|
||||
// jiti-loader-cache prefers native require() for compiled .js before falling
|
||||
// plugin-module-loader-cache prefers native require() for compiled .js before falling
|
||||
// back to jiti. These tests scripts plugin-loading behaviour through the
|
||||
// jiti mock — disable the native-require fast path so the mocked jiti loader
|
||||
// source-transform mock — disable the native-require fast path so the mocked source transformer
|
||||
// stays authoritative for the test fixture files on disk.
|
||||
vi.mock("./native-module-require.js", () => ({
|
||||
isJavaScriptModulePath: (_modulePath: string) => false,
|
||||
@@ -171,7 +171,7 @@ afterEach(() => {
|
||||
cleanupTrackedTempDirs(tempDirs);
|
||||
});
|
||||
|
||||
describe("setup-registry getJiti", () => {
|
||||
describe("setup-registry module loader", () => {
|
||||
beforeEach(async () => {
|
||||
resetRegistryJitiMocks();
|
||||
vi.resetModules();
|
||||
@@ -185,7 +185,7 @@ describe("setup-registry getJiti", () => {
|
||||
clearPluginSetupRegistryCache();
|
||||
});
|
||||
|
||||
it("uses the runtime-supported Jiti boundary on Windows for setup-api modules", () => {
|
||||
it("uses the runtime-supported source-transform boundary on Windows for setup-api modules", () => {
|
||||
const pluginRoot = makeTempDir();
|
||||
fs.writeFileSync(path.join(pluginRoot, "setup-api.js"), "export default {};\n", "utf-8");
|
||||
mocks.loadPluginManifestRegistry.mockReturnValue({
|
||||
|
||||
@@ -5,8 +5,11 @@ import { normalizeProviderId } from "../agents/provider-id.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { buildPluginApi } from "./api-builder.js";
|
||||
import { collectPluginConfigContractMatches } from "./config-contracts.js";
|
||||
import { getCachedPluginJitiLoader, type PluginJitiLoaderCache } from "./jiti-loader-cache.js";
|
||||
import type { PluginManifestRecord, PluginManifestRegistry } from "./manifest-registry.js";
|
||||
import {
|
||||
getCachedPluginModuleLoader,
|
||||
type PluginModuleLoaderCache,
|
||||
} from "./plugin-module-loader-cache.js";
|
||||
import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js";
|
||||
import type { PluginRuntime } from "./runtime/types.js";
|
||||
import { listSetupCliBackendIds, listSetupProviderIds } from "./setup-descriptors.js";
|
||||
@@ -82,15 +85,15 @@ const NOOP_LOGGER: PluginLogger = {
|
||||
error() {},
|
||||
};
|
||||
|
||||
const jitiLoaders: PluginJitiLoaderCache = new Map();
|
||||
const moduleLoaders: PluginModuleLoaderCache = new Map();
|
||||
|
||||
export function clearPluginSetupRegistryCache(): void {
|
||||
jitiLoaders.clear();
|
||||
moduleLoaders.clear();
|
||||
}
|
||||
|
||||
function getJiti(modulePath: string) {
|
||||
return getCachedPluginJitiLoader({
|
||||
cache: jitiLoaders,
|
||||
function getModuleLoader(modulePath: string) {
|
||||
return getCachedPluginModuleLoader({
|
||||
cache: moduleLoaders,
|
||||
modulePath,
|
||||
importerUrl: import.meta.url,
|
||||
});
|
||||
@@ -224,7 +227,7 @@ function resolveSetupRegistration(record: PluginManifestRecord): {
|
||||
|
||||
let mod: OpenClawPluginModule;
|
||||
try {
|
||||
mod = getJiti(setupSource)(setupSource) as OpenClawPluginModule;
|
||||
mod = getModuleLoader(setupSource)(setupSource) as OpenClawPluginModule;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import type { PluginJitiLoaderCache } from "./jiti-loader-cache.js";
|
||||
import { getCachedPluginJitiLoader } from "./jiti-loader-cache.js";
|
||||
import { withProfile } from "./plugin-load-profile.js";
|
||||
import type { PluginModuleLoaderCache } from "./plugin-module-loader-cache.js";
|
||||
import { getCachedPluginSourceModuleLoader } from "./plugin-module-loader-cache.js";
|
||||
|
||||
export type PluginSourceLoader = (modulePath: string) => unknown;
|
||||
|
||||
export function createPluginSourceLoader(): PluginSourceLoader {
|
||||
const loaders: PluginJitiLoaderCache = new Map();
|
||||
const loaders: PluginModuleLoaderCache = new Map();
|
||||
return (modulePath) => {
|
||||
const jiti = getCachedPluginJitiLoader({
|
||||
const sourceLoader = getCachedPluginSourceModuleLoader({
|
||||
cache: loaders,
|
||||
modulePath,
|
||||
importerUrl: import.meta.url,
|
||||
jitiFilename: import.meta.url,
|
||||
loaderFilename: import.meta.url,
|
||||
});
|
||||
// Direct source loads are not associated with a specific plugin id —
|
||||
// preserve the existing `plugin=(direct)` field used by tooling that
|
||||
// scrapes [plugin-load-profile] lines.
|
||||
return withProfile({ pluginId: "(direct)", source: modulePath }, "source-loader", () =>
|
||||
jiti(modulePath),
|
||||
sourceLoader(modulePath),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user