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
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:

- uses: actions/setup-node@v4
with:
node-version: "20"
node-version: "22"

- uses: oven-sh/setup-bun@v2
with:
Expand Down
13 changes: 5 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@wgtechlabs/config-engine",
"version": "0.1.0",
"description": "Configs should be easy. Fast, Bun-first configuration SDK for AI agent frameworks, CLI apps, and applications.",
"description": "Configs should be easy. Node.js configuration SDK with SQLite-backed storage for AI agent frameworks, CLI apps, and applications.",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand Down Expand Up @@ -37,8 +37,8 @@
"settings",
"preferences",
"sqlite",
"bun",
"fast",
"node",
"nodejs",
"typed",
"zod",
"ai",
Expand All @@ -57,24 +57,21 @@
},
"homepage": "https://github.com/wgtechlabs/config-engine#readme",
"engines": {
"node": ">=20"
"node": ">=22.0.0"
},
"dependencies": {
"zod": "^3.24.2"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@types/bun": "^1.2.4",
"@types/better-sqlite3": "^9.6.0",
"typescript": "^5.7.3"
},
"peerDependencies": {
"better-sqlite3": ">=11.0.0",
"@wgtechlabs/secrets-engine": ">=1.0.0"
},
"peerDependenciesMeta": {
"better-sqlite3": {
"optional": true
},
"@wgtechlabs/secrets-engine": {
"optional": true
}
Expand Down
1 change: 0 additions & 1 deletion scripts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ const result = await Bun.build({
splitting: true,
sourcemap: "external",
external: [
"bun:sqlite",
"better-sqlite3",
"@wgtechlabs/secrets-engine",
"zod",
Expand Down
2 changes: 1 addition & 1 deletion src/encryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export class SecretsEngineEncryptor implements Encryptor {
} catch {
throw new Error(
'Encryption requires "@wgtechlabs/secrets-engine" as a peer dependency. ' +
"Install it with: bun add @wgtechlabs/secrets-engine",
"Install it with: npm install @wgtechlabs/secrets-engine",
);
}

Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/**
* @module config-engine
* Fast, Bun-first configuration engine with SQLite-backed storage.
* Node.js configuration engine with SQLite-backed storage.
* Bun is used as the build and test toolchain; Node.js is the target runtime.
*
* @example
* ```ts
Expand Down
75 changes: 5 additions & 70 deletions src/runtime.ts
Original file line number Diff line number Diff line change
@@ -1,86 +1,26 @@
/**
* @module runtime
* Runtime detection and SQLite adapter layer.
* Uses `bun:sqlite` on Bun, `better-sqlite3` on Node.js.
* SQLite adapter layer using `better-sqlite3`.
* Bun is supported as a toolchain (build/test) but Node.js is the target runtime.
*/

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";
}

/**
* Open a SQLite database. Automatically selects the driver:
* - Bun → `bun:sqlite` (built-in, zero deps)
* - Node → `better-sqlite3` (peer dependency)
* Open a SQLite database using `better-sqlite3`.
* Requires `better-sqlite3` as a peer dependency.
*/
export function openDatabase(filepath: string): DatabaseAdapter {
if (isBun()) {
return openBunDatabase(filepath);
}
return openNodeDatabase(filepath);
}

// ---------------------------------------------------------------------------
// Bun adapter
// ---------------------------------------------------------------------------

function openBunDatabase(filepath: string): DatabaseAdapter {
// 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 } = runtimeRequire(getBunSqliteSpecifier()) as any;
const db = new Database(filepath);

// Enable WAL for concurrent read performance
db.exec("PRAGMA journal_mode = WAL;");
db.exec("PRAGMA busy_timeout = 5000;");

return {
prepare(sql: string): StatementAdapter {
const stmt = db.prepare(sql);
return {
run(...params: unknown[]) {
stmt.run(...params);
},
get(...params: unknown[]): unknown {
return stmt.get(...params);
},
all(...params: unknown[]): unknown[] {
return stmt.all(...params);
},
};
},
exec(sql: string) {
db.exec(sql);
},
close() {
db.close();
},
transaction<T>(fn: () => T): () => T {
return db.transaction(fn);
},
};
}

// ---------------------------------------------------------------------------
// Node adapter (better-sqlite3)
// ---------------------------------------------------------------------------

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: 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. ' +
'config-engine requires "better-sqlite3" as a peer dependency. ' +
"Install it with: npm install better-sqlite3",
);
}
Expand Down Expand Up @@ -117,8 +57,3 @@ 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);
}
4 changes: 2 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import type { ZodSchema } from "zod";
// ---------------------------------------------------------------------------

/**
* Minimal interface that both `bun:sqlite` and `better-sqlite3` satisfy.
* We program against this so the rest of the codebase is runtime-agnostic.
* Minimal interface that `better-sqlite3` satisfies.
* We program against this so the rest of the codebase stays decoupled from the driver.
*/
export interface DatabaseAdapter {
prepare(sql: string): StatementAdapter;
Expand Down
21 changes: 7 additions & 14 deletions tests/runtime.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Tests for the runtime SQLite adapter selection.
* Tests for the runtime SQLite adapter.
*/

import { describe, expect, test } from "bun:test";
Expand All @@ -9,7 +9,7 @@ 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 () => {
test("node-targeted bundle externalises better-sqlite3 and has no bun-specific imports", async () => {
const workDir = mkdtempSync(join(tmpdir(), "config-engine-runtime-"));
const entryPath = join(workDir, "entry.ts");
const outDir = join(workDir, "dist");
Expand All @@ -22,14 +22,9 @@ describe("runtime bundling", () => {
// The bundle is emitted as ESM, so mark the output directory as a module
// package, otherwise Node parses the .js output as CommonJS and fails.
writeFileSync(join(outDir, "package.json"), `${JSON.stringify({ type: "module" })}\n`);
// Actually reference openDatabase (not just import it) so the bundler cannot
// tree-shake the SQLite adapter code, so the bun:sqlite guard is really
// exercised by the bundle.
writeFileSync(
entryPath,
`import { isBun, openDatabase } from ${JSON.stringify(runtimePath)};\n` +
"console.log(isBun());\n" +
"console.log(typeof openDatabase);\n",
`import { openDatabase } from ${JSON.stringify(runtimePath)};\nconsole.log(typeof openDatabase);\n`,
);

const result = await Bun.build({
Expand All @@ -45,20 +40,18 @@ describe("runtime bundling", () => {

const outputPath = join(outDir, "entry.js");
const bundled = readFileSync(outputPath, "utf8");
// Allow for minified output: `from"bun:sqlite"`, `import"bun:sqlite"`,
// and `require("bun:sqlite")` must all be absent.
// No bun-specific imports should appear in a node-targeted bundle
expect(bundled).not.toMatch(/(?:from|import|require\s*\()\s*["']bun:sqlite["']/);
// better-sqlite3 must remain external (not inlined)
expect(bundled).toMatch(/better-sqlite3/);
Comment thread
warengonzaga marked this conversation as resolved.
Outdated

const run = Bun.spawnSync(["node", outputPath], {
stdout: "pipe",
stderr: "pipe",
});

expect(run.exitCode).toBe(0);
expect(new TextDecoder().decode(run.stdout).trim().split(/\r?\n/)).toEqual([
"false",
"function",
]);
expect(new TextDecoder().decode(run.stdout).trim()).toBe("function");
} finally {
rmSync(workDir, { recursive: true, force: true });
}
Expand Down
3 changes: 1 addition & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@
"noUnusedLocals": false,
"noUnusedParameters": false,
"exactOptionalPropertyTypes": false,
"noFallthroughCasesInSwitch": true,
"types": ["bun"]
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist", "tests"]
Expand Down
Loading