Skip to content

Commit

Permalink
feat: fleshed out --help with template options (#117)
Browse files Browse the repository at this point in the history
* test: fill in missing tryImportConfig tests

The one root-level red line in coverage reports was bugging me.

* feat: fleshed out --help with template options

* Fix up linting and tests

* Deduplicated spinner logic into tryImportTemplate

* witho
  • Loading branch information
JoshuaKGoldberg authored Jan 6, 2025
1 parent 8a2853e commit 224268c
Show file tree
Hide file tree
Showing 51 changed files with 1,162 additions and 304 deletions.
19 changes: 6 additions & 13 deletions packages/create/src/cli/findPositionalFrom.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,12 @@ import { findPositionalFrom } from "./findPositionalFrom.js";

describe("findPositionalFrom", () => {
test.each([
[["my-app"], "create-my-app"],
[["create-my-app"], "create-my-app"],
[["/bin/node", "create"], undefined],
[["/bin/node", "create", "create-my-app"], "create-my-app"],
[["/bin/node", "create", "my-app"], "create-my-app"],
[["/bin/node", "create", "/create-my-app"], "/create-my-app"],
[["/bin/node", "create", "/my-app"], "/my-app"],
[["/bin/node", "create", "./create-my-app"], "./create-my-app"],
[["/bin/node", "create", "./my-app"], "./my-app"],
// npx create /repos/create-my-app
[["/repos/create-my-app"], "/repos/create-my-app"],
// npx create /repos/create-my-app --preset common
[["/repos/create-my-app", "--preset", "common"], "/repos/create-my-app"],
[[], undefined],
[["./create-typescript-app"], "./create-typescript-app"],
[["./create-typescript-app", "other"], "./create-typescript-app"],
[["./typescript-app"], "./typescript-app"],
[["create-typescript-app"], "create-typescript-app"],
[["typescript-app"], "create-typescript-app"],
])("%j", (input, expected) => {
expect(findPositionalFrom(input)).toBe(expected);
});
Expand Down
7 changes: 1 addition & 6 deletions packages/create/src/cli/findPositionalFrom.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import { isLocalPath } from "./utils.js";

export function findPositionalFrom(positionals: string[]) {
const indexOfDashes = positionals.findIndex((positional) =>
positional.startsWith("--"),
);
const potentials =
indexOfDashes === -1 ? positionals : positionals.slice(0, indexOfDashes);
const from = potentials.at(potentials.length >= 2 ? 2 : -1);
const from = positionals.find((positional) => !positional.startsWith("-"));

return from && !isLocalPath(from) && !from.startsWith("create-")
? `create-${from}`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,6 @@ import { describe, expect, it, vi } from "vitest";

import { tryImportAndInstallIfNecessary } from "./tryImportAndInstallIfNecessary.js";

const mockSpinner = {
start: vi.fn(),
stop: vi.fn(),
};

vi.mock("@clack/prompts", () => ({
spinner: () => mockSpinner,
}));

const mockImportLocalOrNpx = vi.fn();

vi.mock("import-local-or-npx", () => ({
Expand All @@ -33,12 +24,6 @@ describe("tryImportAndInstallIfNecessary", () => {
const actual = await tryImportAndInstallIfNecessary("../create-my-app");

expect(actual).toBe(errorLocal);
expect(mockSpinner.start.mock.calls).toEqual([
["Retrieving ../create-my-app"],
]);
expect(mockSpinner.stop.mock.calls).toEqual([
["Could not retrieve ../create-my-app"],
]);
});

it("returns the npx error when importLocalOrNpx resolves with a failure for a package name", async () => {
Expand All @@ -51,12 +36,6 @@ describe("tryImportAndInstallIfNecessary", () => {
const actual = await tryImportAndInstallIfNecessary("create-my-app");

expect(actual).toBe(errorNpx);
expect(mockSpinner.start.mock.calls).toEqual([
["Retrieving create-my-app"],
]);
expect(mockSpinner.stop.mock.calls).toEqual([
["Could not retrieve create-my-app"],
]);
});

it("returns the package when importLocalOrNpx resolves a package", async () => {
Expand All @@ -70,9 +49,5 @@ describe("tryImportAndInstallIfNecessary", () => {
const actual = await tryImportAndInstallIfNecessary("create-my-app");

expect(actual).toBe(resolved);
expect(mockSpinner.start.mock.calls).toEqual([
["Retrieving create-my-app"],
]);
expect(mockSpinner.stop.mock.calls).toEqual([["Retrieved create-my-app"]]);
});
});
Original file line number Diff line number Diff line change
@@ -1,26 +1,19 @@
import * as prompts from "@clack/prompts";
import { importLocalOrNpx } from "import-local-or-npx";

import { isLocalPath } from "../utils.js";

export async function tryImportAndInstallIfNecessary(
from: string,
): Promise<Error | object> {
const spinner = prompts.spinner();
spinner.start(`Retrieving ${from}`);

const imported = await importLocalOrNpx(from, {
// We ignore logs because we don't want to clutter CLI output
// eslint-disable-next-line @typescript-eslint/no-empty-function
logger: () => {},
});

if (imported.kind === "failure") {
spinner.stop(`Could not retrieve ${from}`);
return isLocalPath(from) ? imported.local : imported.npx;
}

spinner.stop(`Retrieved ${from}`);

return imported.resolved;
}
58 changes: 58 additions & 0 deletions packages/create/src/cli/importers/tryImportTemplate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import chalk from "chalk";
import { describe, expect, it, vi } from "vitest";

import { tryImportTemplate } from "./tryImportTemplate.js";

const mockSpinner = {
start: vi.fn(),
stop: vi.fn(),
};

vi.mock("@clack/prompts", () => ({
spinner: () => mockSpinner,
}));

const mockTryImportWithPredicate = vi.fn();

vi.mock("../tryImportWithPredicate.js", () => ({
get tryImportWithPredicate() {
return mockTryImportWithPredicate;
},
}));

describe("tryImportTemplate", () => {
it("returns the error when tryImportWithPredicate resolves with an error", async () => {
const error = new Error("Oh no!");

mockTryImportWithPredicate.mockResolvedValueOnce(error);

const actual = await tryImportTemplate("create-my-app");

expect(actual).toEqual(error);
expect(mockSpinner.start.mock.calls).toEqual([
[`Loading ${chalk.blue("create-my-app")}`],
]);
expect(mockSpinner.stop.mock.calls).toEqual([
[
`Could not load ${chalk.blue("create-my-app")}: ${chalk.red(error.message)}`,
1,
],
]);
});

it("returns the template when tryImportWithPredicate resolves with a preset", async () => {
const template = { isTemplate: true };

mockTryImportWithPredicate.mockResolvedValueOnce(template);

const actual = await tryImportTemplate("create-my-app");

expect(actual).toEqual(template);
expect(mockSpinner.start.mock.calls).toEqual([
[`Loading ${chalk.blue("create-my-app")}`],
]);
expect(mockSpinner.stop.mock.calls).toEqual([
[`Loaded ${chalk.blue("create-my-app")}`],
]);
});
});
30 changes: 30 additions & 0 deletions packages/create/src/cli/importers/tryImportTemplate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as prompts from "@clack/prompts";
import chalk from "chalk";

import { isTemplate } from "../../predicates/isTemplate.js";
import { tryImportWithPredicate } from "../tryImportWithPredicate.js";
import { tryImportAndInstallIfNecessary } from "./tryImportAndInstallIfNecessary.js";

export async function tryImportTemplate(from: string) {
const spinner = prompts.spinner();
spinner.start(`Loading ${chalk.blue(from)}`);

const template = await tryImportWithPredicate(
tryImportAndInstallIfNecessary,
from,
isTemplate,
"Template",
);

if (template instanceof Error) {
spinner.stop(
`Could not load ${chalk.blue(from)}: ${chalk.red(template.message)}`,
1,
);
return template;
}

spinner.stop(`Loaded ${chalk.blue(from)}`);

return template;
}
33 changes: 15 additions & 18 deletions packages/create/src/cli/importers/tryImportTemplatePreset.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { describe, expect, it, vi } from "vitest";

import { tryImportTemplatePreset } from "./tryImportTemplatePreset.js";

const mockIsCancel = vi.fn();
const mockCancel = Symbol("");
const mockIsCancel = (value: unknown) => value === mockCancel;

vi.mock("@clack/prompts", () => ({
get isCancel() {
Expand All @@ -18,47 +19,43 @@ vi.mock("../prompts/promptForPreset.js", () => ({
},
}));

const mockTryImportWithPredicate = vi.fn();
const mockTryImportTemplate = vi.fn();

vi.mock("../tryImportWithPredicate.js", () => ({
get tryImportWithPredicate() {
return mockTryImportWithPredicate;
vi.mock("./tryImportTemplate.js", () => ({
get tryImportTemplate() {
return mockTryImportTemplate;
},
}));

describe("tryImportTemplatePreset", () => {
it("returns the error when tryImportWithPredicate resolves with an error", async () => {
it("returns the error when tryImportTemplate resolves with an error", async () => {
const error = new Error("Oh no!");

mockTryImportWithPredicate.mockResolvedValueOnce(error);
mockTryImportTemplate.mockResolvedValueOnce(error);

const actual = await tryImportTemplatePreset("my-app");
const actual = await tryImportTemplatePreset("create-my-app");

expect(actual).toEqual(error);
expect(mockPromptForPreset).not.toHaveBeenCalled();
});

it("returns the cancellation when promptForPreset is cancelled", async () => {
const preset = Symbol.for("cancel");
mockTryImportTemplate.mockResolvedValueOnce({});
mockPromptForPreset.mockResolvedValueOnce(mockCancel);

mockTryImportWithPredicate.mockResolvedValueOnce({});
mockPromptForPreset.mockResolvedValueOnce(preset);
mockIsCancel.mockReturnValueOnce(true);

const actual = await tryImportTemplatePreset("my-app");
const actual = await tryImportTemplatePreset("create-my-app");

expect(actual).toBe(preset);
expect(actual).toBe(mockCancel);
});

it("returns the template and preset when promptForPreset resolves with a preset", async () => {
const template = { isTemplate: true };
const preset = { isPreset: true };

mockTryImportWithPredicate.mockResolvedValueOnce(template);
mockTryImportTemplate.mockResolvedValueOnce(template);
mockPromptForPreset.mockResolvedValueOnce(preset);
mockIsCancel.mockReturnValueOnce(false);

const actual = await tryImportTemplatePreset("my-app");
const actual = await tryImportTemplatePreset("create-my-app");

expect(actual).toEqual({ preset, template });
});
Expand Down
11 changes: 2 additions & 9 deletions packages/create/src/cli/importers/tryImportTemplatePreset.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,13 @@
import * as prompts from "@clack/prompts";

import { isTemplate } from "../../predicates/isTemplate.js";
import { promptForPreset } from "../prompts/promptForPreset.js";
import { tryImportWithPredicate } from "../tryImportWithPredicate.js";
import { tryImportAndInstallIfNecessary } from "./tryImportAndInstallIfNecessary.js";
import { tryImportTemplate } from "./tryImportTemplate.js";

export async function tryImportTemplatePreset(
from: string,
requestedPreset?: string,
) {
const template = await tryImportWithPredicate(
tryImportAndInstallIfNecessary,
from,
isTemplate,
"Template",
);
const template = await tryImportTemplate(from);
if (template instanceof Error) {
return template;
}
Expand Down
Loading

0 comments on commit 224268c

Please sign in to comment.