Skip to content

feat: added --auto mode #1046

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Nov 21, 2023
Merged
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
4 changes: 4 additions & 0 deletions src/bin/help.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ describe("logHelpText", () => {
default, an existing npm author, or the currently logged in npm user, or
owner.toLowerCase())",
],
[
"
--auto: Whether to infer all options from files on disk.",
],
[
"
--directory (string): Directory to create the repository in (by default, the same
Expand Down
5 changes: 4 additions & 1 deletion src/bin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ export async function bin(args: string[]) {
logLine(introWarnings[0]);
logLine(introWarnings[1]);

const { mode, options: promptedOptions } = await promptForMode(values.mode);
const { mode, options: promptedOptions } = await promptForMode(
!!values.auto,
values.mode,
);
if (typeof mode !== "string") {
prompts.outro(chalk.red(mode?.message ?? operationMessage("cancelled")));
return 1;
Expand Down
24 changes: 18 additions & 6 deletions src/bin/promptForMode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,20 @@ vi.mock("../shared/cli/lines.js", () => ({
},
}));
describe("promptForMode", () => {
it("returns an error when auto exists and input is not migrate", async () => {
const mode = await promptForMode(true, "create");

expect(mode).toMatchInlineSnapshot(
`
{
"mode": [Error: --auto can only be used with --mode migrate.],
}
`,
);
});

it("returns an error when the input exists and is not a mode", async () => {
const mode = await promptForMode("other");
const mode = await promptForMode(false, "other");

expect(mode).toMatchInlineSnapshot(
`
Expand All @@ -51,7 +63,7 @@ describe("promptForMode", () => {
it("returns the input when it is a mode", async () => {
const input = "create";

const mode = await promptForMode(input);
const mode = await promptForMode(false, input);

expect(mode).toEqual({ mode: input });
});
Expand All @@ -63,7 +75,7 @@ describe("promptForMode", () => {
mockReaddir.mockResolvedValueOnce([]);
mockCwd.mockReturnValueOnce(`/path/to/${directory}`);

const actual = await promptForMode(undefined);
const actual = await promptForMode(false, undefined);

expect(actual).toEqual({
mode: "create",
Expand All @@ -79,7 +91,7 @@ describe("promptForMode", () => {
mockReaddir.mockResolvedValueOnce([]);
mockCwd.mockReturnValueOnce(`/path/to/${directory}`);

const actual = await promptForMode(undefined);
const actual = await promptForMode(false, undefined);

expect(actual).toEqual({
mode: "create",
Expand All @@ -93,7 +105,7 @@ describe("promptForMode", () => {

mockReaddir.mockResolvedValueOnce([".git"]);

const actual = await promptForMode(undefined);
const actual = await promptForMode(false, undefined);

expect(actual).toEqual({ mode });
expect(mockLogLine).not.toHaveBeenCalled();
Expand All @@ -104,7 +116,7 @@ describe("promptForMode", () => {

mockReaddir.mockResolvedValueOnce(["file"]);

const actual = await promptForMode(undefined);
const actual = await promptForMode(false, undefined);

expect(actual).toEqual({ mode });
expect(mockSelect).not.toHaveBeenCalled();
Expand Down
7 changes: 7 additions & 0 deletions src/bin/promptForMode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,15 @@ export interface PromptedMode {
}

export async function promptForMode(
auto: boolean,
input: boolean | string | undefined,
): Promise<PromptedMode> {
if (auto && input !== "migrate") {
return {
mode: new Error("--auto can only be used with --mode migrate."),
};
}

if (input) {
if (!isMode(input)) {
return {
Expand Down
1 change: 0 additions & 1 deletion src/create/createRerunSuggestion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ const options = {
excludeTests: undefined,
funding: undefined,
keywords: ["abc", "def ghi", "jkl mno pqr"],
logo: undefined,
mode: "create",
owner: "TestOwner",
repository: "test-repository",
Expand Down
1 change: 0 additions & 1 deletion src/shared/generateNextSteps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ const options = {
github: "[email protected]",
npm: "[email protected]",
},
logo: undefined,
mode: "create",
owner: "TestOwner",
repository: "test-repository",
Expand Down
5 changes: 5 additions & 0 deletions src/shared/options/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ export const allArgOptions = {
docsSection: "optional",
type: "string",
},
auto: {
description: `Whether to infer all options from files on disk.`,
docsSection: "optional",
type: "boolean",
},
base: {
description: `Whether to scaffold the repository with:
• everything: that comes with the template (${chalk.cyanBright.bold(
Expand Down
53 changes: 41 additions & 12 deletions src/shared/options/getPrefillOrPromptedOption.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,37 @@ vi.mock("@clack/prompts", () => ({
}));

describe("getPrefillOrPromptedValue", () => {
it("provides no placeholder when one is not provided", async () => {
it("returns the placeholder when auto is true and it exists", async () => {
const value = "Test Value";

const actual = await getPrefillOrPromptedOption(
"field",
true,
"Input message.",
vi.fn().mockResolvedValue(value),
);

expect(actual).toEqual({ error: undefined, value });
});

it("returns an error when auto is true and no placeholder exists", async () => {
const actual = await getPrefillOrPromptedOption(
"field",
true,
"Input message.",
vi.fn().mockResolvedValue(undefined),
);

expect(actual).toEqual({
error: "Could not infer a default value for field.",
value: undefined,
});
});

it("provides no placeholder when one is not provided and auto is false", async () => {
const message = "Test message";

await getPrefillOrPromptedOption(message);
await getPrefillOrPromptedOption("Input message.", false, message);

expect(mockText).toHaveBeenCalledWith({
message,
Expand All @@ -25,36 +52,38 @@ describe("getPrefillOrPromptedValue", () => {
});
});

it("provides the placeholder's awaited return when a placeholder function is provided", async () => {
it("provides the placeholder's awaited return when a placeholder function is provided and auto is false", async () => {
const message = "Test message";
const placeholder = "Test placeholder";

await getPrefillOrPromptedOption(
const actual = await getPrefillOrPromptedOption(
"field",
false,
message,
vi.fn().mockResolvedValue(placeholder),
);

expect(mockText).toHaveBeenCalledWith({
message,
placeholder,
validate: expect.any(Function),
expect(actual).toEqual({
error: undefined,
value: placeholder,
});
expect(mockText).not.toHaveBeenCalled();
});

it("validates entered text when it's not blank", async () => {
it("validates entered text when it's not blank and auto is false", async () => {
const message = "Test message";

await getPrefillOrPromptedOption(message);
await getPrefillOrPromptedOption("Input message.", false, message);

const { validate } = (mockText.mock.calls[0] as [Required<TextOptions>])[0];

expect(validate(message)).toBeUndefined();
});

it("invalidates entered text when it's blank", async () => {
it("invalidates entered text when it's blank and auto is false", async () => {
const message = "";

await getPrefillOrPromptedOption(message);
await getPrefillOrPromptedOption("Input message.", false, message);

const { validate } = (mockText.mock.calls[0] as [Required<TextOptions>])[0];

Expand Down
39 changes: 27 additions & 12 deletions src/shared/options/getPrefillOrPromptedOption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,33 @@ import * as prompts from "@clack/prompts";
import { filterPromptCancel } from "../prompts.js";

export async function getPrefillOrPromptedOption(
name: string,
auto: boolean,
message: string,
getPlaceholder?: () => Promise<string | undefined>,
getDefaultValue?: () => Promise<string | undefined>,
) {
return filterPromptCancel(
await prompts.text({
message,
placeholder: await getPlaceholder?.(),
validate: (val) => {
if (val.length === 0) {
return "Please enter a value.";
}
},
}),
);
const defaultValue = await getDefaultValue?.();

if (auto || defaultValue) {
return {
error: defaultValue
? undefined
: `Could not infer a default value for ${name}.`,
value: defaultValue,
};
}

return {
value: filterPromptCancel(
await prompts.text({
message,
placeholder: defaultValue,
validate: (val) => {
if (val.length === 0) {
return "Please enter a value.";
}
},
}),
),
};
}
118 changes: 118 additions & 0 deletions src/shared/options/logInferredOptions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { describe, expect, it, vi } from "vitest";

import { InferredOptions, logInferredOptions } from "./logInferredOptions.js";

function makeProxy<T extends object>(receiver: T): T {
return new Proxy(receiver, {
get: () => makeProxy((input: string) => input),
});
}

vi.mock("chalk", () => ({
default: makeProxy({}),
}));

const mockLogLine = vi.fn();

vi.mock("../cli/lines.js", () => ({
get logLine() {
return mockLogLine;
},
}));

const options = {
description: "Test description.",
email: {
github: "[email protected]",
npm: "[email protected]",
},
owner: "TestOwner",
repository: "test-repository",
title: "Test Title",
} satisfies InferredOptions;

describe("logInferredOptions", () => {
it("logs the required inferred values when only they exist", () => {
logInferredOptions(options);

expect(mockLogLine.mock.calls).toMatchInlineSnapshot(`
[
[],
[
"--auto inferred the following values:",
],
[
"- description: Test description.",
],
[
"- email-github: [email protected]",
],
[
"- email-npm: [email protected]",
],
[
"- owner: TestOwner",
],
[
"- repository: test-repository",
],
[
"- title: Test Title",
],
]
`);
});

it("logs additional and required inferred values when all they exist", () => {
logInferredOptions({
...options,
guide: {
href: "https://example.com/guide",
title: "Example Guide",
},
logo: {
alt: "Logo text.",
src: "https://example.com/logo",
},
});

expect(mockLogLine.mock.calls).toMatchInlineSnapshot(`
[
[],
[
"--auto inferred the following values:",
],
[
"- description: Test description.",
],
[
"- email-github: [email protected]",
],
[
"- email-npm: [email protected]",
],
[
"- guide: https://example.com/guide",
],
[
"- guide-title: Example Guide",
],
[
"- logo: https://example.com/logo",
],
[
"- logo-alt: Logo text.",
],
[
"- owner: TestOwner",
],
[
"- repository: test-repository",
],
[
"- title: Test Title",
],
]
`);
});
});
Loading