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

Issue:
🚀 Feature: Better use tseslint.config extends and otherwise simplify eslint.config.js
### 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

Splitting out of #1670: I realized that `eslint.config.js` file is missing some size-cutting optimizations:

* It has a nested [`tseslint.config`](https://typescript-eslint.io/packages/typescript-eslint#config). The inner one is redundant.
* Instead of manually configuring `perfectionist/sort-objects`, it could use the newer [`settings`](https://perfectionist.dev/guide/getting-started#settings) available as of https://github.com/azat-io/eslint-plugin-perfectionist/pull/206
* I _think_ the `plugins: { vitest }` block could use the cleaner `vitest.configs.recommended`
* ~~I _think_ `n/no-missing-import` is no longer necessary now that lint relies on building~~ No, it's just that `create-typescript-app` itself doesn't have a `ts` code block in its README.md...
* ~~I _think_ `tseslint.disableTypeChecked` on `.md/*.ts` files is no longer necessary now that lint relies on building~~ No, it's just that `create-typescript-app` itself doesn't have a `ts` code block in its README.md...
* The disable of `no-constant-condition` can be removed now that it allows `while(true)` by default in ESLint v9.1.0
* `"@typescript-eslint/no-unused-vars": ["error", { caughtErrors: "all" }],` is now redundant too
* `parserOptions.projectService.defaultProject` can be omitted now that its default is `'tsconfig.json'` as of TSESLint 8.5.0
* The `files: ["*.jsonc"],` overrides are no longer necessary now that no `.jsonc` files exist

See https://github.com/JoshuaKGoldberg/create-typescript-app/blob/48c734da02f14e4e890d955607d9ad2426d2052a/eslint.config.js for the trimmed-down version.

### Additional Info

💖 

Pull request:
feat: streamlined eslint.config.js with better tseslint.config usage
## PR Checklist

- [x] Addresses an existing open issue: fixes #1690
- [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

Applies the changes in the issue. This brings the file line count -including comments- from 166 to 140.

Removes the `files: ["**/*.md/*.ts"],` from this repo's `eslint.config.js` because there's no `ts` codeblock in the README.md.

Updates the `typescript-eslint` package by necessity for the `defaultProject` default.

💖 

Patch:
diff --git a/eslint.config.js b/eslint.config.js
index 6a5a2a1..8d2312e 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -49,7 +49,7 @@ export default tseslint.config(
 	packageJson,
 	perfectionist.configs["recommended-natural"],
 	regexp.configs["flat/recommended"],
-	...tseslint.config({
+	{
 		extends: [
 			...tseslint.configs.strictTypeChecked,
 			...tseslint.configs.stylisticTypeChecked,
@@ -59,23 +59,13 @@ export default tseslint.config(
 			parserOptions: {
 				projectService: {
 					allowDefaultProject: ["*.config.*s", "bin/*.js", "script/*.ts"],
-					defaultProject: "./tsconfig.json",
 				},
 				tsconfigRootDir: import.meta.dirname,
 			},
 		},
 		rules: {
-			// These off-by-default rules work well for this repo and we like them on.
-			"logical-assignment-operators": [
-				"error",
-				"always",
-				{ enforceForIfStatements: true },
-			],
-			"operator-assignment": "error",
-
 			// These on-by-default rules don't work well for this repo and we like them off.
 			"jsdoc/lines-before-block": "off",
-			"no-constant-condition": "off",
 
 			// These on-by-default rules work well for this repo if configured
 			"@typescript-eslint/no-unnecessary-condition": [
@@ -84,7 +74,6 @@ export default tseslint.config(
 					allowConstantLoopConditions: true,
 				},
 			],
-			"@typescript-eslint/no-unused-vars": ["error", { caughtErrors: "all" }],
 			"@typescript-eslint/prefer-nullish-coalescing": [
 				"error",
 				{ ignorePrimitives: true },
@@ -97,47 +86,32 @@ export default tseslint.config(
 				"error",
 				{ allowExperimental: true },
 			],
-			"perfectionist/sort-objects": [
-				"error",
-				{
-					order: "asc",
-					partitionByComment: true,
-					type: "natural",
-				},
-			],
 
 			// Stylistic concerns that don't interfere with Prettier
+			"logical-assignment-operators": [
+				"error",
+				"always",
+				{ enforceForIfStatements: true },
+			],
 			"no-useless-rename": "error",
 			"object-shorthand": "error",
+			"operator-assignment": "error",
 		},
-	}),
-	{
-		files: ["*.jsonc"],
-		rules: {
-			"jsonc/comma-dangle": "off",
-			"jsonc/no-comments": "off",
-			"jsonc/sort-keys": "error",
+		settings: {
+			perfectionist: {
+				partitionByComment: true,
+				type: "natural",
+			},
 		},
 	},
 	{
 		extends: [tseslint.configs.disableTypeChecked],
 		files: ["**/*.md/*.ts"],
-		rules: {
-			"n/no-missing-import": [
-				"error",
-				{ allowModules: ["create-typescript-app"] },
-			],
-		},
 	},
 	{
+		extends: [vitest.configs.recommended],
 		files: ["**/*.test.*"],
-		languageOptions: {
-			globals: vitest.environments.env.globals,
-		},
-		plugins: { vitest },
 		rules: {
-			...vitest.configs.recommended.rules,
-
 			// These on-by-default rules aren't useful in test files.
 			"@typescript-eslint/no-unsafe-assignment": "off",
 			"@typescript-eslint/no-unsafe-call": "off",
diff --git a/package.json b/package.json
index 63a20a8..0f22386 100644
--- a/package.json
+++ b/package.json
@@ -100,7 +100,7 @@
 		"tsup": "^8.3.0",
 		"tsx": "^4.17.0",
 		"typescript": "^5.6.2",
-		"typescript-eslint": "^8.1.0",
+		"typescript-eslint": "^8.15.0",
 		"vitest": "^2.1.0"
 	},
 	"engines": {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a59b34a..cff5a28 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -98,7 +98,7 @@ importers:
         version: 2.1.1(vitest@2.1.1(@types/node@22.3.0))
       '@vitest/eslint-plugin':
         specifier: ^1.1.0
-        version: 1.1.0(@typescript-eslint/utils@8.1.0(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2))(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.3.0))
+        version: 1.1.0(@typescript-eslint/utils@8.15.0(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2))(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.3.0))
       c8:
         specifier: ^10.1.2
         version: 10.1.2
@@ -181,8 +181,8 @@ importers:
         specifier: ^5.6.2
         version: 5.6.2
       typescript-eslint:
-        specifier: ^8.1.0
-        version: 8.1.0(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2)
+        specifier: ^8.15.0
+        version: 8.15.0(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2)
       vitest:
         specifier: ^2.1.0
         version: 2.1.1(@types/node@22.3.0)
@@ -1162,8 +1162,8 @@ packages:
   '@types/unist@2.0.6':
     resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==}
 
-  '@typescript-eslint/eslint-plugin@8.1.0':
-    resolution: {integrity: sha512-LlNBaHFCEBPHyD4pZXb35mzjGkuGKXU5eeCA1SxvHfiRES0E82dOounfVpL4DCqYvJEKab0bZIA0gCRpdLKkCw==}
+  '@typescript-eslint/eslint-plugin@8.15.0':
+    resolution: {integrity: sha512-+zkm9AR1Ds9uLWN3fkoeXgFppaQ+uEVtfOV62dDmsy9QCNqlRHWNEck4yarvRNrvRcHQLGfqBNui3cimoz8XAg==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     peerDependencies:
       '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0
@@ -1173,8 +1173,8 @@ packages:
       typescript:
         optional: true
 
-  '@typescript-eslint/parser@8.1.0':
-    resolution: {integrity: sha512-U7iTAtGgJk6DPX9wIWPPOlt1gO57097G06gIcl0N0EEnNw8RGD62c+2/DiP/zL7KrkqnnqF7gtFGR7YgzPllTA==}
+  '@typescript-eslint/parser@8.15.0':
+    resolution: {integrity: sha512-7n59qFpghG4uazrF9qtGKBZXn7Oz4sOMm8dwNWDQY96Xlm2oX67eipqcblDj+oY1lLCbf1oltMZFpUso66Kl1A==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     peerDependencies:
       eslint: ^8.57.0 || ^9.0.0
@@ -1183,25 +1183,26 @@ packages:
       typescript:
         optional: true
 
-  '@typescript-eslint/scope-manager@8.1.0':
-    resolution: {integrity: sha512-DsuOZQji687sQUjm4N6c9xABJa7fjvfIdjqpSIIVOgaENf2jFXiM9hIBZOL3hb6DHK9Nvd2d7zZnoMLf9e0OtQ==}
+  '@typescript-eslint/scope-manager@8.15.0':
+    resolution: {integrity: sha512-QRGy8ADi4J7ii95xz4UoiymmmMd/zuy9azCaamnZ3FM8T5fZcex8UfJcjkiEZjJSztKfEBe3dZ5T/5RHAmw2mA==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  '@typescript-eslint/type-utils@8.1.0':
-    resolution: {integrity: sha512-oLYvTxljVvsMnldfl6jIKxTaU7ok7km0KDrwOt1RHYu6nxlhN3TIx8k5Q52L6wR33nOwDgM7VwW1fT1qMNfFIA==}
+  '@typescript-eslint/type-utils@8.15.0':
+    resolution: {integrity: sha512-UU6uwXDoI3JGSXmcdnP5d8Fffa2KayOhUUqr/AiBnG1Gl7+7ut/oyagVeSkh7bxQ0zSXV9ptRh/4N15nkCqnpw==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     peerDependencies:
+      eslint: ^8.57.0 || ^9.0.0
       typescript: '*'
     peerDependenciesMeta:
       typescript:
         optional: true
 
-  '@typescript-eslint/types@8.1.0':
-    resolution: {integrity: sha512-q2/Bxa0gMOu/2/AKALI0tCKbG2zppccnRIRCW6BaaTlRVaPKft4oVYPp7WOPpcnsgbr0qROAVCVKCvIQ0tbWog==}
+  '@typescript-eslint/types@8.15.0':
+    resolution: {integrity: sha512-n3Gt8Y/KyJNe0S3yDCD2RVKrHBC4gTUcLTebVBXacPy091E6tNspFLKRXlk3hwT4G55nfr1n2AdFqi/XMxzmPQ==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  '@typescript-eslint/typescript-estree@8.1.0':
-    resolution: {integrity: sha512-NTHhmufocEkMiAord/g++gWKb0Fr34e9AExBRdqgWdVBaKoei2dIyYKD9Q0jBnvfbEA5zaf8plUFMUH6kQ0vGg==}
+  '@typescript-eslint/typescript-estree@8.15.0':
+    resolution: {integrity: sha512-1eMp2JgNec/niZsR7ioFBlsh/Fk0oJbhaqO0jRyQBMgkz7RrFfkqF9lYYmBoGBaSiLnu8TAPQTwoTUiSTUW9dg==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     peerDependencies:
       typescript: '*'
@@ -1209,14 +1210,18 @@ packages:
       typescript:
         optional: true
 
-  '@typescript-eslint/utils@8.1.0':
-    resolution: {integrity: sha512-ypRueFNKTIFwqPeJBfeIpxZ895PQhNyH4YID6js0UoBImWYoSjBsahUn9KMiJXh94uOjVBgHD9AmkyPsPnFwJA==}
+  '@typescript-eslint/utils@8.15.0':
+    resolution: {integrity: sha512-k82RI9yGhr0QM3Dnq+egEpz9qB6Un+WLYhmoNcvl8ltMEededhh7otBVVIDDsEEttauwdY/hQoSsOv13lxrFzQ==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     peerDependencies:
       eslint: ^8.57.0 || ^9.0.0
+      typescript: '*'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
 
-  '@typescript-eslint/visitor-keys@8.1.0':
-    resolution: {integrity: sha512-ba0lNI19awqZ5ZNKh6wCModMwoZs457StTebQ0q1NP58zSi2F6MOZRXwfKZy+jB78JNJ/WH8GSh2IQNzXX8Nag==}
+  '@typescript-eslint/visitor-keys@8.15.0':
+    resolution: {integrity: sha512-h8vYOulWec9LhpwfAdZf2bjr8xIp0KNKnpgqSz0qqYYKAW/QZKw3ktRndbiAtUz4acH4QLQavwZBYCc0wulA/Q==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
   '@vitest/coverage-v8@2.1.1':
@@ -2034,8 +2039,8 @@ packages:
     resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
 
-  eslint-visitor-keys@4.0.0:
-    resolution: {integrity: sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==}
+  eslint-visitor-keys@4.2.0:
+    resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
   eslint@9.9.0:
@@ -2311,10 +2316,6 @@ packages:
     resolution: {integrity: sha512-3LifW9M4joGZasyYPz2A1U74zbC/45fvpXUvO/9KbSa+VV0aGZarWkfdgKyR9sExNP0t0x0ss/UMJpNpcaTspw==}
     engines: {node: '>=8'}
 
-  globby@11.1.0:
-    resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
-    engines: {node: '>=10'}
-
   globby@13.2.2:
     resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==}
     engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@@ -3765,10 +3766,11 @@ packages:
   typedarray@0.0.6:
     resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
 
-  typescript-eslint@8.1.0:
-    resolution: {integrity: sha512-prB2U3jXPJLpo1iVLN338Lvolh6OrcCZO+9Yv6AR+tvegPPptYCDBIHiEEUdqRi8gAv2bXNKfMUrgAd2ejn/ow==}
+  typescript-eslint@8.15.0:
+    resolution: {integrity: sha512-wY4FRGl0ZI+ZU4Jo/yjdBu0lVTSML58pu6PgGtJmCufvzfV565pUF6iACQt092uFOd49iLOTX/sEVmHtbSrS+w==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     peerDependencies:
+      eslint: ^8.57.0 || ^9.0.0
       typescript: '*'
     peerDependenciesMeta:
       typescript:
@@ -4877,14 +4879,14 @@ snapshots:
 
   '@types/unist@2.0.6': {}
 
-  '@typescript-eslint/eslint-plugin@8.1.0(@typescript-eslint/parser@8.1.0(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2))(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2)':
+  '@typescript-eslint/eslint-plugin@8.15.0(@typescript-eslint/parser@8.15.0(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2))(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2)':
     dependencies:
       '@eslint-community/regexpp': 4.11.0
-      '@typescript-eslint/parser': 8.1.0(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2)
-      '@typescript-eslint/scope-manager': 8.1.0
-      '@typescript-eslint/type-utils': 8.1.0(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2)
-      '@typescript-eslint/utils': 8.1.0(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2)
-      '@typescript-eslint/visitor-keys': 8.1.0
+      '@typescript-eslint/parser': 8.15.0(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2)
+      '@typescript-eslint/scope-manager': 8.15.0
+      '@typescript-eslint/type-utils': 8.15.0(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2)
+      '@typescript-eslint/utils': 8.15.0(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2)
+      '@typescript-eslint/visitor-keys': 8.15.0
       eslint: 9.9.0(jiti@2.4.0)
       graphemer: 1.4.0
       ignore: 5.3.2
@@ -4895,12 +4897,12 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/parser@8.1.0(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2)':
+  '@typescript-eslint/parser@8.15.0(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2)':
     dependencies:
-      '@typescript-eslint/scope-manager': 8.1.0
-      '@typescript-eslint/types': 8.1.0
-      '@typescript-eslint/typescript-estree': 8.1.0(typescript@5.6.2)
-      '@typescript-eslint/visitor-keys': 8.1.0
+      '@typescript-eslint/scope-manager': 8.15.0
+      '@typescript-eslint/types': 8.15.0
+      '@typescript-eslint/typescript-estree': 8.15.0(typescript@5.6.2)
+      '@typescript-eslint/visitor-keys': 8.15.0
       debug: 4.3.6
       eslint: 9.9.0(jiti@2.4.0)
     optionalDependencies:
@@ -4908,31 +4910,31 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/scope-manager@8.1.0':
+  '@typescript-eslint/scope-manager@8.15.0':
     dependencies:
-      '@typescript-eslint/types': 8.1.0
-      '@typescript-eslint/visitor-keys': 8.1.0
+      '@typescript-eslint/types': 8.15.0
+      '@typescript-eslint/visitor-keys': 8.15.0
 
-  '@typescript-eslint/type-utils@8.1.0(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2)':
+  '@typescript-eslint/type-utils@8.15.0(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2)':
     dependencies:
-      '@typescript-eslint/typescript-estree': 8.1.0(typescript@5.6.2)
-      '@typescript-eslint/utils': 8.1.0(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2)
+      '@typescript-eslint/typescript-estree': 8.15.0(typescript@5.6.2)
+      '@typescript-eslint/utils': 8.15.0(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2)
       debug: 4.3.6
+      eslint: 9.9.0(jiti@2.4.0)
       ts-api-utils: 1.3.0(typescript@5.6.2)
     optionalDependencies:
       typescript: 5.6.2
     transitivePeerDependencies:
-      - eslint
       - supports-color
 
-  '@typescript-eslint/types@8.1.0': {}
+  '@typescript-eslint/types@8.15.0': {}
 
-  '@typescript-eslint/typescript-estree@8.1.0(typescript@5.6.2)':
+  '@typescript-eslint/typescript-estree@8.15.0(typescript@5.6.2)':
     dependencies:
-      '@typescript-eslint/types': 8.1.0
-      '@typescript-eslint/visitor-keys': 8.1.0
+      '@typescript-eslint/types': 8.15.0
+      '@typescript-eslint/visitor-keys': 8.15.0
       debug: 4.3.6
-      globby: 11.1.0
+      fast-glob: 3.3.2
       is-glob: 4.0.3
       minimatch: 9.0.5
       semver: 7.6.3
@@ -4942,21 +4944,22 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/utils@8.1.0(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2)':
+  '@typescript-eslint/utils@8.15.0(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2)':
     dependencies:
       '@eslint-community/eslint-utils': 4.4.0(eslint@9.9.0(jiti@2.4.0))
-      '@typescript-eslint/scope-manager': 8.1.0
-      '@typescript-eslint/types': 8.1.0
-      '@typescript-eslint/typescript-estree': 8.1.0(typescript@5.6.2)
+      '@typescript-eslint/scope-manager': 8.15.0
+      '@typescript-eslint/types': 8.15.0
+      '@typescript-eslint/typescript-estree': 8.15.0(typescript@5.6.2)
       eslint: 9.9.0(jiti@2.4.0)
+    optionalDependencies:
+      typescript: 5.6.2
     transitivePeerDependencies:
       - supports-color
-      - typescript
 
-  '@typescript-eslint/visitor-keys@8.1.0':
+  '@typescript-eslint/visitor-keys@8.15.0':
     dependencies:
-      '@typescript-eslint/types': 8.1.0
-      eslint-visitor-keys: 3.4.3
+      '@typescript-eslint/types': 8.15.0
+      eslint-visitor-keys: 4.2.0
 
   '@vitest/coverage-v8@2.1.1(vitest@2.1.1(@types/node@22.3.0))':
     dependencies:
@@ -4976,11 +4979,11 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@vitest/eslint-plugin@1.1.0(@typescript-eslint/utils@8.1.0(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2))(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.3.0))':
+  '@vitest/eslint-plugin@1.1.0(@typescript-eslint/utils@8.15.0(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2))(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.3.0))':
     dependencies:
       eslint: 9.9.0(jiti@2.4.0)
     optionalDependencies:
-      '@typescript-eslint/utils': 8.1.0(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2)
+      '@typescript-eslint/utils': 8.15.0(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2)
       typescript: 5.6.2
       vitest: 2.1.1(@types/node@22.3.0)
 
@@ -5857,8 +5860,8 @@ snapshots:
 
   eslint-plugin-perfectionist@3.2.0(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2):
     dependencies:
-      '@typescript-eslint/types': 8.1.0
-      '@typescript-eslint/utils': 8.1.0(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2)
+      '@typescript-eslint/types': 8.15.0
+      '@typescript-eslint/utils': 8.15.0(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2)
       eslint: 9.9.0(jiti@2.4.0)
       minimatch: 10.0.1
       natural-compare-lite: 1.4.0
@@ -5895,7 +5898,7 @@ snapshots:
 
   eslint-visitor-keys@3.4.3: {}
 
-  eslint-visitor-keys@4.0.0: {}
+  eslint-visitor-keys@4.2.0: {}
 
   eslint@9.9.0(jiti@2.4.0):
     dependencies:
@@ -5913,7 +5916,7 @@ snapshots:
       debug: 4.3.6
       escape-string-regexp: 4.0.0
       eslint-scope: 8.0.2
-      eslint-visitor-keys: 4.0.0
+      eslint-visitor-keys: 4.2.0
       espree: 10.1.0
       esquery: 1.6.0
       esutils: 2.0.3
@@ -5942,7 +5945,7 @@ snapshots:
     dependencies:
       acorn: 8.12.1
       acorn-jsx: 5.3.2(acorn@8.12.1)
-      eslint-visitor-keys: 4.0.0
+      eslint-visitor-keys: 4.2.0
 
   espree@9.6.1:
     dependencies:
@@ -6239,15 +6242,6 @@ snapshots:
       merge2: 1.4.1
       slash: 3.0.0
 
-  globby@11.1.0:
-    dependencies:
-      array-union: 2.1.0
-      dir-glob: 3.0.1
-      fast-glob: 3.3.2
-      ignore: 5.3.2
-      merge2: 1.4.1
-      slash: 3.0.0
-
   globby@13.2.2:
     dependencies:
       dir-glob: 3.0.1
@@ -7723,15 +7717,15 @@ snapshots:
 
   typedarray@0.0.6: {}
 
-  typescript-eslint@8.1.0(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2):
+  typescript-eslint@8.15.0(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2):
     dependencies:
-      '@typescript-eslint/eslint-plugin': 8.1.0(@typescript-eslint/parser@8.1.0(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2))(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2)
-      '@typescript-eslint/parser': 8.1.0(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2)
-      '@typescript-eslint/utils': 8.1.0(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2)
+      '@typescript-eslint/eslint-plugin': 8.15.0(@typescript-eslint/parser@8.15.0(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2))(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2)
+      '@typescript-eslint/parser': 8.15.0(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2)
+      '@typescript-eslint/utils': 8.15.0(eslint@9.9.0(jiti@2.4.0))(typescript@5.6.2)
+      eslint: 9.9.0(jiti@2.4.0)
     optionalDependencies:
       typescript: 5.6.2
     transitivePeerDependencies:
-      - eslint
       - supports-color
 
   typescript@5.6.2: {}
diff --git a/src/steps/writing/creation/createESLintConfig.ts b/src/steps/writing/creation/createESLintConfig.ts
index e7a425e..48809ae 100644
--- a/src/steps/writing/creation/createESLintConfig.ts
+++ b/src/steps/writing/creation/createESLintConfig.ts
@@ -42,6 +42,31 @@ export async function createESLintConfig(options: Options) {
 		!options.excludeLintRegex && `	regexp.configs["flat/recommended"],`,
 	].filter(Boolean);
 
+	const rules = `{
+			${
+				options.excludeLintJSDoc
+					? ""
+					: `
+
+			// These on-by-default rules don't work well for this repo and we like them off.
+			"jsdoc/lines-before-block": "off",`
+			}${
+				options.excludeLintStylistic
+					? ""
+					: `
+
+			// Stylistic concerns that don't interfere with Prettier
+			"logical-assignment-operators": [
+				"error",
+				"always",
+				{ enforceForIfStatements: true },
+			],
+			"no-useless-rename": "error",
+			"object-shorthand": "error",
+			"operator-assignment": "error",`
+			}
+		}`;
+
 	return await formatTypeScript(`${imports.join("\n")}
 
 export default tseslint.config(
@@ -54,8 +79,12 @@ export default tseslint.config(
 		}
 			"lib",
 			"node_modules",
-			"pnpm-lock.yaml",
-			"**/*.snap",
+			"pnpm-lock.yaml",${
+				options.excludeTests
+					? ""
+					: `
+				"**/*.snap",`
+			}
 		],
 	},
 	{
@@ -64,7 +93,7 @@ export default tseslint.config(
 		},
 	},
 	${elements.join("\n")}
-	...tseslint.config({
+	{
 		extends: ${
 			options.excludeLintStylistic
 				? `tseslint.configs.${tseslintBase}TypeChecked`
@@ -77,72 +106,26 @@ export default tseslint.config(
 		languageOptions: {
 			parserOptions: {
 				projectService: {
-					allowDefaultProject: ["*.*s", "eslint.config.js"],
-					defaultProject: "./tsconfig.json",
+					allowDefaultProject: ["*.config.*s"],
 				},
 				tsconfigRootDir: import.meta.dirname
 			},
-		},
-		rules: {
-			${
-				!options.excludeLintJSDoc || !options.excludeLintStylistic
-					? "// These off-by-default rules work well for this repo and we like them on."
-					: ""
-			}${
-				options.excludeLintStylistic
-					? ""
-					: `
-			"logical-assignment-operators": [
-				"error",
-				"always",
-				{ enforceForIfStatements: true },
-			],
-			"operator-assignment": "error",`
-			}
-
-			// These on-by-default rules don't work well for this repo and we like them off.${
-				options.excludeLintJSDoc
-					? ""
-					: `
-			"jsdoc/lines-before-block": "off",`
-			}
-			"no-constant-condition": "off",
-
-			// These on-by-default rules work well for this repo if configured
-			"@typescript-eslint/no-unused-vars": ["error", { caughtErrors: "all" }],${
-				options.excludeLintPerfectionist
-					? ""
-					: `
-			"n/no-unsupported-features/node-builtins": [
-				"error",
-				{ allowExperimental: true },
-			],
-			"perfectionist/sort-objects": [
-				"error",
-				{
-					order: "asc",
-					partitionByComment: true,
-					type: "natural",
-				},
-			],`
-			}${
-				options.excludeLintStylistic
-					? ""
-					: `
-
-			// Stylistic concerns that don't interfere with Prettier
-			"no-useless-rename": "error",
-			"object-shorthand": "error",`
-			}
-		},
-	}),
-	{
-		files: ["*.jsonc"],
-		rules: {
-			"jsonc/comma-dangle": "off",
-			"jsonc/no-comments": "off",
-			"jsonc/sort-keys": "error",
-		},
+		},${
+			rules.replaceAll(/\s+/g, "") === "{}"
+				? ""
+				: `
+		rules: ${rules},`
+		}${
+			options.excludeLintPerfectionist
+				? ""
+				: `
+		settings: {
+			perfectionist: {
+				partitionByComment: true,
+				type: "natural",
+			},
+		},`
+		}
 	},
 	{
 		extends: [tseslint.configs.disableTypeChecked],
@@ -159,13 +142,8 @@ export default tseslint.config(
 			: `
 	{
 		files: ["**/*.test.*"],
-		languageOptions: {
-			globals: vitest.environments.env.globals,
-		},
-		plugins: { vitest, },
+		extends: [vitest.configs.recommended],
 		rules: {
-			...vitest.configs.recommended.rules,
-
 			// These on-by-default rules aren't useful in test files.
 			"@typescript-eslint/no-unsafe-assignment": "off",
 			"@typescript-eslint/no-unsafe-call": "off",


Test patch:
diff --git a/script/__snapshots__/migrate-test-e2e.ts.snap b/script/__snapshots__/migrate-test-e2e.ts.snap
index 1932db5..38e38b5 100644
--- a/script/__snapshots__/migrate-test-e2e.ts.snap
+++ b/script/__snapshots__/migrate-test-e2e.ts.snap
@@ -130,21 +130,21 @@ exports[`expected file changes > eslint.config.js 1`] = `
  			parserOptions: {
  				projectService: {
 -					allowDefaultProject: ["*.config.*s", "bin/*.js", "script/*.ts"],
-+					allowDefaultProject: ["*.*s", "eslint.config.js"],
- 					defaultProject: "./tsconfig.json",
++					allowDefaultProject: ["*.config.*s"],
  				},
  				tsconfigRootDir: import.meta.dirname,
+ 			},
 @@ ... @@ export default tseslint.config(
- 			"no-constant-condition": "off",
+ 			// These on-by-default rules don't work well for this repo and we like them off.
+ 			"jsdoc/lines-before-block": "off",
  
- 			// These on-by-default rules work well for this repo if configured
+-			// These on-by-default rules work well for this repo if configured
 -			"@typescript-eslint/no-unnecessary-condition": [
 -				"error",
 -				{
 -					allowConstantLoopConditions: true,
 -				},
 -			],
- 			"@typescript-eslint/no-unused-vars": ["error", { caughtErrors: "all" }],
 -			"@typescript-eslint/prefer-nullish-coalescing": [
 -				"error",
 -				{ ignorePrimitives: true },
@@ -153,9 +153,27 @@ exports[`expected file changes > eslint.config.js 1`] = `
 -				"error",
 -				{ allowBoolean: true, allowNullish: true, allowNumber: true },
 -			],
- 			"n/no-unsupported-features/node-builtins": [
+-			"n/no-unsupported-features/node-builtins": [
+-				"error",
+-				{ allowExperimental: true },
+-			],
+-
+ 			// Stylistic concerns that don't interfere with Prettier
+ 			"logical-assignment-operators": [
  				"error",
- 				{ allowExperimental: true },"
+@@ ... @@ export default tseslint.config(
+ 	{
+ 		extends: [tseslint.configs.disableTypeChecked],
+ 		files: ["**/*.md/*.ts"],
++		rules: {
++			"n/no-missing-import": [
++				"error",
++				{ allowModules: ["create-typescript-app"] },
++			],
++		},
+ 	},
+ 	{
+ 		extends: [vitest.configs.recommended],"
 `;
 
 exports[`expected file changes > knip.json 1`] = `
diff --git a/src/steps/writing/creation/createESLintConfig.test.ts b/src/steps/writing/creation/createESLintConfig.test.ts
index 8cc1632..e2e2a3e 100644
--- a/src/steps/writing/creation/createESLintConfig.test.ts
+++ b/src/steps/writing/creation/createESLintConfig.test.ts
@@ -56,7 +56,7 @@ describe("createESLintConfig", () => {
 
 				export default tseslint.config(
 				  {
-				    ignores: ["lib", "node_modules", "pnpm-lock.yaml", "**/*.snap"],
+				    ignores: ["lib", "node_modules", "pnpm-lock.yaml"],
 				  },
 				  {
 				    linterOptions: {
@@ -65,33 +65,17 @@ describe("createESLintConfig", () => {
 				  },
 				  eslint.configs.recommended,
 				  n.configs["flat/recommended"],
-				  ...tseslint.config({
+				  {
 				    extends: tseslint.configs.recommendedTypeChecked,
 				    files: ["**/*.js", "**/*.ts"],
 				    languageOptions: {
 				      parserOptions: {
 				        projectService: {
-				          allowDefaultProject: ["*.*s", "eslint.config.js"],
-				          defaultProject: "./tsconfig.json",
+				          allowDefaultProject: ["*.config.*s"],
 				        },
 				        tsconfigRootDir: import.meta.dirname,
 				      },
 				    },
-				    rules: {
-				      // These on-by-default rules don't work well for this repo and we like them off.
-				      "no-constant-condition": "off",
-
-				      // These on-by-default rules work well for this repo if configured
-				      "@typescript-eslint/no-unused-vars": ["error", { caughtErrors: "all" }],
-				    },
-				  }),
-				  {
-				    files: ["*.jsonc"],
-				    rules: {
-				      "jsonc/comma-dangle": "off",
-				      "jsonc/no-comments": "off",
-				      "jsonc/sort-keys": "error",
-				    },
 				  },
 				  {
 				    extends: [tseslint.configs.disableTypeChecked],
@@ -149,7 +133,7 @@ describe("createESLintConfig", () => {
 				  packageJson,
 				  perfectionist.configs["recommended-natural"],
 				  regexp.configs["flat/recommended"],
-				  ...tseslint.config({
+				  {
 				    extends: [
 				      ...tseslint.configs.strictTypeChecked,
 				      ...tseslint.configs.stylisticTypeChecked,
@@ -158,51 +142,30 @@ describe("createESLintConfig", () => {
 				    languageOptions: {
 				      parserOptions: {
 				        projectService: {
-				          allowDefaultProject: ["*.*s", "eslint.config.js"],
-				          defaultProject: "./tsconfig.json",
+				          allowDefaultProject: ["*.config.*s"],
 				        },
 				        tsconfigRootDir: import.meta.dirname,
 				      },
 				    },
 				    rules: {
-				      // These off-by-default rules work well for this repo and we like them on.
-				      "logical-assignment-operators": [
-				        "error",
-				        "always",
-				        { enforceForIfStatements: true },
-				      ],
-				      "operator-assignment": "error",
-
 				      // These on-by-default rules don't work well for this repo and we like them off.
 				      "jsdoc/lines-before-block": "off",
-				      "no-constant-condition": "off",
 
-				      // These on-by-default rules work well for this repo if configured
-				      "@typescript-eslint/no-unused-vars": ["error", { caughtErrors: "all" }],
-				      "n/no-unsupported-features/node-builtins": [
-				        "error",
-				        { allowExperimental: true },
-				      ],
-				      "perfectionist/sort-objects": [
+				      // Stylistic concerns that don't interfere with Prettier
+				      "logical-assignment-operators": [
 				        "error",
-				        {
-				          order: "asc",
-				          partitionByComment: true,
-				          type: "natural",
-				        },
+				        "always",
+				        { enforceForIfStatements: true },
 				      ],
-
-				      // Stylistic concerns that don't interfere with Prettier
 				      "no-useless-rename": "error",
 				      "object-shorthand": "error",
+				      "operator-assignment": "error",
 				    },
-				  }),
-				  {
-				    files: ["*.jsonc"],
-				    rules: {
-				      "jsonc/comma-dangle": "off",
-				      "jsonc/no-comments": "off",
-				      "jsonc/sort-keys": "error",
+				    settings: {
+				      perfectionist: {
+				        partitionByComment: true,
+				        type: "natural",
+				      },
 				    },
 				  },
 				  {
@@ -214,13 +177,8 @@ describe("createESLintConfig", () => {
 				  },
 				  {
 				    files: ["**/*.test.*"],
-				    languageOptions: {
-				      globals: vitest.environments.env.globals,
-				    },
-				    plugins: { vitest },
+				    extends: [vitest.configs.recommended],
 				    rules: {
-				      ...vitest.configs.recommended.rules,
-
 				      // These on-by-default rules aren't useful in test files.
 				      "@typescript-eslint/no-unsafe-assignment": "off",
 				      "@typescript-eslint/no-unsafe-call": "off",
diff --git a/src/steps/writing/creation/dotVSCode.test.ts b/src/steps/writing/creation/dotVSCode.test.ts
index 4732330..cbd9172 100644
--- a/src/steps/writing/creation/dotVSCode.test.ts
+++ b/src/steps/writing/creation/dotVSCode.test.ts
@@ -6,7 +6,7 @@ import { createDotVSCode } from "./dotVSCode.js";
 /* spellchecker: disable */
 function fakeOptions(
 	getExcludeValue: (exclusionName: string) => boolean,
-	bin?: string | undefined,
+	bin?: string,
 ) {
 	return {
 		access: "public",