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.
This commit is contained in:
NBN-N3
2026-07-27 22:38:18 -03:00
committed by GitHub
parent 0bd7ceadf8
commit ff168ab086

View File

@@ -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",
]);