FN-5842: publish plugin SDK subpath export

Expose the plugin SDK as a published CLI subpath while preserving existing workspace behavior.

- add a prepack transform helper that injects the ./plugin-sdk and ./package.json exports only in packed manifests
- split tsup config into dedicated CLI and plugin-sdk builds so dist/plugin-sdk JS and DTS are emitted with @fusion deps inlined
- add coverage for definePlugin/validatePluginManifest behavior and manifest/build export guarantees
- document the new published @runfusion/fusion/plugin-sdk surface for plugin authors

Files changed:
 docs/agents.md                                     |  2 +
 packages/cli/scripts/prepare-publish-manifest.mjs  | 61 +++++++++++++++-------
 packages/cli/src/__tests__/plugin-sdk-export.test.ts| 50 ++++++++++++++++++
 packages/cli/tsup.config.ts                        | 31 +++++++++--
 4 files changed, 121 insertions(+), 23 deletions(-)

Fusion-Task-Id: FN-5842

Fusion-Task-Lineage: b6b481ae-aaac-46c0-a6f7-8cc1e9eed1ce
This commit is contained in:
gsxdsm
2026-06-01 16:09:35 -07:00
parent a312acd754
commit af6e5b5eaf
4 changed files with 122 additions and 24 deletions

View File

@@ -1,21 +1,9 @@
/* global process, URL, console */
import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
import { pathToFileURL } from "node:url";
const mode = process.argv[2];
const packageJsonPath = new URL("../package.json", import.meta.url);
const backupPath = new URL("../package.json.pack-backup", import.meta.url);
if (mode === "prepack") {
if (existsSync(backupPath)) {
// Clean up stale backup from interrupted runs.
unlinkSync(backupPath);
}
const original = readFileSync(packageJsonPath, "utf8");
writeFileSync(backupPath, original, "utf8");
const pkg = JSON.parse(original);
export function applyPrepackTransform(pkg) {
const devDependencies = { ...(pkg.devDependencies || {}) };
delete devDependencies["@fusion/core"];
delete devDependencies["@fusion/dashboard"];
@@ -24,12 +12,42 @@ if (mode === "prepack") {
delete devDependencies["@fusion/pi-llama-cpp"];
delete devDependencies["@fusion-plugin-examples/roadmap"];
pkg.devDependencies = devDependencies;
writeFileSync(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8");
process.exit(0);
return {
...pkg,
devDependencies,
// Inject exports only for the packed manifest so workspace/dev resolution
// remains unchanged (postpack restore reverts this file to original state).
exports: {
...(pkg.exports || {}),
"./plugin-sdk": {
types: "./dist/plugin-sdk/index.d.ts",
import: "./dist/plugin-sdk/index.js",
},
"./package.json": "./package.json",
},
};
}
if (mode === "postpack") {
function run() {
const mode = process.argv[2];
const packageJsonPath = new URL("../package.json", import.meta.url);
const backupPath = new URL("../package.json.pack-backup", import.meta.url);
if (mode === "prepack") {
if (existsSync(backupPath)) {
unlinkSync(backupPath);
}
const original = readFileSync(packageJsonPath, "utf8");
writeFileSync(backupPath, original, "utf8");
const pkg = JSON.parse(original);
const transformed = applyPrepackTransform(pkg);
writeFileSync(packageJsonPath, `${JSON.stringify(transformed, null, 2)}\n`, "utf8");
process.exit(0);
}
if (mode === "postpack") {
if (!existsSync(backupPath)) {
process.exit(0);
}
@@ -38,7 +56,12 @@ if (mode === "postpack") {
writeFileSync(packageJsonPath, backup, "utf8");
unlinkSync(backupPath);
process.exit(0);
}
console.error("Usage: node ./scripts/prepare-publish-manifest.mjs <prepack|postpack>");
process.exit(1);
}
console.error("Usage: node ./scripts/prepare-publish-manifest.mjs <prepack|postpack>");
process.exit(1);
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
run();
}

View File

@@ -0,0 +1,50 @@
import { describe, it, expect } from "vitest";
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { definePlugin, validatePluginManifest } from "@fusion/plugin-sdk";
import { applyPrepackTransform } from "../../scripts/prepare-publish-manifest.mjs";
const workspaceRoot = join(__dirname, "..", "..", "..", "..");
describe("plugin-sdk export surface", () => {
it("keeps definePlugin as identity and validates manifests", () => {
const plugin = { manifest: { id: "demo-plugin", name: "Demo", version: "1.0.0" } } as any;
expect(definePlugin(plugin)).toBe(plugin);
expect(validatePluginManifest(plugin.manifest)).toEqual({ valid: true, errors: [] });
expect(validatePluginManifest({ id: "Bad_ID", name: "", version: "nope" }).valid).toBe(false);
});
it("injects plugin-sdk subpath export into prepack manifest", () => {
const pkgPath = join(workspaceRoot, "packages", "cli", "package.json");
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
const transformed = applyPrepackTransform(pkg);
expect(transformed.exports["./plugin-sdk"]).toEqual({
types: "./dist/plugin-sdk/index.d.ts",
import: "./dist/plugin-sdk/index.js",
});
expect(transformed.exports["./package.json"]).toBe("./package.json");
expect(transformed.bin).toEqual(pkg.bin);
expect(transformed.pi).toEqual(pkg.pi);
});
it("declares plugin-sdk tsup build entry with dts and fusion inlining", () => {
const tsupPath = join(workspaceRoot, "packages", "cli", "tsup.config.ts");
const tsupRaw = readFileSync(tsupPath, "utf-8");
expect(tsupRaw).toContain('"plugin-sdk/index"');
expect(tsupRaw).toContain('"..", "plugin-sdk", "src", "index.ts"');
expect(tsupRaw).toContain("dts:");
expect(tsupRaw).toContain("/^@fusion\\//");
});
it("has no @fusion runtime specifiers in built plugin-sdk artifact when present", () => {
const distPath = join(workspaceRoot, "packages", "cli", "dist", "plugin-sdk", "index.js");
if (!existsSync(distPath)) {
return;
}
const built = readFileSync(distPath, "utf-8");
expect(built.includes("@fusion/")).toBe(false);
});
});

View File

@@ -126,12 +126,14 @@ async function bundlePluginEntry({ pluginId, srcDir, destDir, withMcpAsset = fal
console.log(`Bundled plugin ${pluginId} to dist/plugins/${pluginId}/bundled.js`);
}
export default defineConfig({
const pluginSdkEntry = join(__dirname, "..", "plugin-sdk", "src", "index.ts");
const cliBuildConfig = {
entry: ["src/bin.ts", "src/extension.ts"],
format: ["esm"],
platform: "node",
target: "node22",
esbuildOptions(options) {
esbuildOptions(options: { conditions?: string[] }) {
options.conditions = [...(options.conditions || []), "source"];
},
noExternal: [/^@fusion\//, /^@fusion-plugin-examples\//],
@@ -146,7 +148,9 @@ export default defineConfig({
"cpu-features",
],
splitting: false,
clean: true,
// 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);',
@@ -293,4 +297,23 @@ export default defineConfig({
`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: {
resolve: true,
compilerOptions: {
rootDir: join(__dirname, ".."),
},
},
noExternal: [/^@fusion\//],
clean: false,
outDir: "dist",
};
export default defineConfig([cliBuildConfig, pluginSdkBuildConfig]);