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

Issue:
🐛 Bug: `options.bin` should have preceding `./` removed if present
### 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

Per https://docs.npmjs.com/cli/v11/configuring-npm/package-json#bin, the `bin` property in `package.json` doesn't need to start with `./`. It's cleaner not to, and not all other tools support a preceding `./`.

### Actual

No changes when run in transition mode right now. Example: https://github.com/JoshuaKGoldberg/github-sponsors-to-markdown/pull/749/files#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519R16

### Additional Info

🎁 

Pull request:
fix: trim preceding ./ from package.json bin
## PR Checklist

- [x] Addresses an existing open issue: fixes #2081
- [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/CHANGELOG.md b/CHANGELOG.md
index 0c4eba7..54b8727 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Changelog
 
+## [2.21.3](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/2.21.2...2.21.3) (2025-04-01)
+
+### Bug Fixes
+
+- deduplicate Vitest excludes entries ([#2083](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/2083)) ([713a00d](https://github.com/JoshuaKGoldberg/create-typescript-app/commit/713a00d3d24b089f1b57a9fcc5aa28cb2ba29b84)), closes [#2082](https://github.com/JoshuaKGoldberg/create-typescript-app/issues/2082)
+
 ## [2.21.2](https://github.com/JoshuaKGoldberg/create-typescript-app/compare/2.21.1...2.21.2) (2025-04-01)
 
 ### Bug Fixes
diff --git a/package.json b/package.json
index 5d8a307..fd0eb2a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "create-typescript-app",
-	"version": "2.21.2",
+	"version": "2.21.3",
 	"description": "Quickstart-friendly TypeScript template with comprehensive, configurable, opinionated tooling. 🎁",
 	"repository": {
 		"type": "git",
diff --git a/src/base.ts b/src/base.ts
index 5c60f07..15ece47 100644
--- a/src/base.ts
+++ b/src/base.ts
@@ -7,6 +7,7 @@ import { z } from "zod";
 import { readAccess } from "./options/readAccess.js";
 import { readAllContributors } from "./options/readAllContributors.js";
 import { readAuthor } from "./options/readAuthor.js";
+import { readBin } from "./options/readBin.js";
 import { readDescription } from "./options/readDescription.js";
 import { readDocumentation } from "./options/readDocumentation.js";
 import { readEmailFromCodeOfConduct } from "./options/readEmailFromCodeOfConduct.js";
@@ -181,7 +182,7 @@ export const base = createBase({
 				await readAuthor(getPackageAuthor, getNpmDefaults, options.owner),
 		);
 
-		const getBin = lazyValue(async () => (await getPackageData()).bin);
+		const getBin = lazyValue(async () => await readBin(getPackageData));
 
 		const getEmoji = lazyValue(async () => await readEmoji(getDescription));
 
diff --git a/src/options/readBin.ts b/src/options/readBin.ts
new file mode 100644
index 0000000..b097700
--- /dev/null
+++ b/src/options/readBin.ts
@@ -0,0 +1,17 @@
+import { PartialPackageData } from "../types.js";
+import { trimPrecedingSlash } from "../utils/trimPrecedingSlash.js";
+
+export async function readBin(
+	getPackageData: () => Promise<PartialPackageData>,
+) {
+	const { bin } = await getPackageData();
+
+	return typeof bin === "object"
+		? (Object.fromEntries(
+				Object.entries(bin).map(([key, value]) => [
+					key,
+					trimPrecedingSlash(value),
+				]),
+			) as typeof bin)
+		: trimPrecedingSlash(bin);
+}
diff --git a/src/utils/trimPrecedingSlash.ts b/src/utils/trimPrecedingSlash.ts
new file mode 100644
index 0000000..8323fd3
--- /dev/null
+++ b/src/utils/trimPrecedingSlash.ts
@@ -0,0 +1,3 @@
+export function trimPrecedingSlash(filePath: string | undefined) {
+	return filePath?.replace(/^\.\//, "");
+}


Test patch:
diff --git a/src/options/readBin.test.ts b/src/options/readBin.test.ts
new file mode 100644
index 0000000..c91bf01
--- /dev/null
+++ b/src/options/readBin.test.ts
@@ -0,0 +1,41 @@
+import { describe, expect, it } from "vitest";
+
+import { readBin } from "./readBin.js";
+
+describe(readBin, () => {
+	it("resolves with undefined when package data has no bin", async () => {
+		const getPackageData = () => Promise.resolve({});
+
+		const actual = await readBin(getPackageData);
+
+		expect(actual).toBe(undefined);
+	});
+
+	it("resolves with a trimmed string when the package data has a string bin", async () => {
+		const getPackageData = () =>
+			Promise.resolve({
+				bin: "./index.js",
+			});
+
+		const actual = await readBin(getPackageData);
+
+		expect(actual).toBe("index.js");
+	});
+
+	it("resolves with an object of trimmed bins when the package data has a string bin", async () => {
+		const getPackageData = () =>
+			Promise.resolve({
+				bin: {
+					absolute: "index.js",
+					relative: "./index.js",
+				},
+			});
+
+		const actual = await readBin(getPackageData);
+
+		expect(actual).toEqual({
+			absolute: "index.js",
+			relative: "index.js",
+		});
+	});
+});
diff --git a/src/utils/trimPrecedingSlash.test.ts b/src/utils/trimPrecedingSlash.test.ts
new file mode 100644
index 0000000..31b1e87
--- /dev/null
+++ b/src/utils/trimPrecedingSlash.test.ts
@@ -0,0 +1,15 @@
+import { describe, expect, test } from "vitest";
+
+import { trimPrecedingSlash } from "./trimPrecedingSlash.js";
+
+describe(trimPrecedingSlash, () => {
+	test.each([
+		[undefined, undefined],
+		["", ""],
+		["a", "a"],
+		["./a", "a"],
+	])("%s becomes %s", (input, expected) => {
+		const actual = trimPrecedingSlash(input);
+		expect(actual).toBe(expected);
+	});
+});