Skip to content

Commit 9bc3f69

Browse files
feat: allow description to safely include HTML tags (#1820)
## PR Checklist - [x] Addresses an existing open issue: fixes #1819 - [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 Uses [`html-to-text`](https://www.npmjs.com/package/html-to-text) to remove any HTML tags from descriptions. This ended up being a fun exploration of defaulting. As suggested in the issue, if the `README.md` has a CTA-style paragraph that strictly matches the `package.json` after removing HTML tags, then it's used. 💖
1 parent da58b38 commit 9bc3f69

14 files changed

+248
-93
lines changed

Diff for: package.json

+2
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"execa": "^9.5.2",
5050
"git-remote-origin-url": "^4.0.0",
5151
"git-url-parse": "^16.0.0",
52+
"html-to-text": "^9.0.5",
5253
"image-size": "^1.2.0",
5354
"input-from-file": "0.1.0-alpha.4",
5455
"input-from-file-json": "0.1.0-alpha.4",
@@ -79,6 +80,7 @@
7980
"@release-it/conventional-changelog": "9.0.3",
8081
"@types/eslint-plugin-markdown": "2.0.2",
8182
"@types/git-url-parse": "9.0.3",
83+
"@types/html-to-text": "^9.0.4",
8284
"@types/js-yaml": "4.0.9",
8385
"@types/node": "22.10.2",
8486
"@types/parse-author": "2.0.3",

Diff for: pnpm-lock.yaml

+62
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: src/next/base.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { readEmails } from "../shared/options/createOptionDefaults/readEmails.js
1515
import { readFunding } from "../shared/options/createOptionDefaults/readFunding.js";
1616
import { readGuide } from "../shared/options/createOptionDefaults/readGuide.js";
1717
import { readPackageData } from "../shared/packages.js";
18+
import { readFileSafe } from "../shared/readFileSafe.js";
1819
import { tryCatchLazyValueAsync } from "../shared/tryCatchLazyValueAsync.js";
1920
import { AllContributorsData } from "../shared/types.js";
2021
import { readDescription } from "./readDescription.js";
@@ -156,6 +157,8 @@ export const base = createBase({
156157
)?.stdout?.toString();
157158
});
158159

160+
const readme = lazyValue(async () => await readFileSafe("README.md", ""));
161+
159162
// TODO: Make these all use take
160163

161164
const gitDefaults = tryCatchLazyValueAsync(async () =>
@@ -196,7 +199,7 @@ export const base = createBase({
196199
author,
197200
bin: async () => (await packageData()).bin,
198201
contributors: allContributors,
199-
description: async () => await readDescription(packageData),
202+
description: async () => await readDescription(packageData, readme),
200203
documentation,
201204
email: async () => readEmails(npmDefaults, packageAuthor),
202205
funding: readFunding,
@@ -220,7 +223,7 @@ export const base = createBase({
220223
options.repository ??
221224
(await gitDefaults())?.name ??
222225
(await packageData()).name,
223-
...readDefaultsFromReadme(),
226+
...readDefaultsFromReadme(readme),
224227
version,
225228
};
226229
},

Diff for: src/next/blocks/blockPackageJson.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as htmlToText from "html-to-text";
12
import removeUndefinedObjects from "remove-undefined-objects";
23
import sortPackageJson from "sort-package-json";
34
import { z } from "zod";
@@ -44,6 +45,7 @@ export const blockPackageJson = base.createBlock({
4445
...options.packageData?.devDependencies,
4546
...addons.properties.devDependencies,
4647
};
48+
const description = htmlToText.convert(options.description);
4749

4850
return {
4951
files: {
@@ -56,7 +58,7 @@ export const blockPackageJson = base.createBlock({
5658
dependencies: Object.keys(dependencies).length
5759
? dependencies
5860
: undefined,
59-
description: options.description,
61+
description,
6062
devDependencies: Object.keys(devDependencies).length
6163
? devDependencies
6264
: undefined,

Diff for: src/next/blocks/blockRepositorySettings.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
import * as htmlToText from "html-to-text";
2+
13
import { base } from "../base.js";
24

35
export const blockRepositorySettings = base.createBlock({
46
about: {
57
name: "Repository Settings",
68
},
79
produce({ options }) {
10+
const description = htmlToText.convert(options.description);
11+
812
return {
913
requests: [
1014
{
@@ -16,7 +20,7 @@ export const blockRepositorySettings = base.createBlock({
1620
allow_rebase_merge: false,
1721
allow_squash_merge: true,
1822
delete_branch_on_merge: true,
19-
description: options.description,
23+
description,
2024
has_wiki: false,
2125
owner: options.owner,
2226
repo: options.repository,

Diff for: src/next/readDescription.test.ts

+40-16
Original file line numberDiff line numberDiff line change
@@ -2,44 +2,68 @@ import { describe, expect, it, vi } from "vitest";
22

33
import { readDescription } from "./readDescription.js";
44

5-
const mockSourcePackageJsonDescription = vi.fn<() => string>();
5+
const mockSourcePackageJson = vi.fn<() => string>();
66

77
vi.mock("./blocks/sourcePackageJson", () => ({
88
sourcePackageJson: {
99
get description() {
10-
return mockSourcePackageJsonDescription();
10+
return mockSourcePackageJson();
1111
},
1212
},
1313
}));
1414

15-
describe("finalize", () => {
15+
describe("readDescription", () => {
1616
it("returns undefined when the description matches the current package.json description", async () => {
1717
const existing = "Same description.";
1818

19-
mockSourcePackageJsonDescription.mockReturnValueOnce(existing);
19+
mockSourcePackageJson.mockReturnValueOnce(existing);
2020

21-
const documentation = await readDescription(() =>
22-
Promise.resolve({
23-
description: existing,
24-
}),
21+
const description = await readDescription(
22+
() => Promise.resolve({ description: existing }),
23+
() => Promise.resolve(""),
2524
);
2625

27-
expect(documentation).toBeUndefined();
26+
expect(description).toBeUndefined();
2827
});
2928

3029
it("returns the updated description when neither description nor name match the current package.json", async () => {
3130
const updated = "Updated description.";
3231

33-
mockSourcePackageJsonDescription.mockReturnValueOnce(
34-
"Existing description",
32+
mockSourcePackageJson.mockReturnValueOnce("Existing description");
33+
34+
const description = await readDescription(
35+
() => Promise.resolve({ description: updated }),
36+
() => Promise.resolve(""),
3537
);
3638

37-
const documentation = await readDescription(() =>
38-
Promise.resolve({
39-
description: updated,
40-
}),
39+
expect(description).toBe(updated);
40+
});
41+
42+
it("uses the README.md HTML description when it matches what's inferred from package.json plus HTML tags", async () => {
43+
const plaintext = "Updated description.";
44+
const encoded = "Updated <code>description</code>.";
45+
46+
mockSourcePackageJson.mockReturnValueOnce("Existing description");
47+
48+
const description = await readDescription(
49+
() => Promise.resolve({ description: plaintext }),
50+
() => Promise.resolve(`<p align="center">${encoded}</p>`),
51+
);
52+
53+
expect(description).toBe(encoded);
54+
});
55+
56+
it("uses the package.json description when the README.md HTML description doesn't match what's inferred from package.json plus HTML tags", async () => {
57+
const plaintext = "Updated description.";
58+
const encoded = "Incorrect <code>description</code>.";
59+
60+
mockSourcePackageJson.mockReturnValueOnce("Existing description");
61+
62+
const description = await readDescription(
63+
() => Promise.resolve({ description: plaintext }),
64+
() => Promise.resolve(`<p align="center">${encoded}</p>`),
4165
);
4266

43-
expect(documentation).toBe(updated);
67+
expect(description).toBe(plaintext);
4468
});
4569
});

Diff for: src/next/readDescription.ts

+11
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
import { PartialPackageData } from "../shared/types.js";
22
import { sourcePackageJson } from "./blocks/sourcePackageJson.js";
3+
import { readDescriptionFromReadme } from "./readDescriptionFromReadme.js";
34

45
export async function readDescription(
56
getPackageData: () => Promise<PartialPackageData>,
7+
getReadme: () => Promise<string>,
68
) {
79
const { description: inferred } = await getPackageData();
10+
if (!inferred) {
11+
return undefined;
12+
}
13+
814
const { description: existing } = sourcePackageJson;
15+
const fromReadme = await readDescriptionFromReadme(getReadme);
16+
17+
if (fromReadme?.replaceAll(/<\s*(?:\/\s*)?\w+\s*>/gu, "") === inferred) {
18+
return fromReadme;
19+
}
920

1021
return existing === inferred ? undefined : inferred;
1122
}

0 commit comments

Comments
 (0)