178 lines
5.2 KiB
JavaScript
178 lines
5.2 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import indexToPosition from "index-to-position";
|
|
import { outdent } from "outdent";
|
|
import remarkParse from "remark-parse";
|
|
import unified from "unified";
|
|
import { CHANGELOG_CATEGORIES } from "./utilities/changelog.js";
|
|
|
|
const CHANGELOG_DIR = "changelog_unreleased";
|
|
const TEMPLATE_FILE = "TEMPLATE.md";
|
|
const BLOG_POST_INTRO_TEMPLATE_FILE = "BLOG_POST_INTRO_TEMPLATE.md";
|
|
const BLOG_POST_INTRO_FILE = "blog-post-intro.md";
|
|
const CHANGELOG_ROOT = new URL(`../${CHANGELOG_DIR}/`, import.meta.url);
|
|
const showErrorMessage = (message) => {
|
|
console.error(message);
|
|
process.exitCode = 1;
|
|
};
|
|
|
|
const files = fs.readdirSync(CHANGELOG_ROOT);
|
|
for (const file of files) {
|
|
if (
|
|
file !== TEMPLATE_FILE &&
|
|
file !== BLOG_POST_INTRO_FILE &&
|
|
file !== BLOG_POST_INTRO_TEMPLATE_FILE &&
|
|
!CHANGELOG_CATEGORIES.includes(file)
|
|
) {
|
|
showErrorMessage(`Please remove "${file}" from "${CHANGELOG_DIR}".`);
|
|
}
|
|
}
|
|
for (const file of [
|
|
TEMPLATE_FILE,
|
|
BLOG_POST_INTRO_TEMPLATE_FILE,
|
|
...CHANGELOG_CATEGORIES,
|
|
]) {
|
|
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,}\])/;
|
|
|
|
const template = fs.readFileSync(
|
|
new URL(TEMPLATE_FILE, CHANGELOG_ROOT),
|
|
"utf8",
|
|
);
|
|
const templateComments = template.match(/<!--.*?-->/gs);
|
|
const [templateAuthorLink] = template.match(authorRegex);
|
|
const checkedFiles = new Map();
|
|
|
|
for (const category of CHANGELOG_CATEGORIES) {
|
|
const files = fs.readdirSync(new URL(`${category}/`, CHANGELOG_ROOT));
|
|
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$/);
|
|
const displayPath = `${CHANGELOG_DIR}/${category}/${prFile}`;
|
|
|
|
if (!match) {
|
|
showErrorMessage(
|
|
`[${displayPath}]: Filename is not in form of "{PR_NUMBER}.md".`,
|
|
);
|
|
continue;
|
|
}
|
|
const [, prNumber] = match;
|
|
const prLink = `#${prNumber}`;
|
|
if (checkedFiles.has(prFile)) {
|
|
showErrorMessage(
|
|
outdent`
|
|
Duplicate files for ${prLink} found.
|
|
- ${checkedFiles.get(prFile)}
|
|
- ${displayPath}
|
|
`,
|
|
);
|
|
}
|
|
checkedFiles.set(prFile, displayPath);
|
|
const content = fs.readFileSync(
|
|
new URL(`${category}/${prFile}`, CHANGELOG_ROOT),
|
|
"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,
|
|
)}`,
|
|
);
|
|
}
|
|
}
|
|
if (content.includes(templateAuthorLink)) {
|
|
showErrorMessage(
|
|
`[${displayPath}]: Please change author link to your github account.`,
|
|
);
|
|
}
|
|
if (!content.startsWith("#### ")) {
|
|
showErrorMessage(`[${displayPath}]: Please use h4 ("####") for title.`);
|
|
}
|
|
const titleMatch = content.match(titleRegex);
|
|
if (!titleMatch) {
|
|
showErrorMessage(`[${displayPath}]: Something wrong in title.`);
|
|
continue;
|
|
}
|
|
const [, title] = titleMatch;
|
|
|
|
validateTitle(displayPath, title);
|
|
|
|
const categoryInTitle = title.split(":").shift().trim();
|
|
if (
|
|
[...CHANGELOG_CATEGORIES, "js"].includes(categoryInTitle.toLowerCase())
|
|
) {
|
|
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.`,
|
|
);
|
|
}
|
|
|
|
if (/prettier master/i.test(content)) {
|
|
showErrorMessage(
|
|
`[${displayPath}]: Please use "main" instead of "master".`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
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}`;
|
|
}
|
|
|
|
// Forbid HTML in title
|
|
// https://github.com/prettier/prettier/issues/17089
|
|
function validateTitle(displayPath, title) {
|
|
const processor = unified().use(remarkParse);
|
|
const tree = processor.runSync(processor.parse(title));
|
|
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.`,
|
|
);
|
|
}
|
|
}
|
|
}
|