storebound/tools/diagrams.ts
CRT 710c3a53d0 README, docs and site: one story (the whole job, no avoidable rejections, the desktop app, your AI, install)
README is a short front page (about 120 lines) with one real visual per section; every former section moved
unchanged into docs/*.md with a docs/README.md index (journey, rejections, getting-started, commands, growth,
studios, store-art, listing-languages, api, skills, mcp, safety, faq, project). The site tells the same story on one
page: the teaser in the hero, real CLI output as text, the desktop app as a section with light/dark captures of eight
screens and the per-system downloads (desktop/latest.json); /desktop/ redirects to /#download. New capture tools:
tools/site-desktop-shots.ts (fixture-mode desktop frames) and tools/site-term-shots.ts (real CLI output as text and
WebP). tests/docs-links.test.ts checks README, docs and site links resolve.
2026-10-13 03:29:59 +08:00

66 lines
4.4 KiB
TypeScript

#!/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 (docs/journey.md), 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 <img width height> 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(/^<svg([^>]*)>/, (_m, a: string) => `<svg${a.replace(/ role="[^"]*"/, '').replace('width="100%"', `width="${Math.ceil(w!)}" height="${Math.ceil(h!)}"`).replace(/max-width: [\d.]+px; ?/, '')} role="img"><title>storebound · the journey from zero to Submit for review</title>`);
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 }); }