Skip to content

Commit 85d4d2d

Browse files
committed
Fix Electron main process crashing on startup (node-llama-cpp ESM/CJS)
node-llama-cpp is ESM with top-level await; this app compiles to CommonJS, so a static import compiled straight to require(), which throws ERR_REQUIRE_ASYNC_MODULE and took down the whole main process on launch - nothing in the app was reachable, including Settings. A plain dynamic import() didn't fix it either: TypeScript's CommonJS output rewrites import(x) into Promise.resolve().then(() => require(x)), which is the exact same broken require() just wrapped in a promise. Verified this by inspecting the compiled dist/llamacpp-manager.js output directly rather than assuming dynamic import would be enough. Fixed by building the import() call via `new Function(...)` so TypeScript can't see (and rewrite) it - this forces Node's real ESM dynamic import at runtime instead of the broken require(). Verified by actually launching the Electron dev app (previously crashed instantly; now starts and runs normally).
1 parent 3eb851f commit 85d4d2d

1 file changed

Lines changed: 26 additions & 2 deletions

File tree

‎app/src/llamacpp-manager.ts‎

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,32 @@
11
import * as fs from "node:fs";
22
import * as path from "node:path";
3-
import { getLlama, getLlamaGpuTypes, LlamaChatSession, type Llama, type LlamaModel } from "node-llama-cpp";
4-
import type { ChatHistoryItem } from "node-llama-cpp";
3+
// node-llama-cpp is ESM with top-level await — this app compiles to
4+
// CommonJS, and `require()`-ing an ESM module with top-level await throws
5+
// (ERR_REQUIRE_ASYNC_MODULE) instead of loading. A plain dynamic `import()`
6+
// doesn't dodge this: TypeScript's CommonJS output rewrites `import(x)` into
7+
// `Promise.resolve().then(() => require(x))`, which just wraps the same
8+
// broken require() in a promise. The only way to get Node's *real* dynamic
9+
// import from CJS output is to hide the `import()` call from TypeScript's
10+
// transform entirely — building it via `new Function` does that, since tsc
11+
// can't statically see (or rewrite) an import expression inside a string.
12+
// Type-only imports are erased at compile time and don't hit this problem,
13+
// so those stay static.
14+
import type { ChatHistoryItem, Llama, LlamaModel } from "node-llama-cpp";
515
import type { ChatMessage, ChatChunk, ChatOptions, ToolDefinition } from "./providers/types";
616

717
export type GpuBackend = "auto" | "vulkan" | "cuda" | "metal" | "cpu";
818

19+
type NodeLlamaCppModule = typeof import("node-llama-cpp");
20+
const dynamicImport = new Function("specifier", "return import(specifier)") as (
21+
specifier: string
22+
) => Promise<NodeLlamaCppModule>;
23+
let modulePromise: Promise<NodeLlamaCppModule> | null = null;
24+
25+
function loadNodeLlamaCpp(): Promise<NodeLlamaCppModule> {
26+
if (!modulePromise) modulePromise = dynamicImport("node-llama-cpp");
27+
return modulePromise;
28+
}
29+
930
let llamaInstance: Llama | null = null;
1031
let activeBackend: GpuBackend = "auto";
1132
// Loaded model weights are the expensive, slow-to-load part (can be several
@@ -25,13 +46,15 @@ export function setGpuBackend(backend: GpuBackend): void {
2546

2647
async function getLlamaInstance(): Promise<Llama> {
2748
if (!llamaInstance) {
49+
const { getLlama } = await loadNodeLlamaCpp();
2850
llamaInstance = await getLlama({ gpu: activeBackend === "cpu" ? false : activeBackend });
2951
}
3052
return llamaInstance;
3153
}
3254

3355
export async function getAvailableGpuBackends(): Promise<string[]> {
3456
try {
57+
const { getLlamaGpuTypes } = await loadNodeLlamaCpp();
3558
const types = await getLlamaGpuTypes("supported");
3659
return types.filter((t): t is Exclude<typeof t, false> => t !== false);
3760
} catch {
@@ -122,6 +145,7 @@ export async function chat(
122145
// across turns of the same conversation) would fix that but needs a
123146
// stable conversation identity to key off of, which isn't threaded
124147
// through this call today.
148+
const { LlamaChatSession } = await loadNodeLlamaCpp();
125149
const context = await model.createContext({ contextSize: options?.contextLength });
126150
try {
127151
const sequence = context.getSequence();

0 commit comments

Comments
 (0)