diff --git a/.changeset/node-sqlite-adapter.md b/.changeset/node-sqlite-adapter.md new file mode 100644 index 0000000000..259ccff3ff --- /dev/null +++ b/.changeset/node-sqlite-adapter.md @@ -0,0 +1,11 @@ +--- +"emdash": minor +--- + +Updates the `sqlite()` adapter and CLI to use Node's built-in `node:sqlite` driver instead of better-sqlite3. SQLite sites no longer depend on a natively compiled binary, which removes `NODE_MODULE_VERSION` rebuild errors after Node upgrades and glibc incompatibilities on shared hosting. + +Requires Node.js 22.15 or later. If you are on an older Node 22 release, upgrade Node before updating. + +Connection pragmas are now applied wherever the package opens a SQLite database, so sites using the runtime `sqlite()` adapter get the same settings the CLI already applied: `journal_mode = WAL` (readers no longer block on the writer, and FTS5 shadow tables survive a mid-write process kill), `busy_timeout = 5000` (a competing writer waits instead of failing with `SQLITE_BUSY`), and `foreign_keys = ON`. + +Query parameters are normalized so binding behaviour matches the old driver: `undefined` binds as `NULL` (accepted by better-sqlite3, rejected by `node:sqlite`), and a `Date` raises an actionable error instead of silently binding `NULL`. Booleans now bind as `0`/`1` — neither driver accepted them before, so boolean filters that previously threw at bind time (for example the plugin storage query API, whose `WhereValue` type has always advertised `boolean`) now work. diff --git a/demos/plugins-demo/package.json b/demos/plugins-demo/package.json index 628974ceee..aab4f1aec1 100644 --- a/demos/plugins-demo/package.json +++ b/demos/plugins-demo/package.json @@ -21,7 +21,6 @@ "@tanstack/react-query": "catalog:", "@tanstack/react-router": "catalog:", "astro": "catalog:", - "better-sqlite3": "catalog:", "emdash": "workspace:*", "react": "catalog:", "react-dom": "catalog:" diff --git a/demos/simple/package.json b/demos/simple/package.json index 38c335bfaf..606afb58f4 100644 --- a/demos/simple/package.json +++ b/demos/simple/package.json @@ -22,7 +22,6 @@ "@emdash-cms/plugin-color": "workspace:*", "@emdash-cms/plugin-cli": "workspace:*", "astro": "catalog:", - "better-sqlite3": "catalog:", "emdash": "workspace:*", "react": "catalog:", "react-dom": "catalog:" diff --git a/e2e/fixture/package.json b/e2e/fixture/package.json index 8cffc3c339..4d01babf96 100644 --- a/e2e/fixture/package.json +++ b/e2e/fixture/package.json @@ -8,7 +8,6 @@ "@emdash-cms/auth": "workspace:*", "@emdash-cms/plugin-color": "workspace:*", "astro": "catalog:", - "better-sqlite3": "catalog:", "emdash": "workspace:*", "react": "catalog:", "react-dom": "catalog:" diff --git a/fixtures/perf-site/package.json b/fixtures/perf-site/package.json index 3f3d80472c..019fc78de2 100644 --- a/fixtures/perf-site/package.json +++ b/fixtures/perf-site/package.json @@ -19,7 +19,6 @@ "@astrojs/react": "catalog:", "@emdash-cms/cloudflare": "workspace:*", "astro": "catalog:", - "better-sqlite3": "catalog:", "emdash": "workspace:*", "kysely": "^0.29.0", "react": "catalog:", diff --git a/packages/core/package.json b/packages/core/package.json index 3af653739b..5c5ee9925a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -232,7 +232,6 @@ "@unpic/placeholder": "^0.1.2", "arctic": "^3.7.0", "astro-portabletext": "^0.11.0", - "better-sqlite3": "catalog:", "blurhash": "^2.0.5", "citty": "^0.1.6", "consola": "^3.4.2", @@ -272,6 +271,7 @@ "@arethetypeswrong/cli": "catalog:", "@emdash-cms/blocks": "workspace:*", "@types/better-sqlite3": "^7.6.12", + "better-sqlite3": "catalog:", "@types/pg": "^8.16.0", "@types/react": "catalog:", "@types/sanitize-html": "^2.16.0", @@ -297,5 +297,8 @@ "wordpress" ], "author": "Matt Kane", - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=22.15" + } } diff --git a/packages/core/src/astro/integration/vite-config.ts b/packages/core/src/astro/integration/vite-config.ts index 324698ffba..8f953d63fb 100644 --- a/packages/core/src/astro/integration/vite-config.ts +++ b/packages/core/src/astro/integration/vite-config.ts @@ -336,13 +336,7 @@ export function createVirtualModulesPlugin( // `?url`), so both forms resolve to dist rather than the source alias. const ADMIN_STYLES_ALIAS = /^@emdash-cms\/admin\/styles\.css/; -const NODE_NATIVE_EXTERNALS = [ - "better-sqlite3", - "bindings", - "file-uri-to-path", - "@libsql/kysely-libsql", - "pg", -]; +const NODE_NATIVE_EXTERNALS = ["@libsql/kysely-libsql", "pg"]; /** * Detect whether the Cloudflare adapter is being used. diff --git a/packages/core/src/database/connection.ts b/packages/core/src/database/connection.ts index cf83155c83..9d279bc6a3 100644 --- a/packages/core/src/database/connection.ts +++ b/packages/core/src/database/connection.ts @@ -1,6 +1,6 @@ -import BetterSqlite3 from "better-sqlite3"; import { Kysely, SqliteDialect } from "kysely"; +import { openNodeSqliteDatabase } from "../db/node-sqlite-compat.js"; import { EmDashDatabaseError } from "./errors.js"; import { kyselyLogOption } from "./instrumentation.js"; import type { Database } from "./types.js"; @@ -12,23 +12,6 @@ export interface DatabaseConfig { authToken?: string; } -/** - * Returns a helpful, actionable message when better-sqlite3's native binary - * was compiled against a different Node.js version than the one running. This - * happens after upgrading Node without rebuilding native deps. - * - * Returns null if the error is not a NODE_MODULE_VERSION mismatch. - */ -export function formatNativeModuleVersionError(error: unknown): string | null { - const message = error instanceof Error ? error.message : String(error); - if (!message.includes("NODE_MODULE_VERSION")) return null; - return ( - "better-sqlite3's native binary was compiled against a different Node.js version. " + - "Rebuild it with `pnpm rebuild better-sqlite3` (or `npm rebuild better-sqlite3`), " + - "or reinstall dependencies with your current Node.js version." - ); -} - /** * Creates a Kysely database instance * Supports: @@ -42,15 +25,9 @@ export function createDatabase(config: DatabaseConfig): Kysely { if (config.url.startsWith("file:") || config.url === ":memory:") { const dbPath = config.url === ":memory:" ? ":memory:" : config.url.replace("file:", ""); - const sqlite = new BetterSqlite3(dbPath); - - // Enable WAL mode for crash safety — writes go to a write-ahead log - // before being applied, preventing FTS5 shadow table corruption on - // process kill during content writes. No-op for :memory: databases. - sqlite.pragma("journal_mode = WAL"); - - // Enable foreign key constraints - sqlite.pragma("foreign_keys = ON"); + // Connection pragmas (WAL, busy_timeout, foreign_keys) are applied by + // openNodeSqliteDatabase so every SQLite entry point gets them. + const sqlite = openNodeSqliteDatabase(dbPath); const dialect = new SqliteDialect({ database: sqlite, @@ -73,10 +50,6 @@ export function createDatabase(config: DatabaseConfig): Kysely { if (error instanceof EmDashDatabaseError) { throw error; } - const nativeVersionHint = formatNativeModuleVersionError(error); - if (nativeVersionHint) { - throw new EmDashDatabaseError(nativeVersionHint, error); - } throw new EmDashDatabaseError("Failed to create database", error); } } diff --git a/packages/core/src/database/index.ts b/packages/core/src/database/index.ts index f4967537c0..e42f0fbb4e 100644 --- a/packages/core/src/database/index.ts +++ b/packages/core/src/database/index.ts @@ -1,5 +1,6 @@ // `createDatabase` is intentionally not re-exported here: it lives in -// `connection.ts`, which statically imports `better-sqlite3`. See #947. +// `connection.ts`, which statically imports `node:sqlite` — a Node-only +// builtin that must not load in non-Node runtimes (e.g. workerd). See #947. export { EmDashDatabaseError } from "./errors.js"; export type { DatabaseConfig } from "./connection.js"; export { runMigrations, getMigrationStatus, rollbackMigration } from "./migrations/runner.js"; diff --git a/packages/core/src/db/adapters.ts b/packages/core/src/db/adapters.ts index a4ea100ab8..be1de7b42f 100644 --- a/packages/core/src/db/adapters.ts +++ b/packages/core/src/db/adapters.ts @@ -78,9 +78,10 @@ export interface LibsqlConfig { } /** - * SQLite database adapter (better-sqlite3) + * SQLite database adapter (node:sqlite) * - * For local development and Node.js deployments. + * For local development and Node.js deployments. Uses the Node.js built-in + * SQLite driver — no native compiled dependency. Requires Node >= 22.15. * * @example * ```ts diff --git a/packages/core/src/db/node-sqlite-compat.ts b/packages/core/src/db/node-sqlite-compat.ts new file mode 100644 index 0000000000..5bb09b39c5 --- /dev/null +++ b/packages/core/src/db/node-sqlite-compat.ts @@ -0,0 +1,120 @@ +/** + * better-sqlite3-compatible wrapper around node:sqlite. + * + * Kysely's built-in SqliteDialect drives the database through better-sqlite3's + * surface: `prepare()`, statement `.reader` / `.all(params)` / `.run(params)` / + * `.iterate(params)`, and `close()`. node:sqlite's DatabaseSync supports the + * same operations but binds parameters as spread arguments and has no + * `.reader` flag. This wrapper bridges the two so the dialect needs no native + * compiled dependency. + * + * `.reader` is derived from `StatementSync.columns()` (column count > 0), + * which — like better-sqlite3's flag — is true for any statement that returns + * rows, including `INSERT ... RETURNING`. + * + * Requires Node >= 22.15 (node:sqlite unflagged + StatementSync.columns()). + */ + +import { DatabaseSync } from "node:sqlite"; + +/** + * The subset of better-sqlite3's Database API that emdash consumes: what + * Kysely's SqliteDialect calls, plus `exec`/`pragma` used at connection setup. + */ +export interface NodeSqliteCompatDatabase { + close(): void; + prepare(sql: string): NodeSqliteCompatStatement; + exec(sql: string): void; + pragma(pragma: string): void; +} + +export interface NodeSqliteCompatStatement { + readonly reader: boolean; + all(parameters: ReadonlyArray): unknown[]; + run(parameters: ReadonlyArray): { + changes: number | bigint; + lastInsertRowid: number | bigint; + }; + iterate(parameters: ReadonlyArray): IterableIterator; +} + +/** + * Open a SQLite database via node:sqlite, exposed through the + * better-sqlite3-compatible surface above. Pass the result directly to + * Kysely's `new SqliteDialect({ database })`. + */ +export function openNodeSqliteDatabase(path: string): NodeSqliteCompatDatabase { + const db = new DatabaseSync(path); + + // Connection defaults, applied here because this is now the single place the + // package opens a SQLite database — previously only the CLI path + // (database/connection.ts) set them, so sites running through the runtime + // adapter (db/sqlite.ts) silently got neither. + // + // WAL: readers don't block on the writer, and writes land in a log before + // being applied — this is what prevents FTS5 shadow-table corruption if the + // process is killed mid-write. No-op for `:memory:`. + db.exec("PRAGMA journal_mode = WAL"); + // Wait for a competing writer instead of failing the query outright; without + // it a concurrent write (backup, second process) surfaces as SQLITE_BUSY. + db.exec("PRAGMA busy_timeout = 5000"); + // Referential integrity is off by default in SQLite; the schema declares + // foreign keys, so enforce them. + db.exec("PRAGMA foreign_keys = ON"); + + return { + close: () => db.close(), + exec: (sql) => db.exec(sql), + pragma: (pragma) => db.exec(`PRAGMA ${pragma}`), + prepare(sql) { + const stmt = db.prepare(sql); + return { + reader: stmt.columns().length > 0, + all: (parameters) => stmt.all(...toBindings(parameters)), + run: (parameters) => stmt.run(...toBindings(parameters)), + iterate: (parameters) => stmt.iterate(...toBindings(parameters)), + }; + }, + }; +} + +/** Parameter type accepted by node:sqlite statement bindings. */ +type SQLInputValue = null | number | bigint | string | Uint8Array; + +/** + * Normalize Kysely's compiled parameters to what node:sqlite accepts. + * + * node:sqlite binds a narrower set of JS types than better-sqlite3 did, and + * differs in both directions, so the values are mapped rather than cast: + * + * - `boolean` — rejected by BOTH drivers (better-sqlite3: "SQLite3 can only + * bind numbers, strings, bigints, buffers, and null"). SQLite has no boolean + * type and stores them as 0/1, and `json_extract` yields 0/1 for JSON + * booleans, so mapping here makes boolean filters work rather than throw — + * e.g. the plugin storage query API, whose `WhereValue` advertises `boolean` + * (see plugins/types.ts) but which threw at bind time on either driver. + * - `undefined` — better-sqlite3 bound it as NULL; node:sqlite throws. Kysely + * passes it straight through (e.g. `.where(col, "=", undefined)` from an + * unset optional filter), so mapping to null preserves the old behaviour + * instead of turning a previously-working query into a runtime TypeError. + * - `Date` — better-sqlite3 threw; node:sqlite silently binds NULL, which + * would turn a loud programming error into silent data loss. Rethrow with + * an actionable message: emdash stores timestamps as ISO strings. + * + * Everything else (number, bigint, string, Uint8Array/Buffer, null) binds + * identically on both drivers and is passed through untouched; any remaining + * unsupported type is still rejected by node:sqlite at bind time. + */ +function toBindings(parameters: ReadonlyArray): SQLInputValue[] { + return parameters.map((value) => { + if (typeof value === "boolean") return value ? 1 : 0; + if (value === undefined) return null; + if (value instanceof Date) { + throw new TypeError( + "Cannot bind a Date to a SQLite parameter; convert it to an ISO string first (e.g. date.toISOString()).", + ); + } + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Kysely hands through the values it compiled into the query; node:sqlite rejects any remaining unsupported type at bind time, same as better-sqlite3 did + return value as SQLInputValue; + }); +} diff --git a/packages/core/src/db/sqlite.ts b/packages/core/src/db/sqlite.ts index b00853ac7b..39ade00858 100644 --- a/packages/core/src/db/sqlite.ts +++ b/packages/core/src/db/sqlite.ts @@ -1,14 +1,15 @@ /** * SQLite runtime adapter * - * Creates a Kysely dialect for better-sqlite3. + * Creates a Kysely dialect for node:sqlite (via a better-sqlite3-compatible + * wrapper — see node-sqlite-compat.ts). No native compiled dependency. * Loaded at runtime via virtual module. */ -import BetterSqlite3 from "better-sqlite3"; import { type Dialect, SqliteDialect } from "kysely"; import type { SqliteConfig } from "./adapters.js"; +import { openNodeSqliteDatabase } from "./node-sqlite-compat.js"; /** * Create a SQLite dialect from config @@ -18,7 +19,7 @@ export function createDialect(config: SqliteConfig): Dialect { const url = config.url; const filePath = url.startsWith("file:") ? url.slice(5) : url; - const database = new BetterSqlite3(filePath); + const database = openNodeSqliteDatabase(filePath); return new SqliteDialect({ database }); } diff --git a/packages/core/tests/database/connection.test.ts b/packages/core/tests/database/connection.test.ts index c915c33d92..646a661ea1 100644 --- a/packages/core/tests/database/connection.test.ts +++ b/packages/core/tests/database/connection.test.ts @@ -3,11 +3,7 @@ import { unlinkSync } from "node:fs"; import type { Kysely } from "kysely"; import { describe, it, expect, afterEach } from "vitest"; -import { - createDatabase, - EmDashDatabaseError, - formatNativeModuleVersionError, -} from "../../src/database/connection.js"; +import { createDatabase, EmDashDatabaseError } from "../../src/database/connection.js"; import type { Database } from "../../src/database/types.js"; describe("createDatabase", () => { @@ -142,23 +138,6 @@ describe("createDatabase", () => { }); }); - describe("formatNativeModuleVersionError", () => { - it("returns an actionable message for NODE_MODULE_VERSION mismatch", () => { - const err = new Error( - "The module '/path/better_sqlite3.node' was compiled against a different Node.js version using NODE_MODULE_VERSION 115. This version of Node.js requires NODE_MODULE_VERSION 127.", - ); - const message = formatNativeModuleVersionError(err); - expect(message).not.toBeNull(); - expect(message).toContain("better-sqlite3"); - expect(message).toMatch(/rebuild/i); - }); - - it("returns null for unrelated errors", () => { - expect(formatNativeModuleVersionError(new Error("disk full"))).toBeNull(); - expect(formatNativeModuleVersionError("some string")).toBeNull(); - }); - }); - describe("connection lifecycle", () => { it("should allow closing connection with destroy()", async () => { db = createDatabase({ url: ":memory:" }); diff --git a/packages/core/tests/integration/fixture/package.json b/packages/core/tests/integration/fixture/package.json index 178a6d8142..a5a55cde3f 100644 --- a/packages/core/tests/integration/fixture/package.json +++ b/packages/core/tests/integration/fixture/package.json @@ -8,7 +8,6 @@ "@emdash-cms/auth": "workspace:*", "@emdash-cms/plugin-color": "workspace:*", "astro": "catalog:", - "better-sqlite3": "^11.10.0", "emdash": "workspace:*", "react": "^19.1.0", "react-dom": "^19.1.0" diff --git a/packages/core/tests/unit/db/node-sqlite-compat.test.ts b/packages/core/tests/unit/db/node-sqlite-compat.test.ts new file mode 100644 index 0000000000..33a038d805 --- /dev/null +++ b/packages/core/tests/unit/db/node-sqlite-compat.test.ts @@ -0,0 +1,139 @@ +import { Kysely, SqliteDialect } from "kysely"; +import { describe, expect, it } from "vitest"; + +import { openNodeSqliteDatabase } from "../../../src/db/node-sqlite-compat.js"; + +describe("openNodeSqliteDatabase", () => { + it("exposes exec and pragma", () => { + const db = openNodeSqliteDatabase(":memory:"); + db.pragma("foreign_keys = ON"); + db.exec("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)"); + const stmt = db.prepare("SELECT name FROM t"); + expect(stmt.all([])).toEqual([]); + db.close(); + }); + + it("marks row-returning statements as reader, including RETURNING", () => { + const db = openNodeSqliteDatabase(":memory:"); + db.exec("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)"); + + expect(db.prepare("SELECT * FROM t").reader).toBe(true); + expect(db.prepare("INSERT INTO t (name) VALUES (?)").reader).toBe(false); + expect(db.prepare("INSERT INTO t (name) VALUES (?) RETURNING id").reader).toBe(true); + expect(db.prepare("UPDATE t SET name = ?").reader).toBe(false); + db.close(); + }); + + it("binds array parameters for all/run and reports changes and lastInsertRowid", () => { + const db = openNodeSqliteDatabase(":memory:"); + db.exec("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)"); + + const insert = db.prepare("INSERT INTO t (name) VALUES (?)"); + const first = insert.run(["alpha"]); + expect(Number(first.changes)).toBe(1); + expect(Number(first.lastInsertRowid)).toBe(1); + + insert.run(["beta"]); + const rows = db.prepare("SELECT name FROM t WHERE name = ?").all(["beta"]); + expect(rows).toEqual([expect.objectContaining({ name: "beta" })]); + db.close(); + }); + + it("applies connection pragmas on open", () => { + const db = openNodeSqliteDatabase(":memory:"); + // foreign_keys is off by default in SQLite; the wrapper turns it on so + // every entry point (CLI + runtime adapter) enforces the schema's FKs. + expect(db.prepare("PRAGMA foreign_keys").all([])).toEqual([ + expect.objectContaining({ foreign_keys: 1 }), + ]); + expect(db.prepare("PRAGMA busy_timeout").all([])).toEqual([ + expect.objectContaining({ timeout: 5000 }), + ]); + db.close(); + }); + + // node:sqlite binds a narrower set of JS types than better-sqlite3 did, and + // differs in both directions. toBindings normalizes the gaps; these lock in + // that contract. + describe("parameter binding compatibility", () => { + it("maps booleans to 0/1 instead of throwing", () => { + const db = openNodeSqliteDatabase(":memory:"); + db.exec("CREATE TABLE t (v)"); + db.prepare("INSERT INTO t (v) VALUES (?)").run([true]); + db.prepare("INSERT INTO t (v) VALUES (?)").run([false]); + expect(db.prepare("SELECT v FROM t ORDER BY rowid").all([])).toEqual([ + expect.objectContaining({ v: 1 }), + expect.objectContaining({ v: 0 }), + ]); + db.close(); + }); + + it("binds undefined as NULL, matching better-sqlite3", () => { + const db = openNodeSqliteDatabase(":memory:"); + db.exec("CREATE TABLE t (v)"); + db.prepare("INSERT INTO t (v) VALUES (?)").run([undefined]); + expect(db.prepare("SELECT v FROM t").all([])).toEqual([ + expect.objectContaining({ v: null }), + ]); + db.close(); + }); + + it("rejects Date rather than silently storing NULL", () => { + const db = openNodeSqliteDatabase(":memory:"); + db.exec("CREATE TABLE t (v)"); + expect(() => db.prepare("INSERT INTO t (v) VALUES (?)").run([new Date()])).toThrow( + /Cannot bind a Date/, + ); + db.close(); + }); + + it("passes through the types both drivers accept", () => { + const db = openNodeSqliteDatabase(":memory:"); + db.exec("CREATE TABLE t (v)"); + const insert = db.prepare("INSERT INTO t (v) VALUES (?)"); + insert.run([null]); + insert.run([42]); + insert.run(["text"]); + insert.run([7n]); + insert.run([new Uint8Array([1, 2])]); + const rows = db.prepare("SELECT v FROM t ORDER BY rowid").all([]); + expect(rows).toHaveLength(5); + db.close(); + }); + }); + + it("works end-to-end through Kysely's SqliteDialect", async () => { + const db = new Kysely<{ t: { id: number | null; name: string } }>({ + dialect: new SqliteDialect({ database: openNodeSqliteDatabase(":memory:") }), + }); + + await db.schema + .createTable("t") + .addColumn("id", "integer", (col) => col.primaryKey().autoIncrement()) + .addColumn("name", "text") + .execute(); + + const inserted = await db + .insertInto("t") + .values({ name: "alpha" }) + .returning("id") + .executeTakeFirstOrThrow(); + expect(inserted.id).toBe(1); + + const row = await db + .selectFrom("t") + .select(["id", "name"]) + .where("name", "=", "alpha") + .executeTakeFirstOrThrow(); + expect(row).toEqual({ id: 1, name: "alpha" }); + + const updated = await db + .updateTable("t") + .set({ name: "beta" }) + .where("id", "=", 1) + .executeTakeFirst(); + expect(Number(updated.numUpdatedRows)).toBe(1); + + await db.destroy(); + }); +}); diff --git a/packages/core/tests/utils/test-db.ts b/packages/core/tests/utils/test-db.ts index 12c101fbef..7ebbc3f1a7 100644 --- a/packages/core/tests/utils/test-db.ts +++ b/packages/core/tests/utils/test-db.ts @@ -1,6 +1,5 @@ import { randomUUID } from "node:crypto"; -import Database from "better-sqlite3"; import { Kysely, SqliteDialect } from "kysely"; import { Pool } from "pg"; import { describe } from "vitest"; @@ -9,6 +8,7 @@ import { getMigrationStatus, runMigrations } from "../../src/database/migrations import type { MigrationStatus } from "../../src/database/migrations/runner.js"; import { FailFastPostgresDialect } from "../../src/database/pg-migration-lock.js"; import type { Database as DatabaseSchema } from "../../src/database/types.js"; +import { openNodeSqliteDatabase } from "../../src/db/node-sqlite-compat.js"; import { SchemaRegistry } from "../../src/schema/registry.js"; import { resetTaxonomyDefsCacheForTests } from "../../src/taxonomies/index.js"; @@ -51,7 +51,9 @@ export const hasPgTestDatabase = PG_CONNECTION_STRING.length > 0; */ export function createTestDatabase(): Kysely { resetSchemaCachesForTests(); - const sqlite = new Database(":memory:"); + // Create test databases through the same wrapper the package ships, so the + // suite exercises the production node:sqlite driver rather than a dev-only one. + const sqlite = openNodeSqliteDatabase(":memory:"); return new Kysely({ dialect: new SqliteDialect({ diff --git a/packages/core/tsdown.config.ts b/packages/core/tsdown.config.ts index 9605a82cee..18c9642a8d 100644 --- a/packages/core/tsdown.config.ts +++ b/packages/core/tsdown.config.ts @@ -155,10 +155,6 @@ export default defineConfig({ }, // Externalize native modules, dialect-specific packages, and internal shared modules external: [ - // Native modules that use __filename - "better-sqlite3", - "bindings", - "file-uri-to-path", // Dialect-specific packages "@libsql/kysely-libsql", "pg", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 50967cddea..6e25d2efc7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -533,9 +533,6 @@ importers: astro: specifier: 'catalog:' version: 7.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.13)(jiti@2.7.0)(rollup@4.55.2)(yaml@2.9.0) - better-sqlite3: - specifier: 'catalog:' - version: 12.8.0 emdash: specifier: workspace:* version: link:../../packages/core @@ -653,9 +650,6 @@ importers: astro: specifier: 'catalog:' version: 7.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@25.9.1)(jiti@2.7.0)(rollup@4.55.2)(yaml@2.9.0) - better-sqlite3: - specifier: 'catalog:' - version: 12.8.0 emdash: specifier: workspace:* version: link:../../packages/core @@ -723,9 +717,6 @@ importers: astro: specifier: 'catalog:' version: 7.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@25.9.1)(jiti@2.7.0)(rollup@4.55.2)(yaml@2.9.0) - better-sqlite3: - specifier: 'catalog:' - version: 12.8.0 emdash: specifier: workspace:* version: link:../../packages/core @@ -790,9 +781,6 @@ importers: astro: specifier: 'catalog:' version: 7.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@25.9.1)(jiti@2.7.0)(rollup@4.55.2)(yaml@2.9.0) - better-sqlite3: - specifier: 'catalog:' - version: 12.8.0 emdash: specifier: workspace:* version: link:../../packages/core @@ -1668,9 +1656,6 @@ importers: astro-portabletext: specifier: ^0.11.0 version: 0.11.4(astro@7.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@25.9.1)(jiti@2.7.0)(rollup@4.55.2)(yaml@2.9.0)) - better-sqlite3: - specifier: 'catalog:' - version: 12.8.0 blurhash: specifier: ^2.0.5 version: 2.0.5 @@ -1753,6 +1738,9 @@ importers: '@vitest/ui': specifier: ^4.1.9 version: 4.1.9(vitest@4.1.5) + better-sqlite3: + specifier: 'catalog:' + version: 12.8.0 publint: specifier: 'catalog:' version: 0.3.17 @@ -2363,9 +2351,6 @@ importers: astro: specifier: 'catalog:' version: 7.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@25.9.1)(jiti@2.7.0)(rollup@4.55.2)(yaml@2.9.0) - better-sqlite3: - specifier: 'catalog:' - version: 12.8.0 emdash: specifier: workspace:* version: link:../../packages/core @@ -2394,9 +2379,6 @@ importers: astro: specifier: 'catalog:' version: 7.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@25.9.1)(jiti@2.7.0)(rollup@4.55.2)(yaml@2.9.0) - better-sqlite3: - specifier: 'catalog:' - version: 12.8.0 emdash: specifier: workspace:* version: link:../../packages/core @@ -2468,9 +2450,6 @@ importers: astro-iconset: specifier: 'catalog:' version: 0.0.4(astro@7.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@25.9.1)(jiti@2.7.0)(rollup@4.55.2)(yaml@2.9.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - better-sqlite3: - specifier: 'catalog:' - version: 12.8.0 emdash: specifier: workspace:* version: link:../../packages/core @@ -2536,9 +2515,6 @@ importers: astro: specifier: 'catalog:' version: 7.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@25.9.1)(jiti@2.7.0)(rollup@4.55.2)(yaml@2.9.0) - better-sqlite3: - specifier: 'catalog:' - version: 12.8.0 emdash: specifier: workspace:* version: link:../../packages/core @@ -2598,9 +2574,6 @@ importers: astro: specifier: 'catalog:' version: 7.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@25.9.1)(jiti@2.7.0)(rollup@4.55.2)(yaml@2.9.0) - better-sqlite3: - specifier: 'catalog:' - version: 12.8.0 emdash: specifier: workspace:* version: link:../../packages/core diff --git a/templates/blank/package.json b/templates/blank/package.json index 2871e1685b..716473ec31 100644 --- a/templates/blank/package.json +++ b/templates/blank/package.json @@ -14,7 +14,6 @@ "@astrojs/node": "catalog:", "@astrojs/react": "catalog:", "astro": "catalog:", - "better-sqlite3": "catalog:", "emdash": "workspace:*", "react": "catalog:", "react-dom": "catalog:" diff --git a/templates/blog/package.json b/templates/blog/package.json index 8074abd53d..03ef9c71b0 100644 --- a/templates/blog/package.json +++ b/templates/blog/package.json @@ -18,7 +18,6 @@ "@astrojs/react": "catalog:", "@emdash-cms/plugin-audit-log": "workspace:*", "astro": "catalog:", - "better-sqlite3": "catalog:", "emdash": "workspace:*", "react": "catalog:", "react-dom": "catalog:" diff --git a/templates/marketing/package.json b/templates/marketing/package.json index d513bf3bee..103c97fcef 100644 --- a/templates/marketing/package.json +++ b/templates/marketing/package.json @@ -19,7 +19,6 @@ "@iconify-json/ph": "catalog:", "astro": "catalog:", "astro-iconset": "catalog:", - "better-sqlite3": "catalog:", "emdash": "workspace:*", "react": "catalog:", "react-dom": "catalog:" diff --git a/templates/portfolio/package.json b/templates/portfolio/package.json index 0b723e4c68..80ca4fb730 100644 --- a/templates/portfolio/package.json +++ b/templates/portfolio/package.json @@ -17,7 +17,6 @@ "@astrojs/node": "catalog:", "@astrojs/react": "catalog:", "astro": "catalog:", - "better-sqlite3": "catalog:", "emdash": "workspace:*", "react": "catalog:", "react-dom": "catalog:" diff --git a/templates/starter/package.json b/templates/starter/package.json index 88b4d91e76..22f8a3e37b 100644 --- a/templates/starter/package.json +++ b/templates/starter/package.json @@ -17,7 +17,6 @@ "@astrojs/node": "catalog:", "@astrojs/react": "catalog:", "astro": "catalog:", - "better-sqlite3": "catalog:", "emdash": "workspace:*", "react": "catalog:", "react-dom": "catalog:"