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

Issue:
🚀 Feature: Infer options.development from .github/DEVELOPMENT.md in create engine Base
### 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

Right now, `options.documentation` is a direct file read of `.github/DEVELOPMENT.md`. That's not right! Known headings such as `# Development` and `## Testing` need to be filtered out.

### Additional Info

💖 

Pull request description:
chore: correct options.development sections parsing
## PR Checklist

- [x] Addresses an existing open issue: fixes #1797
- [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

Splits it out into a separate `readDocumentation` function.

💖 

Patch:
diff --git a/package.json b/package.json
index 59597f6..eeaeb07 100644
--- a/package.json
+++ b/package.json
@@ -62,6 +62,7 @@
 		"parse-package-name": "^1.0.0",
 		"populate-all-contributors-for-repository": "^0.1.2",
 		"prettier": "^3.4.2",
+		"remove-undefined-objects": "^5.0.0",
 		"replace-in-file": "^8.3.0",
 		"rimraf": "^6.0.1",
 		"set-github-repository-labels": "^0.1.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5f4fecd..3fafabd 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -71,6 +71,9 @@ importers:
       prettier:
         specifier: ^3.4.2
         version: 3.4.2
+      remove-undefined-objects:
+        specifier: ^5.0.0
+        version: 5.0.0
       replace-in-file:
         specifier: ^8.3.0
         version: 8.3.0
@@ -3683,6 +3686,10 @@ packages:
     engines: {node: ^18.18.0 || ^20.9.0 || ^22.0.0}
     hasBin: true
 
+  remove-undefined-objects@5.0.0:
+    resolution: {integrity: sha512-DE8C17uIWeHaY4SqIkpQpHXm0MIdYHtIqjieWuh0I2PG8YcZRxFE6pqeEhnRetsrQ7Lu9uvSNQkDbg95NLpvnQ==}
+    engines: {node: '>=18'}
+
   repeat-string@1.6.1:
     resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==}
     engines: {node: '>=0.10'}
@@ -8081,6 +8088,8 @@ snapshots:
       - supports-color
       - typescript
 
+  remove-undefined-objects@5.0.0: {}
+
   repeat-string@1.6.1: {}
 
   replace-in-file@8.3.0:
diff --git a/src/next/base.ts b/src/next/base.ts
index 869db5f..a1d7750 100644
--- a/src/next/base.ts
+++ b/src/next/base.ts
@@ -17,6 +17,7 @@ import { readGuide } from "../shared/options/createOptionDefaults/readGuide.js";
 import { readPackageData } from "../shared/packages.js";
 import { tryCatchLazyValueAsync } from "../shared/tryCatchLazyValueAsync.js";
 import { AllContributorsData } from "../shared/types.js";
+import { readDocumentation } from "./readDocumentation.js";
 import { swallowError } from "./utils/swallowError.js";
 
 export const base = createBase({
@@ -135,13 +136,7 @@ export const base = createBase({
 			return contributions.contributors;
 		});
 
-		const documentation = lazyValue(async () =>
-			swallowError(
-				await take(inputFromFile, {
-					filePath: ".github/DEVELOPMENT.md",
-				}),
-			),
-		);
+		const documentation = lazyValue(async () => readDocumentation(take));
 
 		const nvmrc = lazyValue(
 			async () =>
diff --git a/src/next/blocks/blockPackageJson.ts b/src/next/blocks/blockPackageJson.ts
index 1ce8cc7..628d33d 100644
--- a/src/next/blocks/blockPackageJson.ts
+++ b/src/next/blocks/blockPackageJson.ts
@@ -1,3 +1,4 @@
+import removeUndefinedObjects from "remove-undefined-objects";
 import sortPackageJson from "sort-package-json";
 import { z } from "zod";
 
@@ -35,52 +36,61 @@ export const blockPackageJson = base.createBlock({
 		};
 	},
 	produce({ addons, options }) {
+		const dependencies = {
+			...options.packageData?.dependencies,
+			...addons.properties.dependencies,
+		};
+		const devDependencies = {
+			...options.packageData?.devDependencies,
+			...addons.properties.devDependencies,
+		};
+
 		return {
 			files: {
 				"package.json": sortPackageJson(
-					JSON.stringify({
-						...addons.properties,
-						author: { email: options.email.npm, name: options.author },
-						bin: options.bin,
-						dependencies: {
-							...options.packageData?.dependencies,
-							...addons.properties.dependencies,
-						},
-						description: options.description,
-						devDependencies: {
-							...options.packageData?.devDependencies,
-							...addons.properties.devDependencies,
-						},
-						...(options.node && {
-							engines: {
-								node: `>=${options.node.minimum}`,
+					JSON.stringify(
+						removeUndefinedObjects({
+							...addons.properties,
+							author: { email: options.email.npm, name: options.author },
+							bin: options.bin,
+							dependencies: Object.keys(dependencies).length
+								? dependencies
+								: undefined,
+							description: options.description,
+							devDependencies: Object.keys(devDependencies).length
+								? devDependencies
+								: undefined,
+							...(options.node && {
+								engines: {
+									node: `>=${options.node.minimum}`,
+								},
+							}),
+							files: [
+								options.bin?.replace(/^\.\//, ""),
+								...(addons.properties.files ?? []),
+								"package.json",
+								"README.md",
+							]
+								.filter(Boolean)
+								.sort(),
+							keywords: options.keywords?.flatMap((keyword) =>
+								keyword.split(/ /),
+							),
+							license: "MIT",
+							main: "./lib/index.js",
+							name: options.repository,
+							repository: {
+								type: "git",
+								url: `https://github.com/${options.owner}/${options.repository}`,
+							},
+							scripts: {
+								...options.packageData?.scripts,
+								...addons.properties.scripts,
 							},
+							type: "module",
+							version: options.version ?? "0.0.0",
 						}),
-						files: [
-							options.bin?.replace(/^\.\//, ""),
-							...(addons.properties.files ?? []),
-							"package.json",
-							"README.md",
-						]
-							.filter(Boolean)
-							.sort(),
-						keywords: options.keywords?.flatMap((keyword) =>
-							keyword.split(/ /),
-						),
-						license: "MIT",
-						main: "./lib/index.js",
-						name: options.repository,
-						repository: {
-							type: "git",
-							url: `https://github.com/${options.owner}/${options.repository}`,
-						},
-						scripts: {
-							...options.packageData?.scripts,
-							...addons.properties.scripts,
-						},
-						type: "module",
-						version: options.version ?? "0.0.0",
-					}),
+					),
 				),
 			},
 			scripts: [
diff --git a/src/next/readDocumentation.ts b/src/next/readDocumentation.ts
new file mode 100644
index 0000000..5fd0d0b
--- /dev/null
+++ b/src/next/readDocumentation.ts
@@ -0,0 +1,37 @@
+import { TakeInput } from "create";
+import { inputFromFile } from "input-from-file";
+
+import { swallowError } from "./utils/swallowError.js";
+
+const knownHeadings = new Set([
+	"building",
+	"development",
+	"formatting",
+	"linting",
+	"testing",
+	"type checking",
+]);
+
+export async function readDocumentation(take: TakeInput) {
+	const existing = swallowError(
+		await take(inputFromFile, {
+			filePath: ".github/DEVELOPMENT.md",
+		}),
+	);
+	if (!existing) {
+		return undefined;
+	}
+
+	return existing
+		.split(/\n\n(?=##\s)/)
+		.filter((section) => !knownHeadings.has(parseHeading(section)))
+		.join("\n\n");
+}
+
+function parseHeading(section: string) {
+	return section
+		.split("\n")[0]
+		.replace(/^#+\s+/, "")
+		.trim()
+		.toLowerCase();
+}
diff --git a/src/steps/uninstallPackages.ts b/src/steps/uninstallPackages.ts
index 35a8189..4075c2c 100644
--- a/src/steps/uninstallPackages.ts
+++ b/src/steps/uninstallPackages.ts
@@ -29,6 +29,7 @@ export async function uninstallPackages(offline: boolean | undefined) {
 			"parse-package-name",
 			"populate-all-contributors-for-repository",
 			"prettier",
+			"remove-undefined-objects",
 			"replace-in-file",
 			"rimraf",
 			"set-github-repository-labels",


Test patch:
diff --git a/src/next/blocks/blockPackageJson.test.ts b/src/next/blocks/blockPackageJson.test.ts
index dc2d5f1..9535443 100644
--- a/src/next/blocks/blockPackageJson.test.ts
+++ b/src/next/blocks/blockPackageJson.test.ts
@@ -13,7 +13,7 @@ describe("blockPackageJson", () => {
 		expect(creation).toMatchInlineSnapshot(`
 			{
 			  "files": {
-			    "package.json": "{"name":"test-repository","version":"0.0.0","description":"Test description","repository":{"type":"git","url":"https://github.com/test-owner/test-repository"},"license":"MIT","author":{"email":"npm@email.com"},"type":"module","main":"./lib/index.js","files":["README.md","package.json"],"scripts":{},"dependencies":{},"devDependencies":{}}",
+			    "package.json": "{"name":"test-repository","version":"0.0.0","description":"Test description","repository":{"type":"git","url":"https://github.com/test-owner/test-repository"},"license":"MIT","author":{"email":"npm@email.com"},"type":"module","main":"./lib/index.js","files":["README.md","package.json"]}",
 			  },
 			  "scripts": [
 			    {
@@ -36,7 +36,7 @@ describe("blockPackageJson", () => {
 		expect(creation).toMatchInlineSnapshot(`
 			{
 			  "files": {
-			    "package.json": "{"name":"test-repository","version":"0.0.0","description":"Test description","repository":{"type":"git","url":"https://github.com/test-owner/test-repository"},"license":"MIT","author":{"email":"npm@email.com"},"type":"module","main":"./lib/index.js","files":["README.md","package.json"],"scripts":{},"dependencies":{},"devDependencies":{}}",
+			    "package.json": "{"name":"test-repository","version":"0.0.0","description":"Test description","repository":{"type":"git","url":"https://github.com/test-owner/test-repository"},"license":"MIT","author":{"email":"npm@email.com"},"type":"module","main":"./lib/index.js","files":["README.md","package.json"]}",
 			  },
 			  "scripts": [
 			    {
@@ -62,7 +62,7 @@ describe("blockPackageJson", () => {
 				cleanupCommands: ["pnpm dedupe"],
 				properties: {
 					dependencies: {
-						"is-odd": "3.0.1",
+						"is-odd": "1.2.3",
 					},
 					other: true,
 				},
@@ -73,7 +73,42 @@ describe("blockPackageJson", () => {
 		expect(creation).toMatchInlineSnapshot(`
 			{
 			  "files": {
-			    "package.json": "{"name":"test-repository","version":"0.0.0","description":"Test description","repository":{"type":"git","url":"https://github.com/test-owner/test-repository"},"license":"MIT","author":{"email":"npm@email.com"},"type":"module","main":"./lib/index.js","files":["README.md","package.json"],"scripts":{},"dependencies":{"is-odd":"3.0.1"},"devDependencies":{},"other":true}",
+			    "package.json": "{"name":"test-repository","version":"0.0.0","description":"Test description","repository":{"type":"git","url":"https://github.com/test-owner/test-repository"},"license":"MIT","author":{"email":"npm@email.com"},"type":"module","main":"./lib/index.js","files":["README.md","package.json"],"dependencies":{"is-odd":"1.2.3"},"other":true}",
+			  },
+			  "scripts": [
+			    {
+			      "commands": [
+			        "pnpm install",
+			        "pnpm dedupe",
+			      ],
+			      "phase": 1,
+			    },
+			  ],
+			}
+		`);
+	});
+
+	test("with addons adding devDependencies", () => {
+		const creation = testBlock(blockPackageJson, {
+			addons: {
+				cleanupCommands: ["pnpm dedupe"],
+				properties: {
+					dependencies: {
+						"is-odd": "1.2.3",
+					},
+					devDependencies: {
+						"is-even": "4.5.6",
+					},
+					other: true,
+				},
+			},
+			options: optionsBase,
+		});
+
+		expect(creation).toMatchInlineSnapshot(`
+			{
+			  "files": {
+			    "package.json": "{"name":"test-repository","version":"0.0.0","description":"Test description","repository":{"type":"git","url":"https://github.com/test-owner/test-repository"},"license":"MIT","author":{"email":"npm@email.com"},"type":"module","main":"./lib/index.js","files":["README.md","package.json"],"dependencies":{"is-odd":"1.2.3"},"devDependencies":{"is-even":"4.5.6"},"other":true}",
 			  },
 			  "scripts": [
 			    {
diff --git a/src/next/readDocumentation.test.ts b/src/next/readDocumentation.test.ts
new file mode 100644
index 0000000..9529c77
--- /dev/null
+++ b/src/next/readDocumentation.test.ts
@@ -0,0 +1,26 @@
+import { describe, expect, it } from "vitest";
+
+import { readDocumentation } from "./readDocumentation.js";
+
+describe("finalize", () => {
+	it("returns undefined when no .github/DEVELOPMENT.md exists", async () => {
+		const documentation = await readDocumentation(() =>
+			Promise.resolve(undefined),
+		);
+
+		expect(documentation).toBeUndefined();
+	});
+
+	it("filters known headings when .github/DEVELOPMENT.md exists", async () => {
+		const documentation = await readDocumentation(() =>
+			Promise.resolve(`# Development\nremoved\n\n## Unknown\n\nKept.\n`),
+		);
+
+		expect(documentation).toMatchInlineSnapshot(`
+			"## Unknown
+
+			Kept.
+			"
+		`);
+	});
+});
diff --git a/src/steps/writing/creation/index.test.ts b/src/steps/writing/creation/index.test.ts
index ec09b6f..92791c7 100644
--- a/src/steps/writing/creation/index.test.ts
+++ b/src/steps/writing/creation/index.test.ts
@@ -226,6 +226,7 @@ describe("createStructure", () => {
 						"parse-package-name",
 						"populate-all-contributors-for-repository",
 						"prettier",
+						"remove-undefined-objects",
 						"replace-in-file",
 						"rimraf",
 						"set-github-repository-labels",