const fs = require('fs/promises'); const path = require('path'); const readline = require('readline'); const { spawn } = require('child_process'); const os = require('os'); const CONTENT_DIR = path.resolve('content'); const DIAGRAMS_DIR = 'diagrams'; const OUTPUT_DIR = 'images'; const MERMAID_EXTENSION = '.mermaid'; function askQuestion(query) { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); return new Promise(resolve => rl.question(query, answer => { rl.close(); resolve(answer.trim()); })); } async function findLatestBundle(dir) { let latest = { path: null, time: 0 }; async function search(current) { const entries = await fs.readdir(current, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(current, entry.name); if (entry.isDirectory()) { const hasIndex = (await fs.readdir(fullPath)).includes('index.md'); if (hasIndex) { const stat = await fs.stat(fullPath); if (stat.mtimeMs > latest.time) { latest = { path: fullPath, time: stat.mtimeMs }; } } else { await search(fullPath); } } } } await search(dir); return latest.path; } async function directoryExists(dirPath) { try { const stat = await fs.stat(dirPath); return stat.isDirectory(); } catch (error) { if (error.code === 'ENOENT') { return false; } throw error; } } async function collectMermaidFiles(root) { async function walk(current) { const entries = await fs.readdir(current, { withFileTypes: true }); const files = []; for (const entry of entries) { const entryPath = path.join(current, entry.name); if (entry.isDirectory()) { files.push(...await walk(entryPath)); } else if (entry.isFile() && path.extname(entry.name) === MERMAID_EXTENSION) { files.push(entryPath); } } return files; } return walk(root); } function getMermaidCliPath() { const cliName = process.platform === 'win32' ? 'mmdc.cmd' : 'mmdc'; return path.resolve('node_modules', '.bin', cliName); } let cachedPuppeteerExecutablePath; let cachedPuppeteerConfigPath; function resolvePuppeteerExecutablePath() { if (cachedPuppeteerExecutablePath !== undefined) { return cachedPuppeteerExecutablePath; } const candidates = [ () => require('puppeteer'), () => require('@mermaid-js/mermaid-cli/node_modules/puppeteer') ]; for (const load of candidates) { try { const module = load(); const executablePath = typeof module.executablePath === 'function' ? module.executablePath() : module.executablePath; if (executablePath) { cachedPuppeteerExecutablePath = executablePath; return cachedPuppeteerExecutablePath; } } catch (error) { if (error.code !== 'MODULE_NOT_FOUND') { cachedPuppeteerExecutablePath = null; throw error; } } } cachedPuppeteerExecutablePath = null; return cachedPuppeteerExecutablePath; } async function ensurePuppeteerConfig() { if (cachedPuppeteerConfigPath) { return cachedPuppeteerConfigPath; } const executablePath = resolvePuppeteerExecutablePath(); const config = { headless: 'new', args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'], timeout: 60000 }; if (executablePath) { config.executablePath = executablePath; } const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'mmdc-')); const filePath = path.join(dir, 'puppeteer-config.json'); await fs.writeFile(filePath, JSON.stringify(config, null, 2), 'utf8'); cachedPuppeteerConfigPath = filePath; return cachedPuppeteerConfigPath; } async function renderMermaidDiagram(inputPath, outputPath) { const cliPath = getMermaidCliPath(); const args = ['-i', inputPath, '-o', outputPath, '-e', 'png']; const env = { ...process.env }; const executablePath = resolvePuppeteerExecutablePath(); if (executablePath) { env.PUPPETEER_EXECUTABLE_PATH = executablePath; } const puppeteerConfig = await ensurePuppeteerConfig(); if (puppeteerConfig) { args.push('-p', puppeteerConfig); } return new Promise((resolve, reject) => { const child = spawn(cliPath, args, { stdio: 'inherit', env }); child.on('error', reject); child.on('close', code => { if (code === 0) { resolve(); } else { reject(new Error(`mmdc exited with code ${code}`)); } }); }); } async function ensureDirectory(dirPath) { await fs.mkdir(dirPath, { recursive: true }); } async function generateDiagrams(bundlePath) { const diagramsRoot = path.join(bundlePath, DIAGRAMS_DIR); if (!(await directoryExists(diagramsRoot))) { console.log(`No "${DIAGRAMS_DIR}" directory found in ${bundlePath}. Nothing to generate.`); return; } const mermaidFiles = await collectMermaidFiles(diagramsRoot); if (mermaidFiles.length === 0) { console.log(`No Mermaid diagrams found in ${diagramsRoot}. Nothing to generate.`); return; } for (const mermaidFile of mermaidFiles) { const contents = await fs.readFile(mermaidFile, 'utf8'); if (!contents.trim()) { console.log(`Skipped: ${mermaidFile} (file is empty)`); continue; } const relativePath = path.relative(diagramsRoot, mermaidFile); const outputRelative = relativePath.replace(/\.mermaid$/i, '.png'); const outputPath = path.join(bundlePath, OUTPUT_DIR, outputRelative); await ensureDirectory(path.dirname(outputPath)); console.log(`Rendering ${mermaidFile} -> ${outputPath}`); await renderMermaidDiagram(mermaidFile, outputPath); console.log(`Generated: ${outputPath}`); } } async function main() { const manualPath = process.argv[2]; let bundle; if (manualPath) { bundle = path.resolve(manualPath); } else { const latest = await findLatestBundle(CONTENT_DIR); if (!latest) { console.error('No bundle found in content/.'); return; } const confirm = await askQuestion(`Use latest bundle found: ${latest}? (Y/n) `); if (confirm.toLowerCase() === 'n') { const inputPath = await askQuestion('Enter the relative path to your bundle: '); bundle = path.resolve(inputPath); } else { bundle = latest; } } try { await generateDiagrams(bundle); } catch (error) { console.error('Failed to generate Mermaid diagrams.'); console.error(error); process.exitCode = 1; } } main();