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/createInitialCommit.test.ts b/packages/create/src/cli/createInitialCommit.test.ts new file mode 100644 index 00000000..a45004d4 --- /dev/null +++ b/packages/create/src/cli/createInitialCommit.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, test, vi } from "vitest"; + +import { createInitialCommit } from "./createInitialCommit.js"; + +describe("createInitialCommit", () => { + test("runs without --amend when amend is falsy", 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", + ], + ] + `); + }); + + 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", + ], + ] + `); + }); +}); 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/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/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/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 74% rename from packages/create/src/modes/createTrackingBranches.test.ts rename to packages/create/src/cli/initialize/createTrackingBranches.test.ts index 2db9799f..5b56542b 100644 --- a/packages/create/src/modes/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/modes/createTrackingBranches.ts b/packages/create/src/cli/initialize/createTrackingBranches.ts similarity index 53% rename from packages/create/src/modes/createTrackingBranches.ts rename to packages/create/src/cli/initialize/createTrackingBranches.ts index 41d43121..d89fd5d6 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, @@ -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 af1bad22..30c278d5 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"; @@ -9,6 +11,38 @@ vi.mock("@clack/prompts", () => ({ get isCancel() { return mockIsCancel; }, + 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({ + 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(); @@ -19,18 +53,44 @@ vi.mock("../importers/tryImportTemplatePreset.js", () => ({ }, })); -describe("runModeInitialize", () => { - it("returns a CLI error when no positional from can be found", async () => { - const actual = await runModeInitialize({ args: [] }); +const mockPromptForBaseOptions = vi.fn(); - expect(actual).toEqual({ - outro: "Please specify a package to create from.", - status: CLIStatus.Error, - }); +vi.mock("../prompts/promptForBaseOptions.js", () => ({ + get promptForBaseOptions() { + return mockPromptForBaseOptions; + }, +})); - expect(mockTryImportTemplatePreset).not.toHaveBeenCalled(); - }); +const mockPromptForInitializationDirectory = vi.fn(); +vi.mock("../prompts/promptForInitializationDirectory.js", () => ({ + get promptForInitializationDirectory() { + return mockPromptForInitializationDirectory; + }, +})); + +vi.mock("./createRepositoryOnGitHub.js", () => ({ + createRepositoryOnGitHub: vi.fn(), +})); + +vi.mock("./createTrackingBranches.js", () => ({ + createTrackingBranches: vi.fn(), +})); + +const base = createBase({ + options: {}, +}); + +const preset = base.createPreset({ + about: { name: "Test" }, + blocks: [], +}); + +const template = createTemplate({ + presets: [], +}); + +describe("runModeInitialize", () => { it("returns the error when importing tryImportTemplatePreset resolves with an error", async () => { const message = "Oh no!"; @@ -47,8 +107,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({ @@ -57,4 +116,86 @@ describe("runModeInitialize", () => { expect(actual).toEqual({ status: CLIStatus.Cancelled }); }); + + it("returns the cancellation when promptForInitializationDirectory is cancelled", async () => { + mockTryImportTemplatePreset.mockResolvedValueOnce({ preset, template }); + mockPromptForInitializationDirectory.mockResolvedValueOnce(Symbol("")); + mockIsCancel.mockReturnValueOnce(false).mockReturnValueOnce(true); + + const actual = await runModeInitialize({ + args: ["node", "create", "my-app"], + }); + + expect(actual).toEqual({ status: CLIStatus.Cancelled }); + }); + + it("returns the cancellation when promptForBaseOptions is cancelled", async () => { + mockTryImportTemplatePreset.mockResolvedValueOnce({ preset, template }); + mockPromptForInitializationDirectory.mockResolvedValueOnce("."); + mockPromptForBaseOptions.mockResolvedValueOnce(Symbol("")); + mockIsCancel + .mockReturnValueOnce(false) + .mockReturnValueOnce(false) + .mockReturnValueOnce(true); + + const actual = await runModeInitialize({ + args: ["node", "create", "my-app"], + }); + + 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 a22121e7..a4e2249d 100644 --- a/packages/create/src/cli/initialize/runModeInitialize.ts +++ b/packages/create/src/cli/initialize/runModeInitialize.ts @@ -3,35 +3,36 @@ 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"; 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"; +import { createRepositoryOnGitHub } from "./createRepositoryOnGitHub.js"; +import { createTrackingBranches } from "./createTrackingBranches.js"; export interface RunModeInitializeSettings { args: string[]; 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 { - 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 { @@ -58,36 +59,60 @@ 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, }); - if (prompts.isCancel(options)) { return { status: CLIStatus.Cancelled }; } - display.spinner.start("Creating repository..."); + assertOptionsForInitialize(options); - const creation = await runPreset(loaded.preset, { - ...system, - directory, - mode: "initialize", - options, - }); + 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"); + const creation = await runSpinnerTask( + display, + `Running the ${loaded.preset.about.name} preset`, + `Ran the ${loaded.preset.about.name} preset`, + async () => + await runPreset(loaded.preset, { + ...system, + directory, + mode: "initialize", + options, + }), + ); + + await runSpinnerTask( + display, + "Preparing local repository", + "Prepared local repository", + async () => { + await createTrackingBranches(options, system.runner); + await createInitialCommit(system.runner); + await clearLocalGitTags(system.runner); + }, + ); return { outro: [ 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/migrate/clearTemplateFiles.test.ts b/packages/create/src/cli/migrate/clearTemplateFiles.test.ts new file mode 100644 index 00000000..4d9cf776 --- /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.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/clearTemplateFiles.ts b/packages/create/src/cli/migrate/clearTemplateFiles.ts new file mode 100644 index 00000000..fc3e702e --- /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, { recursive: true }); + }), + ); +} 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..c3df5e26 --- /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.mockReturnValueOnce(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.mockReturnValueOnce({ + 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.mockReturnValueOnce({ + 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.mockReturnValueOnce({ + 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..7c05b91a --- /dev/null +++ b/packages/create/src/cli/migrate/getForkedTemplateLocator.ts @@ -0,0 +1,25 @@ +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; + } + + const gitInfo = 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..6ed07fc7 100644 --- a/packages/create/src/cli/migrate/runModeMigrate.test.ts +++ b/packages/create/src/cli/migrate/runModeMigrate.test.ts @@ -37,19 +37,72 @@ vi.mock("../display/createClackDisplay.js", () => ({ }), })); -const mockLoadMigrationPreset = vi.fn(); +const mockPromptForBaseOptions = vi.fn(); -vi.mock("./loadMigrationPreset.js", () => ({ - get loadMigrationPreset() { - return mockLoadMigrationPreset; +vi.mock("../prompts/promptForBaseOptions.js", () => ({ + get promptForBaseOptions() { + return mockPromptForBaseOptions; }, })); +const mockClearLocalGitTags = vi.fn(); + +vi.mock("../clearLocalGitTags.js", () => ({ + get clearLocalGitTags() { + return mockClearLocalGitTags; + }, +})); + +const mockCreateInitialCommit = vi.fn(); + +vi.mock("../createInitialCommit.js", () => ({ + get createInitialCommit() { + return mockCreateInitialCommit; + }, +})); + +const mockClearTemplateFiles = vi.fn(); + +vi.mock("./clearTemplateFiles.js", () => ({ + get clearTemplateFiles() { + return mockClearTemplateFiles; + }, +})); + +const mockGetForkedTemplateLocator = vi.fn(); + +vi.mock("./getForkedTemplateLocator.js", () => ({ + get getForkedTemplateLocator() { + return mockGetForkedTemplateLocator; + }, +})); + +const mockTryLoadMigrationPreset = vi.fn(); + +vi.mock("./tryLoadMigrationPreset.js", () => ({ + get tryLoadMigrationPreset() { + return mockTryLoadMigrationPreset; + }, +})); + +const base = createBase({ + options: {}, + template: { + owner: "TestOwner", + repository: "test-repository", + }, +}); + +const preset = base.createPreset({ + about: { name: "Test" }, + blocks: [], +}); + 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: [], @@ -62,8 +115,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({ @@ -71,20 +124,58 @@ 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("returns a CLI success when importing and running the preset succeeds", async () => { - const base = createBase({ - options: {}, + 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); + + await runModeMigrate({ + args: [], + configFile: "create.config.js", }); - const preset = base.createPreset({ - about: { name: "Test" }, - blocks: [], + + expect(mockClearTemplateFiles).not.toHaveBeenCalled(); + expect(mockClearLocalGitTags).not.toHaveBeenCalled(); + expect(mockCreateInitialCommit).not.toHaveBeenCalled(); + }); + + it("clears Git and template files when a forked template locator is available", async () => { + mockTryLoadMigrationPreset.mockResolvedValueOnce({ preset }); + mockIsCancel.mockReturnValueOnce(false); + mockGetForkedTemplateLocator.mockResolvedValueOnce("a/b"); + + await runModeMigrate({ + args: [], + configFile: "create.config.js", }); - mockLoadMigrationPreset.mockResolvedValueOnce({ preset }); + + expect(mockClearTemplateFiles).toHaveBeenCalled(); + expect(mockClearLocalGitTags).toHaveBeenCalled(); + expect(mockCreateInitialCommit).toHaveBeenCalled(); + }); + + it("returns a CLI success when importing and running the preset succeeds", async () => { + mockTryLoadMigrationPreset.mockResolvedValueOnce({ preset }); mockIsCancel.mockReturnValueOnce(false); const actual = await runModeMigrate({ @@ -92,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 50e17b86..6e231e5a 100644 --- a/packages/create/src/cli/migrate/runModeMigrate.ts +++ b/packages/create/src/cli/migrate/runModeMigrate.ts @@ -1,13 +1,19 @@ 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 { createInitialCommit } from "../createInitialCommit.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 { loadMigrationPreset } from "./loadMigrationPreset.js"; +import { clearTemplateFiles } from "./clearTemplateFiles.js"; +import { getForkedTemplateLocator } from "./getForkedTemplateLocator.js"; +import { tryLoadMigrationPreset } from "./tryLoadMigrationPreset.js"; export interface RunModeMigrateSettings { args: string[]; @@ -24,7 +30,7 @@ export async function runModeMigrate({ from = findPositionalFrom(args), preset: requestedPreset, }: RunModeMigrateSettings): Promise { - const loaded = await loadMigrationPreset({ + const loaded = await tryLoadMigrationPreset({ configFile, from, requestedPreset, @@ -39,28 +45,66 @@ 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 templateLocator = + loaded.preset.base.template && + (await getForkedTemplateLocator(directory, loaded.preset.base.template)); - const options = await produceBase(loaded.preset.base, { - ...system, - directory, - }); + if (templateLocator) { + await runSpinnerTask( + display, + `Clearing from ${templateLocator}`, + `Cleared from ${templateLocator}`, + async () => { + await clearTemplateFiles(directory); + await clearLocalGitTags(system.runner); + }, + ); + } - await runPreset(loaded.preset, { - ...system, - directory, - mode: "migrate", - options, + 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 runSpinnerTask( + display, + `Running the ${loaded.preset.about.name} preset`, + `Ran the ${loaded.preset.about.name} preset`, + async () => { + await runPreset(loaded.preset, { + ...system, + directory, + mode: "migrate", + options, + }); + }, + ); + + if (!templateLocator) { + return { + outro: `You might want to commit any changes.`, + status: CLIStatus.Success, + }; + } - display.spinner.stop(`Ran ${description}.`); + 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/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, 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, 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..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", }, 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/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); 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/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: {}