Files
fusion/packages/cli/tsup.config.ts
gsxdsm 96b2df115e fix(desktop): heal launch-mode/shell split-brain hanging Windows local runtime + fix Windows root build (#1878)
## Windows: local runtime hung at "Starting local Fusion runtime…"

### Root cause
Desktop startup had two independent persisted sources of truth that
could disagree:
- `desktop-launch-mode.json` — decides whether **main** *starts* the
embedded local runtime
- `shell-connections.json` (`desktopMode`) — decides whether the
renderer **launch gate** *waits* for it

`shell:setDesktopMode` persists shell settings **before** the fallible
`startLocalRuntimeOnce()` / `saveDesktopLaunchMode()`. So a first
"local" selection whose runtime start threw or was interrupted left
`shell=local` / `launch-mode=choose` **permanently**. Every later launch
then sat at "Starting local Fusion runtime…" polling a runtime nobody
started → 30s timeout.

### Fix (defense in depth) — `78f0bc31`
- `initializeApp` reconciles: a completed shell `local` selection is
authoritative → heals the launch-mode file and starts the runtime.
- `onDesktopModeChange` / `onDesktopLaunchModeChange` persist
launch-mode **before** the fallible start so it can't re-desync.
- `DesktopLaunchGate` no longer assumes main started the runtime — if
it's not running/starting it actively `setDesktopMode("local")` before
polling.
- Env-gated startup trace (`FUSION_STARTUP_TRACE`) so packaged builds
(which log nothing) are diagnosable.
- Regression tests: split-brain → runtime starts + file heals;
agreement-on-choose → no start.

**Verified end-to-end under real Electron 35 / Node 22.16**: from the
exact split-brain state the runtime now reaches `RUNNING` and the
launch-mode file heals.

### Also: Windows root-build breakages — `bd24bd4c8`
- `scripts/build-workspace.mjs` "run as main" guard compared
`import.meta.url` to `` `file://${process.argv[1]}` ``, which never
matches on Windows → root `pnpm build` silently no-opped (exit 0, no
dist). Now uses `pathToFileURL(process.argv[1]).href`.
- `spawn('pnpm', …)` without `shell:true` (ENOENT on Windows) in
`build-workspace.mjs` and `packages/cli/tsup.config.ts` → pass `shell`
on win32.

### Notes
- `@fusion/desktop` and `@fusion/dashboard` are private → no changeset.
- Build the Windows installer via the `desktop-windows` workflow
(`electron-builder --projectDir deploy`); local `pnpm deploy` staging
hits an unrelated directory-rename race on managed-workspace
filesystems.

🤖 Generated with [Claude Code](https://claude.com/claude-code)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added optional desktop runtime startup tracing (enabled via
environment variable).
* **Bug Fixes**
* Improved local desktop handoff to prevent reload loops and navigate
directly to the embedded local runtime.
* Added “split-brain” healing between persisted launch mode and shell
settings.
* Prevented auto-registration of runtime root/CWD during
desktop/dashboard startup.
* Improved Windows compatibility for CLI/workspace command spawning and
npm install process handling.
* **Tests**
* Expanded local/Electron integration, navigation, and onboarding
regression coverage; improved async flushing for more reliable
initialization.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-03 00:17:58 -07:00

510 lines
21 KiB
TypeScript

import { defineConfig } from "tsup";
import { spawn } from "node:child_process";
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { build as esbuildBuild } from "esbuild";
import { ALL_STAGED_BUNDLED_IDS, RUNTIME_PLUGIN_IDS } from "./src/plugins/staged-bundled-plugin-ids";
export { ALL_STAGED_BUNDLED_IDS };
const RUNTIME_PLUGINS_WITH_MCP_SCHEMA_SERVER = new Set([
"fusion-plugin-openclaw-runtime",
"fusion-plugin-droid-runtime",
]);
const __dirname = dirname(fileURLToPath(import.meta.url));
const workspaceRoot = join(__dirname, "..", "..");
const dashboardClientSrc = join(__dirname, "..", "dashboard", "dist", "client");
const dashboardClientDest = join(__dirname, "dist", "client");
const piClaudeCliSrc = join(__dirname, "..", "pi-claude-cli");
const piClaudeCliDest = join(__dirname, "dist", "pi-claude-cli");
const droidCliSrc = join(__dirname, "..", "droid-cli");
const droidCliDest = join(__dirname, "dist", "droid-cli");
const desktopRuntimeSrc = join(__dirname, "..", "desktop", "dist");
const desktopRuntimeDest = join(__dirname, "dist", "desktop");
const llamaCppSrc = join(__dirname, "..", "pi-llama-cpp");
const llamaCppDest = join(__dirname, "dist", "pi-llama-cpp");
const dependencyGraphPluginSrc = join(__dirname, "..", "..", "plugins", "fusion-plugin-dependency-graph");
const dependencyGraphPluginDest = join(__dirname, "dist", "plugins", "fusion-plugin-dependency-graph");
const whatsappChatPluginSrc = join(__dirname, "..", "..", "plugins", "fusion-plugin-whatsapp-chat");
const whatsappChatPluginDest = join(__dirname, "dist", "plugins", "fusion-plugin-whatsapp-chat");
const roadmapPluginSrc = join(__dirname, "..", "..", "plugins", "fusion-plugin-roadmap");
const roadmapPluginDest = join(__dirname, "dist", "plugins", "fusion-plugin-roadmap");
const reportsPluginSrc = join(__dirname, "..", "..", "plugins", "fusion-plugin-reports");
const reportsPluginDest = join(__dirname, "dist", "plugins", "fusion-plugin-reports");
const cliPrintingPressPluginSrc = join(__dirname, "..", "..", "plugins", "fusion-plugin-cli-printing-press");
const cliPrintingPressPluginDest = join(__dirname, "dist", "plugins", "fusion-plugin-cli-printing-press");
const compoundEngineeringPluginSrc = join(__dirname, "..", "..", "plugins", "fusion-plugin-compound-engineering");
const compoundEngineeringPluginDest = join(__dirname, "dist", "plugins", "fusion-plugin-compound-engineering");
const linearImportPluginSrc = join(__dirname, "..", "..", "plugins", "fusion-plugin-linear-import");
const linearImportPluginDest = join(__dirname, "dist", "plugins", "fusion-plugin-linear-import");
const dashboardClientStub = `<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Fusion Dashboard</title>
</head>
<body>
<main>
<h1>Fusion Dashboard</h1>
<p>Dashboard assets not built — run \`pnpm build\` to generate full client assets.</p>
</main>
</body>
</html>
`;
type BundlePluginEntryOptions = {
pluginId: string;
srcDir: string;
destDir: string;
withMcpAsset?: boolean;
};
type PackageManifest = {
name?: string;
version?: string;
type?: string;
exports?: unknown;
main?: string;
pi?: unknown;
private?: boolean;
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
peerDependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
};
const dependencyMapKeys = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"] as const;
function isDependencyMap(value: unknown): value is Record<string, string> {
return (
typeof value === "object" &&
value !== null &&
!Array.isArray(value) &&
Object.values(value).every((specifier) => typeof specifier === "string")
);
}
function sanitizeDependencyMap(dependencyMap: unknown): Record<string, string> | undefined {
if (!isDependencyMap(dependencyMap)) {
return undefined;
}
const sanitized = Object.fromEntries(
Object.entries(dependencyMap).filter(
([name, specifier]) => !name.startsWith("@fusion/") && !specifier.includes("workspace:"),
),
);
return Object.keys(sanitized).length > 0 ? sanitized : undefined;
}
/*
* FNXC:Packaging 2026-06-26-08:40:
* Copied source manifests in the published CLI must be install-safe outside the workspace. Private @fusion/* dependencies and workspace: specifiers make package managers resolve unpublished packages during npm/npx installs, producing the FN-7060 missing fusion core failure, so raw-src plugin and pi-extension manifests are rewritten while preserving loadable entry metadata and real third-party deps.
*/
function writeSanitizedCopiedManifest(srcPkgPath: string, destPkgPath: string) {
const srcPkg = JSON.parse(readFileSync(srcPkgPath, "utf-8")) as PackageManifest;
const destPkg: PackageManifest = {
name: srcPkg.name,
version: srcPkg.version,
type: srcPkg.type,
private: true,
};
if (srcPkg.exports !== undefined) {
destPkg.exports = srcPkg.exports;
}
if (srcPkg.main !== undefined) {
destPkg.main = srcPkg.main;
}
if (srcPkg.pi !== undefined) {
destPkg.pi = srcPkg.pi;
}
for (const dependencyMapKey of dependencyMapKeys) {
const sanitizedDependencyMap = sanitizeDependencyMap(srcPkg[dependencyMapKey]);
if (sanitizedDependencyMap) {
destPkg[dependencyMapKey] = sanitizedDependencyMap;
}
}
writeFileSync(destPkgPath, JSON.stringify(destPkg, null, 2));
}
async function bundlePluginEntry({ pluginId, srcDir, destDir, withMcpAsset = false }: BundlePluginEntryOptions) {
if (existsSync(destDir)) {
rmSync(destDir, { recursive: true, force: true });
}
if (!existsSync(srcDir)) {
console.warn(
`WARNING: Plugin source not found at ${srcDir}; ${pluginId} will be unavailable in the published package.`,
);
return;
}
mkdirSync(destDir, { recursive: true });
cpSync(join(srcDir, "manifest.json"), join(destDir, "manifest.json"));
const srcPkg = JSON.parse(readFileSync(join(srcDir, "package.json"), "utf-8"));
const destPkg = {
name: srcPkg.name,
version: srcPkg.version,
type: "module",
exports: { ".": { import: "./bundled.js" } },
private: true,
};
writeFileSync(join(destDir, "package.json"), JSON.stringify(destPkg, null, 2));
const srcEntry = join(srcDir, "src", "index.ts");
const builtEntry = join(srcDir, "dist", "index.js");
const entry = existsSync(srcEntry) ? srcEntry : builtEntry;
if (!existsSync(entry)) {
throw new Error(`No entry found for ${pluginId} (looked for src/index.ts and dist/index.js)`);
}
await esbuildBuild({
entryPoints: [entry],
bundle: true,
format: "esm",
platform: "node",
target: "node22",
outfile: join(destDir, "bundled.js"),
external: ["@fusion/core", "@fusion/engine"],
alias: {
"@fusion/plugin-sdk": join(__dirname, "..", "plugin-sdk", "src", "index.ts"),
},
logLevel: "warning",
});
if (withMcpAsset) {
const mcpServerAsset = join(srcDir, "src", "mcp-schema-server.cjs");
if (!existsSync(mcpServerAsset)) {
throw new Error(
`[tsup] Missing required bridge asset for ${pluginId} at ${mcpServerAsset}; expected committed source file mcp-schema-server.cjs.`,
);
}
cpSync(mcpServerAsset, join(destDir, "mcp-schema-server.cjs"));
}
const bundledOutput = join(destDir, "bundled.js");
if (!existsSync(bundledOutput)) {
throw new Error(`[tsup] Missing bundled output for ${pluginId}: expected ${bundledOutput}`);
}
console.log(`Bundled plugin ${pluginId} to dist/plugins/${pluginId}/bundled.js`);
}
function runWorkspaceCommand(command: string, args: string[], cwd: string, timeoutMs = 600_000): Promise<void> {
return new Promise((resolvePromise, reject) => {
const child = spawn(command, args, {
cwd,
stdio: "inherit",
env: process.env,
/*
* FNXC:DesktopPackaging 2026-07-02-15:10:
* On Windows `pnpm` (and other npm bins) resolve to a `.cmd` shim that Node refuses to
* spawn without a shell (ENOENT / EINVAL since CVE-2024-27980). Without shell:true the CLI
* package build failed with `spawn pnpm ENOENT` on Windows at the "building @fusion/desktop"
* step. The command/args here are fixed repo build invocations (no untrusted input), so shell
* quoting is safe.
*/
shell: process.platform === "win32",
});
const timer = setTimeout(() => {
child.kill("SIGTERM");
reject(new Error(`${command} ${args.join(" ")} timed out after ${timeoutMs}ms`));
}, timeoutMs);
child.on("error", (error) => {
clearTimeout(timer);
reject(error);
});
child.on("exit", (code) => {
clearTimeout(timer);
if (code === 0) {
resolvePromise();
return;
}
reject(new Error(`${command} ${args.join(" ")} exited with code ${code ?? "unknown"}`));
});
});
}
async function ensureDesktopRuntimeAssetsBuilt() {
if (existsSync(desktopRuntimeSrc)) {
return;
}
/*
* FNXC:DesktopPackaging 2026-07-01-20:53:
* The published CLI package must contain the desktop runtime it launches. Build the private desktop package during CLI packaging when dist is absent, but keep this strictly in the repository build path — the installed `fusion desktop` command itself must never run pnpm from an operator's cwd.
*/
console.log("Desktop runtime assets missing; building @fusion/desktop before staging CLI package assets...");
await runWorkspaceCommand("pnpm", ["--filter", "@fusion/desktop", "build"], workspaceRoot);
if (!existsSync(desktopRuntimeSrc)) {
throw new Error(`[tsup] Desktop runtime build did not create expected assets at ${desktopRuntimeSrc}`);
}
}
function assertAllStagedBundledPluginsLoadable() {
const missingEntries: string[] = [];
for (const pluginId of ALL_STAGED_BUNDLED_IDS) {
const destDir = join(__dirname, "dist", "plugins", pluginId);
const manifestPath = join(destDir, "manifest.json");
const bundledEntryPath = join(destDir, "bundled.js");
const sourceEntryPath = join(destDir, "src", "index.ts");
if (!existsSync(manifestPath) || (!existsSync(bundledEntryPath) && !existsSync(sourceEntryPath))) {
missingEntries.push(
`${pluginId} (expected manifest.json plus bundled.js or src/index.ts under ${destDir})`,
);
}
}
if (missingEntries.length > 0) {
throw new Error(`[tsup] Missing loadable staged bundled plugin entries:\n${missingEntries.join("\n")}`);
}
}
const pluginSdkEntry = join(__dirname, "..", "plugin-sdk", "src", "index.ts");
const pluginSdkCoreRuntimeShim = join(__dirname, "src", "plugin-sdk-core-runtime-shim.ts");
const cliBuildConfig = {
entry: ["src/bin.ts", "src/extension.ts"],
format: ["esm"],
platform: "node",
target: "node22",
esbuildOptions(options: { conditions?: string[] }) {
options.conditions = [...(options.conditions || []), "source"];
},
noExternal: [/^@fusion\//, /^@fusion-plugin-examples\//],
// 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",
"dockerode",
"ssh2",
"cpu-features",
],
splitting: false,
// Keep clean disabled so the dedicated plugin-sdk tsup config can emit into
// dist/plugin-sdk without being wiped between config executions.
clean: false,
removeNodeProtocol: false,
banner: {
js: 'import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);',
},
onSuccess: async () => {
if (existsSync(desktopRuntimeDest)) {
rmSync(desktopRuntimeDest, { recursive: true, force: true });
}
await ensureDesktopRuntimeAssetsBuilt();
/*
* FNXC:DesktopPackaging 2026-07-01-20:31:
* Published `@runfusion/fusion` desktop launches must resolve Electron runtime assets from the installed package, not from the operator's current directory. Stage the private @fusion/desktop dist output under CLI dist/desktop so npm installs can launch without pnpm workspace discovery or host JSON parsing.
*/
mkdirSync(desktopRuntimeDest, { recursive: true });
cpSync(desktopRuntimeSrc, desktopRuntimeDest, { recursive: true });
console.log("Copied desktop runtime assets to dist/desktop/");
// Stage the vendored pi-claude-cli pi extension into dist/. It can't
// be bundled by esbuild because pi loads extensions as separate files
// at runtime via jiti, so we ship the raw .ts source. This also lets
// us drop @fusion/pi-claude-cli from the published package's
// dependencies — the workspace package is private and would 404 on
// `pnpm install` of @runfusion/fusion otherwise.
if (existsSync(piClaudeCliDest)) {
rmSync(piClaudeCliDest, { recursive: true, force: true });
}
if (existsSync(piClaudeCliSrc)) {
mkdirSync(piClaudeCliDest, { recursive: true });
cpSync(join(piClaudeCliSrc, "index.ts"), join(piClaudeCliDest, "index.ts"));
cpSync(join(piClaudeCliSrc, "src"), join(piClaudeCliDest, "src"), { recursive: true });
writeSanitizedCopiedManifest(join(piClaudeCliSrc, "package.json"), join(piClaudeCliDest, "package.json"));
console.log("Copied pi-claude-cli extension to dist/pi-claude-cli/");
} else {
console.warn(
`WARNING: pi-claude-cli source not found at ${piClaudeCliSrc}; useClaudeCli will not work in the published package.`,
);
}
// Stage the vendored @fusion/droid-cli pi extension into dist/, following
// the same pattern as pi-claude-cli above. The extension ships raw .ts
// source that pi loads via jiti at runtime, so it cannot be bundled by
// esbuild. This lets us drop @fusion/droid-cli from the published
// package's dependencies — the workspace package is private and would 404
// on `pnpm install` of @runfusion/fusion otherwise.
if (existsSync(droidCliDest)) {
rmSync(droidCliDest, { recursive: true, force: true });
}
if (existsSync(droidCliSrc)) {
mkdirSync(droidCliDest, { recursive: true });
cpSync(join(droidCliSrc, "index.ts"), join(droidCliDest, "index.ts"));
cpSync(join(droidCliSrc, "src"), join(droidCliDest, "src"), { recursive: true });
writeSanitizedCopiedManifest(join(droidCliSrc, "package.json"), join(droidCliDest, "package.json"));
console.log("Copied droid-cli extension to dist/droid-cli/");
} else {
console.warn(
`WARNING: droid-cli source not found at ${droidCliSrc}; useDroidCli will not work in the published package.`,
);
}
if (existsSync(llamaCppDest)) {
rmSync(llamaCppDest, { recursive: true, force: true });
}
if (existsSync(llamaCppSrc)) {
mkdirSync(llamaCppDest, { recursive: true });
cpSync(join(llamaCppSrc, "index.ts"), join(llamaCppDest, "index.ts"));
cpSync(join(llamaCppSrc, "src"), join(llamaCppDest, "src"), { recursive: true });
writeSanitizedCopiedManifest(join(llamaCppSrc, "package.json"), join(llamaCppDest, "package.json"));
console.log("Copied pi-llama-cpp extension to dist/pi-llama-cpp/");
} else {
console.warn(
`WARNING: pi-llama-cpp source not found at ${llamaCppSrc}; useLlamaCpp will not work in the published package.`,
);
}
await bundlePluginEntry({
pluginId: "fusion-plugin-dependency-graph",
srcDir: dependencyGraphPluginSrc,
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.`,
);
}
await bundlePluginEntry({
pluginId: "fusion-plugin-roadmap",
srcDir: roadmapPluginSrc,
destDir: roadmapPluginDest,
});
await bundlePluginEntry({
pluginId: "fusion-plugin-compound-engineering",
srcDir: compoundEngineeringPluginSrc,
destDir: compoundEngineeringPluginDest,
});
await bundlePluginEntry({
pluginId: "fusion-plugin-linear-import",
srcDir: linearImportPluginSrc,
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.`,
);
}
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.`,
);
}
// Bundle each runtime plugin into a self-contained ESM file so npm/npx
// installs can load them without the workspace `@fusion/plugin-sdk`.
for (const pluginId of RUNTIME_PLUGIN_IDS) {
await bundlePluginEntry({
pluginId,
srcDir: join(__dirname, "..", "..", "plugins", pluginId),
destDir: join(__dirname, "dist", "plugins", pluginId),
withMcpAsset: RUNTIME_PLUGINS_WITH_MCP_SCHEMA_SERVER.has(pluginId),
});
}
/*
* FNXC:BundledPlugins 2026-06-17-22:15:
* Build output must cover the complete staged plugin surface, including raw-src copied plugins that do not pass through bundlePluginEntry's per-plugin bundled.js assertion. Droid and ACP runtimes are intentionally staged but not auto-installed pending FN-6623, so this checks loadable staged entries rather than BUNDLED_PLUGIN_IDS equality.
*/
assertAllStagedBundledPluginsLoadable();
if (existsSync(dashboardClientDest)) {
rmSync(dashboardClientDest, { recursive: true, force: true });
}
if (existsSync(dashboardClientSrc)) {
cpSync(dashboardClientSrc, dashboardClientDest, { recursive: true });
console.log("Copied dashboard client assets to dist/client/");
return;
}
mkdirSync(dashboardClientDest, { recursive: true });
writeFileSync(join(dashboardClientDest, "index.html"), dashboardClientStub, "utf-8");
console.warn(
`WARNING: Dashboard client assets not found at ${dashboardClientSrc}. Generated minimal stub at ${join(dashboardClientDest, "index.html")}.`,
);
},
};
const pluginSdkBuildConfig = {
entry: { "plugin-sdk/index": pluginSdkEntry },
format: ["esm"],
platform: "node",
target: "node22",
tsconfig: join(__dirname, "..", "plugin-sdk", "tsconfig.json"),
dts: {
/*
* FNXC:PluginSDK 2026-06-13-12:00:
* FN-6409 requires the published @runfusion/fusion/plugin-sdk declaration entry to be self-contained. External plugin authors cannot resolve private @fusion/core types from scaffolded projects, so leaving @fusion/* imports in dist/plugin-sdk/index.d.ts makes tsc fail with TS2307 before ctx parameters can typecheck.
*/
resolve: [/^@fusion\//],
compilerOptions: {
rootDir: join(__dirname, ".."),
baseUrl: ".",
paths: {
"@fusion/core": ["../core/src/index.ts"],
},
removeComments: true,
},
},
noExternal: [/^@fusion\//],
esbuildOptions(options: { alias?: Record<string, string> }) {
options.alias = {
...(options.alias || {}),
"@fusion/core": pluginSdkCoreRuntimeShim,
};
},
clean: false,
outDir: "dist",
};
export default defineConfig([cliBuildConfig, pluginSdkBuildConfig]);