From c4a4c189f11e1a41b9245b10c3169a22dc5b16ca Mon Sep 17 00:00:00 2001 From: Galin Iliev Date: Wed, 29 Apr 2026 23:32:20 -0700 Subject: [PATCH] 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 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 1 + src/plugins/doctor-contract-registry.ts | 2 +- src/plugins/jiti-loader-cache.test.ts | 13 +++++++++---- src/plugins/jiti-loader-cache.ts | 2 +- src/plugins/loader.ts | 11 +++++++++++ src/plugins/public-surface-loader.test.ts | 4 ++-- src/plugins/sdk-alias.test.ts | 10 +++++----- src/plugins/sdk-alias.ts | 2 +- src/test-utils/jiti-runtime.ts | 4 +--- 9 files changed, 32 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4957e6a217d..d4beb5915c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/plugins/doctor-contract-registry.ts b/src/plugins/doctor-contract-registry.ts index 96c68b2fc8e..0b7af25cf36 100644 --- a/src/plugins/doctor-contract-registry.ts +++ b/src/plugins/doctor-contract-registry.ts @@ -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; } diff --git a/src/plugins/jiti-loader-cache.test.ts b/src/plugins/jiti-loader-cache.test.ts index d327d5fd56e..a1fa92e9111 100644 --- a/src/plugins/jiti-loader-cache.test.ts +++ b/src/plugins/jiti-loader-cache.test.ts @@ -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 () => { diff --git a/src/plugins/jiti-loader-cache.ts b/src/plugins/jiti-loader-cache.ts index d2fc3d6dbfd..90dc20b7498 100644 --- a/src/plugins/jiti-loader-cache.ts +++ b/src/plugins/jiti-loader-cache.ts @@ -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; } diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index 4b1282d8f6a..9c8f6c57c6b 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -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) { diff --git a/src/plugins/public-surface-loader.test.ts b/src/plugins/public-surface-loader.test.ts index 4e9263ae94f..135fd374a0b 100644 --- a/src/plugins/public-surface-loader.test.ts +++ b/src/plugins/public-surface-loader.test.ts @@ -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 { diff --git a/src/plugins/sdk-alias.test.ts b/src/plugins/sdk-alias.test.ts index e115dbe9cd5..c72ab8f19af 100644 --- a/src/plugins/sdk-alias.test.ts +++ b/src/plugins/sdk-alias.test.ts @@ -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, diff --git a/src/plugins/sdk-alias.ts b/src/plugins/sdk-alias.ts index 212537d55f1..26b5aba0d06 100644 --- a/src/plugins/sdk-alias.ts +++ b/src/plugins/sdk-alias.ts @@ -695,7 +695,7 @@ export function buildPluginLoaderJitiOptions(aliasMap: Record) { 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 { diff --git a/src/test-utils/jiti-runtime.ts b/src/test-utils/jiti-runtime.ts index f6051312b63..c11ad43be66 100644 --- a/src/test-utils/jiti-runtime.ts +++ b/src/test-utils/jiti-runtime.ts @@ -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"; }