fix(desktop): dual-ABI prebuilt for better-sqlite3 (Node + Electron)#96
fix(desktop): dual-ABI prebuilt for better-sqlite3 (Node + Electron)#96
Conversation
better-sqlite3 ships a single .node binary that can target either Node's
ABI or Electron's ABI but not both. Electron 33 needs ABI 130 for `pnpm
dev` and Vitest (Node 22.11) needs ABI 127 for `pnpm test`, so any
electron-rebuild flips the binary and breaks the other consumer. Result:
pre-commit hooks have been failing all day with "NODE_MODULE_VERSION 130
... requires 127" and folks have been bypassing them with --no-verify.
Fix: stage two prebuilds side by side at install time, then opt into
the right one at open() via better-sqlite3's `nativeBinding` option.
- scripts/install-sqlite-bindings.cjs (postinstall, idempotent) downloads
Node + Electron prebuilds via prebuild-install (already a transitive
dep, so no new package added) and stashes them as
better_sqlite3.node-{node,electron}.node next to the default binary.
A schemaVersion'd lock file lets the script skip on warm caches.
- snapshots-db.ts dispatches on `process.versions.electron` to pass the
matching binding path. The default binary stays in place as a fallback
for any consumer that doesn't go through this module.
Verified:
- `pnpm test` → 288/288 pass (incl. 108 snapshots tests) even after
swapping the default binary to the Electron-ABI one.
- Node rejects the Electron binary with the expected
NODE_MODULE_VERSION mismatch when pointed at it directly, confirming
the dispatch is doing real work.
- `pnpm typecheck` and `pnpm lint` clean.
There was a problem hiding this comment.
Findings
-
[Major]
postinstallhard-fails whenelectronis not installed (e.g. production-only installs), blocking install workflows —apps/desktop/scripts/install-sqlite-bindings.cjs:43
Suggested fix:function resolveElectronVersion() { try { return require('electron/package.json').version; } catch { return null; } } // ... const electronVersion = resolveElectronVersion(); if (electronVersion) { log(`downloading Electron prebuild (electron=${electronVersion}, ${platform}-${arch})`); downloadPrebuild({ pkgDir, runtime: 'electron', target: electronVersion, arch, platform, dest: electronBinary, }); } else { log('electron not installed; skipping Electron ABI prebuild'); }
-
[Minor] Missing runtime-context error for native binding resolution makes ABI failures harder to diagnose and violates the “throw with context” constraint —
apps/desktop/src/main/snapshots-db.ts:46
Suggested fix:function openDatabase(filename: string, options?: BetterSqlite3.Options): Database { const Database = require('better-sqlite3') as typeof BetterSqlite3; const nativeBinding = resolveNativeBinding(); try { return new Database(filename, { ...options, nativeBinding }); } catch (cause) { throw new Error( `Failed to open SQLite database with native binding (${nativeBinding})`, { cause: cause instanceof Error ? cause : new Error(String(cause)) }, ); } }
Summary
- Review mode: initial
- Two issues were found in the latest diff: one install-time regression risk in the new postinstall flow and one missing error-context guard in native binding open path.
docs/VISION.mdanddocs/PRINCIPLES.md: Not found in repo/docs for this checkout.
Testing
- Not run (automation)
- Suggested: add Vitest coverage for
resolveNativeBindingselection (Node vs Electron runtime) and for installer behavior whenelectronis absent.
open-codesign Bot
| return path.dirname(pkgJsonPath); | ||
| } | ||
|
|
||
| function resolveElectronVersion() { |
There was a problem hiding this comment.
resolveElectronVersion() currently throws when electron is not installed (common in production-only installs), causing postinstall to fail hard. Consider making Electron prebuild fetch conditional and non-fatal when Electron is absent.
Suggested fix:
function resolveElectronVersion() {
try {
return require('electron/package.json').version;
} catch {
return null;
}
}| return path.join(path.dirname(pkgJson), 'build', 'Release', filename); | ||
| } | ||
|
|
||
| function openDatabase(filename: string, options?: BetterSqlite3.Options): Database { |
There was a problem hiding this comment.
openDatabase() now depends on a computed nativeBinding path, but failures here currently bubble up without additional context. Wrapping this call with a contextual error improves diagnosability and aligns with the project rule to throw with context.
Suggested fix:
function openDatabase(filename: string, options?: BetterSqlite3.Options): Database {
const Database = require('better-sqlite3') as typeof BetterSqlite3;
const nativeBinding = resolveNativeBinding();
try {
return new Database(filename, { ...options, nativeBinding });
} catch (cause) {
throw new Error(`Failed to open SQLite database with native binding (${nativeBinding})`, { cause: cause instanceof Error ? cause : new Error(String(cause)) });
}
}…stinstall Prod-only installs (npm install --omit=dev, certain CI bootstraps, end-user installer build steps) skip devDependencies, so the electron module may not be present when postinstall runs. Wrap the electron version lookup in try/catch and skip the Electron native binding stage with a warning instead of hard-failing. The Node ABI binding still installs so vitest and ad-hoc node consumers keep working.
|
Addressing Codex [Major] on |
There was a problem hiding this comment.
Findings
- No new high-confidence issues found in the added/modified lines of this update.
Summary
- Review mode: follow-up after new commits
- Previously reported install regression risk in
apps/desktop/scripts/install-sqlite-bindings.cjsis addressed in the latest head. - Not found in repo/docs:
docs/VISION.md,docs/PRINCIPLES.md. - Residual risk/testing gap: this PR still has no repo-added automated test covering runtime selection of
nativeBinding(Node vs Electron) and postinstall behavior when Electron is absent.
Testing
- Not run (automation)
- Suggested tests: Vitest for
resolveNativeBinding()runtime dispatch and an installer behavior test/path check for Electron-absent environments.
open-codesign Bot
Problem
better-sqlite3ships exactly one.nodebinary which can target either Node's ABI or Electron's ABI but not both. We have two consumers of it:pnpm devwithout theCould not open the local snapshots database… NODE_MODULE_VERSION 127, version requires 130boot dialog.Whichever runtime got rebuilt last broke the other one. Pre-commit hooks have been failing all day because of this and PRs have been merged with
--no-verifyto work around it.Fix (Option 1: dual prebuilt + per-runtime
nativeBinding)apps/desktop/scripts/install-sqlite-bindings.cjsruns as a postinstall and stages two prebuilds side by side underbetter-sqlite3/build/Release/:better_sqlite3.node-node.node— Node ABI, used by vitestbetter_sqlite3.node-electron.node— Electron ABI, used by the desktop appDownloads via
prebuild-install(already a transitive dep of better-sqlite3, so no new package added). Idempotent — aschemaVersion'd lock file lets the script no-op on warm caches.snapshots-db.tsdispatches onprocess.versions.electronand passes the correct path through better-sqlite3'snativeBindingconstructor option.Picked over Option 2 (mock in tests) because it keeps tests exercising real SQLite and over Option 3 (
node:sqliteswap) because Electron 33 still ships Node 20.18 and doesn't exposenode:sqlite.Verification
pnpm test→ 288/288 pass (incl. 26 + 81 + 1 = 108 snapshot tests). Verified to still pass after manually overwriting the defaultbetter_sqlite3.nodewith the Electron-ABI binary, proving the dispatch is doing real work.NODE_MODULE_VERSIONmismatch; passing the Node path loads andCREATE TABLEworks.pnpm typecheck✅ ·pnpm lint✅ · pre-commit hook (pnpm test) ✅ ran on this commit without--no-verify.PRINCIPLES checks
nativeBindingoption; works on macOS arm64/x64, Windows x64/arm64, Linux x64 (all platforms prebuild-install supports). Lock file isschemaVersion-tagged for migration.prebuild-install). The two extra.nodefiles live only innode_modulesand are not packaged into the installer.snapshots-db.ts; postinstall is one self-contained script with an idempotency lock.