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

Issue:
🚀 Feature: Add packageManager back to the root `package.json`
### 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

Coming over from https://github.com/JoshuaKGoldberg/create/issues/145:

> Consider adding `packageManager` to the root `package.json`, since it was unclear which version of `pnpm` was needed (I had the wrong one and had to upgrade)

Agreed, it's irksome having to guess which package manager & version to use. You can kind of clue in which package manager it is from the lockfile existing, but that's manual. And the version is very much educated-guesswork. 

### Additional Info

This actually used to exist in this repo, but I removed it:

1. https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1513
2. https://github.com/JoshuaKGoldberg/create-typescript-app/pull/1529

This was discussed in https://github.com/pnpm/pnpm/issues/8087, and at the time of #1529, still open - hence the removing of the field in this repo. It was later resolved in https://github.com/pnpm/pnpm/pull/8166. Phew! So we are unblocked in this repo.

🎁 

Co-authored-by: @boneskull 

Pull request description:
feat: add packageManager back to package.json, and set to pnpm 10.4.0 in CI
## PR Checklist

- [x] Addresses an existing open issue: fixes #1906
- [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

Adds a `pnpm` option to the Base, which defaults to the existing `packageManager`'s pnpm version if it exists.

🎁 

Patch:
diff --git a/.github/actions/prepare/action.yml b/.github/actions/prepare/action.yml
index e8b953c..edf107c 100644
--- a/.github/actions/prepare/action.yml
+++ b/.github/actions/prepare/action.yml
@@ -5,8 +5,6 @@ name: Prepare
 runs:
   steps:
     - uses: pnpm/action-setup@v4
-      with:
-        version: 9
     - uses: actions/setup-node@v4
       with:
         cache: pnpm
diff --git a/package.json b/package.json
index 039bf76..7bbf451 100644
--- a/package.json
+++ b/package.json
@@ -108,6 +108,7 @@
 		"typescript-eslint": "8.26.0",
 		"vitest": "3.0.7"
 	},
+	"packageManager": "pnpm@10.4.0",
 	"engines": {
 		"node": ">=18.3.0"
 	},
diff --git a/src/base.ts b/src/base.ts
index 530748b..85e77e6 100644
--- a/src/base.ts
+++ b/src/base.ts
@@ -19,6 +19,7 @@ import { readFileSafe } from "./options/readFileSafe.js";
 import { readFunding } from "./options/readFunding.js";
 import { readGuide } from "./options/readGuide.js";
 import { readPackageData } from "./options/readPackageData.js";
+import { readPnpm } from "./options/readPnpm.js";
 import { swallowError } from "./utils/swallowError.js";
 import { tryCatchLazyValueAsync } from "./utils/tryCatchLazyValueAsync.js";
 
@@ -122,6 +123,10 @@ export const base = createBase({
 			})
 			.optional()
 			.describe("additional properties to include in `package.json`"),
+		pnpm: z
+			.string()
+			.optional()
+			.describe("pnpm version for package.json's packageManager field"),
 		repository: z
 			.string()
 			.describe("'kebab-case' or 'PascalCase' title of the repository"),
@@ -207,6 +212,8 @@ export const base = createBase({
 			};
 		});
 
+		const pnpm = lazyValue(async () => readPnpm(packageData));
+
 		const version = lazyValue(async () => (await packageData()).version);
 
 		const owner = lazyValue(
@@ -248,6 +255,7 @@ export const base = createBase({
 					scripts: original.scripts,
 				};
 			},
+			pnpm,
 			repository,
 			rulesetId,
 			...readDefaultsFromReadme(readme, repository),
diff --git a/src/blocks/blockGitHubActionsCI.ts b/src/blocks/blockGitHubActionsCI.ts
index 6f96396..240a366 100644
--- a/src/blocks/blockGitHubActionsCI.ts
+++ b/src/blocks/blockGitHubActionsCI.ts
@@ -51,7 +51,6 @@ export const blockGitHubActionsCI = base.createBlock({
 										steps: [
 											{
 												uses: "pnpm/action-setup@v4",
-												with: { version: 9 },
 											},
 											{
 												uses: "actions/setup-node@v4",
diff --git a/src/blocks/blockPackageJson.ts b/src/blocks/blockPackageJson.ts
index 69e304b..04d781a 100644
--- a/src/blocks/blockPackageJson.ts
+++ b/src/blocks/blockPackageJson.ts
@@ -59,6 +59,9 @@ export const blockPackageJson = base.createBlock({
 									node: `>=${options.node.minimum}`,
 								},
 							}),
+							...(options.pnpm && {
+								packageManager: `pnpm@${options.pnpm}`,
+							}),
 							files: [
 								options.bin?.replace(/^\.\//, ""),
 								...(addons.properties.files ?? []),
diff --git a/src/options/readPnpm.ts b/src/options/readPnpm.ts
new file mode 100644
index 0000000..0fed674
--- /dev/null
+++ b/src/options/readPnpm.ts
@@ -0,0 +1,9 @@
+import { PartialPackageData } from "../types.js";
+
+export async function readPnpm(packageData: () => Promise<PartialPackageData>) {
+	const { packageManager } = await packageData();
+
+	return packageManager?.startsWith("pnpm@")
+		? packageManager.slice("pnpm@".length)
+		: "10.4.0";
+}
diff --git a/src/types.ts b/src/types.ts
index e4f3729..4dc2aed 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -19,6 +19,7 @@ export interface PartialPackageData {
 	email?: string;
 	engines?: { node?: string };
 	name?: string;
+	packageManager?: string;
 	repository?: string | { type: string; url: string };
 	scripts?: Record<string, string>;
 	version?: string;


Test patch:
diff --git a/src/base.test.ts b/src/base.test.ts
index aaa16a8..678427d 100644
--- a/src/base.test.ts
+++ b/src/base.test.ts
@@ -47,6 +47,7 @@ describe("base", () => {
 			},
 			owner: "JoshuaKGoldberg",
 			packageData: expect.any(Object),
+			pnpm: expect.any(String),
 			repository: "create-typescript-app",
 			title: "Create TypeScript App",
 			usage: expect.any(String),
diff --git a/src/blocks/blockGitHubActionsCI.test.ts b/src/blocks/blockGitHubActionsCI.test.ts
index acd93a7..6f1bd64 100644
--- a/src/blocks/blockGitHubActionsCI.test.ts
+++ b/src/blocks/blockGitHubActionsCI.test.ts
@@ -31,8 +31,6 @@ describe("blockGitHubActionsCI", () => {
 			runs:
 			  steps:
 			    - uses: pnpm/action-setup@v4
-			      with:
-			        version: 9
 			    - uses: actions/setup-node@v4
 			      with:
 			        cache: pnpm
@@ -128,8 +126,6 @@ describe("blockGitHubActionsCI", () => {
 			runs:
 			  steps:
 			    - uses: pnpm/action-setup@v4
-			      with:
-			        version: 9
 			    - uses: actions/setup-node@v4
 			      with:
 			        cache: pnpm
@@ -250,8 +246,6 @@ describe("blockGitHubActionsCI", () => {
 			runs:
 			  steps:
 			    - uses: pnpm/action-setup@v4
-			      with:
-			        version: 9
 			    - uses: actions/setup-node@v4
 			      with:
 			        cache: pnpm
diff --git a/src/blocks/blockPackageJson.test.ts b/src/blocks/blockPackageJson.test.ts
index 3ec7da6..009b215 100644
--- a/src/blocks/blockPackageJson.test.ts
+++ b/src/blocks/blockPackageJson.test.ts
@@ -126,4 +126,32 @@ describe("blockPackageJson", () => {
 			}
 		`);
 	});
+
+	test("with node and pnpm versions", () => {
+		const creation = testBlock(blockPackageJson, {
+			options: {
+				...options,
+				node: {
+					minimum: "22.0.0",
+				},
+				pnpm: "10.4.0",
+			},
+		});
+
+		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. 🧵","repository":{"type":"git","url":"git+https://github.com/test-owner/test-repository.git"},"license":"MIT","author":{"email":"npm@email.com"},"type":"module","main":"lib/index.js","files":["README.md","package.json"],"packageManager":"pnpm@10.4.0","engines":{"node":">=22.0.0"}}",
+			  },
+			  "scripts": [
+			    {
+			      "commands": [
+			        "pnpm install",
+			      ],
+			      "phase": 1,
+			    },
+			  ],
+			}
+		`);
+	});
 });
diff --git a/src/options/readPnpm.test.ts b/src/options/readPnpm.test.ts
new file mode 100644
index 0000000..36e2592
--- /dev/null
+++ b/src/options/readPnpm.test.ts
@@ -0,0 +1,31 @@
+import { describe, expect, it } from "vitest";
+
+import { readPnpm } from "./readPnpm.js";
+
+describe(readPnpm, () => {
+	it("returns 10.4.0 when there is no existing packageManager", async () => {
+		const actual = await readPnpm(() => Promise.resolve({}));
+
+		expect(actual).toEqual("10.4.0");
+	});
+
+	it("returns 10.4.0 when an existing packageManager is not pnpm", async () => {
+		const actual = await readPnpm(() =>
+			Promise.resolve({
+				packageManager: "yarn@1.2.3",
+			}),
+		);
+
+		expect(actual).toEqual("10.4.0");
+	});
+
+	it("returns the existing pnpm version when an existing packageManager is pnpm", async () => {
+		const actual = await readPnpm(() =>
+			Promise.resolve({
+				packageManager: "pnpm@10.11.12",
+			}),
+		);
+
+		expect(actual).toEqual("10.11.12");
+	});
+});