diff --git a/src/base.ts b/src/base.ts index d62966484..8cb20e5c1 100644 --- a/src/base.ts +++ b/src/base.ts @@ -289,7 +289,7 @@ export const base = createBase({ ); const getReadmeUsage = lazyValue( - async () => await readReadmeUsage(getEmoji, getReadme, getRepository), + async () => await readReadmeUsage(getReadme), ); const getRepository = lazyValue( diff --git a/src/blocks/blockESLintPlugin.test.ts b/src/blocks/blockESLintPlugin.test.ts index e78964c3d..5cb690f05 100644 --- a/src/blocks/blockESLintPlugin.test.ts +++ b/src/blocks/blockESLintPlugin.test.ts @@ -88,10 +88,41 @@ describe("blockESLintPlugin", () => { }, "block": [Function], }, + { + "addons": { + "defaultUsage": [ + "Add this plugin to the list of plugins in your [ESLint configuration file](https://eslint.org/docs/latest/use/configure/configuration-files): + + \`\`\`shell + npm i test-repository -D + \`\`\` + + \`\`\`ts + import testRepository from "test-repository"; + + export default [ + // (other plugins) + testRepository.configs.recommended, // 👈 + ]; + \`\`\` + + ### Rules + + These are all set to \`"error"\` in the recommended config: + + ", + ], + }, + "block": [Function], + }, { "addons": { "properties": { + "dependencies": { + "@typescript-eslint/utils": "^8.29.0", + }, "devDependencies": { + "@typescript-eslint/rule-tester": "8.29.1", "eslint-doc-generator": "2.1.0", "eslint-plugin-eslint-plugin": "6.4.0", }, @@ -131,6 +162,20 @@ describe("blockESLintPlugin", () => { export default config; ", }, + "scripts": [ + { + "commands": [ + "pnpm build", + ], + "phase": 2, + }, + { + "commands": [ + "pnpm eslint-doc-generator --init-rule-docs", + ], + "phase": 3, + }, + ], } `); }); @@ -221,10 +266,41 @@ describe("blockESLintPlugin", () => { }, "block": [Function], }, + { + "addons": { + "defaultUsage": [ + "Add this plugin to the list of plugins in your [ESLint configuration file](https://eslint.org/docs/latest/use/configure/configuration-files): + + \`\`\`shell + npm i test-repository -D + \`\`\` + + \`\`\`ts + import testRepository from "test-repository"; + + export default [ + // (other plugins) + testRepository.configs.recommended, // 👈 + ]; + \`\`\` + + ### Rules + + These are all set to \`"error"\` in the recommended config: + + ", + ], + }, + "block": [Function], + }, { "addons": { "properties": { + "dependencies": { + "@typescript-eslint/utils": "^8.29.0", + }, "devDependencies": { + "@typescript-eslint/rule-tester": "8.29.1", "eslint-doc-generator": "2.1.0", "eslint-plugin-eslint-plugin": "6.4.0", }, @@ -264,6 +340,20 @@ describe("blockESLintPlugin", () => { export default config; ", }, + "scripts": [ + { + "commands": [ + "pnpm build", + ], + "phase": 2, + }, + { + "commands": [ + "pnpm eslint-doc-generator --init-rule-docs", + ], + "phase": 3, + }, + ], } `); }); @@ -352,10 +442,41 @@ describe("blockESLintPlugin", () => { }, "block": [Function], }, + { + "addons": { + "defaultUsage": [ + "Add this plugin to the list of plugins in your [ESLint configuration file](https://eslint.org/docs/latest/use/configure/configuration-files): + + \`\`\`shell + npm i test-repository -D + \`\`\` + + \`\`\`ts + import testRepository from "test-repository"; + + export default [ + // (other plugins) + testRepository.configs.recommended, // 👈 + ]; + \`\`\` + + ### Rules + + These are all set to \`"error"\` in the recommended config: + + ", + ], + }, + "block": [Function], + }, { "addons": { "properties": { + "dependencies": { + "@typescript-eslint/utils": "^8.29.0", + }, "devDependencies": { + "@typescript-eslint/rule-tester": "8.29.1", "eslint-doc-generator": "2.1.0", "eslint-plugin-eslint-plugin": "6.4.0", }, @@ -429,7 +550,7 @@ describe("blockESLintPlugin", () => { export default plugin; ", "rules": { - "example.test.ts": "import { rule } from "./enums.js"; + "enums.test.ts": "import { rule } from "./enums.js"; import { ruleTester } from "./ruleTester.js"; ruleTester.run("enums", rule, { @@ -450,7 +571,7 @@ describe("blockESLintPlugin", () => { valid: [\`const Values = {};\`, \`const Values = {} as const;\`], }); ", - "example.ts": "import { createRule } from "../utils.js"; + "enums.ts": "import { createRule } from "../utils.js"; export const rule = createRule({ create(context) { @@ -477,10 +598,10 @@ describe("blockESLintPlugin", () => { name: "enums", }); ", - "index.ts": "import { rule as example } from "./example.js"; + "index.ts": "import { rule as enums } from "./enums.js"; export const rules = { - example, + enums, }; ", "ruleTester.ts": "import { RuleTester } from "@typescript-eslint/rule-tester"; @@ -503,6 +624,20 @@ describe("blockESLintPlugin", () => { ", }, }, + "scripts": [ + { + "commands": [ + "pnpm build", + ], + "phase": 2, + }, + { + "commands": [ + "pnpm eslint-doc-generator --init-rule-docs", + ], + "phase": 3, + }, + ], } `); }); @@ -596,10 +731,41 @@ describe("blockESLintPlugin", () => { }, "block": [Function], }, + { + "addons": { + "defaultUsage": [ + "Add this plugin to the list of plugins in your [ESLint configuration file](https://eslint.org/docs/latest/use/configure/configuration-files): + + \`\`\`shell + npm i test-repository -D + \`\`\` + + \`\`\`ts + import testRepository from "test-repository"; + + export default [ + // (other plugins) + testRepository.configs.recommended, // 👈 + ]; + \`\`\` + + ### Rules + + These are all set to \`"error"\` in the recommended config: + + ", + ], + }, + "block": [Function], + }, { "addons": { "properties": { + "dependencies": { + "@typescript-eslint/utils": "^8.29.0", + }, "devDependencies": { + "@typescript-eslint/rule-tester": "8.29.1", "eslint-doc-generator": "2.1.0", "eslint-plugin-eslint-plugin": "6.4.0", }, @@ -640,6 +806,20 @@ describe("blockESLintPlugin", () => { export default config; ", }, + "scripts": [ + { + "commands": [ + "pnpm build", + ], + "phase": 2, + }, + { + "commands": [ + "pnpm eslint-doc-generator --init-rule-docs", + ], + "phase": 3, + }, + ], } `); }); diff --git a/src/blocks/blockESLintPlugin.ts b/src/blocks/blockESLintPlugin.ts index eba752a6d..e41ec71bf 100644 --- a/src/blocks/blockESLintPlugin.ts +++ b/src/blocks/blockESLintPlugin.ts @@ -4,10 +4,12 @@ import { blockDevelopmentDocs } from "./blockDevelopmentDocs.js"; import { blockESLint } from "./blockESLint.js"; import { blockGitHubActionsCI } from "./blockGitHubActionsCI.js"; import { blockPackageJson } from "./blockPackageJson.js"; +import { blockREADME } from "./blockREADME.js"; import { blockVitest } from "./blockVitest.js"; import { blockESLintPluginIntake } from "./eslint/blockESLintPluginIntake.js"; import { zConfigEmoji } from "./eslint/schemas.js"; import { intakeFile } from "./intake/intakeFile.js"; +import { CommandPhase } from "./phases.js"; export const blockESLintPlugin = base.createBlock({ about: { @@ -28,6 +30,9 @@ export const blockESLintPlugin = base.createBlock({ produce({ addons, options }) { const { configEmoji } = addons; const configFileName = `.eslint-doc-generatorrc.${options.type === "commonjs" ? "mjs" : "js"}`; + const pluginName = options.repository + .replace(/^eslint-plugin-/, "") + .replaceAll(/-\w/g, (matched) => matched[1].toUpperCase()); return { addons: [ @@ -83,9 +88,37 @@ pnpm build:docs }, ], }), + blockREADME({ + defaultUsage: [ + `Add this plugin to the list of plugins in your [ESLint configuration file](https://eslint.org/docs/latest/use/configure/configuration-files): + +\`\`\`shell +npm i ${options.repository} -D +\`\`\` + +\`\`\`ts +import ${pluginName} from "${options.repository}"; + +export default [ + // (other plugins) + ${pluginName}.configs.recommended, // 👈 +]; +\`\`\` + +### Rules + +These are all set to \`"error"\` in the recommended config: + +`, + ], + }), blockPackageJson({ properties: { + dependencies: { + "@typescript-eslint/utils": "^8.29.0", + }, devDependencies: { + "@typescript-eslint/rule-tester": "8.29.1", "eslint-doc-generator": "2.1.0", "eslint-plugin-eslint-plugin": "6.4.0", }, @@ -117,6 +150,16 @@ const config = { export default config; `, }, + scripts: [ + { + commands: ["pnpm build"], + phase: CommandPhase.Build, + }, + { + commands: ["pnpm eslint-doc-generator --init-rule-docs"], + phase: CommandPhase.Process, + }, + ], }; }, setup({ options }) { @@ -159,7 +202,7 @@ export { rules }; export default plugin; `, rules: { - "example.test.ts": `import { rule } from "./enums.js"; + "enums.test.ts": `import { rule } from "./enums.js"; import { ruleTester } from "./ruleTester.js"; ruleTester.run("enums", rule, { @@ -180,7 +223,7 @@ ruleTester.run("enums", rule, { valid: [\`const Values = {};\`, \`const Values = {} as const;\`], }); `, - "example.ts": `import { createRule } from "../utils.js"; + "enums.ts": `import { createRule } from "../utils.js"; export const rule = createRule({ create(context) { @@ -207,10 +250,10 @@ export const rule = createRule({ name: "enums", }); `, - "index.ts": `import { rule as example } from "./example.js"; + "index.ts": `import { rule as enums } from "./enums.js"; export const rules = { - example, + enums, }; `, "ruleTester.ts": `import { RuleTester } from "@typescript-eslint/rule-tester"; diff --git a/src/blocks/blockExampleFiles.test.ts b/src/blocks/blockExampleFiles.test.ts index 089d6d0d4..983c9ebbf 100644 --- a/src/blocks/blockExampleFiles.test.ts +++ b/src/blocks/blockExampleFiles.test.ts @@ -40,6 +40,14 @@ describe("blockExampleFiles", () => { expect(creation).toMatchInlineSnapshot(` { + "addons": [ + { + "addons": { + "defaultUsage": undefined, + }, + "block": [Function], + }, + ], "files": { "src": { "index.ts": "console.log('Hello, world!');", diff --git a/src/blocks/blockExampleFiles.ts b/src/blocks/blockExampleFiles.ts index f04a9aa67..4c4ccf814 100644 --- a/src/blocks/blockExampleFiles.ts +++ b/src/blocks/blockExampleFiles.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { base } from "../base.js"; +import { blockREADME } from "./blockREADME.js"; export const blockExampleFiles = base.createBlock({ about: { @@ -8,9 +9,17 @@ export const blockExampleFiles = base.createBlock({ }, addons: { files: z.record(z.string()).default({}), + usage: z.array(z.string()).default([]), }, setup({ addons }) { + const { usage } = addons; + return { + addons: [ + blockREADME({ + defaultUsage: usage, + }), + ], files: { src: addons.files, }, diff --git a/src/blocks/blockREADME.ts b/src/blocks/blockREADME.ts index a6c969283..bfa7e4891 100644 --- a/src/blocks/blockREADME.ts +++ b/src/blocks/blockREADME.ts @@ -29,11 +29,12 @@ export const blockREADME = base.createBlock({ }, addons: { badges: z.array(zBadge).default([]), + defaultUsage: z.array(z.string()).default([]), notices: z.array(z.string()).default([]), sections: z.array(z.string()).default([]), }, produce({ addons, options }) { - const { badges, notices, sections } = addons; + const { badges, defaultUsage, notices, sections } = addons; const explainer = options.documentation.readme.explainer && @@ -60,7 +61,7 @@ ${formatBadges(badges)} ${[logo, explainer].filter(Boolean).join("")} ## Usage -${options.documentation.readme.usage} +${options.documentation.readme.usage ?? defaultUsage.join("\n\n")} ## Development diff --git a/src/blocks/blockTypeScript.test.ts b/src/blocks/blockTypeScript.test.ts index 813b808da..efe229192 100644 --- a/src/blocks/blockTypeScript.test.ts +++ b/src/blocks/blockTypeScript.test.ts @@ -64,6 +64,16 @@ describe("blockTypeScript", () => { } ", }, + "usage": [ + "\`\`\`shell + npm i test-repository + \`\`\` + \`\`\`ts + import { greet } from "test-repository"; + + greet("Hello, world! 💖"); + \`\`\`", + ], }, "block": [Function], }, @@ -224,6 +234,16 @@ describe("blockTypeScript", () => { } ", }, + "usage": [ + "\`\`\`shell + npm i test-repository + \`\`\` + \`\`\`ts + import { greet } from "test-repository"; + + greet("Hello, world! 💖"); + \`\`\`", + ], }, "block": [Function], }, @@ -382,6 +402,16 @@ describe("blockTypeScript", () => { } ", }, + "usage": [ + "\`\`\`shell + npm i test-repository + \`\`\` + \`\`\`ts + import { greet } from "test-repository"; + + greet("Hello, world! 💖"); + \`\`\`", + ], }, "block": [Function], }, @@ -549,6 +579,16 @@ describe("blockTypeScript", () => { } ", }, + "usage": [ + "\`\`\`shell + npm i test-repository + \`\`\` + \`\`\`ts + import { greet } from "test-repository"; + + greet("Hello, world! 💖"); + \`\`\`", + ], }, "block": [Function], }, diff --git a/src/blocks/blockTypeScript.ts b/src/blocks/blockTypeScript.ts index b67aa2c40..647598372 100644 --- a/src/blocks/blockTypeScript.ts +++ b/src/blocks/blockTypeScript.ts @@ -87,6 +87,16 @@ export * from "./types.js"; } `, }, + usage: [ + `\`\`\`shell +npm i ${options.repository} +\`\`\` +\`\`\`ts +import { greet } from "${options.repository}"; + +greet("Hello, world! ${options.emoji}"); +\`\`\``, + ], }), blockGitignore({ ignores: ["/lib"], diff --git a/src/options/readDocumentation.ts b/src/options/readDocumentation.ts index d828f348e..5c6984ca9 100644 --- a/src/options/readDocumentation.ts +++ b/src/options/readDocumentation.ts @@ -5,7 +5,7 @@ export async function readDocumentation( getReadmeAdditional: () => Promise, getReadmeExplainer: () => Promise, getReadmeFootnotes: () => Promise, - getReadmeUsage: () => Promise, + getReadmeUsage: () => Promise, ): Promise { const [additional, explainer, footnotes, development, usage] = await Promise.all([ diff --git a/src/options/readReadmeUsage.test.ts b/src/options/readReadmeUsage.test.ts index 086eb338a..cf0f640ca 100644 --- a/src/options/readReadmeUsage.test.ts +++ b/src/options/readReadmeUsage.test.ts @@ -1,46 +1,51 @@ -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, it } from "vitest"; import { readReadmeUsage } from "./readReadmeUsage.js"; -const mockReadUsageFromReadme = vi.fn(); +describe(readReadmeUsage, () => { + it("returns undefined when ## Usage is not found", async () => { + const actual = await readReadmeUsage(() => Promise.resolve("## Other")); -vi.mock("./readUsageFromReadme.js", () => ({ - get readUsageFromReadme() { - return mockReadUsageFromReadme; - }, -})); + expect(actual).toBeUndefined(); + }); -describe(readReadmeUsage, () => { - it("returns the existing usage when readUsageFromReadme provides one", async () => { - const existing = "Use it."; + it("returns existing content when ## Usage is found and a next important heading is not found", async () => { + const actual = await readReadmeUsage(() => + Promise.resolve("## Usage\n\nContents."), + ); - mockReadUsageFromReadme.mockReturnValueOnce(existing); + expect(actual).toBe(`\n\nContents.`); + }); - const usage = await readReadmeUsage( - () => Promise.resolve("💖"), - () => Promise.resolve(""), - () => Promise.resolve(undefined), + it("returns undefined when there is no content between ## Usage and ## Development", async () => { + const actual = await readReadmeUsage(() => + Promise.resolve("## Usage\n\n \n## Development"), ); - expect(usage).toBe(existing); + expect(actual).toBeUndefined(); }); - it("returns sample usage when readUsageFromReadme doesn't provide usage", async () => { - mockReadUsageFromReadme.mockReturnValueOnce(undefined); + it("returns the content when content exists between ## Usage and ## Development", async () => { + const actual = await readReadmeUsage(() => + Promise.resolve("## Usage\n\n Content.\n## Development"), + ); - const usage = await readReadmeUsage( - () => Promise.resolve("💖"), - () => Promise.resolve(""), - () => Promise.resolve("test-repository"), + expect(actual).toBe("Content."); + }); + + it("returns the content when content exists between ## Usage and ## Contributing", async () => { + const actual = await readReadmeUsage(() => + Promise.resolve("## Usage\n\n Content.\n## Contributing"), ); - expect(usage).toBe(`\`\`\`shell -npm i test-repository -\`\`\` -\`\`\`ts -import { greet } from "test-repository"; + expect(actual).toBe("Content."); + }); + + it("returns the content when content exists between ## Usage and ## Contributors", async () => { + const actual = await readReadmeUsage(() => + Promise.resolve("## Usage\n\n Content.\n## Contributors"), + ); -greet("Hello, world! 💖"); -\`\`\``); + expect(actual).toBe("Content."); }); }); diff --git a/src/options/readReadmeUsage.ts b/src/options/readReadmeUsage.ts index 3efbc8427..97cf6c377 100644 --- a/src/options/readReadmeUsage.ts +++ b/src/options/readReadmeUsage.ts @@ -1,19 +1,22 @@ -import { readUsageFromReadme } from "./readUsageFromReadme.js"; +const startUsage = "## Usage"; -export async function readReadmeUsage( - getEmoji: () => Promise, - getReadme: () => Promise, - getRepository: () => Promise, -) { - return ( - readUsageFromReadme(await getReadme()) ?? - `\`\`\`shell -npm i ${await getRepository()} -\`\`\` -\`\`\`ts -import { greet } from "${await getRepository()}"; +export async function readReadmeUsage(getReadme: () => Promise) { + const readme = await getReadme(); -greet("Hello, world! ${await getEmoji()}"); -\`\`\`` - ); + const indexOfUsage = readme.indexOf(startUsage); + if (indexOfUsage === -1) { + return undefined; + } + + const offset = indexOfUsage + startUsage.length; + const indexOfNextKnownHeading = readme + .slice(offset) + .search(/## (?:Development|Contributing|Contributors)/); + if (indexOfNextKnownHeading === -1) { + return readme.slice(offset) || undefined; + } + + const usage = readme.slice(offset, indexOfNextKnownHeading + offset).trim(); + + return usage || undefined; } diff --git a/src/options/readUsageFromReadme.test.ts b/src/options/readUsageFromReadme.test.ts deleted file mode 100644 index 2775768d5..000000000 --- a/src/options/readUsageFromReadme.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { readUsageFromReadme } from "./readUsageFromReadme.js"; - -describe(readUsageFromReadme, () => { - it("returns undefined when ## Usage is not found", () => { - const usage = readUsageFromReadme("## Other"); - - expect(usage).toBeUndefined(); - }); - - it("returns existing content when ## Usage is found and a next important heading is not found", () => { - const usage = readUsageFromReadme("## Usage\n\nContents."); - - expect(usage).toBe(`\n\nContents.`); - }); - - it("returns undefined when there is no content between ## Usage and ## Development", () => { - const usage = readUsageFromReadme("## Usage\n\n \n## Development"); - - expect(usage).toBeUndefined(); - }); - - it("returns the content when content exists between ## Usage and ## Development", () => { - const usage = readUsageFromReadme("## Usage\n\n Content.\n## Development"); - - expect(usage).toBe("Content."); - }); - - it("returns the content when content exists between ## Usage and ## Contributing", () => { - const usage = readUsageFromReadme( - "## Usage\n\n Content.\n## Contributing", - ); - - expect(usage).toBe("Content."); - }); - - it("returns the content when content exists between ## Usage and ## Contributors", () => { - const usage = readUsageFromReadme( - "## Usage\n\n Content.\n## Contributors", - ); - - expect(usage).toBe("Content."); - }); -}); diff --git a/src/options/readUsageFromReadme.ts b/src/options/readUsageFromReadme.ts deleted file mode 100644 index d3bcddff1..000000000 --- a/src/options/readUsageFromReadme.ts +++ /dev/null @@ -1,20 +0,0 @@ -const startUsage = "## Usage"; - -export function readUsageFromReadme(readme: string) { - const indexOfUsage = readme.indexOf(startUsage); - if (indexOfUsage === -1) { - return undefined; - } - - const offset = indexOfUsage + startUsage.length; - const indexOfNextKnownHeading = readme - .slice(offset) - .search(/## (?:Development|Contributing|Contributors)/); - if (indexOfNextKnownHeading === -1) { - return readme.slice(offset); - } - - const usage = readme.slice(offset, indexOfNextKnownHeading + offset).trim(); - - return usage ? usage : undefined; -} diff --git a/src/schemas.ts b/src/schemas.ts index 841502169..3b1686192 100644 --- a/src/schemas.ts +++ b/src/schemas.ts @@ -14,7 +14,7 @@ export const zReadme = z.object({ additional: z.string().optional(), explainer: z.string().optional(), footnotes: z.string().optional(), - usage: z.string(), + usage: z.string().optional(), }); export type Readme = z.infer;