feat(FN-3262): harden CLI native bundle externalization with tests

The merge adds Step 2 of the native externalization hardening for the CLI package, including a minor changeset for `@runfusion/fusion` and tests validating bundle output and package configuration behavior. The changes strengthen the contract governing how native modules are handled during the build

Fusion-Task-Id: FN-3262
This commit is contained in:
Fusion
2026-05-04 06:18:16 -07:00
committed by gsxdsm
parent cc5c8c668b
commit d761ea8dd2
4 changed files with 28 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Hardened CLI packaging against native module build regressions by asserting `dockerode`/`ssh2`/`cpu-features` remain externalized in tsup bundle config, preventing native `.node` artifact strings from being inlined into the bundle, and declaring `dockerode` as a runtime dependency for published installs.

View File

@@ -57,6 +57,7 @@
"dependencies": { "dependencies": {
"@mariozechner/pi-ai": "^0.72.1", "@mariozechner/pi-ai": "^0.72.1",
"@mariozechner/pi-coding-agent": "^0.72.1", "@mariozechner/pi-coding-agent": "^0.72.1",
"dockerode": "^4.0.12",
"express": "^5.1.0", "express": "^5.1.0",
"ink": "^6.8.0", "ink": "^6.8.0",
"ink-spinner": "^5.0.0", "ink-spinner": "^5.0.0",

View File

@@ -80,6 +80,14 @@ describe("CLI bundle output", () => {
expect(tsupConfig).toContain("cpSync(dashboardClientSrc, dashboardClientDest, { recursive: true });"); expect(tsupConfig).toContain("cpSync(dashboardClientSrc, dashboardClientDest, { recursive: true });");
}); });
it("keeps native module loaders externalized in tsup config", () => {
const tsupConfig = readFileSync(tsupConfigPath, "utf-8");
expect(tsupConfig).toContain('"dockerode"');
expect(tsupConfig).toContain('"ssh2"');
expect(tsupConfig).toContain('"cpu-features"');
});
it("loads sqlite from Node built-ins and never from bare sqlite npm package", () => { it("loads sqlite from Node built-ins and never from bare sqlite npm package", () => {
const content = readFileSync(bundlePath, "utf-8"); const content = readFileSync(bundlePath, "utf-8");
// The bundle must resolve sqlite through Node's built-in module. // The bundle must resolve sqlite through Node's built-in module.
@@ -89,6 +97,12 @@ describe("CLI bundle output", () => {
expect(content).not.toMatch(/from\s+["']sqlite["'][^s]/); expect(content).not.toMatch(/from\s+["']sqlite["'][^s]/);
}); });
it("does not inline native artifact filenames into the bundled CLI", () => {
const content = readFileSync(bundlePath, "utf-8");
expect(content).not.toContain("sshcrypto.node");
expect(content).not.toContain("cpufeatures.node");
});
it("provides require via createRequire banner", () => { it("provides require via createRequire banner", () => {
const content = readFileSync(bundlePath, "utf-8"); const content = readFileSync(bundlePath, "utf-8");
// Banner should inject createRequire for ESM CJS interop // Banner should inject createRequire for ESM CJS interop

View File

@@ -78,6 +78,14 @@ describe("CLI package.json publishing config", () => {
const deps = Object.keys(pkg.dependencies || {}); const deps = Object.keys(pkg.dependencies || {});
expect(deps).toContain("ioredis"); expect(deps).toContain("ioredis");
}); });
it("declares dockerode as a runtime dependency when kept external in CLI bundling", () => {
const deps = Object.keys(pkg.dependencies || {});
const devDeps = Object.keys(pkg.devDependencies || {});
expect(deps).toContain("dockerode");
expect(devDeps).not.toContain("dockerode");
});
}); });
describe("Scoped @fusion/* packages publishing config", () => { describe("Scoped @fusion/* packages publishing config", () => {