UNPKG

userpravah

Version:

UserPravah is an extensible, framework-agnostic tool for analyzing user flows and navigation patterns in web applications. It supports multiple frameworks (Angular, React) and output formats (DOT/Graphviz, JSON) with a plugin-based architecture for easy e

136 lines (135 loc) 6.08 kB
import { digraph, toDot, attribute } from 'ts-graphviz'; import * as fs from 'fs'; import * as path from 'path'; import { execSync } from 'child_process'; export class DotOutputFormatter { constructor() { this.formatName = 'dot'; } determineNodeColor(category, isRoot) { if (isRoot) return '#FF8C00'; // Orange for Root if (!category) return '#E8E8E8'; // Default light grey const stringToHexColor = (str) => { let hash = 0; for (let i = 0; i < str.length; i++) { hash = str.charCodeAt(i) + ((hash << 5) - hash); } let color = '#'; for (let i = 0; i < 3; i++) { const value = ((hash >> (i * 8)) & 0xFF); const brightValue = Math.max(value, 160); color += ('00' + brightValue.toString(16)).slice(-2); } return color; }; if (category === 'root') return '#FF8C00'; if (category === 'auth') return '#ADD8E6'; return stringToHexColor(category); } async generateOutput(analysisResult, outputDirectory) { const { routes, flows } = analysisResult; console.log('[DotFormatter] Starting DOT graph generation...'); const g = digraph('UserFlows', { rankdir: 'LR', splines: 'polyline', nodesep: 1.5, ranksep: 2.2, overlap: 'false', concentrate: false, labelloc: 't', label: `Application Flow Diagram - ${new Date().toLocaleDateString()}`, fontsize: 16, }); g.attributes.node.set(attribute.shape, 'box'); g.attributes.node.set(attribute.style, 'filled,rounded'); g.attributes.node.set(attribute.fontname, 'Arial'); g.attributes.node.set(attribute.fontsize, 10); g.attributes.node.set(attribute.margin, "0.15,0.1"); g.attributes.node.set(attribute.height, 0.5); g.attributes.edge.set(attribute.fontname, 'Arial'); g.attributes.edge.set(attribute.fontsize, 8); const existingEdges = new Set(); for (const routeNode of routes) { if (!routeNode.id) { console.warn(`[DotFormatter] Skipping route node with no ID: ${routeNode.displayName}`); continue; } const nodeAttrs = { label: `${routeNode.displayName}\n(${routeNode.originalPath.replace(/"/g, '\\"')})${routeNode.guards ? '\nGuards: ' + routeNode.guards.join(', ') : ''}`, fillcolor: this.determineNodeColor(routeNode.category, routeNode.isRoot), fontcolor: '#333333', }; if (routeNode.component && !routeNode.displayName.includes(routeNode.component.replace(/Component$/, ''))) { nodeAttrs.label += `\nComp: ${routeNode.component}`; } g.createNode(routeNode.id, nodeAttrs); } for (const flowEdge of flows) { if (!g.getNode(flowEdge.sourceNodeId)) { console.warn(`[DotFormatter] Source node ID "${flowEdge.sourceNodeId}" not found. Skipping edge.`); continue; } if (!g.getNode(flowEdge.targetNodeId)) { console.warn(`[DotFormatter] Target node ID "${flowEdge.targetNodeId}" not found. Skipping edge.`); continue; } const edgeKey = `${flowEdge.sourceNodeId}->${flowEdge.targetNodeId}->${flowEdge.type}${flowEdge.label || ''}`; if (existingEdges.has(edgeKey)) continue; const edgeAttrs = { label: flowEdge.label || '' }; switch (flowEdge.type) { case 'static': edgeAttrs.color = '#4682B4'; break; case 'dynamic': edgeAttrs.color = '#006400'; if (flowEdge.condition) edgeAttrs.label = flowEdge.condition; break; case 'redirect': edgeAttrs.style = 'dashed'; edgeAttrs.color = 'blue'; edgeAttrs.label = flowEdge.label || 'Redirect'; break; case 'hierarchy': edgeAttrs.color = '#A9A9A9'; edgeAttrs.arrowhead = 'none'; edgeAttrs.style = 'dotted'; break; case 'guard': edgeAttrs.color = '#FF8C00'; edgeAttrs.style = 'bold'; edgeAttrs.label = flowEdge.condition || 'Guard'; break; default: edgeAttrs.color = '#333333'; } g.createEdge([flowEdge.sourceNodeId, flowEdge.targetNodeId], edgeAttrs); existingEdges.add(edgeKey); } if (!fs.existsSync(outputDirectory)) { fs.mkdirSync(outputDirectory, { recursive: true }); } const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const baseFileName = `user_flows_${path.basename(path.resolve(analysisResult.routes.find(r => r.isRoot)?.originalPath || 'unknown_project'))}_${timestamp}`; const dotPath = path.join(outputDirectory, `${baseFileName}.dot`); const pngPath = path.join(outputDirectory, `${baseFileName}.png`); try { fs.writeFileSync(dotPath, toDot(g)); console.log(`[DotFormatter] DOT file written to: ${dotPath}`); try { execSync(`dot -Tpng "${dotPath}" -o "${pngPath}"`); console.log(`[DotFormatter] PNG file generated: ${pngPath}`); } catch (error) { console.error(`[DotFormatter] Error generating PNG (Graphviz 'dot' command may not be installed or in PATH): ${error.message}`); } } catch (error) { console.error(`[DotFormatter] Error writing DOT file: ${error.message}`); } } }