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
242 lines (241 loc) • 10.3 kB
JavaScript
import * as fs from "fs";
import * as path from "path";
export class FlowAnalyzer {
constructor() {
this.frameworkAnalyzers = new Map();
this.outputGenerators = new Map();
}
/**
* Register a framework analyzer
*/
registerFrameworkAnalyzer(analyzer) {
this.frameworkAnalyzers.set(analyzer.getFrameworkName().toLowerCase(), analyzer);
}
/**
* Register an output generator
*/
registerOutputGenerator(generator) {
this.outputGenerators.set(generator.getFormatName().toLowerCase(), generator);
}
/**
* Get all registered framework names
*/
getAvailableFrameworks() {
return Array.from(this.frameworkAnalyzers.keys());
}
/**
* Get all registered output format names
*/
getAvailableOutputFormats() {
return Array.from(this.outputGenerators.keys());
}
/**
* Recursively search for framework projects up to maxDepth levels deep
*/
async findFrameworkProjectsRecursively(rootPath, maxDepth = 3, currentDepth = 0) {
const results = [];
if (currentDepth > maxDepth) {
return results;
}
// Check current directory for frameworks
for (const [name, analyzer] of this.frameworkAnalyzers) {
if (await analyzer.canAnalyze(rootPath)) {
results.push({ framework: name, path: rootPath });
console.log(`✅ Found ${name} project at: ${rootPath}`);
}
}
// If we found a framework at this level and it's not the root, we can return early
// to avoid finding nested projects within the same framework
if (results.length > 0 && currentDepth > 0) {
return results;
}
// Search subdirectories if we haven't reached max depth
if (currentDepth < maxDepth) {
try {
const entries = fs.readdirSync(rootPath, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && !this.shouldSkipDirectory(entry.name)) {
const subPath = path.join(rootPath, entry.name);
const subResults = await this.findFrameworkProjectsRecursively(subPath, maxDepth, currentDepth + 1);
results.push(...subResults);
}
}
}
catch (error) {
// Ignore directories we can't read
console.warn(`⚠️ Could not read directory: ${rootPath}`);
}
}
return results;
}
/**
* Check if a directory should be skipped during recursive search
*/
shouldSkipDirectory(dirName) {
const skipDirs = [
'node_modules',
'.git',
'dist',
'build',
'.next',
'coverage',
'.nyc_output',
'tmp',
'temp',
'.cache',
'.vscode',
'.idea',
'__pycache__',
'.pytest_cache',
'venv',
'.venv',
'env',
'.env'
];
return skipDirs.includes(dirName) || dirName.startsWith('.');
}
/**
* Auto-detect the framework for a project, searching recursively up to 3 levels deep
*/
async detectFramework(projectPath) {
console.log(`🔍 Searching for frameworks in ${projectPath} (up to 3 levels deep)...`);
const foundProjects = await this.findFrameworkProjectsRecursively(projectPath, 3);
if (foundProjects.length === 0) {
console.log("❌ No framework projects found");
return null;
}
if (foundProjects.length === 1) {
console.log(`✅ Found single ${foundProjects[0].framework} project at: ${foundProjects[0].path}`);
return foundProjects[0].framework;
}
// Multiple projects found - prioritize by depth (closer to root) and then by framework preference
foundProjects.sort((a, b) => {
const depthA = a.path.split(path.sep).length;
const depthB = b.path.split(path.sep).length;
if (depthA !== depthB) {
return depthA - depthB; // Prefer shallower paths
}
// If same depth, prefer Angular over React (arbitrary preference)
const frameworkPriority = { 'angular': 1, 'react': 2 };
const priorityA = frameworkPriority[a.framework.toLowerCase()] || 999;
const priorityB = frameworkPriority[b.framework.toLowerCase()] || 999;
return priorityA - priorityB;
});
console.log(`🎯 Multiple projects found, selecting: ${foundProjects[0].framework} at ${foundProjects[0].path}`);
console.log(` Other projects found:`);
foundProjects.slice(1).forEach(project => {
console.log(` - ${project.framework} at ${project.path}`);
});
return foundProjects[0].framework;
}
/**
* Get the detected project path for a framework
*/
async getFrameworkProjectPath(projectPath, frameworkName) {
const foundProjects = await this.findFrameworkProjectsRecursively(projectPath, 3);
if (frameworkName) {
const project = foundProjects.find(p => p.framework.toLowerCase() === frameworkName.toLowerCase());
return project ? project.path : projectPath;
}
// Return the first (highest priority) project path
return foundProjects.length > 0 ? foundProjects[0].path : projectPath;
}
/**
* Analyze a project using the specified or auto-detected framework
*/
async analyze(options) {
console.log("🚀 Starting project analysis...");
// Validate project path
if (!fs.existsSync(options.projectPath)) {
throw new Error(`Project path does not exist: ${options.projectPath}`);
}
let frameworkName = options.framework.toLowerCase();
let actualProjectPath = options.projectPath;
// Auto-detect framework if not specified or if specified framework is not available
if (!frameworkName ||
frameworkName === "auto" ||
!this.frameworkAnalyzers.has(frameworkName)) {
console.log("🔍 Auto-detecting framework...");
const detectedFramework = await this.detectFramework(options.projectPath);
if (!detectedFramework) {
throw new Error(`Could not detect framework for project at: ${options.projectPath}. Available frameworks: ${this.getAvailableFrameworks().join(", ")}`);
}
frameworkName = detectedFramework;
console.log(`✅ Detected framework: ${frameworkName}`);
// Get the actual project path where the framework was detected
actualProjectPath = await this.getFrameworkProjectPath(options.projectPath, frameworkName);
if (actualProjectPath !== options.projectPath) {
console.log(`📁 Using framework project path: ${actualProjectPath}`);
}
}
else {
// Even if framework is specified, get the correct project path
actualProjectPath = await this.getFrameworkProjectPath(options.projectPath, frameworkName);
if (actualProjectPath !== options.projectPath) {
console.log(`📁 Using framework project path: ${actualProjectPath}`);
}
}
// Get the framework analyzer
const analyzer = this.frameworkAnalyzers.get(frameworkName);
if (!analyzer) {
throw new Error(`No analyzer registered for framework: ${frameworkName}. Available: ${this.getAvailableFrameworks().join(", ")}`);
}
// Create modified options with the actual project path
const modifiedOptions = {
...options,
projectPath: actualProjectPath
};
// Perform the analysis
console.log(`📊 Analyzing with ${frameworkName} analyzer...`);
const result = await analyzer.analyze(modifiedOptions);
console.log(`✨ Analysis complete! Found ${result.routes.length} routes and ${result.flows.length} navigation flows.`);
return result;
}
/**
* Generate outputs in the specified formats
*/
async generateOutputs(analysisResult, outputFormats, baseOptions) {
console.log("📄 Generating outputs...");
const outputs = [];
const defaultOptions = {
outputDirectory: process.cwd(),
...baseOptions,
};
for (const formatName of outputFormats) {
const generator = this.outputGenerators.get(formatName.toLowerCase());
if (!generator) {
console.warn(`⚠️ No generator registered for format: ${formatName}. Available: ${this.getAvailableOutputFormats().join(", ")}`);
continue;
}
try {
// Validate options for this generator
const errors = generator.validateOptions(defaultOptions);
if (errors.length > 0) {
console.warn(`⚠️ Invalid options for ${formatName} generator:`, errors);
continue;
}
console.log(`🎨 Generating ${formatName} output...`);
const output = await generator.generate(analysisResult, defaultOptions);
outputs.push(output);
console.log(`✅ Generated ${formatName}: ${output.filePath}`);
if (output.additionalFiles) {
output.additionalFiles.forEach((file) => {
console.log(` 📎 Additional file: ${file}`);
});
}
}
catch (error) {
console.error(`❌ Error generating ${formatName} output:`, error);
}
}
return outputs;
}
/**
* Full analysis and output generation workflow
*/
async analyzeAndGenerate(options, outputOptions = {}) {
const analysis = await this.analyze(options);
const outputs = await this.generateOutputs(analysis, options.outputFormats, outputOptions);
return { analysis, outputs };
}
}