fix: enable native require fast path on Windows for bundled plugins (#74173)

Removes the win32 exclusion from supportsNativeJitiRuntime() and adds { allowWindows: true } to all tryNativeRequireJavaScriptModule call sites, so bundled plugin modules use native require() instead of Jiti on Windows. Also adds an attempted-load counter to the debug timing log and a changelog entry.

Fixes #68656

Co-authored-by: Galin Iliev <galiniliev@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Galin Iliev
2026-04-29 23:32:20 -07:00
committed by GitHub
parent e0c75cd0bd
commit c4a4c189f1
9 changed files with 32 additions and 17 deletions

View File

@@ -315,6 +315,7 @@ Docs: https://docs.openclaw.ai
- Auth/device pairing: bound bootstrap handoff token issuance, redemption, and approved pairing baselines to the documented per-role scope allowlist, so bootstrap approvals cannot persistently grant `operator.admin`, `operator.pairing`, or `node.exec` scopes. Thanks @eleqtrizit.
- Providers/GitHub Copilot: support the GUI/RPC wizard device-code auth flow so onboarding from non-TTY clients (gateway RPC bridge, GUI wizards) completes instead of returning empty profiles. Dangerous-state handling now distinguishes `access_denied` and `expired_token` from transport errors. (#73290) Thanks @indierawk2k2.
- Installer/Linux: warn before switching an unwritable npm global prefix to `~/.npm-global`, then tell users to run future global updates with `npm i -g openclaw@latest` without `sudo` so npm keeps using the redirected user prefix. Fixes #44365; carries forward #50479. Thanks @Sayeem3051.
- Gateway/plugins: enable the native `require()` fast path on Windows for bundled plugin modules so plugin loading uses `require()` instead of Jiti's transform pipeline, reducing startup from ~39s to ~2s on typical 6-plugin setups. Fixes #68656. (#74173) Thanks @galiniliev.
## 2026.4.27

View File

@@ -48,7 +48,7 @@ function getJiti(modulePath: string) {
}
function loadPluginDoctorContractModule(modulePath: string): PluginDoctorContractModule {
const nativeModule = tryNativeRequireJavaScriptModule(modulePath);
const nativeModule = tryNativeRequireJavaScriptModule(modulePath, { allowWindows: true });
if (nativeModule.ok) {
return nativeModule.moduleExport as PluginDoctorContractModule;
}

View File

@@ -208,13 +208,14 @@ describe("getCachedPluginJitiLoader", () => {
const jitiLoader = vi.fn();
const createJiti = vi.fn(() => jitiLoader);
vi.doMock("jiti", () => ({ createJiti }));
const nativeStub = vi.fn((target: string) => ({
ok: true as const,
moduleExport: { loadedFrom: target },
}));
vi.doMock("./native-module-require.js", () => ({
isJavaScriptModulePath: (p: string) =>
p.endsWith(".js") || p.endsWith(".mjs") || p.endsWith(".cjs"),
tryNativeRequireJavaScriptModule: (target: string) => ({
ok: true,
moduleExport: { loadedFrom: target },
}),
tryNativeRequireJavaScriptModule: nativeStub,
}));
const { getCachedPluginJitiLoader } = await importFreshModule<
typeof import("./jiti-loader-cache.js")
@@ -233,6 +234,10 @@ describe("getCachedPluginJitiLoader", () => {
// jiti is created eagerly, but its loader must NOT be invoked for .js
// targets that `tryNativeRequireJavaScriptModule` resolves.
expect(jitiLoader).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 () => {

View File

@@ -105,7 +105,7 @@ export function getCachedPluginJitiLoader(params: {
// async-module fallbacks `tryNativeRequireJavaScriptModule` declines to
// handle.
const loader = ((target: string, ...rest: unknown[]) => {
const native = tryNativeRequireJavaScriptModule(target);
const native = tryNativeRequireJavaScriptModule(target, { allowWindows: true });
if (native.ok) {
return native.moduleExport;
}

View File

@@ -1316,6 +1316,8 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
let selectedMemoryPluginId: string | null = null;
let memorySlotMatched = false;
const dreamingEngineId = resolveDreamingSidecarEngineId({ cfg, memorySlot });
const pluginLoadStartMs = performance.now();
let pluginLoadAttemptCount = 0;
for (const candidate of orderedCandidates) {
const manifestRecord = manifestByRoot.get(candidate.rootDir);
@@ -1702,6 +1704,8 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
// Track the plugin as imported once module evaluation begins. Top-level
// code may have already executed even if evaluation later throws.
recordImportedPluginId(record.id);
pluginLoadAttemptCount++;
logger.debug?.(`[plugins] loading ${record.id} from ${safeSource}`);
mod = withProfile(
{ pluginId: record.id, source: safeSource },
registrationMode,
@@ -2065,6 +2069,13 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
}
}
const pluginLoadElapsedMs = performance.now() - pluginLoadStartMs;
if (pluginLoadAttemptCount > 0) {
logger.debug?.(
`[plugins] loaded ${registry.plugins.length} plugin(s) (${pluginLoadAttemptCount} attempted) in ${pluginLoadElapsedMs.toFixed(1)}ms`,
);
}
// Scoped snapshot loads may intentionally omit the configured memory plugin, so only
// emit the missing-memory diagnostic for full registry loads.
if (!onlyPluginIdSet && typeof memorySlot === "string" && !memorySlotMatched) {

View File

@@ -111,7 +111,7 @@ afterEach(() => {
});
describe("bundled plugin public surface loader", () => {
it("uses transpiled Jiti import for Windows dist public artifact loads", async () => {
it("uses native Jiti import for Windows dist public artifact loads", async () => {
const createJiti = vi.fn(() => vi.fn(() => ({ marker: "windows-dist-ok" })));
vi.doMock("jiti", () => ({
createJiti,
@@ -140,7 +140,7 @@ describe("bundled plugin public surface loader", () => {
expect(createJiti).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
tryNative: false,
tryNative: true,
}),
);
} finally {

View File

@@ -952,7 +952,7 @@ describe("plugin sdk alias helpers", () => {
}
});
it("disables native Jiti loads on Windows for built JavaScript entries", () => {
it("enables native Jiti loads on Windows for built JavaScript entries", () => {
const originalPlatform = process.platform;
Object.defineProperty(process, "platform", {
configurable: true,
@@ -960,9 +960,9 @@ describe("plugin sdk alias helpers", () => {
});
try {
expect(shouldPreferNativeJiti("/repo/dist/plugins/runtime/index.js")).toBe(false);
expect(shouldPreferNativeJiti("/repo/dist/plugins/runtime/index.js")).toBe(true);
expect(shouldPreferNativeJiti(`/repo/${bundledDistPluginFile("browser", "index.js")}`)).toBe(
false,
true,
);
} finally {
Object.defineProperty(process, "platform", {
@@ -972,7 +972,7 @@ describe("plugin sdk alias helpers", () => {
}
});
it("keeps plugin loader dist shortcuts on transpiled Jiti on Windows", () => {
it("keeps plugin loader dist shortcuts on native Jiti on Windows for JS entries", () => {
const originalPlatform = process.platform;
Object.defineProperty(process, "platform", {
configurable: true,
@@ -984,7 +984,7 @@ describe("plugin sdk alias helpers", () => {
resolvePluginLoaderJitiTryNative(`/repo/${bundledDistPluginFile("browser", "index.js")}`, {
preferBuiltDist: true,
}),
).toBe(false);
).toBe(true);
expect(
resolvePluginLoaderJitiTryNative(`/repo/${bundledDistPluginFile("browser", "helper.ts")}`, {
preferBuiltDist: true,

View File

@@ -695,7 +695,7 @@ export function buildPluginLoaderJitiOptions(aliasMap: Record<string, string>) {
function supportsNativeJitiRuntime(): boolean {
const versions = process.versions as { bun?: string };
return typeof versions.bun !== "string" && process.platform !== "win32";
return typeof versions.bun !== "string";
}
function isBundledPluginDistModulePath(modulePath: string): boolean {

View File

@@ -1,5 +1,3 @@
export function shouldExpectNativeJitiForJavaScriptTestRuntime(): boolean {
return (
typeof (process.versions as { bun?: string }).bun !== "string" && process.platform !== "win32"
);
return typeof (process.versions as { bun?: string }).bun !== "string";
}