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

Issue:
🚀 Feature: Add an opt-in block for setting up an ESLint plugin
### 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

Now that this project is fully on Bingo (formerly: `create`), it's a lot easier to add opt-in (as in: not included in any presets) Blocks. One already exists in `blockAreTheTypesWrong` (#1633 -> #1644). I'd like to add more, representing other repo forms I personally have repeatedly set up.

I think it'd be useful to have a general "ESLint plugin" block. It should:

* Add [`eslint-plugin-eslint-plugin`](https://github.com/eslint-community/eslint-plugin-eslint-plugin) addons to the ESLint block
* Add [`eslint-doc-generator`](https://github.com/bmish/eslint-doc-generator) outputs
* Create example files of a `src/index.ts`, `src/rules`, etc.

### Additional Info

https://github.com/JoshuaKGoldberg/eslint-plugin-erasable-syntax-only is an example of a plugin I set up recently.

🎁 

Pull request description:
feat: add opt-in blockESLintPlugin
## PR Checklist

- [x] Addresses an existing open issue: fixes #1936
- [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 Block that devs can use to sync tooling for ESLint plugins built on CTA. It replaces the default example files with its own, so creating a new repo will look like:

```shell
npx create-typescript-app --add-eslint-plugin --exclude-example-files
```

🎁 

Patch:
diff --git a/cspell.json b/cspell.json
index 5bb799f..2b6087d 100644
--- a/cspell.json
+++ b/cspell.json
@@ -15,6 +15,7 @@
 		"attw",
 		"boop",
 		"dbaeumer",
+		"eslint-doc-generatorrc.js",
 		"infile",
 		"joshuakgoldberg",
 		"markdownlintignore",
diff --git a/docs/Blocks.md b/docs/Blocks.md
index 9cc5254..e08bc5c 100644
--- a/docs/Blocks.md
+++ b/docs/Blocks.md
@@ -23,6 +23,7 @@ This table summarizes each block and which base levels they're included in:
 | ESLint Node Plugin           | `--add-eslint-node-plugin`, `--exclude-eslint-node-plugin`                     |         |        | 💯         |
 | ESLint package.json Plugin   | `--add-eslint-package-json-plugin`, `--exclude-eslint-package-json-plugin`     |         |        | 💯         |
 | ESLint Perfectionist Plugin  | `--add-eslint-perfectionist-plugin`, `--exclude-eslint-perfectionist-plugin`   |         |        | 💯         |
+| ESLint Plugin                | `--add-eslint-plugin`, `--exclude-eslint-plugin`                               |         |        |            |
 | ESLint Regexp Plugin         | `--add-eslint-regexp-plugin`, `--exclude-eslint-regexp-plugin`                 |         |        | 💯         |
 | ESLint YML Plugin            | `--add-eslint-yml-plugin`, `--exclude-eslint-yml-plugin`                       |         |        | 💯         |
 | Funding                      | `--add-funding`, `--exclude-funding`                                           |         | ✅     | 💯         |
diff --git a/src/blocks/blockESLint.ts b/src/blocks/blockESLint.ts
index 757c924..1d6391d 100644
--- a/src/blocks/blockESLint.ts
+++ b/src/blocks/blockESLint.ts
@@ -49,7 +49,10 @@ const zExtension = z.object({
 });
 
 const zPackageImport = z.object({
-	source: z.string(),
+	source: z.union([
+		z.string(),
+		z.object({ packageName: z.string(), version: z.string() }),
+	]),
 	specifier: z.string(),
 	types: z.boolean().optional(),
 });
@@ -88,7 +91,7 @@ export const blockESLint = base.createBlock({
 			'import tseslint from "typescript-eslint";',
 			...imports.map(
 				(packageImport) =>
-					`import ${packageImport.specifier} from "${packageImport.source}"`,
+					`import ${packageImport.specifier} from "${typeof packageImport.source === "string" ? packageImport.source : packageImport.source.packageName}"`,
 			),
 		].sort((a, b) =>
 			a.replace(/.+from/, "").localeCompare(b.replace(/.+from/, "")),
@@ -183,17 +186,36 @@ Each should be shown in VS Code, and can be run manually on the command-line:
 				}),
 				blockPackageJson({
 					properties: {
-						devDependencies: getPackageDependencies(
-							"@eslint/js",
-							"@types/node",
-							"eslint",
-							"typescript-eslint",
-							...imports.flatMap(({ source, types }) => {
-								// eslint-disable-next-line @typescript-eslint/no-unsafe-call -- https://github.com/egoist/parse-package-name/issues/30
-								const { name } = parsePackageName(source) as { name: string };
-								return types ? [name, `@types/${name}`] : [name];
-							}),
-						),
+						devDependencies: {
+							...getPackageDependencies(
+								"@eslint/js",
+								"@types/node",
+								"eslint",
+								"typescript-eslint",
+								...imports
+									.filter((imported) => typeof imported.source === "string")
+									.flatMap(({ source, types }) => {
+										// eslint-disable-next-line @typescript-eslint/no-unsafe-call -- https://github.com/egoist/parse-package-name/issues/30
+										const { name } = parsePackageName(source) as {
+											name: string;
+										};
+										return types ? [name, `@types/${name}`] : [name];
+									}),
+							),
+							...Object.fromEntries(
+								imports
+									.filter(
+										(
+											imported,
+										): imported is typeof imported & { source: object } =>
+											typeof imported.source === "object",
+									)
+									.map((imported) => [
+										imported.source.packageName,
+										imported.source.version,
+									]),
+							),
+						},
 						scripts: {
 							lint: "eslint . --max-warnings 0",
 						},
diff --git a/src/blocks/blockESLintPlugin.ts b/src/blocks/blockESLintPlugin.ts
new file mode 100644
index 0000000..045403b
--- /dev/null
+++ b/src/blocks/blockESLintPlugin.ts
@@ -0,0 +1,225 @@
+import { base } from "../base.js";
+import { blockCSpell } from "./blockCSpell.js";
+import { blockDevelopmentDocs } from "./blockDevelopmentDocs.js";
+import { blockESLint } from "./blockESLint.js";
+import { blockGitHubActionsCI } from "./blockGitHubActionsCI.js";
+import { blockPackageJson } from "./blockPackageJson.js";
+import { blockVitest } from "./blockVitest.js";
+
+export const blockESLintPlugin = base.createBlock({
+	about: {
+		name: "ESLint Plugin",
+	},
+	produce() {
+		return {
+			addons: [
+				blockCSpell({
+					words: ["eslint-doc-generatorrc.js"],
+				}),
+				blockDevelopmentDocs({
+					sections: {
+						Building: {
+							innerSections: [
+								{
+									contents: `
+Run [\`eslint-doc-generator\`](https://github.com/bmish/eslint-doc-generator) to generate Markdown files documenting rules.
+
+\`\`\`shell
+pnpm build:docs
+\`\`\`
+		`,
+									heading: "Building Docs",
+								},
+							],
+						},
+						Linting: {
+							contents: {
+								items: [
+									`- \`pnpm lint:docs\` ([eslint-doc-generator](https://github.com/bmish/eslint-doc-generator)): Generates and validates documentation for ESLint rules`,
+								],
+							},
+						},
+					},
+				}),
+				blockESLint({
+					extensions: ['eslintPlugin.configs["flat/recommended"]'],
+					ignores: [".eslint-doc-generatorrc.js", "docs/rules/*/*.ts"],
+					imports: [
+						{
+							source: {
+								packageName: "eslint-plugin-eslint-plugin",
+								version: "6.4.0",
+							},
+							specifier: "eslintPlugin",
+						},
+					],
+				}),
+				blockGitHubActionsCI({
+					jobs: [
+						{
+							name: "Lint Docs",
+							steps: [
+								{ run: "pnpm build || exit 0" },
+								{ run: "pnpm lint:docs" },
+							],
+						},
+					],
+				}),
+				blockPackageJson({
+					properties: {
+						devDependencies: {
+							"eslint-doc-generator": "2.1.0",
+							"eslint-plugin-eslint-plugin": "6.4.0",
+						},
+						peerDependencies: {
+							"@typescript-eslint/parser": ">=8",
+							eslint: ">=9",
+							typescript: ">=5",
+						},
+						scripts: {
+							"build:docs": "eslint-doc-generator",
+							"lint:docs": "eslint-doc-generator --check",
+						},
+					},
+				}),
+				blockVitest({
+					coverage: {
+						exclude: ["src/index.ts", "src/rules/index.ts"],
+					},
+				}),
+			],
+			files: {
+				".eslint-doc-generatorrc.js": `import prettier from "prettier";
+
+/** @type {import('eslint-doc-generator').GenerateOptions} */
+const config = {
+	postprocess: async (content, path) =>
+		prettier.format(content, {
+			...(await prettier.resolveConfig(path)),
+			parser: "markdown",
+		}),
+	ruleDocTitleFormat: "prefix-name",
+};
+
+export default config;
+`,
+			},
+		};
+	},
+	setup({ options }) {
+		const pluginName = options.repository.replace("eslint-plugin-", "");
+
+		return {
+			files: {
+				src: {
+					"index.ts": `import Module from "node:module";
+
+import { rules } from "./rules/index.js";
+
+const require = Module.createRequire(import.meta.url);
+
+const { name, version } =
+	// \`import\`ing here would bypass the TSConfig's \`"rootDir": "src"\`
+	require("../package.json") as typeof import("../package.json");
+
+export const plugin = {
+	configs: {
+		get recommended() {
+			return recommended;
+		},
+	},
+	meta: { name, version },
+	rules,
+};
+
+const recommended = {
+	plugins: {
+		"${pluginName}": plugin,
+	},
+	rules: Object.fromEntries(
+		Object.keys(rules).map((rule) => [\`${pluginName}/\${rule}\`, "error"]),
+	),
+};
+
+export { rules };
+
+export default plugin;
+`,
+					rules: {
+						"example.test.ts": `import { rule } from "./enums.js";
+import { ruleTester } from "./ruleTester.js";
+
+ruleTester.run("enums", rule, {
+	invalid: [
+		{
+			code: \`enum Values {}\`,
+			errors: [
+				{
+					column: 1,
+					endColumn: 15,
+					endLine: 1,
+					line: 1,
+					messageId: "enum",
+				},
+			],
+		},
+	],
+	valid: [\`const Values = {};\`, \`const Values = {} as const;\`],
+});
+`,
+						"example.ts": `import { createRule } from "../utils.js";
+
+export const rule = createRule({
+	create(context) {
+		return {
+			TSEnumDeclaration(node) {
+				context.report({
+					messageId: "enum",
+					node,
+				});
+			},
+		};
+	},
+	defaultOptions: [],
+	meta: {
+		docs: {
+			description: "Avoid using TypeScript's enums.",
+		},
+		messages: {
+			enum: "This enum will not be allowed under TypeScript's --erasableSyntaxOnly.",
+		},
+		schema: [],
+		type: "problem",
+	},
+	name: "enums",
+});
+`,
+						"index.ts": `import { rule as example } from "./example.js";
+
+export const rules = {
+	example,
+};
+`,
+						"ruleTester.ts": `import { RuleTester } from "@typescript-eslint/rule-tester";
+import * as vitest from "vitest";
+
+RuleTester.afterAll = vitest.afterAll;
+RuleTester.it = vitest.it;
+RuleTester.itOnly = vitest.it.only;
+RuleTester.describe = vitest.describe;
+
+export const ruleTester = new RuleTester();
+`,
+					},
+					"utils.ts": `import { ESLintUtils } from "@typescript-eslint/utils";
+
+export const createRule = ESLintUtils.RuleCreator(
+	(name) =>
+		\`https://github.com/${options.owner}/${options.repository}/blob/main/docs/rules/\${name}.md\`,
+);
+`,
+				},
+			},
+		};
+	},
+});
diff --git a/src/blocks/index.ts b/src/blocks/index.ts
index b2b8db0..9e705fd 100644
--- a/src/blocks/index.ts
+++ b/src/blocks/index.ts
@@ -14,6 +14,7 @@ import { blockESLintMoreStyling } from "./blockESLintMoreStyling.js";
 import { blockESLintNode } from "./blockESLintNode.js";
 import { blockESLintPackageJson } from "./blockESLintPackageJson.js";
 import { blockESLintPerfectionist } from "./blockESLintPerfectionist.js";
+import { blockESLintPlugin } from "./blockESLintPlugin.js";
 import { blockESLintRegexp } from "./blockESLintRegexp.js";
 import { blockESLintYML } from "./blockESLintYML.js";
 import { blockFunding } from "./blockFunding.js";
@@ -61,6 +62,7 @@ export const blocks = {
 	blockESLintNode,
 	blockESLintPackageJson,
 	blockESLintPerfectionist,
+	blockESLintPlugin,
 	blockESLintRegexp,
 	blockESLintYML,
 	blockFunding,
@@ -109,6 +111,7 @@ export { blockESLintMoreStyling } from "./blockESLintMoreStyling.js";
 export { blockESLintNode } from "./blockESLintNode.js";
 export { blockESLintPackageJson } from "./blockESLintPackageJson.js";
 export { blockESLintPerfectionist } from "./blockESLintPerfectionist.js";
+export { blockESLintPlugin } from "./blockESLintPlugin.js";
 export { blockESLintRegexp } from "./blockESLintRegexp.js";
 export { blockESLintYML } from "./blockESLintYML.js";
 export { blockFunding } from "./blockFunding.js";
diff --git a/src/template.ts b/src/template.ts
index 26c3e1f..9c7e5e4 100644
--- a/src/template.ts
+++ b/src/template.ts
@@ -1,6 +1,7 @@
 import { base } from "./base.js";
 import { blockAreTheTypesWrong } from "./blocks/blockAreTheTypesWrong.js";
 import { blockCTATransitions } from "./blocks/blockCTATransitions.js";
+import { blockESLintPlugin } from "./blocks/blockESLintPlugin.js";
 import { blockNcc } from "./blocks/blockNcc.js";
 import { blockRemoveDependencies } from "./blocks/blockRemoveDependencies.js";
 import { blockRemoveFiles } from "./blocks/blockRemoveFiles.js";
@@ -19,6 +20,7 @@ export const template = base.createStratumTemplate({
 	blocks: [
 		blockAreTheTypesWrong,
 		blockCTATransitions,
+		blockESLintPlugin,
 		blockNcc,
 		blockRemoveDependencies,
 		blockRemoveFiles,


Test patch:
diff --git a/src/blocks/blockESLint.test.ts b/src/blocks/blockESLint.test.ts
index 56d277d..f7d6948 100644
--- a/src/blocks/blockESLint.test.ts
+++ b/src/blocks/blockESLint.test.ts
@@ -465,6 +465,10 @@ describe("blockESLint", () => {
 				imports: [
 					{ source: "eslint-plugin-markdown", specifier: "a", types: true },
 					{ source: "eslint-plugin-regexp", specifier: "b" },
+					{
+						source: { packageName: "eslint-plugin-unknown", version: "1.2.3" },
+						specifier: "c",
+					},
 				],
 				rules: {
 					"a/b": "error",
@@ -536,6 +540,7 @@ describe("blockESLint", () => {
 			            "eslint": "9.22.0",
 			            "eslint-plugin-markdown": "5.1.0",
 			            "eslint-plugin-regexp": "2.7.0",
+			            "eslint-plugin-unknown": "1.2.3",
 			            "typescript-eslint": "8.26.1",
 			          },
 			          "scripts": {
@@ -586,6 +591,7 @@ describe("blockESLint", () => {
 			import eslint from "@eslint/js";
 			import a from "eslint-plugin-markdown"
 			import b from "eslint-plugin-regexp"
+			import c from "eslint-plugin-unknown"
 			import tseslint from "typescript-eslint";
 
 			export default tseslint.config(
diff --git a/src/blocks/blockESLintPlugin.test.ts b/src/blocks/blockESLintPlugin.test.ts
new file mode 100644
index 0000000..da3bd9f
--- /dev/null
+++ b/src/blocks/blockESLintPlugin.test.ts
@@ -0,0 +1,386 @@
+import { testBlock } from "bingo-stratum-testers";
+import { describe, expect, test } from "vitest";
+
+import { blockESLintPlugin } from "./blockESLintPlugin.js";
+import { optionsBase } from "./options.fakes.js";
+
+describe("blockESLintPlugin", () => {
+	test("without mode", () => {
+		const creation = testBlock(blockESLintPlugin, {
+			options: optionsBase,
+		});
+
+		expect(creation).toMatchInlineSnapshot(`
+			{
+			  "addons": [
+			    {
+			      "addons": {
+			        "words": [
+			          "eslint-doc-generatorrc.js",
+			        ],
+			      },
+			      "block": [Function],
+			    },
+			    {
+			      "addons": {
+			        "sections": {
+			          "Building": {
+			            "innerSections": [
+			              {
+			                "contents": "
+			Run [\`eslint-doc-generator\`](https://github.com/bmish/eslint-doc-generator) to generate Markdown files documenting rules.
+
+			\`\`\`shell
+			pnpm build:docs
+			\`\`\`
+					",
+			                "heading": "Building Docs",
+			              },
+			            ],
+			          },
+			          "Linting": {
+			            "contents": {
+			              "items": [
+			                "- \`pnpm lint:docs\` ([eslint-doc-generator](https://github.com/bmish/eslint-doc-generator)): Generates and validates documentation for ESLint rules",
+			              ],
+			            },
+			          },
+			        },
+			      },
+			      "block": [Function],
+			    },
+			    {
+			      "addons": {
+			        "extensions": [
+			          "eslintPlugin.configs["flat/recommended"]",
+			        ],
+			        "ignores": [
+			          ".eslint-doc-generatorrc.js",
+			          "docs/rules/*/*.ts",
+			        ],
+			        "imports": [
+			          {
+			            "source": {
+			              "packageName": "eslint-plugin-eslint-plugin",
+			              "version": "6.4.0",
+			            },
+			            "specifier": "eslintPlugin",
+			          },
+			        ],
+			      },
+			      "block": [Function],
+			    },
+			    {
+			      "addons": {
+			        "jobs": [
+			          {
+			            "name": "Lint Docs",
+			            "steps": [
+			              {
+			                "run": "pnpm build || exit 0",
+			              },
+			              {
+			                "run": "pnpm lint:docs",
+			              },
+			            ],
+			          },
+			        ],
+			      },
+			      "block": [Function],
+			    },
+			    {
+			      "addons": {
+			        "properties": {
+			          "devDependencies": {
+			            "eslint-doc-generator": "2.1.0",
+			            "eslint-plugin-eslint-plugin": "6.4.0",
+			          },
+			          "peerDependencies": {
+			            "@typescript-eslint/parser": ">=8",
+			            "eslint": ">=9",
+			            "typescript": ">=5",
+			          },
+			          "scripts": {
+			            "build:docs": "eslint-doc-generator",
+			            "lint:docs": "eslint-doc-generator --check",
+			          },
+			        },
+			      },
+			      "block": [Function],
+			    },
+			    {
+			      "addons": {
+			        "coverage": {
+			          "exclude": [
+			            "src/index.ts",
+			            "src/rules/index.ts",
+			          ],
+			        },
+			      },
+			      "block": [Function],
+			    },
+			  ],
+			  "files": {
+			    ".eslint-doc-generatorrc.js": "import prettier from "prettier";
+
+			/** @type {import('eslint-doc-generator').GenerateOptions} */
+			const config = {
+				postprocess: async (content, path) =>
+					prettier.format(content, {
+						...(await prettier.resolveConfig(path)),
+						parser: "markdown",
+					}),
+				ruleDocTitleFormat: "prefix-name",
+			};
+
+			export default config;
+			",
+			  },
+			}
+		`);
+	});
+
+	test("setup mode", () => {
+		const creation = testBlock(blockESLintPlugin, {
+			mode: "setup",
+			options: optionsBase,
+		});
+
+		expect(creation).toMatchInlineSnapshot(`
+			{
+			  "addons": [
+			    {
+			      "addons": {
+			        "words": [
+			          "eslint-doc-generatorrc.js",
+			        ],
+			      },
+			      "block": [Function],
+			    },
+			    {
+			      "addons": {
+			        "sections": {
+			          "Building": {
+			            "innerSections": [
+			              {
+			                "contents": "
+			Run [\`eslint-doc-generator\`](https://github.com/bmish/eslint-doc-generator) to generate Markdown files documenting rules.
+
+			\`\`\`shell
+			pnpm build:docs
+			\`\`\`
+					",
+			                "heading": "Building Docs",
+			              },
+			            ],
+			          },
+			          "Linting": {
+			            "contents": {
+			              "items": [
+			                "- \`pnpm lint:docs\` ([eslint-doc-generator](https://github.com/bmish/eslint-doc-generator)): Generates and validates documentation for ESLint rules",
+			              ],
+			            },
+			          },
+			        },
+			      },
+			      "block": [Function],
+			    },
+			    {
+			      "addons": {
+			        "extensions": [
+			          "eslintPlugin.configs["flat/recommended"]",
+			        ],
+			        "ignores": [
+			          ".eslint-doc-generatorrc.js",
+			          "docs/rules/*/*.ts",
+			        ],
+			        "imports": [
+			          {
+			            "source": {
+			              "packageName": "eslint-plugin-eslint-plugin",
+			              "version": "6.4.0",
+			            },
+			            "specifier": "eslintPlugin",
+			          },
+			        ],
+			      },
+			      "block": [Function],
+			    },
+			    {
+			      "addons": {
+			        "jobs": [
+			          {
+			            "name": "Lint Docs",
+			            "steps": [
+			              {
+			                "run": "pnpm build || exit 0",
+			              },
+			              {
+			                "run": "pnpm lint:docs",
+			              },
+			            ],
+			          },
+			        ],
+			      },
+			      "block": [Function],
+			    },
+			    {
+			      "addons": {
+			        "properties": {
+			          "devDependencies": {
+			            "eslint-doc-generator": "2.1.0",
+			            "eslint-plugin-eslint-plugin": "6.4.0",
+			          },
+			          "peerDependencies": {
+			            "@typescript-eslint/parser": ">=8",
+			            "eslint": ">=9",
+			            "typescript": ">=5",
+			          },
+			          "scripts": {
+			            "build:docs": "eslint-doc-generator",
+			            "lint:docs": "eslint-doc-generator --check",
+			          },
+			        },
+			      },
+			      "block": [Function],
+			    },
+			    {
+			      "addons": {
+			        "coverage": {
+			          "exclude": [
+			            "src/index.ts",
+			            "src/rules/index.ts",
+			          ],
+			        },
+			      },
+			      "block": [Function],
+			    },
+			  ],
+			  "files": {
+			    ".eslint-doc-generatorrc.js": "import prettier from "prettier";
+
+			/** @type {import('eslint-doc-generator').GenerateOptions} */
+			const config = {
+				postprocess: async (content, path) =>
+					prettier.format(content, {
+						...(await prettier.resolveConfig(path)),
+						parser: "markdown",
+					}),
+				ruleDocTitleFormat: "prefix-name",
+			};
+
+			export default config;
+			",
+			    "src": {
+			      "index.ts": "import Module from "node:module";
+
+			import { rules } from "./rules/index.js";
+
+			const require = Module.createRequire(import.meta.url);
+
+			const { name, version } =
+				// \`import\`ing here would bypass the TSConfig's \`"rootDir": "src"\`
+				require("../package.json") as typeof import("../package.json");
+
+			export const plugin = {
+				configs: {
+					get recommended() {
+						return recommended;
+					},
+				},
+				meta: { name, version },
+				rules,
+			};
+
+			const recommended = {
+				plugins: {
+					"test-repository": plugin,
+				},
+				rules: Object.fromEntries(
+					Object.keys(rules).map((rule) => [\`test-repository/\${rule}\`, "error"]),
+				),
+			};
+
+			export { rules };
+
+			export default plugin;
+			",
+			      "rules": {
+			        "example.test.ts": "import { rule } from "./enums.js";
+			import { ruleTester } from "./ruleTester.js";
+
+			ruleTester.run("enums", rule, {
+				invalid: [
+					{
+						code: \`enum Values {}\`,
+						errors: [
+							{
+								column: 1,
+								endColumn: 15,
+								endLine: 1,
+								line: 1,
+								messageId: "enum",
+							},
+						],
+					},
+				],
+				valid: [\`const Values = {};\`, \`const Values = {} as const;\`],
+			});
+			",
+			        "example.ts": "import { createRule } from "../utils.js";
+
+			export const rule = createRule({
+				create(context) {
+					return {
+						TSEnumDeclaration(node) {
+							context.report({
+								messageId: "enum",
+								node,
+							});
+						},
+					};
+				},
+				defaultOptions: [],
+				meta: {
+					docs: {
+						description: "Avoid using TypeScript's enums.",
+					},
+					messages: {
+						enum: "This enum will not be allowed under TypeScript's --erasableSyntaxOnly.",
+					},
+					schema: [],
+					type: "problem",
+				},
+				name: "enums",
+			});
+			",
+			        "index.ts": "import { rule as example } from "./example.js";
+
+			export const rules = {
+				example,
+			};
+			",
+			        "ruleTester.ts": "import { RuleTester } from "@typescript-eslint/rule-tester";
+			import * as vitest from "vitest";
+
+			RuleTester.afterAll = vitest.afterAll;
+			RuleTester.it = vitest.it;
+			RuleTester.itOnly = vitest.it.only;
+			RuleTester.describe = vitest.describe;
+
+			export const ruleTester = new RuleTester();
+			",
+			      },
+			      "utils.ts": "import { ESLintUtils } from "@typescript-eslint/utils";
+
+			export const createRule = ESLintUtils.RuleCreator(
+				(name) =>
+					\`https://github.com/test-owner/test-repository/blob/main/docs/rules/\${name}.md\`,
+			);
+			",
+			    },
+			  },
+			}
+		`);
+	});
+});