mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-14 14:23:14 -06:00
Node 22 preparation: upgrade runbook, changelog notes, and the one test that breaks (#265)
* Document the Node.js upgrade procedure and this build's reinstall requirement Upgrading the runtime does not go through scripts/upgrade.sh, so nothing reinstalls dependencies — which is precisely when the one remaining native module goes stale. The runbook now covers the version floor imposed by --env-file-if-exists, why the better-sqlite3 pin is exact, and why a version without a matching prebuild can turn Restart=always into a boot loop. Also records that this build changes dependencies in both directions: rolling back past it needs the reinstall too, because earlier builds import sharp at runtime and this one drops it from production dependencies. Kept deployment-neutral — no hostnames, addresses, or environment specifics. * Fix the only test that fails on Node 22 Node 22 added a built-in `navigator` global, defined as a getter with no setter. The test's shim assigned to it, which throws "only a getter" under 'use strict' on 22 while being a normal assignment on Node 20, where the global does not exist at all. It is configurable, so define it instead of assigning. Defining it unconditionally is also the better fixture: Node 22's own navigator reports the HOST locale, so a test reading its language would otherwise depend on the machine or CI runner it happens to run on. This was the single failure in an otherwise clean Node 22 run (1639/1640 with better-sqlite3 12.9.0), and it is confined to test code — no production server or frontend file assigns to globalThis.navigator. 1649/1649 on Node 20.
This commit is contained in:
parent
c436a44c89
commit
128a5be1b1
55
CHANGELOG.md
55
CHANGELOG.md
|
|
@ -1,5 +1,60 @@
|
|||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
<!-- Rename this heading to the version when cutting the release — scripts/bump-version.sh
|
||||
warns if CHANGELOG.md has no entry matching the new version. -->
|
||||
|
||||
### ⚠️ Upgrading to this build requires reinstalling dependencies
|
||||
|
||||
Both changes below alter `server/package.json`, so **`npm ci --omit=dev` is required, not
|
||||
optional** — in both directions. The runbook's rollback step marks that command "only if
|
||||
dependencies changed"; for this release they did.
|
||||
|
||||
- **Upgrading**: `scripts/upgrade.sh` already runs it. A hand-rolled deploy that skips it leaves a
|
||||
`better-sqlite3` that no longer matches `package.json`.
|
||||
- **Rolling back past this build**: the reinstall is **mandatory**. Earlier builds import `sharp` at
|
||||
runtime to thumbnail images, and this build removes it from the production dependencies — so
|
||||
rolling back the code without reinstalling leaves a server whose image ingest cannot load its
|
||||
decoder. `lib/preflight-deps.js` catches this at boot and repairs it, but do not rely on that as
|
||||
the plan.
|
||||
|
||||
Docker deployments need no action either way: dependencies are installed inside the image.
|
||||
|
||||
No migrations, no configuration changes, no player-side changes.
|
||||
|
||||
### Changed — image processing no longer uses a native module
|
||||
`sharp` is gone. Thumbnailing and image measurement are pure JavaScript (Jimp) with WebAssembly
|
||||
codecs for webp and avif, running on a worker thread. `sharp` remains as a development dependency
|
||||
for test fixtures, so `--omit=dev` excludes it from a deployed install entirely.
|
||||
|
||||
The motivation is that a native module needs a prebuilt binary matching both the platform and the
|
||||
Node ABI; when there isn't one, the failure arrives at load time and reads like database corruption
|
||||
rather than a missing image library. `better-sqlite3` is now the only native module left.
|
||||
|
||||
Format support is unchanged in practice. jpeg, png, gif, tiff and bmp decode natively; webp and avif
|
||||
via WebAssembly. `.heic` still produces no thumbnail — it never did, because the `sharp` builds in
|
||||
use decode AV1 but refuse HEVC.
|
||||
|
||||
Images are decoded on a worker thread rather than in-process. Pure-JavaScript decoding costs about a
|
||||
second for a 12-megapixel photo, which in-process would block the event loop — and the thumbnail
|
||||
backfill walks an entire library at boot, which is exactly how a maintenance task turns into missed
|
||||
heartbeats and players marked offline. Thumbnailing is slower in wall-clock terms than the native
|
||||
library was, and no longer competes with serving requests.
|
||||
|
||||
### Changed — `better-sqlite3` pinned to 12.9.0
|
||||
Preparation for a future Node 22 upgrade, landed separately so the runtime move and the database
|
||||
driver move stay independently reversible.
|
||||
|
||||
The pin is **exact on purpose**. 12.9.0 is the last release publishing prebuilt binaries for both
|
||||
the current and the next Node major; later 12.x releases dropped the older one while still
|
||||
advertising support for it in `engines`. A caret range would resolve to one of those and silently
|
||||
turn installation into a from-source compile. `lib/preflight-deps.js` explains this at the point
|
||||
anyone debugging the resulting failure would be reading.
|
||||
|
||||
Nothing in the query API changed — every major since 9.x was bumped only to drop end-of-life Node
|
||||
and Electron versions.
|
||||
|
||||
## 1.9.34-alpha6
|
||||
|
||||
### Added — a setup guide for single sign-on
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ did not behave.
|
|||
- [Verifying a deploy](#verifying-a-deploy)
|
||||
- [Rolling back](#rolling-back)
|
||||
- [Releases and version numbers](#releases-and-version-numbers)
|
||||
- [Upgrading Node.js](#upgrading-nodejs)
|
||||
- [Traps worth knowing before they bite](#traps-worth-knowing-before-they-bite)
|
||||
|
||||
---
|
||||
|
|
@ -253,12 +254,68 @@ or an image, delete-and-repush is not a fix; cut the next version instead.
|
|||
|
||||
---
|
||||
|
||||
## Upgrading Node.js
|
||||
|
||||
Upgrading the runtime is not like deploying a release: nothing in the app's own upgrade path is
|
||||
involved, so the usual `scripts/upgrade.sh` never runs and nothing reinstalls dependencies. Read
|
||||
this before changing the Node major.
|
||||
|
||||
**Do it as two separate deploys, never one.** Move the app to a release whose dependencies support
|
||||
both the old and new Node major first, confirm it on the runtime you already have, and only then
|
||||
change Node. Each half is then independently reversible. Doing both at once means a failure gives
|
||||
you nothing to bisect and no single step to undo.
|
||||
|
||||
**Check the version floor.** `npm start` uses `node --env-file-if-exists=.env`. That flag reached
|
||||
the Node 22 line only in **22.9.0** — it works on Node 20 because it was separately backported
|
||||
there. On Node 22.0–22.8 the server refuses to start with `node: bad option`. Target 22.9.0 or
|
||||
newer.
|
||||
|
||||
**One native module has to survive the move.** `better-sqlite3` is compiled against a single Node
|
||||
ABI, so changing Node invalidates it. Two things make this survivable:
|
||||
|
||||
- `lib/preflight-deps.js` runs before anything else at boot, detects the mismatch by *opening a
|
||||
database* (a bare `require` succeeds even on a wrong ABI, so it is not a valid check), and repairs
|
||||
it with `npm rebuild better-sqlite3`.
|
||||
- The pinned version ships **prebuilt binaries for both the current and the next Node major**, so
|
||||
that repair downloads a binary instead of compiling one.
|
||||
|
||||
⚠️ **That second point is why the version is pinned exactly rather than with a caret**, and why
|
||||
widening it is risky in a way `package.json` does not show. A version with no prebuild for your Node
|
||||
falls back to a from-source `node-gyp` build — and because preflight rebuilds *synchronously before
|
||||
the server listens*, a compile that outlives `TimeoutStartSec` turns `Restart=always` into a boot
|
||||
loop that never finishes. Before changing that pin, check the project's release assets and confirm a
|
||||
prebuild exists for every Node ABI you intend to run. A build toolchain (`python3`, `make`, `g++`)
|
||||
should still be present as a fallback.
|
||||
|
||||
**Native (git + systemd)**
|
||||
|
||||
1. Back up first — a Node upgrade cannot corrupt the database, but you want the rollback anyway.
|
||||
2. Change the Node major. If Node came from a distribution repository pinned to a major, the repo
|
||||
definition itself must be repointed — upgrading the package alone can never cross majors, and
|
||||
this pin lives in system configuration rather than in this repository.
|
||||
3. `node --version` to confirm.
|
||||
4. Rebuild the native module explicitly (`npm rebuild better-sqlite3` as the service user, in
|
||||
`server/`), or let preflight do it on the next restart. Doing it by hand keeps the logs readable.
|
||||
5. Restart, then verify as in [Verifying a deploy](#verifying-a-deploy). In the logs, confirm
|
||||
preflight reports a successful rebuild rather than exiting.
|
||||
|
||||
**Docker** — nothing to rebuild. Change the base image, build, and deploy the new tag: dependencies
|
||||
are installed inside the image against its own Node, so the ABI can never be stale. Rollback is
|
||||
repinning the previous tag.
|
||||
|
||||
**Afterwards, move CI too.** CI pins its own Node version, and it will happily keep validating a
|
||||
version nobody runs — which is worse than no signal, because it looks like coverage. The Docker base
|
||||
image is a separate pin from the CI one; both need changing or what CI tests and what ships diverge.
|
||||
|
||||
---
|
||||
|
||||
## Traps worth knowing before they bite
|
||||
|
||||
**Native modules are built for one Node ABI.** `better-sqlite3` is compiled against the Node that
|
||||
installed it. Run the app — or its tests — under a different major version and it fails with
|
||||
`NODE_MODULE_VERSION` mismatch, which presents as hundreds of unrelated test failures rather than
|
||||
one clear error. Use the same Node the service runs.
|
||||
one clear error. Use the same Node the service runs. See [Upgrading Node.js](#upgrading-nodejs)
|
||||
before changing it deliberately.
|
||||
|
||||
**SQLite foreign keys are off unless enabled per connection.** A declared `ON DELETE CASCADE` does
|
||||
not fire on its own, so deleting a parent row can leave orphaned children. Check with
|
||||
|
|
|
|||
|
|
@ -24,7 +24,15 @@ globalThis.localStorage = {
|
|||
removeItem: (k) => store.delete(k),
|
||||
clear: () => store.clear(),
|
||||
};
|
||||
globalThis.navigator = globalThis.navigator || { language: 'en' };
|
||||
// Node 22 added a built-in `navigator` global, defined as a getter with NO setter — so the plain
|
||||
// assignment this used to do throws ("only a getter") under 'use strict' there, while being fine on
|
||||
// Node 20 where the global does not exist at all. It is configurable, so define it rather than
|
||||
// assign. Doing that unconditionally is also the more honest fixture: Node 22's own navigator
|
||||
// reports the HOST locale (en-US here, something else on another machine or in CI), and a test that
|
||||
// reads its language should not depend on where it runs.
|
||||
Object.defineProperty(globalThis, 'navigator', {
|
||||
value: { language: 'en' }, configurable: true, writable: true,
|
||||
});
|
||||
|
||||
const MOD = pathToFileURL(path.join(__dirname, '..', '..', 'frontend', 'js', 'components', 'getting-started.js')).href;
|
||||
let GS;
|
||||
|
|
|
|||
Loading…
Reference in a new issue