Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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);
}
53 changes: 53 additions & 0 deletions tests/runtime.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* 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";

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");
// Use a file:// URL so the import path is portable across platforms (including Windows).
const runtimeUrl = new URL("../src/runtime.ts", import.meta.url).href;

try {
mkdirSync(outDir, { recursive: true });
// Import openDatabase (not just isBun) so the bundler cannot tree-shake
// the SQLite adapter code β€” this ensures the bun:sqlite guard is actually tested.
writeFileSync(
entryPath,
`import { isBun, openDatabase } from ${JSON.stringify(runtimeUrl)};\nconsole.log(isBun());\n`,
);
Comment thread
warengonzaga marked this conversation as resolved.
Outdated

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",
});
Comment thread
warengonzaga marked this conversation as resolved.

expect(run.exitCode).toBe(0);
expect(new TextDecoder().decode(run.stdout).trim()).toBe("false");
} finally {
rmSync(workDir, { recursive: true, force: true });
}
});
});
Loading