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

Issue:
🚀 Feature: Allow specifying a CODECOV_TOKEN
### 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

Following the, ah, unexpected work around #1303, I've started populating a `CODECOV_TOKEN` in my repos:

https://github.com/JoshuaKGoldberg/create-typescript-app/blob/1d09b76debb2ac2b5e6fa6c69f0e77958f97fa84/.github/workflows/ci.yml#L68-L71

I'll want to add this in as an option so that template consumers -including my other repos- can have it be set for them as part of templating.

### Additional Info

I don't think this needs to be done by default for all users. I keep getting 429s from the Codecov service because I have dozens of repos. Most people don't need this by default and wouldn't benefit from having the getting-started steps also ask them to set this up.

That being said, it _would_ be a nice followup to see if that token can be made automatically through the API...

💖 

Pull request description:
feat: extract codecov uploading to a blockCodecov
## PR Checklist

- [x] Addresses an existing open issue: fixes #1848
- [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

Also removes `addons.flags` from `blockVitest` since this repo won't use them after #1839.

💖 

Patch:
diff --git a/README.md b/README.md
index efc7f1e..de519ca 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,9 @@
 <h1 align="center">Create TypeScript App</h1>
 
-<p align="center">Quickstart-friendly TypeScript template with comprehensive, configurable, opinionated tooling. 🎁</p>
+<p align="center">
+  Quickstart-friendly TypeScript template with comprehensive, configurable, opinionated tooling.
+  🎁
+</p>
 
 <p align="center">
 	<!-- prettier-ignore-start -->
diff --git a/src/next/blocks/blockCodecov.ts b/src/next/blocks/blockCodecov.ts
new file mode 100644
index 0000000..36203aa
--- /dev/null
+++ b/src/next/blocks/blockCodecov.ts
@@ -0,0 +1,47 @@
+import { z } from "zod";
+
+import { base } from "../base.js";
+import { blockGitHubApps } from "./blockGitHubApps.js";
+import { blockVitest } from "./blockVitest.js";
+import { CommandPhase } from "./phases.js";
+
+export const blockCodecov = base.createBlock({
+	about: {
+		name: "Codecov",
+	},
+	addons: { env: z.record(z.string(), z.string()).optional() },
+	migrate() {
+		return {
+			scripts: [
+				{
+					commands: ["rm .github/codecov.yml codecov.yml"],
+					phase: CommandPhase.Migrations,
+				},
+			],
+		};
+	},
+	produce({ addons }) {
+		const { env } = addons;
+		return {
+			addons: [
+				blockGitHubApps({
+					apps: [
+						{
+							name: "Codecov",
+							url: "https://github.com/apps/codecov",
+						},
+					],
+				}),
+				blockVitest({
+					actionSteps: [
+						{
+							...(env && { env }),
+							if: "always()",
+							uses: "codecov/codecov-action@v3",
+						},
+					],
+				}),
+			],
+		};
+	},
+});
diff --git a/src/next/blocks/blockESLint.ts b/src/next/blocks/blockESLint.ts
index cbbf38f..ed3a141 100644
--- a/src/next/blocks/blockESLint.ts
+++ b/src/next/blocks/blockESLint.ts
@@ -57,6 +57,7 @@ export const blockESLint = base.createBlock({
 	},
 	addons: {
 		beforeLint: z.string().optional(),
+		explanations: z.array(z.string()).default([]),
 		extensions: z.array(z.union([z.string(), zExtension])).default([]),
 		ignores: z.array(z.string()).default([]),
 		imports: z.array(zPackageImport).default([]),
@@ -74,7 +75,8 @@ export const blockESLint = base.createBlock({
 		};
 	},
 	produce({ addons, options }) {
-		const { extensions, ignores, imports, rules, settings } = addons;
+		const { explanations, extensions, ignores, imports, rules, settings } =
+			addons;
 
 		const importLines = [
 			'import eslint from "@eslint/js";',
@@ -101,7 +103,9 @@ export const blockESLint = base.createBlock({
 				languageOptions: {
 					parserOptions: {
 						projectService: {
-							allowDefaultProject: ["*.config.*s"],
+							allowDefaultProject: Array.from(
+								new Set(["*.config.*s", options.bin].filter(Boolean).sort()),
+							),
 						},
 						tsconfigRootDir: "import.meta.dirname",
 					},
@@ -197,7 +201,7 @@ Each should be shown in VS Code, and can be run manually on the command-line:
 				}),
 			],
 			files: {
-				"eslint.config.js": `${importLines.join("\n")}
+				"eslint.config.js": `${explanations.map((explanation) => `/*\n${explanation}\n*/\n`).join("")}${importLines.join("\n")}
 
 export default tseslint.config(
 	{ ignores: [${ignoreLines.join(", ")}] },
diff --git a/src/next/blocks/blockGitHubActionsCI.ts b/src/next/blocks/blockGitHubActionsCI.ts
index ad5f9d6..f9e995a 100644
--- a/src/next/blocks/blockGitHubActionsCI.ts
+++ b/src/next/blocks/blockGitHubActionsCI.ts
@@ -6,6 +6,15 @@ import { createSoloWorkflowFile } from "../../steps/writing/creation/dotGitHub/c
 import { base } from "../base.js";
 import { CommandPhase } from "./phases.js";
 
+export const zActionStep = z.intersection(
+	z.object({
+		env: z.record(z.string(), z.string()).optional(),
+		if: z.string().optional(),
+		with: z.record(z.string(), z.string()).optional(),
+	}),
+	z.union([z.object({ run: z.string() }), z.object({ uses: z.string() })]),
+);
+
 export const blockGitHubActionsCI = base.createBlock({
 	about: {
 		name: "GitHub Actions CI",
@@ -15,19 +24,7 @@ export const blockGitHubActionsCI = base.createBlock({
 			.array(
 				z.object({
 					name: z.string(),
-					steps: z.array(
-						z.intersection(
-							z.object({
-								env: z.record(z.string(), z.string()).optional(),
-								if: z.string().optional(),
-								with: z.record(z.string(), z.string()).optional(),
-							}),
-							z.union([
-								z.object({ run: z.string() }),
-								z.object({ uses: z.string() }),
-							]),
-						),
-					),
+					steps: z.array(zActionStep),
 				}),
 			)
 			.optional(),
diff --git a/src/next/presets/presetCommon.ts b/src/next/presets/presetCommon.ts
index 2f402f4..fa0e03e 100644
--- a/src/next/presets/presetCommon.ts
+++ b/src/next/presets/presetCommon.ts
@@ -1,5 +1,6 @@
 import { base } from "../base.js";
 import { blockAllContributors } from "../blocks/blockAllContributors.js";
+import { blockCodecov } from "../blocks/blockCodecov.js";
 import { blockReleaseIt } from "../blocks/blockReleaseIt.js";
 import { blockVitest } from "../blocks/blockVitest.js";
 import { presetMinimal } from "./presetMinimal.js";
@@ -13,6 +14,7 @@ export const presetCommon = base.createPreset({
 	blocks: [
 		...presetMinimal.blocks,
 		blockAllContributors,
+		blockCodecov,
 		blockReleaseIt,
 		blockVitest,
 	],
diff --git a/src/steps/writing/creation/createESLintConfig.ts b/src/steps/writing/creation/createESLintConfig.ts
index a7891d5..b7b3195 100644
--- a/src/steps/writing/creation/createESLintConfig.ts
+++ b/src/steps/writing/creation/createESLintConfig.ts
@@ -80,7 +80,7 @@ export default tseslint.config(
 		files: ["**/*.js", "**/*.ts"],
 		languageOptions: {
 			parserOptions: {
-				projectService: { allowDefaultProject: ["*.config.*s"] },
+				projectService: { allowDefaultProject: ${JSON.stringify(["*.config.*s", options.bin].filter(Boolean).sort())} },
 				tsconfigRootDir: import.meta.dirname
 			},
 		},${


Test patch:
diff --git a/src/next/blocks/blockCodecov.test.ts b/src/next/blocks/blockCodecov.test.ts
new file mode 100644
index 0000000..878e424
--- /dev/null
+++ b/src/next/blocks/blockCodecov.test.ts
@@ -0,0 +1,129 @@
+import { testBlock } from "create-testers";
+import { describe, expect, test } from "vitest";
+
+import { blockCodecov } from "./blockCodecov.js";
+import { optionsBase } from "./options.fakes.js";
+
+describe("blockCodecov", () => {
+	test("without addons or mode", () => {
+		const creation = testBlock(blockCodecov, {
+			options: optionsBase,
+		});
+
+		expect(creation).toMatchInlineSnapshot(`
+			{
+			  "addons": [
+			    {
+			      "addons": {
+			        "apps": [
+			          {
+			            "name": "Codecov",
+			            "url": "https://github.com/apps/codecov",
+			          },
+			        ],
+			      },
+			      "block": [Function],
+			    },
+			    {
+			      "addons": {
+			        "actionSteps": [
+			          {
+			            "if": "always()",
+			            "uses": "codecov/codecov-action@v3",
+			          },
+			        ],
+			      },
+			      "block": [Function],
+			    },
+			  ],
+			}
+		`);
+	});
+
+	test("migration mode", () => {
+		const creation = testBlock(blockCodecov, {
+			mode: "migrate",
+			options: optionsBase,
+		});
+
+		expect(creation).toMatchInlineSnapshot(`
+			{
+			  "addons": [
+			    {
+			      "addons": {
+			        "apps": [
+			          {
+			            "name": "Codecov",
+			            "url": "https://github.com/apps/codecov",
+			          },
+			        ],
+			      },
+			      "block": [Function],
+			    },
+			    {
+			      "addons": {
+			        "actionSteps": [
+			          {
+			            "if": "always()",
+			            "uses": "codecov/codecov-action@v3",
+			          },
+			        ],
+			      },
+			      "block": [Function],
+			    },
+			  ],
+			  "scripts": [
+			    {
+			      "commands": [
+			        "rm .github/codecov.yml codecov.yml",
+			      ],
+			      "phase": 0,
+			    },
+			  ],
+			}
+		`);
+	});
+
+	test("with addons", () => {
+		const creation = testBlock(blockCodecov, {
+			addons: {
+				env: {
+					CODECOV_TOKEN: "${{ secrets.CODECOV_TOKEN }}",
+				},
+			},
+			options: optionsBase,
+		});
+
+		expect(creation).toMatchInlineSnapshot(`
+			{
+			  "addons": [
+			    {
+			      "addons": {
+			        "apps": [
+			          {
+			            "name": "Codecov",
+			            "url": "https://github.com/apps/codecov",
+			          },
+			        ],
+			      },
+			      "block": [Function],
+			    },
+			    {
+			      "addons": {
+			        "actionSteps": [
+			          {
+			            "env": {
+			              "CODECOV_TOKEN": "\${{ secrets.CODECOV_TOKEN }}",
+			            },
+			            "if": "always()",
+			            "uses": "codecov/codecov-action@v3",
+			          },
+			        ],
+			      },
+			      "block": [Function],
+			    },
+			  ],
+			}
+		`);
+	});
+});
diff --git a/src/next/blocks/blockESLint.test.ts b/src/next/blocks/blockESLint.test.ts
index 4c24fa1..87e5098 100644
--- a/src/next/blocks/blockESLint.test.ts
+++ b/src/next/blocks/blockESLint.test.ts
@@ -272,6 +272,7 @@ describe("blockESLint", () => {
 		const creation = testBlock(blockESLint, {
 			addons: {
 				beforeLint: "Before lint.",
+				explanations: ["This is a great config!", "You should use it!"],
 				extensions: [
 					"a.configs.recommended",
 					{
@@ -406,7 +407,13 @@ describe("blockESLint", () => {
 			    },
 			  ],
 			  "files": {
-			    "eslint.config.js": "import eslint from "@eslint/js";
+			    "eslint.config.js": "/*
+			This is a great config!
+			*/
+			/*
+			You should use it!
+			*/
+			import eslint from "@eslint/js";
 			import a from "eslint-plugin-markdown"
 			import b from "eslint-plugin-regexp"
 			import tseslint from "typescript-eslint";
@@ -429,4 +436,138 @@ describe("blockESLint", () => {
 			}
 		`);
 	});
+
+	test("with options.bin", () => {
+		const creation = testBlock(blockESLint, {
+			options: {
+				...optionsBase,
+				bin: "bin/index.js",
+			},
+		});
+
+		expect(creation).toMatchInlineSnapshot(`
+			{
+			  "addons": [
+			    {
+			      "addons": {
+			        "words": [
+			          "tseslint",
+			        ],
+			      },
+			      "block": [Function],
+			    },
+			    {
+			      "addons": {
+			        "sections": {
+			          "Linting": {
+			            "contents": {
+			              "after": [
+			                "
+			For example, ESLint can be run with \`--fix\` to auto-fix some lint rule complaints:
+
+			\`\`\`shell
+			pnpm run lint --fix
+			\`\`\`
+			",
+			              ],
+			              "before": "
+			This package includes several forms of linting to enforce consistent code quality and styling.
+			Each should be shown in VS Code, and can be run manually on the command-line:
+			",
+			              "items": [
+			                "- \`pnpm lint\` ([ESLint](https://eslint.org) with [typescript-eslint](https://typescript-eslint.io)): Lints JavaScript and TypeScript source files",
+			              ],
+			              "plural": "Read the individual documentation for each linter to understand how it can be configured and used best.",
+			            },
+			          },
+			        },
+			      },
+			      "block": [Function],
+			    },
+			    {
+			      "addons": {
+			        "jobs": [
+			          {
+			            "name": "Lint",
+			            "steps": [
+			              {
+			                "run": "pnpm build",
+			              },
+			              {
+			                "run": "pnpm lint",
+			              },
+			            ],
+			          },
+			        ],
+			      },
+			      "block": [Function],
+			    },
+			    {
+			      "addons": {
+			        "properties": {
+			          "devDependencies": {
+			            "@eslint/js": "9.17.0",
+			            "@types/node": "22.10.2",
+			            "eslint": "9.17.0",
+			            "typescript-eslint": "8.19.0",
+			          },
+			          "scripts": {
+			            "lint": "eslint . --max-warnings 0",
+			          },
+			        },
+			      },
+			      "block": [Function],
+			    },
+			    {
+			      "addons": {
+			        "extensions": [
+			          "dbaeumer.vscode-eslint",
+			        ],
+			        "settings": {
+			          "editor.codeActionsOnSave": {
+			            "source.fixAll.eslint": "explicit",
+			          },
+			          "eslint.probe": [
+			            "javascript",
+			            "javascriptreact",
+			            "json",
+			            "jsonc",
+			            "markdown",
+			            "typescript",
+			            "typescriptreact",
+			            "yaml",
+			          ],
+			          "eslint.rules.customizations": [
+			            {
+			              "rule": "*",
+			              "severity": "warn",
+			            },
+			          ],
+			        },
+			      },
+			      "block": [Function],
+			    },
+			  ],
+			  "files": {
+			    "eslint.config.js": "import eslint from "@eslint/js";
+			import tseslint from "typescript-eslint";
+
+			export default tseslint.config(
+				{ ignores: ["lib", "node_modules", "pnpm-lock.yaml"] },
+				{ linterOptions: {"reportUnusedDisableDirectives":"error"} },
+				eslint.configs.recommended,
+				{ extends: [tseslint.configs.strictTypeChecked, tseslint.configs.stylisticTypeChecked], files: ["**/*.js", "**/*.ts"], languageOptions: {"parserOptions":{"projectService":{"allowDefaultProject":["*.config.*s","bin/index.js"]},"tsconfigRootDir":import.meta.dirname}}, }
+			);",
+			  },
+			  "scripts": [
+			    {
+			      "commands": [
+			        "pnpm lint --fix",
+			      ],
+			      "phase": 3,
+			    },
+			  ],
+			}
+		`);
+	});
 });
diff --git a/src/next/blocks/blockVitest.test.ts b/src/next/blocks/blockVitest.test.ts
index 7e36afe..3ae3e90 100644
--- a/src/next/blocks/blockVitest.test.ts
+++ b/src/next/blocks/blockVitest.test.ts
@@ -150,27 +150,12 @@ describe("blockVitest", () => {
 			              {
 			                "run": "pnpm run test --coverage",
 			              },
-			              {
-			                "if": "always()",
-			                "uses": "codecov/codecov-action@v3",
-			              },
 			            ],
 			          },
 			        ],
 			      },
 			      "block": [Function],
 			    },
-			    {
-			      "addons": {
-			        "apps": [
-			          {
-			            "name": "Codecov",
-			            "url": "https://github.com/apps/codecov",
-			          },
-			        ],
-			      },
-			      "block": [Function],
-			    },
 			    {
 			      "addons": {
 			        "properties": {
@@ -398,27 +383,12 @@ describe("blockVitest", () => {
 			              {
 			                "run": "pnpm run test --coverage",
 			              },
-			              {
-			                "if": "always()",
-			                "uses": "codecov/codecov-action@v3",
-			              },
 			            ],
 			          },
 			        ],
 			      },
 			      "block": [Function],
 			    },
-			    {
-			      "addons": {
-			        "apps": [
-			          {
-			            "name": "Codecov",
-			            "url": "https://github.com/apps/codecov",
-			          },
-			        ],
-			      },
-			      "block": [Function],
-			    },
 			    {
 			      "addons": {
 			        "properties": {
@@ -499,7 +469,7 @@ describe("blockVitest", () => {
 			  "scripts": [
 			    {
 			      "commands": [
-			        "rm .github/codecov.yml .mocha* codecov.yml jest.config.* vitest.config.*",
+			        "rm .mocha* jest.config.* vitest.config.*",
 			      ],
 			      "phase": 0,
 			    },
@@ -514,7 +484,6 @@ describe("blockVitest", () => {
 				coverage: {
 					directory: "coverage*",
 					exclude: ["other"],
-					flags: "unit",
 					include: ["src/"],
 				},
 				exclude: ["lib/"],
@@ -662,30 +631,12 @@ describe("blockVitest", () => {
 			              {
 			                "run": "pnpm run test --coverage",
 			              },
-			              {
-			                "if": "always()",
-			                "uses": "codecov/codecov-action@v3",
-			                "with": {
-			                  "flags": "unit",
-			                },
-			              },
 			            ],
 			          },
 			        ],
 			      },
 			      "block": [Function],
 			    },
-			    {
-			      "addons": {
-			        "apps": [
-			          {
-			            "name": "Codecov",
-			            "url": "https://github.com/apps/codecov",
-			          },
-			        ],
-			      },
-			      "block": [Function],
-			    },
 			    {
 			      "addons": {
 			        "properties": {
diff --git a/src/next/blocks/blockVitest.ts b/src/next/blocks/blockVitest.ts
index d1281b7..bdf8f6b 100644
--- a/src/next/blocks/blockVitest.ts
+++ b/src/next/blocks/blockVitest.ts
@@ -5,8 +5,7 @@ import { blockCSpell } from "./blockCSpell.js";
 import { blockDevelopmentDocs } from "./blockDevelopmentDocs.js";
 import { blockESLint } from "./blockESLint.js";
 import { blockExampleFiles } from "./blockExampleFiles.js";
-import { blockGitHubActionsCI } from "./blockGitHubActionsCI.js";
-import { blockGitHubApps } from "./blockGitHubApps.js";
+import { blockGitHubActionsCI, zActionStep } from "./blockGitHubActionsCI.js";
 import { blockGitignore } from "./blockGitignore.js";
 import { blockPackageJson } from "./blockPackageJson.js";
 import { blockPrettier } from "./blockPrettier.js";
@@ -20,32 +19,28 @@ export const blockVitest = base.createBlock({
 		name: "Vitest",
 	},
 	addons: {
+		actionSteps: z.array(zActionStep).default([]),
 		coverage: z
 			.object({
 				directory: z.string().optional(),
 				exclude: z.array(z.string()).optional(),
-				flags: z.string().optional(),
 				include: z.array(z.string()).optional(),
 			})
 			.default({}),
-		env: z.record(z.string(), z.string()).default({}),
 		exclude: z.array(z.string()).default([]),
-		flags: z.array(z.string()).default([]),
 	},
 	migrate() {
 		return {
 			scripts: [
 				{
-					commands: [
-						"rm .github/codecov.yml .mocha* codecov.yml jest.config.* vitest.config.*",
-					],
+					commands: ["rm .mocha* jest.config.* vitest.config.*"],
 					phase: CommandPhase.Migrations,
 				},
 			],
 		};
 	},
 	produce({ addons }) {
-		const { coverage, env, exclude = [], flags } = addons;
+		const { actionSteps, coverage, exclude = [] } = addons;
 		const coverageDirectory = coverage.directory ?? "coverage";
 		const excludeText = JSON.stringify(exclude);
 
@@ -152,23 +147,7 @@ describe("greet", () => {
 					jobs: [
 						{
 							name: "Test",
-							steps: [
-								{ run: "pnpm run test --coverage" },
-								{
-									...(Object.keys(env).length && { env }),
-									if: "always()",
-									uses: "codecov/codecov-action@v3",
-									...(coverage.flags && { with: { flags: coverage.flags } }),
-								},
-							],
-						},
-					],
-				}),
-				blockGitHubApps({
-					apps: [
-						{
-							name: "Codecov",
-							url: "https://github.com/apps/codecov",
+							steps: [{ run: "pnpm run test --coverage" }, ...actionSteps],
 						},
 					],
 				}),
@@ -181,7 +160,7 @@ describe("greet", () => {
 							"vitest",
 						),
 						scripts: {
-							test: `vitest ${flags.join(" ")}`.trim(),
+							test: "vitest",
 						},
 					},
 				}),
diff --git a/src/steps/writing/creation/index.test.ts b/src/steps/writing/creation/index.test.ts
index 5dd7e9b..80b2c88 100644
--- a/src/steps/writing/creation/index.test.ts
+++ b/src/steps/writing/creation/index.test.ts
@@ -180,7 +180,7 @@ const optionsBaseline: Options = {
 	access: "public",
 	author: "Test Author",
 	base: "everything",
-	bin: "bin/test.js",
+	bin: "bin/index.js",
 	description: "Test Description",
 	directory: "test-directory",
 	email: { github: "github@example.com", npm: "npm@example.com" },