diff --git a/docs/Configuration Files.md b/docs/Configuration Files.md index c18428348..1504edfe3 100644 --- a/docs/Configuration Files.md +++ b/docs/Configuration Files.md @@ -24,7 +24,6 @@ These options are generally only programmatically used internally, but can still | `contributors` | AllContributors contributors to store in `.all-contributorsrc` | Existing contributors in the file, or just your username | | `documentation` | additional docs to add to `.github/DEVELOPMENT.md` and/or `README.md` | Extra content in those two files | | `existingLabels` | existing labels to switch to the standard template labels | Existing labels on the repository from the GitHub API | -| `explainer` | additional `README.md` sentence(s) describing the package | Extra content in `README.md` after badges and description | | `guide` | link to a contribution guide to place at the top of development docs | Block quote on top of `.github/DEVELOPMENT.md` | | `logo` | local image file and alt text to display near the top of the `README.md` | First non-badge image's `alt` and `src` in `README.md` | | `node` | Node.js engine version(s) to pin and require a minimum of | Values from `.nvmrc` and `package.json`'s `"engines"` | diff --git a/src/base.test.ts b/src/base.test.ts index 4b092a8a9..c6154a03b 100644 --- a/src/base.test.ts +++ b/src/base.test.ts @@ -24,6 +24,10 @@ describe("base", () => { development: expect.any(String), readme: { additional: expect.any(String), + explainer: [ + `\`create-typescript-app\` is a one-stop-shop solution to set up a new or existing repository with the latest and greatest TypeScript tooling.`, + `It includes options not just for building and testing but also automated release management, contributor recognition, GitHub repository settings, and more.`, + ].join("\n"), usage: expect.any(String), }, }, @@ -33,10 +37,6 @@ describe("base", () => { }, emoji: "๐", existingLabels: expect.any(Array), - explainer: [ - `\`create-typescript-app\` is a one-stop-shop solution to set up a new or existing repository with the latest and greatest TypeScript tooling.`, - `It includes options not just for building and testing but also automated release management, contributor recognition, GitHub repository settings, and more.`, - ], funding: "JoshuaKGoldberg", guide: { href: "https://www.joshuakgoldberg.com/blog/contributing-to-a-create-typescript-app-repository", diff --git a/src/base.ts b/src/base.ts index 6d761b02d..b35dc66de 100644 --- a/src/base.ts +++ b/src/base.ts @@ -17,7 +17,6 @@ import { readEmailFromNpm } from "./options/readEmailFromNpm.js"; import { readEmails } from "./options/readEmails.js"; import { readEmoji } from "./options/readEmoji.js"; import { readExistingLabels } from "./options/readExistingLabels.js"; -import { readExplainer } from "./options/readExplainer.js"; import { readFileSafe } from "./options/readFileSafe.js"; import { readFunding } from "./options/readFunding.js"; import { readGitDefaults } from "./options/readGitDefaults.js"; @@ -31,6 +30,7 @@ import { readPackageAuthor } from "./options/readPackageAuthor.js"; import { readPackageData } from "./options/readPackageData.js"; import { readPnpm } from "./options/readPnpm.js"; import { readReadmeAdditional } from "./options/readReadmeAdditional.js"; +import { readReadmeExplainer } from "./options/readReadmeExplainer.js"; import { readReadmeUsage } from "./options/readReadmeUsage.js"; import { readRepository } from "./options/readRepository.js"; import { readRulesetId } from "./options/readRulesetId.js"; @@ -94,10 +94,6 @@ export const base = createBase({ ) .optional() .describe("existing labels from the GitHub repository"), - explainer: z - .array(z.string()) - .optional() - .describe("additional README.md sentence(s) describing the package"), funding: z .string() .optional() @@ -200,6 +196,7 @@ export const base = createBase({ await readDocumentation( getDevelopmentDocumentation, getReadmeAdditional, + getReadmeExplainer, getReadmeUsage, ), ); @@ -232,8 +229,6 @@ export const base = createBase({ async () => await readExistingLabels(take, getOwner, getRepository), ); - const getExplainer = lazyValue(async () => await readExplainer(getReadme)); - const getFunding = lazyValue(async () => await readFunding(take)); const getGitDefaults = lazyValue(async () => await readGitDefaults(take)); @@ -282,6 +277,10 @@ export const base = createBase({ async () => await readReadmeAdditional(getReadme), ); + const getReadmeExplainer = lazyValue( + async () => await readReadmeExplainer(getReadme), + ); + const getReadmeUsage = lazyValue( async () => await readReadmeUsage(getEmoji, getReadme, getRepository), ); @@ -318,7 +317,6 @@ export const base = createBase({ email: getEmail, emoji: getEmoji, existingLabels: getExistingLabels, - explainer: getExplainer, funding: getFunding, guide: getGuide, keywords: getKeywords, diff --git a/src/blocks/blockREADME.test.ts b/src/blocks/blockREADME.test.ts index 3cc7b485b..ad38c95ad 100644 --- a/src/blocks/blockREADME.test.ts +++ b/src/blocks/blockREADME.test.ts @@ -121,7 +121,13 @@ describe("blockREADME", () => { const creation = testBlock(blockREADME, { options: { ...optionsBase, - explainer: ["And a one.", "And a two."], + documentation: { + ...optionsBase.documentation, + readme: { + ...optionsBase.documentation.readme, + explainer: "\nAnd a one.\nAnd a two.\n", + }, + }, }, }); @@ -136,9 +142,11 @@ describe("blockREADME", () => { <img alt="๐ช TypeScript: Strict" src="https://img.shields.io/badge/%F0%9F%92%AA_typescript-strict-21bb42.svg" /> </p> + And a one. And a two. + ## Usage Test usage. @@ -238,7 +246,13 @@ describe("blockREADME", () => { const creation = testBlock(blockREADME, { options: { ...optionsBase, - explainer: ["And a one.", "And a two."], + documentation: { + ...optionsBase.documentation, + readme: { + ...optionsBase.documentation.readme, + explainer: "And a one.\nAnd a two.", + }, + }, logo: { alt: "My logo", height: 100, diff --git a/src/blocks/blockREADME.ts b/src/blocks/blockREADME.ts index fd29bfdc3..b0300950e 100644 --- a/src/blocks/blockREADME.ts +++ b/src/blocks/blockREADME.ts @@ -36,7 +36,8 @@ export const blockREADME = base.createBlock({ const { badges, notices, sections } = addons; const explainer = - options.explainer && `\n${options.explainer.join("\n")}\n`; + options.documentation.readme.explainer && + `\n${options.documentation.readme.explainer}\n`; const logo = options.logo && diff --git a/src/options/readDocumentation.ts b/src/options/readDocumentation.ts index b3802fa74..fc1eb7065 100644 --- a/src/options/readDocumentation.ts +++ b/src/options/readDocumentation.ts @@ -3,10 +3,12 @@ import { Documentation } from "../schemas.js"; export async function readDocumentation( getDevelopmentDocumentation: () => Promise<string | undefined>, getReadmeAdditional: () => Promise<string | undefined>, + getReadmeExplainer: () => Promise<string | undefined>, getReadmeUsage: () => Promise<string>, ): Promise<Documentation> { - const [additional, development, usage] = await Promise.all([ + const [additional, explainer, development, usage] = await Promise.all([ getReadmeAdditional(), + getReadmeExplainer(), getDevelopmentDocumentation(), getReadmeUsage(), ]); @@ -15,6 +17,7 @@ export async function readDocumentation( development, readme: { additional, + explainer, usage, }, }; diff --git a/src/options/readExplainer.test.ts b/src/options/readExplainer.test.ts deleted file mode 100644 index d69c22909..000000000 --- a/src/options/readExplainer.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { readExplainer } from "./readExplainer.js"; - -describe(readExplainer, () => { - it("defaults to undefined when it cannot be found", async () => { - const actual = await readExplainer(() => Promise.resolve(`nothing.`)); - - expect(actual).toBeUndefined(); - }); - - it("parses a line after badges", async () => { - const actual = await readExplainer(() => - Promise.resolve(` -</p> - -This is my project. - -## Usage - .`), - ); - - expect(actual).toEqual(["This is my project."]); - }); - - it("parses multiple lines after badges", async () => { - const actual = await readExplainer(() => - Promise.resolve(` -</p> - -This is my project. -It is good. - -## Usage - .`), - ); - - expect(actual).toEqual(["This is my project.", "It is good."]); - }); - - it("parses multiple lines after full badges and a logo", async () => { - const actual = await readExplainer(() => - Promise.resolve(` -<p align="center"> - <!-- prettier-ignore-start --> - <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section --> - <a href="#contributors" target="_blank"><img alt="๐ช All Contributors: 52" src="https://img.shields.io/badge/%F0%9F%91%AA_all_contributors-52-21bb42.svg" /></a> -<!-- ALL-CONTRIBUTORS-BADGE:END --> - <!-- prettier-ignore-end --> - <a href="https://github.com/JoshuaKGoldberg/create-typescript-app/blob/main/.github/CODE_OF_CONDUCT.md" target="_blank"><img alt="๐ค Code of Conduct: Kept" src="https://img.shields.io/badge/%F0%9F%A4%9D_code_of_conduct-kept-21bb42" /></a> - <a href="https://codecov.io/gh/JoshuaKGoldberg/create-typescript-app" target="_blank"><img alt="๐งช Coverage" src="https://img.shields.io/codecov/c/github/JoshuaKGoldberg/create-typescript-app?label=%F0%9F%A7%AA%20coverage" /></a> - <a href="https://github.com/JoshuaKGoldberg/create-typescript-app/blob/main/LICENSE.md" target="_blank"><img alt="๐ License: MIT" src="https://img.shields.io/badge/%F0%9F%93%9D_license-MIT-21bb42.svg"></a> - <a href="http://npmjs.com/package/create-typescript-app"><img alt="๐ฆ npm version" src="https://img.shields.io/npm/v/create-typescript-app?color=21bb42&label=%F0%9F%93%A6%20npm" /></a> - <img alt="๐ช TypeScript: Strict" src="https://img.shields.io/badge/%F0%9F%92%AA_typescript-strict-21bb42.svg" /> -</p> - -<img align="right" alt="Project logo: the TypeScript blue square with rounded corners, but a plus sign instead of 'TS'" height="128" src="./docs/create-typescript-app.png" width="128"> - -This is my project. -It is good. - -## Usage - .`), - ); - - expect(actual).toEqual(["This is my project.", "It is good."]); - }); -}); diff --git a/src/options/readExplainer.ts b/src/options/readExplainer.ts deleted file mode 100644 index 57d534301..000000000 --- a/src/options/readExplainer.ts +++ /dev/null @@ -1,16 +0,0 @@ -export async function readExplainer(getReadme: () => Promise<string>) { - const readme = await getReadme(); - - const indexOfH2 = readme.indexOf("##"); - if (indexOfH2 === -1) { - return undefined; - } - - const lastTag = readme.slice(0, indexOfH2).lastIndexOf(">"); - - return readme - .slice(lastTag + 1, indexOfH2) - .split("\n") - .map((line) => line.trim()) - .filter(Boolean); -} diff --git a/src/options/readReadmeExplainer.test.ts b/src/options/readReadmeExplainer.test.ts new file mode 100644 index 000000000..d545d13c3 --- /dev/null +++ b/src/options/readReadmeExplainer.test.ts @@ -0,0 +1,185 @@ +import { describe, expect, it } from "vitest"; + +import { readReadmeExplainer } from "./readReadmeExplainer.js"; + +describe(readReadmeExplainer, () => { + it("resolves with undefined when an h2 cannot be found", async () => { + const actual = await readReadmeExplainer(() => Promise.resolve(`nothing.`)); + + expect(actual).toBeUndefined(); + }); + + it("resolves with undefined before h2 when a Usage h2 exists and there are no preceding tags", async () => { + const actual = await readReadmeExplainer(() => + Promise.resolve(`# Title + +## Usage`), + ); + + expect(actual).toBeUndefined(); + }); + + it("resolves with undefined before h2 when a non-Usage h2 exists and there are no preceding tags", async () => { + const actual = await readReadmeExplainer(() => + Promise.resolve(`# Title + +## What?`), + ); + + expect(actual).toBeUndefined(); + }); + + it("parses a line after badges", async () => { + const actual = await readReadmeExplainer(() => + Promise.resolve(` +</p> + +This is my project. + +## Usage + .`), + ); + + expect(actual).toEqual("This is my project."); + }); + + it("parses multiple lines after badges", async () => { + const actual = await readReadmeExplainer(() => + Promise.resolve(` +</p> + +This is my project. +It is good. + +## Usage + .`), + ); + + expect(actual).toEqual("This is my project.\nIt is good."); + }); + + it("parses multiple lines after full badges and a logo", async () => { + const actual = await readReadmeExplainer(() => + Promise.resolve(` +<p align="center"> + <!-- prettier-ignore-start --> + <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section --> + <a href="#contributors" target="_blank"><img alt="๐ช All Contributors: 52" src="https://img.shields.io/badge/%F0%9F%91%AA_all_contributors-52-21bb42.svg" /></a> +<!-- ALL-CONTRIBUTORS-BADGE:END --> + <!-- prettier-ignore-end --> + <a href="https://github.com/JoshuaKGoldberg/create-typescript-app/blob/main/.github/CODE_OF_CONDUCT.md" target="_blank"><img alt="๐ค Code of Conduct: Kept" src="https://img.shields.io/badge/%F0%9F%A4%9D_code_of_conduct-kept-21bb42" /></a> + <a href="https://codecov.io/gh/JoshuaKGoldberg/create-typescript-app" target="_blank"><img alt="๐งช Coverage" src="https://img.shields.io/codecov/c/github/JoshuaKGoldberg/create-typescript-app?label=%F0%9F%A7%AA%20coverage" /></a> + <a href="https://github.com/JoshuaKGoldberg/create-typescript-app/blob/main/LICENSE.md" target="_blank"><img alt="๐ License: MIT" src="https://img.shields.io/badge/%F0%9F%93%9D_license-MIT-21bb42.svg"></a> + <a href="http://npmjs.com/package/create-typescript-app"><img alt="๐ฆ npm version" src="https://img.shields.io/npm/v/create-typescript-app?color=21bb42&label=%F0%9F%93%A6%20npm" /></a> + <img alt="๐ช TypeScript: Strict" src="https://img.shields.io/badge/%F0%9F%92%AA_typescript-strict-21bb42.svg" /> +</p> + +<img align="right" alt="Project logo: the TypeScript blue square with rounded corners, but a plus sign instead of 'TS'" height="128" src="./docs/create-typescript-app.png" width="128"> + +This is my project. +It is good. + +## Usage + .`), + ); + + expect(actual).toEqual("This is my project.\nIt is good."); + }); + + it("parses a non-Usage h2 after full badges", async () => { + const actual = await readReadmeExplainer(() => + Promise.resolve(` +<p align="center"> + <!-- prettier-ignore-start --> + <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section --> + <a href="#contributors" target="_blank"><img alt="๐ช All Contributors: 52" src="https://img.shields.io/badge/%F0%9F%91%AA_all_contributors-52-21bb42.svg" /></a> +<!-- ALL-CONTRIBUTORS-BADGE:END --> + <!-- prettier-ignore-end --> + <a href="https://github.com/JoshuaKGoldberg/create-typescript-app/blob/main/.github/CODE_OF_CONDUCT.md" target="_blank"><img alt="๐ค Code of Conduct: Kept" src="https://img.shields.io/badge/%F0%9F%A4%9D_code_of_conduct-kept-21bb42" /></a> + <a href="https://codecov.io/gh/JoshuaKGoldberg/create-typescript-app" target="_blank"><img alt="๐งช Coverage" src="https://img.shields.io/codecov/c/github/JoshuaKGoldberg/create-typescript-app?label=%F0%9F%A7%AA%20coverage" /></a> + <a href="https://github.com/JoshuaKGoldberg/create-typescript-app/blob/main/LICENSE.md" target="_blank"><img alt="๐ License: MIT" src="https://img.shields.io/badge/%F0%9F%93%9D_license-MIT-21bb42.svg"></a> + <a href="http://npmjs.com/package/create-typescript-app"><img alt="๐ฆ npm version" src="https://img.shields.io/npm/v/create-typescript-app?color=21bb42&label=%F0%9F%93%A6%20npm" /></a> + <img alt="๐ช TypeScript: Strict" src="https://img.shields.io/badge/%F0%9F%92%AA_typescript-strict-21bb42.svg" /> +</p> + +## What? + +This is my project. +It is good. + +## Usage + .`), + ); + + expect(actual).toEqual("## What?\n\nThis is my project.\nIt is good."); + }); + + it("parses a non-Usage h2 with a block quote after full badges", async () => { + const actual = await readReadmeExplainer(() => + Promise.resolve(` + <a href="http://npmjs.com/package/create-typescript-app"><img alt="๐ฆ npm version" src="https://img.shields.io/npm/v/create-typescript-app?color=21bb42&label=%F0%9F%93%A6%20npm" /></a> + <img alt="๐ช TypeScript: Strict" src="https://img.shields.io/badge/%F0%9F%92%AA_typescript-strict-21bb42.svg" /> +</p> + +## What? + +This is my project. +It is good. + +> See here. + +## Usage + .`), + ); + + expect(actual).toEqual( + "## What?\n\nThis is my project.\nIt is good.\n\n> See here.", + ); + }); + + it("parses a non-Usage h2 after full badges and a logo", async () => { + const actual = await readReadmeExplainer(() => + Promise.resolve(` + <a href="http://npmjs.com/package/create-typescript-app"><img alt="๐ฆ npm version" src="https://img.shields.io/npm/v/create-typescript-app?color=21bb42&label=%F0%9F%93%A6%20npm" /></a> + <img alt="๐ช TypeScript: Strict" src="https://img.shields.io/badge/%F0%9F%92%AA_typescript-strict-21bb42.svg" /> +</p> + +<img align="right" alt="Project logo: the TypeScript blue square with rounded corners, but a plus sign instead of 'TS'" height="128" src="./docs/create-typescript-app.png" width="128"> + +## What? + +This is my project. +It is good. + +## Usage + .`), + ); + + expect(actual).toEqual("## What?\n\nThis is my project.\nIt is good."); + }); + + it("parses a non-Usage h2 with a block quote after full badges and a logo", async () => { + const actual = await readReadmeExplainer(() => + Promise.resolve(` + <a href="http://npmjs.com/package/create-typescript-app"><img alt="๐ฆ npm version" src="https://img.shields.io/npm/v/create-typescript-app?color=21bb42&label=%F0%9F%93%A6%20npm" /></a> + <img alt="๐ช TypeScript: Strict" src="https://img.shields.io/badge/%F0%9F%92%AA_typescript-strict-21bb42.svg" /> +</p> + +<img align="right" alt="Project logo: the TypeScript blue square with rounded corners, but a plus sign instead of 'TS'" height="128" src="./docs/create-typescript-app.png" width="128"> + +## What? + +This is my project. +It is good. + +> See here. + +## Usage + .`), + ); + + expect(actual).toEqual( + "## What?\n\nThis is my project.\nIt is good.\n\n> See here.", + ); + }); +}); diff --git a/src/options/readReadmeExplainer.ts b/src/options/readReadmeExplainer.ts new file mode 100644 index 000000000..aff6a33da --- /dev/null +++ b/src/options/readReadmeExplainer.ts @@ -0,0 +1,36 @@ +export async function readReadmeExplainer(getReadme: () => Promise<string>) { + const readme = await getReadme(); + + const indexOfUsageH2 = /## Usage/.exec(readme)?.index ?? readme.indexOf("##"); + if (indexOfUsageH2 === -1) { + return undefined; + } + + const beforeUsageH2 = readme.slice(0, indexOfUsageH2); + + const [indexOfLastTag, lastTagMatcher] = lastLastIndexOf(beforeUsageH2, [ + `">`, + "/p>", + "/>", + ]); + if (!lastTagMatcher) { + return undefined; + } + + return readme + .slice(indexOfLastTag + lastTagMatcher.length, indexOfUsageH2) + .trim(); +} + +function lastLastIndexOf(text: string, matchers: string[]) { + let pair: [number, string | undefined] = [-1, undefined]; + + for (const matcher of matchers) { + const indexOf = text.lastIndexOf(matcher); + if (indexOf > pair[0]) { + pair = [indexOf, matcher]; + } + } + + return pair; +} diff --git a/src/schemas.ts b/src/schemas.ts index e8804bb5e..508ba0cef 100644 --- a/src/schemas.ts +++ b/src/schemas.ts @@ -12,6 +12,7 @@ export type Contributor = z.infer<typeof zContributor>; export const zReadme = z.object({ additional: z.string().optional(), + explainer: z.string().optional(), usage: z.string(), });