Skip to content

Commit 7f1350e

Browse files
feat: add --guide options (#983)
## PR Checklist - [x] Addresses an existing open issue: fixes #982 - [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 Adds `--guide` with the accompanying `--guide-title`, similar to the existing `--logo` and `--logo-alt` options. It will be inferred from an existing `.github/DEVELOPMENT.md` when present.
1 parent 88f87b1 commit 7f1350e

15 files changed

+266
-32
lines changed

Diff for: .github/DEVELOPMENT.md

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Development
22

3+
> If you'd like a more guided walkthrough, see [Contributing to a create-typescript-app Repository](https://www.joshuakgoldberg.com/blog/contributing-to-a-create-typescript-app-repository).
4+
> It'll walk you through the common activities you'll need to contribute.
5+
36
After [forking the repo from GitHub](https://help.github.com/articles/fork-a-repo) and [installing pnpm](https://pnpm.io/installation):
47

58
```shell

Diff for: docs/FAQs.md

+10-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# FAQs
22

3+
## Do you have a guide to working with a `create-typescript-app` repository?
4+
5+
Yes!
6+
See [Contributing to a create-typescript-app Repository](https://www.joshuakgoldberg.com/blog/contributing-to-a-create-typescript-app-repository).
7+
It'll walk you through the common activities you'll need to contribute to a repository scaffolded with `create-typescript-app`.
8+
39
## Can I use _(insert tool here)_ with this template?
410

511
Yes!
@@ -61,13 +67,13 @@ You can always copy & paste them in manually, and/or re-run `npx create-typescri
6167

6268
See [🚀 Feature: Add a script to sync the tooling updates from forked template repo #498](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/498): it will likely eventually be possible.
6369

64-
### What about `eslint-config-prettier`?
70+
## What about `eslint-config-prettier`?
6571

6672
[`eslint-config-prettier`](https://github.com/prettier/eslint-config-prettier) is an ESLint plugin that serves only to turn off all rules that are unnecessary or might conflict with formatters such as Prettier.
6773
None of the ESLint configs enabled by this repository's tooling leave any rules enabled that would need to be disabled.
6874
Using `eslint-config-prettier` would be redundant.
6975

70-
### What determines which "base" a tool goes into?
76+
## What determines which "base" a tool goes into?
7177

7278
The four bases correspond to what have seemed to be the most common user needs of template consumers:
7379

@@ -80,7 +86,7 @@ The four bases correspond to what have seemed to be the most common user needs o
8086

8187
Note that users can always customize exactly with portions are kept in with `--base` **`prompt`**.
8288

83-
### Which tools can't I remove?
89+
## Which tools can't I remove?
8490

8591
The following pieces of this template's tooling don't have options to be removed:
8692

@@ -99,7 +105,7 @@ Each of the included tools exists for a good reason and provides real value.
99105

100106
If you don't want to use any particular tool, you can always remove it manually.
101107

102-
### What tooling does this package use that isn't part of created repositories?
108+
## What tooling does this package use that isn't part of created repositories?
103109

104110
Glad you asked!
105111
These are the projects used across many parts of `create-typescript-app`:

Diff for: docs/Options.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,12 @@ The setup scripts also allow for optional overrides of the following inputs whos
5757
- `--email` _(`string`)_: Email address to be listed as the point of contact in docs and packages (e.g. `[email protected]`)
5858
- Optionally, `--email-github` _(`string`)_ and/or `--email-npm` _(`string`)_ may be provided to use different emails in `.md` files and `package.json`, respectively
5959
- `--funding` _(`string`)_: GitHub organization or username to mention in `funding.yml` (by default, `owner`)
60+
- `--guide` _(`string`)_: Link to a contribution guide to place at the top of the development docs
61+
- `--guide-title` _(`string`)_: If `--guide` is provided or detected from an existing DEVELOPMENT.md, the text title to place in the guide link
6062
- `--keywords` _(`string[]`)_: Any number of keywords to include in `package.json` (by default, none)
6163
- This can be specified any number of times, like `--keywords apple --keywords "banana cherry"`
6264
- `--logo` _(`string`)_: Local image file in the repository to display near the top of the README.md as a logo
63-
- `--logo-alt` _(`string`)_: If `--logo` is provided or detected from an existing README.md, alt text that describes the image will be prompted for if not provided
65+
- `--logo-alt` _(`string`)_: If `--logo` is provided or detected from an existing README.md, alt text that describes the image (will be prompted for if not provided)
6466
- `--preserve-generated-from` _(`boolean`)_: Whether to keep the GitHub repository _generated from_ notice (by default, `false`)
6567

6668
For example, customizing the ownership and users associated with a new repository:

Diff for: src/create/createRerunSuggestion.test.ts

+15
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,21 @@ describe("createRerunSuggestion", () => {
5050
);
5151
});
5252

53+
it("includes stringified guide when it exists", () => {
54+
const actual = createRerunSuggestion({
55+
...options,
56+
guide: {
57+
href: "https://example.com",
58+
title: "Test Title",
59+
},
60+
mode: "initialize",
61+
});
62+
63+
expect(actual).toMatchInlineSnapshot(
64+
'"npx create-typescript-app --mode initialize --base everything --access public --author TestAuthor --description \\"Test description.\\" --directory . --email-github [email protected] --email-npm [email protected] --exclude-all-contributors true --exclude-compliance true --exclude-lint-jsdoc true --exclude-lint-json true --exclude-lint-knip true --exclude-lint-package-json true --exclude-lint-perfectionist true --guide https://example.com --guide-title \\"Test Title\\" --keywords \\"abc def ghi jkl mno pqr\\" --mode initialize --owner TestOwner --repository test-repository --skip-github-api true --skip-install true --skip-removal true --title \\"Test Title\\""',
65+
);
66+
});
67+
5368
it("includes stringified logo when it exists", () => {
5469
const actual = createRerunSuggestion({
5570
...options,

Diff for: src/create/createRerunSuggestion.ts

+9-5
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,17 @@ function getFirstMatchingArg(key: string) {
1010
export function createRerunSuggestion(options: Partial<Options>): string {
1111
const optionsNormalized = {
1212
...options,
13-
...(options.email
13+
email: undefined,
14+
...(options.email && {
15+
emailGitHub: options.email.github,
16+
emailNpm: options.email.npm,
17+
}),
18+
...(options.guide
1419
? {
15-
email: undefined,
16-
emailGitHub: options.email.github,
17-
emailNpm: options.email.npm,
20+
guide: options.guide.href,
21+
guideTitle: options.guide.title,
1822
}
19-
: { email: undefined }),
23+
: { guide: undefined }),
2024
...(options.logo
2125
? {
2226
logo: options.logo.src,

Diff for: src/shared/options/args.ts

+2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ export const allArgOptions = {
2929
"exclude-renovate": { type: "boolean" },
3030
"exclude-tests": { type: "boolean" },
3131
funding: { type: "string" },
32+
guide: { type: "string" },
33+
"guide-title": { type: "string" },
3234
keywords: { multiple: true, type: "string" },
3335
logo: { type: "string" },
3436
"logo-alt": { type: "string" },

Diff for: src/shared/options/createOptionDefaults/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { tryCatchAsync } from "../../tryCatchAsync.js";
1010
import { tryCatchLazyValueAsync } from "../../tryCatchLazyValueAsync.js";
1111
import { PromptedOptions } from "../../types.js";
1212
import { parsePackageAuthor } from "./parsePackageAuthor.js";
13+
import { readDefaultsFromDevelopment } from "./readDefaultsFromDevelopment.js";
1314
import { readDefaultsFromReadme } from "./readDefaultsFromReadme.js";
1415

1516
export function createOptionDefaults(promptedOptions?: PromptedOptions) {
@@ -57,6 +58,7 @@ export function createOptionDefaults(promptedOptions?: PromptedOptions) {
5758
promptedOptions?.repository ??
5859
(await gitDefaults())?.name ??
5960
(await packageData()).name,
61+
...readDefaultsFromDevelopment(),
6062
...readDefaultsFromReadme(),
6163
};
6264
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
3+
import { readDefaultsFromDevelopment } from "./readDefaultsFromDevelopment.js";
4+
5+
const mockReadFileSafe = vi.fn();
6+
7+
vi.mock("../../readFileSafe.js", () => ({
8+
get readFileSafe() {
9+
return mockReadFileSafe;
10+
},
11+
}));
12+
13+
describe("readDefaultsFromDevelopment", () => {
14+
describe("guide", () => {
15+
it("defaults to undefined when .github/DEVELOPMENT.md cannot be found", async () => {
16+
mockReadFileSafe.mockResolvedValue("");
17+
18+
const guide = await readDefaultsFromDevelopment().guide();
19+
20+
expect(guide).toBeUndefined();
21+
});
22+
23+
it("reads guide when it exists", async () => {
24+
mockReadFileSafe.mockResolvedValue(`# Development
25+
26+
> If you'd like a more guided walkthrough, see [Contributing to a create-typescript-app Repository](https://www.joshuakgoldberg.com/blog/contributing-to-a-create-typescript-app-repository).
27+
> It'll walk you through the common activities you'll need to contribute.
28+
`);
29+
30+
const guide = await readDefaultsFromDevelopment().guide();
31+
32+
expect(guide).toBe(
33+
"https://www.joshuakgoldberg.com/blog/contributing-to-a-create-typescript-app-repository",
34+
);
35+
});
36+
});
37+
38+
describe("guideTitle", () => {
39+
it("defaults to undefined when .github/DEVELOPMENT.md cannot be found", async () => {
40+
mockReadFileSafe.mockResolvedValue("");
41+
42+
const guideTitle = await readDefaultsFromDevelopment().guideTitle();
43+
44+
expect(guideTitle).toBeUndefined();
45+
});
46+
47+
it("reads guideTitle when it exists", async () => {
48+
mockReadFileSafe.mockResolvedValue(`# Development
49+
50+
> If you'd like a more guided walkthrough, see [Contributing to a create-typescript-app Repository](https://www.joshuakgoldberg.com/blog/contributing-to-a-create-typescript-app-repository).
51+
> It'll walk you through the common activities you'll need to contribute.
52+
`);
53+
54+
const guideTitle = await readDefaultsFromDevelopment().guideTitle();
55+
56+
expect(guideTitle).toBe(
57+
"Contributing to a create-typescript-app Repository",
58+
);
59+
});
60+
});
61+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import lazyValue from "lazy-value";
2+
3+
import { readFileSafe } from "../../readFileSafe.js";
4+
5+
export function readDefaultsFromDevelopment() {
6+
const development = lazyValue(
7+
async () => await readFileSafe(".github/DEVELOPMENT.md", ""),
8+
);
9+
10+
const guideTag = lazyValue(async () =>
11+
(await development()).match(
12+
/> .*guided walkthrough, see \[((?!\[).+)\]\((.+)\)/i,
13+
),
14+
);
15+
16+
return {
17+
guide: async () => (await guideTag())?.[2],
18+
guideTitle: async () => (await guideTag())?.[1],
19+
};
20+
}

Diff for: src/shared/options/optionsSchema.ts

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ export const optionsSchemaShape = {
3939
excludeRenovate: z.boolean().optional(),
4040
excludeTests: z.boolean().optional(),
4141
funding: z.string().optional(),
42+
guide: z.string().url().optional(),
43+
guideTitle: z.string().optional(),
4244
keywords: z.array(z.string()).optional(),
4345
logo: z.string().optional(),
4446
logoAlt: z.string().optional(),

Diff for: src/shared/options/readOptions.test.ts

+97
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ const emptyOptions = {
3131
excludeRenovate: undefined,
3232
excludeTests: undefined,
3333
funding: undefined,
34+
guide: undefined,
35+
logo: undefined,
3436
offline: undefined,
3537
owner: undefined,
3638
preserveGeneratedFrom: false,
@@ -227,6 +229,62 @@ describe("readOptions", () => {
227229
});
228230
});
229231

232+
it("returns a cancellation when the guide title prompt is cancelled", async () => {
233+
mockDetectEmailRedundancy.mockReturnValue(false);
234+
mockGetPrefillOrPromptedOption
235+
.mockImplementationOnce(() => "MockOwner")
236+
.mockImplementationOnce(() => "MockRepository")
237+
.mockImplementationOnce(() => "Mock description.")
238+
.mockImplementationOnce(() => "Mock Title")
239+
.mockImplementation(() => undefined);
240+
mockEnsureRepositoryExists.mockResolvedValue({
241+
github: mockOptions.github,
242+
repository: mockOptions.repository,
243+
});
244+
245+
expect(
246+
await readOptions(["--guide", "https://example.com"], "create"),
247+
).toStrictEqual({
248+
cancelled: true,
249+
options: {
250+
...emptyOptions,
251+
description: "Mock description.",
252+
guide: "https://example.com",
253+
owner: "MockOwner",
254+
repository: "MockRepository",
255+
title: "Mock Title",
256+
},
257+
});
258+
});
259+
260+
it("returns a cancellation when the guide alt prompt is cancelled", async () => {
261+
mockDetectEmailRedundancy.mockReturnValue(false);
262+
mockGetPrefillOrPromptedOption
263+
.mockImplementationOnce(() => "MockOwner")
264+
.mockImplementationOnce(() => "MockRepository")
265+
.mockImplementationOnce(() => "Mock description.")
266+
.mockImplementationOnce(() => "Mock Title")
267+
.mockImplementation(() => undefined);
268+
mockEnsureRepositoryExists.mockResolvedValue({
269+
github: mockOptions.github,
270+
repository: mockOptions.repository,
271+
});
272+
273+
expect(
274+
await readOptions(["--guide", "https://example.com"], "create"),
275+
).toStrictEqual({
276+
cancelled: true,
277+
options: {
278+
...emptyOptions,
279+
description: "Mock description.",
280+
guide: "https://example.com",
281+
owner: "MockOwner",
282+
repository: "MockRepository",
283+
title: "Mock Title",
284+
},
285+
});
286+
});
287+
230288
it("returns a cancellation when the logo alt prompt is cancelled", async () => {
231289
mockDetectEmailRedundancy.mockReturnValue(false);
232290
mockGetPrefillOrPromptedOption
@@ -244,6 +302,7 @@ describe("readOptions", () => {
244302
options: {
245303
...emptyOptions,
246304
description: "Mock description.",
305+
logo: "logo.svg",
247306
owner: "MockOwner",
248307
repository: "MockRepository",
249308
},
@@ -307,6 +366,10 @@ describe("readOptions", () => {
307366
...mockOptions,
308367
});
309368
mockGetPrefillOrPromptedOption.mockImplementation(() => "mock");
369+
mockEnsureRepositoryExists.mockResolvedValue({
370+
github: mockOptions.github,
371+
repository: mockOptions.repository,
372+
});
310373

311374
expect(
312375
await readOptions(["--base", mockOptions.base], "create"),
@@ -320,6 +383,39 @@ describe("readOptions", () => {
320383
});
321384
});
322385

386+
it("returns success options when --base is valid with all optional options", async () => {
387+
mockAugmentOptionsWithExcludes.mockResolvedValue({
388+
...emptyOptions,
389+
...mockOptions,
390+
});
391+
mockGetPrefillOrPromptedOption.mockImplementation(() => "mock");
392+
mockEnsureRepositoryExists.mockResolvedValue({
393+
github: mockOptions.github,
394+
repository: mockOptions.repository,
395+
});
396+
397+
expect(
398+
await readOptions(
399+
[
400+
"--base",
401+
mockOptions.base,
402+
"--guide",
403+
"https://example.com",
404+
"--logo",
405+
"logo.svg",
406+
],
407+
"create",
408+
),
409+
).toStrictEqual({
410+
cancelled: false,
411+
github: mockOptions.github,
412+
options: {
413+
...emptyOptions,
414+
...mockOptions,
415+
},
416+
});
417+
});
418+
323419
it("returns cancelled options when augmentOptionsWithExcludes returns undefined", async () => {
324420
mockAugmentOptionsWithExcludes.mockResolvedValue(undefined);
325421
mockGetPrefillOrPromptedOption.mockImplementation(() => "mock");
@@ -417,6 +513,7 @@ describe("readOptions", () => {
417513
github: "mock",
418514
npm: "mock",
419515
},
516+
guide: undefined,
420517
logo: undefined,
421518
mode: "create",
422519
offline: true,

0 commit comments

Comments
 (0)