From ff168ab0862b38453a75bab2d8d65ac813dc7ae8 Mon Sep 17 00:00:00 2001 From: NBN-N3 Date: Mon, 27 Jul 2026 22:38:18 -0300 Subject: [PATCH] fix(electron): use NEXT_DIST_DIR when stripping stale native modules (#8794) removeNativeModules() was called with a hardcoded ".next" path while the actual distDir is NEXT_DIST_DIR (".build/next" by default). Because the function early-returns when the directory does not exist, the cleanup silently no-opped and the plain-Node-ABI better-sqlite3 copy produced by `next build` survived into the packaged app. At runtime the standalone server runs under ELECTRON_RUN_AS_NODE, so it needs the Electron ABI (148 for electron 43). Loading the ABI-137 copy fails with ERR_DLOPEN_FAILED, the app falls back to the sql.js WASM driver, the connection is closed and retried in a loop, WASM memory is never reclaimed and the process OOMs -> HTTP 500 on every route. Also adds assertNoStaleHashedNatives() so a wrong baseDir fails the build instead of silently shipping a broken installer. This has regressed at least twice (#1497 with ABI 127 vs 145, #7082/#7681 with 137 vs 148). Refs #7082, #7681, #1497, #8792. Supersedes the abandoned #7123. --- scripts/build/prepare-electron-standalone.mjs | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/scripts/build/prepare-electron-standalone.mjs b/scripts/build/prepare-electron-standalone.mjs index 7965656b59..e195f6480f 100644 --- a/scripts/build/prepare-electron-standalone.mjs +++ b/scripts/build/prepare-electron-standalone.mjs @@ -83,6 +83,24 @@ function removeNativeModules(baseDir, prefixes = ["keytar"]) { } } +// Fail the build if hashed native copies survived the cleanup above. Without this, +// a wrong baseDir makes removeNativeModules() a silent no-op (it early-returns when +// the directory does not exist) and the ABI mismatch only surfaces at runtime on a +// user machine as "Internal Server Error" on every route. +function assertNoStaleHashedNatives(baseDir, prefixes) { + if (!existsSync(baseDir)) return; + const leftovers = readdirSync(baseDir).filter((dir) => + prefixes.some((p) => dir.startsWith(p)) + ); + if (leftovers.length > 0) { + throw new Error( + `[electron] stale native module copies survived cleanup in ${baseDir}: ` + + `${leftovers.join(", ")}. These carry the plain-Node ABI and shadow the ` + + `Electron-rebuilt binaries at runtime (ERR_DLOPEN_FAILED -> sql.js fallback -> OOM).` + ); + } +} + // --- Electron-UNIQUE: rebuild better-sqlite3 against the Electron ABI -------- // // The `npm ci` at the repo root compiles better-sqlite3 for the CI *Node* ABI @@ -197,7 +215,16 @@ removeGeneratedElectronArtifacts(); // so it cannot shadow the rebuilt one. rebuildBetterSqlite3ForElectron(join(ELECTRON_STANDALONE_DIR, "node_modules")); removeNativeModules(join(ELECTRON_STANDALONE_DIR, "node_modules"), ["keytar"]); -removeNativeModules(join(ELECTRON_STANDALONE_DIR, ".next", "node_modules"), [ +removeNativeModules(join(ELECTRON_STANDALONE_DIR, NEXT_DIST_DIR, "node_modules"), [ + "better-sqlite3", + "keytar", +]); + +// Post-condition: the cleanup above must actually have removed the stale Node-ABI +// copies. It silently no-opped across releases because the path was hardcoded to +// ".next" while distDir is ".build/next", so an ABI-mismatched better_sqlite3.node +// shipped inside the installer and the app fell back to sql.js and OOM-ed. +assertNoStaleHashedNatives(join(ELECTRON_STANDALONE_DIR, NEXT_DIST_DIR, "node_modules"), [ "better-sqlite3", "keytar", ]);