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
JavaScript
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}`);
}
}
}