From 24c94deda01add52f3e882baf28bcf0f7b01fc07 Mon Sep 17 00:00:00 2001 From: ScreenTinker Date: Tue, 18 Aug 2026 23:29:47 -0500 Subject: [PATCH] Repair a missing sqlite driver on hosts that have no built-in one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1.9.38 made better-sqlite3 optional so a compiler-less host installs cleanly and falls back to node:sqlite. That fallback is Node 24 in practice — absent on 20.x, flagged off on 22.x — and this project's declared floor is 22.9. On those runtimes a missing native driver is not a slower server, it is no server. Found by running the suite the way CI does rather than the way it is convenient to: `npm ci` on a machine that declines to run install scripts reported success, silently left no better-sqlite3 on disk, and 291 tests failed with "server did not boot" and nothing anywhere explaining why. npm drops an optional dependency in that situation where it would have installed a required one. missingDeps() now treats optionalDependencies as required whenever node:sqlite is unavailable, so preflight repairs it as it would any other missing package. The decision is split into requiredDeps(pkg, builtinDriver) so both kinds of host can be tested from either runtime, rather than only the one the suite happens to run on. Full suite 1782 pass / 0 fail on the native driver and on the fallback. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014kfhrUPit5MCqxeTQyqr56 --- CHANGELOG.md | 13 +++++++++++- server/lib/preflight-deps.js | 32 ++++++++++++++++++++++++++++-- server/test/preflight-deps.test.js | 30 ++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fd4a94..c1ee99b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,18 @@ attribution, not the operator's own name, so on an instance branded "Acme" it of branding. It now reads "Hide platform branding", and the equivalent in the six other translated languages. -Nothing else changed. + +### Fixed — a missing database driver is repaired again on Node 20 and 22 + +1.9.38 made `better-sqlite3` an optional dependency, so that a host without a compiler installs +cleanly and falls back to Node's built-in driver. That fallback needs Node 24: on the 20.x and 22.x +lines the built-in is absent or behind a flag, and there a missing `better-sqlite3` means the server +cannot start at all. + +npm will quietly skip an optional dependency whose install script it declines to run — leaving an +install that reports success and a server that will not boot. The startup check now treats optional +dependencies as required on any host that has no built-in driver, and repairs them like any other +missing package. ### Upgrading diff --git a/server/lib/preflight-deps.js b/server/lib/preflight-deps.js index d8cb64b..3854ad6 100644 --- a/server/lib/preflight-deps.js +++ b/server/lib/preflight-deps.js @@ -46,6 +46,32 @@ const SERVER_DIR = path.join(__dirname, '..'); const NODE_MODULES = path.join(SERVER_DIR, 'node_modules'); const INSTALL_TIMEOUT_MS = 10 * 60 * 1000; // a cold install on a Pi is genuinely slow +/** + * Which dependency names this host actually needs on disk. + * + * ⚠️ OPTIONAL IS NOT OPTIONAL WHERE THERE IS NOTHING TO FALL BACK TO. + * + * better-sqlite3 is an optionalDependency so a host with no compiler installs cleanly and lets + * db/sqlite-driver.js drop to the built-in node:sqlite. That only works on Node 24 (23.4+); on the + * 20.x and 22.x lines the built-in is absent or flagged off, and there a missing better-sqlite3 is + * a server that cannot start at all. + * + * npm will quietly skip an optional dependency whose install script it declines to run. Observed + * exactly that: `npm ci` reported success, left no better-sqlite3 on disk, and every test that + * spawns the server failed with "server did not boot" — with nothing anywhere saying why. + * + * Split out from missingDeps so the decision can be tested on either kind of host, rather than only + * on whichever Node happens to be running the suite. + * + * @param {{dependencies?: object, optionalDependencies?: object}} pkg + * @param {boolean} builtinDriver whether this runtime has node:sqlite + */ +function requiredDeps(pkg, builtinDriver) { + const names = Object.keys((pkg && pkg.dependencies) || {}); + if (!builtinDriver) names.push(...Object.keys((pkg && pkg.optionalDependencies) || {})); + return names; +} + /** Which declared dependencies are not on disk. */ function missingDeps() { let pkg; @@ -54,7 +80,9 @@ function missingDeps() { } catch { return []; // no package.json is not our problem to diagnose } - const declared = Object.keys(pkg.dependencies || {}); + let builtinDriver = false; + try { require('node:sqlite'); builtinDriver = true; } catch { /* absent on this runtime */ } + const declared = requiredDeps(pkg, builtinDriver); return declared.filter((name) => { // A scoped or nested name is still one directory below node_modules. try { return !fs.existsSync(path.join(NODE_MODULES, name, 'package.json')); } catch { return true; } @@ -237,4 +265,4 @@ function preflight() { } } -module.exports = { preflight, missingDeps, nativeModuleBroken }; +module.exports = { preflight, missingDeps, nativeModuleBroken, requiredDeps }; diff --git a/server/test/preflight-deps.test.js b/server/test/preflight-deps.test.js index 1d68c92..21fb951 100644 --- a/server/test/preflight-deps.test.js +++ b/server/test/preflight-deps.test.js @@ -90,3 +90,33 @@ test('server.js runs the preflight BEFORE requiring anything', () => { assert.ok(preflightAt > -1, 'server.js must run the preflight'); assert.ok(preflightAt < firstDep, 'it must come before the first dependency, or it cannot help'); }); + +/* + * ⚠️ AN OPTIONAL DEPENDENCY IS NOT OPTIONAL ON A HOST WITH NO FALLBACK. + * + * better-sqlite3 became optional so a compiler-less host installs cleanly and drops to the built-in + * node:sqlite. But the built-in is Node 24 in practice — absent on 20.x, flagged off on 22.x — and + * this project's declared floor is 22.9. On those runtimes a missing better-sqlite3 is not a slower + * server, it is no server. + * + * This is not theoretical: `npm ci` on a machine that declines to run install scripts reported + * success, silently left no better-sqlite3 on disk, and every test that spawns the server failed + * with "server did not boot". Preflight has to notice and repair that. + */ +test('an optional dependency counts as required where node:sqlite is unavailable', () => { + const pkg = { dependencies: { express: '^4' }, optionalDependencies: { 'better-sqlite3': '12.9.0' } }; + + const withBuiltin = preflight.requiredDeps(pkg, true); + assert.deepEqual(withBuiltin, ['express'], + 'on Node 24 the native driver is genuinely optional — the built-in can serve'); + + const withoutBuiltin = preflight.requiredDeps(pkg, false); + assert.deepEqual(withoutBuiltin.sort(), ['better-sqlite3', 'express'], + 'on Node 20/22 a missing native driver means the server cannot start, so repair it'); +}); + +test('requiredDeps copes with a package.json missing either section', () => { + assert.deepEqual(preflight.requiredDeps({}, false), []); + assert.deepEqual(preflight.requiredDeps({ optionalDependencies: { a: '1' } }, false), ['a']); + assert.deepEqual(preflight.requiredDeps({ dependencies: { b: '1' } }, false), ['b']); +});