Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 13 additions & 4 deletions src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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
Expand Down Expand Up @@ -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. ' +
Expand Down Expand Up @@ -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);
}
51 changes: 51 additions & 0 deletions tests/runtime.test.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
});
});