Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 87 additions & 1 deletion packages/shared/src/cli/commands/plugin/create/create.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import {
buildResourceFromType,
parseResourcesJson,
parseResourcesShorthand,
printNextSteps,
} from "./create";
import type { CreateAnswers } from "./types";

describe("create non-interactive helpers", () => {
describe("buildResourceFromType", () => {
Expand Down Expand Up @@ -150,4 +152,88 @@ describe("create non-interactive helpers", () => {
}
});
});

describe("printNextSteps", () => {
let consoleLogSpy: ReturnType<typeof vi.spyOn>;

beforeEach(() => {
consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {});
});

afterEach(() => {
vi.restoreAllMocks();
});

const answers: CreateAnswers = {
placement: "isolated",
targetPath: "plugins/my-plugin",
name: "myPlugin",
displayName: "My Plugin",
description: "Test plugin",
resources: [],
version: "0.1.0",
};

const targetDir = "/home/user/project/plugins/my-plugin";

it("shows pnpm commands when pm is pnpm", () => {
printNextSteps(answers, targetDir, "pnpm");
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining("pnpm install"),
);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining("pnpm run build"),
);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining("pnpm add"),
);
});

it("shows npm commands when pm is npm", () => {
printNextSteps(answers, targetDir, "npm");
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining("npm install"),
);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining("npm run build"),
);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining("npm install"),
);
});

it("shows yarn commands when pm is yarn", () => {
printNextSteps(answers, targetDir, "yarn");
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining("yarn install"),
);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining("yarn run build"),
);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining("yarn add"),
);
});

it("shows bun commands when pm is bun", () => {
printNextSteps(answers, targetDir, "bun");
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining("bun install"),
);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining("bun run build"),
);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining("bun add"),
);
});

it("shows npx for in-repo placement (PM-neutral)", () => {
const inRepoAnswers = { ...answers, placement: "in-repo" as const };
printNextSteps(inRepoAnswers, targetDir, "npm");
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining("npx appkit plugin sync"),
);
});
});
});
45 changes: 35 additions & 10 deletions packages/shared/src/cli/commands/plugin/create/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import process from "node:process";
import { Command, Option } from "commander";

import { PLUGIN_NAME_PATTERN } from "../../../../naming";
import {
detectPackageManager,
PM_COMMANDS,
type PackageManager,
} from "../../../package-manager";
import { promptOneResource } from "./prompt-resource";
import {
DEFAULT_PERMISSION_BY_TYPE,
Expand Down Expand Up @@ -116,7 +121,11 @@ function parseResourcesShorthand(csv: string): SelectedResource[] {
return types.map(buildResourceFromType);
}

function printNextSteps(answers: CreateAnswers, targetDir: string): void {
function printNextSteps(
answers: CreateAnswers,
targetDir: string,
pm: PackageManager,
): void {
const relativePath = path.relative(process.cwd(), targetDir);
const importPath = relativePath.startsWith(".")
? relativePath
Expand All @@ -133,12 +142,16 @@ function printNextSteps(answers: CreateAnswers, targetDir: string): void {
` 2. Run \`npx appkit plugin sync --write\` to update appkit.plugins.json.\n`,
);
} else {
console.log(` 1. cd into the new package and install dependencies:`);
console.log(` cd ${answers.targetPath} && pnpm install`);
console.log(` 2. Build: pnpm build`);
console.log(
` 3. In your app: pnpm add ./${answers.targetPath} @databricks/appkit`,
const installCmd = PM_COMMANDS[pm].install;
const buildCmd = PM_COMMANDS[pm].build;
const addCmd = PM_COMMANDS[pm].add(
`./${answers.targetPath} @databricks/appkit`,
);

console.log(` 1. cd into the new package and install dependencies:`);
console.log(` cd ${answers.targetPath} && ${installCmd}`);
console.log(` 2. Build: ${buildCmd}`);
console.log(` 3. In your app: ${addCmd}`);
console.log(
` 4. Import and register: import { ${exportName} } from "<package-name>";\n`,
);
Expand Down Expand Up @@ -213,12 +226,16 @@ function runNonInteractive(opts: CreateOptions): void {
process.exit(1);
}

scaffoldPlugin(targetDir, answers, { isolated: placement === "isolated" });
const pm = detectPackageManager(process.cwd());
scaffoldPlugin(targetDir, answers, {
isolated: placement === "isolated",
pm,
});

console.log(
`Plugin "${answers.name}" created at ${path.relative(process.cwd(), targetDir)}`,
);
printNextSteps(answers, targetDir);
printNextSteps(answers, targetDir, pm);
}

async function runInteractive(): Promise<void> {
Expand Down Expand Up @@ -403,8 +420,10 @@ async function runInteractive(): Promise<void> {
const s = spinner();
s.start("Writing files…");
try {
const pm = detectPackageManager(process.cwd());
scaffoldPlugin(targetDir, answers, {
isolated: placement === "isolated",
pm,
});
s.stop("Files written.");
} catch (err) {
Expand All @@ -413,7 +432,8 @@ async function runInteractive(): Promise<void> {
}

outro("Plugin created successfully.");
printNextSteps(answers, targetDir);
const pm = detectPackageManager(process.cwd());
printNextSteps(answers, targetDir, pm);
} catch (err) {
console.error(err);
process.exit(1);
Expand Down Expand Up @@ -485,4 +505,9 @@ Examples:
);

/** Exported for testing. */
export { buildResourceFromType, parseResourcesJson, parseResourcesShorthand };
export {
buildResourceFromType,
parseResourcesJson,
parseResourcesShorthand,
printNextSteps,
};
82 changes: 82 additions & 0 deletions packages/shared/src/cli/commands/plugin/create/scaffold.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,88 @@ describe("scaffold", () => {
});
});

describe("README.md package manager commands", () => {
it("generates README with pnpm commands when pm is pnpm", () => {
const tmp = makeTempDir();
tempDirs.push(tmp);
const targetDir = path.join(tmp, "test");

scaffoldPlugin(targetDir, BASE_ANSWERS, { isolated: true, pm: "pnpm" });

const readme = fs.readFileSync(
path.join(targetDir, "README.md"),
"utf-8",
);
expect(readme).toContain(
"pnpm add appkit-plugin-my-plugin @databricks/appkit",
);
});

it("generates README with npm commands when pm is npm", () => {
const tmp = makeTempDir();
tempDirs.push(tmp);
const targetDir = path.join(tmp, "test");

scaffoldPlugin(targetDir, BASE_ANSWERS, { isolated: true, pm: "npm" });

const readme = fs.readFileSync(
path.join(targetDir, "README.md"),
"utf-8",
);
expect(readme).toContain(
"npm install appkit-plugin-my-plugin @databricks/appkit",
);
});

it("generates README with yarn commands when pm is yarn", () => {
const tmp = makeTempDir();
tempDirs.push(tmp);
const targetDir = path.join(tmp, "test");

scaffoldPlugin(targetDir, BASE_ANSWERS, { isolated: true, pm: "yarn" });

const readme = fs.readFileSync(
path.join(targetDir, "README.md"),
"utf-8",
);
expect(readme).toContain(
"yarn add appkit-plugin-my-plugin @databricks/appkit",
);
});

it("generates README with bun commands when pm is bun", () => {
const tmp = makeTempDir();
tempDirs.push(tmp);
const targetDir = path.join(tmp, "test");

scaffoldPlugin(targetDir, BASE_ANSWERS, { isolated: true, pm: "bun" });

const readme = fs.readFileSync(
path.join(targetDir, "README.md"),
"utf-8",
);
expect(readme).toContain(
"bun add appkit-plugin-my-plugin @databricks/appkit",
);
});

it("defaults to pnpm when pm is not provided", () => {
const tmp = makeTempDir();
tempDirs.push(tmp);
const targetDir = path.join(tmp, "test");

scaffoldPlugin(targetDir, BASE_ANSWERS, { isolated: true });

const readme = fs.readFileSync(
path.join(targetDir, "README.md"),
"utf-8",
);
expect(readme).toContain(
"pnpm add appkit-plugin-my-plugin @databricks/appkit",
);
});
});

describe("rollback on failure", () => {
it("cleans up written files when a write fails partway through", () => {
const tmp = makeTempDir();
Expand Down
8 changes: 6 additions & 2 deletions packages/shared/src/cli/commands/plugin/create/scaffold.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fs from "node:fs";
import path from "node:path";

import { PM_COMMANDS, type PackageManager } from "../../../package-manager";
import { humanizeResourceType, MANIFEST_SCHEMA_ID } from "./resource-defaults";
import type { CreateAnswers } from "./types";

Expand Down Expand Up @@ -100,7 +101,7 @@ function rollback(written: string[], targetDir: string): void {
export function scaffoldPlugin(
targetDir: string,
answers: CreateAnswers,
options: { isolated: boolean },
options: { isolated: boolean; pm?: PackageManager },
): void {
fs.mkdirSync(targetDir, { recursive: true });

Expand Down Expand Up @@ -203,14 +204,17 @@ export const ${exportName} = toPlugin(${className});
written,
);

const pm = options.pm ?? "pnpm";
const addCmd = PM_COMMANDS[pm].add(`${packageName} @databricks/appkit`);

const readme = `# ${answers.displayName}

${answers.description}

## Installation

\`\`\`bash
pnpm add ${packageName} @databricks/appkit
${addCmd}
\`\`\`

## Usage
Expand Down
Loading
Loading