diff --git a/script/initialize-test-e2e.js b/script/initialize-test-e2e.js index 6acefd4a6..756207e01 100644 --- a/script/initialize-test-e2e.js +++ b/script/initialize-test-e2e.js @@ -30,7 +30,7 @@ for (const search of [`/JoshuaKGoldberg/`, "create-typescript-app"]) { const { stdout } = await $`grep -i ${search} ${files}`; assert.equal( stdout, - `README.md:> 💙 This package was templated with [create-typescript-app](https://github.com/JoshuaKGoldberg/create-typescript-app).`, + `README.md:> 💙 This package was templated with [\`create-typescript-app\`](https://github.com/JoshuaKGoldberg/create-typescript-app).`, ); } diff --git a/src/initialize/initializeWithOptions.ts b/src/initialize/initializeWithOptions.ts index 83ff2b017..960c2c329 100644 --- a/src/initialize/initializeWithOptions.ts +++ b/src/initialize/initializeWithOptions.ts @@ -22,7 +22,12 @@ export async function initializeWithOptions({ await updateLocalFiles(options); }, ], - ["Updating README.md", updateReadme], + [ + "Updating README.md", + async () => { + await updateReadme(options); + }, + ], ["Clearing changelog", clearChangelog], [ "Updating all-contributors table", diff --git a/src/steps/createJoshuaKGoldbergReplacement.test.ts b/src/steps/createJoshuaKGoldbergReplacement.test.ts new file mode 100644 index 000000000..fe16449cf --- /dev/null +++ b/src/steps/createJoshuaKGoldbergReplacement.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, test } from "vitest"; + +import { createJoshuaKGoldbergReplacement } from "./createJoshuaKGoldbergReplacement.js"; + +const options = { + owner: "NewOwner", + repository: "new-repository", +}; + +describe("createJoshuaKGoldbergReplacement", () => { + test.each([ + [`JoshuaKGoldberg`, options.owner], + [ + `JoshuaKGoldberg/${options.repository}`, + `${options.owner}/${options.repository}`, + ], + [`JoshuaKGoldberg/other-repository`, `JoshuaKGoldberg/other-repository`], + ])("%s", (before, expected) => { + const [matcher, replacer] = createJoshuaKGoldbergReplacement(options); + + const actual = replacer(before, matcher.exec(before)?.[1]); + + expect(actual).toBe(expected); + }); +}); diff --git a/src/steps/createJoshuaKGoldbergReplacement.ts b/src/steps/createJoshuaKGoldbergReplacement.ts new file mode 100644 index 000000000..056078e83 --- /dev/null +++ b/src/steps/createJoshuaKGoldbergReplacement.ts @@ -0,0 +1,22 @@ +import { Options } from "../shared/types.js"; + +/** + * Creates a replace-in-file replacement for JoshuaKGoldberg/... matches, + * keeping repository names not being migrated (e.g. for GitHub actions). + */ +export const createJoshuaKGoldbergReplacement = ( + options: Pick, +) => + [ + /JoshuaKGoldberg(?:\/(.+))?/g, + (full: string, capture: string | undefined) => + capture + ? // If this was a "JoshuaKGoldberg/..." repository link, + // swap the owner if it's the repository being migrated. + capture.startsWith(options.repository) + ? `${options.owner}/${capture}` + : full + : // Otherwise it's just "JoshuaKGoldberg" standalone, + // so swap to the new owner. + options.owner, + ] as const; diff --git a/src/steps/updateLocalFiles.test.ts b/src/steps/updateLocalFiles.test.ts index d6955a8dc..5a4bbf568 100644 --- a/src/steps/updateLocalFiles.test.ts +++ b/src/steps/updateLocalFiles.test.ts @@ -75,7 +75,15 @@ describe("updateLocalFiles", () => { "./.github/**/*", "./*.*", ], - "from": /JoshuaKGoldberg\\(\\?!\\\\/console-fail-test\\)/g, + "from": /JoshuaKGoldberg\\(\\?:\\\\/\\(\\.\\+\\)\\)\\?/g, + "to": [Function], + }, + ], + [ + { + "allowEmptyPaths": true, + "files": "package.json", + "from": /JoshuaKGoldberg/g, "to": "StubOwner", }, ], @@ -200,8 +208,8 @@ describe("updateLocalFiles", () => { { "allowEmptyPaths": true, "files": "./README.md", - "from": "> 💙 This package is based on [@StubOwner](https://github.com/StubOwner)'s [stub-repository](https://github.com/JoshuaKGoldberg/stub-repository).", - "to": "> 💙 This package is based on [@JoshuaKGoldberg](https://github.com/JoshuaKGoldberg)'s [create-typescript-app](https://github.com/JoshuaKGoldberg/create-typescript-app).", + "from": /> 💙 This package was templated with \\.\\+\\\\\\./g, + "to": "> 💙 This package was templated with [\`create-typescript-app\`](https://github.com/JoshuaKGoldberg/create-typescript-app).", }, ], ] @@ -234,7 +242,15 @@ describe("updateLocalFiles", () => { "./.github/**/*", "./*.*", ], - "from": /JoshuaKGoldberg\\(\\?!\\\\/console-fail-test\\)/g, + "from": /JoshuaKGoldberg\\(\\?:\\\\/\\(\\.\\+\\)\\)\\?/g, + "to": [Function], + }, + ], + [ + { + "allowEmptyPaths": true, + "files": "package.json", + "from": /JoshuaKGoldberg/g, "to": "StubOwner", }, ], @@ -359,8 +375,8 @@ describe("updateLocalFiles", () => { { "allowEmptyPaths": true, "files": "./README.md", - "from": "> 💙 This package is based on [@StubOwner](https://github.com/StubOwner)'s [stub-repository](https://github.com/JoshuaKGoldberg/stub-repository).", - "to": "> 💙 This package is based on [@JoshuaKGoldberg](https://github.com/JoshuaKGoldberg)'s [create-typescript-app](https://github.com/JoshuaKGoldberg/create-typescript-app).", + "from": /> 💙 This package was templated with \\.\\+\\\\\\./g, + "to": "> 💙 This package was templated with [\`create-typescript-app\`](https://github.com/JoshuaKGoldberg/create-typescript-app).", }, ], ] diff --git a/src/steps/updateLocalFiles.ts b/src/steps/updateLocalFiles.ts index b7f3fa9a9..8856d702d 100644 --- a/src/steps/updateLocalFiles.ts +++ b/src/steps/updateLocalFiles.ts @@ -2,6 +2,8 @@ import replaceInFile from "replace-in-file"; import { readFileSafeAsJson } from "../shared/readFileSafeAsJson.js"; import { Options } from "../shared/types.js"; +import { createJoshuaKGoldbergReplacement } from "./createJoshuaKGoldbergReplacement.js"; +import { endOfReadmeTemplateLine } from "./updateReadme.js"; interface ExistingPackageData { description?: string; @@ -14,7 +16,8 @@ export async function updateLocalFiles(options: Options) { const replacements = [ [/Create TypeScript App/g, options.title], - [/JoshuaKGoldberg(?!\/console-fail-test)/g, options.owner], + createJoshuaKGoldbergReplacement(options), + [/JoshuaKGoldberg/g, options.owner, "package.json"], [/create-typescript-app/g, options.repository], [/\/\*\n.+\*\/\n\n/gs, ``, ".eslintrc.cjs"], [/"author": ".+"/g, `"author": "${options.author}"`, "./package.json"], @@ -37,8 +40,8 @@ export async function updateLocalFiles(options: Options) { [`["src/**/*.ts!", "script/**/*.js"]`, `"src/**/*.ts!"`, "./knip.jsonc"], // Edge case: migration scripts will rewrite README.md attribution [ - `> 💙 This package is based on [@${options.owner}](https://github.com/${options.owner})'s [${options.repository}](https://github.com/JoshuaKGoldberg/${options.repository}).`, - `> 💙 This package is based on [@JoshuaKGoldberg](https://github.com/JoshuaKGoldberg)'s [create-typescript-app](https://github.com/JoshuaKGoldberg/create-typescript-app).`, + /> 💙 This package was templated with .+\./g, + endOfReadmeTemplateLine, "./README.md", ], ]; @@ -65,8 +68,9 @@ export async function updateLocalFiles(options: Options) { to, }); } catch (error) { + const toString = typeof to === "function" ? "(function)" : to; throw new Error( - `Failed to replace ${from.toString()} with ${to} in ${files.toString()}`, + `Failed to replace ${from.toString()} with ${toString} in ${files.toString()}`, { cause: error, }, diff --git a/src/steps/updateReadme.test.ts b/src/steps/updateReadme.test.ts index 94f27f9d4..8cd24333c 100644 --- a/src/steps/updateReadme.test.ts +++ b/src/steps/updateReadme.test.ts @@ -2,12 +2,12 @@ import { describe, expect, it, vi } from "vitest"; import { updateReadme } from "./updateReadme.js"; -const mockAppendFile = vi.fn(); +const mockWriteFile = vi.fn(); vi.mock("node:fs/promises", () => ({ default: { - get appendFile() { - return mockAppendFile; + get writeFile() { + return mockWriteFile; }, }, })); @@ -20,20 +20,26 @@ vi.mock("../shared/readFileSafe.js", () => ({ }, })); +const options = { + owner: "NewOwner", +}; + describe("updateReadme", () => { it("adds a notice when the file does not contain it already", async () => { - mockReadFileSafe.mockResolvedValue(""); + mockReadFileSafe.mockResolvedValue( + "Existing JoshuaKGoldberg/create-typescript-app content.", + ); - await updateReadme(); + await updateReadme(options); - expect(mockAppendFile.mock.calls).toMatchInlineSnapshot(` + expect(mockWriteFile.mock.calls).toMatchInlineSnapshot(` [ [ "./README.md", - " + "Existing NewOwner/create-typescript-app content. - > 💙 This package was templated with [create-typescript-app](https://github.com/JoshuaKGoldberg/create-typescript-app). + > 💙 This package was templated with [\`create-typescript-app\`](https://github.com/JoshuaKGoldberg/create-typescript-app). ", ], ] @@ -42,23 +48,51 @@ describe("updateReadme", () => { it("doesn't add a notice when the file contains it already", async () => { mockReadFileSafe.mockResolvedValue(` + Existing JoshuaKGoldberg/create-typescript-app content. + > 💙 This package was templated using [create-typescript-app](https://github.com/JoshuaKGoldberg/create-typescript-app). `); - await updateReadme(); + await updateReadme(options); - expect(mockAppendFile.mock.calls).toMatchInlineSnapshot("[]"); + expect(mockWriteFile.mock.calls).toMatchInlineSnapshot(` + [ + [ + "./README.md", + " + Existing NewOwner/create-typescript-app content. + + + + > 💙 This package was templated using [create-typescript-app](https://github.com/NewOwner/create-typescript-app). + ", + ], + ] + `); }); it("doesn't add a notice when the file contains an older version of it already", async () => { mockReadFileSafe.mockResolvedValue(` + Existing JoshuaKGoldberg/create-typescript-app content. + 💙 This package is based on [@JoshuaKGoldberg](https://github.com/JoshuaKGoldberg)'s [create-typescript-app](https://github.com/JoshuaKGoldberg/create-typescript-app). `); - await updateReadme(); + await updateReadme(options); - expect(mockAppendFile.mock.calls).toMatchInlineSnapshot("[]"); + expect(mockWriteFile.mock.calls).toMatchInlineSnapshot(` + [ + [ + "./README.md", + " + Existing NewOwner/create-typescript-app content. + + 💙 This package is based on [@NewOwner](https://github.com/NewOwner)'s [create-typescript-app](https://github.com/NewOwner/create-typescript-app). + ", + ], + ] + `); }); }); diff --git a/src/steps/updateReadme.ts b/src/steps/updateReadme.ts index be75b7b82..e5b5c404d 100644 --- a/src/steps/updateReadme.ts +++ b/src/steps/updateReadme.ts @@ -2,22 +2,29 @@ import fs from "node:fs/promises"; import { EOL } from "node:os"; import { readFileSafe } from "../shared/readFileSafe.js"; +import { Options } from "../shared/types.js"; + +export const endOfReadmeTemplateLine = `> 💙 This package was templated with [\`create-typescript-app\`](https://github.com/JoshuaKGoldberg/create-typescript-app).`; export const endOfReadmeNotice = [ ``, ``, ``, - `> 💙 This package was templated with [create-typescript-app](https://github.com/JoshuaKGoldberg/create-typescript-app).`, + endOfReadmeTemplateLine, ``, ].join(EOL); export const endOfReadmeMatcher = /💙.+(?:based|built|templated).+(?:from|using|on|with).+create-typescript-app/; -export async function updateReadme() { - const contents = await readFileSafe("./README.md", ""); +export async function updateReadme(options: Pick) { + let contents = await readFileSafe("./README.md", ""); + + contents = contents.replaceAll("JoshuaKGoldberg", options.owner); if (!endOfReadmeMatcher.test(contents)) { - await fs.appendFile("./README.md", endOfReadmeNotice); + contents += endOfReadmeNotice; } + + await fs.writeFile("./README.md", contents); } diff --git a/src/steps/writeReadme/index.test.ts b/src/steps/writeReadme/index.test.ts index 9534e5a2a..b84d96f4b 100644 --- a/src/steps/writeReadme/index.test.ts +++ b/src/steps/writeReadme/index.test.ts @@ -95,7 +95,7 @@ describe("writeReadme", () => { - > 💙 This package was templated with [create-typescript-app](https://github.com/JoshuaKGoldberg/create-typescript-app). + > 💙 This package was templated with [\`create-typescript-app\`](https://github.com/JoshuaKGoldberg/create-typescript-app). ", ], ] @@ -156,7 +156,7 @@ describe("writeReadme", () => { - > 💙 This package was templated with [create-typescript-app](https://github.com/JoshuaKGoldberg/create-typescript-app). + > 💙 This package was templated with [\`create-typescript-app\`](https://github.com/JoshuaKGoldberg/create-typescript-app). ", ], ] @@ -220,7 +220,7 @@ describe("writeReadme", () => { - > 💙 This package was templated with [create-typescript-app](https://github.com/JoshuaKGoldberg/create-typescript-app). + > 💙 This package was templated with [\`create-typescript-app\`](https://github.com/JoshuaKGoldberg/create-typescript-app). ", ], ] diff --git a/src/steps/writing/creation/dotGitHub/createDotGitHubFiles.ts b/src/steps/writing/creation/dotGitHub/createDotGitHubFiles.ts index bed2bb090..5ddc42dcc 100644 --- a/src/steps/writing/creation/dotGitHub/createDotGitHubFiles.ts +++ b/src/steps/writing/creation/dotGitHub/createDotGitHubFiles.ts @@ -163,8 +163,8 @@ There are two steps involved: ### Finding an Issue -With the exception of very small typos, all changes to this repository generally need to correspond to an [unassigned open issue marked as \`status: accepting prs\` on the issue tracker](https://github.com/JoshuaKGoldberg/create-typescript-app/issues?q=is%3Aissue+is%3Aopen+label%3A%22status%3A+accepting+prs%22+no%3Aassignee+). -If this is your first time contributing, consider searching for [unassigned issues that also have the \`good first issue\` label](https://github.com/JoshuaKGoldberg/create-typescript-app/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22+label%3A%22status%3A+accepting+prs%22+no%3Aassignee+). +With the exception of very small typos, all changes to this repository generally need to correspond to an [unassigned open issue marked as \`status: accepting prs\` on the issue tracker](https://github.com/${options.owner}/${options.repository}/issues?q=is%3Aissue+is%3Aopen+label%3A%22status%3A+accepting+prs%22+no%3Aassignee+). +If this is your first time contributing, consider searching for [unassigned issues that also have the \`good first issue\` label](https://github.com/${options.owner}/${options.repository}/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22+label%3A%22status%3A+accepting+prs%22+no%3Aassignee+). If the issue you'd like to fix isn't found on the issue, see [Reporting Issues](#reporting-issues) for filing your own (please do!). #### Issue Claiming