diff --git a/script/__snapshots__/migrate-test-e2e.js.snap b/script/__snapshots__/migrate-test-e2e.js.snap index a17b84baf..be8e746e3 100644 --- a/script/__snapshots__/migrate-test-e2e.js.snap +++ b/script/__snapshots__/migrate-test-e2e.js.snap @@ -174,8 +174,10 @@ exports[`expected file changes > cspell.json 1`] = ` "mtfoley", "npmignore", @@ ... @@ + "packagejson", "quickstart", "tada", ++ "templating", "tsup", - "vitest" + "vitest", diff --git a/src/steps/writing/creation/dotGitHub/createDevelopment.test.ts b/src/steps/writing/creation/dotGitHub/createDevelopment/index.test.ts similarity index 62% rename from src/steps/writing/creation/dotGitHub/createDevelopment.test.ts rename to src/steps/writing/creation/dotGitHub/createDevelopment/index.test.ts index fbda14f80..202fef504 100644 --- a/src/steps/writing/creation/dotGitHub/createDevelopment.test.ts +++ b/src/steps/writing/creation/dotGitHub/createDevelopment/index.test.ts @@ -1,7 +1,15 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; -import { Options } from "../../../../shared/types.js"; -import { createDevelopment } from "./createDevelopment.js"; +import { Options } from "../../../../../shared/types.js"; +import { createDevelopment } from "./index.js"; + +const mockReadFileSafe = vi.fn(); + +vi.mock("../../../../../shared/readFileSafe.js", () => ({ + get readFileSafe() { + return mockReadFileSafe; + }, +})); const options = { access: "public", @@ -21,8 +29,10 @@ const options = { } satisfies Options; describe("createDevelopment", () => { - it("creates a file with extra options turned on when options disable them", () => { - const actual = createDevelopment({ + it("creates a file with extra options turned on when options disable them", async () => { + mockReadFileSafe.mockResolvedValue(""); + + const actual = await createDevelopment({ ...options, excludeLintKnip: false, excludeLintMd: false, @@ -77,7 +87,7 @@ describe("createDevelopment", () => { - \`pnpm lint\` ([ESLint](https://eslint.org) with [typescript-eslint](https://typescript-eslint.io)): Lints JavaScript and TypeScript source files - \`pnpm lint:knip\` ([knip](https://github.com/webpro/knip)): Detects unused files, dependencies, and code exports - - \`pnpm lint:md\` ([Markdownlint](https://github.com/DavidAnson/markdownlint)): Checks Markdown source files + - \`pnpm lint:md\` ([Markdownlint](https://github.com/DavidAnson/markdownlint): Checks Markdown source files - \`pnpm lint:package-json\` ([npm-package-json-lint](https://npmpackagejsonlint.org/)): Lints the \`package.json\` file - \`pnpm lint:packages\` ([pnpm dedupe --check](https://pnpm.io/cli/dedupe)): Checks for unnecessarily duplicated packages in the \`pnpm-lock.yml\` file - \`pnpm lint:spelling\` ([cspell](https://cspell.org)): Spell checks across all source files @@ -134,8 +144,10 @@ describe("createDevelopment", () => { `); }); - it("creates a file with extra options turned off when options enable them", () => { - const actual = createDevelopment({ + it("creates a file with extra options turned off when options enable them", async () => { + mockReadFileSafe.mockResolvedValue(""); + + const actual = await createDevelopment({ ...options, excludeLintKnip: true, excludeLintMd: true, @@ -192,7 +204,7 @@ describe("createDevelopment", () => { ## Linting - [ESLint](https://eslint.org) is used with with [typescript-eslint](https://typescript-eslint.io) to lint JavaScript and TypeScript source files. + [ESLint](https://eslint.org) is used with with [typescript-eslint](https://typescript-eslint.io)) to lint JavaScript and TypeScript source files. You can run it locally on the command-line: \`\`\`shell @@ -248,4 +260,136 @@ describe("createDevelopment", () => { " `); }); + + it("preserves existing sections when they don't match the new sections", async () => { + mockReadFileSafe.mockResolvedValue(`## Existing One + +Abc 123. + +## Building + +Will be removed. + +## Tests + +Will be removed. + +## Existing Two + +Def 456. +`); + + const actual = await createDevelopment({ + ...options, + excludeLintKnip: false, + excludeLintMd: false, + excludeLintPackageJson: false, + excludeLintPackages: false, + excludeLintSpelling: false, + }); + + expect(actual).toMatchInlineSnapshot(` + "# Development + + After [forking the repo from GitHub](https://help.github.com/articles/fork-a-repo) and [installing pnpm](https://pnpm.io/installation): + + \`\`\`shell + git clone https://github.com//test-repository + cd test-repository + pnpm install + \`\`\` + + > This repository includes a list of suggested VS Code extensions. + > It's a good idea to use [VS Code](https://code.visualstudio.com) and accept its suggestion to install them, as they'll help with development. + + ## Building + + Will be removed. + + ## Formatting + + [Prettier](https://prettier.io) is used to format code. + It should be applied automatically when you save files in VS Code or make a Git commit. + + To manually reformat all files, you can run: + + \`\`\`shell + pnpm format --write + \`\`\` + + ## Linting + + 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: + + - \`pnpm lint\` ([ESLint](https://eslint.org) with [typescript-eslint](https://typescript-eslint.io)): Lints JavaScript and TypeScript source files + - \`pnpm lint:knip\` ([knip](https://github.com/webpro/knip)): Detects unused files, dependencies, and code exports + - \`pnpm lint:md\` ([Markdownlint](https://github.com/DavidAnson/markdownlint): Checks Markdown source files + - \`pnpm lint:package-json\` ([npm-package-json-lint](https://npmpackagejsonlint.org/)): Lints the \`package.json\` file + - \`pnpm lint:packages\` ([pnpm dedupe --check](https://pnpm.io/cli/dedupe)): Checks for unnecessarily duplicated packages in the \`pnpm-lock.yml\` file + - \`pnpm lint:spelling\` ([cspell](https://cspell.org)): Spell checks across all source files + + Read the individual documentation for each linter to understand how it can be configured and used best. + + For example, ESLint can be run with \`--fix\` to auto-fix some lint rule complaints: + + \`\`\`shell + pnpm run lint --fix + \`\`\` + + Note that you'll likely need to run \`pnpm build\` before \`pnpm lint\` so that lint rules which check the file system can pick up on any built files. + + ## Testing + + [Vitest](https://vitest.dev) is used for tests. + You can run it locally on the command-line: + + \`\`\`shell + pnpm run test + \`\`\` + + Add the \`--coverage\` flag to compute test coverage and place reports in the \`coverage/\` directory: + + \`\`\`shell + pnpm run test --coverage + \`\`\` + + Note that [console-fail-test](https://github.com/JoshuaKGoldberg/console-fail-test) is enabled for all test runs. + Calls to \`console.log\`, \`console.warn\`, and other console methods will cause a test to fail. + + ### Debugging Tests + + This repository includes a [VS Code launch configuration](https://code.visualstudio.com/docs/editor/debugging) for debugging unit tests. + To launch it, open a test file, then run _Debug Current Test File_ from the VS Code Debug panel (or press F5). + + ## Type Checking + + You should be able to see suggestions from [TypeScript](https://typescriptlang.org) in your editor for all open files. + + However, it can be useful to run the TypeScript command-line (\`tsc\`) to type check all files in \`src/\`: + + \`\`\`shell + pnpm tsc + \`\`\` + + Add \`--watch\` to keep the type checker running in a watch mode that updates the display as you save files: + + \`\`\`shell + pnpm tsc --watch + \`\`\` + + ## Existing One + + Abc 123. + + ## Tests + + Will be removed. + + ## Existing Two + + Def 456. + " + `); + }); }); diff --git a/src/steps/writing/creation/dotGitHub/createDevelopment.ts b/src/steps/writing/creation/dotGitHub/createDevelopment/index.ts similarity index 62% rename from src/steps/writing/creation/dotGitHub/createDevelopment.ts rename to src/steps/writing/creation/dotGitHub/createDevelopment/index.ts index 0854dcfd4..9e3fe9512 100644 --- a/src/steps/writing/creation/dotGitHub/createDevelopment.ts +++ b/src/steps/writing/creation/dotGitHub/createDevelopment/index.ts @@ -1,11 +1,22 @@ -import { Options } from "../../../../shared/types.js"; - -export function createDevelopment(options: Options) { +import { readFileSafe } from "../../../../../shared/readFileSafe.js"; +import { Options } from "../../../../../shared/types.js"; +import { splitIntoSections } from "./splitIntoSections.js"; + +const headingAliases = new Map([ + ["build", "Building"], + ["builds", "Building"], + ["format", "Formatting"], + ["lint", "Linting"], + ["test", "Testing"], + ["tests", "Testing"], +]); + +function createLintingSection(options: Options) { const lintLines = [ !options.excludeLintKnip && `- \`pnpm lint:knip\` ([knip](https://github.com/webpro/knip)): Detects unused files, dependencies, and code exports`, !options.excludeLintMd && - `- \`pnpm lint:md\` ([Markdownlint](https://github.com/DavidAnson/markdownlint)): Checks Markdown source files`, + `- \`pnpm lint:md\` ([Markdownlint](https://github.com/DavidAnson/markdownlint): Checks Markdown source files`, !options.excludeLintPackageJson && `- \`pnpm lint:package-json\` ([npm-package-json-lint](https://npmpackagejsonlint.org/)): Lints the \`package.json\` file`, !options.excludeLintPackages && @@ -14,29 +25,33 @@ export function createDevelopment(options: Options) { `- \`pnpm lint:spelling\` ([cspell](https://cspell.org)): Spell checks across all source files`, ].filter(Boolean); - return `# Development -${ - options.guide - ? ` -> If you'd like a more guided walkthrough, see [${options.guide.title}](${options.guide.href}). -> It'll walk you through the common activities you'll need to contribute. -` - : "" -} -After [forking the repo from GitHub](https://help.github.com/articles/fork-a-repo) and [installing pnpm](https://pnpm.io/installation): + return lintLines.length + ? [ + `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:`, + ``, + `- \`pnpm lint\` ([ESLint](https://eslint.org) with [typescript-eslint](https://typescript-eslint.io)): Lints JavaScript and TypeScript source files`, + ...lintLines, + ``, + `Read the individual documentation for each linter to understand how it can be configured and used best.`, + ``, + `For example, ESLint can be run with \`--fix\` to auto-fix some lint rule complaints:`, + ].join("\n") + : `[ESLint](https://eslint.org) is used with with [typescript-eslint](https://typescript-eslint.io)) to lint JavaScript and TypeScript source files. +You can run it locally on the command-line: \`\`\`shell -git clone https://github.com//${options.repository} -cd ${options.repository} -pnpm install +pnpm run lint \`\`\` -> This repository includes a list of suggested VS Code extensions. -> It's a good idea to use [VS Code](https://code.visualstudio.com) and accept its suggestion to install them, as they'll help with development. +ESLint can be run with \`--fix\` to auto-fix some lint rule complaints:`; +} -## Building +export async function createDevelopment(options: Options) { + const existingContents = await readFileSafe(".github/DEVELOPMENT.md", ""); -Run [**tsup**](https://tsup.egoist.dev) locally to build source files from \`src/\` into output files in \`lib/\`: + const newSections = { + "## Building": `Run [**tsup**](https://tsup.egoist.dev) locally to build source files from \`src/\` into output files in \`lib/\`: \`\`\`shell pnpm build @@ -46,55 +61,24 @@ Add \`--watch\` to run the builder in a watch mode that continuously cleans and \`\`\`shell pnpm build --watch -\`\`\` - -## Formatting - -[Prettier](https://prettier.io) is used to format code. +\`\`\``, + "## Formatting": `[Prettier](https://prettier.io) is used to format code. It should be applied automatically when you save files in VS Code or make a Git commit. To manually reformat all files, you can run: \`\`\`shell pnpm format --write -\`\`\` - -## Linting - -${ - lintLines.length - ? [ - `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:`, - ``, - `- \`pnpm lint\` ([ESLint](https://eslint.org) with [typescript-eslint](https://typescript-eslint.io)): Lints JavaScript and TypeScript source files`, - ...lintLines, - ``, - `Read the individual documentation for each linter to understand how it can be configured and used best.`, - ``, - `For example, ESLint can be run with \`--fix\` to auto-fix some lint rule complaints:`, - ].join("\n") - : `[ESLint](https://eslint.org) is used with with [typescript-eslint](https://typescript-eslint.io) to lint JavaScript and TypeScript source files. -You can run it locally on the command-line: - -\`\`\`shell -pnpm run lint -\`\`\` - -ESLint can be run with \`--fix\` to auto-fix some lint rule complaints:` -} +\`\`\``, + "## Linting": `${createLintingSection(options)} \`\`\`shell pnpm run lint --fix \`\`\` -Note that you'll likely need to run \`pnpm build\` before \`pnpm lint\` so that lint rules which check the file system can pick up on any built files. - -${ - !options.excludeTests && - `## Testing - -[Vitest](https://vitest.dev) is used for tests. +Note that you'll likely need to run \`pnpm build\` before \`pnpm lint\` so that lint rules which check the file system can pick up on any built files.`, + ...(!options.excludeTests && { + "## Testing": `[Vitest](https://vitest.dev) is used for tests. You can run it locally on the command-line: \`\`\`shell @@ -108,17 +92,12 @@ pnpm run test --coverage \`\`\` Note that [console-fail-test](https://github.com/JoshuaKGoldberg/console-fail-test) is enabled for all test runs. -Calls to \`console.log\`, \`console.warn\`, and other console methods will cause a test to fail. - -### Debugging Tests - -This repository includes a [VS Code launch configuration](https://code.visualstudio.com/docs/editor/debugging) for debugging unit tests. -To launch it, open a test file, then run _Debug Current Test File_ from the VS Code Debug panel (or press F5). -` -} -## Type Checking +Calls to \`console.log\`, \`console.warn\`, and other console methods will cause a test to fail.`, -You should be able to see suggestions from [TypeScript](https://typescriptlang.org) in your editor for all open files. + "### Debugging Tests": `This repository includes a [VS Code launch configuration](https://code.visualstudio.com/docs/editor/debugging) for debugging unit tests. +To launch it, open a test file, then run _Debug Current Test File_ from the VS Code Debug panel (or press F5).`, + }), + "## Type Checking": `You should be able to see suggestions from [TypeScript](https://typescriptlang.org) in your editor for all open files. However, it can be useful to run the TypeScript command-line (\`tsc\`) to type check all files in \`src/\`: @@ -130,6 +109,51 @@ Add \`--watch\` to keep the type checker running in a watch mode that updates th \`\`\`shell pnpm tsc --watch +\`\`\``, + }; + + const newSectionHeadings = new Set([ + "Development", + Object.keys(newSections).map((key) => key.replace(/^#* /, "")), + ]); + + const preservedSections = Object.fromEntries( + splitIntoSections(existingContents).filter(([key]) => { + const keyText = key.replace(/^#* /, ""); + return !newSectionHeadings.has( + headingAliases.get(keyText.toLowerCase()) ?? keyText, + ); + }), + ); + + const result = `# Development +${ + options.guide + ? ` +> If you'd like a more guided walkthrough, see [${options.guide.title}](${options.guide.href}). +> It'll walk you through the common activities you'll need to contribute. +` + : "" +} +After [forking the repo from GitHub](https://help.github.com/articles/fork-a-repo) and [installing pnpm](https://pnpm.io/installation): + +\`\`\`shell +git clone https://github.com//${options.repository} +cd ${options.repository} +pnpm install \`\`\` + +> This repository includes a list of suggested VS Code extensions. +> It's a good idea to use [VS Code](https://code.visualstudio.com) and accept its suggestion to install them, as they'll help with development. + +${Object.entries({ ...newSections, ...preservedSections }) + .map( + ([heading, content]) => `${heading} + +${content}`, + ) + .join("\n\n")} `; + + return result; } diff --git a/src/steps/writing/creation/dotGitHub/createDevelopment/splitIntoSections.test.ts b/src/steps/writing/creation/dotGitHub/createDevelopment/splitIntoSections.test.ts new file mode 100644 index 000000000..bd00ebc2c --- /dev/null +++ b/src/steps/writing/creation/dotGitHub/createDevelopment/splitIntoSections.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, test } from "vitest"; + +import { splitIntoSections } from "./splitIntoSections.js"; + +describe("createDevelopment", () => { + test.each([ + ["", []], + ["# Development \nabc 123", [["# Development", "abc 123"]]], + ["# Development \n\nabc 123 ", [["# Development", "abc 123"]]], + ["# Development \n Indented. ", [["# Development", " Indented."]]], + ["# Development \n Indented. ", [["# Development", " Indented."]]], + ["# Development \n\tIndented. ", [["# Development", "\tIndented."]]], + [ + `# Development + +. + +## Abc + +abc 123 + +### Def + +def 456 + +## Ghi + +ghi 789 +`, + [ + ["# Development", "."], + ["## Abc", "abc 123"], + ["### Def", "def 456"], + ["## Ghi", "ghi 789"], + ], + ], + ])("%j", (text, expected) => { + expect(splitIntoSections(text)).toEqual(expected); + }); +}); diff --git a/src/steps/writing/creation/dotGitHub/createDevelopment/splitIntoSections.ts b/src/steps/writing/creation/dotGitHub/createDevelopment/splitIntoSections.ts new file mode 100644 index 000000000..6ac430cf2 --- /dev/null +++ b/src/steps/writing/creation/dotGitHub/createDevelopment/splitIntoSections.ts @@ -0,0 +1,27 @@ +export function splitIntoSections(text: string) { + const sections: [string, string][] = []; + if (!text) { + return sections; + } + + let remaining = `${text}\n`; + + while (remaining) { + const indexOfNewline = remaining.indexOf("\n", 1); + let nextStart = remaining.indexOf("\n#", 1); + if (nextStart === -1) { + nextStart = remaining.length; + } + + const heading = remaining.slice(0, indexOfNewline).trim(); + const contents = remaining + .slice(indexOfNewline, nextStart) + .trimEnd() + .replace(/^\n+/, ""); + + sections.push([heading, contents]); + remaining = remaining.slice(nextStart); + } + + return sections; +} diff --git a/src/steps/writing/creation/dotGitHub/createDotGitHubFiles.ts b/src/steps/writing/creation/dotGitHub/createDotGitHubFiles.ts index 5ddc42dcc..1e2788fac 100644 --- a/src/steps/writing/creation/dotGitHub/createDotGitHubFiles.ts +++ b/src/steps/writing/creation/dotGitHub/createDotGitHubFiles.ts @@ -2,7 +2,7 @@ import { Options } from "../../../../shared/types.js"; import { formatJson } from "../formatters/formatJson.js"; import { formatYaml } from "../formatters/formatYaml.js"; -import { createDevelopment } from "./createDevelopment.js"; +import { createDevelopment } from "./createDevelopment/index.js"; export async function createDotGitHubFiles(options: Options) { return { @@ -237,7 +237,7 @@ If you made it all the way to the end, bravo dear user, we love you. Please include your favorite emoji in the bottom of your issues and PRs to signal to us that you did in fact read this file and are trying to conform to it as best as possible. 💖 is a good starter if you're not sure which to use. `, - "DEVELOPMENT.md": createDevelopment(options), + "DEVELOPMENT.md": await createDevelopment(options), ...(options.funding && { "FUNDING.yml": formatYaml({ github: options.funding }), }),