Génération de diagrammes mermaid server-side
This commit is contained in:
1909
package-lock.json
generated
1909
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@
|
||||
"user-agents": "^1.1.480"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mermaid-js/mermaid-cli": "^10.9.1",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"postcss": "^8.5.3",
|
||||
"postcss-cli": "^11.0.1"
|
||||
|
||||
257
tools/generate_mermaid_diagrams.js
Normal file
257
tools/generate_mermaid_diagrams.js
Normal file
@@ -0,0 +1,257 @@
|
||||
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];
|
||||
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, '.svg');
|
||||
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();
|
||||
Reference in New Issue
Block a user