Skip to content

Commit d2d3ca9

Browse files
authored
feat(db): support Node.js 18+ for the npm package via WASM SQLite fallback (#1260)
## Why The npm package required Node.js 22.15+ purely because our config/cache/auth layer depends on the built-in `node:sqlite` module, which older runtimes don't ship. That excluded users still on Node 18/20 for no fundamental reason — the CLI's actual SQLite footprint is tiny (prepare → get/all/run, exec, manual transactions). A user asked for Node 20 support and proposed `better-sqlite3`. We pushed back on that: a native addon breaks the single-file bundle, reintroduces per-platform/per-ABI install flakiness, and violates our zero-runtime-dependencies rule. ## Approach Fall back to a pure-WASM driver (`node-sqlite3-wasm`) on Node < 22.15, selected at runtime behind the existing single-file adapter: - **22.15+** → native `node:sqlite` (unchanged fast path) - **18–22.14** → bundled WASM driver It's a bundled devDependency, so the single-file bundle and no-runtime-deps guarantees both hold. Critically, the driver is `require()`d lazily so the standalone binary build externalizes it: the SEA binary always embeds a modern LTS Node and uses native `node:sqlite`, making the WASM path dead code there that adds **zero bytes** to the binary. This was the hard constraint for taking this route over `better-sqlite3`. ## Tradeoff The WASM fallback can't use SQLite WAL mode, so its local cache is slower and less concurrency-friendly. Acceptable for a single-process CLI, and the docs now steer people toward the standalone binary or Node 22.15+. ## Version floors Dev tooling still needs 22.15+ (it runs the sources against `node:sqlite` via tsx). So `engines.node` advertises the consumer floor (`>=18`) while a new `devEngines` field records the development floor — the doc generator was decoupled to keep the two independent. CI now runs the npm-package job on a Node 20 matrix entry so the WASM fallback is exercised, not just shipped.
1 parent 7b6a33c commit d2d3ca9

16 files changed

Lines changed: 922 additions & 213 deletions

File tree

.github/workflows/ci.yml

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ env:
3939
# APIs). Fixed in 24.18.0 / 22.23.1 (nodejs/node#64004). A floating major
4040
# ("24"/"22") silently reuses the runner's pre-cached buggy patch, so pin the
4141
# exact patched versions here and bump them in one place.
42+
#
43+
# Node 20 is the WASM-SQLite floor: the npm package supports Node >= 18, and
44+
# on < 22.15 (which lacks node:sqlite) it falls back to the bundled
45+
# node-sqlite3-wasm driver. The npm-package job runs a Node 20 matrix entry
46+
# so that fallback path is exercised in CI rather than only in production.
47+
NODE_VERSION_20: "20.20.2"
4248
NODE_VERSION_22: "22.23.1"
4349
NODE_VERSION_24: "24.18.0"
4450

@@ -770,23 +776,25 @@ jobs:
770776
run: pnpm run test:e2e
771777

772778
build-npm:
773-
name: Build npm Package (Node ${{ matrix.node }})
779+
name: Build npm Package (smoke Node ${{ matrix.node }})
774780
needs: [lint, test-unit]
775781
runs-on: ubuntu-latest
776782
environment: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) && 'production' || '' }}
777783
strategy:
778784
fail-fast: false
779785
matrix:
780-
node: ["22", "24"]
786+
node: ["20", "22", "24"]
781787
steps:
782788
- uses: actions/checkout@v6
783789
- uses: pnpm/action-setup@v4
790+
# Build under our development Node floor (22.15+). The build tooling
791+
# (tsx loader hooks in script/require-shim.mjs) uses node:module APIs
792+
# only present on 22.15+, so the bundle is always produced on a modern
793+
# Node — matching how we actually publish. The matrix Node version is
794+
# only used to *run* the produced artifact below.
784795
- uses: actions/setup-node@v6
785796
with:
786-
# Matrix entries stay bare majors ("22"/"24") for the job name and the
787-
# `matrix.node == '22'` artifact guard below; map each to its exact
788-
# patched version from the central env block here.
789-
node-version: ${{ matrix.node == '24' && env.NODE_VERSION_24 || env.NODE_VERSION_22 }}
797+
node-version: ${{ env.NODE_VERSION_22 }}
790798
- uses: actions/cache@v5
791799
id: cache
792800
with:
@@ -799,6 +807,16 @@ jobs:
799807
# Environment-scoped (production) — see note in build-binary.
800808
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
801809
run: pnpm run bundle
810+
- run: npm pack
811+
# Switch to the matrix Node to run the produced artifact. This is the
812+
# consumer's perspective: Node 20 exercises the WASM SQLite fallback
813+
# (no node:sqlite before 22.15); 22/24 use the native driver.
814+
- uses: actions/setup-node@v6
815+
with:
816+
# Matrix entries stay bare majors ("20"/"22"/"24") for the job name
817+
# and the `matrix.node == '22'` artifact guard below; map each to its
818+
# exact patched version from the central env block here.
819+
node-version: ${{ matrix.node == '24' && env.NODE_VERSION_24 || matrix.node == '20' && env.NODE_VERSION_20 || env.NODE_VERSION_22 }}
802820
- name: Smoke test (Node.js)
803821
run: node dist/bin.cjs --help
804822
- name: Smoke test (Node.js — deep)
@@ -809,7 +827,8 @@ jobs:
809827
run: |
810828
# auth status without a token exercises SQLite init, schema
811829
# migrations, telemetry lazy import, and the CJS require chain.
812-
# Expected: exit 10 (AUTH_NOT_AUTHENTICATED), NOT a crash/syntax error.
830+
# On Node 20 this runs entirely through the bundled WASM SQLite
831+
# driver. Expected: exit 10 (AUTH_NOT_AUTHENTICATED), NOT a crash.
813832
OUTPUT=$(node dist/bin.cjs auth status 2>&1) && EXIT_CODE=$? || EXIT_CODE=$?
814833
if [[ $EXIT_CODE -ne 10 ]]; then
815834
echo "::error::Expected exit code 10 (not authenticated), got $EXIT_CODE"
@@ -821,7 +840,6 @@ jobs:
821840
echo "$OUTPUT"
822841
exit 1
823842
fi
824-
- run: npm pack
825843
- name: Upload artifact
826844
if: matrix.node == '22'
827845
uses: actions/upload-artifact@v7

.lore.md

Lines changed: 142 additions & 166 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ yarn global add sentry
3737
bun add -g sentry
3838
```
3939

40-
> The npm/pnpm/yarn packages require Node.js 22.15+.
40+
> The npm/pnpm/yarn packages require Node.js 18+. On Node.js 22.15+ the CLI uses the built-in `node:sqlite`; on Node.js 18–22.14 it transparently falls back to a bundled WASM SQLite driver.
4141
4242
### Run Without Installing
4343

@@ -83,7 +83,7 @@ Credentials are stored in `~/.sentry/` with restricted permissions (mode 600).
8383
## Library Usage
8484

8585
<!-- GENERATED:START library-prereq -->
86-
Use Sentry CLI programmatically in Node.js (≥22.15) without spawning a subprocess:
86+
Use Sentry CLI programmatically in Node.js (≥18.0) without spawning a subprocess:
8787
<!-- GENERATED:END library-prereq -->
8888

8989
```typescript

docs/src/content/docs/library-usage.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,11 +211,15 @@ Calls should be sequential (awaited one at a time).
211211
| **Output** | Parsed object (zero-copy) | String (needs JSON.parse) |
212212
| **Errors** | `SentryError` with typed fields | Exit code + stderr string |
213213
| **Auth** | `token` option or env vars | Env vars only |
214-
| **Node.js** | >=22 required | Any version |
214+
| **Node.js** | >=18 required | Any version |
215215

216216
## Requirements
217217

218-
- **Node.js >= 22** (required for `node:sqlite`)
218+
- **Node.js >= 18**. On Node.js 22.15+ the built-in `node:sqlite` module is used; on Node.js 18–22.14 the CLI transparently falls back to a bundled WASM SQLite driver, so no native module or extra install step is required.
219+
220+
:::caution
221+
The WASM SQLite fallback (Node.js 18–22.14) does not support [WAL mode](https://www.sqlite.org/wal.html), so concurrent access from multiple processes is slower and its local cache reads/writes are less efficient. For the best performance and reliability we **strongly recommend** the [standalone binary](/installation/) (which bundles a modern runtime) or running on **Node.js 22.15+** so the native `node:sqlite` driver is used.
222+
:::
219223

220224
## Streaming Commands
221225

docs/src/content/docs/migrating-from-v3.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,18 @@ await sdk.release["set-commits"]({ orgVersion: "1.0.0", auto: true });
340340
await sdk.release.finalize({ orgVersion: "1.0.0" });
341341
```
342342

343+
:::note
344+
The `sentry` npm package requires **Node.js 18+** (v3's `@sentry/cli` supported
345+
older runtimes). On Node.js 22.15+ it uses the built-in `node:sqlite` module; on
346+
Node.js 18–22.14 it transparently falls back to a bundled WASM SQLite driver, so
347+
no native module or extra install step is needed. The standalone binary bundles
348+
its own runtime and is unaffected by your installed Node.js version.
349+
350+
For best performance we strongly recommend the standalone binary or Node.js
351+
22.15+ — the WASM fallback can't use SQLite WAL mode, making its local cache
352+
slower and less concurrency-friendly.
353+
:::
354+
343355
Mapping:
344356

345357
| v3 (`@sentry/cli`) | v4 (`sentry`) |

package.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"ink-spinner": "^5.0.0",
4444
"jpeg-js": "^0.4.4",
4545
"marked": "^15.0.12",
46+
"node-sqlite3-wasm": "0.8.59",
4647
"p-limit": "^7.3.0",
4748
"peggy": "^5.1.0",
4849
"picomatch": "^4.0.4",
@@ -75,13 +76,20 @@
7576
},
7677
"description": "Sentry CLI - A command-line interface for using Sentry built by robots and humans for robots and humans",
7778
"engines": {
78-
"node": ">=22.15"
79+
"node": ">=18.0"
80+
},
81+
"devEngines": {
82+
"runtime": {
83+
"name": "node",
84+
"version": ">=22.15"
85+
}
7986
},
8087
"files": [
8188
"dist/bin.cjs",
8289
"dist/index.cjs",
8390
"dist/index.d.cts",
8491
"dist/ink-app.js",
92+
"dist/node-sqlite3-wasm.wasm",
8593
"dist/vendor/symbolic_bg.wasm"
8694
],
8795
"license": "FSL-1.1-Apache-2.0",

pnpm-lock.yaml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

script/build.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,12 @@ async function bundleJs(): Promise<boolean> {
135135
"react-reconciler/*",
136136
// The DIF loader resolves this .wasm at runtime (dev only); never bundle it.
137137
"@sentry/symbolic/symbolic_bg.wasm",
138+
// WASM SQLite fallback for Node.js < 22.15. The SEA binary always
139+
// embeds a modern LTS Node.js (>= 22.15) and uses the native
140+
// node:sqlite driver, so this fallback branch is dead code here.
141+
// Externalizing it keeps the driver (and its ~1MB .wasm) out of the
142+
// binary entirely — see src/lib/db/sqlite.ts resolveDriver().
143+
"node-sqlite3-wasm",
138144
],
139145
sourcemap: "linked",
140146
minify: true,

script/bundle.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,17 @@ const sentrySourcemapPlugin: Plugin = {
152152
};
153153

154154
// Always inject debug IDs (even without auth token); upload is gated inside the plugin
155-
/** Files that use _require() for lazy relative imports (circular dep breaking). */
155+
/**
156+
* Files that use `_require()` for lazy imports. The `require-alias` plugin
157+
* rewrites `_require(` → `require(` in these so esbuild resolves them at
158+
* bundle time. `db/sqlite` is included so the WASM SQLite fallback
159+
* (`node-sqlite3-wasm`) is actually inlined into the npm bundle — otherwise
160+
* it stays a runtime `require()` that fails in a real install (the driver is
161+
* a devDependency, not shipped). `node:sqlite` in the same file stays a
162+
* builtin and is left external by esbuild regardless.
163+
*/
156164
const REQUIRE_ALIAS_FILTER =
157-
/(?:db[\\/](?:index|schema)|list-command|telemetry)\.ts$/;
165+
/(?:db[\\/](?:index|schema|sqlite)|list-command|telemetry)\.ts$/;
158166
const REQUIRE_ALIAS_RE = /\b_require\(/g;
159167

160168
/** Transform _require() → require() so esbuild resolves lazy relative requires. */
@@ -194,7 +202,10 @@ const result = await build({
194202
// The library bundle must not suppress the host application's warnings.
195203
sourcemap: true,
196204
platform: "node",
197-
target: "node24",
205+
// Target Node.js 18 — the published package's floor (engines.node).
206+
// Older Node.js uses the bundled WASM SQLite driver (node-sqlite3-wasm);
207+
// 22.15+ uses the native node:sqlite. Downlevels newer syntax accordingly.
208+
target: "node18",
198209
format: "cjs",
199210
outfile: "./dist/index.cjs",
200211
// Inject Bun polyfills and import.meta.url shim for CJS compatibility
@@ -218,10 +229,6 @@ const result = await build({
218229
// from trying to resolve these packages in the main bundle graph.
219230
external: [
220231
"node:*",
221-
// bun:sqlite is referenced as a fallback in src/lib/db/sqlite.ts (never
222-
// reached on Node 22+ where node:sqlite is available). Mark external so
223-
// esbuild doesn't fail trying to resolve a Bun-only module.
224-
"bun:sqlite",
225232
// The DIF loader resolves this .wasm at runtime (dev only); never bundle it.
226233
"@sentry/symbolic/symbolic_bg.wasm",
227234
"ink",
@@ -240,7 +247,7 @@ const result = await build({
240247
// Write the CLI bin wrapper (tiny — shebang + version check + dispatch).
241248
// Version floor must track `engines.node` in package.json.
242249
const BIN_WRAPPER = `#!/usr/bin/env node
243-
{let v=process.versions.node.split(".").map(Number);if(v[0]<22||(v[0]===22&&v[1]<15)){console.error("Error: sentry requires Node.js 22.15 or later (found "+process.version+").\\n\\nEither upgrade Node.js, or install the standalone binary instead:\\n curl -fsSL https://cli.sentry.dev/install | bash\\n");process.exit(1)}}
250+
{let v=process.versions.node.split(".").map(Number);if(v[0]<18){console.error("Error: sentry requires Node.js 18 or later (found "+process.version+").\\n\\nEither upgrade Node.js, or install the standalone binary instead:\\n curl -fsSL https://cli.sentry.dev/install | bash\\n");process.exit(1)}}
244251
{let e=process.emit;process.emit=function(n,...a){return n==="warning"?!1:e.apply(this,[n,...a])}}
245252
require('./index.cjs')._cli().catch(()=>{process.exitCode=1});
246253
`;
@@ -310,6 +317,18 @@ await copyFile(
310317
);
311318
console.log(" -> dist/vendor/symbolic_bg.wasm (DIF parser)");
312319

320+
// Ship the WASM SQLite driver's .wasm next to the bundle. The driver JS
321+
// (node-sqlite3-wasm) is inlined into dist/index.cjs by esbuild, and its
322+
// Emscripten glue locates the .wasm via `__dirname + "/node-sqlite3-wasm.wasm"`.
323+
// Once bundled, `__dirname` is `dist/`, so the file must sit at
324+
// dist/node-sqlite3-wasm.wasm. Only loaded on Node.js < 22.15 (see
325+
// src/lib/db/sqlite.ts resolveDriver()); harmless dead weight on newer Node.
326+
await copyFile(
327+
"./node_modules/node-sqlite3-wasm/dist/node-sqlite3-wasm.wasm",
328+
"./dist/node-sqlite3-wasm.wasm"
329+
);
330+
console.log(" -> dist/node-sqlite3-wasm.wasm (WASM SQLite fallback)");
331+
313332
// Calculate bundle size (only the main bundle, not source maps)
314333
const bundleOutput = result.metafile?.outputs["dist/index.cjs"];
315334
const bundleSize = bundleOutput?.bytes ?? 0;

script/generate-docs-sections.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -310,15 +310,41 @@ function extractNodeVersion(): string {
310310
return match[1];
311311
}
312312

313+
/**
314+
* Extract the Node.js minimum version for *development* from
315+
* `devEngines.runtime.version`, falling back to `engines.node`.
316+
*
317+
* The published package supports a lower Node.js floor than local
318+
* development: consumers on older Node.js use a bundled WASM SQLite driver,
319+
* but contributors run the sources directly (tsx) against `node:sqlite`,
320+
* which requires Node.js 22.15+. This keeps the two floors independent.
321+
*/
322+
function extractDevNodeVersion(): string {
323+
const constraint: string | undefined =
324+
// biome-ignore lint/suspicious/noExplicitAny: devEngines is not in the pkg type
325+
(pkg as any).devEngines?.runtime?.version ?? pkg.engines?.node;
326+
if (!constraint) {
327+
throw new Error("Missing devEngines.runtime.version and engines.node");
328+
}
329+
const match = constraint.match(SEMVER_RE);
330+
if (!match) {
331+
throw new Error(
332+
`Cannot extract dev Node.js version from "${constraint}". ` +
333+
"Expected a semver-like version (e.g., >=22.15)"
334+
);
335+
}
336+
return match[1];
337+
}
338+
313339
/** Generate dev prerequisite line for README.md. */
314340
function generateDevPrereq(): string {
315-
return `- [Node.js](https://nodejs.org) v${extractNodeVersion()}+ and [pnpm](https://pnpm.io) v${extractPnpmVersion()}+`;
341+
return `- [Node.js](https://nodejs.org) v${extractDevNodeVersion()}+ and [pnpm](https://pnpm.io) v${extractPnpmVersion()}+`;
316342
}
317343

318344
/** Generate dev prerequisite lines for contributing.md. */
319345
function generateDevPrereqContributing(): string {
320346
return [
321-
`- [Node.js](https://nodejs.org) (v${extractNodeVersion()} or later)`,
347+
`- [Node.js](https://nodejs.org) (v${extractDevNodeVersion()} or later)`,
322348
`- [pnpm](https://pnpm.io) (v${extractPnpmVersion()} or later)`,
323349
].join("\n");
324350
}
@@ -331,7 +357,7 @@ function generateLibraryPrereq(): string {
331357
/** Generate dev prerequisite lines for DEVELOPMENT.md. */
332358
function generateDevPrereqDevelopment(): string {
333359
return [
334-
`- [Node.js](https://nodejs.org/) v${extractNodeVersion()}+ installed`,
360+
`- [Node.js](https://nodejs.org/) v${extractDevNodeVersion()}+ installed`,
335361
`- [pnpm](https://pnpm.io/) v${extractPnpmVersion()}+ installed`,
336362
].join("\n");
337363
}

0 commit comments

Comments
 (0)