diff --git a/.changeset/FN-6435-plugin-scaffold-state.md b/.changeset/FN-6435-plugin-scaffold-state.md new file mode 100644 index 0000000000..ef685d3dd1 --- /dev/null +++ b/.changeset/FN-6435-plugin-scaffold-state.md @@ -0,0 +1,9 @@ +--- +"@runfusion/fusion": patch +--- + +Fix the standalone `fn plugin new` scaffold so generated plugins include the required `state: "installed"` field and build unedited with `pnpm build`. This also lets the documented `fn plugin dev . --once` path complete its pre-load build step instead of failing TypeScript validation for a missing `FusionPlugin.state`. + +Manual end-to-end spot-check for release validation: `npx @runfusion/fusion@ plugin new proof-point-plugin && cd proof-point-plugin && pnpm install && pnpm build && npx @runfusion/fusion@ plugin dev . --once`. + +Registry evidence captured for the original failing release: `npm view @runfusion/fusion@0.43.0 dist.integrity` returned `sha512-kvxicT+e8ulc7FDhBVP9NsgaioZv6NDW81N8cXNS/X8M32Eo3Y33xT6JFW2DrSiFXsJmAaib/GnpQE0nYQYApQ==`. diff --git a/docs/PLUGIN_AUTHORING.md b/docs/PLUGIN_AUTHORING.md index 9d578e203d..3436b106dd 100644 --- a/docs/PLUGIN_AUTHORING.md +++ b/docs/PLUGIN_AUTHORING.md @@ -1547,6 +1547,7 @@ const traits: PluginTraitContribution[] = [ export default definePlugin({ manifest: { id: "my-plugin", name: "My Plugin", version: "1.0.0" }, + state: "installed", hooks: {}, traits, }); @@ -1666,6 +1667,7 @@ const workflowExtensions: WorkflowExtensionContribution[] = [ export default definePlugin({ manifest: { id: "my-plugin", name: "My Plugin", version: "1.0.0" }, + state: "installed", workflowExtensions, }); ``` diff --git a/packages/cli/src/__tests__/plugin-scaffold.test.ts b/packages/cli/src/__tests__/plugin-scaffold.test.ts index 3e16a3849e..b4fcbceef9 100644 --- a/packages/cli/src/__tests__/plugin-scaffold.test.ts +++ b/packages/cli/src/__tests__/plugin-scaffold.test.ts @@ -5,6 +5,10 @@ import { tmpdir } from "node:os"; import { validatePluginManifest } from "@fusion/plugin-sdk"; import { resolvePluginEntryFile } from "../commands/plugin.js"; import { runPluginCreate, runPluginNew } from "../commands/plugin-scaffold.js"; +import { + standaloneScaffoldPluginFixture, + verifyStandaloneScaffoldPluginFixture, +} from "../type-guards/plugin-scaffold-fusion-plugin.js"; describe("plugin-scaffold", () => { const tmpBase = join(tmpdir(), `fn-scaffold-${Date.now()}-${Math.random().toString(36).slice(2)}`); @@ -16,6 +20,13 @@ describe("plugin-scaffold", () => { ]; const caretRangePattern = /^\^\d+\.\d+\.\d+$/; + function expectStandaloneIndexInvariants(indexContents: string): void { + expect(indexContents).toContain('import { definePlugin } from "@runfusion/fusion/plugin-sdk";'); + expect(indexContents).toContain('state: "installed"'); + expect(indexContents).not.toContain("@fusion/"); + expect(indexContents).not.toContain("workspace:"); + } + beforeEach(() => { mkdirSync(tmpBase, { recursive: true }); }); @@ -90,8 +101,7 @@ describe("plugin-scaffold", () => { const readmeContents = readFileSync(join(outputDir, "README.md"), "utf-8"); expect(packageContents).not.toContain("@fusion/"); expect(packageContents).not.toContain("workspace:"); - expect(indexContents).not.toContain("@fusion/"); - expect(indexContents).not.toContain("workspace:"); + expectStandaloneIndexInvariants(indexContents); expect(readmeContents).toContain("fn plugin dev ."); expect(readmeContents).toContain("fn plugin dev . --once"); @@ -118,6 +128,14 @@ describe("plugin-scaffold", () => { }; expect(packageJson.name).toBe("@acme/fusion-plugin-scoped-plugin"); expect(Object.keys(packageJson.devDependencies)).toEqual(standaloneDevDependencyKeys); + + const indexContents = readFileSync(join(outputDir, "src/index.ts"), "utf-8"); + expectStandaloneIndexInvariants(indexContents); + }); + + it("keeps the standalone scaffold shape assignable to FusionPlugin", () => { + // The runtime identity assertion is intentionally small; the regression value is the tsc guard in the imported fixture. + expect(verifyStandaloneScaffoldPluginFixture()).toBe(standaloneScaffoldPluginFixture); }); it("rejects invalid plugin names", async () => { diff --git a/packages/cli/src/commands/plugin-scaffold.ts b/packages/cli/src/commands/plugin-scaffold.ts index 3d102f9c84..4dc61ae4ce 100644 --- a/packages/cli/src/commands/plugin-scaffold.ts +++ b/packages/cli/src/commands/plugin-scaffold.ts @@ -257,6 +257,10 @@ export default definePlugin({ `; } +/** + * FNXC:PluginScaffold 2026-06-14-01:40: + * The published FusionPlugin type requires state: PluginState, so standalone `fn plugin new` output must emit `state: "installed"` and stay in sync with the workspace scaffold plus SDK type surface to build unedited. + */ function generateStandaloneIndexTs(name: string): string { const titleCase = toTitleCase(name); return `import { definePlugin } from "@runfusion/fusion/plugin-sdk"; @@ -268,6 +272,7 @@ export default definePlugin({ version: "0.1.0", description: "A standalone Fusion plugin", }, + state: "installed", hooks: { onLoad: async (ctx) => { ctx.logger.info("${titleCase} plugin loaded"); diff --git a/packages/cli/src/type-guards/plugin-scaffold-fusion-plugin.ts b/packages/cli/src/type-guards/plugin-scaffold-fusion-plugin.ts new file mode 100644 index 0000000000..831642c716 --- /dev/null +++ b/packages/cli/src/type-guards/plugin-scaffold-fusion-plugin.ts @@ -0,0 +1,30 @@ +import type { FusionPlugin } from "@fusion/core"; + +function defineScaffoldPluginFixture(plugin: FusionPlugin): FusionPlugin { + return plugin; +} + +/** + * FNXC:PluginScaffold 2026-06-14-01:48: + * This fixture mirrors the standalone scaffold's emitted plugin object so the CLI build fails when the SDK-backed FusionPlugin contract adds a required field that `fn plugin new` must emit. + */ +const standaloneScaffoldPluginFixture: FusionPlugin = { + manifest: { + id: "hello-plugin", + name: "Hello Plugin", + version: "0.1.0", + description: "A standalone Fusion plugin", + }, + state: "installed", + hooks: { + onLoad: async (ctx) => { + ctx.logger.info("Hello Plugin plugin loaded"); + }, + }, +}; + +export function verifyStandaloneScaffoldPluginFixture(): FusionPlugin { + return defineScaffoldPluginFixture(standaloneScaffoldPluginFixture); +} + +export { standaloneScaffoldPluginFixture };