Skip to content

Commit f9d63fc

Browse files
feat: run getGitAndNpmDefaults lazily (#782)
## PR Checklist - [x] Addresses an existing open issue: fixes #781 - [x] That issue was marked as [`status: accepting prs`](https://github.com/JoshuaKGoldberg/create-typescript-app/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+accepting+prs%22) - [x] Steps in [CONTRIBUTING.md](https://github.com/JoshuaKGoldberg/create-typescript-app/blob/main/.github/CONTRIBUTING.md) were taken ## Overview Wraps all the defaults in `lazy-value` to lazily evaluate them. The `getGitAndNpmDefaults` function itself is pretty complex so I'm still not unit testing it. Ah well.
1 parent 2f57efc commit f9d63fc

File tree

9 files changed

+113
-28
lines changed

9 files changed

+113
-28
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"git-remote-origin-url": "^4.0.0",
5252
"git-url-parse": "^13.1.0",
5353
"js-yaml": "^4.1.0",
54+
"lazy-value": "^3.0.0",
5455
"npm-user": "^5.0.1",
5556
"octokit": "^3.1.0",
5657
"prettier": "^3.0.2",

pnpm-lock.yaml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/shared/options/getPrefillOrPromptedOption.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,33 @@ describe("getPrefillOrPromptedValue", () => {
2929

3030
expect(actual).toEqual(expected);
3131
});
32+
33+
it("provides no placeholder when one is not provided", async () => {
34+
const message = "Test message";
35+
36+
await getPrefillOrPromptedOption(undefined, message);
37+
38+
expect(mockText).toHaveBeenCalledWith({
39+
message,
40+
placeholder: undefined,
41+
validate: expect.any(Function),
42+
});
43+
});
44+
45+
it("provides the placeholder's awaited return when a placeholder function is provided", async () => {
46+
const message = "Test message";
47+
const placeholder = "Test placeholder";
48+
49+
await getPrefillOrPromptedOption(
50+
undefined,
51+
message,
52+
vi.fn().mockResolvedValue(placeholder),
53+
);
54+
55+
expect(mockText).toHaveBeenCalledWith({
56+
message,
57+
placeholder,
58+
validate: expect.any(Function),
59+
});
60+
});
3261
});

src/shared/options/getPrefillOrPromptedOption.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { filterPromptCancel } from "../prompts.js";
55
export async function getPrefillOrPromptedOption(
66
existingValue: string | undefined,
77
message: string,
8-
placeholder?: string,
8+
getPlaceholder?: () => Promise<string | undefined>,
99
) {
1010
if (existingValue) {
1111
return existingValue;
@@ -14,7 +14,7 @@ export async function getPrefillOrPromptedOption(
1414
const value = filterPromptCancel(
1515
await prompts.text({
1616
message,
17-
placeholder,
17+
placeholder: await getPlaceholder?.(),
1818
validate: (val) => {
1919
if (val.length === 0) {
2020
return "Please enter a value.";
Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,51 @@
11
import { $ } from "execa";
22
import gitRemoteOriginUrl from "git-remote-origin-url";
33
import gitUrlParse from "git-url-parse";
4+
import lazyValue from "lazy-value";
45
import fs from "node:fs/promises";
56
import npmUser from "npm-user";
67

78
import { readPackageData } from "../../packages.js";
89
import { tryCatchAsync } from "../../tryCatchAsync.js";
10+
import { tryCatchLazyValueAsync } from "../../tryCatchLazyValueAsync.js";
911
import { parsePackageAuthor } from "./parsePackageAuthor.js";
1012
import { readTitleFromReadme } from "./readTitleFromReadme.js";
1113

12-
export async function getGitAndNpmDefaults() {
13-
const gitDefaults = await tryCatchAsync(async () =>
14+
export function getGitAndNpmDefaults() {
15+
const gitDefaults = tryCatchLazyValueAsync(async () =>
1416
gitUrlParse(await gitRemoteOriginUrl()),
1517
);
1618

17-
const npmDefaults = await tryCatchAsync(
19+
const npmDefaults = tryCatchLazyValueAsync(
1820
async () => await npmUser((await $`npm whoami`).stdout),
1921
);
2022

21-
const packageData = await readPackageData();
22-
const packageAuthor = parsePackageAuthor(packageData);
23+
const packageData = lazyValue(readPackageData);
24+
const packageAuthor = lazyValue(async () =>
25+
parsePackageAuthor(await packageData()),
26+
);
2327

2428
return {
25-
author: packageAuthor.author ?? npmDefaults?.name,
26-
description: packageData.description,
27-
email:
28-
npmDefaults?.email ??
29-
packageAuthor.email ??
29+
author: async () => (await packageAuthor()).author ?? npmDefaults.name,
30+
description: async () => (await packageData()).description,
31+
email: async () =>
32+
(await npmDefaults())?.email ??
33+
(await packageAuthor()).email ??
3034
(await tryCatchAsync(
3135
async () => (await $`git config --get user.email`).stdout,
3236
)),
33-
funding: await tryCatchAsync(
34-
async () =>
35-
(await fs.readFile(".github/FUNDING.yml"))
36-
.toString()
37-
.split(":")[1]
38-
?.trim(),
39-
),
40-
owner: gitDefaults?.organization ?? packageAuthor.author,
41-
repository: gitDefaults?.name ?? packageData.name,
42-
title: await readTitleFromReadme(),
37+
funding: async () =>
38+
await tryCatchAsync(
39+
async () =>
40+
(await fs.readFile(".github/FUNDING.yml"))
41+
.toString()
42+
.split(":")[1]
43+
?.trim(),
44+
),
45+
owner: async () =>
46+
(await gitDefaults())?.organization ?? (await packageAuthor()).author,
47+
repository: async () =>
48+
(await gitDefaults())?.name ?? (await packageData()).name,
49+
title: async () => await readTitleFromReadme(),
4350
};
4451
}

src/shared/options/readOptions.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export interface OptionsParseSuccess extends OctokitAndOptions {
2828
export type OptionsParseResult = OptionsParseCancelled | OptionsParseSuccess;
2929

3030
export async function readOptions(args: string[]): Promise<OptionsParseResult> {
31-
const defaults = await getGitAndNpmDefaults();
31+
const defaults = getGitAndNpmDefaults();
3232
const { values } = parseArgs({
3333
args,
3434
options: allArgOptions,
@@ -111,7 +111,8 @@ export async function readOptions(args: string[]): Promise<OptionsParseResult> {
111111
options.description ??= await getPrefillOrPromptedOption(
112112
options.description,
113113
"How would you describe the new package?",
114-
defaults.description ?? "A very lovely package. Hooray!",
114+
async () =>
115+
(await defaults.description()) ?? "A very lovely package. Hooray!",
115116
);
116117
if (!options.description) {
117118
return { cancelled: true, options };
@@ -120,17 +121,18 @@ export async function readOptions(args: string[]): Promise<OptionsParseResult> {
120121
options.title ??= await getPrefillOrPromptedOption(
121122
options.title,
122123
"What will the Title Case title of the repository be?",
123-
defaults.title ?? titleCase(repository).replaceAll("-", " "),
124+
async () =>
125+
(await defaults.title()) ?? titleCase(repository).replaceAll("-", " "),
124126
);
125127
if (!options.title) {
126128
return { cancelled: true, options };
127129
}
128130

129131
const augmentedOptions = await augmentOptionsWithExcludes({
130132
...options,
131-
author: options.author ?? defaults.owner,
132-
email: options.email ?? defaults.email,
133-
funding: options.funding ?? defaults.funding,
133+
author: options.author ?? (await defaults.owner()),
134+
email: options.email ?? (await defaults.email()),
135+
funding: options.funding ?? (await defaults.funding()),
134136
repository,
135137
} as Options);
136138

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
3+
import { tryCatchLazyValueAsync } from "./tryCatchLazyValueAsync.js";
4+
5+
describe("tryCatchLazyValueAsync", () => {
6+
it("does not run get when it has not been called", () => {
7+
const get = vi.fn();
8+
9+
tryCatchLazyValueAsync(get);
10+
11+
expect(get).not.toHaveBeenCalled();
12+
});
13+
14+
it("returns get's resolved value when it resolves", async () => {
15+
const expected = "value";
16+
const get = vi.fn().mockResolvedValue(expected);
17+
18+
const lazy = tryCatchLazyValueAsync(get);
19+
20+
expect(await lazy()).toEqual(expected);
21+
});
22+
23+
it("returns undefined when get rejects", async () => {
24+
const get = vi.fn().mockRejectedValue(new Error("Oh no!"));
25+
26+
const lazy = tryCatchLazyValueAsync(get);
27+
28+
expect(await lazy()).toEqual(undefined);
29+
});
30+
});

src/shared/tryCatchLazyValueAsync.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import lazyValue from "lazy-value";
2+
3+
import { tryCatchAsync } from "./tryCatchAsync.js";
4+
5+
export function tryCatchLazyValueAsync<T>(get: () => Promise<T>) {
6+
return lazyValue(async () => await tryCatchAsync(get));
7+
}

src/steps/uninstallPackages.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export async function uninstallPackages() {
1313
"execa",
1414
"git-remote-origin-url",
1515
"git-url-parse",
16+
"lazy-value",
1617
"js-yaml",
1718
"npm-user",
1819
"octokit",

0 commit comments

Comments
 (0)