From 8bfc58022c625c487631ea8c3e034ce03014218a Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 30 Dec 2024 20:43:28 -0500 Subject: [PATCH 01/12] feat: clear local repository if hydrating from a template --- packages/create-testers/src/testBlock.ts | 2 +- packages/create/package.json | 5 + .../{modes => cli}/clearLocalGitTags.test.ts | 0 .../src/{modes => cli}/clearLocalGitTags.ts | 0 .../initialize/assertOptionsForInitialize.ts | 5 +- .../createRepositoryOnGitHub.test.ts | 0 .../initialize}/createRepositoryOnGitHub.ts | 4 +- .../createTrackingBranches.test.ts | 0 .../initialize}/createTrackingBranches.ts | 4 +- .../src/cli/initialize/runModeInitialize.ts | 29 +++++- .../cli/migrate/clearTemplateFiles.test.ts | 44 +++++++++ .../src/cli/migrate/clearTemplateFiles.ts | 13 +++ .../migrate/getForkedTemplateLocator.test.ts | 94 +++++++++++++++++++ .../cli/migrate/getForkedTemplateLocator.ts | 26 +++++ .../src/cli/migrate/runModeMigrate.test.ts | 65 +++++++++++-- .../create/src/cli/migrate/runModeMigrate.ts | 23 ++++- .../create/src/cli/readProductionSettings.ts | 2 +- packages/create/src/cli/runCli.ts | 2 + packages/create/src/index.ts | 1 + packages/create/src/modes/types.ts | 6 -- .../src/producers/executePresetBlocks.ts | 2 +- packages/create/src/producers/produceBlock.ts | 2 +- .../create/src/producers/producePreset.ts | 2 +- packages/create/src/runners/runPreset.ts | 34 +------ packages/create/src/types/bases.ts | 1 + packages/create/src/types/modes.ts | 1 + pnpm-lock.yaml | 23 +++++ 27 files changed, 332 insertions(+), 58 deletions(-) rename packages/create/src/{modes => cli}/clearLocalGitTags.test.ts (100%) rename packages/create/src/{modes => cli}/clearLocalGitTags.ts (100%) rename packages/create/src/{modes => cli/initialize}/createRepositoryOnGitHub.test.ts (100%) rename packages/create/src/{modes => cli/initialize}/createRepositoryOnGitHub.ts (84%) rename packages/create/src/{modes => cli/initialize}/createTrackingBranches.test.ts (100%) rename packages/create/src/{modes => cli/initialize}/createTrackingBranches.ts (76%) create mode 100644 packages/create/src/cli/migrate/clearTemplateFiles.test.ts create mode 100644 packages/create/src/cli/migrate/clearTemplateFiles.ts create mode 100644 packages/create/src/cli/migrate/getForkedTemplateLocator.test.ts create mode 100644 packages/create/src/cli/migrate/getForkedTemplateLocator.ts delete mode 100644 packages/create/src/modes/types.ts create mode 100644 packages/create/src/types/modes.ts diff --git a/packages/create-testers/src/testBlock.ts b/packages/create-testers/src/testBlock.ts index e0706e53..c701aa80 100644 --- a/packages/create-testers/src/testBlock.ts +++ b/packages/create-testers/src/testBlock.ts @@ -4,8 +4,8 @@ import { BlockWithoutAddons, Creation, produceBlock, + ProductionMode, } from "create"; -import { ProductionMode } from "create/lib/modes/types.js"; import { createFailingObject } from "./utils.js"; diff --git a/packages/create/package.json b/packages/create/package.json index 583825d1..b32929b8 100644 --- a/packages/create/package.json +++ b/packages/create/package.json @@ -31,12 +31,17 @@ "execa": "^9.5.2", "get-github-auth-token": "^0.1.0", "hash-object": "^5.0.1", + "hosted-git-info": "^8.0.2", "import-local-or-npx": "^0.1.0", "octokit": "^4.0.2", "prettier": "3.4.2", + "read-pkg": "^9.0.1", "without-undefined-properties": "^0.1.1", "zod": "^3.24.1" }, + "devDependencies": { + "@types/hosted-git-info": "^3.0.5" + }, "engines": { "node": ">=18" } diff --git a/packages/create/src/modes/clearLocalGitTags.test.ts b/packages/create/src/cli/clearLocalGitTags.test.ts similarity index 100% rename from packages/create/src/modes/clearLocalGitTags.test.ts rename to packages/create/src/cli/clearLocalGitTags.test.ts diff --git a/packages/create/src/modes/clearLocalGitTags.ts b/packages/create/src/cli/clearLocalGitTags.ts similarity index 100% rename from packages/create/src/modes/clearLocalGitTags.ts rename to packages/create/src/cli/clearLocalGitTags.ts diff --git a/packages/create/src/cli/initialize/assertOptionsForInitialize.ts b/packages/create/src/cli/initialize/assertOptionsForInitialize.ts index 9adfefd5..cdc6ee79 100644 --- a/packages/create/src/cli/initialize/assertOptionsForInitialize.ts +++ b/packages/create/src/cli/initialize/assertOptionsForInitialize.ts @@ -1,4 +1,7 @@ -import { CreationOptions } from "../../modes/types.js"; +export interface CreationOptions { + owner: string; + repository: string; +} export function assertOptionsForInitialize( options: object, diff --git a/packages/create/src/modes/createRepositoryOnGitHub.test.ts b/packages/create/src/cli/initialize/createRepositoryOnGitHub.test.ts similarity index 100% rename from packages/create/src/modes/createRepositoryOnGitHub.test.ts rename to packages/create/src/cli/initialize/createRepositoryOnGitHub.test.ts diff --git a/packages/create/src/modes/createRepositoryOnGitHub.ts b/packages/create/src/cli/initialize/createRepositoryOnGitHub.ts similarity index 84% rename from packages/create/src/modes/createRepositoryOnGitHub.ts rename to packages/create/src/cli/initialize/createRepositoryOnGitHub.ts index 344b6c51..4159860e 100644 --- a/packages/create/src/modes/createRepositoryOnGitHub.ts +++ b/packages/create/src/cli/initialize/createRepositoryOnGitHub.ts @@ -1,7 +1,7 @@ import { Octokit } from "octokit"; -import { RepositoryTemplate } from "../types/bases.js"; -import { CreationOptions } from "./types.js"; +import { RepositoryTemplate } from "../../types/bases.js"; +import { CreationOptions } from "./assertOptionsForInitialize.js"; export async function createRepositoryOnGitHub( { owner, repository }: CreationOptions, diff --git a/packages/create/src/modes/createTrackingBranches.test.ts b/packages/create/src/cli/initialize/createTrackingBranches.test.ts similarity index 100% rename from packages/create/src/modes/createTrackingBranches.test.ts rename to packages/create/src/cli/initialize/createTrackingBranches.test.ts diff --git a/packages/create/src/modes/createTrackingBranches.ts b/packages/create/src/cli/initialize/createTrackingBranches.ts similarity index 76% rename from packages/create/src/modes/createTrackingBranches.ts rename to packages/create/src/cli/initialize/createTrackingBranches.ts index 41d43121..d3c070b3 100644 --- a/packages/create/src/modes/createTrackingBranches.ts +++ b/packages/create/src/cli/initialize/createTrackingBranches.ts @@ -1,5 +1,5 @@ -import { SystemRunner } from "../types/system.js"; -import { CreationOptions } from "./types.js"; +import { SystemRunner } from "../../types/system.js"; +import { CreationOptions } from "./assertOptionsForInitialize.js"; export async function createTrackingBranches( { owner, repository }: CreationOptions, diff --git a/packages/create/src/cli/initialize/runModeInitialize.ts b/packages/create/src/cli/initialize/runModeInitialize.ts index a22121e7..3412bd67 100644 --- a/packages/create/src/cli/initialize/runModeInitialize.ts +++ b/packages/create/src/cli/initialize/runModeInitialize.ts @@ -3,6 +3,7 @@ import chalk from "chalk"; import { runPreset } from "../../runners/runPreset.js"; import { createSystemContextWithAuth } from "../../system/createSystemContextWithAuth.js"; +import { clearLocalGitTags } from "../clearLocalGitTags.js"; import { createClackDisplay } from "../display/createClackDisplay.js"; import { findPositionalFrom } from "../findPositionalFrom.js"; import { tryImportTemplatePreset } from "../importers/tryImportTemplatePreset.js"; @@ -11,6 +12,9 @@ import { promptForInitializationDirectory } from "../prompts/promptForInitializa import { promptForPresetOptions } from "../prompts/promptForPresetOptions.js"; import { CLIStatus } from "../status.js"; import { ModeResults } from "../types.js"; +import { assertOptionsForInitialize } from "./assertOptionsForInitialize.js"; +import { createRepositoryOnGitHub } from "./createRepositoryOnGitHub.js"; +import { createTrackingBranches } from "./createTrackingBranches.js"; export interface RunModeInitializeSettings { args: string[]; @@ -71,7 +75,21 @@ export async function runModeInitialize({ return { status: CLIStatus.Cancelled }; } - display.spinner.start("Creating repository..."); + assertOptionsForInitialize(options); + + display.spinner.start("Creating repository on GitHub..."); + + await createRepositoryOnGitHub( + options, + system.fetchers.octokit, + loaded.preset.base.template, + ); + + display.spinner.stop("Created repository on GitHub."); + + const description = `the ${loaded.preset.about.name} preset`; + + display.spinner.start(`Running ${description}...`); const creation = await runPreset(loaded.preset, { ...system, @@ -80,7 +98,14 @@ export async function runModeInitialize({ options, }); - display.spinner.stop("Created repository"); + display.spinner.stop(`Ran ${description}.`); + + display.spinner.start(`Preparing local repository...`); + + await createTrackingBranches(options, system.runner); + await clearLocalGitTags(system.runner); + + display.spinner.start(`Prepared local repository.`); return { outro: [ diff --git a/packages/create/src/cli/migrate/clearTemplateFiles.test.ts b/packages/create/src/cli/migrate/clearTemplateFiles.test.ts new file mode 100644 index 00000000..b2bf10df --- /dev/null +++ b/packages/create/src/cli/migrate/clearTemplateFiles.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it, vi } from "vitest"; + +import { clearTemplateFiles } from "./clearTemplateFiles.js"; + +const mockReaddir = vi.fn(); +const mockRm = vi.fn(); + +vi.mock("node:fs/promises", () => ({ + get readdir() { + return mockReaddir; + }, + get rm() { + return mockRm; + }, +})); + +const mockIsForkOfTemplate = vi.fn(); + +vi.mock("./isForkOfTemplate", () => ({ + get isForkOfTemplate() { + return mockIsForkOfTemplate; + }, +})); + +const directory = "path/to"; + +describe("clearTemplateFiles", () => { + it("deletes non-.git children", async () => { + mockIsForkOfTemplate.mockResolvedValueOnce(true); + mockReaddir.mockResolvedValueOnce([ + ".git", + ".github", + "src", + "package.json", + ]); + + await clearTemplateFiles(directory); + + expect(mockRm).not.toHaveBeenCalledWith(".git"); + expect(mockRm).toHaveBeenCalledWith(".github"); + expect(mockRm).toHaveBeenCalledWith("src"); + expect(mockRm).toHaveBeenCalledWith("package.json"); + }); +}); diff --git a/packages/create/src/cli/migrate/clearTemplateFiles.ts b/packages/create/src/cli/migrate/clearTemplateFiles.ts new file mode 100644 index 00000000..2b433b98 --- /dev/null +++ b/packages/create/src/cli/migrate/clearTemplateFiles.ts @@ -0,0 +1,13 @@ +import * as fs from "node:fs/promises"; + +export async function clearTemplateFiles(directory: string) { + const children = await fs.readdir(directory); + + await Promise.all( + children + .filter((child) => child !== ".git") + .map(async (child) => { + await fs.rm(child); + }), + ); +} diff --git a/packages/create/src/cli/migrate/getForkedTemplateLocator.test.ts b/packages/create/src/cli/migrate/getForkedTemplateLocator.test.ts new file mode 100644 index 00000000..0eb95629 --- /dev/null +++ b/packages/create/src/cli/migrate/getForkedTemplateLocator.test.ts @@ -0,0 +1,94 @@ +import { describe, expect, it, vi } from "vitest"; + +import { getForkedTemplateLocator } from "./getForkedTemplateLocator.js"; + +const mockFromUrl = vi.fn(); + +vi.mock("hosted-git-info", () => ({ + default: { + get fromUrl() { + return mockFromUrl; + }, + }, +})); + +const mockReadPackage = vi.fn(); + +vi.mock("read-pkg", () => ({ + get readPackage() { + return mockReadPackage; + }, +})); + +const template = { + owner: "TestOwner", + repository: "test-repository", +}; + +describe("getForkedTemplateLocator", () => { + it("returns undefined when there is no package repository url", async () => { + mockReadPackage.mockResolvedValueOnce({}); + + const actual = await getForkedTemplateLocator(".", template); + + expect(actual).toBeUndefined(); + expect(mockFromUrl).not.toHaveBeenCalled(); + }); + + it("returns undefined when the Git repository doesn't have information", async () => { + mockReadPackage.mockResolvedValueOnce({ + repository: { url: "..." }, + }); + + mockFromUrl.mockResolvedValueOnce(undefined); + + const actual = await getForkedTemplateLocator(".", template); + + expect(actual).toBeUndefined(); + }); + + it("returns undefined when the Git repository user doesn't match the template owner", async () => { + mockReadPackage.mockResolvedValueOnce({ + repository: { url: "..." }, + }); + + mockFromUrl.mockResolvedValueOnce({ + project: template.repository, + user: "other", + }); + + const actual = await getForkedTemplateLocator(".", template); + + expect(actual).toBeUndefined(); + }); + + it("returns undefined when the Git repository project doesn't match the template repository", async () => { + mockReadPackage.mockResolvedValueOnce({ + repository: { url: "..." }, + }); + + mockFromUrl.mockResolvedValueOnce({ + project: "other", + user: template.owner, + }); + + const actual = await getForkedTemplateLocator(".", template); + + expect(actual).toBeUndefined(); + }); + + it("returns a locator when the Git repository matches the template repository", async () => { + mockReadPackage.mockResolvedValueOnce({ + repository: { url: "..." }, + }); + + mockFromUrl.mockResolvedValueOnce({ + project: template.repository, + user: template.owner, + }); + + const actual = await getForkedTemplateLocator(".", template); + + expect(actual).toBe(`TestOwner/test-repository`); + }); +}); diff --git a/packages/create/src/cli/migrate/getForkedTemplateLocator.ts b/packages/create/src/cli/migrate/getForkedTemplateLocator.ts new file mode 100644 index 00000000..bd3ef77b --- /dev/null +++ b/packages/create/src/cli/migrate/getForkedTemplateLocator.ts @@ -0,0 +1,26 @@ +import hostedGitInfo from "hosted-git-info"; +import { readPackage } from "read-pkg"; + +import { RepositoryTemplate } from "../../types/bases.js"; + +export async function getForkedTemplateLocator( + directory: string, + template: RepositoryTemplate, +) { + const { repository } = await readPackage({ cwd: directory }); + if (!repository?.url) { + return undefined; + } + + // eslint-disable-next-line @typescript-eslint/await-thenable + const gitInfo = await hostedGitInfo.fromUrl(repository.url); + + if ( + gitInfo?.user !== template.owner || + gitInfo.project !== template.repository + ) { + return undefined; + } + + return `${template.owner}/${template.repository}`; +} diff --git a/packages/create/src/cli/migrate/runModeMigrate.test.ts b/packages/create/src/cli/migrate/runModeMigrate.test.ts index 63bf599a..0c685a6b 100644 --- a/packages/create/src/cli/migrate/runModeMigrate.test.ts +++ b/packages/create/src/cli/migrate/runModeMigrate.test.ts @@ -37,6 +37,30 @@ vi.mock("../display/createClackDisplay.js", () => ({ }), })); +const mockClearLocalGitTags = vi.fn(); + +vi.mock("./clearLocalGitTags.js", () => ({ + get clearLocalGitTags() { + return mockClearLocalGitTags; + }, +})); + +const mockClearTemplateFiles = vi.fn(); + +vi.mock("./clearTemplateFiles.js", () => ({ + get clearTemplateFiles() { + return mockClearTemplateFiles; + }, +})); + +const mockIsForkOfTemplate = vi.fn(); + +vi.mock("./isForkOfTemplate.js", () => ({ + get isForkOfTemplate() { + return mockIsForkOfTemplate; + }, +})); + const mockLoadMigrationPreset = vi.fn(); vi.mock("./loadMigrationPreset.js", () => ({ @@ -45,6 +69,14 @@ vi.mock("./loadMigrationPreset.js", () => ({ }, })); +const base = createBase({ + options: {}, +}); +const preset = base.createPreset({ + about: { name: "Test" }, + blocks: [], +}); + describe("runModeMigrate", () => { it("returns the error when loadMigrationPreset resolves with an error", async () => { const error = new Error("Oh no!"); @@ -76,14 +108,35 @@ describe("runModeMigrate", () => { }); }); - it("returns a CLI success when importing and running the preset succeeds", async () => { - const base = createBase({ - options: {}, + it("doesn't clear template files or tags when not a template fork", async () => { + mockLoadMigrationPreset.mockResolvedValueOnce({ preset }); + mockIsCancel.mockReturnValueOnce(false); + mockIsForkOfTemplate.mockResolvedValueOnce(false); + + await runModeMigrate({ + args: [], + configFile: "create.config.js", }); - const preset = base.createPreset({ - about: { name: "Test" }, - blocks: [], + + expect(mockClearTemplateFiles).not.toHaveBeenCalled(); + expect(mockClearLocalGitTags).not.toHaveBeenCalled(); + }); + + it("clears template files and tags when a template fork", async () => { + mockLoadMigrationPreset.mockResolvedValueOnce({ preset }); + mockIsCancel.mockReturnValueOnce(false); + mockIsForkOfTemplate.mockResolvedValueOnce(true); + + await runModeMigrate({ + args: [], + configFile: "create.config.js", }); + + expect(mockClearTemplateFiles).not.toHaveBeenCalled(); + expect(mockClearLocalGitTags).not.toHaveBeenCalled(); + }); + + it("returns a CLI success when importing and running the preset succeeds", async () => { mockLoadMigrationPreset.mockResolvedValueOnce({ preset }); mockIsCancel.mockReturnValueOnce(false); diff --git a/packages/create/src/cli/migrate/runModeMigrate.ts b/packages/create/src/cli/migrate/runModeMigrate.ts index 50e17b86..a4d07285 100644 --- a/packages/create/src/cli/migrate/runModeMigrate.ts +++ b/packages/create/src/cli/migrate/runModeMigrate.ts @@ -3,10 +3,13 @@ import * as prompts from "@clack/prompts"; import { produceBase } from "../../producers/produceBase.js"; import { runPreset } from "../../runners/runPreset.js"; import { createSystemContextWithAuth } from "../../system/createSystemContextWithAuth.js"; +import { clearLocalGitTags } from "../clearLocalGitTags.js"; import { createClackDisplay } from "../display/createClackDisplay.js"; import { findPositionalFrom } from "../findPositionalFrom.js"; import { CLIStatus } from "../status.js"; import { ModeResults } from "../types.js"; +import { clearTemplateFiles } from "./clearTemplateFiles.js"; +import { getForkedTemplateLocator } from "./getForkedTemplateLocator.js"; import { loadMigrationPreset } from "./loadMigrationPreset.js"; export interface RunModeMigrateSettings { @@ -39,11 +42,23 @@ export async function runModeMigrate({ return { status: CLIStatus.Cancelled }; } - const description = `the ${loaded.preset.about.name} preset`; const display = createClackDisplay(); - const system = await createSystemContextWithAuth({ directory, display }); - display.spinner.start(`Running ${description}...`); + const [templateDescription, system] = await Promise.all([ + loaded.preset.base.template && + getForkedTemplateLocator(directory, loaded.preset.base.template), + createSystemContextWithAuth({ directory, display }), + ]); + + if (templateDescription) { + display.spinner.start(`Clearing from ${templateDescription}...`); + await clearTemplateFiles(directory); + await clearLocalGitTags(system.runner); + display.spinner.start(`Cleared from ${templateDescription}.`); + } + + const presetDescription = `the ${loaded.preset.about.name} preset`; + display.spinner.start(`Running ${presetDescription}...`); const options = await produceBase(loaded.preset.base, { ...system, @@ -57,7 +72,7 @@ export async function runModeMigrate({ options, }); - display.spinner.stop(`Ran ${description}.`); + display.spinner.stop(`Ran ${presetDescription}.`); return { outro: `You might want to commit any changes.`, diff --git a/packages/create/src/cli/readProductionSettings.ts b/packages/create/src/cli/readProductionSettings.ts index efec2d1f..bd7529f3 100644 --- a/packages/create/src/cli/readProductionSettings.ts +++ b/packages/create/src/cli/readProductionSettings.ts @@ -1,7 +1,7 @@ import * as fs from "node:fs/promises"; import path from "node:path"; -import { ProductionMode } from "../modes/types.js"; +import { ProductionMode } from "../types/modes.js"; import { ProductionSettings } from "./types.js"; export interface ReadProductionSettingsOptions { diff --git a/packages/create/src/cli/runCli.ts b/packages/create/src/cli/runCli.ts index 0a9b650e..82102004 100644 --- a/packages/create/src/cli/runCli.ts +++ b/packages/create/src/cli/runCli.ts @@ -70,6 +70,8 @@ export async function runCli(args: string[], logger: Logger) { return CLIStatus.Error; } + console.log({ productionSettings }); + const { outro, status, suggestions } = productionSettings.mode === "initialize" ? await runModeInitialize({ ...validatedValues, args }) diff --git a/packages/create/src/index.ts b/packages/create/src/index.ts index 5af4e6a8..0a973956 100644 --- a/packages/create/src/index.ts +++ b/packages/create/src/index.ts @@ -34,6 +34,7 @@ export type * from "./types/blocks.js"; export type * from "./types/context.js"; export type * from "./types/creations.js"; export type * from "./types/inputs.js"; +export type * from "./types/modes.js"; export type * from "./types/presets.js"; export type * from "./types/system.js"; export type * from "./types/templates.js"; diff --git a/packages/create/src/modes/types.ts b/packages/create/src/modes/types.ts deleted file mode 100644 index 10eea0f6..00000000 --- a/packages/create/src/modes/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface CreationOptions { - owner: string; - repository: string; -} - -export type ProductionMode = "initialize" | "migrate"; diff --git a/packages/create/src/producers/executePresetBlocks.ts b/packages/create/src/producers/executePresetBlocks.ts index 24ab0aec..1a90e514 100644 --- a/packages/create/src/producers/executePresetBlocks.ts +++ b/packages/create/src/producers/executePresetBlocks.ts @@ -3,10 +3,10 @@ import { getUpdatedBlockAddons, } from "../mergers/getUpdatedBlockAddons.js"; import { mergeCreations } from "../mergers/mergeCreations.js"; -import { ProductionMode } from "../modes/types.js"; import { AnyShape, InferredObject } from "../options.js"; import { Block, BlockWithAddons } from "../types/blocks.js"; import { Creation } from "../types/creations.js"; +import { ProductionMode } from "../types/modes.js"; import { Preset } from "../types/presets.js"; import { SystemContext } from "../types/system.js"; import { produceBlock } from "./produceBlock.js"; diff --git a/packages/create/src/producers/produceBlock.ts b/packages/create/src/producers/produceBlock.ts index 0b1b6aa2..b95ad27a 100644 --- a/packages/create/src/producers/produceBlock.ts +++ b/packages/create/src/producers/produceBlock.ts @@ -1,11 +1,11 @@ import { mergeCreations } from "../mergers/mergeCreations.js"; -import { ProductionMode } from "../modes/types.js"; import { BlockContextWithAddons, BlockWithAddons, BlockWithoutAddons, } from "../types/blocks.js"; import { Creation, IndirectCreation } from "../types/creations.js"; +import { ProductionMode } from "../types/modes.js"; export type BlockProductionSettings< Addons extends object | undefined, diff --git a/packages/create/src/producers/producePreset.ts b/packages/create/src/producers/producePreset.ts index f778e1b9..04d171c1 100644 --- a/packages/create/src/producers/producePreset.ts +++ b/packages/create/src/producers/producePreset.ts @@ -1,7 +1,7 @@ -import { ProductionMode } from "../modes/types.js"; import { AnyShape, InferredObject } from "../options.js"; import { createSystemContextWithAuth } from "../system/createSystemContextWithAuth.js"; import { Creation } from "../types/creations.js"; +import { ProductionMode } from "../types/modes.js"; import { Preset } from "../types/presets.js"; import { NativeSystem } from "../types/system.js"; import { executePresetBlocks } from "./executePresetBlocks.js"; diff --git a/packages/create/src/runners/runPreset.ts b/packages/create/src/runners/runPreset.ts index b74b018a..636c8bc7 100644 --- a/packages/create/src/runners/runPreset.ts +++ b/packages/create/src/runners/runPreset.ts @@ -1,14 +1,10 @@ import fs from "node:fs/promises"; -import { assertOptionsForInitialize } from "../cli/initialize/assertOptionsForInitialize.js"; -import { clearLocalGitTags } from "../modes/clearLocalGitTags.js"; -import { createRepositoryOnGitHub } from "../modes/createRepositoryOnGitHub.js"; -import { createTrackingBranches } from "../modes/createTrackingBranches.js"; -import { ProductionMode } from "../modes/types.js"; import { AnyShape, InferredObject } from "../options.js"; import { producePreset } from "../producers/producePreset.js"; import { createSystemContextWithAuth } from "../system/createSystemContextWithAuth.js"; import { Creation } from "../types/creations.js"; +import { ProductionMode } from "../types/modes.js"; import { Preset } from "../types/presets.js"; import { NativeSystem } from "../types/system.js"; import { applyCreation } from "./applyCreation.js"; @@ -33,7 +29,7 @@ export async function runPreset( preset: Preset, settings: PresetRunSettings, ): Promise>> { - const { directory = ".", options } = settings; + const { directory = "." } = settings; await fs.mkdir(directory, { recursive: true }); const system = await createSystemContextWithAuth({ @@ -41,31 +37,9 @@ export async function runPreset( ...settings, }); - const run = async () => { - const creation = await producePreset(preset, { ...system, ...settings }); - await applyCreation(creation, system); - return creation; - }; + const creation = await producePreset(preset, { ...system, ...settings }); - if (settings.mode !== "initialize") { - return await run(); - } - - assertOptionsForInitialize(options); - - await createRepositoryOnGitHub( - options, - system.fetchers.octokit, - preset.base.template, - ); - - const creation = await run(); - - await createTrackingBranches(options, system.runner); - - if (preset.base.template) { - await clearLocalGitTags(system.runner); - } + await applyCreation(creation, system); return creation; } diff --git a/packages/create/src/types/bases.ts b/packages/create/src/types/bases.ts index 02682408..99f5226e 100644 --- a/packages/create/src/types/bases.ts +++ b/packages/create/src/types/bases.ts @@ -63,6 +63,7 @@ export type LazyOptionalOptions = { }; export interface RepositoryTemplate { + deletions?: string[]; owner: string; repository: string; } diff --git a/packages/create/src/types/modes.ts b/packages/create/src/types/modes.ts new file mode 100644 index 00000000..a7a206ad --- /dev/null +++ b/packages/create/src/types/modes.ts @@ -0,0 +1 @@ +export type ProductionMode = "initialize" | "migrate"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 559c62ec..f4e7a530 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -122,6 +122,9 @@ importers: hash-object: specifier: ^5.0.1 version: 5.0.1 + hosted-git-info: + specifier: ^8.0.2 + version: 8.0.2 import-local-or-npx: specifier: ^0.1.0 version: 0.1.0(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.0.0)(typescript@5.7.2) @@ -131,12 +134,19 @@ importers: prettier: specifier: 3.4.2 version: 3.4.2 + read-pkg: + specifier: ^9.0.1 + version: 9.0.1 without-undefined-properties: specifier: ^0.1.1 version: 0.1.1 zod: specifier: ^3.24.1 version: 3.24.1 + devDependencies: + '@types/hosted-git-info': + specifier: ^3.0.5 + version: 3.0.5 packages/create-testers: dependencies: @@ -1473,6 +1483,9 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/hosted-git-info@3.0.5': + resolution: {integrity: sha512-Dmngh7U003cOHPhKGyA7LWqrnvcTyILNgNPmNCxlx7j8MIi54iBliiT8XqVLIQ3GchoOjVAyBzNJVyuaJjqokg==} + '@types/js-yaml@4.0.9': resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} @@ -2816,6 +2829,10 @@ packages: resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} engines: {node: ^16.14.0 || >=18.0.0} + hosted-git-info@8.0.2: + resolution: {integrity: sha512-sYKnA7eGln5ov8T8gnYlkSOxFJvywzEx9BueN6xo/GKO8PGiI6uK6xx+DIGe45T3bdVjLAQDQW1aicT8z8JwQg==} + engines: {node: ^18.17.0 || >=20.5.0} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -6285,6 +6302,8 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/hosted-git-info@3.0.5': {} + '@types/js-yaml@4.0.9': {} '@types/json-schema@7.0.15': {} @@ -8073,6 +8092,10 @@ snapshots: dependencies: lru-cache: 10.4.3 + hosted-git-info@8.0.2: + dependencies: + lru-cache: 10.4.3 + html-escaper@2.0.2: {} html-escaper@3.0.3: {} From 1ee1bc13a468ea661862ef0244eae4b67ddd6eca Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 30 Dec 2024 20:47:20 -0500 Subject: [PATCH 02/12] no console.log --- packages/create/src/cli/runCli.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/create/src/cli/runCli.ts b/packages/create/src/cli/runCli.ts index 82102004..0a9b650e 100644 --- a/packages/create/src/cli/runCli.ts +++ b/packages/create/src/cli/runCli.ts @@ -70,8 +70,6 @@ export async function runCli(args: string[], logger: Logger) { return CLIStatus.Error; } - console.log({ productionSettings }); - const { outro, status, suggestions } = productionSettings.mode === "initialize" ? await runModeInitialize({ ...validatedValues, args }) From e02223fec60c95bab5fac4653e6734cdc589bcd5 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 30 Dec 2024 22:40:52 -0500 Subject: [PATCH 03/12] A bit more initialize testing --- .../cli/initialize/runModeInitialize.test.ts | 36 +++++++++++++++++++ .../src/cli/migrate/runModeMigrate.test.ts | 27 ++++++++------ .../create/src/cli/migrate/runModeMigrate.ts | 8 ++--- 3 files changed, 56 insertions(+), 15 deletions(-) diff --git a/packages/create/src/cli/initialize/runModeInitialize.test.ts b/packages/create/src/cli/initialize/runModeInitialize.test.ts index af1bad22..df0b42b7 100644 --- a/packages/create/src/cli/initialize/runModeInitialize.test.ts +++ b/packages/create/src/cli/initialize/runModeInitialize.test.ts @@ -1,5 +1,7 @@ import { describe, expect, it, vi } from "vitest"; +import { createBase } from "../../creators/createBase.js"; +import { createTemplate } from "../../creators/createTemplate.js"; import { CLIStatus } from "../status.js"; import { runModeInitialize } from "./runModeInitialize.js"; @@ -19,6 +21,27 @@ vi.mock("../importers/tryImportTemplatePreset.js", () => ({ }, })); +const mockPromptForInitializationDirectory = vi.fn(); + +vi.mock("../prompts/promptForInitializationDirectory.js", () => ({ + get promptForInitializationDirectory() { + return mockPromptForInitializationDirectory; + }, +})); + +const base = createBase({ + options: {}, +}); + +const preset = base.createPreset({ + about: { name: "Test" }, + blocks: [], +}); + +const template = createTemplate({ + presets: [], +}); + describe("runModeInitialize", () => { it("returns a CLI error when no positional from can be found", async () => { const actual = await runModeInitialize({ args: [] }); @@ -57,4 +80,17 @@ describe("runModeInitialize", () => { expect(actual).toEqual({ status: CLIStatus.Cancelled }); }); + + it("returns the cancellation when promptForInitializationDirectory is cancelled", async () => { + const cancellation = Symbol.for("cancel"); + mockTryImportTemplatePreset.mockResolvedValueOnce({ preset, template }); + mockPromptForInitializationDirectory.mockResolvedValueOnce(cancellation); + mockIsCancel.mockReturnValueOnce(false).mockReturnValueOnce(true); + + const actual = await runModeInitialize({ + args: ["node", "create", "my-app"], + }); + + expect(actual).toEqual({ status: CLIStatus.Cancelled }); + }); }); diff --git a/packages/create/src/cli/migrate/runModeMigrate.test.ts b/packages/create/src/cli/migrate/runModeMigrate.test.ts index 0c685a6b..923ca6c1 100644 --- a/packages/create/src/cli/migrate/runModeMigrate.test.ts +++ b/packages/create/src/cli/migrate/runModeMigrate.test.ts @@ -39,7 +39,7 @@ vi.mock("../display/createClackDisplay.js", () => ({ const mockClearLocalGitTags = vi.fn(); -vi.mock("./clearLocalGitTags.js", () => ({ +vi.mock("../clearLocalGitTags.js", () => ({ get clearLocalGitTags() { return mockClearLocalGitTags; }, @@ -53,11 +53,11 @@ vi.mock("./clearTemplateFiles.js", () => ({ }, })); -const mockIsForkOfTemplate = vi.fn(); +const mockGetForkedTemplateLocator = vi.fn(); -vi.mock("./isForkOfTemplate.js", () => ({ - get isForkOfTemplate() { - return mockIsForkOfTemplate; +vi.mock("./getForkedTemplateLocator.js", () => ({ + get getForkedTemplateLocator() { + return mockGetForkedTemplateLocator; }, })); @@ -71,7 +71,12 @@ vi.mock("./loadMigrationPreset.js", () => ({ const base = createBase({ options: {}, + template: { + owner: "TestOwner", + repository: "test-repository", + }, }); + const preset = base.createPreset({ about: { name: "Test" }, blocks: [], @@ -108,10 +113,10 @@ describe("runModeMigrate", () => { }); }); - it("doesn't clear template files or tags when not a template fork", async () => { + it("doesn't clear template files or tags when no forked template locator is available", async () => { mockLoadMigrationPreset.mockResolvedValueOnce({ preset }); mockIsCancel.mockReturnValueOnce(false); - mockIsForkOfTemplate.mockResolvedValueOnce(false); + mockGetForkedTemplateLocator.mockResolvedValueOnce(undefined); await runModeMigrate({ args: [], @@ -122,18 +127,18 @@ describe("runModeMigrate", () => { expect(mockClearLocalGitTags).not.toHaveBeenCalled(); }); - it("clears template files and tags when a template fork", async () => { + it("clears template files and tags when a forked template locator is available", async () => { mockLoadMigrationPreset.mockResolvedValueOnce({ preset }); mockIsCancel.mockReturnValueOnce(false); - mockIsForkOfTemplate.mockResolvedValueOnce(true); + mockGetForkedTemplateLocator.mockResolvedValueOnce("a/b"); await runModeMigrate({ args: [], configFile: "create.config.js", }); - expect(mockClearTemplateFiles).not.toHaveBeenCalled(); - expect(mockClearLocalGitTags).not.toHaveBeenCalled(); + expect(mockClearTemplateFiles).toHaveBeenCalled(); + expect(mockClearLocalGitTags).toHaveBeenCalled(); }); it("returns a CLI success when importing and running the preset succeeds", async () => { diff --git a/packages/create/src/cli/migrate/runModeMigrate.ts b/packages/create/src/cli/migrate/runModeMigrate.ts index a4d07285..11ae1276 100644 --- a/packages/create/src/cli/migrate/runModeMigrate.ts +++ b/packages/create/src/cli/migrate/runModeMigrate.ts @@ -44,17 +44,17 @@ export async function runModeMigrate({ const display = createClackDisplay(); - const [templateDescription, system] = await Promise.all([ + const [templateLocator, system] = await Promise.all([ loaded.preset.base.template && getForkedTemplateLocator(directory, loaded.preset.base.template), createSystemContextWithAuth({ directory, display }), ]); - if (templateDescription) { - display.spinner.start(`Clearing from ${templateDescription}...`); + if (templateLocator) { + display.spinner.start(`Clearing from ${templateLocator}...`); await clearTemplateFiles(directory); await clearLocalGitTags(system.runner); - display.spinner.start(`Cleared from ${templateDescription}.`); + display.spinner.start(`Cleared from ${templateLocator}.`); } const presetDescription = `the ${loaded.preset.about.name} preset`; From 60a6a4642d15bcfefabea39cc32dae0538734a61 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 30 Dec 2024 22:43:35 -0500 Subject: [PATCH 04/12] Correct hostedGitInfo.fromUrl call --- .../src/cli/migrate/getForkedTemplateLocator.test.ts | 8 ++++---- .../create/src/cli/migrate/getForkedTemplateLocator.ts | 3 +-- packages/create/src/types/bases.ts | 1 - 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/create/src/cli/migrate/getForkedTemplateLocator.test.ts b/packages/create/src/cli/migrate/getForkedTemplateLocator.test.ts index 0eb95629..c3df5e26 100644 --- a/packages/create/src/cli/migrate/getForkedTemplateLocator.test.ts +++ b/packages/create/src/cli/migrate/getForkedTemplateLocator.test.ts @@ -40,7 +40,7 @@ describe("getForkedTemplateLocator", () => { repository: { url: "..." }, }); - mockFromUrl.mockResolvedValueOnce(undefined); + mockFromUrl.mockReturnValueOnce(undefined); const actual = await getForkedTemplateLocator(".", template); @@ -52,7 +52,7 @@ describe("getForkedTemplateLocator", () => { repository: { url: "..." }, }); - mockFromUrl.mockResolvedValueOnce({ + mockFromUrl.mockReturnValueOnce({ project: template.repository, user: "other", }); @@ -67,7 +67,7 @@ describe("getForkedTemplateLocator", () => { repository: { url: "..." }, }); - mockFromUrl.mockResolvedValueOnce({ + mockFromUrl.mockReturnValueOnce({ project: "other", user: template.owner, }); @@ -82,7 +82,7 @@ describe("getForkedTemplateLocator", () => { repository: { url: "..." }, }); - mockFromUrl.mockResolvedValueOnce({ + mockFromUrl.mockReturnValueOnce({ project: template.repository, user: template.owner, }); diff --git a/packages/create/src/cli/migrate/getForkedTemplateLocator.ts b/packages/create/src/cli/migrate/getForkedTemplateLocator.ts index bd3ef77b..7c05b91a 100644 --- a/packages/create/src/cli/migrate/getForkedTemplateLocator.ts +++ b/packages/create/src/cli/migrate/getForkedTemplateLocator.ts @@ -12,8 +12,7 @@ export async function getForkedTemplateLocator( return undefined; } - // eslint-disable-next-line @typescript-eslint/await-thenable - const gitInfo = await hostedGitInfo.fromUrl(repository.url); + const gitInfo = hostedGitInfo.fromUrl(repository.url); if ( gitInfo?.user !== template.owner || diff --git a/packages/create/src/types/bases.ts b/packages/create/src/types/bases.ts index 99f5226e..02682408 100644 --- a/packages/create/src/types/bases.ts +++ b/packages/create/src/types/bases.ts @@ -63,7 +63,6 @@ export type LazyOptionalOptions = { }; export interface RepositoryTemplate { - deletions?: string[]; owner: string; repository: string; } From ec8d3243966ae93260382d5fae6b3b97f031f78c Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 30 Dec 2024 23:00:25 -0500 Subject: [PATCH 05/12] fix: recursive true for dirs --- packages/create/src/cli/migrate/clearTemplateFiles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create/src/cli/migrate/clearTemplateFiles.ts b/packages/create/src/cli/migrate/clearTemplateFiles.ts index 2b433b98..fc3e702e 100644 --- a/packages/create/src/cli/migrate/clearTemplateFiles.ts +++ b/packages/create/src/cli/migrate/clearTemplateFiles.ts @@ -7,7 +7,7 @@ export async function clearTemplateFiles(directory: string) { children .filter((child) => child !== ".git") .map(async (child) => { - await fs.rm(child); + await fs.rm(child, { recursive: true }); }), ); } From 937c4c355ff3498cc0d128e9b23475322af90b45 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 30 Dec 2024 23:18:48 -0500 Subject: [PATCH 06/12] runSpinnerTask, and fixed base option prompting --- .../src/cli/display/createClackDisplay.ts | 10 ++- .../create/src/cli/display/runSpinnerTask.ts | 16 +++++ .../src/cli/initialize/runModeInitialize.ts | 70 ++++++++++--------- .../create/src/cli/migrate/runModeMigrate.ts | 58 +++++++++------ ...esetOptions.ts => promptForBaseOptions.ts} | 6 +- 5 files changed, 102 insertions(+), 58 deletions(-) create mode 100644 packages/create/src/cli/display/runSpinnerTask.ts rename packages/create/src/cli/prompts/{promptForPresetOptions.ts => promptForBaseOptions.ts} (87%) diff --git a/packages/create/src/cli/display/createClackDisplay.ts b/packages/create/src/cli/display/createClackDisplay.ts index 9f64084b..0cbbcb2f 100644 --- a/packages/create/src/cli/display/createClackDisplay.ts +++ b/packages/create/src/cli/display/createClackDisplay.ts @@ -3,9 +3,17 @@ import { CachedFactory } from "cached-factory"; import { SystemDisplay, SystemDisplayItem } from "../../types/system.js"; +export interface ClackDisplay extends SystemDisplay { + dumpItems(): SystemItemsDump; + spinner: ClackSpinner; +} + +// TODO: suggest making a type for all these things to Clack :) +export type ClackSpinner = ReturnType; + export type SystemItemsDump = Record>; -export function createClackDisplay() { +export function createClackDisplay(): ClackDisplay { const spinner = prompts.spinner(); const groups = new CachedFactory< string, diff --git a/packages/create/src/cli/display/runSpinnerTask.ts b/packages/create/src/cli/display/runSpinnerTask.ts new file mode 100644 index 00000000..fc3a541b --- /dev/null +++ b/packages/create/src/cli/display/runSpinnerTask.ts @@ -0,0 +1,16 @@ +import { ClackDisplay } from "./createClackDisplay.js"; + +export async function runSpinnerTask( + display: ClackDisplay, + start: string, + stop: string, + task: () => Promise, +) { + display.spinner.start(`${start}...`); + + const result = await task(); + + display.spinner.stop(`${stop}.`); + + return result; +} diff --git a/packages/create/src/cli/initialize/runModeInitialize.ts b/packages/create/src/cli/initialize/runModeInitialize.ts index 3412bd67..ce6f4301 100644 --- a/packages/create/src/cli/initialize/runModeInitialize.ts +++ b/packages/create/src/cli/initialize/runModeInitialize.ts @@ -5,11 +5,12 @@ import { runPreset } from "../../runners/runPreset.js"; import { createSystemContextWithAuth } from "../../system/createSystemContextWithAuth.js"; import { clearLocalGitTags } from "../clearLocalGitTags.js"; import { createClackDisplay } from "../display/createClackDisplay.js"; +import { runSpinnerTask } from "../display/runSpinnerTask.js"; import { findPositionalFrom } from "../findPositionalFrom.js"; import { tryImportTemplatePreset } from "../importers/tryImportTemplatePreset.js"; import { parseZodArgs } from "../parsers/parseZodArgs.js"; +import { promptForBaseOptions } from "../prompts/promptForBaseOptions.js"; import { promptForInitializationDirectory } from "../prompts/promptForInitializationDirectory.js"; -import { promptForPresetOptions } from "../prompts/promptForPresetOptions.js"; import { CLIStatus } from "../status.js"; import { ModeResults } from "../types.js"; import { assertOptionsForInitialize } from "./assertOptionsForInitialize.js"; @@ -62,12 +63,9 @@ export async function runModeInitialize({ const display = createClackDisplay(); const system = await createSystemContextWithAuth({ directory, display }); - const options = await promptForPresetOptions({ + const options = await promptForBaseOptions({ base: loaded.preset.base, - existingOptions: { - repository: directory, - ...parseZodArgs(args, loaded.preset.base.options), - }, + existingOptions: parseZodArgs(args, loaded.preset.base.options), system, }); @@ -77,35 +75,43 @@ export async function runModeInitialize({ assertOptionsForInitialize(options); - display.spinner.start("Creating repository on GitHub..."); - - await createRepositoryOnGitHub( - options, - system.fetchers.octokit, - loaded.preset.base.template, + await runSpinnerTask( + display, + "Creating repository on GitHub", + "Created repository on GitHub", + async () => { + await createRepositoryOnGitHub( + options, + system.fetchers.octokit, + loaded.preset.base.template, + ); + }, ); - display.spinner.stop("Created repository on GitHub."); - - const description = `the ${loaded.preset.about.name} preset`; - - display.spinner.start(`Running ${description}...`); - - const creation = await runPreset(loaded.preset, { - ...system, - directory, - mode: "initialize", - options, - }); - - display.spinner.stop(`Ran ${description}.`); - - display.spinner.start(`Preparing local repository...`); - - await createTrackingBranches(options, system.runner); - await clearLocalGitTags(system.runner); + const presetDescription = `the ${loaded.preset.about.name} preset`; + + const creation = await runSpinnerTask( + display, + `Running ${presetDescription}`, + `Ran ${presetDescription}`, + async () => + await runPreset(loaded.preset, { + ...system, + directory, + mode: "initialize", + options, + }), + ); - display.spinner.start(`Prepared local repository.`); + await runSpinnerTask( + display, + "Preparing local repository", + "Prepared local repository", + async () => { + await createTrackingBranches(options, system.runner); + await clearLocalGitTags(system.runner); + }, + ); return { outro: [ diff --git a/packages/create/src/cli/migrate/runModeMigrate.ts b/packages/create/src/cli/migrate/runModeMigrate.ts index 11ae1276..3dd1c802 100644 --- a/packages/create/src/cli/migrate/runModeMigrate.ts +++ b/packages/create/src/cli/migrate/runModeMigrate.ts @@ -1,11 +1,13 @@ import * as prompts from "@clack/prompts"; -import { produceBase } from "../../producers/produceBase.js"; import { runPreset } from "../../runners/runPreset.js"; import { createSystemContextWithAuth } from "../../system/createSystemContextWithAuth.js"; import { clearLocalGitTags } from "../clearLocalGitTags.js"; import { createClackDisplay } from "../display/createClackDisplay.js"; +import { runSpinnerTask } from "../display/runSpinnerTask.js"; import { findPositionalFrom } from "../findPositionalFrom.js"; +import { parseZodArgs } from "../parsers/parseZodArgs.js"; +import { promptForBaseOptions } from "../prompts/promptForBaseOptions.js"; import { CLIStatus } from "../status.js"; import { ModeResults } from "../types.js"; import { clearTemplateFiles } from "./clearTemplateFiles.js"; @@ -43,36 +45,48 @@ export async function runModeMigrate({ } const display = createClackDisplay(); + const system = await createSystemContextWithAuth({ directory, display }); - const [templateLocator, system] = await Promise.all([ + const templateLocator = loaded.preset.base.template && - getForkedTemplateLocator(directory, loaded.preset.base.template), - createSystemContextWithAuth({ directory, display }), - ]); + (await getForkedTemplateLocator(directory, loaded.preset.base.template)); if (templateLocator) { - display.spinner.start(`Clearing from ${templateLocator}...`); - await clearTemplateFiles(directory); - await clearLocalGitTags(system.runner); - display.spinner.start(`Cleared from ${templateLocator}.`); + await runSpinnerTask( + display, + `Clearing from ${templateLocator}`, + `Cleared from ${templateLocator}`, + async () => { + await clearTemplateFiles(directory); + await clearLocalGitTags(system.runner); + }, + ); } - const presetDescription = `the ${loaded.preset.about.name} preset`; - display.spinner.start(`Running ${presetDescription}...`); - - const options = await produceBase(loaded.preset.base, { - ...system, - directory, + const options = await promptForBaseOptions({ + base: loaded.preset.base, + existingOptions: parseZodArgs(args, loaded.preset.base.options), + system, }); + if (prompts.isCancel(options)) { + return { status: CLIStatus.Cancelled }; + } - await runPreset(loaded.preset, { - ...system, - directory, - mode: "migrate", - options, - }); + const presetDescription = `the ${loaded.preset.about.name} preset`; - display.spinner.stop(`Ran ${presetDescription}.`); + await runSpinnerTask( + display, + `Running ${presetDescription}`, + `Ran ${presetDescription}`, + async () => { + await runPreset(loaded.preset, { + ...system, + directory, + mode: "migrate", + options, + }); + }, + ); return { outro: `You might want to commit any changes.`, diff --git a/packages/create/src/cli/prompts/promptForPresetOptions.ts b/packages/create/src/cli/prompts/promptForBaseOptions.ts similarity index 87% rename from packages/create/src/cli/prompts/promptForPresetOptions.ts rename to packages/create/src/cli/prompts/promptForBaseOptions.ts index ec91473f..9914cda9 100644 --- a/packages/create/src/cli/prompts/promptForPresetOptions.ts +++ b/packages/create/src/cli/prompts/promptForBaseOptions.ts @@ -6,17 +6,17 @@ import { Base } from "../../types/bases.js"; import { SystemContext } from "../../types/system.js"; import { promptForSchema } from "./promptForSchema.js"; -export interface PromptForPresetOptionsSettings { +export interface PromptForBaseOptionsSettings { base: Base; existingOptions: Partial>; system: SystemContext; } -export async function promptForPresetOptions({ +export async function promptForBaseOptions({ base, existingOptions, system, -}: PromptForPresetOptionsSettings) { +}: PromptForBaseOptionsSettings) { const { directory } = system; const options: InferredObject = { directory, From 41873f864c2c6096f68899506478a1ea54291922 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 30 Dec 2024 23:20:09 -0500 Subject: [PATCH 07/12] Default directory to repository --- packages/create/src/cli/initialize/runModeInitialize.ts | 6 ++++-- packages/create/src/cli/runCli.ts | 8 ++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/create/src/cli/initialize/runModeInitialize.ts b/packages/create/src/cli/initialize/runModeInitialize.ts index ce6f4301..4a261e78 100644 --- a/packages/create/src/cli/initialize/runModeInitialize.ts +++ b/packages/create/src/cli/initialize/runModeInitialize.ts @@ -22,11 +22,13 @@ export interface RunModeInitializeSettings { directory?: string; from?: string; preset?: string; + repository?: string; } export async function runModeInitialize({ args, - directory: requestedDirectory, + repository, + directory: requestedDirectory = repository, from = findPositionalFrom(args), preset: requestedPreset, }: RunModeInitializeSettings): Promise { @@ -118,7 +120,7 @@ export async function runModeInitialize({ chalk.blue("Your new repository is ready in:"), chalk.green(directory.startsWith(".") ? directory : `./${directory}`), ].join(" "), - status: CLIStatus.Error, + status: CLIStatus.Success, suggestions: creation.suggestions, }; } diff --git a/packages/create/src/cli/runCli.ts b/packages/create/src/cli/runCli.ts index 0a9b650e..3af323d1 100644 --- a/packages/create/src/cli/runCli.ts +++ b/packages/create/src/cli/runCli.ts @@ -16,7 +16,9 @@ const valuesSchema = z.object({ directory: z.string().optional(), from: z.string().optional(), mode: z.union([z.literal("initialize"), z.literal("migrate")]).optional(), + owner: z.string().optional(), preset: z.string().optional(), + repository: z.string().optional(), }); export async function runCli(args: string[], logger: Logger) { @@ -35,9 +37,15 @@ export async function runCli(args: string[], logger: Logger) { mode: { type: "string", }, + owner: { + type: "string", + }, preset: { type: "string", }, + repository: { + type: "string", + }, version: { type: "boolean", }, From 92325920299ccde65db80a96bbfe4b0b164a8c8c Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 30 Dec 2024 23:31:25 -0500 Subject: [PATCH 08/12] Tweak namings --- .../importers/tryImportTemplatePreset.test.ts | 9 +++++ .../cli/importers/tryImportTemplatePreset.ts | 6 +++- .../cli/initialize/runModeInitialize.test.ts | 33 ++++++++++++------- .../src/cli/initialize/runModeInitialize.ts | 14 ++------ .../cli/migrate/clearTemplateFiles.test.ts | 8 ++--- .../src/cli/migrate/runModeMigrate.test.ts | 22 ++++++------- .../create/src/cli/migrate/runModeMigrate.ts | 10 +++--- ...test.ts => tryLoadMigrationPreset.test.ts} | 12 +++---- ...ionPreset.ts => tryLoadMigrationPreset.ts} | 2 +- 9 files changed, 64 insertions(+), 52 deletions(-) rename packages/create/src/cli/migrate/{loadMigrationPreset.test.ts => tryLoadMigrationPreset.test.ts} (86%) rename packages/create/src/cli/migrate/{loadMigrationPreset.ts => tryLoadMigrationPreset.ts} (94%) diff --git a/packages/create/src/cli/importers/tryImportTemplatePreset.test.ts b/packages/create/src/cli/importers/tryImportTemplatePreset.test.ts index d6a4f084..9510ef4d 100644 --- a/packages/create/src/cli/importers/tryImportTemplatePreset.test.ts +++ b/packages/create/src/cli/importers/tryImportTemplatePreset.test.ts @@ -27,6 +27,15 @@ vi.mock("../tryImportWithPredicate.js", () => ({ })); describe("tryImportTemplatePreset", () => { + it("returns an error when from is undefined", async () => { + const actual = await tryImportTemplatePreset(undefined); + + expect(actual).toEqual( + new Error("Please specify a package to create from."), + ); + expect(mockTryImportWithPredicate).not.toHaveBeenCalled(); + }); + it("returns the error when tryImportWithPredicate resolves with an error", async () => { const error = new Error("Oh no!"); diff --git a/packages/create/src/cli/importers/tryImportTemplatePreset.ts b/packages/create/src/cli/importers/tryImportTemplatePreset.ts index f0eb5e21..f5b4a412 100644 --- a/packages/create/src/cli/importers/tryImportTemplatePreset.ts +++ b/packages/create/src/cli/importers/tryImportTemplatePreset.ts @@ -6,9 +6,13 @@ import { tryImportWithPredicate } from "../tryImportWithPredicate.js"; import { tryImportAndInstallIfNecessary } from "./tryImportAndInstallIfNecessary.js"; export async function tryImportTemplatePreset( - from: string, + from: string | undefined, requestedPreset?: string, ) { + if (!from) { + return new Error("Please specify a package to create from."); + } + const template = await tryImportWithPredicate( tryImportAndInstallIfNecessary, from, diff --git a/packages/create/src/cli/initialize/runModeInitialize.test.ts b/packages/create/src/cli/initialize/runModeInitialize.test.ts index df0b42b7..4be424fe 100644 --- a/packages/create/src/cli/initialize/runModeInitialize.test.ts +++ b/packages/create/src/cli/initialize/runModeInitialize.test.ts @@ -21,6 +21,14 @@ vi.mock("../importers/tryImportTemplatePreset.js", () => ({ }, })); +const mockPromptForBaseOptions = vi.fn(); + +vi.mock("../prompts/promptForBaseOptions.js", () => ({ + get promptForBaseOptions() { + return mockPromptForBaseOptions; + }, +})); + const mockPromptForInitializationDirectory = vi.fn(); vi.mock("../prompts/promptForInitializationDirectory.js", () => ({ @@ -43,17 +51,6 @@ const template = createTemplate({ }); describe("runModeInitialize", () => { - it("returns a CLI error when no positional from can be found", async () => { - const actual = await runModeInitialize({ args: [] }); - - expect(actual).toEqual({ - outro: "Please specify a package to create from.", - status: CLIStatus.Error, - }); - - expect(mockTryImportTemplatePreset).not.toHaveBeenCalled(); - }); - it("returns the error when importing tryImportTemplatePreset resolves with an error", async () => { const message = "Oh no!"; @@ -93,4 +90,18 @@ describe("runModeInitialize", () => { expect(actual).toEqual({ status: CLIStatus.Cancelled }); }); + + it("returns the cancellation when promptForBaseOptions is cancelled", async () => { + const cancellation = Symbol.for("cancel"); + mockTryImportTemplatePreset.mockResolvedValueOnce({ preset, template }); + mockPromptForInitializationDirectory.mockResolvedValueOnce("."); + mockPromptForBaseOptions.mockResolvedValueOnce(cancellation); + mockIsCancel.mockReturnValueOnce(false).mockReturnValueOnce(true); + + const actual = await runModeInitialize({ + args: ["node", "create", "my-app"], + }); + + expect(actual).toEqual({ status: CLIStatus.Cancelled }); + }); }); diff --git a/packages/create/src/cli/initialize/runModeInitialize.ts b/packages/create/src/cli/initialize/runModeInitialize.ts index 4a261e78..d734445b 100644 --- a/packages/create/src/cli/initialize/runModeInitialize.ts +++ b/packages/create/src/cli/initialize/runModeInitialize.ts @@ -32,13 +32,6 @@ export async function runModeInitialize({ from = findPositionalFrom(args), preset: requestedPreset, }: RunModeInitializeSettings): Promise { - if (!from) { - return { - outro: "Please specify a package to create from.", - status: CLIStatus.Error, - }; - } - const loaded = await tryImportTemplatePreset(from, requestedPreset); if (loaded instanceof Error) { return { @@ -70,7 +63,6 @@ export async function runModeInitialize({ existingOptions: parseZodArgs(args, loaded.preset.base.options), system, }); - if (prompts.isCancel(options)) { return { status: CLIStatus.Cancelled }; } @@ -90,12 +82,10 @@ export async function runModeInitialize({ }, ); - const presetDescription = `the ${loaded.preset.about.name} preset`; - const creation = await runSpinnerTask( display, - `Running ${presetDescription}`, - `Ran ${presetDescription}`, + `Running the ${loaded.preset.about.name} preset`, + `Ran the ${loaded.preset.about.name} preset`, async () => await runPreset(loaded.preset, { ...system, diff --git a/packages/create/src/cli/migrate/clearTemplateFiles.test.ts b/packages/create/src/cli/migrate/clearTemplateFiles.test.ts index b2bf10df..4d9cf776 100644 --- a/packages/create/src/cli/migrate/clearTemplateFiles.test.ts +++ b/packages/create/src/cli/migrate/clearTemplateFiles.test.ts @@ -36,9 +36,9 @@ describe("clearTemplateFiles", () => { await clearTemplateFiles(directory); - expect(mockRm).not.toHaveBeenCalledWith(".git"); - expect(mockRm).toHaveBeenCalledWith(".github"); - expect(mockRm).toHaveBeenCalledWith("src"); - expect(mockRm).toHaveBeenCalledWith("package.json"); + expect(mockRm).not.toHaveBeenCalledWith(".git", expect.any(Object)); + expect(mockRm).toHaveBeenCalledWith(".github", { recursive: true }); + expect(mockRm).toHaveBeenCalledWith("src", { recursive: true }); + expect(mockRm).toHaveBeenCalledWith("package.json", { recursive: true }); }); }); diff --git a/packages/create/src/cli/migrate/runModeMigrate.test.ts b/packages/create/src/cli/migrate/runModeMigrate.test.ts index 923ca6c1..cde1f582 100644 --- a/packages/create/src/cli/migrate/runModeMigrate.test.ts +++ b/packages/create/src/cli/migrate/runModeMigrate.test.ts @@ -61,11 +61,11 @@ vi.mock("./getForkedTemplateLocator.js", () => ({ }, })); -const mockLoadMigrationPreset = vi.fn(); +const mockTryLoadMigrationPreset = vi.fn(); -vi.mock("./loadMigrationPreset.js", () => ({ - get loadMigrationPreset() { - return mockLoadMigrationPreset; +vi.mock("./tryLoadMigrationPreset.js", () => ({ + get tryLoadMigrationPreset() { + return mockTryLoadMigrationPreset; }, })); @@ -83,10 +83,10 @@ const preset = base.createPreset({ }); describe("runModeMigrate", () => { - it("returns the error when loadMigrationPreset resolves with an error", async () => { + it("returns the error when tryLoadMigrationPreset resolves with an error", async () => { const error = new Error("Oh no!"); - mockLoadMigrationPreset.mockResolvedValueOnce(error); + mockTryLoadMigrationPreset.mockResolvedValueOnce(error); const actual = await runModeMigrate({ args: [], @@ -99,8 +99,8 @@ describe("runModeMigrate", () => { }); }); - it("returns a cancellation status when loadMigrationPreset resolves with an error", async () => { - mockLoadMigrationPreset.mockResolvedValueOnce({}); + it("returns a cancellation status when tryLoadMigrationPreset resolves with an error", async () => { + mockTryLoadMigrationPreset.mockResolvedValueOnce({}); mockIsCancel.mockReturnValueOnce(true); const actual = await runModeMigrate({ @@ -114,7 +114,7 @@ describe("runModeMigrate", () => { }); it("doesn't clear template files or tags when no forked template locator is available", async () => { - mockLoadMigrationPreset.mockResolvedValueOnce({ preset }); + mockTryLoadMigrationPreset.mockResolvedValueOnce({ preset }); mockIsCancel.mockReturnValueOnce(false); mockGetForkedTemplateLocator.mockResolvedValueOnce(undefined); @@ -128,7 +128,7 @@ describe("runModeMigrate", () => { }); it("clears template files and tags when a forked template locator is available", async () => { - mockLoadMigrationPreset.mockResolvedValueOnce({ preset }); + mockTryLoadMigrationPreset.mockResolvedValueOnce({ preset }); mockIsCancel.mockReturnValueOnce(false); mockGetForkedTemplateLocator.mockResolvedValueOnce("a/b"); @@ -142,7 +142,7 @@ describe("runModeMigrate", () => { }); it("returns a CLI success when importing and running the preset succeeds", async () => { - mockLoadMigrationPreset.mockResolvedValueOnce({ preset }); + mockTryLoadMigrationPreset.mockResolvedValueOnce({ preset }); mockIsCancel.mockReturnValueOnce(false); const actual = await runModeMigrate({ diff --git a/packages/create/src/cli/migrate/runModeMigrate.ts b/packages/create/src/cli/migrate/runModeMigrate.ts index 3dd1c802..bd1938fd 100644 --- a/packages/create/src/cli/migrate/runModeMigrate.ts +++ b/packages/create/src/cli/migrate/runModeMigrate.ts @@ -12,7 +12,7 @@ import { CLIStatus } from "../status.js"; import { ModeResults } from "../types.js"; import { clearTemplateFiles } from "./clearTemplateFiles.js"; import { getForkedTemplateLocator } from "./getForkedTemplateLocator.js"; -import { loadMigrationPreset } from "./loadMigrationPreset.js"; +import { tryLoadMigrationPreset } from "./tryLoadMigrationPreset.js"; export interface RunModeMigrateSettings { args: string[]; @@ -29,7 +29,7 @@ export async function runModeMigrate({ from = findPositionalFrom(args), preset: requestedPreset, }: RunModeMigrateSettings): Promise { - const loaded = await loadMigrationPreset({ + const loaded = await tryLoadMigrationPreset({ configFile, from, requestedPreset, @@ -72,12 +72,10 @@ export async function runModeMigrate({ return { status: CLIStatus.Cancelled }; } - const presetDescription = `the ${loaded.preset.about.name} preset`; - await runSpinnerTask( display, - `Running ${presetDescription}`, - `Ran ${presetDescription}`, + `Running the ${loaded.preset.about.name} preset`, + `Ran the ${loaded.preset.about.name} preset`, async () => { await runPreset(loaded.preset, { ...system, diff --git a/packages/create/src/cli/migrate/loadMigrationPreset.test.ts b/packages/create/src/cli/migrate/tryLoadMigrationPreset.test.ts similarity index 86% rename from packages/create/src/cli/migrate/loadMigrationPreset.test.ts rename to packages/create/src/cli/migrate/tryLoadMigrationPreset.test.ts index 6aa190bb..887a8299 100644 --- a/packages/create/src/cli/migrate/loadMigrationPreset.test.ts +++ b/packages/create/src/cli/migrate/tryLoadMigrationPreset.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; -import { loadMigrationPreset } from "./loadMigrationPreset.js"; +import { tryLoadMigrationPreset } from "./tryLoadMigrationPreset.js"; const mockTryImportConfig = vi.fn(); @@ -27,9 +27,9 @@ vi.mock("../importers/tryImportTemplatePreset.js", () => ({ }, })); -describe("loadMigrationPreset", () => { +describe("tryLoadMigrationPreset", () => { it("returns a CLI error when configFile and from are undefined", async () => { - const actual = await loadMigrationPreset({ + const actual = await tryLoadMigrationPreset({ configFile: undefined, }); @@ -44,7 +44,7 @@ describe("loadMigrationPreset", () => { }); it("returns a CLI error when configFile and from are both defined", async () => { - const actual = await loadMigrationPreset({ + const actual = await tryLoadMigrationPreset({ configFile: "create.config.js", from: "my-app", }); @@ -64,7 +64,7 @@ describe("loadMigrationPreset", () => { mockTryImportConfig.mockResolvedValueOnce(expected); - const actual = await loadMigrationPreset({ + const actual = await tryLoadMigrationPreset({ configFile: "create.config.js", }); @@ -77,7 +77,7 @@ describe("loadMigrationPreset", () => { mockTryImportTemplatePreset.mockResolvedValueOnce(expected); - const actual = await loadMigrationPreset({ + const actual = await tryLoadMigrationPreset({ from: "my-app", }); diff --git a/packages/create/src/cli/migrate/loadMigrationPreset.ts b/packages/create/src/cli/migrate/tryLoadMigrationPreset.ts similarity index 94% rename from packages/create/src/cli/migrate/loadMigrationPreset.ts rename to packages/create/src/cli/migrate/tryLoadMigrationPreset.ts index 6022d74a..5ed5a29b 100644 --- a/packages/create/src/cli/migrate/loadMigrationPreset.ts +++ b/packages/create/src/cli/migrate/tryLoadMigrationPreset.ts @@ -7,7 +7,7 @@ export interface MigrationLoadSettings { requestedPreset?: string; } -export async function loadMigrationPreset({ +export async function tryLoadMigrationPreset({ configFile, from, requestedPreset, From b096293ee63ed976ca64b385f2f049143a5a57c7 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 30 Dec 2024 23:35:21 -0500 Subject: [PATCH 09/12] Fix up tests --- .../cli/initialize/runModeInitialize.test.ts | 15 ++++++----- .../src/cli/migrate/runModeMigrate.test.ts | 27 +++++++++++++++++-- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/packages/create/src/cli/initialize/runModeInitialize.test.ts b/packages/create/src/cli/initialize/runModeInitialize.test.ts index 4be424fe..b4afe851 100644 --- a/packages/create/src/cli/initialize/runModeInitialize.test.ts +++ b/packages/create/src/cli/initialize/runModeInitialize.test.ts @@ -11,6 +11,7 @@ vi.mock("@clack/prompts", () => ({ get isCancel() { return mockIsCancel; }, + spinner: vi.fn(), })); const mockTryImportTemplatePreset = vi.fn(); @@ -67,8 +68,7 @@ describe("runModeInitialize", () => { }); it("returns the cancellation when tryImportTemplatePreset is cancelled", async () => { - const cancellation = Symbol.for("cancel"); - mockTryImportTemplatePreset.mockResolvedValueOnce(cancellation); + mockTryImportTemplatePreset.mockResolvedValueOnce(Symbol("")); mockIsCancel.mockReturnValueOnce(true); const actual = await runModeInitialize({ @@ -79,9 +79,8 @@ describe("runModeInitialize", () => { }); it("returns the cancellation when promptForInitializationDirectory is cancelled", async () => { - const cancellation = Symbol.for("cancel"); mockTryImportTemplatePreset.mockResolvedValueOnce({ preset, template }); - mockPromptForInitializationDirectory.mockResolvedValueOnce(cancellation); + mockPromptForInitializationDirectory.mockResolvedValueOnce(Symbol("")); mockIsCancel.mockReturnValueOnce(false).mockReturnValueOnce(true); const actual = await runModeInitialize({ @@ -92,11 +91,13 @@ describe("runModeInitialize", () => { }); it("returns the cancellation when promptForBaseOptions is cancelled", async () => { - const cancellation = Symbol.for("cancel"); mockTryImportTemplatePreset.mockResolvedValueOnce({ preset, template }); mockPromptForInitializationDirectory.mockResolvedValueOnce("."); - mockPromptForBaseOptions.mockResolvedValueOnce(cancellation); - mockIsCancel.mockReturnValueOnce(false).mockReturnValueOnce(true); + mockPromptForBaseOptions.mockResolvedValueOnce(Symbol("")); + mockIsCancel + .mockReturnValueOnce(false) + .mockReturnValueOnce(false) + .mockReturnValueOnce(true); const actual = await runModeInitialize({ args: ["node", "create", "my-app"], diff --git a/packages/create/src/cli/migrate/runModeMigrate.test.ts b/packages/create/src/cli/migrate/runModeMigrate.test.ts index cde1f582..77908773 100644 --- a/packages/create/src/cli/migrate/runModeMigrate.test.ts +++ b/packages/create/src/cli/migrate/runModeMigrate.test.ts @@ -37,6 +37,14 @@ vi.mock("../display/createClackDisplay.js", () => ({ }), })); +const mockPromptForBaseOptions = vi.fn(); + +vi.mock("../prompts/promptForBaseOptions.js", () => ({ + get promptForBaseOptions() { + return mockPromptForBaseOptions; + }, +})); + const mockClearLocalGitTags = vi.fn(); vi.mock("../clearLocalGitTags.js", () => ({ @@ -108,9 +116,24 @@ describe("runModeMigrate", () => { configFile: undefined, }); - expect(actual).toEqual({ - status: CLIStatus.Cancelled, + expect(actual).toEqual({ status: CLIStatus.Cancelled }); + }); + + it("returns the cancellation when promptForBaseOptions is cancelled", async () => { + mockTryLoadMigrationPreset.mockResolvedValueOnce({ preset }); + mockIsCancel.mockReturnValueOnce(false).mockReturnValueOnce(true); + mockGetForkedTemplateLocator.mockResolvedValueOnce(undefined); + mockPromptForBaseOptions.mockResolvedValueOnce(Symbol.for("cancel")); + + const actual = await runModeMigrate({ + args: [], + configFile: "create.config.js", }); + + expect(mockClearTemplateFiles).not.toHaveBeenCalled(); + expect(mockClearLocalGitTags).not.toHaveBeenCalled(); + + expect(actual).toEqual({ status: CLIStatus.Cancelled }); }); it("doesn't clear template files or tags when no forked template locator is available", async () => { From e33379b6f62f8449dbfb004c64c54c50b59fabc5 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 30 Dec 2024 23:37:19 -0500 Subject: [PATCH 10/12] Fix up test auth --- packages/create/src/cli/initialize/runModeInitialize.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/create/src/cli/initialize/runModeInitialize.test.ts b/packages/create/src/cli/initialize/runModeInitialize.test.ts index b4afe851..22c25680 100644 --- a/packages/create/src/cli/initialize/runModeInitialize.test.ts +++ b/packages/create/src/cli/initialize/runModeInitialize.test.ts @@ -14,6 +14,10 @@ vi.mock("@clack/prompts", () => ({ spinner: vi.fn(), })); +vi.mock("../../system/createSystemContextWithAuth.js", () => ({ + createSystemContextWithAuth: vi.fn().mockResolvedValue({}), +})); + const mockTryImportTemplatePreset = vi.fn(); vi.mock("../importers/tryImportTemplatePreset.js", () => ({ From e72ff5d23519d3309e0e5f0ba0da8e13a858bb78 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Tue, 31 Dec 2024 00:07:30 -0500 Subject: [PATCH 11/12] Also reset initial commit in template hydration migration --- .../src/cli/createInitialCommit.test.ts | 25 +++++ .../create/src/cli/createInitialCommit.ts | 14 +++ .../initialize/createTrackingBranches.test.ts | 9 -- .../cli/initialize/createTrackingBranches.ts | 3 - .../cli/initialize/runModeInitialize.test.ts | 91 ++++++++++++++++++- .../src/cli/initialize/runModeInitialize.ts | 2 + .../src/cli/migrate/runModeMigrate.test.ts | 16 +++- .../create/src/cli/migrate/runModeMigrate.ts | 19 +++- .../src/predicates/isCreateConfig.test.ts | 4 +- 9 files changed, 165 insertions(+), 18 deletions(-) create mode 100644 packages/create/src/cli/createInitialCommit.test.ts create mode 100644 packages/create/src/cli/createInitialCommit.ts diff --git a/packages/create/src/cli/createInitialCommit.test.ts b/packages/create/src/cli/createInitialCommit.test.ts new file mode 100644 index 00000000..b543f79c --- /dev/null +++ b/packages/create/src/cli/createInitialCommit.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, test, vi } from "vitest"; + +import { createInitialCommit } from "./createInitialCommit.js"; + +describe("createInitialCommit", () => { + test("commands", async () => { + const runner = vi.fn(); + + await createInitialCommit(runner); + + expect(runner.mock.calls).toMatchInlineSnapshot(` + [ + [ + "git add -A", + ], + [ + "git commit --message feat:\\ initialized\\ repo\\ ✨ --no-gpg-sign", + ], + [ + "git push -u origin main --force", + ], + ] + `); + }); +}); diff --git a/packages/create/src/cli/createInitialCommit.ts b/packages/create/src/cli/createInitialCommit.ts new file mode 100644 index 00000000..f05adba2 --- /dev/null +++ b/packages/create/src/cli/createInitialCommit.ts @@ -0,0 +1,14 @@ +import { SystemRunner } from "../types/system.js"; + +export async function createInitialCommit( + runner: SystemRunner, + amend?: boolean, +) { + for (const command of [ + `git add -A`, + `git commit --message feat:\\ initialized\\ repo\\ ✨ ${amend ? "--amend " : ""}--no-gpg-sign`, + `git push -u origin main --force`, + ]) { + await runner(command); + } +} diff --git a/packages/create/src/cli/initialize/createTrackingBranches.test.ts b/packages/create/src/cli/initialize/createTrackingBranches.test.ts index 2db9799f..5b56542b 100644 --- a/packages/create/src/cli/initialize/createTrackingBranches.test.ts +++ b/packages/create/src/cli/initialize/createTrackingBranches.test.ts @@ -19,15 +19,6 @@ describe("createTrackingBranches", () => { [ "git remote add origin https://github.com/TestOwner/test-repository", ], - [ - "git add -A", - ], - [ - "git commit --message "feat:\\ initialized\\ repo\\ ✨" --no-gpg-sign", - ], - [ - "git push -u origin main --force", - ], ] `); }); diff --git a/packages/create/src/cli/initialize/createTrackingBranches.ts b/packages/create/src/cli/initialize/createTrackingBranches.ts index d3c070b3..d89fd5d6 100644 --- a/packages/create/src/cli/initialize/createTrackingBranches.ts +++ b/packages/create/src/cli/initialize/createTrackingBranches.ts @@ -8,9 +8,6 @@ export async function createTrackingBranches( for (const command of [ `git init`, `git remote add origin https://github.com/${owner}/${repository}`, - `git add -A`, - `git commit --message "feat:\\ initialized\\ repo\\ ✨" --no-gpg-sign`, - `git push -u origin main --force`, ]) { await runner(command); } diff --git a/packages/create/src/cli/initialize/runModeInitialize.test.ts b/packages/create/src/cli/initialize/runModeInitialize.test.ts index 22c25680..30c278d5 100644 --- a/packages/create/src/cli/initialize/runModeInitialize.test.ts +++ b/packages/create/src/cli/initialize/runModeInitialize.test.ts @@ -14,8 +14,35 @@ vi.mock("@clack/prompts", () => ({ spinner: vi.fn(), })); +const mockRunPreset = vi.fn(); + +vi.mock("../../runners/runPreset.js", () => ({ + get runPreset() { + return mockRunPreset; + }, +})); + vi.mock("../../system/createSystemContextWithAuth.js", () => ({ - createSystemContextWithAuth: vi.fn().mockResolvedValue({}), + createSystemContextWithAuth: vi.fn().mockResolvedValue({ + fetchers: {}, + }), +})); + +vi.mock("../createInitialCommit.js", () => ({ + createInitialCommit: vi.fn(), +})); + +vi.mock("../clearLocalGitTags.js", () => ({ + clearLocalGitTags: vi.fn(), +})); + +vi.mock("../display/createClackDisplay.js", () => ({ + createClackDisplay: () => ({ + spinner: { + start: vi.fn(), + stop: vi.fn(), + }, + }), })); const mockTryImportTemplatePreset = vi.fn(); @@ -42,6 +69,14 @@ vi.mock("../prompts/promptForInitializationDirectory.js", () => ({ }, })); +vi.mock("./createRepositoryOnGitHub.js", () => ({ + createRepositoryOnGitHub: vi.fn(), +})); + +vi.mock("./createTrackingBranches.js", () => ({ + createTrackingBranches: vi.fn(), +})); + const base = createBase({ options: {}, }); @@ -109,4 +144,58 @@ describe("runModeInitialize", () => { expect(actual).toEqual({ status: CLIStatus.Cancelled }); }); + + it("returns a CLI success and makes an absolute directory relative when importing and running the preset succeeds", async () => { + const directory = "local-directory"; + const suggestions = ["abc"]; + + mockTryImportTemplatePreset.mockResolvedValueOnce({ preset, template }); + mockPromptForInitializationDirectory.mockResolvedValueOnce(directory); + mockPromptForBaseOptions.mockResolvedValueOnce({ + owner: "TestOwner", + repository: "test-repository", + }); + mockIsCancel.mockReturnValue(false); + mockRunPreset.mockResolvedValueOnce({ + suggestions, + }); + + const actual = await runModeInitialize({ + args: ["node", "create", "my-app"], + }); + + expect(actual).toEqual({ + outro: "Your new repository is ready in: ./local-directory", + status: CLIStatus.Success, + suggestions, + }); + expect(mockRunPreset).toHaveBeenCalledWith(preset, expect.any(Object)); + }); + + it("returns a CLI success and keeps a relative directory when importing and running the preset succeeds", async () => { + const directory = "./local-directory"; + const suggestions = ["abc"]; + + mockTryImportTemplatePreset.mockResolvedValueOnce({ preset, template }); + mockPromptForInitializationDirectory.mockResolvedValueOnce(directory); + mockPromptForBaseOptions.mockResolvedValueOnce({ + owner: "TestOwner", + repository: "test-repository", + }); + mockIsCancel.mockReturnValue(false); + mockRunPreset.mockResolvedValueOnce({ + suggestions, + }); + + const actual = await runModeInitialize({ + args: ["node", "create", "my-app"], + }); + + expect(actual).toEqual({ + outro: "Your new repository is ready in: ./local-directory", + status: CLIStatus.Success, + suggestions, + }); + expect(mockRunPreset).toHaveBeenCalledWith(preset, expect.any(Object)); + }); }); diff --git a/packages/create/src/cli/initialize/runModeInitialize.ts b/packages/create/src/cli/initialize/runModeInitialize.ts index d734445b..a4e2249d 100644 --- a/packages/create/src/cli/initialize/runModeInitialize.ts +++ b/packages/create/src/cli/initialize/runModeInitialize.ts @@ -4,6 +4,7 @@ import chalk from "chalk"; import { runPreset } from "../../runners/runPreset.js"; import { createSystemContextWithAuth } from "../../system/createSystemContextWithAuth.js"; import { clearLocalGitTags } from "../clearLocalGitTags.js"; +import { createInitialCommit } from "../createInitialCommit.js"; import { createClackDisplay } from "../display/createClackDisplay.js"; import { runSpinnerTask } from "../display/runSpinnerTask.js"; import { findPositionalFrom } from "../findPositionalFrom.js"; @@ -101,6 +102,7 @@ export async function runModeInitialize({ "Prepared local repository", async () => { await createTrackingBranches(options, system.runner); + await createInitialCommit(system.runner); await clearLocalGitTags(system.runner); }, ); diff --git a/packages/create/src/cli/migrate/runModeMigrate.test.ts b/packages/create/src/cli/migrate/runModeMigrate.test.ts index 77908773..6ed07fc7 100644 --- a/packages/create/src/cli/migrate/runModeMigrate.test.ts +++ b/packages/create/src/cli/migrate/runModeMigrate.test.ts @@ -53,6 +53,14 @@ vi.mock("../clearLocalGitTags.js", () => ({ }, })); +const mockCreateInitialCommit = vi.fn(); + +vi.mock("../createInitialCommit.js", () => ({ + get createInitialCommit() { + return mockCreateInitialCommit; + }, +})); + const mockClearTemplateFiles = vi.fn(); vi.mock("./clearTemplateFiles.js", () => ({ @@ -136,7 +144,7 @@ describe("runModeMigrate", () => { expect(actual).toEqual({ status: CLIStatus.Cancelled }); }); - it("doesn't clear template files or tags when no forked template locator is available", async () => { + it("doesn't clear Git or template files when no forked template locator is available", async () => { mockTryLoadMigrationPreset.mockResolvedValueOnce({ preset }); mockIsCancel.mockReturnValueOnce(false); mockGetForkedTemplateLocator.mockResolvedValueOnce(undefined); @@ -148,9 +156,10 @@ describe("runModeMigrate", () => { expect(mockClearTemplateFiles).not.toHaveBeenCalled(); expect(mockClearLocalGitTags).not.toHaveBeenCalled(); + expect(mockCreateInitialCommit).not.toHaveBeenCalled(); }); - it("clears template files and tags when a forked template locator is available", async () => { + it("clears Git and template files when a forked template locator is available", async () => { mockTryLoadMigrationPreset.mockResolvedValueOnce({ preset }); mockIsCancel.mockReturnValueOnce(false); mockGetForkedTemplateLocator.mockResolvedValueOnce("a/b"); @@ -162,6 +171,7 @@ describe("runModeMigrate", () => { expect(mockClearTemplateFiles).toHaveBeenCalled(); expect(mockClearLocalGitTags).toHaveBeenCalled(); + expect(mockCreateInitialCommit).toHaveBeenCalled(); }); it("returns a CLI success when importing and running the preset succeeds", async () => { @@ -173,10 +183,10 @@ describe("runModeMigrate", () => { configFile: "create.config.js", }); - expect(mockRunPreset).toHaveBeenCalledWith(preset, expect.any(Object)); expect(actual).toEqual({ outro: `You might want to commit any changes.`, status: CLIStatus.Success, }); + expect(mockRunPreset).toHaveBeenCalledWith(preset, expect.any(Object)); }); }); diff --git a/packages/create/src/cli/migrate/runModeMigrate.ts b/packages/create/src/cli/migrate/runModeMigrate.ts index bd1938fd..6e231e5a 100644 --- a/packages/create/src/cli/migrate/runModeMigrate.ts +++ b/packages/create/src/cli/migrate/runModeMigrate.ts @@ -3,6 +3,7 @@ import * as prompts from "@clack/prompts"; import { runPreset } from "../../runners/runPreset.js"; import { createSystemContextWithAuth } from "../../system/createSystemContextWithAuth.js"; import { clearLocalGitTags } from "../clearLocalGitTags.js"; +import { createInitialCommit } from "../createInitialCommit.js"; import { createClackDisplay } from "../display/createClackDisplay.js"; import { runSpinnerTask } from "../display/runSpinnerTask.js"; import { findPositionalFrom } from "../findPositionalFrom.js"; @@ -86,8 +87,24 @@ export async function runModeMigrate({ }, ); + if (!templateLocator) { + return { + outro: `You might want to commit any changes.`, + status: CLIStatus.Success, + }; + } + + await runSpinnerTask( + display, + "Creating initial commit", + "Created initial commit", + async () => { + await createInitialCommit(system.runner, true); + }, + ); + return { - outro: `You might want to commit any changes.`, + outro: `Done!`, status: CLIStatus.Success, }; } diff --git a/packages/create/src/predicates/isCreateConfig.test.ts b/packages/create/src/predicates/isCreateConfig.test.ts index cca1b8ad..1d9a9ede 100644 --- a/packages/create/src/predicates/isCreateConfig.test.ts +++ b/packages/create/src/predicates/isCreateConfig.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "vitest"; +import { createConfig } from "../config/createConfig.js"; import { createBase } from "../creators/createBase.js"; import { isCreateConfig } from "./isCreateConfig.js"; @@ -26,7 +27,8 @@ describe("isCreateConfig", () => { [{ preset: {}, settings: {} }, false], [{ preset: {}, settings: {} }, false], [{ preset }, true], - [{ preset, settings: {} }, true], + [createConfig(preset), true], + [createConfig(preset, {}), true], ])("%j", (input, expected) => { const actual = isCreateConfig(input); From cee58d3b88dbc273f2ec050ce5c1fa8315a0fdf8 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Tue, 31 Dec 2024 00:09:22 -0500 Subject: [PATCH 12/12] --amend test --- .../src/cli/createInitialCommit.test.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/create/src/cli/createInitialCommit.test.ts b/packages/create/src/cli/createInitialCommit.test.ts index b543f79c..a45004d4 100644 --- a/packages/create/src/cli/createInitialCommit.test.ts +++ b/packages/create/src/cli/createInitialCommit.test.ts @@ -3,7 +3,7 @@ import { describe, expect, test, vi } from "vitest"; import { createInitialCommit } from "./createInitialCommit.js"; describe("createInitialCommit", () => { - test("commands", async () => { + test("runs without --amend when amend is falsy", async () => { const runner = vi.fn(); await createInitialCommit(runner); @@ -22,4 +22,24 @@ describe("createInitialCommit", () => { ] `); }); + + test("runs with --amend when amend is true", async () => { + const runner = vi.fn(); + + await createInitialCommit(runner, true); + + expect(runner.mock.calls).toMatchInlineSnapshot(` + [ + [ + "git add -A", + ], + [ + "git commit --message feat:\\ initialized\\ repo\\ ✨ --amend --no-gpg-sign", + ], + [ + "git push -u origin main --force", + ], + ] + `); + }); });