#!/usr/bin/env bun // Renders the diagrams in docs/diagrams/*.mmd with the Mermaid CLI (@mermaid-js/mermaid-cli, a devDependency; never // shipped). Plain SVG text (no HTML labels), a fixed dark canvas so it reads the same on light and dark pages. // bun tools/diagrams.ts // journey.mmd -> docs/img/journey.svg (README), site/img/journey.svg (wide screens) and site/img/journey-tall.svg // (phones: every row turned into a column). Chrome: STOREBOUND_CHROME, Playwright's Chromium, or Google Chrome. import { copyFileSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; const root = new URL('..', import.meta.url).pathname; const BG = '#0f172a'; function chrome(): string { const cands = [process.env.STOREBOUND_CHROME ?? '']; try { cands.push((require('playwright') as typeof import('playwright')).chromium.executablePath()); } catch { /* not installed */ } cands.push('/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', '/usr/bin/google-chrome', '/usr/bin/chromium', 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'); const home = process.env.HOME ?? ''; // a newer Playwright Chromium than the installed playwright expects is fine for Mermaid for (const base of [join(home, 'Library/Caches/ms-playwright'), join(home, '.cache/ms-playwright')]) { if (!existsSync(base)) continue; for (const d of Array.from(new Bun.Glob('chromium-*/chrome-*/**/{Google Chrome for Testing,chrome}').scanSync({ cwd: base, onlyFiles: true }))) cands.push(join(base, d)); } const hit = cands.find(c => c && existsSync(c)); if (!hit) throw new Error('diagrams: no Chrome found (set STOREBOUND_CHROME or run `bunx playwright install chromium`)'); return hit; } const mermaidConfig = { theme: 'base', htmlLabels: false, flowchart: { htmlLabels: false, curve: 'basis', nodeSpacing: 28, rankSpacing: 36, padding: 14, wrappingWidth: 400 }, themeVariables: { fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif', fontSize: '15px', background: BG, primaryColor: '#1e293b', primaryTextColor: '#e2e8f0', primaryBorderColor: '#475569', lineColor: '#94a3b8', textColor: '#e2e8f0', titleColor: '#f8fafc', clusterBkg: '#111a2e', clusterBorder: '#334155', }, }; function render(src: string, out: string, tmp: string, exe: string) { const input = join(tmp, 'in.mmd'); writeFileSync(input, src); writeFileSync(join(tmp, 'mermaid.json'), JSON.stringify(mermaidConfig)); writeFileSync(join(tmp, 'puppeteer.json'), JSON.stringify({ executablePath: exe, args: ['--no-sandbox'] })); const r = Bun.spawnSync(['node', join(root, 'node_modules/@mermaid-js/mermaid-cli/src/cli.js'), '-q', '-i', input, '-o', out, '-c', join(tmp, 'mermaid.json'), '-p', join(tmp, 'puppeteer.json'), '-b', BG], { stdout: 'inherit', stderr: 'inherit' }); if (r.exitCode !== 0) throw new Error(`diagrams: mmdc failed for ${out}`); // a self-describing image: title for screen readers, the dark canvas filled in (some viewers drop the background style) let svg = readFileSync(out, 'utf8'); // Mermaid embeds its default web font; the diagram uses the system font stack instead, so drop the font files svg = svg.replace(/@font-face\s*\{[^}]*\}/g, ''); // a fixed intrinsic size (from the viewBox) so and README renderers keep the aspect ratio const [, , w, h] = (svg.match(/viewBox="([^"]+)"/)?.[1] ?? '0 0 960 560').split(/\s+/).map(Number); svg = svg.replace(/^]*)>/, (_m, a: string) => `storebound ยท the journey from zero to Submit for review`); writeFileSync(out, svg); console.log(`diagrams: ${out.replace(root, '')} (${(svg.length / 1024).toFixed(1)} KB)`); } const exe = chrome(); const tmp = mkdtempSync(join(tmpdir(), 'sb-diagrams-')); try { const journey = readFileSync(join(root, 'docs/diagrams/journey.mmd'), 'utf8'); render(journey, join(root, 'docs/img/journey.svg'), tmp, exe); copyFileSync(join(root, 'docs/img/journey.svg'), join(root, 'site/img/journey.svg')); render(journey.replace(/direction LR/g, 'direction TB'), join(root, 'site/img/journey-tall.svg'), tmp, exe); } finally { rmSync(tmp, { recursive: true, force: true }); }