feat(runtime): raise the baseline to Node 24 LTS and refresh the dependency tree (#326) - #334
Merged
Conversation
…ndency tree (#326) The agent runtime the LibreDB Agent epic depends on needs a modern Node and ESM, so the app and the published package move to Node 24 LTS before any agent code lands. This is the runway change, not an agent feature. engines.node goes to >=24.0.0 and the launcher preflight follows it. Note what that field does NOT do: it produces no error for a bare spec. npm's version picker silently resolves the newest ENGINE-COMPATIBLE release instead, which was measured, not assumed - `npm install eslint` under Node 18 installs 9.39.5 with no warning at all. So a Node 20/22 user running `npx @libredb/studio` is pinned to the last pre-floor release rather than refused, and the acceptance criterion asking for "a clear preflight error" cannot be met through engines. The release runbook now deprecates the pinned version with a pointer, because a deprecation notice is the one message npm does print on install, and npx-engine-smoke.yml grows `pinned` legs that assert those users still land on a release that BOOTS (the #130 regression class). better-sqlite3 12 -> 13, which is what makes Node 26 fully supported rather than degraded. v13 moved to the N-API: one prebuilt binary per platform, valid across Node majors. Probed both directions in containers - v12's binding (NODE_MODULE_VERSION 137) throws ERR_DLOPEN_FAILED on Node 26 (147), v13 runs clean on 24 and 26 from the same install. Consequences: the launcher has a floor and no ceiling, the engine-smoke matrix drops the 20.9/22 tiers for 24 + 26 where the node26 leg proves a Node-24-built payload runs its native module on another major, and `bindings` / `file-uri-to-path` leave the payload and the image entirely. The storage ABI guard said "requires Node.js 24+", which a Node 26 user reads as satisfied while the module still refuses to load. A native binding's constraint is the ABI it was built against, never a version floor - reworded, with a regression test that fails on floor-shaped wording. trustedDependencies is now an explicit allowlist. bun synthesizes a `node-gyp rebuild` for any package shipping a binding.gyp and ignores better-sqlite3 13's `gypfile: false`, so installs died on a toolchain the package does not need. The three named are exactly what ran scripts before the field existed. Everything else outdated moves within its existing range (~50 packages). Two of those bumps were real work: - mysql2 3.23 narrowed `execute`'s values parameter from `any` to a union that excludes undefined; the shared provider interface passes `unknown[]`, so the 15 call sites go through one documented boundary cast. - happy-dom 20.11 now validates dispatchEvent's argument against its own Event class. tests/setup-dom.ts only copied DOM globals that were missing, and Bun ships its own CustomEvent, so events were built in one realm and dispatched into another. Latent since the setup was written; the event constructors now always come from the window. knip 6.32 (which `^6.17.1` already resolved to) correctly reports 39 type re-exports in internal barrels that nothing imports. They are not API - the package's public surface is src/exports/*, which re-exports from src/lib/types directly and never passes through those barrels - so they are removed. .dockerignore did not exclude the sibling product trees, so a local `docker build` copied whatever the developer had built: a Rust target/ dir put 2.3 GB of desktop-shell objects into the production image. 5.96 GB -> 539 MB, back in line with the published images, and a locally built image is now representative of what CI publishes. Verified beyond the six gates: 100% line coverage held at 30830 lines, and the full editor -> API -> SQLite provider -> results grid path was driven in Chrome against both `bun dev` and the built image, the latter with zero console errors. TypeScript 7 was probed too - it compiles this project with no errors in 1.8s against 7.7s - but typescript-eslint's peer range still excludes it; recorded as BACKLOG A7 with A6 (Druid's JSON.rawJSON workaround, now unnecessary).
…326) A7 put the onus on typescript-eslint's peer range. Measuring the published packages shows the blocker is a level up: typescript@7.0.2 and today's 7.1.0-dev nightly both export only `version` and `versionMajorMinor` from `require("typescript")` - `createProgram` and `Extension` are undefined, so there is no API for typescript-eslint, eslint-config-next or tsup to port to. Records the real tracking issue (#10940, open, labelled 'blocked by external API'), notes that #12518 reading as 'not planned' is just how close-as-duplicate renders, and captures Microsoft's documented 6.0/7.0 side-by-side setup as the interim option if the 4x typecheck gain is wanted before the API lands.
Trivy flagged lodash 4.17.21 (CVE-2026-4800, arbitrary code execution via untrusted input in template imports, HIGH). The alert reads as introduced by this pull request only because bun.lock moved - main carries the identical version and hash - but it is fixable here and cheap to take. lodash reaches the tree solely through recharts, which asks for ^4.17.21, so the patched 4.18.1 is already inside the parent's range and nothing in this repository imports lodash directly. An override forces the resolution forward without claiming a direct dependency the code does not have; `bun update lodash` would have added one, which knip would then correctly call unused. Remove the entry once recharts raises its own floor.
Contributor
There was a problem hiding this comment.
Pull request overview
Raises the runtime baseline to Node 24, modernizes dependencies, and validates Node 26 compatibility without introducing agent functionality.
Changes:
- Enforces Node 24+ and updates runtime documentation and smoke tests.
- Migrates to better-sqlite3 13 N-API prebuilds.
- Refreshes dependencies and removes unused internal type exports.
Reviewed changes
Copilot reviewed 39 out of 42 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
.dockerignore |
Excludes non-image build artifacts. |
.github/workflows/ci.yml |
Tests Node 24 and 26 payloads. |
.github/workflows/npx-engine-smoke.yml |
Validates supported and pinned npm resolutions. |
Dockerfile |
Updates better-sqlite3 packaging. |
README.md |
Documents the Node 24 floor. |
README_zh.md |
Updates Chinese runtime guidance. |
README_ja.md |
Updates Japanese runtime guidance. |
package.json |
Raises engines and refreshes dependencies. |
bun.lock |
Resolves the refreshed dependency graph. |
bin/studio.js |
Removes obsolete degraded-runtime warnings. |
bin/lib/launcher-utils.mjs |
Enforces the Node 24 minimum. |
scripts/build-standalone-payload.sh |
Packages self-contained better-sqlite3 v13. |
scripts/engine-smoke.sh |
Validates Node 24/26 runtime behavior. |
docs/BACKLOG.md |
Records deferred runtime follow-ups. |
docs/DESKTOP_WRAPPER_SPIKE.md |
Updates native-module packaging notes. |
docs/DISTRIBUTION.md |
Documents runtime support and npm pinning. |
docs/providers/druid.md |
Updates JSON serialization rationale. |
docs/providers/sqlite.md |
Documents the Node 24 SQLite floor. |
src/components/studio/index.ts |
Removes an unused type re-export. |
src/lib/db/index.ts |
Trims unused barrel exports. |
src/lib/db/providers/sql/druid/http-transport.ts |
Updates runtime rationale. |
src/lib/db/providers/sql/mysql.ts |
Adapts parameters to mysql2 typings. |
src/lib/db/providers/sql/sqlite-driver.ts |
Updates runtime guidance. |
src/lib/db/types.ts |
Removes unused type re-exports. |
src/lib/explain/index.ts |
Trims unused explain exports. |
src/lib/llm/index.ts |
Removes unused type mirroring. |
src/lib/seed/index.ts |
Trims unused seed exports. |
src/lib/sql/index.ts |
Removes unused SQL type exports. |
src/lib/storage/index.ts |
Trims unused storage exports. |
src/lib/storage/providers/sqlite.ts |
Rewords native-module errors. |
tests/setup-dom.ts |
Aligns event constructors with happy-dom. |
tests/components/QuerySafetyDialog.test.tsx |
Reformats parameterized tests. |
tests/integration/db/druid-provider.test.ts |
Reformats maintenance tests. |
tests/security/agent-statement-boundary.test.ts |
Reformats SQLite boundary tests. |
tests/unit/lib/auth-bootstrap.test.ts |
Reformats bootstrap tests. |
tests/unit/db/clickhouse/introspect.test.ts |
Reformats introspection tests. |
tests/unit/db/clickhouse/transport.test.ts |
Reformats transport tests. |
tests/unit/db/druid/introspect.test.ts |
Reformats introspection tests. |
tests/unit/db/druid/transport.test.ts |
Reformats transport tests. |
tests/unit/launcher-utils.test.ts |
Tests the Node 24 floor. |
tests/unit/lib/storage/providers/sqlite.test.ts |
Tests ABI-neutral error wording. |
tests/unit/sql/grammar.test.ts |
Reformats grammar tests. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…seline (#326) All six were valid; each was checked against the source rather than accepted. - mysql.ts: the boundary comment claimed an `undefined` bind value 'has always been sent as NULL'. It has not - mysql2 throws 'Bind parameters must not contain undefined. To pass SQL NULL specify JS null' from lib/base/connection.js. Corrected, because the wrong version invites a future caller to pass a value the driver rejects. - BACKLOG A6 named src/lib/db/providers/sql/druid.ts, which does not exist; the serializer lives in druid/http-transport.ts. A follow-up note that points at the wrong file is worse than no note. - Dockerfile: the libc comment survived the v13 upgrade and was no longer true. lib/binding.js picks the prebuild in the RUNNING process (process.platform, process.arch, and musl via process.report), so neither the ABI nor the libc of the installing stage constrains the stage that requires it. - storage/providers/sqlite.ts: the file header still framed the guard as an older-Node ABI failure while the error message it guards now describes the N-API reality - one module documenting two mutually exclusive behaviours. - DISTRIBUTION.md: the Homebrew section still gave the ABI as the reason for the node@24 dependency. The pin stays, but as conservatism (keeping users on the major CI exercises), not necessity. - channels.yaml: the issue's runtime-ownership sweep was genuinely missing. Every channel now carries a required `runtime` field, validated against user_supplied | channel_supplied, so a new channel must state who owns the Node rather than omit it. The sweep also settled the question the issue guessed at: only 2 of 27 channels are user_supplied (npm and the standalone tarballs). deb/rpm are NOT among them - packaging/linux/fetch-node.sh installs a private bin/node into the payload - so the floor is invisible on 25 channels. That is recorded in the manifest and in DISTRIBUTION.md as a stated field, precisely because it is not derivable from `kind`.
…deploy walks the AppDir (#326) The Flatpak Smoke AppImage jobs failed with a bare 'failed to run linuxdeploy'. Caused by this branch, and by the same mechanism the script already documents one screen above for sharp. better-sqlite3 12 shipped a single binding compiled for the build host. v13 is N-API, so the one package carries a prebuild for every platform: Mach-O for darwin, PE for win32, and - the part that matters - Linux ELFs for the other arch and for musl. linuxdeploy walks every ELF in the AppDir and treats an unresolvable dependency as fatal, and `readelf -d` on prebuilds/linuxmusl-x64.node shows NEEDED libc.musl-x86_64.so.1, which is exactly the dependency named in the existing @img/*musl* prune comment. Same fix, same place, same rule the sharp prune already states: the release tarball keeps every prebuild, only this bundle drops what it cannot load. Fails loudly if the prebuild for the build arch is missing, so a future packaging change cannot silently ship an AppImage without SQLite storage. Isolated rather than assumed: the workflow is path-filtered on scripts/build-standalone-payload.sh, so this PR was its first run since 2026-07-31, which made 'pre-existing breakage' a live alternative until the musl ELF was found in the staged tree.
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 43 out of 46 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
distribution/channels.yaml:80
- This row cannot truthfully use either runtime-owner value: its own name says it covers standalone tarballs (user-supplied Node) as well as deb/rpm and snap assets (which bundle Node). Classifying the whole GitHub Release channel as
user_suppliedmakes the new channel-impact metadata wrong for most asset types. Split this inventory row by runtime ownership or allow an explicit mixed classification.
runtime: user_supplied
docs/DISTRIBUTION.md:334
- This does not make the bare-
npxpin visible to the affected Node 20/22 users. The linked #142 validation records that npm 10'snpxflow does not surface deprecation notices, and those runtimes ship that flow; the new smoke legs likewise only assert that the pinned release boots. Consequently the PR still leaves the issue's npx population silently pinned while documenting the deprecation as the notification mechanism. Provide and test a mechanism that reaches bare-npx users, or document that this acceptance criterion remains unmet.
> To make the pin visible, the release runbook `npm deprecate`s the last pre-floor version with a
> pointer to the Node 24 requirement - a deprecation notice is the one message npm *does* print on
> install. A user who pins the new version explicitly (`npx @libredb/studio@<version>`) gets the
> launcher's own preflight refusal naming the required runtime.
The payload script's recovery path for an unloadable native binding became a no-op with the v13 upgrade, and the flag I added to 'fix' it does not exist. Measured, not inferred. With prebuilds/ removed from a clean install: $ npm rebuild better-sqlite3 --build-from-source --foreground-scripts npm warn Unknown cli config "--build-from-source". This will stop working [...] rebuilt dependencies successfully $ ls node_modules/better-sqlite3/build/Release/ ls: cannot access [...]: No such file or directory Two independent faults. --build-from-source was a prebuild-install flag, and v13 dropped prebuild-install, so npm does not recognise it. And v13 declares no install lifecycle with gypfile: false, which is exactly the switch that stops npm synthesising node-gyp from binding.gyp - so the rebuild queues nothing and still reports success. Under v12 this worked, because the package declared 'install: prebuild-install || node-gyp rebuild'. The upgrade silently removed a working recovery path. CI never caught it because the block sits behind a probe that passes on every platform we build on: v13 prebuilds all five targets this script accepts. The cost was diagnostic - on a genuinely broken tree it printed 'building from source', did nothing, and died on the next line with a bare Node stack. Automating a real source build was the other option and is the wrong trade: it would put python3 and a C++ toolchain on the macOS and Windows release runners to recover a case that cannot arise on a complete install. The guard now fails with the diagnosis and hands over the one-off node-gyp command, matching the prebuild guard added to build-desktop-appimage.sh in 4f6dffc.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Closes #326.
The agent runtime the LibreDB Agent epic depends on needs a modern Node and ESM, so the app and the published package move to Node 24 LTS before any agent code lands. This is the runway change; it contains no agent feature.
The issue contradicts itself, and the measurement says which half wins
Scope bullet 1 asks for
engines.node >= 24. Acceptance criterion 1 asks thatnpx @libredb/studioon Node 20/22 "stops with a clear preflight error naming the required version". The first makes the second impossible, becauseenginesis precisely the mechanism that hides the new release from those runtimes.Measured rather than assumed -
npm install eslintunder Node 18 installs9.39.5instead of10.x, and the entire output isadded 86 packages in 3s. No warning. npm's picker silently resolves the newest engine-compatible version, which is the mechanism PR #142 documented when it lowered this same floor to fix #130.So a Node 20/22 user is pinned, not refused. Three things follow:
npm deprecateon the last pre-floor version. A deprecation notice is the one message npm does print on install, and it targets exactly the pinned population - anyone resolving a supported version never sees it.npx-engine-smoke.ymlgrowspinnedlegs (20.9, 22) asserting those users still land on a release that boots. That is the npx @libredb/studio resolves to ancient 0.9.13 for Node 24.0-24.15 users (engines floor too strict) #130 regression class, and it is now under test rather than assumed.docs/DISTRIBUTION.mdstates the pinning behaviour plainly instead of claiming a refusal that does not happen.better-sqlite3 13 is what makes Node 26 supported rather than degraded
The first draft of this change documented Node 26 as "works, minus server-side SQLite storage", because the payload's binding is compiled for whichever ABI built it. That turned out to be a stale-dependency problem, not a packaging one.
better-sqlite3 v13.0.0 moved to the N-API: one prebuilt binary per platform, valid across Node majors. Probed in containers, both directions:
ERR_DLOPEN_FAILEDConsequences: the launcher has a floor and no ceiling; the engine-smoke matrix drops the 20.9/22 tiers for 24 + 26, where the
node26leg proves a Node-24-built payload runs its native module on a different major; andbindings/file-uri-to-pathleave the payload and the runtime image entirely (they are no longer in the lockfile).The ABI guard was making a floor-shaped claim
src/lib/storage/providers/sqlite.tssaidrequires Node.js 24+. A Node 26 user reads that as satisfied while the module still refuses to load. A native binding's constraint is the ABI it was built against, never a version floor. Reworded, with a regression test that fails on floor-shaped wording in either direction.Dependency refresh
Everything outdated moves within its existing semver range (~50 packages). Two of those bumps were real work rather than a version string:
execute's values parameter fromanyto a union that excludesundefined. The provider interface every driver implements passesunknown[]and cannot be narrowed here without narrowing it for MongoDB and Redis too, so the 15 call sites go through one documented boundary cast derived from the method signature.dispatchEvent's argument against its ownEventclass.tests/setup-dom.tsonly copied DOM globals that were missing, and Bun ships its ownCustomEvent, so events were constructed in one realm and dispatched into another. Latent since the setup was written; 20.11 is just the first version that checks. Event constructors now always come from the window.better-sqlite3 12 -> 13 additionally required
trustedDependencies: bun synthesizes anode-gyp rebuildfor any package shipping abinding.gypand ignores v13'sgypfile: false, sobun installdied on a toolchain the package does not need. The field is now an explicit allowlist of the three packages that already ran scripts before it existed - which is also a supply-chain improvement, since nothing else can execute code at install time.knip 6.32 (which
^6.17.1already resolved to) correctly reports 39 type re-exports in internal barrels that nothing imports. They are not API: the package's public surface issrc/exports/*, which re-exports fromsrc/lib/typesdirectly and never passes through those barrels. Removed.The image being smoke tested was not the image CI publishes
.dockerignoredid not exclude the sibling product trees, so a localdocker buildcopied whatever the developer happened to have built. A Rusttarget/directory alone put 2.3 GB of desktop-shell objects inside the production image: 5.96 GB, against ~500-550 MB for the published ones. After excluding those trees: 539 MB. CI checkouts are clean so the published image was never affected, but local verification was measuring something else.Verification
Six gates plus
build:libandattwgreen; line coverage held at 30830/30830 (100%).Driven in Chrome with Playwright, separately against
bun devand against the built image: login, schema introspection, and the editor -> API -> SQLite provider -> results grid path. The image ranSELECT dept_name, COUNT(*) ... JOIN ... GROUP BY ...and returned correct rows with zero console errors or warnings.Deliberately not in this PR
recharts2->3,@tanstack/react-table8->9,react-resizable-panels3->4,react-day-picker9->10,ioredis5->6,oracledb6->7,framer-motion12->13,lucide-react0.562->1.31,eslint9->10,@types/node25->26.docs/BACKLOG.mdA7: it compiles this project with zero errors in 1.8s against 7.7s for 6.0.3, buttypescript-eslint@8.67.0declarespeerDependencies.typescript: ">=4.8.4 <6.1.0", and TS 7 no longer ships the in-process compiler API that typescript-eslint,eslint-config-nextand tsup's declaration build all consume. A one-line bump the day that peer range opens.node:24.16.0. Bumping the patch would reduce CVEs but invalidates the dated measurement recorded insecurity-scan.yml(2026-08-09: 4 critical, 18 high), which should be re-measured with trivy in its own change.docs/BACKLOG.mdA6 records that Druid's hand-written bigint serializer exists only becauseJSON.rawJSONwas unavailable below Node 22.2 - a constraint this PR removes.