Skip to content

Commit

Permalink
feat: added --auto mode (#1046)
Browse files Browse the repository at this point in the history
## PR Checklist

- [x] Addresses an existing open issue: fixes #1042
- [x] That issue was marked as [`status: accepting
prs`](https://github.com/JoshuaKGoldberg/create-typescript-app/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+accepting+prs%22)
- [x] Steps in
[CONTRIBUTING.md](https://github.com/JoshuaKGoldberg/create-typescript-app/blob/main/.github/CONTRIBUTING.md)
were taken

## Overview

Adds extra logic to `getPrefillOrPromptedOption` so that it can return
immediately without prompting if needed.
  • Loading branch information
JoshuaKGoldberg committed Nov 21, 2023
1 parent e1c008f commit 082b0d9
Show file tree
Hide file tree
Showing 23 changed files with 404 additions and 218 deletions.
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

0 comments on commit 082b0d9

Please sign in to comment.