diff --git a/.changeset/fn-7956-bundle-three-plugins.md b/.changeset/fn-7956-bundle-three-plugins.md new file mode 100644 index 0000000000..8ea5ef57ca --- /dev/null +++ b/.changeset/fn-7956-bundle-three-plugins.md @@ -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. diff --git a/packages/cli/src/__tests__/bundle-output-helpers.ts b/packages/cli/src/__tests__/bundle-output-helpers.ts index d0504a3222..af704fb26b 100644 --- a/packages/cli/src/__tests__/bundle-output-helpers.ts +++ b/packages/cli/src/__tests__/bundle-output-helpers.ts @@ -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 }); diff --git a/packages/cli/src/__tests__/bundle-output.test.ts b/packages/cli/src/__tests__/bundle-output.test.ts index ee5f094b2a..be45910bf2 100644 --- a/packages/cli/src/__tests__/bundle-output.test.ts +++ b/packages/cli/src/__tests__/bundle-output.test.ts @@ -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", () => { diff --git a/packages/cli/src/plugin-sdk-core-runtime-shim.ts b/packages/cli/src/plugin-sdk-core-runtime-shim.ts index 416adb601d..55f7980d70 100644 --- a/packages/cli/src/plugin-sdk-core-runtime-shim.ts +++ b/packages/cli/src/plugin-sdk-core-runtime-shim.ts @@ -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 { diff --git a/packages/cli/tsup.config.ts b/packages/cli/tsup.config.ts index 56acd123c8..d2bb980296 100644 --- a/packages/cli/tsup.config.ts +++ b/packages/cli/tsup.config.ts @@ -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`.