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

Issue:
🐛 Bug: keywords are not inferred from existing `package.json`
### Bug Report Checklist

- [x] I have tried restarting my IDE and the issue persists.
- [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.

### Expected

Running `npx create-typescript-app` on a repo with `keywords` in its `package.json` file should not wipe those keywords.

### Actual

https://github.com/JoshuaKGoldberg/package-json-validator/pull/178/files#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519L11 shows that they are.

### Additional Info

Keywords were originally added in #886.

🎁 

Pull request:
fix: read keywords from package.json during transition
## PR Checklist

- [x] Addresses an existing open issue: fixes #2094
- [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

Removes the removal & flat-mapping from spaces because keywords actually can have spaces in them. CLI folks will have to specify them individually.

🎁 

Patch:
diff --git a/docs/CLI.md b/docs/CLI.md
index 50d1ff5..160e192 100644
--- a/docs/CLI.md
+++ b/docs/CLI.md
@@ -65,11 +65,11 @@ For example, customizing the npm author and funding source to different values t
 npx create-typescript-app --author my-npm-username --funding MyGitHubOrganization
 ```
 
-Array flags can be specified as space-delineated strings and/or multiple times.
-For example, customizing keywords to three:
+Array flags can be specified as multiple times.
+For example, customizing keywords to two:
 
 ```shell
-npx create-typescript-app --keywords eslint --keywords "javascript typescript"
+npx create-typescript-app --keywords eslint --keywords typescript
 ```
 
 ## Block Exclusion Flags
diff --git a/src/base.ts b/src/base.ts
index df48725..8622509 100644
--- a/src/base.ts
+++ b/src/base.ts
@@ -22,6 +22,7 @@ import { readFileSafe } from "./options/readFileSafe.js";
 import { readFunding } from "./options/readFunding.js";
 import { readGitDefaults } from "./options/readGitDefaults.js";
 import { readGuide } from "./options/readGuide.js";
+import { readKeywords } from "./options/readKeywords.js";
 import { readLogo } from "./options/readLogo.js";
 import { readNode } from "./options/readNode.js";
 import { readNpmDefaults } from "./options/readNpmDefaults.js";
@@ -208,6 +209,10 @@ export const base = createBase({
 				),
 		);
 
+		const getKeywords = lazyValue(
+			async () => await readKeywords(getPackageData),
+		);
+
 		const getEmailFromCodeOfConduct = lazyValue(
 			async () => await readEmailFromCodeOfConduct(take),
 		);
@@ -309,6 +314,7 @@ export const base = createBase({
 			explainer: getExplainer,
 			funding: getFunding,
 			guide: getGuide,
+			keywords: getKeywords,
 			logo: getLogo,
 			node: getNode,
 			owner: getOwner,
diff --git a/src/blocks/blockPackageJson.ts b/src/blocks/blockPackageJson.ts
index 2f9c55c..dd9b5e8 100644
--- a/src/blocks/blockPackageJson.ts
+++ b/src/blocks/blockPackageJson.ts
@@ -67,9 +67,7 @@ export const blockPackageJson = base.createBlock({
 							]
 								.filter(Boolean)
 								.sort(),
-							keywords: options.keywords?.flatMap((keyword) =>
-								keyword.split(/ /),
-							),
+							keywords: options.keywords,
 							license: "MIT",
 							name: options.repository,
 							repository: {
diff --git a/src/options/readKeywords.ts b/src/options/readKeywords.ts
new file mode 100644
index 0000000..4dc6379
--- /dev/null
+++ b/src/options/readKeywords.ts
@@ -0,0 +1,11 @@
+import { PartialPackageData } from "../types.js";
+
+export async function readKeywords(
+	getPackageData: () => Promise<PartialPackageData>,
+) {
+	const { keywords } = await getPackageData();
+
+	return (
+		keywords && Array.from(new Set((await getPackageData()).keywords)).sort()
+	);
+}
diff --git a/src/types.ts b/src/types.ts
index 509c865..8c42444 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -18,6 +18,7 @@ export interface PartialPackageData {
 	devDependencies?: Record<string, string>;
 	email?: string;
 	engines?: { node?: string };
+	keywords?: string[];
 	name?: string;
 	packageManager?: string;
 	publishConfig?: PartialPublishConfig;


Test patch:
diff --git a/src/blocks/blockPackageJson.test.ts b/src/blocks/blockPackageJson.test.ts
index 1c43051..81c7cf1 100644
--- a/src/blocks/blockPackageJson.test.ts
+++ b/src/blocks/blockPackageJson.test.ts
@@ -142,7 +142,7 @@ describe("blockPackageJson", () => {
 		expect(creation).toMatchInlineSnapshot(`
 			{
 			  "files": {
-			    "package.json": "{"name":"test-repository","version":"0.0.0","description":"A very very very very very very very very very very very very very very very very long HTML-ish description ending with an emoji. 🧵","keywords":["abc","def","ghi"],"repository":{"type":"git","url":"git+https://github.com/test-owner/test-repository.git"},"license":"MIT","author":{"email":"npm@email.com"},"type":"module","files":["README.md","package.json"],"engines":{"node":">=20.12.0"}}",
+			    "package.json": "{"name":"test-repository","version":"0.0.0","description":"A very very very very very very very very very very very very very very very very long HTML-ish description ending with an emoji. 🧵","keywords":["abc","def ghi"],"repository":{"type":"git","url":"git+https://github.com/test-owner/test-repository.git"},"license":"MIT","author":{"email":"npm@email.com"},"type":"module","files":["README.md","package.json"],"engines":{"node":">=20.12.0"}}",
 			  },
 			  "scripts": [
 			    {
diff --git a/src/options/readKeywords.test.ts b/src/options/readKeywords.test.ts
new file mode 100644
index 0000000..a79e470
--- /dev/null
+++ b/src/options/readKeywords.test.ts
@@ -0,0 +1,21 @@
+import { describe, expect, it } from "vitest";
+
+import { readKeywords } from "./readKeywords.js";
+
+describe(readKeywords, () => {
+	it("resolves with undefined when there are no existing keywords", async () => {
+		const actual = await readKeywords(() => Promise.resolve({}));
+
+		expect(actual).toBeUndefined();
+	});
+
+	it("resolves with deduplicated and sorted keywords when there are existing keywords", async () => {
+		const actual = await readKeywords(() =>
+			Promise.resolve({
+				keywords: ["b", "a", "c d", "b", "a"],
+			}),
+		);
+
+		expect(actual).toEqual(["a", "b", "c d"]);
+	});
+});