From 001882b7c4224d885d2e03a4da1b8a2d6024502d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Sun, 1 Oct 2023 16:35:10 +0200 Subject: [PATCH] feat: smarter initial mode prompt (#919) ## PR Checklist - [x] Addresses an existing open issue: fixes #884 - [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 Enhances `promptForMode` to give different options based on the current directory: * If it's empty, offer to `create` a new repository in it or a child directory * If it's a Git directory, offer to `initialize` or `migrate` * If it's not a Git directory, runs `create` for a new repository in a child directory In doing so, adds an optional `--directory` that defaults to the repository's name. Also cleans up `getPrefillOrPromptedOption` a bit. Instead of allowing an `existingValue` parameter, calls to `getPrefillOrPromptedOption` are just put in the right-hand-side of a `??`. --- docs/Options.md | 1 + src/bin/index.test.ts | 33 ++--- src/bin/index.ts | 6 +- src/bin/mode.test.ts | 39 ------ src/bin/mode.ts | 67 ---------- src/bin/promptForMode.test.ts | 117 +++++++++++++++++ src/bin/promptForMode.ts | 119 ++++++++++++++++++ src/create/createAndEnterGitDirectory.test.ts | 65 ++++++++++ src/create/createAndEnterGitDirectory.ts | 19 +++ src/create/createAndEnterRepository.ts | 14 --- src/create/createRerunSuggestion.test.ts | 7 +- src/create/index.test.ts | 15 +-- src/create/index.ts | 15 ++- src/initialize/index.ts | 2 +- src/migrate/index.ts | 2 +- src/shared/generateNextSteps.test.ts | 1 + src/shared/options/args.ts | 1 + .../augmentOptionsWithExcludes.test.ts | 1 + .../index.test.ts | 50 ++++++-- .../index.ts | 7 +- .../parsePackageAuthor.test.ts | 0 .../parsePackageAuthor.ts | 0 .../readDefaultsFromReadme.test.ts | 0 .../readDefaultsFromReadme.ts | 0 .../getPrefillOrPromptedOption.test.ts | 42 ++++--- .../options/getPrefillOrPromptedOption.ts | 5 - src/shared/options/optionsSchema.ts | 1 + src/shared/options/readOptions.test.ts | 6 +- src/shared/options/readOptions.ts | 24 ++-- src/shared/types.ts | 36 +++++- src/steps/finalizeDependencies.test.ts | 1 + src/steps/updateLocalFiles.test.ts | 1 + .../writeReadme/generateTopContent.test.ts | 1 + src/steps/writeReadme/index.test.ts | 1 + .../writing/creation/createESLintRC.test.ts | 1 + .../dotGitHub/createDevelopment.test.ts | 1 + .../dotGitHub/createWorkflows.test.ts | 3 +- .../writing/creation/writePackageJson.test.ts | 7 +- 38 files changed, 500 insertions(+), 211 deletions(-) delete mode 100644 src/bin/mode.test.ts delete mode 100644 src/bin/mode.ts create mode 100644 src/bin/promptForMode.test.ts create mode 100644 src/bin/promptForMode.ts create mode 100644 src/create/createAndEnterGitDirectory.test.ts create mode 100644 src/create/createAndEnterGitDirectory.ts delete mode 100644 src/create/createAndEnterRepository.ts rename src/shared/options/{readOptionDefaults => createOptionDefaults}/index.test.ts (61%) rename src/shared/options/{readOptionDefaults => createOptionDefaults}/index.ts (89%) rename src/shared/options/{readOptionDefaults => createOptionDefaults}/parsePackageAuthor.test.ts (100%) rename src/shared/options/{readOptionDefaults => createOptionDefaults}/parsePackageAuthor.ts (100%) rename src/shared/options/{readOptionDefaults => createOptionDefaults}/readDefaultsFromReadme.test.ts (100%) rename src/shared/options/{readOptionDefaults => createOptionDefaults}/readDefaultsFromReadme.ts (100%) diff --git a/docs/Options.md b/docs/Options.md index 83a8538d2..856a7eeb1 100644 --- a/docs/Options.md +++ b/docs/Options.md @@ -54,6 +54,7 @@ The setup scripts also allow for optional overrides of the following inputs whos - `--access` _(`"public" | "restricted"`)_: Which [`npm publish --access`](https://docs.npmjs.com/cli/commands/npm-publish#access) to release npm packages with (by default, `"public"`) - `--author` _(`string`)_: Username on npm to publish packages under (by default, an existing npm author, or the currently logged in npm user, or `owner.toLowerCase()`) +- `--directory` _(`string`)_: Directory to create the repository in (by default, the same name as the repository) - `--email` _(`string`)_: Email address to be listed as the point of contact in docs and packages (e.g. `example@joshuakgoldberg.com`) - Optionally, `--email-github` _(`string`)_ and/or `--email-npm` _(`string`)_ may be provided to use different emails in `.md` files and `package.json`, respectively - `--funding` _(`string`)_: GitHub organization or username to mention in `funding.yml` (by default, `owner`) diff --git a/src/bin/index.test.ts b/src/bin/index.test.ts index d76bad6de..1ae744d91 100644 --- a/src/bin/index.test.ts +++ b/src/bin/index.test.ts @@ -54,7 +54,7 @@ vi.mock("../migrate/index.js", () => ({ const mockPromptForMode = vi.fn(); -vi.mock("./mode.js", () => ({ +vi.mock("./promptForMode.js", () => ({ get promptForMode() { return mockPromptForMode; }, @@ -65,8 +65,8 @@ describe("bin", () => { vi.spyOn(console, "clear").mockImplementation(() => undefined); }); - it("returns 1 when promptForMode returns undefined", async () => { - mockPromptForMode.mockResolvedValue(undefined); + it("returns 1 when promptForMode returns an undefined mode", async () => { + mockPromptForMode.mockResolvedValue({}); const result = await bin([]); @@ -76,9 +76,9 @@ describe("bin", () => { expect(result).toBe(1); }); - it("returns 1 when promptForMode returns an error", async () => { + it("returns 1 when promptForMode returns an error mode", async () => { const error = new Error("Oh no!"); - mockPromptForMode.mockResolvedValue(error); + mockPromptForMode.mockResolvedValue({ mode: error }); const result = await bin([]); @@ -90,13 +90,14 @@ describe("bin", () => { const mode = "create"; const args = ["--owner", "abc123"]; const code = 0; + const promptedOptions = { directory: "." }; - mockPromptForMode.mockResolvedValue(mode); + mockPromptForMode.mockResolvedValue({ mode, options: promptedOptions }); mockCreate.mockResolvedValue({ code, options: {} }); const result = await bin(args); - expect(mockCreate).toHaveBeenCalledWith(args); + expect(mockCreate).toHaveBeenCalledWith(args, promptedOptions); expect(mockCancel).not.toHaveBeenCalled(); expect(mockInitialize).not.toHaveBeenCalled(); expect(mockMigrate).not.toHaveBeenCalled(); @@ -107,13 +108,14 @@ describe("bin", () => { const mode = "create"; const args = ["--owner", "abc123"]; const code = 2; + const promptedOptions = { directory: "." }; - mockPromptForMode.mockResolvedValue(mode); + mockPromptForMode.mockResolvedValue({ mode, options: promptedOptions }); mockCreate.mockResolvedValue({ code, options: {} }); const result = await bin(args); - expect(mockCreate).toHaveBeenCalledWith(args); + expect(mockCreate).toHaveBeenCalledWith(args, promptedOptions); expect(mockCancel).toHaveBeenCalledWith( `Operation cancelled. Exiting - maybe another time? 👋`, ); @@ -126,7 +128,7 @@ describe("bin", () => { const code = 2; const error = "Oh no!"; - mockPromptForMode.mockResolvedValue(mode); + mockPromptForMode.mockResolvedValue({ mode }); mockInitialize.mockResolvedValue({ code: 2, error, @@ -135,7 +137,7 @@ describe("bin", () => { const result = await bin(args); - expect(mockInitialize).toHaveBeenCalledWith(args); + expect(mockInitialize).toHaveBeenCalledWith(args, undefined); expect(mockLogLine).toHaveBeenCalledWith(chalk.red(error)); expect(mockCancel).toHaveBeenCalledWith( `Operation cancelled. Exiting - maybe another time? 👋`, @@ -152,7 +154,7 @@ describe("bin", () => { .object({ email: z.string().email() }) .safeParse({ email: "abc123" }); - mockPromptForMode.mockResolvedValue(mode); + mockPromptForMode.mockResolvedValue({ mode }); mockInitialize.mockResolvedValue({ code: 2, error: (validationResult as z.SafeParseError<{ email: string }>).error, @@ -161,7 +163,7 @@ describe("bin", () => { const result = await bin(args); - expect(mockInitialize).toHaveBeenCalledWith(args); + expect(mockInitialize).toHaveBeenCalledWith(args, undefined); expect(mockLogLine).toHaveBeenCalledWith( chalk.red('Validation error: Invalid email at "email"'), ); @@ -175,13 +177,14 @@ describe("bin", () => { const mode = "create"; const args = ["--owner", "abc123"]; const code = 1; + const promptedOptions = { directory: "." }; - mockPromptForMode.mockResolvedValue(mode); + mockPromptForMode.mockResolvedValue({ mode, options: promptedOptions }); mockCreate.mockResolvedValue({ code, options: {} }); const result = await bin(args); - expect(mockCreate).toHaveBeenCalledWith(args); + expect(mockCreate).toHaveBeenCalledWith(args, promptedOptions); expect(mockCancel).toHaveBeenCalledWith( `Operation failed. Exiting - maybe another time? 👋`, ); diff --git a/src/bin/index.ts b/src/bin/index.ts index b8778c319..09d064cba 100644 --- a/src/bin/index.ts +++ b/src/bin/index.ts @@ -9,7 +9,7 @@ import { initialize } from "../initialize/index.js"; import { migrate } from "../migrate/index.js"; import { logLine } from "../shared/cli/lines.js"; import { StatusCodes } from "../shared/codes.js"; -import { promptForMode } from "./mode.js"; +import { promptForMode } from "./promptForMode.js"; const operationMessage = (verb: string) => `Operation ${verb}. Exiting - maybe another time? 👋`; @@ -45,14 +45,14 @@ export async function bin(args: string[]) { strict: false, }); - const mode = await promptForMode(values.mode); + const { mode, options: promptedOptions } = await promptForMode(values.mode); if (typeof mode !== "string") { prompts.outro(chalk.red(mode?.message ?? operationMessage("cancelled"))); return 1; } const runners = { create, initialize, migrate }; - const { code, error, options } = await runners[mode](args); + const { code, error, options } = await runners[mode](args, promptedOptions); prompts.log.info( [ diff --git a/src/bin/mode.test.ts b/src/bin/mode.test.ts deleted file mode 100644 index bca3bd8c0..000000000 --- a/src/bin/mode.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; - -import { promptForMode } from "./mode.js"; - -const mockSelect = vi.fn(); - -vi.mock("@clack/prompts", () => ({ - isCancel: () => false, - get select() { - return mockSelect; - }, -})); - -describe("promptForMode", () => { - it("returns an error when the input exists and is not a mode", async () => { - const mode = await promptForMode("other"); - - expect(mode).toMatchInlineSnapshot( - "[Error: Unknown --mode: other. Allowed modes are: create, initialize, migrate.]", - ); - }); - - it("returns the input when it is a mode", async () => { - const input = "create"; - - const mode = await promptForMode(input); - - expect(mode).toEqual(input); - }); - - it("returns the selection when input is undefined and the user selects it", async () => { - const mode = "create"; - mockSelect.mockResolvedValue(mode); - - const actual = await promptForMode(undefined); - - expect(actual).toEqual(mode); - }); -}); diff --git a/src/bin/mode.ts b/src/bin/mode.ts deleted file mode 100644 index 17c67752e..000000000 --- a/src/bin/mode.ts +++ /dev/null @@ -1,67 +0,0 @@ -import * as prompts from "@clack/prompts"; -import chalk from "chalk"; -import z from "zod"; - -import { StatusCode } from "../shared/codes.js"; -import { filterPromptCancel } from "../shared/prompts.js"; -import { Options } from "../shared/types.js"; - -export interface ModeResult { - code: StatusCode; - error?: string | z.ZodError; - options: Partial; -} - -export type ModeRunner = (args: string[]) => Promise; - -export type Mode = "create" | "initialize" | "migrate"; - -const allowedModes = ["create", "initialize", "migrate"] satisfies Mode[]; - -function isMode(input: boolean | string): input is Mode { - return allowedModes.includes(input as Mode); -} - -export async function promptForMode(input: boolean | string | undefined) { - if (input) { - if (!isMode(input)) { - return new Error( - `Unknown --mode: ${input}. Allowed modes are: ${allowedModes.join( - ", ", - )}.`, - ); - } - - return input; - } - - const label = (base: string, text: string) => `${chalk.bold(base)} ${text}`; - - const selection = filterPromptCancel( - (await prompts.select({ - message: chalk.blue("How would you like to use the template?"), - options: [ - { - label: label("create", "a new repository in a child directory"), - value: "create", - }, - { - label: label( - "initialize", - "a freshly repository in the current directory", - ), - value: "initialize", - }, - { - label: label( - "migrate", - "an existing repository in the current directory", - ), - value: "migrate", - }, - ], - })) as Mode | symbol, - ); - - return selection; -} diff --git a/src/bin/promptForMode.test.ts b/src/bin/promptForMode.test.ts new file mode 100644 index 000000000..d63ae4b3b --- /dev/null +++ b/src/bin/promptForMode.test.ts @@ -0,0 +1,117 @@ +import chalk from "chalk"; +import { describe, expect, it, vi } from "vitest"; + +import { promptForMode } from "./promptForMode.js"; + +const mockSelect = vi.fn(); + +vi.mock("@clack/prompts", () => ({ + isCancel: () => false, + get select() { + return mockSelect; + }, +})); + +const mockReaddir = vi.fn(); + +vi.mock("node:fs/promises", () => ({ + get readdir() { + return mockReaddir; + }, +})); + +const mockCwd = vi.fn(); + +vi.mock("node:process", () => ({ + get cwd() { + return mockCwd; + }, +})); + +const mockLogLine = vi.fn(); + +vi.mock("../shared/cli/lines.js", () => ({ + get logLine() { + return mockLogLine; + }, +})); +describe("promptForMode", () => { + it("returns an error when the input exists and is not a mode", async () => { + const mode = await promptForMode("other"); + + expect(mode).toMatchInlineSnapshot( + ` + { + "mode": [Error: Unknown --mode: other. Allowed modes are: create, initialize, migrate.], + } + `, + ); + }); + + it("returns the input when it is a mode", async () => { + const input = "create"; + + const mode = await promptForMode(input); + + expect(mode).toEqual({ mode: input }); + }); + + it("returns creating in the current directory when the current directory is empty and the user selects create-current", async () => { + mockSelect.mockResolvedValueOnce("create-current"); + const directory = "test-directory"; + + mockReaddir.mockResolvedValueOnce([]); + mockCwd.mockReturnValueOnce(`/path/to/${directory}`); + + const actual = await promptForMode(undefined); + + expect(actual).toEqual({ + mode: "create", + options: { directory: ".", repository: directory }, + }); + expect(mockLogLine).not.toHaveBeenCalled(); + }); + + it("returns creating in a child directory when the current directory is empty and the user selects create-child", async () => { + mockSelect.mockResolvedValueOnce("create-child"); + const directory = "test-directory"; + + mockReaddir.mockResolvedValueOnce([]); + mockCwd.mockReturnValueOnce(`/path/to/${directory}`); + + const actual = await promptForMode(undefined); + + expect(actual).toEqual({ + mode: "create", + }); + expect(mockLogLine).not.toHaveBeenCalled(); + }); + + it("returns the user selection when the current directory is a Git directory", async () => { + const mode = "initialize"; + mockSelect.mockResolvedValueOnce(mode); + + mockReaddir.mockResolvedValueOnce([".git"]); + + const actual = await promptForMode(undefined); + + expect(actual).toEqual({ mode }); + expect(mockLogLine).not.toHaveBeenCalled(); + }); + + it("returns create without prompting when the current directory contains children but is not a Git directory", async () => { + const mode = "create"; + + mockReaddir.mockResolvedValueOnce(["file"]); + + const actual = await promptForMode(undefined); + + expect(actual).toEqual({ mode }); + expect(mockSelect).not.toHaveBeenCalled(); + expect(mockLogLine).toHaveBeenCalledWith( + chalk.gray( + "Defaulting to --mode create because the directory contains children and isn't a Git repository.", + ), + ); + }); +}); diff --git a/src/bin/promptForMode.ts b/src/bin/promptForMode.ts new file mode 100644 index 000000000..4ccaae7d6 --- /dev/null +++ b/src/bin/promptForMode.ts @@ -0,0 +1,119 @@ +import * as prompts from "@clack/prompts"; +import chalk from "chalk"; +import * as fs from "fs/promises"; +import path from "node:path"; +import * as process from "node:process"; + +import { logLine } from "../shared/cli/lines.js"; +import { filterPromptCancel } from "../shared/prompts.js"; +import { Mode, PromptedOptions } from "../shared/types.js"; + +const allowedModes = ["create", "initialize", "migrate"] satisfies Mode[]; + +function isMode(input: boolean | string): input is Mode { + return allowedModes.includes(input as Mode); +} + +function label(base: string, text: string) { + return `${chalk.bold(base)} ${text}`; +} + +export interface PromptedMode { + mode: Error | Mode | undefined; + options?: PromptedOptions; +} + +export async function promptForMode( + input: boolean | string | undefined, +): Promise { + if (input) { + if (!isMode(input)) { + return { + mode: new Error( + `Unknown --mode: ${input}. Allowed modes are: ${allowedModes.join( + ", ", + )}.`, + ), + }; + } + + return { mode: input }; + } + + const dir = await fs.readdir("."); + + if (dir.length === 0) { + const mode = filterPromptCancel( + (await prompts.select({ + message: chalk.blue("How would you like to use the template?"), + options: [ + { + label: label( + "create", + "a new repository in the current empty directory", + ), + value: "create-current", + }, + { + label: label("create", "a new repository in a new child directory"), + value: "create-child", + }, + ], + })) as string, + ); + + const directory = path.basename(process.cwd()); + + return { + mode: "create", + ...(mode === "create-current" && { + options: { + directory: ".", + repository: directory, + }, + }), + }; + } + + if (dir.includes(".git")) { + return { + mode: filterPromptCancel( + (await prompts.select({ + initialValue: "migrate" as Mode, + message: chalk.blue("How would you like to use the template?"), + options: [ + { + label: label("create", "a new repository in a child directory"), + value: "create", + }, + { + label: label( + "initialize", + "a freshly cloned repository in the current directory", + ), + value: "initialize", + }, + { + label: label( + "migrate", + "the existing repository in the current directory", + ), + value: "migrate", + }, + ], + })) as Mode | symbol, + ), + }; + } + + logLine(); + logLine( + chalk.gray( + "Defaulting to --mode create because the directory contains children and isn't a Git repository.", + ), + ); + + return { + mode: "create", + }; +} diff --git a/src/create/createAndEnterGitDirectory.test.ts b/src/create/createAndEnterGitDirectory.test.ts new file mode 100644 index 000000000..1ce7cd7b5 --- /dev/null +++ b/src/create/createAndEnterGitDirectory.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it, vi } from "vitest"; + +import { createAndEnterGitDirectory } from "./createAndEnterGitDirectory.js"; + +const mockMkdir = vi.fn(); +const mockReaddir = vi.fn(); + +vi.mock("node:fs/promises", () => ({ + get mkdir() { + return mockMkdir; + }, + get readdir() { + return mockReaddir; + }, +})); + +const mockChdir = vi.fn(); + +vi.mock("node:process", () => ({ + get chdir() { + return mockChdir; + }, +})); + +describe("createAndEnterGitDirectory", () => { + it("returns false and doesn't run fs.mkdir when the directory is '.' and has children", async () => { + mockReaddir.mockResolvedValueOnce(["file"]); + + const actual = await createAndEnterGitDirectory("."); + + expect(actual).toBe(false); + expect(mockMkdir).not.toHaveBeenCalled(); + }); + + it("returns true and doesn't run fs.mkdir when the directory is '.' and is empty", async () => { + mockReaddir.mockResolvedValueOnce([]); + + const actual = await createAndEnterGitDirectory("."); + + expect(actual).toBe(true); + expect(mockMkdir).not.toHaveBeenCalled(); + }); + + it("returns false and doesn't run fs.chdir when the directory is a child directory with children", async () => { + const directory = "dir"; + mockReaddir + .mockResolvedValueOnce([directory]) + .mockResolvedValueOnce(["file"]); + + const actual = await createAndEnterGitDirectory(directory); + + expect(actual).toBe(false); + expect(mockChdir).not.toHaveBeenCalled(); + }); + + it("returns true and runs fs.chdir when the directory is a child directory that doesn't exist", async () => { + const directory = "dir"; + mockReaddir.mockResolvedValueOnce([directory]).mockResolvedValueOnce([]); + + const actual = await createAndEnterGitDirectory(directory); + + expect(actual).toBe(true); + expect(mockChdir).toHaveBeenCalledWith(directory); + }); +}); diff --git a/src/create/createAndEnterGitDirectory.ts b/src/create/createAndEnterGitDirectory.ts new file mode 100644 index 000000000..dee1c712e --- /dev/null +++ b/src/create/createAndEnterGitDirectory.ts @@ -0,0 +1,19 @@ +import { $ } from "execa"; +import * as fs from "node:fs/promises"; +import * as process from "node:process"; + +export async function createAndEnterGitDirectory(directory: string) { + if (directory !== "." && !(await fs.readdir(".")).includes(directory)) { + await fs.mkdir(directory); + } else if ((await fs.readdir(directory)).length) { + return false; + } + + if (directory !== ".") { + process.chdir(directory); + } + + await $`git init -b main`; + + return true; +} diff --git a/src/create/createAndEnterRepository.ts b/src/create/createAndEnterRepository.ts deleted file mode 100644 index d36a953a2..000000000 --- a/src/create/createAndEnterRepository.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { $ } from "execa"; -import fs from "node:fs/promises"; - -export async function createAndEnterRepository(repository: string) { - if ((await fs.readdir(".")).includes(repository)) { - return false; - } - - await fs.mkdir(repository); - process.chdir(repository); - await $`git init`; - - return true; -} diff --git a/src/create/createRerunSuggestion.test.ts b/src/create/createRerunSuggestion.test.ts index 43daedc8e..f8c7a729c 100644 --- a/src/create/createRerunSuggestion.test.ts +++ b/src/create/createRerunSuggestion.test.ts @@ -9,6 +9,7 @@ const options = { base: "everything", createRepository: true, description: "Test description.", + directory: ".", email: { github: "github@email.com", npm: "npm@email.com", @@ -46,7 +47,7 @@ describe("createRerunSuggestion", () => { const actual = createRerunSuggestion(options); expect(actual).toMatchInlineSnapshot( - '"npx create-typescript-app --mode create --base everything --access public --author TestAuthor --create-repository true --description \\"Test description.\\" --email-github github@email.com --email-npm npm@email.com --exclude-all-contributors true --exclude-compliance true --exclude-lint-jsdoc true --exclude-lint-json true --exclude-lint-knip true --exclude-lint-package-json true --exclude-lint-perfectionist true --keywords \\"abc def ghi jkl mno pqr\\" --mode create --owner TestOwner --repository test-repository --skip-github-api true --skip-install true --skip-removal true --title \\"Test Title\\""', + '"npx create-typescript-app --mode create --base everything --access public --author TestAuthor --create-repository true --description \\"Test description.\\" --directory . --email-github github@email.com --email-npm npm@email.com --exclude-all-contributors true --exclude-compliance true --exclude-lint-jsdoc true --exclude-lint-json true --exclude-lint-knip true --exclude-lint-package-json true --exclude-lint-perfectionist true --keywords \\"abc def ghi jkl mno pqr\\" --mode create --owner TestOwner --repository test-repository --skip-github-api true --skip-install true --skip-removal true --title \\"Test Title\\""', ); }); @@ -61,7 +62,7 @@ describe("createRerunSuggestion", () => { }); expect(actual).toMatchInlineSnapshot( - '"npx create-typescript-app --mode initialize --base everything --access public --author TestAuthor --create-repository true --description \\"Test description.\\" --email-github github@email.com --email-npm npm@email.com --exclude-all-contributors true --exclude-compliance true --exclude-lint-jsdoc true --exclude-lint-json true --exclude-lint-knip true --exclude-lint-package-json true --exclude-lint-perfectionist true --keywords \\"abc def ghi jkl mno pqr\\" --logo test/src.png --logo-alt \\"Test alt.\\" --mode initialize --owner TestOwner --repository test-repository --skip-github-api true --skip-install true --skip-removal true --title \\"Test Title\\""', + '"npx create-typescript-app --mode initialize --base everything --access public --author TestAuthor --create-repository true --description \\"Test description.\\" --directory . --email-github github@email.com --email-npm npm@email.com --exclude-all-contributors true --exclude-compliance true --exclude-lint-jsdoc true --exclude-lint-json true --exclude-lint-knip true --exclude-lint-package-json true --exclude-lint-perfectionist true --keywords \\"abc def ghi jkl mno pqr\\" --logo test/src.png --logo-alt \\"Test alt.\\" --mode initialize --owner TestOwner --repository test-repository --skip-github-api true --skip-install true --skip-removal true --title \\"Test Title\\""', ); }); @@ -75,7 +76,7 @@ describe("createRerunSuggestion", () => { }); expect(actual).toMatchInlineSnapshot( - '"npx create-typescript-app --mode initialize --base everything --access public --author TestAuthor --create-repository true --description \\"Test description.\\" --email-github github@email.com --email-npm npm@email.com --exclude-all-contributors true --exclude-compliance true --exclude-lint-jsdoc true --exclude-lint-json true --exclude-lint-knip true --exclude-lint-md true --exclude-lint-package-json true --exclude-lint-perfectionist true --exclude-lint-spelling true --keywords \\"abc def ghi jkl mno pqr\\" --mode initialize --owner TestOwner --repository test-repository --skip-github-api true --skip-install true --skip-removal true --title \\"Test Title\\""', + '"npx create-typescript-app --mode initialize --base everything --access public --author TestAuthor --create-repository true --description \\"Test description.\\" --directory . --email-github github@email.com --email-npm npm@email.com --exclude-all-contributors true --exclude-compliance true --exclude-lint-jsdoc true --exclude-lint-json true --exclude-lint-knip true --exclude-lint-md true --exclude-lint-package-json true --exclude-lint-perfectionist true --exclude-lint-spelling true --keywords \\"abc def ghi jkl mno pqr\\" --mode initialize --owner TestOwner --repository test-repository --skip-github-api true --skip-install true --skip-removal true --title \\"Test Title\\""', ); }); }); diff --git a/src/create/index.test.ts b/src/create/index.test.ts index 5030651ab..d86ec6b3f 100644 --- a/src/create/index.test.ts +++ b/src/create/index.test.ts @@ -21,15 +21,16 @@ vi.mock("../shared/options/readOptions.js", () => ({ }, })); -const mockCreateAndEnterRepository = vi.fn(); +const mockCreateAndEnterGitDirectory = vi.fn(); -vi.mock("./createAndEnterRepository.js", () => ({ - get createAndEnterRepository() { - return mockCreateAndEnterRepository; +vi.mock("./createAndEnterGitDirectory.js", () => ({ + get createAndEnterGitDirectory() { + return mockCreateAndEnterGitDirectory; }, })); const optionsBase = { + directory: "TestDirectory", repository: "TestRepository", }; @@ -48,13 +49,13 @@ describe("create", () => { }); }); - it("returns a failure code when createAndEnterRepository returns false", async () => { + it("returns a failure code when createAndEnterGitDirectory returns false", async () => { mockReadOptions.mockResolvedValue({ cancelled: false, options: optionsBase, }); - mockCreateAndEnterRepository.mockResolvedValue(false); + mockCreateAndEnterGitDirectory.mockResolvedValue(false); const result = await create([]); @@ -64,7 +65,7 @@ describe("create", () => { }); expect(mockOutro).toHaveBeenCalledWith( chalk.red( - "The TestRepository directory already exists. Please remove the directory or try a different name.", + "The TestDirectory directory already exists and is not empty. Please clear the directory, run with --mode initialize, or try a different directory.", ), ); }); diff --git a/src/create/index.ts b/src/create/index.ts index fddea49a1..3ee49f8c6 100644 --- a/src/create/index.ts +++ b/src/create/index.ts @@ -1,18 +1,18 @@ import * as prompts from "@clack/prompts"; import chalk from "chalk"; -import { ModeResult } from "../bin/mode.js"; import { outro } from "../shared/cli/outro.js"; import { StatusCodes } from "../shared/codes.js"; import { generateNextSteps } from "../shared/generateNextSteps.js"; import { readOptions } from "../shared/options/readOptions.js"; import { runOrRestore } from "../shared/runOrRestore.js"; -import { createAndEnterRepository } from "./createAndEnterRepository.js"; +import { ModeRunner } from "../shared/types.js"; +import { createAndEnterGitDirectory } from "./createAndEnterGitDirectory.js"; import { createRerunSuggestion } from "./createRerunSuggestion.js"; import { createWithOptions } from "./createWithOptions.js"; -export async function create(args: string[]): Promise { - const inputs = await readOptions(args, "create"); +export const create: ModeRunner = async (args, promptedOptions) => { + const inputs = await readOptions(args, "create", promptedOptions); if (inputs.cancelled) { return { code: StatusCodes.Cancelled, @@ -21,11 +21,10 @@ export async function create(args: string[]): Promise { }; } - const wat = await createAndEnterRepository(inputs.options.repository); - if (!wat) { + if (!(await createAndEnterGitDirectory(inputs.options.directory))) { prompts.outro( chalk.red( - `The ${inputs.options.repository} directory already exists. Please remove the directory or try a different name.`, + `The ${inputs.options.directory} directory already exists and is not empty. Please clear the directory, run with --mode initialize, or try a different directory.`, ), ); return { code: StatusCodes.Failure, options: inputs.options }; @@ -66,4 +65,4 @@ export async function create(args: string[]): Promise { }), options: inputs.options, }; -} +}; diff --git a/src/initialize/index.ts b/src/initialize/index.ts index 69c954695..a2b4caa3e 100644 --- a/src/initialize/index.ts +++ b/src/initialize/index.ts @@ -1,10 +1,10 @@ -import { ModeRunner } from "../bin/mode.js"; import { outro } from "../shared/cli/outro.js"; import { StatusCodes } from "../shared/codes.js"; import { ensureGitRepository } from "../shared/ensureGitRepository.js"; import { generateNextSteps } from "../shared/generateNextSteps.js"; import { readOptions } from "../shared/options/readOptions.js"; import { runOrRestore } from "../shared/runOrRestore.js"; +import { ModeRunner } from "../shared/types.js"; import { initializeWithOptions } from "./initializeWithOptions.js"; export const initialize: ModeRunner = async (args) => { diff --git a/src/migrate/index.ts b/src/migrate/index.ts index ecb219360..7f4fd02ed 100644 --- a/src/migrate/index.ts +++ b/src/migrate/index.ts @@ -1,10 +1,10 @@ -import { ModeRunner } from "../bin/mode.js"; import { outro } from "../shared/cli/outro.js"; import { StatusCodes } from "../shared/codes.js"; import { ensureGitRepository } from "../shared/ensureGitRepository.js"; import { generateNextSteps } from "../shared/generateNextSteps.js"; import { readOptions } from "../shared/options/readOptions.js"; import { runOrRestore } from "../shared/runOrRestore.js"; +import { ModeRunner } from "../shared/types.js"; import { migrateWithOptions } from "./migrateWithOptions.js"; export const migrate: ModeRunner = async (args) => { diff --git a/src/shared/generateNextSteps.test.ts b/src/shared/generateNextSteps.test.ts index 3a000e094..26b697aa6 100644 --- a/src/shared/generateNextSteps.test.ts +++ b/src/shared/generateNextSteps.test.ts @@ -7,6 +7,7 @@ const options = { access: "public", base: "everything", description: "Test description.", + directory: ".", email: { github: "github@email.com", npm: "npm@email.com", diff --git a/src/shared/options/args.ts b/src/shared/options/args.ts index 8d250589d..0d78d1a1b 100644 --- a/src/shared/options/args.ts +++ b/src/shared/options/args.ts @@ -6,6 +6,7 @@ export const allArgOptions = { base: { type: "string" }, "create-repository": { type: "boolean" }, description: { type: "string" }, + directory: { type: "string" }, email: { type: "string" }, "email-github": { type: "string" }, "email-npm": { type: "string" }, diff --git a/src/shared/options/augmentOptionsWithExcludes.test.ts b/src/shared/options/augmentOptionsWithExcludes.test.ts index a63064736..37d4c2b4e 100644 --- a/src/shared/options/augmentOptionsWithExcludes.test.ts +++ b/src/shared/options/augmentOptionsWithExcludes.test.ts @@ -9,6 +9,7 @@ const optionsBase = { base: "everything", createRepository: undefined, description: "", + directory: ".", email: { github: "github@email.com", npm: "npm@email.com", diff --git a/src/shared/options/readOptionDefaults/index.test.ts b/src/shared/options/createOptionDefaults/index.test.ts similarity index 61% rename from src/shared/options/readOptionDefaults/index.test.ts rename to src/shared/options/createOptionDefaults/index.test.ts index 586209bb5..d02c0cb10 100644 --- a/src/shared/options/readOptionDefaults/index.test.ts +++ b/src/shared/options/createOptionDefaults/index.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; -import { readOptionDefaults } from "./index.js"; +import { createOptionDefaults } from "./index.js"; const mock$ = vi.fn(); @@ -10,6 +10,14 @@ vi.mock("execa", () => ({ }, })); +const mockGitUrlParse = vi.fn(); + +vi.mock("git-url-parse", () => ({ + get default() { + return mockGitUrlParse; + }, +})); + const mockNpmUser = vi.fn(); vi.mock("npm-user", () => ({ @@ -26,7 +34,7 @@ vi.mock("../../packages.js", () => ({ }, })); -describe("readOptionDefaults", () => { +describe("createOptionDefaults", () => { describe("email", () => { it("returns the npm whoami email from npm when only an npm exists", async () => { mock$.mockImplementation(([command]: string[]) => @@ -36,7 +44,7 @@ describe("readOptionDefaults", () => { email: `test@${username}.com`, })); - const actual = await readOptionDefaults().email(); + const actual = await createOptionDefaults().email(); expect(actual).toEqual({ github: "test@npm-username.com", @@ -52,7 +60,7 @@ describe("readOptionDefaults", () => { }, }); - const actual = await readOptionDefaults().email(); + const actual = await createOptionDefaults().email(); expect(actual).toEqual({ github: "test@package.com", @@ -68,7 +76,7 @@ describe("readOptionDefaults", () => { ); mockReadPackageData.mockResolvedValue({}); - const actual = await readOptionDefaults().email(); + const actual = await createOptionDefaults().email(); expect(actual).toEqual({ github: "test@git.com", @@ -85,7 +93,7 @@ describe("readOptionDefaults", () => { })); mockReadPackageData.mockResolvedValue({}); - const actual = await readOptionDefaults().email(); + const actual = await createOptionDefaults().email(); expect(actual).toEqual({ github: "test@git.com", @@ -97,9 +105,37 @@ describe("readOptionDefaults", () => { mock$.mockResolvedValue({ stdout: "" }); mockReadPackageData.mockResolvedValue({}); - const actual = await readOptionDefaults().email(); + const actual = await createOptionDefaults().email(); expect(actual).toBeUndefined(); }); }); + + describe("repository", () => { + it("returns promptedOptions.repository when it exists", async () => { + const repository = "test-prompted-repository"; + const promptedOptions = { repository }; + const actual = await createOptionDefaults(promptedOptions).repository(); + + expect(actual).toBe(repository); + }); + + it("returns the Git name when it exists and promptedOptions.repository doesn't", async () => { + const name = "test-git-repository"; + mockGitUrlParse.mockResolvedValueOnce({ name }); + + const actual = await createOptionDefaults().repository(); + + expect(actual).toBe(name); + }); + + it("returns the package name when it exists and promptedOptions.repository a Git name don't", async () => { + const name = "test-package-name"; + mockReadPackageData.mockResolvedValueOnce({ name }); + + const actual = await createOptionDefaults().repository(); + + expect(actual).toBe(name); + }); + }); }); diff --git a/src/shared/options/readOptionDefaults/index.ts b/src/shared/options/createOptionDefaults/index.ts similarity index 89% rename from src/shared/options/readOptionDefaults/index.ts rename to src/shared/options/createOptionDefaults/index.ts index af1728994..c9fc1e9a0 100644 --- a/src/shared/options/readOptionDefaults/index.ts +++ b/src/shared/options/createOptionDefaults/index.ts @@ -8,10 +8,11 @@ import npmUser from "npm-user"; import { readPackageData } from "../../packages.js"; import { tryCatchAsync } from "../../tryCatchAsync.js"; import { tryCatchLazyValueAsync } from "../../tryCatchLazyValueAsync.js"; +import { PromptedOptions } from "../../types.js"; import { parsePackageAuthor } from "./parsePackageAuthor.js"; import { readDefaultsFromReadme } from "./readDefaultsFromReadme.js"; -export function readOptionDefaults() { +export function createOptionDefaults(promptedOptions?: PromptedOptions) { const gitDefaults = tryCatchLazyValueAsync(async () => gitUrlParse(await gitRemoteOriginUrl()), ); @@ -53,7 +54,9 @@ export function readOptionDefaults() { owner: async () => (await gitDefaults())?.organization ?? (await packageAuthor()).author, repository: async () => - (await gitDefaults())?.name ?? (await packageData()).name, + promptedOptions?.repository ?? + (await gitDefaults())?.name ?? + (await packageData()).name, ...readDefaultsFromReadme(), }; } diff --git a/src/shared/options/readOptionDefaults/parsePackageAuthor.test.ts b/src/shared/options/createOptionDefaults/parsePackageAuthor.test.ts similarity index 100% rename from src/shared/options/readOptionDefaults/parsePackageAuthor.test.ts rename to src/shared/options/createOptionDefaults/parsePackageAuthor.test.ts diff --git a/src/shared/options/readOptionDefaults/parsePackageAuthor.ts b/src/shared/options/createOptionDefaults/parsePackageAuthor.ts similarity index 100% rename from src/shared/options/readOptionDefaults/parsePackageAuthor.ts rename to src/shared/options/createOptionDefaults/parsePackageAuthor.ts diff --git a/src/shared/options/readOptionDefaults/readDefaultsFromReadme.test.ts b/src/shared/options/createOptionDefaults/readDefaultsFromReadme.test.ts similarity index 100% rename from src/shared/options/readOptionDefaults/readDefaultsFromReadme.test.ts rename to src/shared/options/createOptionDefaults/readDefaultsFromReadme.test.ts diff --git a/src/shared/options/readOptionDefaults/readDefaultsFromReadme.ts b/src/shared/options/createOptionDefaults/readDefaultsFromReadme.ts similarity index 100% rename from src/shared/options/readOptionDefaults/readDefaultsFromReadme.ts rename to src/shared/options/createOptionDefaults/readDefaultsFromReadme.ts diff --git a/src/shared/options/getPrefillOrPromptedOption.test.ts b/src/shared/options/getPrefillOrPromptedOption.test.ts index 75906cd21..07e7dd3c7 100644 --- a/src/shared/options/getPrefillOrPromptedOption.test.ts +++ b/src/shared/options/getPrefillOrPromptedOption.test.ts @@ -1,3 +1,4 @@ +import { TextOptions } from "@clack/prompts"; import { describe, expect, it, vi } from "vitest"; import { getPrefillOrPromptedOption } from "./getPrefillOrPromptedOption.js"; @@ -12,28 +13,10 @@ vi.mock("@clack/prompts", () => ({ })); describe("getPrefillOrPromptedValue", () => { - it("logs a pre-fill message when a first value already exists", async () => { - const existing = "existing value"; - - const actual = await getPrefillOrPromptedOption(existing, ""); - - expect(actual).toEqual(existing); - }); - - it("prompts for a new value when the value doesn't already exist", async () => { - const expected = "expected value"; - - mockText.mockResolvedValue(expected); - - const actual = await getPrefillOrPromptedOption(undefined, ""); - - expect(actual).toEqual(expected); - }); - it("provides no placeholder when one is not provided", async () => { const message = "Test message"; - await getPrefillOrPromptedOption(undefined, message); + await getPrefillOrPromptedOption(message); expect(mockText).toHaveBeenCalledWith({ message, @@ -47,7 +30,6 @@ describe("getPrefillOrPromptedValue", () => { const placeholder = "Test placeholder"; await getPrefillOrPromptedOption( - undefined, message, vi.fn().mockResolvedValue(placeholder), ); @@ -58,4 +40,24 @@ describe("getPrefillOrPromptedValue", () => { validate: expect.any(Function), }); }); + + it("validates entered text when it's not blank", async () => { + const message = "Test message"; + + await getPrefillOrPromptedOption(message); + + const { validate } = (mockText.mock.calls[0] as [Required])[0]; + + expect(validate(message)).toBeUndefined(); + }); + + it("invalidates entered text when it's blank", async () => { + const message = ""; + + await getPrefillOrPromptedOption(message); + + const { validate } = (mockText.mock.calls[0] as [Required])[0]; + + expect(validate(message)).toBe("Please enter a value."); + }); }); diff --git a/src/shared/options/getPrefillOrPromptedOption.ts b/src/shared/options/getPrefillOrPromptedOption.ts index f79bb5eb9..e0c5c6d30 100644 --- a/src/shared/options/getPrefillOrPromptedOption.ts +++ b/src/shared/options/getPrefillOrPromptedOption.ts @@ -3,14 +3,9 @@ import * as prompts from "@clack/prompts"; import { filterPromptCancel } from "../prompts.js"; export async function getPrefillOrPromptedOption( - existingValue: string | undefined, message: string, getPlaceholder?: () => Promise, ) { - if (existingValue) { - return existingValue; - } - return filterPromptCancel( await prompts.text({ message, diff --git a/src/shared/options/optionsSchema.ts b/src/shared/options/optionsSchema.ts index 44309ee90..1a8250cef 100644 --- a/src/shared/options/optionsSchema.ts +++ b/src/shared/options/optionsSchema.ts @@ -13,6 +13,7 @@ export const optionsSchemaShape = { .optional(), createRepository: z.boolean().optional(), description: z.string().optional(), + directory: z.string().optional(), email: z .object({ github: z.string().email(), diff --git a/src/shared/options/readOptions.test.ts b/src/shared/options/readOptions.test.ts index 6b35e82ba..d967b4ca4 100644 --- a/src/shared/options/readOptions.test.ts +++ b/src/shared/options/readOptions.test.ts @@ -11,6 +11,7 @@ const emptyOptions = { base: undefined, createRepository: undefined, description: undefined, + directory: undefined, email: undefined, excludeAllContributors: undefined, excludeCompliance: undefined, @@ -93,8 +94,8 @@ vi.mock("./getGitHub.js", () => ({ }, })); -vi.mock("./readOptionDefaults/index.js", () => ({ - readOptionDefaults() { +vi.mock("./createOptionDefaults/index.js", () => ({ + createOptionDefaults() { return { author: vi.fn(), description: vi.fn(), @@ -341,6 +342,7 @@ describe("readOptions", () => { ...mockOptions, access: "public", description: "mock", + directory: "mock", email: { github: "mock", npm: "mock", diff --git a/src/shared/options/readOptions.ts b/src/shared/options/readOptions.ts index 619403643..50708d3e2 100644 --- a/src/shared/options/readOptions.ts +++ b/src/shared/options/readOptions.ts @@ -2,17 +2,17 @@ import { parseArgs } from "node:util"; import { titleCase } from "title-case"; import { z } from "zod"; -import { Mode } from "../../bin/mode.js"; import { withSpinner } from "../cli/spinners.js"; +import { Mode, PromptedOptions } from "../types.js"; import { Options, OptionsLogo } from "../types.js"; import { allArgOptions } from "./args.js"; import { augmentOptionsWithExcludes } from "./augmentOptionsWithExcludes.js"; +import { createOptionDefaults } from "./createOptionDefaults/index.js"; import { detectEmailRedundancy } from "./detectEmailRedundancy.js"; import { ensureRepositoryExists } from "./ensureRepositoryExists.js"; import { GitHub, getGitHub } from "./getGitHub.js"; import { getPrefillOrPromptedOption } from "./getPrefillOrPromptedOption.js"; import { optionsSchema } from "./optionsSchema.js"; -import { readOptionDefaults } from "./readOptionDefaults/index.js"; export interface GitHubAndOptions { github: GitHub | undefined; @@ -34,8 +34,9 @@ export type OptionsParseResult = OptionsParseCancelled | OptionsParseSuccess; export async function readOptions( args: string[], mode: Mode, + promptedOptions: PromptedOptions = {}, ): Promise { - const defaults = readOptionDefaults(); + const defaults = createOptionDefaults(promptedOptions); const { values } = parseArgs({ args, options: allArgOptions, @@ -49,6 +50,7 @@ export async function readOptions( base: values.base, createRepository: values["create-repository"], description: values.description, + directory: values.directory, email: values.email ?? values["email-github"] ?? values["email-npm"] ? { @@ -110,7 +112,6 @@ export async function readOptions( const options = optionsParseResult.data; options.owner ??= await getPrefillOrPromptedOption( - options.owner, "What organization or user will the repository be under?", defaults.owner, ); @@ -122,7 +123,6 @@ export async function readOptions( } options.repository ??= await getPrefillOrPromptedOption( - options.repository, "What will the kebab-case name of the repository be?", defaults.repository, ); @@ -148,7 +148,6 @@ export async function readOptions( } options.description ??= await getPrefillOrPromptedOption( - options.description, "How would you describe the new package?", async () => (await defaults.description()) ?? "A very lovely package. Hooray!", @@ -158,7 +157,6 @@ export async function readOptions( } options.title ??= await getPrefillOrPromptedOption( - options.title, "What will the Title Case title of the repository be?", async () => (await defaults.title()) ?? titleCase(repository).replaceAll("-", " "), @@ -170,10 +168,11 @@ export async function readOptions( let logo: OptionsLogo | undefined; if (options.logo) { - const alt = await getPrefillOrPromptedOption( - options.logoAlt, - "What is the alt text (non-visual description) of the logo?", - ); + const alt = + options.logoAlt ?? + (await getPrefillOrPromptedOption( + "What is the alt text (non-visual description) of the logo?", + )); if (!alt) { return { cancelled: true, options }; } @@ -185,7 +184,6 @@ export async function readOptions( options.email ?? (await defaults.email()) ?? (await getPrefillOrPromptedOption( - undefined, "What email should be used in package.json and .md files?", )); if (!email) { @@ -197,6 +195,8 @@ export async function readOptions( access: options.access ?? "public", author: options.author ?? (await defaults.owner()), description: options.description, + directory: + options.directory ?? promptedOptions.directory ?? options.repository, email: typeof email === "string" ? { github: email, npm: email } : email, funding: options.funding ?? (await defaults.funding()), logo, diff --git a/src/shared/types.ts b/src/shared/types.ts index 0d5158458..78516ec88 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -1,4 +1,6 @@ -import { Mode } from "../bin/mode.js"; +import { z } from "zod"; + +import { StatusCode } from "./codes.js"; export interface AllContributorContributor { contributions: string[]; @@ -33,12 +35,16 @@ export interface OptionsLogo { src: string; } +/** + * All runtime options that may (or must) be specified for setup. + */ export interface Options { access: OptionsAccess; author?: string; base?: OptionsBase; createRepository?: boolean; description: string; + directory: string; email: OptionsEmail; excludeAllContributors?: boolean; excludeCompliance?: boolean; @@ -74,3 +80,31 @@ export interface Options { skipUninstall?: boolean; title: string; } + +/** + * Options that might be suggested by how the user is running setup. + */ +export interface PromptedOptions { + /** + * Directory for the repository, if it may differ from the repository name. + */ + directory?: string; + + /** + * Repository name, if it may differ from the current directory. + */ + repository?: string; +} + +export interface ModeResult { + code: StatusCode; + error?: string | z.ZodError; + options: Partial; +} + +export type ModeRunner = ( + args: string[], + promptedOptions?: PromptedOptions, +) => Promise; + +export type Mode = "create" | "initialize" | "migrate"; diff --git a/src/steps/finalizeDependencies.test.ts b/src/steps/finalizeDependencies.test.ts index 4c86ea4ce..17ec08477 100644 --- a/src/steps/finalizeDependencies.test.ts +++ b/src/steps/finalizeDependencies.test.ts @@ -22,6 +22,7 @@ const options = { base: "everything", createRepository: undefined, description: "Stub description.", + directory: ".", email: { github: "github@email.com", npm: "npm@email.com", diff --git a/src/steps/updateLocalFiles.test.ts b/src/steps/updateLocalFiles.test.ts index 20d673991..76d96aeb1 100644 --- a/src/steps/updateLocalFiles.test.ts +++ b/src/steps/updateLocalFiles.test.ts @@ -25,6 +25,7 @@ const options = { base: "everything", createRepository: undefined, description: "Stub description.", + directory: ".", email: { github: "github@email.com", npm: "npm@email.com", diff --git a/src/steps/writeReadme/generateTopContent.test.ts b/src/steps/writeReadme/generateTopContent.test.ts index d85afee6a..4be232346 100644 --- a/src/steps/writeReadme/generateTopContent.test.ts +++ b/src/steps/writeReadme/generateTopContent.test.ts @@ -9,6 +9,7 @@ const optionsBase = { base: undefined, createRepository: undefined, description: "", + directory: ".", email: { github: "github@email.com", npm: "npm@email.com", diff --git a/src/steps/writeReadme/index.test.ts b/src/steps/writeReadme/index.test.ts index d45f249cd..8583f9146 100644 --- a/src/steps/writeReadme/index.test.ts +++ b/src/steps/writeReadme/index.test.ts @@ -29,6 +29,7 @@ const options = { base: "everything", createRepository: false, description: "Test description.", + directory: ".", email: { github: "github@email.com", npm: "npm@email.com", diff --git a/src/steps/writing/creation/createESLintRC.test.ts b/src/steps/writing/creation/createESLintRC.test.ts index 5eab2434f..0d6a2ce5a 100644 --- a/src/steps/writing/creation/createESLintRC.test.ts +++ b/src/steps/writing/creation/createESLintRC.test.ts @@ -10,6 +10,7 @@ function fakeOptions(getExcludeValue: (exclusionName: string) => boolean) { base: "everything", createRepository: true, description: "Test description.", + directory: ".", email: { github: "github@email.com", npm: "npm@email.com", diff --git a/src/steps/writing/creation/dotGitHub/createDevelopment.test.ts b/src/steps/writing/creation/dotGitHub/createDevelopment.test.ts index 02a2779d5..832d57005 100644 --- a/src/steps/writing/creation/dotGitHub/createDevelopment.test.ts +++ b/src/steps/writing/creation/dotGitHub/createDevelopment.test.ts @@ -9,6 +9,7 @@ const options = { base: "everything", createRepository: false, description: "Test description.", + directory: ".", email: { github: "github@email.com", npm: "npm@email.com", diff --git a/src/steps/writing/creation/dotGitHub/createWorkflows.test.ts b/src/steps/writing/creation/dotGitHub/createWorkflows.test.ts index 3ecc91fdd..7c0dbf752 100644 --- a/src/steps/writing/creation/dotGitHub/createWorkflows.test.ts +++ b/src/steps/writing/creation/dotGitHub/createWorkflows.test.ts @@ -8,7 +8,8 @@ const createOptions = (exclude: boolean) => access: "public", author: undefined, base: "everything", - description: "Stub description.", + description: "Test description.", + directory: ".", email: { github: "github@email.com", npm: "npm@email.com", diff --git a/src/steps/writing/creation/writePackageJson.test.ts b/src/steps/writing/creation/writePackageJson.test.ts index 6c9ae78b9..c39183282 100644 --- a/src/steps/writing/creation/writePackageJson.test.ts +++ b/src/steps/writing/creation/writePackageJson.test.ts @@ -16,7 +16,8 @@ const options = { author: "test-author", base: "everything", createRepository: undefined, - description: "test-description", + description: "Test description.", + directory: ".", email: { github: "github@email.com", npm: "npm@email.com", @@ -94,7 +95,7 @@ describe("writePackageJson", () => { "email": "npm@email.com", "name": "test-author", }, - "description": "test-description", + "description": "Test description.", "devDependencies": {}, "engines": { "node": ">=18", @@ -164,7 +165,7 @@ describe("writePackageJson", () => { "email": "npm@email.com", "name": "test-author", }, - "description": "test-description", + "description": "Test description.", "devDependencies": {}, "engines": { "node": ">=18",