mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 13:53:12 -06:00
scripts/upgrade.sh already runs `npm ci`, so this is not for the normal path. It is
for the ways a box ends up with the wrong node_modules, both of which present as
"server will not start" with an error naming a file rather than the action needed:
ROLLBACK checking out an older tag to back out a bad release restores that
tag's package.json but not its packages. This branch removes
google-auth-library, so a rollback to main would not boot — and you
are rolling back because something else already broke.
NODE UPGRADE better-sqlite3 is compiled against one ABI. Upgrading Node makes every
boot fail with NODE_MODULE_VERSION, which reads like database
corruption and is not.
Runs as the FIRST statement in server.js, before any dependency is required, and uses
only Node builtins — anything it imported could be the thing that is missing. Repairs
with `npm install --omit=dev` (never `ci` on a partly-populated tree, which would
delete a working node_modules to fix one package) or `npm rebuild better-sqlite3`, and
exits with the command to run if it cannot. ST_SKIP_DEP_PREFLIGHT=1 opts out.
⚠️ The first version of the native check was WRONG and I caught it only by running it
under a real version mismatch: better-sqlite3's entry point is plain JavaScript that
loads the compiled binding lazily, so `require()` succeeds under a Node the binary was
never built for. It reported a genuinely broken install as healthy. It now opens an
in-memory database, which is what actually pulls the binding in. A test pins that,
because the failure is invisible — the check keeps passing on every machine where
nothing is wrong.
Verified: a deleted dependency is detected, installed and the server boots (200); the
ABI mismatch is detected under Node 18 against a module built for Node 20 and reported
clean under Node 20; a healthy tree costs 8ms and touches no network.
1609 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bvjey4FNam49MN7ybjcq6A
84 lines
4.2 KiB
JavaScript
84 lines
4.2 KiB
JavaScript
'use strict';
|
|
|
|
/*
|
|
* The boot-time dependency check.
|
|
*
|
|
* It exists for the moments nobody is at their best: a rollback that restores an older
|
|
* package.json but not its packages, and a Node upgrade that leaves the native database module
|
|
* compiled against the wrong ABI. Both present as "server will not start", with an error naming a
|
|
* file rather than the action needed.
|
|
*/
|
|
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const preflight = require('../lib/preflight-deps');
|
|
|
|
test('a healthy install reports nothing missing and nothing broken', () => {
|
|
assert.deepEqual(preflight.missingDeps(), [], 'this tree should be complete');
|
|
assert.equal(preflight.nativeModuleBroken(), null, 'and the native module should load');
|
|
});
|
|
|
|
test('THE NATIVE CHECK CONSTRUCTS A DATABASE, it does not merely require the module', () => {
|
|
/*
|
|
* better-sqlite3's entry point is plain JavaScript that loads the compiled binding lazily, so
|
|
* `require()` SUCCEEDS under a Node whose ABI the binary was never built for. The first version
|
|
* of this check stopped at require and therefore reported a genuinely broken install — verified
|
|
* against a real Node 18 / Node 20 mismatch — as healthy.
|
|
*
|
|
* Pinned as source because the failure is invisible: the check keeps passing, on every machine
|
|
* where nothing is wrong, right up until the one where something is.
|
|
*/
|
|
const src = fs.readFileSync(path.join(__dirname, '..', 'lib', 'preflight-deps.js'), 'utf8');
|
|
const fn = src.slice(src.indexOf('function nativeModuleBroken'), src.indexOf('function run('));
|
|
assert.match(fn, /new Database\(':memory:'\)/,
|
|
'nativeModuleBroken must open a database, or an ABI mismatch goes undetected');
|
|
assert.ok(!/^\s*require\('better-sqlite3'\);\s*$/m.test(fn), 'a bare require is not a load test');
|
|
});
|
|
|
|
test('missingDeps agrees with what is actually on disk', () => {
|
|
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
const declared = Object.keys(pkg.dependencies || {});
|
|
assert.ok(declared.length > 0, 'the server declares dependencies');
|
|
const reported = preflight.missingDeps();
|
|
for (const name of declared) {
|
|
const present = fs.existsSync(path.join(__dirname, '..', 'node_modules', name, 'package.json'));
|
|
assert.equal(present, !reported.includes(name), `${name}: presence and report disagree`);
|
|
}
|
|
});
|
|
|
|
test('the preflight uses only Node builtins', () => {
|
|
/*
|
|
* It runs BEFORE dependencies are installed, so anything it imported could be the very thing
|
|
* that is missing — and the failure would be the one it exists to prevent, with an extra layer
|
|
* of confusion on top.
|
|
*/
|
|
const src = fs.readFileSync(path.join(__dirname, '..', 'lib', 'preflight-deps.js'), 'utf8');
|
|
const requires = [...src.matchAll(/require\('([^']+)'\)/g)].map((m) => m[1]);
|
|
const builtins = new Set(['fs', 'path', 'child_process', 'os', 'crypto', 'util']);
|
|
for (const r of requires) {
|
|
// better-sqlite3 is the thing being TESTED for loadability, not a dependency of this file.
|
|
if (r === 'better-sqlite3') continue;
|
|
assert.ok(builtins.has(r) || r.startsWith('.'), `preflight must not depend on ${r}`);
|
|
}
|
|
});
|
|
|
|
test('it can be turned off for an air-gapped host', () => {
|
|
const src = fs.readFileSync(path.join(__dirname, '..', 'lib', 'preflight-deps.js'), 'utf8');
|
|
assert.match(src, /ST_SKIP_DEP_PREFLIGHT/, 'an operator who manages node_modules must be able to opt out');
|
|
const saved = process.env.ST_SKIP_DEP_PREFLIGHT;
|
|
process.env.ST_SKIP_DEP_PREFLIGHT = '1';
|
|
try { assert.doesNotThrow(() => preflight.preflight(), 'opting out must be a clean no-op'); }
|
|
finally { if (saved === undefined) delete process.env.ST_SKIP_DEP_PREFLIGHT; else process.env.ST_SKIP_DEP_PREFLIGHT = saved; }
|
|
});
|
|
|
|
test('server.js runs the preflight BEFORE requiring anything', () => {
|
|
const src = fs.readFileSync(path.join(__dirname, '..', 'server.js'), 'utf8');
|
|
const preflightAt = src.indexOf("require('./lib/preflight-deps')");
|
|
const firstDep = src.indexOf("require('express')");
|
|
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');
|
|
});
|