fix(core,cli): make standalone binary actually run

Bun's --compile binary previously crashed at startup because:
  1. node:sqlite isn't implemented in Bun 1.3.8 (require returns
     undefined; import throws "No such built-in module")
  2. ink imports react-devtools-core inside its reconciler; even though
     gated by isDev(), the bundled module path failed to resolve at
     runtime

Fixes:
  - Add packages/core/src/sqlite-adapter.ts: a thin DatabaseSync wrapper
    that picks bun:sqlite under Bun and node:sqlite under Node via
    createRequire (so the bundler doesn't statically pull in either).
    Drop-in for the three core files that import DatabaseSync.
  - Install react-devtools-core as a workspace devDependency so it
    resolves at bundle time. The dev-only code path is still gated by
    DEV=true, so it stays inert in production.
  - Revert the prior --external react-devtools-core flag (no longer
    needed and was causing a different runtime error).
  - Mark node-pty external in tsup so esbuild stops choking on the
    homebridge fork's conditional native require()s
    (build/Release/conpty.node etc.) when bundling for the npm package.
  - Update bundle-output test: the bundle now contains both
    bun:sqlite and node:sqlite specifiers (loaded via createRequire).

Verified end-to-end: dist/fn dashboard -p 0 starts cleanly (no PTY,
sqlite, or devtools errors). Core tests 3038/3038, CLI tests 826/826
(up from 822/826 baseline).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-26 15:25:06 -07:00
parent 112813f122
commit e759a2d91a
9 changed files with 157 additions and 18 deletions

View File

@@ -0,0 +1,100 @@
/**
* SQLite adapter that picks the runtime's native SQLite at construction time:
* - Bun runtime → `bun:sqlite` (built-in, no native module dance)
* - Node runtime → `node:sqlite` (Node 22+ built-in)
*
* Exports a `DatabaseSync` class with the subset of node:sqlite's API that the
* fn codebase actually uses: `prepare`, `exec`, `close`, and prepared
* statement methods `all`, `get`, `run`.
*
* The adapter exists because Bun's --compile bundler does not implement
* `node:sqlite` (require returns undefined silently; import throws), so a
* standalone Bun binary cannot use the same module that plain `node` uses.
*/
import { createRequire } from "node:module";
const isBun = typeof (globalThis as { Bun?: unknown }).Bun !== "undefined";
// Use createRequire so the bundler does not statically trace these specifiers.
// Bun's bundler will skip require() calls whose argument it cannot resolve at
// build time, which keeps `node:sqlite` from being eagerly pulled into the
// compiled binary (where it would fail to resolve at runtime).
const requireFromHere = createRequire(import.meta.url);
export interface SqliteRunResult {
changes: number | bigint;
lastInsertRowid: number | bigint;
}
export interface SqliteStatement {
all(...params: unknown[]): unknown[];
get(...params: unknown[]): unknown;
run(...params: unknown[]): SqliteRunResult;
}
interface RawStatement {
all: (...params: unknown[]) => unknown[];
get: (...params: unknown[]) => unknown;
run: (...params: unknown[]) => { changes: number | bigint; lastInsertRowid: number | bigint };
}
interface RawDatabase {
exec(sql: string): void;
prepare(sql: string): RawStatement;
close(): void;
}
type DatabaseCtor = new (path: string) => RawDatabase;
let cachedCtor: DatabaseCtor | null = null;
function loadDatabaseCtor(): DatabaseCtor {
if (cachedCtor) return cachedCtor;
if (isBun) {
const mod = requireFromHere("bun:sqlite") as { Database: DatabaseCtor };
cachedCtor = mod.Database;
} else {
const mod = requireFromHere("node:sqlite") as { DatabaseSync: DatabaseCtor };
cachedCtor = mod.DatabaseSync;
}
return cachedCtor;
}
/**
* Drop-in replacement for `node:sqlite`'s `DatabaseSync`. Backed by
* `bun:sqlite` under Bun and `node:sqlite` under Node.
*/
export class DatabaseSync {
private impl: RawDatabase;
constructor(path: string) {
const Ctor = loadDatabaseCtor();
this.impl = new Ctor(path);
}
exec(sql: string): void {
this.impl.exec(sql);
}
close(): void {
this.impl.close();
}
prepare(sql: string): SqliteStatement {
const stmt = this.impl.prepare(sql);
// Both node:sqlite and bun:sqlite expose the same .all/.get/.run shape.
// Normalize `get` to return undefined (not null) when no row matches, and
// pass run() through unchanged — both runtimes already produce the same
// { changes, lastInsertRowid } shape.
return {
all: (...params: unknown[]) => stmt.all(...params),
get: (...params: unknown[]) => {
const row = stmt.get(...params);
return row ?? undefined;
},
run: (...params: unknown[]) => stmt.run(...params),
};
}
}