From 082b0d906cfa0f2237e81c1aef7e7460fac2a75b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Tue, 21 Nov 2023 21:23:22 +0100 Subject: [PATCH] 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. --- src/bin/help.test.ts | 4 + src/bin/index.ts | 5 +- src/bin/promptForMode.test.ts | 24 +++- src/bin/promptForMode.ts | 7 ++ src/create/createRerunSuggestion.test.ts | 1 - src/shared/generateNextSteps.test.ts | 1 - src/shared/options/args.ts | 5 + .../getPrefillOrPromptedOption.test.ts | 53 ++++++-- .../options/getPrefillOrPromptedOption.ts | 39 ++++-- src/shared/options/logInferredOptions.test.ts | 118 ++++++++++++++++++ src/shared/options/logInferredOptions.ts | 31 +++++ src/shared/options/optionsSchema.ts | 1 + src/shared/options/readOptions.test.ts | 115 ++++++++++------- src/shared/options/readOptions.ts | 107 +++++++++++----- src/shared/types.ts | 2 +- src/steps/finalizeDependencies.test.ts | 22 ---- src/steps/updateLocalFiles.test.ts | 21 ---- .../writeReadme/generateTopContent.test.ts | 22 ---- src/steps/writeReadme/index.test.ts | 19 --- .../writing/creation/createESLintRC.test.ts | 1 - .../dotGitHub/createDevelopment.test.ts | 1 - .../dotGitHub/createWorkflows.test.ts | 3 - .../writing/creation/writePackageJson.test.ts | 20 --- 23 files changed, 404 insertions(+), 218 deletions(-) create mode 100644 src/shared/options/logInferredOptions.test.ts create mode 100644 src/shared/options/logInferredOptions.ts diff --git a/src/bin/help.test.ts b/src/bin/help.test.ts index b509d5e0c..c8b74e367 100644 --- a/src/bin/help.test.ts +++ b/src/bin/help.test.ts @@ -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 diff --git a/src/bin/index.ts b/src/bin/index.ts index 4e77604fa..d5823d358 100644 --- a/src/bin/index.ts +++ b/src/bin/index.ts @@ -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; diff --git a/src/bin/promptForMode.test.ts b/src/bin/promptForMode.test.ts index d63ae4b3b..f508c6d14 100644 --- a/src/bin/promptForMode.test.ts +++ b/src/bin/promptForMode.test.ts @@ -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( ` @@ -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 }); }); @@ -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", @@ -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", @@ -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(); @@ -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(); diff --git a/src/bin/promptForMode.ts b/src/bin/promptForMode.ts index 4ccaae7d6..72d3f398d 100644 --- a/src/bin/promptForMode.ts +++ b/src/bin/promptForMode.ts @@ -24,8 +24,15 @@ export interface PromptedMode { } export async function promptForMode( + auto: boolean, input: boolean | string | undefined, ): Promise { + if (auto && input !== "migrate") { + return { + mode: new Error("--auto can only be used with --mode migrate."), + }; + } + if (input) { if (!isMode(input)) { return { diff --git a/src/create/createRerunSuggestion.test.ts b/src/create/createRerunSuggestion.test.ts index a3e828918..c332b6ccb 100644 --- a/src/create/createRerunSuggestion.test.ts +++ b/src/create/createRerunSuggestion.test.ts @@ -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", diff --git a/src/shared/generateNextSteps.test.ts b/src/shared/generateNextSteps.test.ts index 26b697aa6..88c83812a 100644 --- a/src/shared/generateNextSteps.test.ts +++ b/src/shared/generateNextSteps.test.ts @@ -12,7 +12,6 @@ const options = { github: "github@email.com", npm: "npm@email.com", }, - logo: undefined, mode: "create", owner: "TestOwner", repository: "test-repository", diff --git a/src/shared/options/args.ts b/src/shared/options/args.ts index 9409ae588..0e9f71613 100644 --- a/src/shared/options/args.ts +++ b/src/shared/options/args.ts @@ -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( diff --git a/src/shared/options/getPrefillOrPromptedOption.test.ts b/src/shared/options/getPrefillOrPromptedOption.test.ts index 07e7dd3c7..ec68f5f6d 100644 --- a/src/shared/options/getPrefillOrPromptedOption.test.ts +++ b/src/shared/options/getPrefillOrPromptedOption.test.ts @@ -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, @@ -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])[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])[0]; diff --git a/src/shared/options/getPrefillOrPromptedOption.ts b/src/shared/options/getPrefillOrPromptedOption.ts index e0c5c6d30..bb56f0db4 100644 --- a/src/shared/options/getPrefillOrPromptedOption.ts +++ b/src/shared/options/getPrefillOrPromptedOption.ts @@ -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, + getDefaultValue?: () => Promise, ) { - 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."; + } + }, + }), + ), + }; } diff --git a/src/shared/options/logInferredOptions.test.ts b/src/shared/options/logInferredOptions.test.ts new file mode 100644 index 000000000..f3a8ec9e2 --- /dev/null +++ b/src/shared/options/logInferredOptions.test.ts @@ -0,0 +1,118 @@ +import { describe, expect, it, vi } from "vitest"; + +import { InferredOptions, logInferredOptions } from "./logInferredOptions.js"; + +function makeProxy(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: "github@email.com", + npm: "npm@email.com", + }, + 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: github@email.com", + ], + [ + "- email-npm: github@email.com", + ], + [ + "- 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: github@email.com", + ], + [ + "- email-npm: github@email.com", + ], + [ + "- 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", + ], + ] + `); + }); +}); diff --git a/src/shared/options/logInferredOptions.ts b/src/shared/options/logInferredOptions.ts new file mode 100644 index 000000000..dc0bacd67 --- /dev/null +++ b/src/shared/options/logInferredOptions.ts @@ -0,0 +1,31 @@ +import chalk from "chalk"; + +import { logLine } from "../cli/lines.js"; +import { Options } from "../types.js"; + +export type InferredOptions = Pick< + Options, + "description" | "email" | "guide" | "logo" | "owner" | "repository" | "title" +>; + +export function logInferredOptions(augmentedOptions: InferredOptions) { + logLine(); + logLine(chalk.gray("--auto inferred the following values:")); + logLine(chalk.gray(`- description: ${augmentedOptions.description}`)); + logLine(chalk.gray(`- email-github: ${augmentedOptions.email.github}`)); + logLine(chalk.gray(`- email-npm: ${augmentedOptions.email.github}`)); + + if (augmentedOptions.guide) { + logLine(chalk.gray(`- guide: ${augmentedOptions.guide.href}`)); + logLine(chalk.gray(`- guide-title: ${augmentedOptions.guide.title}`)); + } + + if (augmentedOptions.logo) { + logLine(chalk.gray(`- logo: ${augmentedOptions.logo.src}`)); + logLine(chalk.gray(`- logo-alt: ${augmentedOptions.logo.alt}`)); + } + + logLine(chalk.gray(`- owner: ${augmentedOptions.owner}`)); + logLine(chalk.gray(`- repository: ${augmentedOptions.repository}`)); + logLine(chalk.gray(`- title: ${augmentedOptions.title}`)); +} diff --git a/src/shared/options/optionsSchema.ts b/src/shared/options/optionsSchema.ts index c55d85030..444609236 100644 --- a/src/shared/options/optionsSchema.ts +++ b/src/shared/options/optionsSchema.ts @@ -3,6 +3,7 @@ import { z } from "zod"; export const optionsSchemaShape = { access: z.union([z.literal("public"), z.literal("restricted")]).optional(), author: z.string().optional(), + auto: z.boolean().optional(), base: z .union([ z.literal("common"), diff --git a/src/shared/options/readOptions.test.ts b/src/shared/options/readOptions.test.ts index 97cbaddce..7e01c66d3 100644 --- a/src/shared/options/readOptions.test.ts +++ b/src/shared/options/readOptions.test.ts @@ -8,6 +8,7 @@ import { readOptions } from "./readOptions.js"; const emptyOptions = { access: undefined, author: undefined, + auto: false, base: undefined, description: undefined, directory: undefined, @@ -136,7 +137,9 @@ describe("readOptions", () => { it("returns a cancellation when an email redundancy is detected", async () => { const error = "Too many emails!"; mockDetectEmailRedundancy.mockReturnValue(error); - mockGetPrefillOrPromptedOption.mockImplementation(() => undefined); + mockGetPrefillOrPromptedOption.mockImplementation(() => ({ + value: undefined, + })); expect(await readOptions([], "create")).toStrictEqual({ cancelled: true, @@ -149,10 +152,13 @@ describe("readOptions", () => { it("returns a cancellation when the owner prompt is cancelled", async () => { mockDetectEmailRedundancy.mockReturnValue(false); - mockGetPrefillOrPromptedOption.mockImplementation(() => undefined); + mockGetPrefillOrPromptedOption.mockImplementation(() => ({ + value: undefined, + })); expect(await readOptions([], "create")).toStrictEqual({ cancelled: true, + error: undefined, options: { ...emptyOptions, }, @@ -162,11 +168,12 @@ describe("readOptions", () => { it("returns a cancellation when the repository prompt is cancelled", async () => { mockDetectEmailRedundancy.mockReturnValue(false); mockGetPrefillOrPromptedOption - .mockImplementationOnce(() => "MockOwner") - .mockImplementation(() => undefined); + .mockImplementationOnce(() => ({ value: "MockOwner" })) + .mockImplementation(() => ({ value: undefined })); expect(await readOptions([], "create")).toStrictEqual({ cancelled: true, + error: undefined, options: { ...emptyOptions, owner: "MockOwner", @@ -177,13 +184,14 @@ describe("readOptions", () => { it("returns a cancellation when ensureRepositoryPrompt does not return a repository", async () => { mockDetectEmailRedundancy.mockReturnValue(false); mockGetPrefillOrPromptedOption - .mockImplementationOnce(() => "MockOwner") - .mockImplementationOnce(() => "MockRepository") - .mockImplementation(() => undefined); + .mockImplementationOnce(() => ({ value: "MockOwner" })) + .mockImplementationOnce(() => ({ value: "MockRepository" })) + .mockImplementation(() => ({ value: undefined })); mockEnsureRepositoryExists.mockResolvedValue({}); expect(await readOptions([], "create")).toStrictEqual({ cancelled: true, + error: undefined, options: { ...emptyOptions, owner: "MockOwner", @@ -195,9 +203,9 @@ describe("readOptions", () => { it("returns a cancellation when the description prompt is cancelled", async () => { mockDetectEmailRedundancy.mockReturnValue(false); mockGetPrefillOrPromptedOption - .mockImplementationOnce(() => "MockOwner") - .mockImplementationOnce(() => "MockRepository") - .mockImplementation(() => undefined); + .mockImplementationOnce(() => ({ value: "MockOwner" })) + .mockImplementationOnce(() => ({ value: "MockRepository" })) + .mockImplementation(() => ({ value: undefined })); mockEnsureRepositoryExists.mockResolvedValue({ github: mockOptions.github, repository: mockOptions.repository, @@ -205,6 +213,7 @@ describe("readOptions", () => { expect(await readOptions([], "create")).toStrictEqual({ cancelled: true, + error: undefined, options: { ...emptyOptions, owner: "MockOwner", @@ -216,10 +225,10 @@ describe("readOptions", () => { it("returns a cancellation when the title prompt is cancelled", async () => { mockDetectEmailRedundancy.mockReturnValue(false); mockGetPrefillOrPromptedOption - .mockImplementationOnce(() => "MockOwner") - .mockImplementationOnce(() => "MockRepository") - .mockImplementationOnce(() => "Mock description.") - .mockImplementation(() => undefined); + .mockImplementationOnce(() => ({ value: "MockOwner" })) + .mockImplementationOnce(() => ({ value: "MockRepository" })) + .mockImplementationOnce(() => ({ value: "Mock description." })) + .mockImplementation(() => ({ value: undefined })); mockEnsureRepositoryExists.mockResolvedValue({ github: mockOptions.github, repository: mockOptions.repository, @@ -227,6 +236,7 @@ describe("readOptions", () => { expect(await readOptions([], "create")).toStrictEqual({ cancelled: true, + error: undefined, options: { ...emptyOptions, description: "Mock description.", @@ -239,11 +249,11 @@ describe("readOptions", () => { it("returns a cancellation when the guide title prompt is cancelled", async () => { mockDetectEmailRedundancy.mockReturnValue(false); mockGetPrefillOrPromptedOption - .mockImplementationOnce(() => "MockOwner") - .mockImplementationOnce(() => "MockRepository") - .mockImplementationOnce(() => "Mock description.") - .mockImplementationOnce(() => "Mock Title") - .mockImplementation(() => undefined); + .mockImplementationOnce(() => ({ value: "MockOwner" })) + .mockImplementationOnce(() => ({ value: "MockRepository" })) + .mockImplementationOnce(() => ({ value: "Mock description." })) + .mockImplementationOnce(() => ({ value: "Mock Title" })) + .mockImplementation(() => ({ value: undefined })); mockEnsureRepositoryExists.mockResolvedValue({ github: mockOptions.github, repository: mockOptions.repository, @@ -253,6 +263,7 @@ describe("readOptions", () => { await readOptions(["--guide", "https://example.com"], "create"), ).toStrictEqual({ cancelled: true, + error: undefined, options: { ...emptyOptions, description: "Mock description.", @@ -267,11 +278,11 @@ describe("readOptions", () => { it("returns a cancellation when the guide alt prompt is cancelled", async () => { mockDetectEmailRedundancy.mockReturnValue(false); mockGetPrefillOrPromptedOption - .mockImplementationOnce(() => "MockOwner") - .mockImplementationOnce(() => "MockRepository") - .mockImplementationOnce(() => "Mock description.") - .mockImplementationOnce(() => "Mock Title") - .mockImplementation(() => undefined); + .mockImplementationOnce(() => ({ value: "MockOwner" })) + .mockImplementationOnce(() => ({ value: "MockRepository" })) + .mockImplementationOnce(() => ({ value: "Mock description." })) + .mockImplementationOnce(() => ({ value: "Mock Title" })) + .mockImplementation(() => ({ value: undefined })); mockEnsureRepositoryExists.mockResolvedValue({ github: mockOptions.github, repository: mockOptions.repository, @@ -281,6 +292,7 @@ describe("readOptions", () => { await readOptions(["--guide", "https://example.com"], "create"), ).toStrictEqual({ cancelled: true, + error: undefined, options: { ...emptyOptions, description: "Mock description.", @@ -295,10 +307,10 @@ describe("readOptions", () => { it("returns a cancellation when the logo alt prompt is cancelled", async () => { mockDetectEmailRedundancy.mockReturnValue(false); mockGetPrefillOrPromptedOption - .mockImplementationOnce(() => "MockOwner") - .mockImplementationOnce(() => "MockRepository") - .mockImplementationOnce(() => "Mock description.") - .mockImplementation(() => undefined); + .mockImplementationOnce(() => ({ value: "MockOwner" })) + .mockImplementationOnce(() => ({ value: "MockRepository" })) + .mockImplementationOnce(() => ({ value: "Mock description." })) + .mockImplementation(() => ({ value: undefined })); mockEnsureRepositoryExists.mockResolvedValue({ github: mockOptions.github, repository: mockOptions.repository, @@ -306,6 +318,7 @@ describe("readOptions", () => { expect(await readOptions(["--logo", "logo.svg"], "create")).toStrictEqual({ cancelled: true, + error: undefined, options: { ...emptyOptions, description: "Mock description.", @@ -319,11 +332,11 @@ describe("readOptions", () => { it("returns a cancellation when the email prompt is cancelled", async () => { mockDetectEmailRedundancy.mockReturnValue(false); mockGetPrefillOrPromptedOption - .mockImplementationOnce(() => "MockOwner") - .mockImplementationOnce(() => "MockRepository") - .mockImplementationOnce(() => "Mock description.") - .mockImplementationOnce(() => "Mock title.") - .mockImplementation(() => undefined); + .mockImplementationOnce(() => ({ value: "MockOwner" })) + .mockImplementationOnce(() => ({ value: "MockRepository" })) + .mockImplementationOnce(() => ({ value: "Mock description." })) + .mockImplementationOnce(() => ({ value: "Mock title." })) + .mockImplementation(() => ({ value: undefined })); mockEnsureRepositoryExists.mockResolvedValue({ github: mockOptions.github, repository: mockOptions.repository, @@ -331,6 +344,7 @@ describe("readOptions", () => { expect(await readOptions([], "create")).toStrictEqual({ cancelled: true, + error: undefined, options: { ...emptyOptions, description: "Mock description.", @@ -344,11 +358,11 @@ describe("readOptions", () => { it("returns a cancellation when augmentOptionsWithExcludes returns undefined", async () => { mockDetectEmailRedundancy.mockReturnValue(false); mockGetPrefillOrPromptedOption - .mockImplementationOnce(() => "MockOwner") - .mockImplementationOnce(() => "MockRepository") - .mockImplementationOnce(() => "Mock description.") - .mockImplementationOnce(() => "Mock title.") - .mockImplementation(() => undefined); + .mockImplementationOnce(() => ({ value: "MockOwner" })) + .mockImplementationOnce(() => ({ value: "MockRepository" })) + .mockImplementationOnce(() => ({ value: "Mock description." })) + .mockImplementationOnce(() => ({ value: "Mock title." })) + .mockImplementation(() => ({ value: undefined })); mockEnsureRepositoryExists.mockResolvedValue({ github: mockOptions.github, repository: mockOptions.repository, @@ -357,6 +371,7 @@ describe("readOptions", () => { expect(await readOptions([], "create")).toStrictEqual({ cancelled: true, + error: undefined, options: { ...emptyOptions, description: "Mock description.", @@ -372,7 +387,9 @@ describe("readOptions", () => { ...emptyOptions, ...mockOptions, }); - mockGetPrefillOrPromptedOption.mockImplementation(() => "mock"); + mockGetPrefillOrPromptedOption.mockImplementation(() => ({ + value: "mock", + })); mockEnsureRepositoryExists.mockResolvedValue({ github: mockOptions.github, repository: mockOptions.repository, @@ -395,7 +412,9 @@ describe("readOptions", () => { ...emptyOptions, ...mockOptions, }); - mockGetPrefillOrPromptedOption.mockImplementation(() => "mock"); + mockGetPrefillOrPromptedOption.mockImplementation(() => ({ + value: "mock", + })); mockEnsureRepositoryExists.mockResolvedValue({ github: mockOptions.github, repository: mockOptions.repository, @@ -425,7 +444,9 @@ describe("readOptions", () => { it("returns cancelled options when augmentOptionsWithExcludes returns undefined", async () => { mockAugmentOptionsWithExcludes.mockResolvedValue(undefined); - mockGetPrefillOrPromptedOption.mockImplementation(() => "mock"); + mockGetPrefillOrPromptedOption.mockImplementation(() => ({ + value: "mock", + })); expect( await readOptions(["--base", mockOptions.base], "create"), @@ -453,7 +474,9 @@ describe("readOptions", () => { github: mockOptions.github, repository: mockOptions.repository, }); - mockGetPrefillOrPromptedOption.mockImplementation(() => "mock"); + mockGetPrefillOrPromptedOption.mockImplementation(() => ({ + value: "mock", + })); expect( await readOptions(["--base", mockOptions.base], "create"), @@ -477,7 +500,9 @@ describe("readOptions", () => { github: mockOptions.github, repository: mockOptions.repository, }); - mockGetPrefillOrPromptedOption.mockImplementation(() => "mock"); + mockGetPrefillOrPromptedOption.mockImplementation(() => ({ + value: "mock", + })); expect( await readOptions( @@ -499,7 +524,9 @@ describe("readOptions", () => { ...mockOptions, ...options, })); - mockGetPrefillOrPromptedOption.mockImplementation(() => "mock"); + mockGetPrefillOrPromptedOption.mockImplementation(() => ({ + value: "mock", + })); mockEnsureRepositoryExists.mockResolvedValue({ github: mockOptions.github, repository: mockOptions.repository, diff --git a/src/shared/options/readOptions.ts b/src/shared/options/readOptions.ts index eed8d7947..cfb510ac7 100644 --- a/src/shared/options/readOptions.ts +++ b/src/shared/options/readOptions.ts @@ -13,6 +13,7 @@ import { ensureRepositoryExists } from "./ensureRepositoryExists.js"; import { getBase } from "./getBase.js"; import { GitHub, getGitHub } from "./getGitHub.js"; import { getPrefillOrPromptedOption } from "./getPrefillOrPromptedOption.js"; +import { logInferredOptions } from "./logInferredOptions.js"; import { optionsSchema } from "./optionsSchema.js"; export interface GitHubAndOptions { @@ -52,6 +53,7 @@ export async function readOptions( const mappedOptions = { access: values.access, author: values.author, + auto: !!values.auto, base: values.base, description: values.description, directory: values.directory, @@ -119,24 +121,36 @@ export async function readOptions( const options = optionsParseResult.data; - options.owner ??= await getPrefillOrPromptedOption( + const ownerOption = await getPrefillOrPromptedOption( + "owner", + !!mappedOptions.auto, "What organization or user will the repository be under?", defaults.owner, ); + + options.owner ??= ownerOption.value; + if (!options.owner) { return { cancelled: true, + error: ownerOption.error, options, }; } - options.repository ??= await getPrefillOrPromptedOption( + const repositoryOption = await getPrefillOrPromptedOption( + "repository", + !!mappedOptions.auto, "What will the kebab-case name of the repository be?", defaults.repository, ); + + options.repository ??= repositoryOption.value; + if (!options.repository) { return { cancelled: true, + error: repositoryOption.error, options, }; } @@ -151,66 +165,93 @@ export async function readOptions( repository: options.repository, }, ); + if (!repository) { - return { cancelled: true, options }; + return { cancelled: true, error: repositoryOption.error, options }; } - options.description ??= await getPrefillOrPromptedOption( + const descriptionOption = await getPrefillOrPromptedOption( + "description", + !!mappedOptions.auto, "How would you describe the new package?", async () => (await defaults.description()) ?? "A very lovely package. Hooray!", ); + + options.description ??= descriptionOption.value; + if (!options.description) { - return { cancelled: true, options }; + return { cancelled: true, error: descriptionOption.error, options }; } - options.title ??= await getPrefillOrPromptedOption( + const titleOption = await getPrefillOrPromptedOption( + "title", + !!mappedOptions.auto, "What will the Title Case title of the repository be?", async () => (await defaults.title()) ?? titleCase(repository).replaceAll("-", " "), ); + + options.title ??= titleOption.value; + if (!options.title) { - return { cancelled: true, options }; + return { cancelled: true, error: titleOption.error, options }; } let guide: OptionsGuide | undefined; if (options.guide) { - const title = - options.guideTitle ?? - (await getPrefillOrPromptedOption( + if (options.guideTitle) { + guide = { href: options.guide, title: options.guideTitle }; + } else { + const titleOption = await getPrefillOrPromptedOption( + "getPrefillOrPromptedOption", + !!mappedOptions.auto, "What is the title text for the guide?", - )); - if (!title) { - return { cancelled: true, options }; - } + ); + + if (!titleOption.value) { + return { cancelled: true, error: titleOption.error, options }; + } - guide = { href: options.guide, title }; + guide = { href: options.guide, title: titleOption.value }; + } } let logo: OptionsLogo | undefined; if (options.logo) { - const alt = - options.logoAlt ?? - (await getPrefillOrPromptedOption( + if (options.logoAlt) { + logo = { alt: options.logoAlt, src: options.logo }; + } else { + const logoAltOption = await getPrefillOrPromptedOption( + "getPrefillOrPromptedOption", + !!mappedOptions.auto, "What is the alt text (non-visual description) of the logo?", - )); - if (!alt) { - return { cancelled: true, options }; - } + ); + + if (!logoAltOption.value) { + return { cancelled: true, error: logoAltOption.error, options }; + } - logo = { alt, src: options.logo }; + logo = { alt: logoAltOption.value, src: options.logo }; + } } - const email = - options.email ?? - (await defaults.email()) ?? - (await getPrefillOrPromptedOption( - "What email should be used in package.json and .md files?", - )); + let email = options.email ?? (await defaults.email()); + if (!email) { - return { cancelled: true, options }; + const emailOption = await getPrefillOrPromptedOption( + "email", + !!mappedOptions.auto, + "What email should be used in package.json and .md files?", + ); + + if (!emailOption.value) { + return { cancelled: true, error: emailOption.error, options }; + } + + email = { github: emailOption.value, npm: emailOption.value }; } const augmentedOptions = await augmentOptionsWithExcludes({ @@ -220,7 +261,7 @@ export async function readOptions( description: options.description, directory: options.directory ?? promptedOptions.directory ?? options.repository, - email: typeof email === "string" ? { github: email, npm: email } : email, + email, funding: options.funding ?? (await defaults.funding()), guide, logo, @@ -237,6 +278,10 @@ export async function readOptions( }; } + if (options.auto) { + logInferredOptions(augmentedOptions); + } + return { cancelled: false, github, diff --git a/src/shared/types.ts b/src/shared/types.ts index b54dc69aa..a7f33b78c 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -73,7 +73,7 @@ export interface Options { funding?: string; guide?: OptionsGuide; keywords?: string[]; - logo: OptionsLogo | undefined; + logo?: OptionsLogo; mode: Mode; offline?: boolean; owner: string; diff --git a/src/steps/finalizeDependencies.test.ts b/src/steps/finalizeDependencies.test.ts index d48386445..0b3d46b77 100644 --- a/src/steps/finalizeDependencies.test.ts +++ b/src/steps/finalizeDependencies.test.ts @@ -18,7 +18,6 @@ vi.mock("../shared/packages.js", () => ({ const options = { access: "public", - author: undefined, base: "everything", description: "Stub description.", directory: ".", @@ -26,30 +25,9 @@ const options = { github: "github@email.com", npm: "npm@email.com", }, - excludeAllContributors: undefined, - excludeCompliance: undefined, - excludeLintJson: undefined, - excludeLintKnip: undefined, - excludeLintMd: undefined, - excludeLintPackageJson: undefined, - excludeLintPackages: undefined, - excludeLintPerfectionist: undefined, - excludeLintSpelling: undefined, - excludeLintYml: undefined, - excludeReleases: undefined, - excludeRenovate: undefined, - excludeTests: undefined, - funding: undefined, - logo: undefined, mode: "create", - offline: false, owner: "StubOwner", repository: "stub-repository", - skipGitHubApi: false, - skipInstall: undefined, - skipRemoval: undefined, - skipRestore: undefined, - skipUninstall: undefined, title: "Stub Title", } satisfies Options; diff --git a/src/steps/updateLocalFiles.test.ts b/src/steps/updateLocalFiles.test.ts index 6fae33a68..d6955a8dc 100644 --- a/src/steps/updateLocalFiles.test.ts +++ b/src/steps/updateLocalFiles.test.ts @@ -21,7 +21,6 @@ vi.mock("../shared/readFileSafeAsJson.js", () => ({ const options = { access: "public", - author: undefined, base: "everything", description: "Stub description.", directory: ".", @@ -29,30 +28,10 @@ const options = { github: "github@email.com", npm: "npm@email.com", }, - excludeAllContributors: undefined, - excludeCompliance: undefined, - excludeLintJson: undefined, - excludeLintKnip: undefined, - excludeLintMd: undefined, - excludeLintPackageJson: undefined, - excludeLintPackages: undefined, - excludeLintPerfectionist: undefined, - excludeLintSpelling: undefined, - excludeLintYml: undefined, - excludeReleases: undefined, - excludeRenovate: undefined, - excludeTests: undefined, - funding: undefined, - logo: undefined, mode: "create", offline: true, owner: "StubOwner", repository: "stub-repository", - skipGitHubApi: false, - skipInstall: undefined, - skipRemoval: undefined, - skipRestore: undefined, - skipUninstall: undefined, title: "Stub Title", } satisfies Options; diff --git a/src/steps/writeReadme/generateTopContent.test.ts b/src/steps/writeReadme/generateTopContent.test.ts index a16512b2c..6f44787e1 100644 --- a/src/steps/writeReadme/generateTopContent.test.ts +++ b/src/steps/writeReadme/generateTopContent.test.ts @@ -5,37 +5,15 @@ import { generateTopContent } from "./generateTopContent.js"; const optionsBase = { access: "public", - author: undefined, - base: undefined, description: "", directory: ".", email: { github: "github@email.com", npm: "npm@email.com", }, - excludeAllContributors: undefined, - excludeCompliance: undefined, - excludeLintJson: undefined, - excludeLintKnip: undefined, - excludeLintMd: undefined, - excludeLintPackageJson: undefined, - excludeLintPackages: undefined, - excludeLintPerfectionist: undefined, - excludeLintSpelling: undefined, - excludeLintYml: undefined, - excludeReleases: undefined, - excludeRenovate: undefined, - excludeTests: undefined, - funding: undefined, - logo: undefined, mode: "create", owner: "", repository: "", - skipGitHubApi: false, - skipInstall: undefined, - skipRemoval: undefined, - skipRestore: undefined, - skipUninstall: undefined, title: "", } satisfies Options; diff --git a/src/steps/writeReadme/index.test.ts b/src/steps/writeReadme/index.test.ts index d07c301e0..9534e5a2a 100644 --- a/src/steps/writeReadme/index.test.ts +++ b/src/steps/writeReadme/index.test.ts @@ -33,29 +33,10 @@ const options = { github: "github@email.com", npm: "npm@email.com", }, - excludeAllContributors: undefined, - excludeCompliance: undefined, - excludeLintJson: undefined, - excludeLintKnip: undefined, - excludeLintMd: undefined, - excludeLintPackageJson: undefined, - excludeLintPackages: undefined, - excludeLintPerfectionist: undefined, - excludeLintSpelling: undefined, - excludeLintYml: undefined, - excludeReleases: undefined, - excludeRenovate: undefined, - excludeTests: undefined, funding: "TestFunding", - logo: undefined, mode: "create", owner: "TestOwner", repository: "test-repository", - skipGitHubApi: false, - skipInstall: true, - skipRemoval: false, - skipRestore: false, - skipUninstall: false, title: "Test Title", } satisfies Options; diff --git a/src/steps/writing/creation/createESLintRC.test.ts b/src/steps/writing/creation/createESLintRC.test.ts index d32b61ba3..f33fccfd1 100644 --- a/src/steps/writing/creation/createESLintRC.test.ts +++ b/src/steps/writing/creation/createESLintRC.test.ts @@ -37,7 +37,6 @@ function fakeOptions(getExcludeValue: (exclusionName: string) => boolean) { "excludeTests", ].map((key) => [key, getExcludeValue(key)]), ), - logo: undefined, mode: "create", owner: "TestOwner", repository: "test-repository", diff --git a/src/steps/writing/creation/dotGitHub/createDevelopment.test.ts b/src/steps/writing/creation/dotGitHub/createDevelopment.test.ts index 07a8f1e58..586949458 100644 --- a/src/steps/writing/creation/dotGitHub/createDevelopment.test.ts +++ b/src/steps/writing/creation/dotGitHub/createDevelopment.test.ts @@ -13,7 +13,6 @@ const options = { github: "github@email.com", npm: "npm@email.com", }, - logo: undefined, mode: "create", owner: "TestOwner", repository: "test-repository", diff --git a/src/steps/writing/creation/dotGitHub/createWorkflows.test.ts b/src/steps/writing/creation/dotGitHub/createWorkflows.test.ts index d7f108e3c..a6d163e3b 100644 --- a/src/steps/writing/creation/dotGitHub/createWorkflows.test.ts +++ b/src/steps/writing/creation/dotGitHub/createWorkflows.test.ts @@ -6,7 +6,6 @@ import { createWorkflows } from "./createWorkflows.js"; const createOptions = (exclude: boolean) => ({ access: "public", - author: undefined, base: "everything", description: "Test description.", directory: ".", @@ -27,8 +26,6 @@ const createOptions = (exclude: boolean) => excludeReleases: exclude, excludeRenovate: exclude, excludeTests: exclude, - funding: undefined, - logo: undefined, mode: "create", owner: "StubOwner", repository: "stub-repository", diff --git a/src/steps/writing/creation/writePackageJson.test.ts b/src/steps/writing/creation/writePackageJson.test.ts index 919aeffef..44a47cff7 100644 --- a/src/steps/writing/creation/writePackageJson.test.ts +++ b/src/steps/writing/creation/writePackageJson.test.ts @@ -21,29 +21,9 @@ const options = { github: "github@email.com", npm: "npm@email.com", }, - excludeAllContributors: undefined, - excludeCompliance: undefined, - excludeLintJson: undefined, - excludeLintKnip: undefined, - excludeLintMd: undefined, - excludeLintPackageJson: undefined, - excludeLintPackages: undefined, - excludeLintPerfectionist: undefined, - excludeLintSpelling: undefined, - excludeLintYml: undefined, - excludeReleases: false, - excludeRenovate: undefined, - excludeTests: false, - funding: undefined, - logo: undefined, mode: "create", owner: "test-owner", repository: "test-repository", - skipGitHubApi: false, - skipInstall: undefined, - skipRemoval: undefined, - skipRestore: undefined, - skipUninstall: undefined, title: "", } satisfies Options;