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:
@@ -61,6 +61,7 @@
|
|||||||
"@changesets/cli": "^2.30.0",
|
"@changesets/cli": "^2.30.0",
|
||||||
"@eslint/js": "^9.0.0",
|
"@eslint/js": "^9.0.0",
|
||||||
"eslint": "^9.0.0",
|
"eslint": "^9.0.0",
|
||||||
|
"react-devtools-core": "^7.0.1",
|
||||||
"tsx": "^4.19.0",
|
"tsx": "^4.19.0",
|
||||||
"typescript": "^5.7.0",
|
"typescript": "^5.7.0",
|
||||||
"typescript-eslint": "^8.0.0"
|
"typescript-eslint": "^8.0.0"
|
||||||
|
|||||||
@@ -338,10 +338,6 @@ function compileBinary(outFile: string, target: string, isCrossCompile: boolean)
|
|||||||
target,
|
target,
|
||||||
"--minify",
|
"--minify",
|
||||||
"--conditions=source",
|
"--conditions=source",
|
||||||
// ink imports react-devtools-core dynamically only when DEV=true; mark
|
|
||||||
// external so Bun's static bundler doesn't try to resolve it at compile.
|
|
||||||
"--external",
|
|
||||||
"react-devtools-core",
|
|
||||||
],
|
],
|
||||||
cwd: workspaceRoot,
|
cwd: workspaceRoot,
|
||||||
stdout: "inherit",
|
stdout: "inherit",
|
||||||
|
|||||||
@@ -68,10 +68,13 @@ describe("CLI bundle output", () => {
|
|||||||
expect(tsupConfig).toContain("cpSync(dashboardClientSrc, dashboardClientDest, { recursive: true });");
|
expect(tsupConfig).toContain("cpSync(dashboardClientSrc, dashboardClientDest, { recursive: true });");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("preserves node: prefix in node:sqlite imports", () => {
|
it("loads sqlite via runtime adapter (bun:sqlite under Bun, node:sqlite under Node)", () => {
|
||||||
const content = readFileSync(bundlePath, "utf-8");
|
const content = readFileSync(bundlePath, "utf-8");
|
||||||
// Should have node:sqlite, not bare "sqlite"
|
// The sqlite-adapter uses createRequire to pick the runtime backend at
|
||||||
expect(content).toContain('from "node:sqlite"');
|
// construction time; both specifiers should appear as require() targets.
|
||||||
|
expect(content).toMatch(/["']bun:sqlite["']/);
|
||||||
|
expect(content).toMatch(/["']node:sqlite["']/);
|
||||||
|
// No bare "sqlite" import (we never want to pull in an npm package named sqlite)
|
||||||
expect(content).not.toMatch(/from\s+["']sqlite["'][^s]/);
|
expect(content).not.toMatch(/from\s+["']sqlite["'][^s]/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ export default defineConfig({
|
|||||||
options.conditions = [...(options.conditions || []), "source"];
|
options.conditions = [...(options.conditions || []), "source"];
|
||||||
},
|
},
|
||||||
noExternal: [/^@fusion\//],
|
noExternal: [/^@fusion\//],
|
||||||
|
// Native module: leave node-pty (aliased to @homebridge fork) out of the
|
||||||
|
// bundle. esbuild can't statically resolve its conditional native require()s
|
||||||
|
// (build/Release/pty.node, build/Debug/conpty.node, ...).
|
||||||
|
external: ["node-pty", "@homebridge/node-pty-prebuilt-multiarch"],
|
||||||
splitting: false,
|
splitting: false,
|
||||||
clean: true,
|
clean: true,
|
||||||
removeNodeProtocol: false,
|
removeNodeProtocol: false,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DatabaseSync } from "node:sqlite";
|
import { DatabaseSync } from "./sqlite-adapter.js";
|
||||||
import { existsSync, mkdirSync } from "node:fs";
|
import { existsSync, mkdirSync } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import type { ArchivedTaskEntry } from "./types.js";
|
import type { ArchivedTaskEntry } from "./types.js";
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* unified activity feed, global concurrency limits, and project health.
|
* unified activity feed, global concurrency limits, and project health.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { DatabaseSync } from "node:sqlite";
|
import { DatabaseSync } from "./sqlite-adapter.js";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { mkdirSync, existsSync } from "node:fs";
|
import { mkdirSync, existsSync } from "node:fs";
|
||||||
import type { Statement } from "./db.js";
|
import type { Statement } from "./db.js";
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
* Schema version tracking is managed via a `__meta` table.
|
* Schema version tracking is managed via a `__meta` table.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { DatabaseSync } from "node:sqlite";
|
import { DatabaseSync } from "./sqlite-adapter.js";
|
||||||
import { isAbsolute, join } from "node:path";
|
import { isAbsolute, join } from "node:path";
|
||||||
import { mkdirSync, existsSync } from "node:fs";
|
import { mkdirSync, existsSync } from "node:fs";
|
||||||
import { DEFAULT_PROJECT_SETTINGS } from "./types.js";
|
import { DEFAULT_PROJECT_SETTINGS } from "./types.js";
|
||||||
|
|||||||
100
packages/core/src/sqlite-adapter.ts
Normal file
100
packages/core/src/sqlite-adapter.ts
Normal 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),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
51
pnpm-lock.yaml
generated
51
pnpm-lock.yaml
generated
@@ -17,6 +17,9 @@ importers:
|
|||||||
eslint:
|
eslint:
|
||||||
specifier: ^9.0.0
|
specifier: ^9.0.0
|
||||||
version: 9.39.4(jiti@2.6.1)
|
version: 9.39.4(jiti@2.6.1)
|
||||||
|
react-devtools-core:
|
||||||
|
specifier: ^7.0.1
|
||||||
|
version: 7.0.1
|
||||||
tsx:
|
tsx:
|
||||||
specifier: ^4.19.0
|
specifier: ^4.19.0
|
||||||
version: 4.21.0
|
version: 4.21.0
|
||||||
@@ -40,13 +43,13 @@ importers:
|
|||||||
version: 5.2.1
|
version: 5.2.1
|
||||||
ink:
|
ink:
|
||||||
specifier: ^6.8.0
|
specifier: ^6.8.0
|
||||||
version: 6.8.0(@types/react@19.2.14)(react@19.2.4)
|
version: 6.8.0(@types/react@19.2.14)(react-devtools-core@7.0.1)(react@19.2.4)
|
||||||
ink-spinner:
|
ink-spinner:
|
||||||
specifier: ^5.0.0
|
specifier: ^5.0.0
|
||||||
version: 5.0.0(ink@6.8.0(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
|
version: 5.0.0(ink@6.8.0(@types/react@19.2.14)(react-devtools-core@7.0.1)(react@19.2.4))(react@19.2.4)
|
||||||
ink-text-input:
|
ink-text-input:
|
||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.0.0(ink@6.8.0(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
|
version: 6.0.0(ink@6.8.0(@types/react@19.2.14)(react-devtools-core@7.0.1)(react@19.2.4))(react@19.2.4)
|
||||||
ioredis:
|
ioredis:
|
||||||
specifier: ^5.6.0
|
specifier: ^5.6.0
|
||||||
version: 5.10.1
|
version: 5.10.1
|
||||||
@@ -5243,6 +5246,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
|
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
react-devtools-core@7.0.1:
|
||||||
|
resolution: {integrity: sha512-C3yNvRHaizlpiASzy7b9vbnBGLrhvdhl1CbdU6EnZgxPNbai60szdLtl+VL76UNOt5bOoVTOz5rNWZxgGt+Gsw==}
|
||||||
|
|
||||||
react-dom@19.2.4:
|
react-dom@19.2.4:
|
||||||
resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==}
|
resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -5465,6 +5471,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
shell-quote@1.8.3:
|
||||||
|
resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
side-channel-list@1.0.0:
|
side-channel-list@1.0.0:
|
||||||
resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
|
resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -6129,6 +6139,18 @@ packages:
|
|||||||
wrappy@1.0.2:
|
wrappy@1.0.2:
|
||||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||||
|
|
||||||
|
ws@7.5.10:
|
||||||
|
resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==}
|
||||||
|
engines: {node: '>=8.3.0'}
|
||||||
|
peerDependencies:
|
||||||
|
bufferutil: ^4.0.1
|
||||||
|
utf-8-validate: ^5.0.2
|
||||||
|
peerDependenciesMeta:
|
||||||
|
bufferutil:
|
||||||
|
optional: true
|
||||||
|
utf-8-validate:
|
||||||
|
optional: true
|
||||||
|
|
||||||
ws@8.20.0:
|
ws@8.20.0:
|
||||||
resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==}
|
resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
@@ -11153,24 +11175,24 @@ snapshots:
|
|||||||
|
|
||||||
ini@4.1.3: {}
|
ini@4.1.3: {}
|
||||||
|
|
||||||
ink-spinner@5.0.0(ink@6.8.0(@types/react@19.2.14)(react@19.2.4))(react@19.2.4):
|
ink-spinner@5.0.0(ink@6.8.0(@types/react@19.2.14)(react-devtools-core@7.0.1)(react@19.2.4))(react@19.2.4):
|
||||||
dependencies:
|
dependencies:
|
||||||
cli-spinners: 2.9.2
|
cli-spinners: 2.9.2
|
||||||
ink: 6.8.0(@types/react@19.2.14)(react@19.2.4)
|
ink: 6.8.0(@types/react@19.2.14)(react-devtools-core@7.0.1)(react@19.2.4)
|
||||||
react: 19.2.4
|
react: 19.2.4
|
||||||
|
|
||||||
ink-testing-library@4.0.0(@types/react@19.2.14):
|
ink-testing-library@4.0.0(@types/react@19.2.14):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.14
|
'@types/react': 19.2.14
|
||||||
|
|
||||||
ink-text-input@6.0.0(ink@6.8.0(@types/react@19.2.14)(react@19.2.4))(react@19.2.4):
|
ink-text-input@6.0.0(ink@6.8.0(@types/react@19.2.14)(react-devtools-core@7.0.1)(react@19.2.4))(react@19.2.4):
|
||||||
dependencies:
|
dependencies:
|
||||||
chalk: 5.6.2
|
chalk: 5.6.2
|
||||||
ink: 6.8.0(@types/react@19.2.14)(react@19.2.4)
|
ink: 6.8.0(@types/react@19.2.14)(react-devtools-core@7.0.1)(react@19.2.4)
|
||||||
react: 19.2.4
|
react: 19.2.4
|
||||||
type-fest: 4.41.0
|
type-fest: 4.41.0
|
||||||
|
|
||||||
ink@6.8.0(@types/react@19.2.14)(react@19.2.4):
|
ink@6.8.0(@types/react@19.2.14)(react-devtools-core@7.0.1)(react@19.2.4):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@alcalzone/ansi-tokenize': 0.2.5
|
'@alcalzone/ansi-tokenize': 0.2.5
|
||||||
ansi-escapes: 7.3.0
|
ansi-escapes: 7.3.0
|
||||||
@@ -11200,6 +11222,7 @@ snapshots:
|
|||||||
yoga-layout: 3.2.1
|
yoga-layout: 3.2.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.14
|
'@types/react': 19.2.14
|
||||||
|
react-devtools-core: 7.0.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bufferutil
|
- bufferutil
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
@@ -12426,6 +12449,14 @@ snapshots:
|
|||||||
minimist: 1.2.8
|
minimist: 1.2.8
|
||||||
strip-json-comments: 2.0.1
|
strip-json-comments: 2.0.1
|
||||||
|
|
||||||
|
react-devtools-core@7.0.1:
|
||||||
|
dependencies:
|
||||||
|
shell-quote: 1.8.3
|
||||||
|
ws: 7.5.10
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
react-dom@19.2.4(react@19.2.4):
|
react-dom@19.2.4(react@19.2.4):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.4
|
react: 19.2.4
|
||||||
@@ -12741,6 +12772,8 @@ snapshots:
|
|||||||
|
|
||||||
shebang-regex@3.0.0: {}
|
shebang-regex@3.0.0: {}
|
||||||
|
|
||||||
|
shell-quote@1.8.3: {}
|
||||||
|
|
||||||
side-channel-list@1.0.0:
|
side-channel-list@1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
@@ -13610,6 +13643,8 @@ snapshots:
|
|||||||
|
|
||||||
wrappy@1.0.2: {}
|
wrappy@1.0.2: {}
|
||||||
|
|
||||||
|
ws@7.5.10: {}
|
||||||
|
|
||||||
ws@8.20.0: {}
|
ws@8.20.0: {}
|
||||||
|
|
||||||
xml-name-validator@5.0.0: {}
|
xml-name-validator@5.0.0: {}
|
||||||
|
|||||||
Reference in New Issue
Block a user