feat(FN-5611): strip react devtools from bun compile builds

Complete the standalone build pipeline fix by setting `DEV=false` during bun compile to eliminate React DevTools from production executables, with hardened regression tests and documentation.

Fusion-Task-Id: FN-5611

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5611
This commit is contained in:
gsxdsm
2026-05-27 07:02:19 -07:00
parent bb19823ed4
commit 8a0fbf00b9
5 changed files with 40 additions and 6 deletions

View File

@@ -160,6 +160,8 @@ Prebuilt standalone binaries are available that require no Node.js runtime. You
bun run build.ts
```
The standalone build defines `process.env.DEV` as `false` at compile time so Ink's DEV-only React devtools import path is removed from the binary (preventing runtime `react-devtools-core` resolution failures).
### Runtime Assets
When using standalone binaries, the dashboard's integrated terminal requires native platform assets that must be co-located with the binary:

View File

@@ -16,6 +16,9 @@
* Notes:
* - If dashboard client assets are missing, this script generates a
* minimal dist/client/index.html stub so clean-checkout tests can run.
* - Ink's DEV-only react-devtools import is eliminated at compile time via
* --define "process.env.DEV='false'" to keep the standalone binary
* self-contained without node_modules.
*/
import { join, dirname } from "node:path";
@@ -339,10 +342,11 @@ function compileBinary(outFile: string, target: string, isCrossCompile: boolean)
target,
"--minify",
"--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",
// Ink conditionally loads devtools when process.env.DEV === "true".
// Force DEV to false at compile-time so Bun/minify can eliminate that branch
// and avoid shipping runtime references to react-devtools-core.
"--define",
"process.env.DEV='false'",
// cpu-features: native .node binding from ssh2 (transitive via dockerode); ssh2 falls back to pure JS when unavailable
"--external",
"cpu-features",

View File

@@ -90,7 +90,14 @@ describe.skipIf(!SHOULD_RUN_BUILD_EXE)("build-exe-cross: --all builds all platfo
const allBuilt = SUPPORTED_TARGETS.every((target) =>
existsSync(join(distDir, expectedBinaryName(target))),
);
if (allBuilt) return;
const platform = process.platform === "darwin" ? "darwin" :
process.platform === "linux" ? "linux" :
process.platform === "win32" ? "win32" : "unknown";
const arch = process.arch === "arm64" ? "arm64" :
process.arch === "x64" ? "x64" : "unknown";
const hostRuntimeDir = join(distDir, "runtime", `${platform}-${arch}`);
const hostRuntimeReady = existsSync(join(hostRuntimeDir, "pty.node"));
if (allBuilt && hostRuntimeReady) return;
execSync("bun run build.ts --all", {
cwd: cliRoot,
stdio: "pipe",
@@ -121,6 +128,16 @@ describe.skipIf(!SHOULD_RUN_BUILD_EXE)("build-exe-cross: --all builds all platfo
const prebuildName = `${platform}-${arch}`;
const runtimeDir = join(distDir, "runtime", prebuildName);
if (!existsSync(join(runtimeDir, "pty.node"))) {
// Ensure host-native runtime assets exist even if a previous --all build
// ended on a non-host target.
execSync("bun run build.ts", {
cwd: cliRoot,
stdio: "pipe",
timeout: 120_000,
});
}
// pty.node is required for all platforms
expect(existsSync(join(runtimeDir, "pty.node"))).toBe(true);
// spawn-helper is only for Unix platforms

View File

@@ -10,6 +10,7 @@ const outBinary = join(cliRoot, "dist", process.platform === "win32" ? "fn.exe"
const binaryName = process.platform === "win32" ? "fn.exe" : "fn";
const clientDir = join(cliRoot, "dist", "client");
const runtimeDir = join(cliRoot, "dist", "runtime");
const childEnv = { ...process.env, VITEST: "false" };
/**
* Create an isolated temp directory containing the binary, client/,
@@ -72,6 +73,7 @@ type AsyncSpawnResult = {
async function runCommandWithTimeout(binary: string, args: string[], timeoutMs: number): Promise<AsyncSpawnResult> {
const child = spawn(binary, args, {
stdio: ["ignore", "pipe", "pipe"],
env: childEnv,
});
let stdout = "";
@@ -179,6 +181,7 @@ describe("build-exe", () => {
expect(result.stdout).toContain("dashboard");
expect(result.stdout).toContain("task create");
expect(result.stdout).toContain("task list");
expect(result.stderr).not.toContain("react-devtools-core");
} finally {
cleanup();
}
@@ -192,12 +195,14 @@ describe("build-exe", () => {
const result = spawnSync(binary, ["task", "list"], {
encoding: "utf-8",
timeout: 30_000,
env: childEnv,
});
if (hasKnownBunSqliteLimitation(result)) {
return;
}
expect(result.status).toBe(0);
expect(result.stdout).toContain("No tasks yet");
expect(result.stderr ?? "").not.toContain("react-devtools-core");
} finally {
cleanup();
}
@@ -212,9 +217,10 @@ describe("build-exe", () => {
try {
// Ask the OS for an ephemeral port so this smoke test never collides
// with a developer's running dashboard or dev server.
child = spawn(binary, ["dashboard", "-p", "0"], {
child = spawn(binary, ["dashboard", "-p", "0", "--no-auth"], {
cwd: dir,
stdio: ["ignore", "pipe", "pipe"],
env: childEnv,
});
if (!child.stdout || !child.stderr) {