From 7912fb574a56c31d13f073ebe73ea6bb9cae0040 Mon Sep 17 00:00:00 2001 From: SandObserver <260779319+SandObserver@users.noreply.github.com> Date: Sun, 9 Aug 2026 15:31:14 -0400 Subject: [PATCH] Chore: declare the Node floor the API actually needs routes/config.js requires ui/js/link-url.js, an ES module, and that route is loaded at startup. require() of an ES module was behind a flag on Node 20.x until 20.19.0, so the declared floor of >=20 named versions where the API does not start at all. Raise it to >=20.19 and pin it: one test fails if the floor drops below the release where require(esm) stopped needing a flag, another fails if the cross-boundary require goes away and the floor can be revisited. --- api/package.json | 2 +- api/test/link-url.test.js | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/api/package.json b/api/package.json index f444c751..26388474 100644 --- a/api/package.json +++ b/api/package.json @@ -4,7 +4,7 @@ "description": "Stackyard API - Self-hosted homelab dashboard backend", "main": "src/server.js", "engines": { - "node": ">=20" + "node": ">=20.19" }, "scripts": { "start": "node src/server.js", diff --git a/api/test/link-url.test.js b/api/test/link-url.test.js index 21f1220c..ead66139 100644 --- a/api/test/link-url.test.js +++ b/api/test/link-url.test.js @@ -316,3 +316,28 @@ test('a config save is rejected when two items share an id', async () => { await new Promise(r => { server.closeAllConnections?.(); server.close(r); }); } }); + +/* ── the Node floor that cross-boundary require depends on ────────────────── */ + +/* require() of an ES module was behind --experimental-require-module on Node + 20.x until 20.19.0. Below that, requiring this file throws and routes/config.js + fails to load, which takes the whole API down rather than one route. The + engines floor is the only place the project states what it runs on, so it has + to be at or above the release where that stopped being true. */ +test('the declared Node floor supports requiring an ES module', () => { + const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')); + const declared = String(pkg.engines?.node || ''); + const m = declared.match(/^>=\s*(\d+)\.(\d+)/); + assert.ok(m, `engines.node should be a ">=major.minor" floor, got ${JSON.stringify(declared)}`); + const [major, minor] = [Number(m[1]), Number(m[2])]; + const ok = major > 20 || (major === 20 && minor >= 19); + assert.ok(ok, `engines.node is ${declared}; require(esm) needs 20.19 or newer`); +}); + +/* The guard above only matters while a server module requires a browser one. + If that stops being true, this test is what says the floor can be revisited. */ +test('a src module still requires across the CommonJS/ESM boundary', () => { + const src = fs.readFileSync(path.join(__dirname, '..', 'src', 'routes', 'config.js'), 'utf8'); + assert.match(src, /require\(['"][^'"]*ui\/js\/link-url\.js['"]\)/, + 'routes/config.js no longer requires the browser module; revisit the engines floor'); +});