Repository: JoshuaKGoldberg/create-typescript-app (ts)

Issue:
🚀 Feature: Add a 'usage' option for README.md contents
### Bug Report Checklist

- [X] I have pulled the latest `main` branch of the repository.
- [X] I have [searched for related issues](https://github.com/JoshuaKGoldberg/create-typescript-app/issues?q=is%3Aissue) and found none that matched my issue.

### Overview

Every repository I've templated with CTA has customized the `## Usage` section of the `README.md` file in some way. This will need to be an inferable option for the new `create` engine. That way, when folks run `npx create typescript-app` in an existing repo, it won't clobber over their README.md contents.

### Additional Info

💖 

Pull request:
chore: add usage option for create engine
## PR Checklist

- [x] Addresses an existing open issue: fixes #1821
- [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

Marking as `chore:` because this is only for the new `create` Blocks engine.

💖 

Patch:
diff --git a/src/next/base.ts b/src/next/base.ts
index e0edf28..7aff74e 100644
--- a/src/next/base.ts
+++ b/src/next/base.ts
@@ -126,6 +126,10 @@ export const base = createBase({
 			.string()
 			.describe("'Sentence case.' description of the repository"),
 		title: z.string().describe("'Title Case' title for the repository"),
+		usage: z
+			.string()
+			.optional()
+			.describe("markdown docs to put in README.md under the ## Usage heading"),
 		version: z
 			.string()
 			.optional()
@@ -223,7 +227,7 @@ export const base = createBase({
 				options.repository ??
 				(await gitDefaults())?.name ??
 				(await packageData()).name,
-			...readDefaultsFromReadme(readme),
+			...readDefaultsFromReadme(readme, options.repository),
 			version,
 		};
 	},
diff --git a/src/next/blocks/blockREADME.ts b/src/next/blocks/blockREADME.ts
index 5691e3f..71f5298 100644
--- a/src/next/blocks/blockREADME.ts
+++ b/src/next/blocks/blockREADME.ts
@@ -44,14 +44,7 @@ ${logo}
 
 ## Usage
 
-\`\`\`shell
-npm i ${options.repository}
-\`\`\`
-\`\`\`ts
-import { greet } from "${options.repository}";
-
-greet("Hello, world! 💖");
-\`\`\`
+${options.usage}
 
 ## Development
 
diff --git a/src/shared/options/createOptionDefaults/getUsageFromReadme.ts b/src/shared/options/createOptionDefaults/getUsageFromReadme.ts
new file mode 100644
index 0000000..124b1eb
--- /dev/null
+++ b/src/shared/options/createOptionDefaults/getUsageFromReadme.ts
@@ -0,0 +1,23 @@
+const startDevelopment = "## Development";
+const startUsage = "## Usage";
+
+export function getUsageFromReadme(readme: string) {
+	const indexOfUsage = readme.indexOf(startUsage);
+	if (indexOfUsage === -1) {
+		return undefined;
+	}
+
+	const indexOfDevelopment = readme.indexOf(
+		startDevelopment,
+		indexOfUsage + startUsage.length,
+	);
+	if (indexOfDevelopment === -1) {
+		return undefined;
+	}
+
+	const usage = readme
+		.slice(indexOfUsage + startUsage.length, indexOfDevelopment)
+		.trim();
+
+	return usage ? usage : undefined;
+}
diff --git a/src/shared/options/createOptionDefaults/index.ts b/src/shared/options/createOptionDefaults/index.ts
index 24a0c82..7996c83 100644
--- a/src/shared/options/createOptionDefaults/index.ts
+++ b/src/shared/options/createOptionDefaults/index.ts
@@ -53,6 +53,6 @@ export function createOptionDefaults(promptedOptions?: PromptedOptions) {
 			promptedOptions?.repository ??
 			(await gitDefaults())?.name ??
 			(await packageData()).name,
-		...readDefaultsFromReadme(readme),
+		...readDefaultsFromReadme(readme, promptedOptions?.repository),
 	};
 }
diff --git a/src/shared/options/createOptionDefaults/readDefaultsFromReadme.ts b/src/shared/options/createOptionDefaults/readDefaultsFromReadme.ts
index 5f5da1d..39fb4d2 100644
--- a/src/shared/options/createOptionDefaults/readDefaultsFromReadme.ts
+++ b/src/shared/options/createOptionDefaults/readDefaultsFromReadme.ts
@@ -1,8 +1,12 @@
 import lazyValue from "lazy-value";
 
 import { readLogoSizing } from "../readLogoSizing.js";
+import { getUsageFromReadme } from "./getUsageFromReadme.js";
 
-export function readDefaultsFromReadme(readme: () => Promise<string>) {
+export function readDefaultsFromReadme(
+	readme: () => Promise<string>,
+	repository: string | undefined,
+) {
 	const imageTag = lazyValue(
 		async () => /\n<img.+src=.+>/.exec(await readme())?.[0],
 	);
@@ -34,5 +38,19 @@ export function readDefaultsFromReadme(readme: () => Promise<string>) {
 			return (/^<h1\s+align="center">(.+)<\/h1>/.exec(text) ??
 				/^# (.+)/.exec(text))?.[1];
 		},
+
+		usage: async () => {
+			return (
+				getUsageFromReadme(await readme()) ??
+				`\`\`\`shell
+npm i ${repository}
+\`\`\`
+\`\`\`ts
+import { greet } from "${repository}";
+
+greet("Hello, world! 💖");
+\`\`\``
+			);
+		},
 	};
 }


Test patch:
diff --git a/src/next/blocks/blockREADME.test.ts b/src/next/blocks/blockREADME.test.ts
index 825121a..28c2446 100644
--- a/src/next/blocks/blockREADME.test.ts
+++ b/src/next/blocks/blockREADME.test.ts
@@ -4,11 +4,16 @@ import { describe, expect, test } from "vitest";
 import { blockREADME } from "./blockREADME.js";
 import { optionsBase } from "./options.fakes.js";
 
+const options = {
+	...optionsBase,
+	usage: "Use it.",
+};
+
 describe("blockREADME", () => {
 	test("options.logo without sizing", () => {
 		const creation = testBlock(blockREADME, {
 			options: {
-				...optionsBase,
+				...options,
 				logo: {
 					alt: "My logo",
 					src: "img.jpg",
@@ -39,14 +44,7 @@ describe("blockREADME", () => {
 
 			## Usage
 
-			\`\`\`shell
-			npm i test-repository
-			\`\`\`
-			\`\`\`ts
-			import { greet } from "test-repository";
-
-			greet("Hello, world! 💖");
-			\`\`\`
+			Use it.
 
 			## Development
 
@@ -68,7 +66,7 @@ describe("blockREADME", () => {
 	test("options.logo with sizing", () => {
 		const creation = testBlock(blockREADME, {
 			options: {
-				...optionsBase,
+				...options,
 				logo: {
 					alt: "My logo",
 					height: 100,
@@ -101,14 +99,7 @@ describe("blockREADME", () => {
 
 			## Usage
 
-			\`\`\`shell
-			npm i test-repository
-			\`\`\`
-			\`\`\`ts
-			import { greet } from "test-repository";
-
-			greet("Hello, world! 💖");
-			\`\`\`
+			Use it.
 
 			## Development
 
@@ -129,7 +120,7 @@ describe("blockREADME", () => {
 
 	test("without addons", () => {
 		const creation = testBlock(blockREADME, {
-			options: optionsBase,
+			options,
 		});
 
 		expect(creation).toMatchInlineSnapshot(`
@@ -155,14 +146,7 @@ describe("blockREADME", () => {
 
 			## Usage
 
-			\`\`\`shell
-			npm i test-repository
-			\`\`\`
-			\`\`\`ts
-			import { greet } from "test-repository";
-
-			greet("Hello, world! 💖");
-			\`\`\`
+			Use it.
 
 			## Development
 
@@ -186,7 +170,7 @@ describe("blockREADME", () => {
 			addons: {
 				notices: ["> Hello, world! 💖"],
 			},
-			options: optionsBase,
+			options,
 		});
 
 		expect(creation).toMatchInlineSnapshot(`
@@ -212,14 +196,7 @@ describe("blockREADME", () => {
 
 			## Usage
 
-			\`\`\`shell
-			npm i test-repository
-			\`\`\`
-			\`\`\`ts
-			import { greet } from "test-repository";
-
-			greet("Hello, world! 💖");
-			\`\`\`
+			Use it.
 
 			## Development
 
diff --git a/src/shared/options/createOptionDefaults/getUsageFromReadme.test.ts b/src/shared/options/createOptionDefaults/getUsageFromReadme.test.ts
new file mode 100644
index 0000000..113495f
--- /dev/null
+++ b/src/shared/options/createOptionDefaults/getUsageFromReadme.test.ts
@@ -0,0 +1,29 @@
+import { describe, expect, it } from "vitest";
+
+import { getUsageFromReadme } from "./getUsageFromReadme.js";
+
+describe("getUsageFromReadme", () => {
+	it("returns undefined when ## Usage is not found", () => {
+		const usage = getUsageFromReadme("## Other");
+
+		expect(usage).toBeUndefined();
+	});
+
+	it("returns undefined when ## Usage found and ## Development is not found", () => {
+		const usage = getUsageFromReadme("## Usage\n\nContents.");
+
+		expect(usage).toBeUndefined();
+	});
+
+	it("returns undefined when there is no content between ## Usage and ## Development", () => {
+		const usage = getUsageFromReadme("## Usage\n\n  \n## Development");
+
+		expect(usage).toBeUndefined();
+	});
+
+	it("returns the content when content exists between ## Usage and ## Development", () => {
+		const usage = getUsageFromReadme("## Usage\n\n  Content.\n## Development");
+
+		expect(usage).toBe("Content.");
+	});
+});
diff --git a/src/shared/options/createOptionDefaults/readDefaultsFromReadme.test.ts b/src/shared/options/createOptionDefaults/readDefaultsFromReadme.test.ts
index 13c226e..ae4db2a 100644
--- a/src/shared/options/createOptionDefaults/readDefaultsFromReadme.test.ts
+++ b/src/shared/options/createOptionDefaults/readDefaultsFromReadme.test.ts
@@ -13,17 +13,20 @@ vi.mock("../readLogoSizing.js", () => ({
 describe("readDefaultsFromReadme", () => {
 	describe("logo", () => {
 		it("defaults to undefined when it cannot be found", async () => {
-			const logo = await readDefaultsFromReadme(() =>
-				Promise.resolve(`nothing.`),
+			const logo = await readDefaultsFromReadme(
+				() => Promise.resolve(`nothing.`),
+				undefined,
 			).logo();
 
 			expect(logo).toBeUndefined();
 		});
 
 		it("parses when found in an unquoted string", async () => {
-			const logo = await readDefaultsFromReadme(() =>
-				Promise.resolve(`
+			const logo = await readDefaultsFromReadme(
+				() =>
+					Promise.resolve(`
 <img src=abc/def.jpg/>`),
+				undefined,
 			).logo();
 
 			expect(logo).toEqual({
@@ -33,9 +36,11 @@ describe("readDefaultsFromReadme", () => {
 		});
 
 		it("parses when found in a single quoted string", async () => {
-			const logo = await readDefaultsFromReadme(() =>
-				Promise.resolve(`
+			const logo = await readDefaultsFromReadme(
+				() =>
+					Promise.resolve(`
 <img src='abc/def.jpg'/>`),
+				undefined,
 			).logo();
 
 			expect(logo).toEqual({
@@ -45,9 +50,11 @@ describe("readDefaultsFromReadme", () => {
 		});
 
 		it("parses when found in a double quoted string", async () => {
-			const logo = await readDefaultsFromReadme(() =>
-				Promise.resolve(`
+			const logo = await readDefaultsFromReadme(
+				() =>
+					Promise.resolve(`
 <img src="abc/def.jpg"/>`),
+				undefined,
 			).logo();
 
 			expect(logo).toEqual({
@@ -57,9 +64,11 @@ describe("readDefaultsFromReadme", () => {
 		});
 
 		it("includes alt text when it exists in double quotes", async () => {
-			const logo = await readDefaultsFromReadme(() =>
-				Promise.resolve(`
+			const logo = await readDefaultsFromReadme(
+				() =>
+					Promise.resolve(`
 <img alt="Project logo: a fancy circle" src="abc/def.jpg"/>`),
+				undefined,
 			).logo();
 
 			expect(logo).toEqual({
@@ -69,9 +78,11 @@ describe("readDefaultsFromReadme", () => {
 		});
 
 		it("includes alt text when it exists in single quotes", async () => {
-			const logo = await readDefaultsFromReadme(() =>
-				Promise.resolve(`
+			const logo = await readDefaultsFromReadme(
+				() =>
+					Promise.resolve(`
 <img alt='Project logo: a fancy circle' src='abc/def.jpg'/>`),
+				undefined,
 			).logo();
 
 			expect(logo).toEqual({
@@ -85,9 +96,11 @@ describe("readDefaultsFromReadme", () => {
 
 			mockReadLogoSizing.mockReturnValueOnce(sizing);
 
-			const logo = await readDefaultsFromReadme(() =>
-				Promise.resolve(`
+			const logo = await readDefaultsFromReadme(
+				() =>
+					Promise.resolve(`
 <img alt='Project logo: a fancy circle' src='abc/def.jpg'/>`),
+				undefined,
 			).logo();
 
 			expect(logo).toEqual({
@@ -98,11 +111,13 @@ describe("readDefaultsFromReadme", () => {
 		});
 
 		it("parses when found after a badge image", async () => {
-			const logo = await readDefaultsFromReadme(() =>
-				Promise.resolve(`
+			const logo = await readDefaultsFromReadme(
+				() =>
+					Promise.resolve(`
 		<a href="#contributors" target="_blank"><img alt="👪 All Contributors: 48" src="https://img.shields.io/badge/%F0%9F%91%AA_all_contributors-48-21bb42.svg" /></a>
 <img src=abc/def.jpg/>
 `),
+				undefined,
 			).logo();
 
 			expect(logo).toEqual({
@@ -112,8 +127,9 @@ describe("readDefaultsFromReadme", () => {
 		});
 
 		it("parses when found after an h1 and many badge images", async () => {
-			const logo = await readDefaultsFromReadme(() =>
-				Promise.resolve(`
+			const logo = await readDefaultsFromReadme(
+				() =>
+					Promise.resolve(`
 <h1 align="center">Create TypeScript App</h1>
 
 <p align="center">Quickstart-friendly TypeScript template with comprehensive, configurable, opinionated tooling. ❤️‍🔥</p>
@@ -133,6 +149,7 @@ describe("readDefaultsFromReadme", () => {
 
 <img align="right" alt="Project logo: the TypeScript blue square with rounded corners, but a plus sign instead of 'TS'" src="./docs/create-typescript-app.png">
 `),
+				undefined,
 			).logo();
 
 			expect(logo).toEqual({
@@ -144,32 +161,36 @@ describe("readDefaultsFromReadme", () => {
 
 	describe("title", () => {
 		it("defaults to undefined when it cannot be found", async () => {
-			const title = await readDefaultsFromReadme(() =>
-				Promise.resolve(`nothing`),
+			const title = await readDefaultsFromReadme(
+				() => Promise.resolve(`nothing`),
+				undefined,
 			).title();
 
 			expect(title).toBeUndefined();
 		});
 
 		it('reads title as markdown from "README.md" when it exists', async () => {
-			const title = await readDefaultsFromReadme(() =>
-				Promise.resolve(`# My Awesome Package`),
+			const title = await readDefaultsFromReadme(
+				() => Promise.resolve(`# My Awesome Package`),
+				undefined,
 			).title();
 
 			expect(title).toBe("My Awesome Package");
 		});
 
 		it('reads title as HTML from "README.md" when it exists', async () => {
-			const title = await readDefaultsFromReadme(() =>
-				Promise.resolve('<h1 align="center">My Awesome Package</h1>'),
+			const title = await readDefaultsFromReadme(
+				() => Promise.resolve('<h1 align="center">My Awesome Package</h1>'),
+				undefined,
 			).title();
 
 			expect(title).toBe("My Awesome Package");
 		});
 
 		it("returns undefined when title does not exist", async () => {
-			const title = await readDefaultsFromReadme(() =>
-				Promise.resolve(`Other text.`),
+			const title = await readDefaultsFromReadme(
+				() => Promise.resolve(`Other text.`),
+				undefined,
 			).title();
 
 			expect(title).toBeUndefined();