258 lines
6.4 KiB
JavaScript
258 lines
6.4 KiB
JavaScript
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();
|