From 3be02d5c5b2274d98c908d6848d329590aa32a5d Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Fri, 27 Dec 2024 15:52:45 -0500 Subject: [PATCH] Meta: Add a warning to PR previews --- .github/workflows/publish-pr.yml | 22 +++++++++- package.json | 3 +- scripts/insert_warning.mjs | 73 ++++++++++++++++++++++++++++++++ scripts/pr_preview_warning.html | 58 +++++++++++++++++++++++++ 4 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 scripts/insert_warning.mjs create mode 100644 scripts/pr_preview_warning.html diff --git a/.github/workflows/publish-pr.yml b/.github/workflows/publish-pr.yml index edca4e0..0adb2b9 100644 --- a/.github/workflows/publish-pr.yml +++ b/.github/workflows/publish-pr.yml @@ -57,11 +57,31 @@ jobs: id: extract-pr-number run: | cd result - awk -vok=1 \ + awk -v ok=1 \ '{ print; if(!match($0, /^[1-9][0-9]*$/)) ok=0; } END { exit !(NR==1 && ok); }' \ pr-number.txt echo "number=$(cat pr-number.txt)" >> $GITHUB_OUTPUT rm pr-number.txt + - name: Insert preview warning + env: + PR: ${{ steps.extract-pr-number.outputs.number }} + run: | + tmp="$(mktemp -u XXXXXXXX.json)" + repo_url="https://github.com/$GITHUB_REPOSITORY" + commit="$(git rev-parse --verify HEAD)" + jq -n --arg repo_url "$repo_url" --arg PR "$PR" --arg commit "$commit" ' + def repo_link($args): $args as [$path, $contents] + | ($repo_url + ($path // "")) as $url + | "\($contents // $url)"; + { + SUMMARY: "PR #\($PR)", + REPO_LINK: repo_link([]), + PR_LINK: repo_link(["/pull/" + $PR, "PR #\($PR)"]), + COMMIT_LINK: ("commit " + repo_link(["/commit/" + $commit, "\($commit)"])), + } + ' > "$tmp" + find result -name '*.html' -exec \ + node scripts/insert_warning.mjs --strict scripts/pr_preview_warning.html "$tmp" '{}' '+' - name: Publish to gh-pages uses: JamesIves/github-pages-deploy-action@v4.3.3 with: diff --git a/package.json b/package.json index abb4689..98ccf43 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "license": "MIT", "devDependencies": { "@tc39/ecma262-biblio": "^2.1.2775", - "ecmarkup": "^20.0.0" + "ecmarkup": "^20.0.0", + "jsdom": "^25.0.1" }, "engines": { "node": ">= 12" diff --git a/scripts/insert_warning.mjs b/scripts/insert_warning.mjs new file mode 100644 index 0000000..6c6fc7c --- /dev/null +++ b/scripts/insert_warning.mjs @@ -0,0 +1,73 @@ +import fs from 'node:fs'; +import pathlib from 'node:path'; +import { parseArgs } from 'node:util'; +import { JSDOM, VirtualConsole } from 'jsdom'; + +const { positionals: cliArgs, values: cliOpts } = parseArgs({ + allowPositionals: true, + options: { + strict: { type: 'boolean' }, + }, +}); +if (cliArgs.length < 3) { + const self = pathlib.relative(process.cwd(), process.argv[1]); + console.error(`Usage: node ${self} [--strict] ... + +{{identifier}} substrings in template.html are replaced from data.json, then +the result is inserted at the start of the body element in each file.html.`); + process.exit(64); +} + +const main = async (args, options) => { + const [templateFile, dataFile, ...files] = args; + const { strict } = options; + + // Evaluate the template and parse it into nodes for inserting. + // Everything will be prepended to body elements except metadata elements, + // which will be appended to head elements. + // https://html.spec.whatwg.org/multipage/dom.html#metadata-content-2 + const metadataNames = + 'base, link, meta, noscript, script, style, template, title' + .toUpperCase() + .split(', '); + const template = fs.readFileSync(templateFile, 'utf8'); + const { default: data } = + await import(pathlib.resolve(dataFile), { with: { type: 'json' } }); + const namePatt = /[{][{]([\p{ID_Start}$_][\p{ID_Continue}$]*)[}][}]/gu; + const resolved = template.replaceAll(namePatt, (_, name) => { + if (Object.hasOwn(data, name)) return data[name]; + if (strict) throw Error(`no data for {{${name}}}`); + return ''; + }); + const headInserts = [], bodyInserts = []; + let insertDom = JSDOM.fragment(resolved); + for (const node of insertDom.childNodes) { + if (metadataNames.includes(node.nodeName)) headInserts.push(node); + else bodyInserts.push(node); + } + + // Perform the insertions, suppressing JSDOM warnings from e.g. unsupported + // CSS features. + const virtualConsole = new VirtualConsole(); + virtualConsole.on('error', () => {}); + const jsdomOpts = { contentType: 'text/html; charset=utf-8', virtualConsole }; + const getInserts = + files.length > 1 ? nodes => nodes.map(n => n.cloneNode(true)) : x => x; + const results = await Promise.allSettled(files.map(async file => { + let dom = await JSDOM.fromFile(file, jsdomOpts); + const { head, body } = dom.window.document; + if (headInserts.length > 0) head.append(...getInserts(headInserts)); + if (bodyInserts.length > 0) body.prepend(...getInserts(bodyInserts)); + fs.writeFileSync(file, dom.serialize(), 'utf8'); + })); + + const failures = results.flatMap(result => + result.status === 'fulfilled' ? [] : [result.reason], + ); + if (failures.length > 0) throw AggregateError(failures); +}; + +main(cliArgs, cliOpts).catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/pr_preview_warning.html b/scripts/pr_preview_warning.html new file mode 100644 index 0000000..92b7c04 --- /dev/null +++ b/scripts/pr_preview_warning.html @@ -0,0 +1,58 @@ + + +
+ {{SUMMARY}} +

+ This document is a preview of merging {{PR_LINK}}, resulting in {{COMMIT_LINK}}. +

+

+ Do not reference it as authoritative in any way. + Instead, see {{REPO_LINK}} for the living specification. +

+