Repository: JoshuaKGoldberg/template-typescript-node-package (ts)

Issue:
🐛 Bug: Hydration script shouldn't clear existing allcontributors
### 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/template-typescript-node-package/issues?q=is%3Aissue) and found none that matched my issue.

### Expected

#441 added a hydration script that can set up an _existing_ repository with this template's tooling. That hydration script ideally shouldn't remove any information from the repository: for example, it shouldn't wipe out the list of contributors in the `.all-contributorsrc` file...

### Actual

...but in actuality, that's exactly what happens. 😬 

The problem is that the function that generates new `.all-contributorsrc` content doesn't respect the _existing_ contributors in the file, if it exists. It just creates a new file with the same content always:

https://github.com/JoshuaKGoldberg/template-typescript-node-package/blob/791332de33d8c89c62c689b9ed5ca7c10234fcb4/src/hydrate/creation/rootFiles.ts#L24-L46

Let's make the file take in any existing contributors if they exist!

### Additional Info

This'll likely result in the `createRootFiles` function becoming `async`, as well as every function that calls it...

Pull request description:
fix: respect existing contributors in hydration
## PR Checklist

- [x] Addresses an existing open issue: fixes #449
- [x] That issue was marked as [`status: accepting prs`](https://github.com/JoshuaKGoldberg/template-typescript-node-package/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+accepting+prs%22)
- [x] Steps in [CONTRIBUTING.md](https://github.com/JoshuaKGoldberg/template-typescript-node-package/blob/main/.github/CONTRIBUTING.md) were taken

## Overview

Reads the existing `.all-contributorsrc` if it exists.

Shuffles code around a bit to make a new shared `readFileAsJsonSafe`.

Patch:
diff --git a/README.md b/README.md
index d6fbae5..c7b6be3 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
 <!-- prettier-ignore-end -->
 	</a>
 	<a href="https://codecov.io/gh/JoshuaKGoldberg/template-typescript-node-package" target="_blank">
-		<img alt="Codecov Test Coverage" src="https://codecov.io/gh/JoshuaKGoldberg/template-typescript-node-package/branch/main/graph/badge.svg?token=eVIFY4MhfQ"/>
+		<img alt="Codecov Test Coverage" src="https://codecov.io/gh/JoshuaKGoldberg/template-typescript-node-package/branch/main/graph/badge.svg"/>
 	</a>
 	<a href="https://github.com/JoshuaKGoldberg/template-typescript-node-package/blob/main/.github/CODE_OF_CONDUCT.md" target="_blank">
 		<img alt="Code of Conduct: Enforced 🤝" src="https://img.shields.io/badge/code_of_conduct-enforced_🤝-21bb42" />
diff --git a/package.json b/package.json
index 0dce397..15bcd97 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "template-typescript-node-package",
-	"version": "1.28.45",
+	"version": "1.28.46",
 	"description": "A quickstart-friendly TypeScript template with comprehensive formatting, linting, releases, testing, and other great tooling built-in. ✨",
 	"repository": {
 		"type": "git",
diff --git a/src/hydrate/steps/writeReadme.ts b/src/hydrate/steps/writeReadme.ts
index 7191c0d..eb7642a 100644
--- a/src/hydrate/steps/writeReadme.ts
+++ b/src/hydrate/steps/writeReadme.ts
@@ -1,6 +1,6 @@
 import * as fs from "node:fs/promises";
 
-import { readFileSafe } from "../readFileSafe.js";
+import { readFileSafe } from "../../shared/readFileSafe.js";
 import { HydrationInputValues } from "../values/types.js";
 
 const contributorsIndicator = `<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->`;
@@ -71,7 +71,7 @@ function generateTopContent(values: HydrationInputValues) {
 	}" target="_blank">${
 		values.unitTests
 			? `
-	<img alt="Codecov Test Coverage" src="https://codecov.io/gh/${values.owner}/${values.repository}/branch/main/graph/badge.svg?token=INSERT_CODECOV_TOKEN_HERE"/>`
+	<img alt="Codecov Test Coverage" src="https://codecov.io/gh/${values.owner}/${values.repository}/branch/main/graph/badge.svg"/>`
 			: ""
 	}
 	</a>
diff --git a/src/hydrate/steps/writing/creation/rootFiles.ts b/src/hydrate/steps/writing/creation/rootFiles.ts
index c0c3972..d076b2c 100644
--- a/src/hydrate/steps/writing/creation/rootFiles.ts
+++ b/src/hydrate/steps/writing/creation/rootFiles.ts
@@ -1,25 +1,12 @@
 import { HydrationInputValues } from "../../../values/types.js";
 import { formatIgnoreFile } from "./formatters/formatIgnoreFile.js";
 import { formatJson } from "./formatters/formatJson.js";
+import { writeAllContributorsRC } from "./writeAllContributorsRC.js";
 import { writePackageJson } from "./writePackageJson.js";
 
 export async function createRootFiles(values: HydrationInputValues) {
 	return {
-		".all-contributorsrc": await formatJson({
-			badgeTemplate:
-				'<img alt="All Contributors: <%= contributors.length %>" src="https://img.shields.io/badge/all_contributors-<%= contributors.length %>-21bb42.svg" />',
-			commit: false,
-			commitConvention: "angular",
-			contributors: [],
-			contributorsPerLine: 7,
-			contributorsSortAlphabetically: true,
-			files: ["README.md"],
-			imageSize: 100,
-			projectName: values.repository,
-			projectOwner: values.owner,
-			repoHost: "https://github.com",
-			repoType: "github",
-		}),
+		".all-contributorsrc": await writeAllContributorsRC(values),
 		".eslintignore": formatIgnoreFile([
 			"!.*",
 			...(values.unitTests ? ["coverage"] : []),
diff --git a/src/hydrate/steps/writing/creation/writeAllContributorsRC.ts b/src/hydrate/steps/writing/creation/writeAllContributorsRC.ts
new file mode 100644
index 0000000..9ff1a2c
--- /dev/null
+++ b/src/hydrate/steps/writing/creation/writeAllContributorsRC.ts
@@ -0,0 +1,26 @@
+import { readFileSafeAsJson } from "../../../../shared/readFileSafeAsJson.js";
+import { AllContributorsData } from "../../../../shared/types.js";
+import { HydrationInputValues } from "../../../values/types.js";
+import { formatJson } from "./formatters/formatJson.js";
+
+export async function writeAllContributorsRC(values: HydrationInputValues) {
+	const existing = (await readFileSafeAsJson(
+		".all-contributorsrc",
+	)) as AllContributorsData | null;
+
+	return await formatJson({
+		badgeTemplate:
+			'<img alt="All Contributors: <%= contributors.length %>" src="https://img.shields.io/badge/all_contributors-<%= contributors.length %>-21bb42.svg" />',
+		commit: false,
+		commitConvention: "angular",
+		contributors: existing?.contributors ?? [],
+		contributorsPerLine: 7,
+		contributorsSortAlphabetically: true,
+		files: ["README.md"],
+		imageSize: 100,
+		projectName: values.repository,
+		projectOwner: values.owner,
+		repoHost: "https://github.com",
+		repoType: "github",
+	});
+}
diff --git a/src/hydrate/values/getHydrationDefaults.ts b/src/hydrate/values/getHydrationDefaults.ts
index 8fb63e9..2095271 100644
--- a/src/hydrate/values/getHydrationDefaults.ts
+++ b/src/hydrate/values/getHydrationDefaults.ts
@@ -1,6 +1,6 @@
 import type { PartialPackageData } from "./types.js";
 
-import { readFileSafe } from "../readFileSafe.js";
+import { readFileSafe } from "../../shared/readFileSafe.js";
 import { readAuthorIfExists } from "./readAuthorIfExists.js";
 import { readEmailIfExists } from "./readEmailIfExists.js";
 import { readFundingIfExists } from "./readFundingIfExists.js";
diff --git a/src/hydrate/values/readTitleFromReadme.ts b/src/hydrate/values/readTitleFromReadme.ts
index a473d41..2108017 100644
--- a/src/hydrate/values/readTitleFromReadme.ts
+++ b/src/hydrate/values/readTitleFromReadme.ts
@@ -1,4 +1,4 @@
-import { readFileSafe } from "../readFileSafe.js";
+import { readFileSafe } from "../../shared/readFileSafe.js";
 
 export async function readTitleFromReadme() {
 	return (
diff --git a/src/setup/settings/addOwnerAsAllContributor.ts b/src/setup/settings/addOwnerAsAllContributor.ts
index f601f5a..5c32616 100644
--- a/src/setup/settings/addOwnerAsAllContributor.ts
+++ b/src/setup/settings/addOwnerAsAllContributor.ts
@@ -4,6 +4,7 @@ import fs from "node:fs/promises";
 import prettier from "prettier";
 
 import { readFileAsJson } from "../../shared/readFileAsJson.js";
+import { AllContributorsData } from "../../shared/types.js";
 
 interface GhUserOutput {
 	login: string;
@@ -60,10 +61,6 @@ export async function addOwnerAsAllContributor(owner: string) {
 	);
 }
 
-interface AllContributorsData {
-	contributors: { contributions: string[]; login: string }[];
-}
-
 function isValidAllContributorsData(
 	value: unknown,
 ): value is AllContributorsData {
diff --git a/src/setup/setupWithInformation.ts b/src/setup/setupWithInformation.ts
index c594981..cf205f9 100644
--- a/src/setup/setupWithInformation.ts
+++ b/src/setup/setupWithInformation.ts
@@ -13,7 +13,6 @@ import { hydrateRepositoryLabels } from "./steps/labels/hydrateRepositoryLabels.
 import { removeSetupScripts } from "./steps/removeSetupScripts.js";
 import { resetGitTags } from "./steps/resetGitTags.js";
 import { uninstallPackages } from "./steps/uninstallPackages.js";
-import { updateAllContributorsFile } from "./steps/updateAllContributorsFile.js";
 import { updateAllContributorsTable } from "./steps/updateAllContributorsTable.js";
 import { updateLocalFiles } from "./steps/updateLocalFiles.js";
 import { updateReadme } from "./steps/updateReadme.js";
@@ -34,10 +33,6 @@ export async function setupWithInformation({
 		await updateLocalFiles({ ...values, npmAuthor });
 	}, "Updating all the files with provided details.");
 
-	await withSpinner(async () => {
-		await updateAllContributorsFile(values);
-	}, "Updating '.all-contributorsrc' with new repository details.");
-
 	await withSpinner(
 		updateReadme,
 		"Appending template-typescript-node-package notice to 'README.md'",
diff --git a/src/setup/steps/updateAllContributorsFile.ts b/src/setup/steps/updateAllContributorsFile.ts
deleted file mode 100644
index 71805b5..0000000
--- a/src/setup/steps/updateAllContributorsFile.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import fs from "node:fs/promises";
-import prettier from "prettier";
-
-import { InputValues } from "../../shared/inputs.js";
-
-export async function updateAllContributorsFile({
-	owner,
-	repository,
-}: Pick<InputValues, "owner" | "repository">) {
-	await fs.writeFile(
-		".all-contributorsrc",
-		await prettier.format(
-			JSON.stringify({
-				...JSON.parse((await fs.readFile(".all-contributorsrc")).toString()),
-				projectName: repository,
-				projectOwner: owner,
-			}),
-			{ parser: "json" },
-		),
-	);
-}
diff --git a/src/setup/steps/updateAllContributorsTable.ts b/src/setup/steps/updateAllContributorsTable.ts
index c04aa7e..2c0d3c1 100644
--- a/src/setup/steps/updateAllContributorsTable.ts
+++ b/src/setup/steps/updateAllContributorsTable.ts
@@ -2,6 +2,8 @@ import fs from "node:fs/promises";
 import prettier from "prettier";
 
 import { InputValues } from "../../shared/inputs.js";
+import { readFileSafeAsJson } from "../../shared/readFileSafeAsJson.js";
+import { AllContributorsData } from "../../shared/types.js";
 
 export async function updateAllContributorsTable({
 	owner,
@@ -11,7 +13,9 @@ export async function updateAllContributorsTable({
 		".all-contributorsrc",
 		await prettier.format(
 			JSON.stringify({
-				...JSON.parse((await fs.readFile(".all-contributorsrc")).toString()),
+				...((await readFileSafeAsJson(
+					".all-contributorsrc",
+				)) as AllContributorsData),
 				projectName: repository,
 				projectOwner: owner,
 			}),
diff --git a/src/hydrate/readFileSafe.test.ts b/src/shared/readFileSafe.test.ts
similarity index 100%
rename from src/hydrate/readFileSafe.test.ts
rename to src/shared/readFileSafe.test.ts
diff --git a/src/hydrate/readFileSafe.ts b/src/shared/readFileSafe.ts
similarity index 100%
rename from src/hydrate/readFileSafe.ts
rename to src/shared/readFileSafe.ts
diff --git a/src/shared/readFileSafeAsJson.ts b/src/shared/readFileSafeAsJson.ts
new file mode 100644
index 0000000..27b43c0
--- /dev/null
+++ b/src/shared/readFileSafeAsJson.ts
@@ -0,0 +1,5 @@
+import { readFileSafe } from "./readFileSafe.js";
+
+export async function readFileSafeAsJson(filePath: string) {
+	return JSON.parse(await readFileSafe(filePath, "null")) as unknown;
+}
diff --git a/src/shared/types.ts b/src/shared/types.ts
new file mode 100644
index 0000000..a7b1246
--- /dev/null
+++ b/src/shared/types.ts
@@ -0,0 +1,3 @@
+export interface AllContributorsData {
+	contributors: { contributions: string[]; login: string }[];
+}


Test patch:
diff --git a/src/hydrate/values/getHydrationDefaults.test.ts b/src/hydrate/values/getHydrationDefaults.test.ts
index dc7f9a9..01043b5 100644
--- a/src/hydrate/values/getHydrationDefaults.test.ts
+++ b/src/hydrate/values/getHydrationDefaults.test.ts
@@ -4,7 +4,7 @@ import { getHydrationDefaults } from "./getHydrationDefaults.js";
 
 const mockReadFileSafe = vi.fn();
 
-vi.mock("../readFileSafe.js", () => ({
+vi.mock("../../shared/readFileSafe.js", () => ({
 	get readFileSafe() {
 		return mockReadFileSafe;
 	},
diff --git a/src/hydrate/values/readTitleFromReadme.test.ts b/src/hydrate/values/readTitleFromReadme.test.ts
index 925a9ef..6b2223d 100644
--- a/src/hydrate/values/readTitleFromReadme.test.ts
+++ b/src/hydrate/values/readTitleFromReadme.test.ts
@@ -4,7 +4,7 @@ import { readTitleFromReadme } from "./readTitleFromReadme.js";
 
 const mockReadFileSafe = vi.fn();
 
-vi.mock("../readFileSafe.js", () => ({
+vi.mock("../../shared/readFileSafe.js", () => ({
 	get readFileSafe() {
 		return mockReadFileSafe;
 	},