FN-6351: add plugin scaffold dev toolchain dependencies

Ensure standalone plugin scaffolds declare the tools required by their generated config and scripts.

- Add @types/node, TypeScript, and Vitest dev dependencies to generated standalone plugin package manifests.
- Cover scaffold dependency ranges, script/tool alignment, scoped plugin names, and tsconfig types in plugin scaffold tests.
- Add a patch changeset documenting the scaffold dependency fix.

Files changed:
 .changeset/FN-6351-plugin-scaffold-devdeps.md      | 16 +++++++++++++++
 packages/cli/src/__tests__/plugin-scaffold.test.ts | 24 ++++++++++++++++++++--
 packages/cli/src/commands/plugin-scaffold.ts       |  6 ++++++
 3 files changed, 44 insertions(+), 2 deletions(-)

Fusion-Task-Id: FN-6351

Fusion-Task-Lineage: 4134d7cb-b493-49a7-abe1-154a875aec0e
This commit is contained in:
gsxdsm
2026-06-13 04:44:51 -07:00
parent 2a37ee4e59
commit 30e747bebc
3 changed files with 44 additions and 2 deletions

View File

@@ -0,0 +1,16 @@
---
"@runfusion/fusion": patch
---
Standalone plugin scaffolds now declare the dev toolchain they generate scripts and config for: `@types/node`, `vitest`, and `typescript`. This lets projects created with `fn plugin new` install, build, test, and load through `fn plugin dev . --once` via the documented external-author path without relying on transitive or hoisted dependencies.
Manual spot-check for release validation:
```sh
npx @runfusion/fusion@latest plugin new proof-point-plugin
cd proof-point-plugin
pnpm install
pnpm build
pnpm test
fn plugin dev . --once
```

View File

@@ -8,6 +8,13 @@ import { runPluginCreate, runPluginNew } from "../commands/plugin-scaffold.js";
describe("plugin-scaffold", () => { describe("plugin-scaffold", () => {
const tmpBase = join(tmpdir(), `fn-scaffold-${Date.now()}-${Math.random().toString(36).slice(2)}`); const tmpBase = join(tmpdir(), `fn-scaffold-${Date.now()}-${Math.random().toString(36).slice(2)}`);
const standaloneDevDependencyKeys = [
"@runfusion/fusion",
"@types/node",
"typescript",
"vitest",
];
const caretRangePattern = /^\^\d+\.\d+\.\d+$/;
beforeEach(() => { beforeEach(() => {
mkdirSync(tmpBase, { recursive: true }); mkdirSync(tmpBase, { recursive: true });
@@ -64,6 +71,7 @@ describe("plugin-scaffold", () => {
private?: boolean; private?: boolean;
keywords: string[]; keywords: string[];
exports: { ".": { types: string; import: string } }; exports: { ".": { types: string; import: string } };
scripts: { build: string; test: string };
devDependencies: Record<string, string>; devDependencies: Record<string, string>;
}; };
@@ -72,8 +80,10 @@ describe("plugin-scaffold", () => {
expect(packageJson.private).toBeUndefined(); expect(packageJson.private).toBeUndefined();
expect(packageJson.exports["."].types).toBe("./dist/index.d.ts"); expect(packageJson.exports["."].types).toBe("./dist/index.d.ts");
expect(packageJson.exports["."].import).toBe("./dist/index.js"); expect(packageJson.exports["."].import).toBe("./dist/index.js");
expect(Object.keys(packageJson.devDependencies)).toEqual(["@runfusion/fusion"]); expect(Object.keys(packageJson.devDependencies)).toEqual(standaloneDevDependencyKeys);
expect(packageJson.devDependencies["@runfusion/fusion"]).toMatch(/^\^\d+\.\d+\.\d+$/); for (const dependencyName of standaloneDevDependencyKeys) {
expect(packageJson.devDependencies[dependencyName]).toMatch(caretRangePattern);
}
const packageContents = readFileSync(join(outputDir, "package.json"), "utf-8"); const packageContents = readFileSync(join(outputDir, "package.json"), "utf-8");
const indexContents = readFileSync(join(outputDir, "src/index.ts"), "utf-8"); const indexContents = readFileSync(join(outputDir, "src/index.ts"), "utf-8");
@@ -87,8 +97,16 @@ describe("plugin-scaffold", () => {
const tsconfig = JSON.parse(readFileSync(join(outputDir, "tsconfig.json"), "utf-8")) as { const tsconfig = JSON.parse(readFileSync(join(outputDir, "tsconfig.json"), "utf-8")) as {
extends?: string; extends?: string;
compilerOptions: { types?: string[] };
}; };
expect(tsconfig.extends).toBeUndefined(); expect(tsconfig.extends).toBeUndefined();
for (const typeName of tsconfig.compilerOptions.types ?? []) {
expect(packageJson.devDependencies[`@types/${typeName}`]).toBeDefined();
}
expect(packageJson.scripts.test.split(/\s+/)[0]).toBe("vitest");
expect(packageJson.devDependencies.vitest).toBeDefined();
expect(packageJson.scripts.build.split(/\s+/)[0]).toBe("tsc");
expect(packageJson.devDependencies.typescript).toBeDefined();
}); });
it("supports scoped package names", async () => { it("supports scoped package names", async () => {
@@ -96,8 +114,10 @@ describe("plugin-scaffold", () => {
await runPluginNew("scoped-plugin", { output: outputDir, scope: "acme" }); await runPluginNew("scoped-plugin", { output: outputDir, scope: "acme" });
const packageJson = JSON.parse(readFileSync(join(outputDir, "package.json"), "utf-8")) as { const packageJson = JSON.parse(readFileSync(join(outputDir, "package.json"), "utf-8")) as {
name: string; name: string;
devDependencies: Record<string, string>;
}; };
expect(packageJson.name).toBe("@acme/fusion-plugin-scoped-plugin"); expect(packageJson.name).toBe("@acme/fusion-plugin-scoped-plugin");
expect(Object.keys(packageJson.devDependencies)).toEqual(standaloneDevDependencyKeys);
}); });
it("rejects invalid plugin names", async () => { it("rejects invalid plugin names", async () => {

View File

@@ -12,6 +12,9 @@ import { fileURLToPath } from "node:url";
// Valid plugin name pattern: kebab-case // Valid plugin name pattern: kebab-case
const PLUGIN_NAME_REGEX = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/; const PLUGIN_NAME_REGEX = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
const DEFAULT_RUNFUSION_VERSION = "0.39.0"; const DEFAULT_RUNFUSION_VERSION = "0.39.0";
const SCAFFOLD_TYPES_NODE_VERSION = "^22.0.0";
const SCAFFOLD_VITEST_VERSION = "^4.1.0";
const SCAFFOLD_TYPESCRIPT_VERSION = "^5.7.0";
/** /**
* Convert a kebab-case string to Title Case * Convert a kebab-case string to Title Case
@@ -167,6 +170,9 @@ function generateStandalonePackageJson(name: string, scope?: string): string {
}, },
devDependencies: { devDependencies: {
"@runfusion/fusion": resolveFusionCaretVersion(), "@runfusion/fusion": resolveFusionCaretVersion(),
"@types/node": SCAFFOLD_TYPES_NODE_VERSION,
typescript: SCAFFOLD_TYPESCRIPT_VERSION,
vitest: SCAFFOLD_VITEST_VERSION,
}, },
}, },
null, null,