Skip to content

Commit c4a4c18

Browse files
galinilievCopilot
andauthored
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>
1 parent e0c75cd commit c4a4c18

9 files changed

Lines changed: 32 additions & 17 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,7 @@ Docs: https://docs.openclaw.ai
315315
- 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.
316316
- 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.
317317
- 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.
318+
- 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.
318319

319320
## 2026.4.27
320321

src/plugins/doctor-contract-registry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ function getJiti(modulePath: string) {
4848
}
4949

5050
function loadPluginDoctorContractModule(modulePath: string): PluginDoctorContractModule {
51-
const nativeModule = tryNativeRequireJavaScriptModule(modulePath);
51+
const nativeModule = tryNativeRequireJavaScriptModule(modulePath, { allowWindows: true });
5252
if (nativeModule.ok) {
5353
return nativeModule.moduleExport as PluginDoctorContractModule;
5454
}

src/plugins/jiti-loader-cache.test.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -208,13 +208,14 @@ describe("getCachedPluginJitiLoader", () => {
208208
const jitiLoader = vi.fn();
209209
const createJiti = vi.fn(() => jitiLoader);
210210
vi.doMock("jiti", () => ({ createJiti }));
211+
const nativeStub = vi.fn((target: string) => ({
212+
ok: true as const,
213+
moduleExport: { loadedFrom: target },
214+
}));
211215
vi.doMock("./native-module-require.js", () => ({
212216
isJavaScriptModulePath: (p: string) =>
213217
p.endsWith(".js") || p.endsWith(".mjs") || p.endsWith(".cjs"),
214-
tryNativeRequireJavaScriptModule: (target: string) => ({
215-
ok: true,
216-
moduleExport: { loadedFrom: target },
217-
}),
218+
tryNativeRequireJavaScriptModule: nativeStub,
218219
}));
219220
const { getCachedPluginJitiLoader } = await importFreshModule<
220221
typeof import("./jiti-loader-cache.js")
@@ -233,6 +234,10 @@ describe("getCachedPluginJitiLoader", () => {
233234
// jiti is created eagerly, but its loader must NOT be invoked for .js
234235
// targets that `tryNativeRequireJavaScriptModule` resolves.
235236
expect(jitiLoader).not.toHaveBeenCalled();
237+
// allowWindows must be passed so the native fast path works on Windows too.
238+
expect(nativeStub).toHaveBeenCalledWith("/repo/dist/extensions/demo/api.js", {
239+
allowWindows: true,
240+
});
236241
});
237242

238243
it("falls back to jiti when the native-require helper declines", async () => {

src/plugins/jiti-loader-cache.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export function getCachedPluginJitiLoader(params: {
105105
// async-module fallbacks `tryNativeRequireJavaScriptModule` declines to
106106
// handle.
107107
const loader = ((target: string, ...rest: unknown[]) => {
108-
const native = tryNativeRequireJavaScriptModule(target);
108+
const native = tryNativeRequireJavaScriptModule(target, { allowWindows: true });
109109
if (native.ok) {
110110
return native.moduleExport;
111111
}

src/plugins/loader.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1316,6 +1316,8 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
13161316
let selectedMemoryPluginId: string | null = null;
13171317
let memorySlotMatched = false;
13181318
const dreamingEngineId = resolveDreamingSidecarEngineId({ cfg, memorySlot });
1319+
const pluginLoadStartMs = performance.now();
1320+
let pluginLoadAttemptCount = 0;
13191321

13201322
for (const candidate of orderedCandidates) {
13211323
const manifestRecord = manifestByRoot.get(candidate.rootDir);
@@ -1702,6 +1704,8 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
17021704
// Track the plugin as imported once module evaluation begins. Top-level
17031705
// code may have already executed even if evaluation later throws.
17041706
recordImportedPluginId(record.id);
1707+
pluginLoadAttemptCount++;
1708+
logger.debug?.(`[plugins] loading ${record.id} from ${safeSource}`);
17051709
mod = withProfile(
17061710
{ pluginId: record.id, source: safeSource },
17071711
registrationMode,
@@ -2065,6 +2069,13 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
20652069
}
20662070
}
20672071

2072+
const pluginLoadElapsedMs = performance.now() - pluginLoadStartMs;
2073+
if (pluginLoadAttemptCount > 0) {
2074+
logger.debug?.(
2075+
`[plugins] loaded ${registry.plugins.length} plugin(s) (${pluginLoadAttemptCount} attempted) in ${pluginLoadElapsedMs.toFixed(1)}ms`,
2076+
);
2077+
}
2078+
20682079
// Scoped snapshot loads may intentionally omit the configured memory plugin, so only
20692080
// emit the missing-memory diagnostic for full registry loads.
20702081
if (!onlyPluginIdSet && typeof memorySlot === "string" && !memorySlotMatched) {

src/plugins/public-surface-loader.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ afterEach(() => {
111111
});
112112

113113
describe("bundled plugin public surface loader", () => {
114-
it("uses transpiled Jiti import for Windows dist public artifact loads", async () => {
114+
it("uses native Jiti import for Windows dist public artifact loads", async () => {
115115
const createJiti = vi.fn(() => vi.fn(() => ({ marker: "windows-dist-ok" })));
116116
vi.doMock("jiti", () => ({
117117
createJiti,
@@ -140,7 +140,7 @@ describe("bundled plugin public surface loader", () => {
140140
expect(createJiti).toHaveBeenCalledWith(
141141
expect.any(String),
142142
expect.objectContaining({
143-
tryNative: false,
143+
tryNative: true,
144144
}),
145145
);
146146
} finally {

src/plugins/sdk-alias.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -952,17 +952,17 @@ describe("plugin sdk alias helpers", () => {
952952
}
953953
});
954954

955-
it("disables native Jiti loads on Windows for built JavaScript entries", () => {
955+
it("enables native Jiti loads on Windows for built JavaScript entries", () => {
956956
const originalPlatform = process.platform;
957957
Object.defineProperty(process, "platform", {
958958
configurable: true,
959959
value: "win32",
960960
});
961961

962962
try {
963-
expect(shouldPreferNativeJiti("/repo/dist/plugins/runtime/index.js")).toBe(false);
963+
expect(shouldPreferNativeJiti("/repo/dist/plugins/runtime/index.js")).toBe(true);
964964
expect(shouldPreferNativeJiti(`/repo/${bundledDistPluginFile("browser", "index.js")}`)).toBe(
965-
false,
965+
true,
966966
);
967967
} finally {
968968
Object.defineProperty(process, "platform", {
@@ -972,7 +972,7 @@ describe("plugin sdk alias helpers", () => {
972972
}
973973
});
974974

975-
it("keeps plugin loader dist shortcuts on transpiled Jiti on Windows", () => {
975+
it("keeps plugin loader dist shortcuts on native Jiti on Windows for JS entries", () => {
976976
const originalPlatform = process.platform;
977977
Object.defineProperty(process, "platform", {
978978
configurable: true,
@@ -984,7 +984,7 @@ describe("plugin sdk alias helpers", () => {
984984
resolvePluginLoaderJitiTryNative(`/repo/${bundledDistPluginFile("browser", "index.js")}`, {
985985
preferBuiltDist: true,
986986
}),
987-
).toBe(false);
987+
).toBe(true);
988988
expect(
989989
resolvePluginLoaderJitiTryNative(`/repo/${bundledDistPluginFile("browser", "helper.ts")}`, {
990990
preferBuiltDist: true,

src/plugins/sdk-alias.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -695,7 +695,7 @@ export function buildPluginLoaderJitiOptions(aliasMap: Record<string, string>) {
695695

696696
function supportsNativeJitiRuntime(): boolean {
697697
const versions = process.versions as { bun?: string };
698-
return typeof versions.bun !== "string" && process.platform !== "win32";
698+
return typeof versions.bun !== "string";
699699
}
700700

701701
function isBundledPluginDistModulePath(modulePath: string): boolean {

src/test-utils/jiti-runtime.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
export function shouldExpectNativeJitiForJavaScriptTestRuntime(): boolean {
2-
return (
3-
typeof (process.versions as { bun?: string }).bun !== "string" && process.platform !== "win32"
4-
);
2+
return typeof (process.versions as { bun?: string }).bun !== "string";
53
}

0 commit comments

Comments
 (0)