diff --git a/scripts/changelog.js b/scripts/changelog.js index 30e1ca017d..7edb1d423a 100644 --- a/scripts/changelog.js +++ b/scripts/changelog.js @@ -2,6 +2,12 @@ import { execFileSync } from "node:child_process"; import fs from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { + categories, + parseFragment, + placeholderLink, + trailingPrLinks, +} from "./changelogFragments.js"; import { checkMagicNumbers, configPath, @@ -9,61 +15,10 @@ import { showAt, } from "./magicNumbers.js"; -export const categories = { - breaking: "#### :boom: Breaking Change", - feature: "#### :rocket: New Feature", - fix: "#### :bug: Bug fix", - docs: "#### :memo: Documentation", - polish: "#### :nail_care: Polish", - internal: "#### :house: Internal", -}; +export { categories, parseFragment, placeholderLink }; -const prLink = - /https:\/\/github\.com\/rescript-lang\/rescript(?:-compiler)?\/pull\/([1-9]\d*)/; -export const placeholderLink = - "https://github.com/rescript-lang/rescript/pull/XXXX"; -const trailingPrLinks = new RegExp( - `${prLink.source}(?:\\s+${prLink.source})*$`, -); const repositoryUrl = "https://github.com/rescript-lang/rescript"; -export function parseFragment(name, content) { - const match = /^([a-z0-9][a-z0-9-]*)\.([a-z]+)\.md$/.exec(name); - if (!match || !Object.hasOwn(categories, match[2])) { - throw new Error( - `${name}: expected ..md; allowed categories: ${Object.keys(categories).join(", ")}`, - ); - } - const text = content.trim(); - if (text.includes(placeholderLink)) { - throw new Error( - `${name}: replace the placeholder ${placeholderLink} with the PR link`, - ); - } - const bullets = text.split(/\n(?=- )/); - const invalid = bullets.some( - bullet => - !/^- \S/.test(bullet) || - bullet - .split("\n") - .slice(1) - .some(line => line && !/^ {2}\S/.test(line)), - ); - if (!text || invalid) { - throw new Error( - `${name}: expected Markdown bullets (indent continuation lines by two spaces)`, - ); - } - const links = [...text.matchAll(new RegExp(prLink, "g"))].map(link => - Number(link[1]), - ); - // A bullet without a link gets the link of the pull request that added the - // fragment when the release is prepared. - const unlinked = bullets.some(bullet => !trailingPrLinks.test(bullet)); - const pr = links.length ? Math.max(...links) : null; - return { name, category: match[2], text, pr, unlinked }; -} - // The pull request that added a fragment, from the subject of the squash // commit that added it ("... (#1234)"). Rebase merges and merge commits carry // no such subject, which is why pull requests are squash-merged. diff --git a/scripts/changelogFragments.js b/scripts/changelogFragments.js new file mode 100644 index 0000000000..d46ebdd494 --- /dev/null +++ b/scripts/changelogFragments.js @@ -0,0 +1,58 @@ +// Changelog fragments (changes/..md): their +// categories and their parsing. This file imports nothing, so that scripts +// run without node_modules can use it, such as the Semantic Diff workflow's +// scripts/commitRecords.js. + +export const categories = { + breaking: "#### :boom: Breaking Change", + feature: "#### :rocket: New Feature", + fix: "#### :bug: Bug fix", + docs: "#### :memo: Documentation", + polish: "#### :nail_care: Polish", + internal: "#### :house: Internal", +}; + +const prLink = + /https:\/\/github\.com\/rescript-lang\/rescript(?:-compiler)?\/pull\/([1-9]\d*)/; +export const placeholderLink = + "https://github.com/rescript-lang/rescript/pull/XXXX"; +export const trailingPrLinks = new RegExp( + `${prLink.source}(?:\\s+${prLink.source})*$`, +); + +export function parseFragment(name, content) { + const match = /^([a-z0-9][a-z0-9-]*)\.([a-z]+)\.md$/.exec(name); + if (!match || !Object.hasOwn(categories, match[2])) { + throw new Error( + `${name}: expected ..md; allowed categories: ${Object.keys(categories).join(", ")}`, + ); + } + const text = content.trim(); + if (text.includes(placeholderLink)) { + throw new Error( + `${name}: replace the placeholder ${placeholderLink} with the PR link`, + ); + } + const bullets = text.split(/\n(?=- )/); + const invalid = bullets.some( + bullet => + !/^- \S/.test(bullet) || + bullet + .split("\n") + .slice(1) + .some(line => line && !/^ {2}\S/.test(line)), + ); + if (!text || invalid) { + throw new Error( + `${name}: expected Markdown bullets (indent continuation lines by two spaces)`, + ); + } + const links = [...text.matchAll(new RegExp(prLink, "g"))].map(link => + Number(link[1]), + ); + // A bullet without a link gets the link of the pull request that added the + // fragment when the release is prepared. + const unlinked = bullets.some(bullet => !trailingPrLinks.test(bullet)); + const pr = links.length ? Math.max(...links) : null; + return { name, category: match[2], text, pr, unlinked }; +} diff --git a/scripts/commitRecords.js b/scripts/commitRecords.js index 6ac055c354..d0d3390967 100644 --- a/scripts/commitRecords.js +++ b/scripts/commitRecords.js @@ -20,7 +20,7 @@ import { execFileSync } from "node:child_process"; import { readFileSync, writeFileSync } from "node:fs"; -import { categories, parseFragment } from "./changelog.js"; +import { categories, parseFragment } from "./changelogFragments.js"; import { commentMarkdown } from "./recordMarkdown.js"; import { CSS, esc, pageBody, pageTitle, parseRecord } from "./recordRender.js"; diff --git a/scripts/commitRecords.test.js b/scripts/commitRecords.test.js index 72160b78fa..69399910db 100644 --- a/scripts/commitRecords.test.js +++ b/scripts/commitRecords.test.js @@ -114,6 +114,28 @@ Review: Does a PPX still see the extension? Yes: the v0 bridge maps it back, ${SIGNED} `; +test("the renderer imports no packages: the workflow runs it without node_modules", async () => { + const seen = new Set(); + /** @param {string} file */ + const visit = async file => { + if (seen.has(file)) return; + seen.add(file); + const source = await fs.readFile(file, "utf8"); + for (const [, spec] of source.matchAll( + /^(?:import|export)\b[^"'`;]*?\bfrom\s+["']([^"']+)["']/gm, + )) { + assert.ok( + spec.startsWith("./") || spec.startsWith("node:"), + `${path.basename(file)} imports ${spec}`, + ); + if (spec.startsWith("./")) + await visit(path.resolve(path.dirname(file), spec)); + } + }; + await visit(script); + assert.ok(seen.size > 3, [...seen].join(", ")); +}); + test("a minimal record draws its identity and subject", async () => { const html = await render( async (git, dir) => {