Something went wrong. Try again.
A proposal for resolving a short, human-readable name of the form @handle/name into loaded, attributable web content, using AT Protocol identity as the namespace root atproto-web-names.szzt.app
Something went wrong. Try again.
7.1 kB · 129 lines
JavaScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130import { readFile, writeFile, mkdir, rm, copyFile } from "node:fs/promises";import { createHash } from "node:crypto";import { resolve, extname } from "node:path";import { execFile } from "node:child_process";import { promisify } from "node:util";import { load } from "cheerio";import { convert } from "./convert.mjs";import { configureBrowser } from "./browser.mjs";import { serve } from "./server.mjs";
const root = resolve(import.meta.dirname, "..");process.chdir(root);const exec = promisify(execFile);const publication = JSON.parse(await readFile("publication.json", "utf8"));if (typeof publication.preview !== "boolean") throw new Error("publication.preview must explicitly select preview or publication mode.");const source = await readFile("spec.md", "utf8");const { html, diagrams, report } = convert(source, publication);await rm("dist", { recursive: true, force: true });await mkdir("dist/assets", { recursive: true });await mkdir(".build/cache", { recursive: true });await copyFile("styles/spec.css", "dist/assets/spec.css");await copyFile("static/favicon.svg", "dist/assets/favicon.svg");await copyFile("spec.md", "dist/spec.md");await writeFile(".build/source.html", html);const browserPath = await configureBrowser();await writeFile(".build/puppeteer.json", JSON.stringify({ headless: true, args: ["--no-sandbox"], ...(browserPath ? { executablePath: browserPath } : {}) }));await writeFile(".build/mermaid.json", JSON.stringify({ theme: "neutral", flowchart: { htmlLabels: false }, fontFamily: "Arial, sans-serif" }));for (const diagram of diagrams) { await writeFile(`.build/${diagram.name}.mmd`, diagram.source); await exec(process.execPath, ["node_modules/@mermaid-js/mermaid-cli/src/cli.js", "-i", `.build/${diagram.name}.mmd`, "-o", `dist/assets/${diagram.name}.svg`, "-p", ".build/puppeteer.json", "-c", ".build/mermaid.json", "-b", "transparent"], { maxBuffer: 2 ** 20 });}const server = await serve(".build", { mounts: { "/node_modules/respec/": resolve("node_modules/respec"), "/assets/": resolve("dist/assets"),} });let rendered;try { const { toHTML } = await import("respec"); rendered = await toHTML(new URL("source.html", server.url).href, { timeout: 120_000, // Renders this repo's own source; the sandbox cannot start on hosts that // restrict unprivileged user namespaces. disableSandbox: true, onError: error => console.error("ReSpec error:", error.message), onWarning: warning => console.warn("ReSpec warning:", warning.message), });} finally { await server.close(); }if (rendered.errors.length) throw new Error(`ReSpec reported ${rendered.errors.length} error(s).`);report.respecWarnings = rendered.warnings.map(w => w.message);await writeFile(".build/respec.html", rendered.html);
// Cache build-time upstream resources, and ship all actual rendering dependencies locally.const downloads = new Map();async function download(url) { const key = createHash("sha256").update(url).digest("hex"); const file = `.build/cache/${key}`; try { return await readFile(file); } catch {} const response = await fetch(url, { signal: AbortSignal.timeout(30_000) }); if (!response.ok) throw new Error(`Download failed: ${response.status} ${url}`); const bytes = Buffer.from(await response.arrayBuffer()); await writeFile(file, bytes); return bytes;}async function localize(url, css = false) { if (downloads.has(url)) return downloads.get(url); const task = (async () => { const key = createHash("sha256").update(url).digest("hex").slice(0, 16); const extension = css ? ".css" : extname(new URL(url).pathname) || ".bin"; const name = `upstream-${key}${extension}`; let bytes = await download(url); if (css) { let text = bytes.toString(); const pattern = /url\(\s*(?:"([^"]*)"|'([^']*)'|([^\s)]*))\s*\)/g; const matches = [...text.matchAll(pattern)]; for (const match of matches.reverse()) { const target = match[1] ?? match[2] ?? match[3]; if (!target || target.startsWith("data:") || target.startsWith("#")) continue; const absolute = new URL(target, url).href; const path = await localize(absolute, /\.css(?:[?#]|$)/.test(absolute)); text = text.slice(0, match.index) + `url("${path}")` + text.slice(match.index + match[0].length); } const imports = [...text.matchAll(/@import\s+["']([^"']+)["']/g)]; for (const match of imports.reverse()) { const path = await localize(new URL(match[1], url).href, true); text = text.slice(0, match.index) + `@import url("${path}")` + text.slice(match.index + match[0].length); } bytes = Buffer.from(text); } await writeFile(`dist/assets/${name}`, bytes); return name; })(); downloads.set(url, task); return task;}const $ = load(rendered.html);$("head").append('<link rel="icon" href="assets/favicon.svg" type="image/svg+xml">');// No authoring engine, analytics, remote executable, or JS needed to read the document.$("script, link[rel=preconnect], link[rel=dns-prefetch], link[rel=preload], link[rel=modulepreload]").remove();// ReSpec's conformance plugin inserts an h1 even in an unnumbered front-matter section.$("#conformance > h1").replaceWith("<h2>Conformance</h2>");$("link[rel=stylesheet]").each((_, el) => { if (!$(el).attr("href")) $(el).remove();});for (const el of $("link[rel=stylesheet]").toArray()) { const href = $(el).attr("href"); if (/^https?:/.test(href)) $(el).attr("href", `assets/${await localize(href, true)}`); else if (/^\//.test(href)) throw new Error(`Unexpected absolute stylesheet path: ${href}`);}$('link[rel=stylesheet][href="assets/spec.css"]').appendTo("head");$('meta[name="color-scheme"]').attr("content", "light");for (const el of $("img[src]").toArray()) { const src = $(el).attr("src"); if (/^https?:/.test(src)) $(el).attr("src", `assets/${await localize(src)}`);}$("table").each((_, table) => { $(table).wrap('<div class="table-scroll" tabindex="0" role="region" aria-label="Scrollable specification table"></div>'); });// Match prose reference labels against ReSpec's actual numbering, rather than trusting our arithmetic.for (const el of $("a.source-section-ref").toArray()) { const id = $(el).attr("href").slice(1); const actual = $(`#${id}`).find(".secno").first().text().trim().replace(/\.$/, ""); if (!actual) throw new Error(`No generated section number for ${id}`); $(el).text(actual);}report.externalAssets = [...downloads.keys()];await writeFile("dist/index.html", $.html());await writeFile("dist/build-report.json", JSON.stringify(report, null, 2) + "\n");console.log(`Built dist/index.html: ${report.sectionCount} sections, ${report.definitionCount} definitions, ${diagrams.length} diagram(s).`);console.log(`Unresolved source references: ${report.unresolvedSourceReferences.join(", ") || "none"}.`);console.log(`Publication placeholders: ${report.publicationPlaceholders.join(", ") || "none"}.`);console.log(`${publication.preview ? "Local preview" : "Publication"} build. Nothing uploaded by this build.`);