Skip to content

Commit 969b66e

Browse files
committed
Add dot as a dependency for graphs, remove Mermaid.
1 parent 83204e6 commit 969b66e

34 files changed

Lines changed: 1298 additions & 2544 deletions

builder/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,9 @@ the architecture overview.
8484
| 7 | [offline.mjs](offline.mjs) | Mirror to `_site-offline/` with `file://` URL rewrites |
8585
| 8 | [pdf.mjs](pdf.mjs) + [book.mjs](book.mjs) (renderer half) | Sparse `_site-pdf/` tree (book.html + CSS + images) |
8686

87-
A pre-step ([mermaid.mjs](mermaid.mjs)) regenerates stale
88-
`docs/assets/images/mmd/*.svg` from their `.mmd` sources before
89-
discover walks the tree.
87+
A seed task ([dot.mjs](dot.mjs)) regenerates stale
88+
`docs/assets/images/dot/*.svg` from their `.dot` sources via the WASM
89+
build of Graphviz, concurrently with discover.
9090

9191
## Verification
9292

builder/cpu-worker.mjs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { promises as fsP } from "node:fs";
77
import path from "node:path";
88
import { parentPort, workerData } from "node:worker_threads";
99
import { compileLightScss, compileDarkScss } from "./scss.mjs";
10-
import { regenerateMermaid } from "./mermaid.mjs";
10+
import { regenerateDot } from "./dot.mjs";
1111
import { captureBuildInfo } from "./build-info.mjs";
1212

1313
import { createMarkdownIt, renderPhase } from "./render.mjs";
@@ -110,9 +110,9 @@ const handlers = {
110110
return { scssDarkResult };
111111
},
112112

113-
async mermaid() {
114-
const mermaidStats = await regenerateMermaid(ctx.srcRoot);
115-
return { mermaidStats };
113+
async dot() {
114+
const dotStats = await regenerateDot(ctx.srcRoot);
115+
return { dotStats };
116116
},
117117

118118
async buildInfo() {

builder/dot.mjs

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Graphviz/DOT preprocessor: regenerates
2+
// `<srcRoot>/assets/images/dot/*.svg` from the matching `*.dot` source
3+
// when the SVG is missing or older than its source. Runs as a seed task
4+
// concurrently with the rest of the build so the freshly-emitted SVGs
5+
// land in dispatch's site-paths set and the static-file copy pass.
6+
//
7+
// Idempotent: a second build with no source changes is a no-op (mtime
8+
// check). The `.dot` is the canonical source; the SVG is a build
9+
// artifact -- editing the .dot by one character regenerates the SVG on
10+
// the next build.
11+
//
12+
// Drives `@hpcc-js/wasm-graphviz` directly -- a WebAssembly build of
13+
// Graphviz. No puppeteer, no headless Chromium, no in-tree patches.
14+
// `Graphviz.load()` initialises the WASM module once per build (~50 ms);
15+
// `gv.dot(src)` is synchronous after that.
16+
//
17+
// Failure modes split into two:
18+
// - SETUP (@hpcc-js/wasm-graphviz not installed): warn + leave on-disk
19+
// SVGs intact + return early with setupSkipped: true. The
20+
// orchestrator does NOT flip the exit code so a fresh checkout
21+
// without `npm install` still builds against the previous SVGs.
22+
// - CONTENT (one .dot has a syntax error, gv.dot throws): warn + keep
23+
// that diagram's old SVG + continue the rest of the batch. The
24+
// orchestrator (tbdocs.mjs) flips process.exitCode = 1 on the
25+
// returned `failed` count so a broken diagram surfaces in CI.
26+
27+
import { promises as fs } from "node:fs";
28+
import path from "node:path";
29+
30+
const DOT_REL_DIR = path.join("assets", "images", "dot");
31+
32+
export async function regenerateDot(srcRoot) {
33+
const dotRoot = path.join(srcRoot, DOT_REL_DIR);
34+
const sources = await listDotSources(dotRoot);
35+
if (sources.length === 0) {
36+
return { processed: 0, regenerated: 0, svgFiles: [] };
37+
}
38+
39+
const stale = [];
40+
for (const src of sources) {
41+
const svg = svgFor(src);
42+
if (!(await isUpToDate(svg, src))) stale.push({ src, svg });
43+
}
44+
if (stale.length === 0) {
45+
return { processed: sources.length, regenerated: 0,
46+
svgFiles: await statSvgFiles(sources, srcRoot) };
47+
}
48+
49+
let Graphviz;
50+
try {
51+
({ Graphviz } = await import("@hpcc-js/wasm-graphviz"));
52+
} catch (err) {
53+
console.warn(
54+
`dot: skipped batch (${explainLoadFailure(err)}); existing SVGs retained`,
55+
);
56+
return { processed: sources.length, regenerated: 0, failed: 0, setupSkipped: true,
57+
svgFiles: await statSvgFiles(sources, srcRoot) };
58+
}
59+
60+
let gv;
61+
try {
62+
gv = await Graphviz.load();
63+
} catch (err) {
64+
console.warn(
65+
`dot: skipped batch (WASM load failed: ${err.message}); existing SVGs retained`,
66+
);
67+
return { processed: sources.length, regenerated: 0, failed: 0, setupSkipped: true,
68+
svgFiles: await statSvgFiles(sources, srcRoot) };
69+
}
70+
71+
let regenerated = 0;
72+
let failed = 0;
73+
for (const { src, svg } of stale) {
74+
try {
75+
const source = await fs.readFile(src, "utf8");
76+
const svgXml = gv.dot(source);
77+
await fs.writeFile(svg, svgXml, "utf8");
78+
regenerated++;
79+
} catch (err) {
80+
console.warn(
81+
`dot: skipped ${path.basename(src)} (${err.message}); existing SVG retained`,
82+
);
83+
failed++;
84+
}
85+
}
86+
return { processed: sources.length, regenerated, failed,
87+
svgFiles: await statSvgFiles(sources, srcRoot) };
88+
}
89+
90+
async function statSvgFiles(sources, srcRoot) {
91+
const results = [];
92+
for (const src of sources) {
93+
const svgPath = svgFor(src);
94+
try {
95+
const stat = await fs.stat(svgPath);
96+
const srcRel = path.relative(srcRoot, svgPath).replace(/\\/g, "/");
97+
results.push({ srcPath: svgPath, srcRel, destRel: srcRel, size: stat.size });
98+
} catch {
99+
// SVG not on disk (render failed or never generated); skip.
100+
}
101+
}
102+
return results;
103+
}
104+
105+
async function listDotSources(dotRoot) {
106+
try {
107+
const entries = await fs.readdir(dotRoot);
108+
return entries
109+
.filter((n) => n.endsWith(".dot"))
110+
.map((n) => path.join(dotRoot, n));
111+
} catch (err) {
112+
if (err.code === "ENOENT") return [];
113+
throw err;
114+
}
115+
}
116+
117+
function svgFor(src) {
118+
return src.replace(/\.dot$/, ".svg");
119+
}
120+
121+
async function isUpToDate(svg, src) {
122+
try {
123+
const [srcStat, svgStat] = await Promise.all([
124+
fs.stat(src),
125+
fs.stat(svg),
126+
]);
127+
return svgStat.mtimeMs >= srcStat.mtimeMs;
128+
} catch {
129+
return false;
130+
}
131+
}
132+
133+
function explainLoadFailure(err) {
134+
const msg = err?.message ?? String(err);
135+
if (/cannot find module ['"]@hpcc-js\/wasm-graphviz|cannot find package ['"]@hpcc-js\/wasm-graphviz/i.test(msg)) {
136+
return "@hpcc-js/wasm-graphviz not installed; run `npm install`";
137+
}
138+
return msg;
139+
}

0 commit comments

Comments
 (0)