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:
16
.changeset/FN-6351-plugin-scaffold-devdeps.md
Normal file
16
.changeset/FN-6351-plugin-scaffold-devdeps.md
Normal 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
|
||||||
|
```
|
||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user