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

Issue:
🚀 Feature: Infer CSpell entries from disk
### Feature Request 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

Running `npx create-typescript-app` on a repo with existing `cspell.json` customizations should read those in as Addons to `blockCSpell`. That way `create-typescript-app.config.*` files won't need to explicitly describe them under `refinements`.

### Additional Info

https://github.com/JoshuaKGoldberg/package-json-validator/pull/178/files#diff-5b778358ef07c9914c10021cf98855c355bb26724b4a16956a714515b556f750R19-R22 is an example of `ignores` and `words` being specified.

🎁 

Pull request description:
feat: add intake to blockCspell
## PR Checklist

- [x] Addresses an existing open issue: fixes #2113
- [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

🎁 

Patch:
diff --git a/src/blocks/blockCSpell.ts b/src/blocks/blockCSpell.ts
index 7670698..3762216 100644
--- a/src/blocks/blockCSpell.ts
+++ b/src/blocks/blockCSpell.ts
@@ -1,3 +1,4 @@
+import JSON5 from "json5";
 import { getObjectStringsDeep } from "object-strings-deep";
 import { z } from "zod";
 
@@ -9,17 +10,35 @@ import { blockGitHubActionsCI } from "./blockGitHubActionsCI.js";
 import { blockPackageJson } from "./blockPackageJson.js";
 import { blockRemoveWorkflows } from "./blockRemoveWorkflows.js";
 import { blockVSCode } from "./blockVSCode.js";
+import { intakeFile } from "./intake/intakeFile.js";
 import { CommandPhase } from "./phases.js";
 
 const filesGlob = `"**" ".github/**/*"`;
 
+const addons = {
+	ignores: z.array(z.string()).default([]),
+	words: z.array(z.string()).default([]),
+};
+
+const zAddons = z.object(addons);
+
 export const blockCSpell = base.createBlock({
 	about: {
 		name: "CSpell",
 	},
-	addons: {
-		ignores: z.array(z.string()).default([]),
-		words: z.array(z.string()).default([]),
+	addons,
+	intake({ files }) {
+		const cspellJson = intakeFile(files, ["cspell.json"]);
+		if (!cspellJson) {
+			return undefined;
+		}
+
+		const { data } = zAddons.safeParse(JSON5.parse<unknown>(cspellJson[0]));
+		if (!data) {
+			return undefined;
+		}
+
+		return data;
 	},
 	produce({ addons, options }) {
 		const { ignores, words } = addons;


Test patch:
diff --git a/src/blocks/blockCSpell.test.ts b/src/blocks/blockCSpell.test.ts
index fb8ae68..602c04e 100644
--- a/src/blocks/blockCSpell.test.ts
+++ b/src/blocks/blockCSpell.test.ts
@@ -1,5 +1,5 @@
-import { testBlock } from "bingo-stratum-testers";
-import { describe, expect, test, vi } from "vitest";
+import { testBlock, testIntake } from "bingo-stratum-testers";
+import { describe, expect, it, test, vi } from "vitest";
 
 import { blockCSpell } from "./blockCSpell.js";
 import { optionsBase } from "./options.fakes.js";
@@ -364,4 +364,49 @@ describe("blockCSpell", () => {
 			}
 		`);
 	});
+
+	describe("intake", () => {
+		it("returns undefined when cspell.json does not exist", () => {
+			const actual = testIntake(blockCSpell, {
+				files: {},
+			});
+
+			expect(actual).toBeUndefined();
+		});
+
+		it("returns undefined when cspell.json does not contain truthy data", () => {
+			const actual = testIntake(blockCSpell, {
+				files: {
+					"cspell.json": [JSON.stringify(null)],
+				},
+			});
+
+			expect(actual).toBeUndefined();
+		});
+
+		it("returns undefined when cspell.json contains invalid data", () => {
+			const actual = testIntake(blockCSpell, {
+				files: {
+					"cspell.json": [JSON.stringify({ ignores: true })],
+				},
+			});
+
+			expect(actual).toBeUndefined();
+		});
+
+		it("returns compilerOptions when cspell.json contains ignores and words", () => {
+			const data = {
+				ignores: ["other"],
+				words: ["abc", "def"],
+			};
+
+			const actual = testIntake(blockCSpell, {
+				files: {
+					"cspell.json": [JSON.stringify(data)],
+				},
+			});
+
+			expect(actual).toEqual(data);
+		});
+	});
 });