Files

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

177 lines
5.2 KiB
JavaScript
Raw Permalink Normal View History

2020-03-21 22:24:23 +02:00
#!/usr/bin/env node
2020-01-16 01:25:55 +08:00
2025-10-19 04:44:05 +08:00
import assert from "node:assert/strict";
2021-05-15 20:09:52 +08:00
import fs from "node:fs";
import indexToPosition from "index-to-position";
import { fromMarkdown } from "mdast-util-from-markdown";
import { outdent } from "outdent";
2026-05-28 01:26:59 +08:00
import { CHANGELOG_CATEGORIES } from "./utilities/changelog.js";
2021-05-15 20:09:52 +08:00
2020-01-16 01:25:55 +08:00
const CHANGELOG_DIR = "changelog_unreleased";
const TEMPLATE_FILE = "TEMPLATE.md";
const BLOG_POST_INTRO_TEMPLATE_FILE = "BLOG_POST_INTRO_TEMPLATE.md";
2020-03-21 22:24:23 +02:00
const BLOG_POST_INTRO_FILE = "blog-post-intro.md";
2025-02-16 15:36:51 +08:00
const CHANGELOG_ROOT = new URL(`../${CHANGELOG_DIR}/`, import.meta.url);
2020-01-16 01:25:55 +08:00
const showErrorMessage = (message) => {
console.error(message);
process.exitCode = 1;
};
const files = fs.readdirSync(CHANGELOG_ROOT);
for (const file of files) {
2020-03-21 22:24:23 +02:00
if (
file !== TEMPLATE_FILE &&
file !== BLOG_POST_INTRO_FILE &&
file !== BLOG_POST_INTRO_TEMPLATE_FILE &&
2020-03-21 22:24:23 +02:00
!CHANGELOG_CATEGORIES.includes(file)
) {
2020-01-16 01:25:55 +08:00
showErrorMessage(`Please remove "${file}" from "${CHANGELOG_DIR}".`);
}
}
2020-03-21 22:24:23 +02:00
for (const file of [
TEMPLATE_FILE,
BLOG_POST_INTRO_TEMPLATE_FILE,
2020-03-21 22:24:23 +02:00
...CHANGELOG_CATEGORIES,
]) {
2020-01-16 01:25:55 +08:00
if (!files.includes(file)) {
showErrorMessage(`Please don't remove "${file}" from "${CHANGELOG_DIR}".`);
}
}
const authorRegex = /by @[\w-]+|by \[@([\w-]+)\]\(https:\/\/github\.com\/\1\)/;
const titleRegex = /^#{4} (.*?)\((#\d{4,}|\[#\d{4,}\])/;
2020-01-16 01:25:55 +08:00
const template = fs.readFileSync(
2025-02-16 15:36:51 +08:00
new URL(TEMPLATE_FILE, CHANGELOG_ROOT),
2020-01-16 01:25:55 +08:00
"utf8",
);
const templateComments = template.match(/<!--.*?-->/gs);
2020-01-16 01:25:55 +08:00
const [templateAuthorLink] = template.match(authorRegex);
2020-11-14 12:45:30 +08:00
const checkedFiles = new Map();
2020-01-16 01:25:55 +08:00
for (const category of CHANGELOG_CATEGORIES) {
2025-02-16 15:36:51 +08:00
const files = fs.readdirSync(new URL(`${category}/`, CHANGELOG_ROOT));
2020-01-16 01:25:55 +08:00
if (!files.includes(".gitkeep")) {
showErrorMessage(
`Please don't remove ".gitkeep" from "${CHANGELOG_DIR}/${category}".`,
);
}
for (const prFile of files) {
if (prFile === ".gitkeep") {
continue;
}
const match = prFile.match(/^(\d{4,})(-\d+)?\.md$/);
2020-01-16 01:25:55 +08:00
const displayPath = `${CHANGELOG_DIR}/${category}/${prFile}`;
if (!match) {
showErrorMessage(
`[${displayPath}]: Filename is not in form of "{PR_NUMBER}.md".`,
2020-01-16 01:25:55 +08:00
);
continue;
}
const [, prNumber] = match;
2020-11-14 12:45:30 +08:00
const prLink = `#${prNumber}`;
if (checkedFiles.has(prFile)) {
2020-11-14 12:45:30 +08:00
showErrorMessage(
outdent`
Duplicate files for ${prLink} found.
- ${checkedFiles.get(prFile)}
2020-11-14 12:45:30 +08:00
- ${displayPath}
`,
);
}
checkedFiles.set(prFile, displayPath);
2020-01-16 01:25:55 +08:00
const content = fs.readFileSync(
2025-02-17 07:44:10 +08:00
new URL(`${category}/${prFile}`, CHANGELOG_ROOT),
2020-01-16 01:25:55 +08:00
"utf8",
);
if (!content.includes(prLink)) {
showErrorMessage(`[${displayPath}]: PR link "${prLink}" is missing.`);
}
if (!authorRegex.test(content)) {
showErrorMessage(`[${displayPath}]: Author link is missing.`);
}
for (const comment of templateComments) {
if (comment !== "<!-- prettier-ignore -->" && content.includes(comment)) {
showErrorMessage(
`[${displayPath}]: Please remove ${getCommentDescription(
content,
comment,
)}`,
);
}
2020-01-16 01:25:55 +08:00
}
if (content.includes(templateAuthorLink)) {
showErrorMessage(
`[${displayPath}]: Please change author link to your github account.`,
);
}
if (!content.startsWith("#### ")) {
showErrorMessage(`[${displayPath}]: Please use h4 ("####") for title.`);
2020-01-16 01:25:55 +08:00
}
const titleMatch = content.match(titleRegex);
if (!titleMatch) {
showErrorMessage(
`[${displayPath}]: Invalid title format. Expected: "#### <title> (#12345 by @author)".`,
);
2020-01-16 01:25:55 +08:00
continue;
}
const [, title] = titleMatch;
2025-10-19 04:44:05 +08:00
validateTitle(displayPath, title);
2020-01-16 01:25:55 +08:00
const categoryInTitle = title.split(":").shift().trim();
if (
[...CHANGELOG_CATEGORIES, "js"].includes(categoryInTitle.toLowerCase())
) {
2020-01-16 01:25:55 +08:00
showErrorMessage(
`[${displayPath}]: Please remove "${categoryInTitle}:" in title.`,
);
}
if (!title.endsWith(" ") || title.length - title.trimEnd().length !== 1) {
showErrorMessage(
`[${displayPath}]: Please put one space between title and PR link.`,
);
}
2021-01-24 01:14:13 +08:00
if (/prettier master/i.test(content)) {
2021-01-24 01:14:13 +08:00
showErrorMessage(
`[${displayPath}]: Please use "main" instead of "master".`,
);
}
2020-01-16 01:25:55 +08:00
}
}
function getCommentDescription(content, comment) {
const start = content.indexOf(comment);
const end = start + comment.length;
const [startLine, endLine] = [start, end].map(
(index) => indexToPosition(content, index, { oneBased: true }).line,
);
if (startLine === endLine) {
return `template comment "${comment}" on line ${startLine}`;
}
return `template comment on line ${startLine}-${endLine}`;
}
2025-10-19 04:44:05 +08:00
2026-06-16 02:09:45 +08:00
// Forbid HTML in title
2025-10-19 04:44:05 +08:00
// https://github.com/prettier/prettier/issues/17089
function validateTitle(displayPath, title) {
const tree = fromMarkdown(title);
2025-10-19 04:44:05 +08:00
assert.equal(tree.children.length, 1);
assert.equal(tree.children[0].type, "paragraph");
const { children } = tree.children[0];
for (const node of children) {
if (node.type === "html") {
showErrorMessage(
`[${displayPath}]: HTML "${node.value}" is restricted in title.`,
);
}
}
}