Skip to content

Commit

Permalink
feat: add rerun suggestions to the CLI (#121)
Browse files Browse the repository at this point in the history
* feat: add rerun suggestions to the CLI

* Revert reorder
  • Loading branch information
JoshuaKGoldberg authored Jan 6, 2025
1 parent 9934655 commit 1d82f53
Show file tree
Hide file tree
Showing 18 changed files with 326 additions and 126 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,10 @@ import { describe, expect, it, vi } from "vitest";

import { tryImportTemplatePreset } from "./tryImportTemplatePreset.js";

const mockCancel = Symbol("");
const mockIsCancel = (value: unknown) => value === mockCancel;
const mockCancel = Symbol("cancel");

vi.mock("@clack/prompts", () => ({
get isCancel() {
return mockIsCancel;
},
isCancel: (value: unknown) => value === mockCancel,
}));

const mockPromptForPreset = vi.fn();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { describe, expect, it } from "vitest";

import { assertOptionsForInitialize } from "./assertOptionsForInitialize.js";
import { asCreationOptions } from "./asCreationOptions.js";

const owner = "TestOwner";
const repository = "test-repository";

describe("assertOptionsForInitialize", () => {
describe("asCreationOptions", () => {
it("throws an error when the options are missing owner", () => {
const options = { repository };

expect(() => {
assertOptionsForInitialize(options);
asCreationOptions(options);
}).toThrowError(
`To run with --mode initialize, the Template must have a --owner Option of type string.`,
);
Expand All @@ -20,7 +20,7 @@ describe("assertOptionsForInitialize", () => {
const options = { owner: 123, repository };

expect(() => {
assertOptionsForInitialize(options);
asCreationOptions(options);
}).toThrowError(
`To run with --mode initialize, the Template must have a --owner Option of type string.`,
);
Expand All @@ -30,7 +30,7 @@ describe("assertOptionsForInitialize", () => {
const options = { owner };

expect(() => {
assertOptionsForInitialize(options);
asCreationOptions(options);
}).toThrowError(
`To run with --mode initialize, the Template must have a --repository Option of type string.`,
);
Expand All @@ -40,7 +40,7 @@ describe("assertOptionsForInitialize", () => {
const options = { owner, repository: 123 };

expect(() => {
assertOptionsForInitialize(options);
asCreationOptions(options);
}).toThrowError(
`To run with --mode initialize, the Template must have a --repository Option of type string.`,
);
Expand All @@ -49,8 +49,6 @@ describe("assertOptionsForInitialize", () => {
it("does not throw an error when the options have owner and repository", () => {
const options = { owner, repository };

expect(() => {
assertOptionsForInitialize(options);
}).not.toThrow();
expect(asCreationOptions(options)).toBe(options);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ export interface CreationOptions {
repository: string;
}

export function assertOptionsForInitialize(
options: object,
): asserts options is CreationOptions {
export function asCreationOptions(options: object): CreationOptions {
for (const key of ["owner", "repository"]) {
if (typeof (options as Record<string, unknown>)[key] !== "string") {
throw new Error(
`To run with --mode initialize, the Template must have a --${key} Option of type string.`,
);
}
}

return options as CreationOptions;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Octokit } from "octokit";

import { RepositoryTemplate } from "../../types/bases.js";
import { CreationOptions } from "./assertOptionsForInitialize.js";
import { CreationOptions } from "./asCreationOptions.js";

export async function createRepositoryOnGitHub(
{ owner, repository }: CreationOptions,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SystemRunner } from "../../types/system.js";
import { CreationOptions } from "./assertOptionsForInitialize.js";
import { CreationOptions } from "./asCreationOptions.js";

export async function createTrackingBranches(
{ owner, repository }: CreationOptions,
Expand Down
98 changes: 70 additions & 28 deletions packages/create/src/cli/initialize/runModeInitialize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,14 @@ import { CLIMessage } from "../messages.js";
import { CLIStatus } from "../status.js";
import { runModeInitialize } from "./runModeInitialize.js";

const mockCancel = Symbol("");
const mockIsCancel = (value: unknown) => value === mockCancel;
const mockCancel = Symbol("cancel");
const mockLog = {
error: vi.fn(),
message: vi.fn(),
};

vi.mock("@clack/prompts", () => ({
get isCancel() {
return mockIsCancel;
},
isCancel: (value: unknown) => value === mockCancel,
get log() {
return mockLog;
},
Expand All @@ -32,6 +29,14 @@ vi.mock("../loggers/logInitializeHelpText.js", () => ({
},
}));

const mockLogRerunSuggestion = vi.fn();

vi.mock("../loggers/logRerunSuggestion.js", () => ({
get logRerunSuggestion() {
return mockLogRerunSuggestion;
},
}));

const mockRunPreset = vi.fn();

vi.mock("../../runners/runPreset.js", () => ({
Expand Down Expand Up @@ -122,6 +127,12 @@ const template = base.createTemplate({
presets: [],
});

const args = ["create-my-app"];

const promptedOptions = {
abc: "def",
};

describe("runModeInitialize", () => {
it("logs help text when from is undefined", async () => {
const actual = await runModeInitialize({
Expand Down Expand Up @@ -154,7 +165,7 @@ describe("runModeInitialize", () => {
mockTryImportTemplatePreset.mockResolvedValueOnce(new Error("Oh no!"));

const actual = await runModeInitialize({
args: ["node", "create", "my-app"],
args,
display,
from: "create-my-app",
});
Expand All @@ -169,7 +180,7 @@ describe("runModeInitialize", () => {
mockTryImportTemplatePreset.mockResolvedValueOnce(mockCancel);

const actual = await runModeInitialize({
args: ["node", "create", "my-app"],
args,
display,
from: "create-my-app",
});
Expand All @@ -182,7 +193,7 @@ describe("runModeInitialize", () => {
mockPromptForDirectory.mockResolvedValueOnce(mockCancel);

const actual = await runModeInitialize({
args: ["node", "create", "my-app"],
args,
display,
from: "create-my-app",
});
Expand All @@ -193,15 +204,20 @@ describe("runModeInitialize", () => {
it("returns the cancellation when promptForBaseOptions is cancelled", async () => {
mockTryImportTemplatePreset.mockResolvedValueOnce({ preset, template });
mockPromptForDirectory.mockResolvedValueOnce(".");
mockPromptForBaseOptions.mockResolvedValueOnce(mockCancel);

mockPromptForBaseOptions.mockResolvedValueOnce({
cancelled: true,
prompted: promptedOptions,
});

const actual = await runModeInitialize({
args: ["node", "create", "my-app"],
args,
display,
from: "create-my-app",
});

expect(actual).toEqual({ status: CLIStatus.Cancelled });
expect(mockLogRerunSuggestion).toHaveBeenCalledWith(args, promptedOptions);
});

it("returns an error when applyArgsToSettings returns an error", async () => {
Expand All @@ -210,13 +226,16 @@ describe("runModeInitialize", () => {
mockTryImportTemplatePreset.mockResolvedValueOnce({ preset, template });
mockPromptForDirectory.mockResolvedValueOnce(".");
mockPromptForBaseOptions.mockResolvedValueOnce({
owner: "TestOwner",
repository: "test-repository",
completed: {
owner: "TestOwner",
repository: "test-repository",
},
prompted: promptedOptions,
});
mockApplyArgsToSettings.mockReturnValueOnce(new Error(message));

const actual = await runModeInitialize({
args: ["node", "create", "my-app"],
args,
display,
from: "create-my-app",
});
Expand All @@ -225,6 +244,8 @@ describe("runModeInitialize", () => {
outro: message,
status: CLIStatus.Error,
});

expect(mockLogRerunSuggestion).toHaveBeenCalledWith(args, promptedOptions);
});

it("runs createRepositoryOnGitHub when offline is falsy", async () => {
Expand All @@ -234,15 +255,18 @@ describe("runModeInitialize", () => {
mockTryImportTemplatePreset.mockResolvedValueOnce({ preset, template });
mockPromptForDirectory.mockResolvedValueOnce(directory);
mockPromptForBaseOptions.mockResolvedValueOnce({
owner: "TestOwner",
repository: "test-repository",
completed: {
owner: "TestOwner",
repository: "test-repository",
},
prompted: promptedOptions,
});
mockRunPreset.mockResolvedValueOnce({
suggestions,
});

await runModeInitialize({
args: ["node", "create", "my-app"],
args,
display,
from: "create-my-app",
});
Expand Down Expand Up @@ -272,15 +296,18 @@ describe("runModeInitialize", () => {
mockTryImportTemplatePreset.mockResolvedValueOnce({ preset, template });
mockPromptForDirectory.mockResolvedValueOnce(directory);
mockPromptForBaseOptions.mockResolvedValueOnce({
owner: "TestOwner",
repository: "test-repository",
completed: {
owner: "TestOwner",
repository: "test-repository",
},
prompted: promptedOptions,
});
mockRunPreset.mockResolvedValueOnce({
suggestions,
});

await runModeInitialize({
args: ["node", "create", "my-app"],
args,
display,
from: "create-my-app",
offline: true,
Expand Down Expand Up @@ -310,13 +337,16 @@ describe("runModeInitialize", () => {
mockTryImportTemplatePreset.mockResolvedValueOnce({ preset, template });
mockPromptForDirectory.mockResolvedValueOnce(directory);
mockPromptForBaseOptions.mockResolvedValueOnce({
owner: "TestOwner",
repository: "test-repository",
completed: {
owner: "TestOwner",
repository: "test-repository",
},
prompted: promptedOptions,
});
mockRunPreset.mockRejectedValueOnce(new Error("Oh no!"));

const actual = await runModeInitialize({
args: ["node", "create", "my-app"],
args,
display,
from: "create-my-app",
});
Expand All @@ -325,6 +355,8 @@ describe("runModeInitialize", () => {
outro: `Leaving changes to the local directory on disk. 👋`,
status: CLIStatus.Error,
});

expect(mockLogRerunSuggestion).toHaveBeenCalledWith(args, promptedOptions);
});

it("returns a CLI success and makes an absolute directory relative when importing and running the preset succeeds", async () => {
Expand All @@ -334,15 +366,18 @@ describe("runModeInitialize", () => {
mockTryImportTemplatePreset.mockResolvedValueOnce({ preset, template });
mockPromptForDirectory.mockResolvedValueOnce(directory);
mockPromptForBaseOptions.mockResolvedValueOnce({
owner: "TestOwner",
repository: "test-repository",
completed: {
owner: "TestOwner",
repository: "test-repository",
},
prompted: promptedOptions,
});
mockRunPreset.mockResolvedValueOnce({
suggestions,
});

const actual = await runModeInitialize({
args: ["node", "create", "my-app"],
args,
display,
from: "create-my-app",
});
Expand All @@ -352,6 +387,8 @@ describe("runModeInitialize", () => {
status: CLIStatus.Success,
suggestions,
});

expect(mockLogRerunSuggestion).toHaveBeenCalledWith(args, promptedOptions);
expect(mockRunPreset).toHaveBeenCalledWith(preset, expect.any(Object));
});

Expand All @@ -362,15 +399,18 @@ describe("runModeInitialize", () => {
mockTryImportTemplatePreset.mockResolvedValueOnce({ preset, template });
mockPromptForDirectory.mockResolvedValueOnce(directory);
mockPromptForBaseOptions.mockResolvedValueOnce({
owner: "TestOwner",
repository: "test-repository",
completed: {
owner: "TestOwner",
repository: "test-repository",
},
prompted: promptedOptions,
});
mockRunPreset.mockResolvedValueOnce({
suggestions,
});

const actual = await runModeInitialize({
args: ["node", "create", "my-app"],
args,
display,
from: "create-my-app",
});
Expand All @@ -380,6 +420,8 @@ describe("runModeInitialize", () => {
status: CLIStatus.Success,
suggestions,
});

expect(mockLogRerunSuggestion).toHaveBeenCalledWith(args, promptedOptions);
expect(mockRunPreset).toHaveBeenCalledWith(preset, expect.any(Object));
});
});
Loading

0 comments on commit 1d82f53

Please sign in to comment.