import * as fs from "node:fs"; import * as path from "node:path"; interface FlowStep { id: string; label: string; major: number; level: number; file: string; // New: Track file path line: number; // New: Track line number } interface FlowMap { [flowId: string]: FlowStep[]; } /** CONFIGURATION **/ const SRC_DIR = "./src"; const OUTPUT_DIR = "./docs/flows"; const FILE_EXTENSIONS = [".ts", ".js", ".tsx", ".jsx"]; // Updated Regex: Handles //FLOWS and // FLOWS const FLOW_REGEX = /\/\/\s*FLOWS:\s*([^|]+)\s*\|\s*([\w.]+)\s*\|\s*(.*)/g; /** UTILS **/ function getFiles(dir: string, allFiles: string[] = []): string[] { if (!fs.existsSync(dir)) return allFiles; const files = fs.readdirSync(dir); for (const file of files) { const name = path.join(dir, file); if (fs.statSync(name).isDirectory()) getFiles(name, allFiles); else if (FILE_EXTENSIONS.includes(path.extname(name))) allFiles.push(name); } return allFiles; } function safeId(id: string): string { // Mermaid IDs cannot contain dots return `step_${id.replace(/\./g, "_")}`; } /** CORE LOGIC **/ function main() { const flowData: FlowMap = {}; const files = getFiles(SRC_DIR); console.log(`🔍 Scanning ${files.length} files for // FLOWS comments...`); // 1. Extraction files.forEach((file) => { const content = fs.readFileSync(file, "utf-8"); const lines = content.split("\n"); lines.forEach((lineText, index) => { let match: RegExpExecArray | null; FLOW_REGEX.lastIndex = 0; match = FLOW_REGEX.exec(lineText); while (match !== null) { const [_, flowId, id, label] = match.map((s) => s.trim()); if (!flowId || !id || !label) { console.warn(`⚠️ Invalid FLOWS comment in ${file}: ${match[0]}`); continue; } if (!flowData[flowId]) flowData[flowId] = []; flowData[flowId].push({ id, label, major: parseInt(id.split(".")?.[0] || "0"), level: id.split(".").length, file: path.relative(process.cwd(), file), // Store relative path line: index + 1, // Store line number (1-based) }); match = FLOW_REGEX.exec(lineText); } }); }); if (Object.keys(flowData).length === 0) { console.log( "⚠️ No flow comments found. Try adding: // FLOWS: myflow | 1 | Start", ); return; } // 2. Generate Output if (!fs.existsSync(OUTPUT_DIR)) fs.mkdirSync(OUTPUT_DIR, { recursive: true }); for (const [flowId, steps] of Object.entries(flowData)) { // Sort logically (1, 1.1, 1.2, 2) const sorted = steps.sort((a, b) => a.id.localeCompare(b.id, undefined, { numeric: true }), ); const mermaid = ["graph TD"]; const connections = new Set(); sorted.forEach((current) => { const nextSteps = sorted.filter((candidate) => { const currentIdPrefix = `${current.id}.`; // 1. DESCEND: Link to immediate children (3 -> 3.1, 3 -> 3.2) // Check if candidate starts with current ID and is exactly one level deeper if ( candidate.id.startsWith(currentIdPrefix) && candidate.level === current.level + 1 ) { // Ensure it's a direct child (e.g., 3 -> 3.1, not 3 -> 3.1.1) const remaining = candidate.id.replace(currentIdPrefix, ""); return !remaining.includes("."); } // 2. MERGE: Link leaf nodes to the next major integer (3.1.2 -> 4, 3.2.1 -> 4) const isLeaf = !sorted.some( (s) => s.id.startsWith(currentIdPrefix) && s.id !== current.id, ); if ( isLeaf && candidate.major === current.major + 1 && candidate.level === 1 ) { return true; } // 3. SEQUENCE (Optional): If you want 3.1.1 -> 3.1.2 // If they share the exact same parent and are consecutive const currentParent = current.id.includes(".") ? current.id.substring(0, current.id.lastIndexOf(".")) : null; const candParent = candidate.id.includes(".") ? candidate.id.substring(0, candidate.id.lastIndexOf(".")) : null; if ( currentParent !== null && currentParent === candParent && candidate.level === current.level ) { // Only link if you want branches to be sequential. // In your case, you likely want 3.1.1 and 3.1.2 to both go to 4 independently. // So we leave this FALSE to keep them as parallel paths. return false; } return false; }); nextSteps.forEach((next) => { const link = ` ${safeId(current.id)}["${current.label}"] --> ${safeId(next.id)}["${next.label}"]`; connections.add(link); }); }); // Handle single-node flows (start node with no connections) if (connections.size === 0 && sorted.length > 0 && sorted[0]) { mermaid.push(` ${safeId(sorted[0].id)}["${sorted[0].label}"]`); } else { connections.forEach((c) => mermaid.push(c)); } const outPath = path.join(OUTPUT_DIR, `${flowId}.md`); const header = "```mermaid\n\n"; const footer = "\n```"; fs.writeFileSync(outPath, header + mermaid.join("\n") + footer); console.log(`✅ Generated: ${outPath}`); } } main();