diff --git a/README.md b/README.md index 7683cc5..a36177a 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,9 @@ For Node.js, also install the SQLite driver: npm install @wgtechlabs/config-engine better-sqlite3 ``` +The Bun SQLite backend is loaded only at runtime on Bun, so Node-targeted +bundles can safely consume the package without pulling in a static `bun:*` import. + ## Quick Start ```typescript diff --git a/src/runtime.ts b/src/runtime.ts index 8f1ba8d..4e6b810 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -4,8 +4,11 @@ * Uses `bun:sqlite` on Bun, `better-sqlite3` on Node.js. */ +import { createRequire } from "node:module"; import type { DatabaseAdapter, StatementAdapter } from "./types.js"; +const runtimeRequire = createRequire(import.meta.url); + /** Returns `true` when running under Bun. */ export function isBun(): boolean { return typeof globalThis.Bun !== "undefined"; @@ -28,9 +31,10 @@ export function openDatabase(filepath: string): DatabaseAdapter { // --------------------------------------------------------------------------- function openBunDatabase(filepath: string): DatabaseAdapter { - // Using dynamic import syntax to avoid static analysis issues on Node + // Construct the module specifier at runtime so Node-targeted bundlers + // don't preserve a static bun:* import in published CLI artifacts. // biome-ignore lint/suspicious/noExplicitAny: bun:sqlite types vary - const { Database } = require("bun:sqlite") as any; + const { Database } = runtimeRequire(getBunSqliteSpecifier()) as any; const db = new Database(filepath); // Enable WAL for concurrent read performance @@ -72,8 +76,8 @@ function openNodeDatabase(filepath: string): DatabaseAdapter { // better-sqlite3 is a peer dep — error if missing let BetterSqlite3: typeof import("better-sqlite3"); try { - // biome-ignore lint/suspicious/noExplicitAny: dynamic require - BetterSqlite3 = require("better-sqlite3") as any; + // biome-ignore lint/suspicious/noExplicitAny: runtime require for ESM output + BetterSqlite3 = runtimeRequire("better-sqlite3") as any; } catch { throw new Error( 'config-engine requires "better-sqlite3" as a peer dependency when running on Node.js. ' + @@ -113,3 +117,8 @@ function openNodeDatabase(filepath: string): DatabaseAdapter { }, }; } + +function getBunSqliteSpecifier(): string { + // Spells "bun:sqlite" without exposing a static bun:* import to bundlers. + return String.fromCharCode(98, 117, 110, 58, 115, 113, 108, 105, 116, 101); +} diff --git a/tests/runtime.test.ts b/tests/runtime.test.ts new file mode 100644 index 0000000..1c28439 --- /dev/null +++ b/tests/runtime.test.ts @@ -0,0 +1,51 @@ +/** + * Tests for the runtime SQLite adapter selection. + */ + +import { describe, expect, test } from "bun:test"; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +describe("runtime bundling", () => { + test("does not emit a static bun:sqlite import in Node bundles", async () => { + const workDir = mkdtempSync(join(tmpdir(), "config-engine-runtime-")); + const entryPath = join(workDir, "entry.ts"); + const outDir = join(workDir, "dist"); + const runtimePath = fileURLToPath(new URL("../src/runtime.ts", import.meta.url)); + + try { + mkdirSync(outDir, { recursive: true }); + writeFileSync( + entryPath, + `import { isBun } from ${JSON.stringify(runtimePath)};\nconsole.log(isBun());\n`, + ); + + const result = await Bun.build({ + entrypoints: [entryPath], + outdir: outDir, + target: "node", + format: "esm", + splitting: false, + external: ["better-sqlite3"], + }); + + expect(result.success).toBe(true); + + const outputPath = join(outDir, "entry.js"); + const bundled = readFileSync(outputPath, "utf8"); + expect(bundled).not.toMatch(/(?:from|import)\s+["']bun:sqlite["']/); + + const run = Bun.spawnSync(["node", outputPath], { + stdout: "pipe", + stderr: "pipe", + }); + + expect(run.exitCode).toBe(0); + expect(new TextDecoder().decode(run.stdout).trim()).toBe("false"); + } finally { + rmSync(workDir, { recursive: true, force: true }); + } + }); +});