FN-7956: bundle three plugins as install-safe JS entrypoints

Ship reports, cli-printing-press, and whatsapp-chat through tsup so global/npm installs no longer load raw .ts under node_modules (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING).

- Route WhatsApp Chat, Reports, and CLI Printing Press through bundlePluginEntry instead of copying src/
- Re-export postgresSchema from the core runtime shim for published plugin bundles
- Keep Baileys optional deps external for WhatsApp Chat bundling
- Extend bundle-output helpers/tests to assert self-contained plugin bundles
- Add patch changeset for @runfusion/fusion packaging fix

Files changed:
 .changeset/fn-7956-bundle-three-plugins.md         |  7 ++
 .../cli/src/__tests__/bundle-output-helpers.ts     | 30 +++++++-
 packages/cli/src/__tests__/bundle-output.test.ts   | 83 ++++++++++++++--------
 packages/cli/src/plugin-sdk-core-runtime-shim.ts   |  6 ++
 packages/cli/tsup.config.ts                        | 71 +++++++-----------
 5 files changed, 123 insertions(+), 74 deletions(-)

Fusion-Task-Id: FN-7956

Fusion-Task-Lineage: e32736d9-e637-450c-966b-6b2d6bc69b88

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-15 12:09:29 -07:00
parent ecd497190d
commit d956abceee
5 changed files with 123 additions and 74 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Reports, CLI Printing Press, and WhatsApp Chat plugins now load from global installs.
category: fix
dev: Stages the three plugins through bundlePluginEntry with postgresSchema shim coverage and bundle-output regression assertions.

View File

@@ -9,6 +9,24 @@ export const bundlePath = join(cliRoot, "dist", "bin.js");
export const clientIndexPath = join(cliRoot, "dist", "client", "index.html");
const cursorPluginManifestPath = join(cliRoot, "dist", "plugins", "fusion-plugin-cursor-runtime", "manifest.json");
const roadmapPluginBundledPath = join(cliRoot, "dist", "plugins", "fusion-plugin-roadmap", "bundled.js");
const reportsPluginBundledPath = join(cliRoot, "dist", "plugins", "fusion-plugin-reports", "bundled.js");
const cliPrintingPressPluginBundledPath = join(
cliRoot,
"dist",
"plugins",
"fusion-plugin-cli-printing-press",
"bundled.js",
);
const whatsappChatPluginBundledPath = join(cliRoot, "dist", "plugins", "fusion-plugin-whatsapp-chat", "bundled.js");
const compoundEngineeringSkillPath = join(
cliRoot,
"dist",
"plugins",
"fusion-plugin-compound-engineering",
"skills",
"ce-brainstorm",
"SKILL.md",
);
export const openclawMcpSchemaServerPath = join(
cliRoot,
"dist",
@@ -50,6 +68,10 @@ export function hasBuiltDashboardAssets(): boolean {
!existsSync(clientIndexPath) ||
!existsSync(cursorPluginManifestPath) ||
!existsSync(roadmapPluginBundledPath) ||
!existsSync(reportsPluginBundledPath) ||
!existsSync(cliPrintingPressPluginBundledPath) ||
!existsSync(whatsappChatPluginBundledPath) ||
!existsSync(compoundEngineeringSkillPath) ||
!existsSync(openclawMcpSchemaServerPath) ||
!existsSync(droidPluginMcpServerPath)
) {
@@ -115,7 +137,11 @@ export function buildCliWithRealDashboardAssets() {
try {
runBuildCommand(`node ${join(workspaceRoot, "scripts", "ensure-test-artifacts.mjs")}`, workspaceRoot);
runBuildCommand("pnpm --filter @fusion/dashboard build:client", workspaceRoot);
runBuildCommand("pnpm build", cliRoot);
/*
* FNXC:BundledPlugins 2026-07-15-09:08:
* bundle-output tests assert the published CLI packaging surface, including staged bundled plugins and skill assets. Local `pnpm build` may use fast package mode, so bootstrap with `build:package` to force FUSION_CLI_FULL_PACKAGE and avoid reading stale raw-src plugin output from dist/.
*/
runBuildCommand("pnpm build:package", cliRoot);
if (hasBuiltDashboardAssets()) {
return;
@@ -124,7 +150,7 @@ export function buildCliWithRealDashboardAssets() {
// Fallback for environments where build:client alone does not refresh the
// dashboard dist/client bundle consumed by the CLI copy step.
runBuildCommand("pnpm --filter @fusion/dashboard build", workspaceRoot);
runBuildCommand("pnpm build", cliRoot);
runBuildCommand("pnpm build:package", cliRoot);
} finally {
if (acquiredLock) {
rmSync(buildLockDir, { recursive: true, force: true });

View File

@@ -22,8 +22,16 @@ const bundlePluginEntryPluginIds = [
"fusion-plugin-dependency-graph",
"fusion-plugin-roadmap",
"fusion-plugin-compound-engineering",
"fusion-plugin-whatsapp-chat",
"fusion-plugin-reports",
"fusion-plugin-cli-printing-press",
"fusion-plugin-linear-import",
] as const;
const selfContainedBundlePluginIds = [
"fusion-plugin-reports",
"fusion-plugin-cli-printing-press",
"fusion-plugin-whatsapp-chat",
] as const;
const knownCompoundEngineeringSkillIds = [
"ce-brainstorm",
"ce-code-review",
@@ -39,6 +47,37 @@ const knownCompoundEngineeringSkillIds = [
"ce-work",
] as const;
function expectSelfContainedBundle(pluginId: typeof selfContainedBundlePluginIds[number]) {
const stagedRoot = join(cliRoot, "dist", "plugins", pluginId);
const manifestPath = join(stagedRoot, "manifest.json");
const packageJsonPath = join(stagedRoot, "package.json");
const bundledPath = join(stagedRoot, "bundled.js");
expect(existsSync(bundledPath), `${pluginId} should ship bundled.js`).toBe(true);
expect(existsSync(join(stagedRoot, "src")), `${pluginId} should not ship raw src/`).toBe(false);
expect(
readdirSync(stagedRoot).some((entry) => /\.index\.reload-\d+\.ts$/.test(entry)),
`${pluginId} should not ship hot-reload TypeScript artifacts`,
).toBe(false);
expect(existsSync(manifestPath), `${pluginId} manifest should exist`).toBe(true);
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as { id?: string; name?: string };
expect(manifest.id).toBe(pluginId);
expect(typeof manifest.name).toBe("string");
expect(manifest.name?.length).toBeGreaterThan(0);
const stagedPkg = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as {
exports?: { "."?: { import?: string } };
};
expect(stagedPkg.exports?.["."]?.import).toBe("./bundled.js");
const bundled = readFileSync(bundledPath, "utf-8");
expect(bundled, `${pluginId} should not keep a bare @fusion/core import`).not.toMatch(
/from\s+["']@fusion\/core["']/,
);
expect(bundled, `${pluginId} should not mention the private @fusion/core package`).not.toContain("@fusion/core");
}
describe("CLI bundle output", () => {
beforeAll(() => {
// Intentional: bundle-output tests validate compiled artifacts, so they
@@ -296,40 +335,28 @@ describe("CLI bundle output", () => {
/from\s+["']@fusion\/core["']/,
);
expect(bundled, `${pluginId} should not mention the private @fusion/core package`).not.toContain(
'"@fusion/core"',
"@fusion/core",
);
}
expect(inspectedPluginIds.length).toBeGreaterThan(0);
});
it("dist/plugins/fusion-plugin-whatsapp-chat/ is staged with a valid manifest", () => {
const stagedRoot = join(cliRoot, "dist", "plugins", "fusion-plugin-whatsapp-chat");
const manifestPath = join(stagedRoot, "manifest.json");
expect(existsSync(manifestPath)).toBe(true);
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as { id?: string; name?: string };
expect(manifest.id).toBe("fusion-plugin-whatsapp-chat");
expect(typeof manifest.name).toBe("string");
expect(manifest.name?.length).toBeGreaterThan(0);
});
it("dist/plugins/fusion-plugin-cli-printing-press/ is staged with source entry for bundled install", () => {
const stagedRoot = join(cliRoot, "dist", "plugins", "fusion-plugin-cli-printing-press");
const manifestPath = join(stagedRoot, "manifest.json");
const packageJsonPath = join(stagedRoot, "package.json");
expect(existsSync(manifestPath)).toBe(true);
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as { id?: string; name?: string };
expect(manifest.id).toBe("fusion-plugin-cli-printing-press");
expect(typeof manifest.name).toBe("string");
expect(manifest.name?.length).toBeGreaterThan(0);
expect(existsSync(join(stagedRoot, "src", "index.ts"))).toBe(true);
const stagedPkg = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as {
exports?: { "."?: { import?: string } };
};
expect(stagedPkg.exports?.["."]?.import).toBe("./src/index.ts");
it("reports, cli-printing-press, and whatsapp-chat ship self-contained bundled.js outputs", () => {
/*
* FNXC:BundledPlugins 2026-07-15-00:00:
* Surface Enumeration for FN-7956:
* - [x] Providers / bridges / execution paths: fusion-plugin-reports, fusion-plugin-cli-printing-press, and fusion-plugin-whatsapp-chat are all asserted through this bundlePluginEntry output invariant.
* - [x] Desktop + mobile breakpoints / platforms: N/A build/packaging-only change; desktop missing-bundle behavior for non-staged dependencies is unchanged.
* - [x] Empty / undefined / duplicate / populated data states: each staged root must contain bundled.js, omit src/, and omit .index.reload-N.ts artifacts.
* - [x] Shared hooks / components / modules / helpers: these ids are included in bundlePluginEntryPluginIds so the shared @fusion/core runtime-shim alias invariant covers them.
* - [x] Every component that renders the affordance: N/A no UI affordance add/remove.
* - [x] Leftover shells after removal: package output assertions fail if the old raw-src branches leave src/ or reload TypeScript files behind.
* - [x] Runtime-value assertion: each emitted bundled.js has no from "@fusion/core" import and no literal @fusion/core occurrence.
*/
for (const pluginId of selfContainedBundlePluginIds) {
expectSelfContainedBundle(pluginId);
}
});
it("dist/plugins/fusion-plugin-openclaw-runtime/ is staged with required bridge assets", () => {

View File

@@ -1,5 +1,11 @@
import type { BoardActionTaskStore, ColumnId, Task } from "@fusion/core";
/**
* FNXC:BundledPlugins 2026-07-15-00:00:
* Reports and CLI Printing Press import the `postgresSchema` runtime namespace while the CLI bundler aliases `@fusion/core` to this shim for published plugin bundles. Re-export the concrete core schema build artifact here so npm-installed `bundled.js` files resolve `postgresSchema.plugin` without keeping a bare private `@fusion/core` runtime specifier or crashing with `Cannot find package '@fusion/core'`; using dist keeps @runfusion/fusion typecheck inside its package root while esbuild still bundles the schema runtime values.
*/
export * as postgresSchema from "../../core/dist/postgres/schema/index.js";
export const WORKFLOW_EXTENSION_SCHEMA_VERSION = 1 as const;
export function workflowExtensionRegistryId(pluginId: string, extensionId: string): string {

View File

@@ -88,6 +88,7 @@ type BundlePluginEntryOptions = {
srcDir: string;
destDir: string;
withMcpAsset?: boolean;
external?: string[];
};
type PackageManifest = {
@@ -161,7 +162,7 @@ function writeSanitizedCopiedManifest(srcPkgPath: string, destPkgPath: string) {
writeFileSync(destPkgPath, JSON.stringify(destPkg, null, 2));
}
async function bundlePluginEntry({ pluginId, srcDir, destDir, withMcpAsset = false }: BundlePluginEntryOptions) {
async function bundlePluginEntry({ pluginId, srcDir, destDir, withMcpAsset = false, external = [] }: BundlePluginEntryOptions) {
if (existsSync(destDir)) {
rmSync(destDir, { recursive: true, force: true });
}
@@ -199,7 +200,7 @@ async function bundlePluginEntry({ pluginId, srcDir, destDir, withMcpAsset = fal
platform: "node",
target: "node22",
outfile: join(destDir, "bundled.js"),
external: ["@fusion/engine"],
external: ["@fusion/engine", ...external],
alias: {
"@fusion/plugin-sdk": join(__dirname, "..", "plugin-sdk", "src", "index.ts"),
/*
@@ -475,20 +476,20 @@ const cliBuildConfig = {
destDir: dependencyGraphPluginDest,
});
if (existsSync(whatsappChatPluginDest)) {
rmSync(whatsappChatPluginDest, { recursive: true, force: true });
}
if (existsSync(whatsappChatPluginSrc)) {
mkdirSync(whatsappChatPluginDest, { recursive: true });
cpSync(join(whatsappChatPluginSrc, "manifest.json"), join(whatsappChatPluginDest, "manifest.json"));
writeSanitizedCopiedManifest(join(whatsappChatPluginSrc, "package.json"), join(whatsappChatPluginDest, "package.json"));
cpSync(join(whatsappChatPluginSrc, "src"), join(whatsappChatPluginDest, "src"), { recursive: true });
console.log("Copied WhatsApp chat plugin to dist/plugins/fusion-plugin-whatsapp-chat/");
} else {
console.warn(
`WARNING: WhatsApp chat plugin source not found at ${whatsappChatPluginSrc}; bundled auto-install will be unavailable.`,
);
}
/*
* FNXC:BundledPlugins 2026-07-15-00:00:
* FN-7956 moves WhatsApp Chat, Reports, and CLI Printing Press off raw `src/` staging because npm/global installs place them under `node_modules`, where Node refuses TypeScript type stripping. Route them through bundlePluginEntry so each published plugin root contains install-safe `bundled.js` output and no `.ts` entry tree.
*/
await bundlePluginEntry({
pluginId: "fusion-plugin-whatsapp-chat",
srcDir: whatsappChatPluginSrc,
destDir: whatsappChatPluginDest,
/*
* FNXC:BundledPlugins 2026-07-15-09:12:
* Baileys contains optional dynamic require() paths for QR/link-preview/media helpers that are not needed for plugin module load. Keep those optional packages external so the published WhatsApp Chat bundled.js can be produced and loaded without reintroducing a raw TypeScript src/ entry under node_modules.
*/
external: ["jimp", "link-preview-js", "qrcode-terminal"],
});
await bundlePluginEntry({
pluginId: "fusion-plugin-roadmap",
@@ -508,35 +509,17 @@ const cliBuildConfig = {
destDir: linearImportPluginDest,
});
if (existsSync(reportsPluginDest)) {
rmSync(reportsPluginDest, { recursive: true, force: true });
}
if (existsSync(reportsPluginSrc)) {
mkdirSync(reportsPluginDest, { recursive: true });
cpSync(join(reportsPluginSrc, "manifest.json"), join(reportsPluginDest, "manifest.json"));
writeSanitizedCopiedManifest(join(reportsPluginSrc, "package.json"), join(reportsPluginDest, "package.json"));
cpSync(join(reportsPluginSrc, "src"), join(reportsPluginDest, "src"), { recursive: true });
console.log("Copied reports plugin to dist/plugins/fusion-plugin-reports/");
} else {
console.warn(
`WARNING: Reports plugin source not found at ${reportsPluginSrc}; bundled auto-install will be unavailable.`,
);
}
await bundlePluginEntry({
pluginId: "fusion-plugin-reports",
srcDir: reportsPluginSrc,
destDir: reportsPluginDest,
});
if (existsSync(cliPrintingPressPluginDest)) {
rmSync(cliPrintingPressPluginDest, { recursive: true, force: true });
}
if (existsSync(cliPrintingPressPluginSrc)) {
mkdirSync(cliPrintingPressPluginDest, { recursive: true });
cpSync(join(cliPrintingPressPluginSrc, "manifest.json"), join(cliPrintingPressPluginDest, "manifest.json"));
writeSanitizedCopiedManifest(join(cliPrintingPressPluginSrc, "package.json"), join(cliPrintingPressPluginDest, "package.json"));
cpSync(join(cliPrintingPressPluginSrc, "src"), join(cliPrintingPressPluginDest, "src"), { recursive: true });
console.log("Copied cli-printing-press plugin to dist/plugins/fusion-plugin-cli-printing-press/");
} else {
console.warn(
`WARNING: cli-printing-press plugin source not found at ${cliPrintingPressPluginSrc}; bundled auto-install will be unavailable.`,
);
}
await bundlePluginEntry({
pluginId: "fusion-plugin-cli-printing-press",
srcDir: cliPrintingPressPluginSrc,
destDir: cliPrintingPressPluginDest,
});
// Bundle each runtime plugin into a self-contained ESM file so npm/npx
// installs can load them without the workspace `@fusion/plugin-sdk`.