Skip to content

Commit 082b0d9

Browse files
feat: added --auto mode (#1046)
## 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.
1 parent e1c008f commit 082b0d9

23 files changed

+404
-218
lines changed

src/bin/help.test.ts

+4
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ describe("logHelpText", () => {
116116
default, an existing npm author, or the currently logged in npm user, or
117117
owner.toLowerCase())",
118118
],
119+
[
120+
"
121+
--auto: Whether to infer all options from files on disk.",
122+
],
119123
[
120124
"
121125
--directory (string): Directory to create the repository in (by default, the same

src/bin/index.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,10 @@ export async function bin(args: string[]) {
6161
logLine(introWarnings[0]);
6262
logLine(introWarnings[1]);
6363

64-
const { mode, options: promptedOptions } = await promptForMode(values.mode);
64+
const { mode, options: promptedOptions } = await promptForMode(
65+
!!values.auto,
66+
values.mode,
67+
);
6568
if (typeof mode !== "string") {
6669
prompts.outro(chalk.red(mode?.message ?? operationMessage("cancelled")));
6770
return 1;

src/bin/promptForMode.test.ts

+18-6
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,20 @@ vi.mock("../shared/cli/lines.js", () => ({
3636
},
3737
}));
3838
describe("promptForMode", () => {
39+
it("returns an error when auto exists and input is not migrate", async () => {
40+
const mode = await promptForMode(true, "create");
41+
42+
expect(mode).toMatchInlineSnapshot(
43+
`
44+
{
45+
"mode": [Error: --auto can only be used with --mode migrate.],
46+
}
47+
`,
48+
);
49+
});
50+
3951
it("returns an error when the input exists and is not a mode", async () => {
40-
const mode = await promptForMode("other");
52+
const mode = await promptForMode(false, "other");
4153

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

54-
const mode = await promptForMode(input);
66+
const mode = await promptForMode(false, input);
5567

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

66-
const actual = await promptForMode(undefined);
78+
const actual = await promptForMode(false, undefined);
6779

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

82-
const actual = await promptForMode(undefined);
94+
const actual = await promptForMode(false, undefined);
8395

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

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

96-
const actual = await promptForMode(undefined);
108+
const actual = await promptForMode(false, undefined);
97109

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

105117
mockReaddir.mockResolvedValueOnce(["file"]);
106118

107-
const actual = await promptForMode(undefined);
119+
const actual = await promptForMode(false, undefined);
108120

109121
expect(actual).toEqual({ mode });
110122
expect(mockSelect).not.toHaveBeenCalled();

src/bin/promptForMode.ts

+7
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,15 @@ export interface PromptedMode {
2424
}
2525

2626
export async function promptForMode(
27+
auto: boolean,
2728
input: boolean | string | undefined,
2829
): Promise<PromptedMode> {
30+
if (auto && input !== "migrate") {
31+
return {
32+
mode: new Error("--auto can only be used with --mode migrate."),
33+
};
34+
}
35+
2936
if (input) {
3037
if (!isMode(input)) {
3138
return {

src/create/createRerunSuggestion.test.ts

-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ const options = {
3030
excludeTests: undefined,
3131
funding: undefined,
3232
keywords: ["abc", "def ghi", "jkl mno pqr"],
33-
logo: undefined,
3433
mode: "create",
3534
owner: "TestOwner",
3635
repository: "test-repository",

src/shared/generateNextSteps.test.ts

-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ const options = {
1212
github: "[email protected]",
1313
1414
},
15-
logo: undefined,
1615
mode: "create",
1716
owner: "TestOwner",
1817
repository: "test-repository",

src/shared/options/args.ts

+5
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ export const allArgOptions = {
1616
docsSection: "optional",
1717
type: "string",
1818
},
19+
auto: {
20+
description: `Whether to infer all options from files on disk.`,
21+
docsSection: "optional",
22+
type: "boolean",
23+
},
1924
base: {
2025
description: `Whether to scaffold the repository with:
2126
• everything: that comes with the template (${chalk.cyanBright.bold(

src/shared/options/getPrefillOrPromptedOption.test.ts

+41-12
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,37 @@ vi.mock("@clack/prompts", () => ({
1313
}));
1414

1515
describe("getPrefillOrPromptedValue", () => {
16-
it("provides no placeholder when one is not provided", async () => {
16+
it("returns the placeholder when auto is true and it exists", async () => {
17+
const value = "Test Value";
18+
19+
const actual = await getPrefillOrPromptedOption(
20+
"field",
21+
true,
22+
"Input message.",
23+
vi.fn().mockResolvedValue(value),
24+
);
25+
26+
expect(actual).toEqual({ error: undefined, value });
27+
});
28+
29+
it("returns an error when auto is true and no placeholder exists", async () => {
30+
const actual = await getPrefillOrPromptedOption(
31+
"field",
32+
true,
33+
"Input message.",
34+
vi.fn().mockResolvedValue(undefined),
35+
);
36+
37+
expect(actual).toEqual({
38+
error: "Could not infer a default value for field.",
39+
value: undefined,
40+
});
41+
});
42+
43+
it("provides no placeholder when one is not provided and auto is false", async () => {
1744
const message = "Test message";
1845

19-
await getPrefillOrPromptedOption(message);
46+
await getPrefillOrPromptedOption("Input message.", false, message);
2047

2148
expect(mockText).toHaveBeenCalledWith({
2249
message,
@@ -25,36 +52,38 @@ describe("getPrefillOrPromptedValue", () => {
2552
});
2653
});
2754

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

32-
await getPrefillOrPromptedOption(
59+
const actual = await getPrefillOrPromptedOption(
60+
"field",
61+
false,
3362
message,
3463
vi.fn().mockResolvedValue(placeholder),
3564
);
3665

37-
expect(mockText).toHaveBeenCalledWith({
38-
message,
39-
placeholder,
40-
validate: expect.any(Function),
66+
expect(actual).toEqual({
67+
error: undefined,
68+
value: placeholder,
4169
});
70+
expect(mockText).not.toHaveBeenCalled();
4271
});
4372

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

47-
await getPrefillOrPromptedOption(message);
76+
await getPrefillOrPromptedOption("Input message.", false, message);
4877

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

5180
expect(validate(message)).toBeUndefined();
5281
});
5382

54-
it("invalidates entered text when it's blank", async () => {
83+
it("invalidates entered text when it's blank and auto is false", async () => {
5584
const message = "";
5685

57-
await getPrefillOrPromptedOption(message);
86+
await getPrefillOrPromptedOption("Input message.", false, message);
5887

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

src/shared/options/getPrefillOrPromptedOption.ts

+27-12
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,33 @@ import * as prompts from "@clack/prompts";
33
import { filterPromptCancel } from "../prompts.js";
44

55
export async function getPrefillOrPromptedOption(
6+
name: string,
7+
auto: boolean,
68
message: string,
7-
getPlaceholder?: () => Promise<string | undefined>,
9+
getDefaultValue?: () => Promise<string | undefined>,
810
) {
9-
return filterPromptCancel(
10-
await prompts.text({
11-
message,
12-
placeholder: await getPlaceholder?.(),
13-
validate: (val) => {
14-
if (val.length === 0) {
15-
return "Please enter a value.";
16-
}
17-
},
18-
}),
19-
);
11+
const defaultValue = await getDefaultValue?.();
12+
13+
if (auto || defaultValue) {
14+
return {
15+
error: defaultValue
16+
? undefined
17+
: `Could not infer a default value for ${name}.`,
18+
value: defaultValue,
19+
};
20+
}
21+
22+
return {
23+
value: filterPromptCancel(
24+
await prompts.text({
25+
message,
26+
placeholder: defaultValue,
27+
validate: (val) => {
28+
if (val.length === 0) {
29+
return "Please enter a value.";
30+
}
31+
},
32+
}),
33+
),
34+
};
2035
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
3+
import { InferredOptions, logInferredOptions } from "./logInferredOptions.js";
4+
5+
function makeProxy<T extends object>(receiver: T): T {
6+
return new Proxy(receiver, {
7+
get: () => makeProxy((input: string) => input),
8+
});
9+
}
10+
11+
vi.mock("chalk", () => ({
12+
default: makeProxy({}),
13+
}));
14+
15+
const mockLogLine = vi.fn();
16+
17+
vi.mock("../cli/lines.js", () => ({
18+
get logLine() {
19+
return mockLogLine;
20+
},
21+
}));
22+
23+
const options = {
24+
description: "Test description.",
25+
email: {
26+
github: "[email protected]",
27+
28+
},
29+
owner: "TestOwner",
30+
repository: "test-repository",
31+
title: "Test Title",
32+
} satisfies InferredOptions;
33+
34+
describe("logInferredOptions", () => {
35+
it("logs the required inferred values when only they exist", () => {
36+
logInferredOptions(options);
37+
38+
expect(mockLogLine.mock.calls).toMatchInlineSnapshot(`
39+
[
40+
[],
41+
[
42+
"--auto inferred the following values:",
43+
],
44+
[
45+
"- description: Test description.",
46+
],
47+
[
48+
"- email-github: [email protected]",
49+
],
50+
[
51+
"- email-npm: [email protected]",
52+
],
53+
[
54+
"- owner: TestOwner",
55+
],
56+
[
57+
"- repository: test-repository",
58+
],
59+
[
60+
"- title: Test Title",
61+
],
62+
]
63+
`);
64+
});
65+
66+
it("logs additional and required inferred values when all they exist", () => {
67+
logInferredOptions({
68+
...options,
69+
guide: {
70+
href: "https://example.com/guide",
71+
title: "Example Guide",
72+
},
73+
logo: {
74+
alt: "Logo text.",
75+
src: "https://example.com/logo",
76+
},
77+
});
78+
79+
expect(mockLogLine.mock.calls).toMatchInlineSnapshot(`
80+
[
81+
[],
82+
[
83+
"--auto inferred the following values:",
84+
],
85+
[
86+
"- description: Test description.",
87+
],
88+
[
89+
"- email-github: [email protected]",
90+
],
91+
[
92+
"- email-npm: [email protected]",
93+
],
94+
[
95+
"- guide: https://example.com/guide",
96+
],
97+
[
98+
"- guide-title: Example Guide",
99+
],
100+
[
101+
"- logo: https://example.com/logo",
102+
],
103+
[
104+
"- logo-alt: Logo text.",
105+
],
106+
[
107+
"- owner: TestOwner",
108+
],
109+
[
110+
"- repository: test-repository",
111+
],
112+
[
113+
"- title: Test Title",
114+
],
115+
]
116+
`);
117+
});
118+
});

0 commit comments

Comments
 (0)