|
| 1 | +import {readFileSync, writeFileSync} from "fs"; |
| 2 | +import {dirname} from "path"; |
| 3 | +import glob from "glob"; |
| 4 | +import mkdirp from "mkdirp"; |
| 5 | + |
| 6 | +// Extract the documentation from the README. |
| 7 | +const readme = readFileSync("./README.md", "utf-8"); |
| 8 | +const docmap = new Map<string, string[]>(); |
| 9 | +let doc: {name: string; lines: string[]} | null = null; |
| 10 | +for (const [i, line] of readme.split("\n").entries()) { |
| 11 | + if (/<!--\s*jsdoc/.test(line)) { |
| 12 | + let match: RegExpExecArray | null; |
| 13 | + if ((match = /^<!--\s+jsdoc\s+(\w+)\s+-->$/.exec(line))) { |
| 14 | + const [, name] = match; |
| 15 | + if (doc) { |
| 16 | + throw new Error(`nested jsdoc directive on line ${i}: ${line}`); |
| 17 | + } |
| 18 | + if (docmap.has(name)) { |
| 19 | + throw new Error(`duplicate jsdoc directive on line ${i}: ${line}`); |
| 20 | + } |
| 21 | + doc = {name, lines: []}; |
| 22 | + } else if ((match = /^<!--\s+jsdocEnd\s+(\w+)\s+-->$/.exec(line))) { |
| 23 | + const [, name] = match; |
| 24 | + if (!doc) { |
| 25 | + throw new Error(`orphaned jsdocEnd directive on line ${i}: ${line}`); |
| 26 | + } |
| 27 | + if (doc.name !== name) { |
| 28 | + throw new Error(`mismatched jsdocEnd ${doc.name} directive on line ${i}: ${line}`); |
| 29 | + } |
| 30 | + docmap.set(doc.name, doc.lines); |
| 31 | + doc = null; |
| 32 | + } else { |
| 33 | + throw new Error(`malformed jsdoc directive on line ${i}: ${line}`); |
| 34 | + } |
| 35 | + } else if (doc) { |
| 36 | + doc.lines.push(line); |
| 37 | + } |
| 38 | +} |
| 39 | + |
| 40 | +// Make relative and anchor links absolute. |
| 41 | +for (const lines of docmap.values()) { |
| 42 | + for (let i = 0, n = lines.length; i < n; ++i) { |
| 43 | + lines[i] = lines[i] |
| 44 | + .replace(/\]\(#([^)]+)\)/g, "](./README.md#$1)") |
| 45 | + .replace(/\]\(\.\/([^)]+)\)/g, "](https://github.com/observablehq/plot/blob/main/$1)"); |
| 46 | + } |
| 47 | +} |
| 48 | + |
| 49 | +// Copy files from build/ to dist/, replacing /** @jsdoc name */ directives. |
| 50 | +const unused = new Set(docmap.keys()); |
| 51 | +for (const file of glob.sync("build/**/*.js")) { |
| 52 | + process.stdout.write(`\x1b[2m${file}\x1b[0m`); |
| 53 | + const lines = readFileSync(file, "utf-8").split("\n"); |
| 54 | + let count = 0; |
| 55 | + for (let i = 0, n = lines.length; i < n; ++i) { |
| 56 | + let match: RegExpExecArray | null; |
| 57 | + if ((match = /^\/\*\*\s+@jsdoc\s+(\w+)\s+\*\/$/.exec(lines[i]))) { |
| 58 | + const [, name] = match; |
| 59 | + const docs = docmap.get(name); |
| 60 | + if (!docs) throw new Error(`missing @jsdoc definition: ${name}`); |
| 61 | + if (!unused.has(name)) throw new Error(`duplicate @jsdoc reference: ${name}`); |
| 62 | + unused.delete(name); |
| 63 | + ++count; |
| 64 | + lines[i] = docs |
| 65 | + .map((line, i, lines) => (i === 0 ? `/** ${line}` : i === lines.length - 1 ? ` * ${line}\n */` : ` * ${line}`)) |
| 66 | + .join("\n"); |
| 67 | + } |
| 68 | + } |
| 69 | + const ofile = file.replace(/^build\//, "dist/"); |
| 70 | + process.stdout.write(` → \x1b[36m${ofile}\x1b[0m${count ? ` (${count} jsdoc${count === 1 ? "" : "s"})` : ""}\n`); |
| 71 | + const odir = dirname(ofile); |
| 72 | + mkdirp.sync(odir); |
| 73 | + writeFileSync(ofile, lines.join("\n"), "utf-8"); |
| 74 | +} |
| 75 | + |
| 76 | +for (const name of unused) { |
| 77 | + console.warn(`\x1b[33m[warning] unused @jsdoc directive:\x1b[0m ${name}`); |
| 78 | +} |
0 commit comments