UNPKG

@re-shell/cli

Version:

Full-stack development platform uniting microservices and microfrontends. Build complete applications with .NET (ASP.NET Core Web API, Minimal API), Java (Spring Boot, Quarkus, Micronaut, Vert.x), Rust (Actix-Web, Warp, Rocket, Axum), Python (FastAPI, Dja

494 lines (493 loc) โ€ข 19.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.ChangeImpactAnalyzer = void 0; exports.createChangeImpactAnalyzer = createChangeImpactAnalyzer; exports.analyzeChangeImpact = analyzeChangeImpact; const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); const error_handler_1 = require("./error-handler"); const change_detector_1 = require("./change-detector"); // Change impact analyzer for workspace dependencies class ChangeImpactAnalyzer { constructor(rootPath, options = {}) { this.rootPath = path.resolve(rootPath); this.dependencyGraph = { nodes: new Map(), edges: new Map(), reverseEdges: new Map() }; this.changeDetector = new change_detector_1.ChangeDetector(rootPath); this.impactRules = this.getDefaultImpactRules(); this.options = { maxDepth: 10, includeTests: true, includeDevDependencies: false, buildOptimization: true, parallelAnalysis: true, ...options }; } // Initialize the analyzer async initialize() { await this.changeDetector.initialize(); await this.buildDependencyGraph(); } // Analyze impact of file changes across workspace dependencies async analyzeChangeImpact(changedFiles) { const startTime = Date.now(); // Get changed files if not provided let files = changedFiles; if (!files) { const changeResult = await this.changeDetector.detectChanges(); files = [...changeResult.added, ...changeResult.modified]; } if (files.length === 0) { return { changedFiles: [], affectedWorkspaces: [], buildOrder: [], testOrder: [], totalImpact: 0, criticalPath: [], recommendations: ['No changes detected'], analysisTime: Date.now() - startTime }; } // Analyze impact for each changed file const impactedWorkspaces = new Set(); const criticalChanges = []; for (const file of files) { const impact = await this.analyzeFileImpact(file); impact.workspaces.forEach(ws => impactedWorkspaces.add(ws)); if (impact.severity === 'critical') { criticalChanges.push(file); } } // Get affected workspace info const affectedWorkspaces = Array.from(impactedWorkspaces) .map(name => this.dependencyGraph.nodes.get(name)) .filter(ws => ws !== undefined); // Calculate build and test order const buildOrder = this.calculateBuildOrder(Array.from(impactedWorkspaces)); const testOrder = this.calculateTestOrder(Array.from(impactedWorkspaces)); // Find critical path const criticalPath = this.findCriticalPath(Array.from(impactedWorkspaces)); // Generate recommendations const recommendations = this.generateRecommendations(files, affectedWorkspaces, criticalChanges); const analysisTime = Date.now() - startTime; return { changedFiles: files, affectedWorkspaces, buildOrder, testOrder, totalImpact: impactedWorkspaces.size, criticalPath, recommendations, analysisTime }; } // Analyze impact of a specific file change async analyzeFileImpact(filePath) { const workspaces = new Set(); const matchedRules = []; let maxSeverity = 'low'; // Find which workspace the file belongs to const ownerWorkspace = this.findFileOwnerWorkspace(filePath); if (ownerWorkspace) { workspaces.add(ownerWorkspace); } // Apply impact rules for (const rule of this.impactRules) { if (rule.pattern.test(filePath)) { matchedRules.push(rule); rule.affects.forEach(ws => workspaces.add(ws)); // Update severity if (this.severityLevel(rule.severity) > this.severityLevel(maxSeverity)) { maxSeverity = rule.severity; } } } // Analyze dependency impact if (ownerWorkspace) { const dependentWorkspaces = this.findDependentWorkspaces(ownerWorkspace); dependentWorkspaces.forEach(ws => workspaces.add(ws)); } return { file: filePath, workspaces: Array.from(workspaces), severity: maxSeverity, rules: matchedRules }; } // Get impact visualization data async getImpactVisualization(changedFiles) { const impact = await this.analyzeChangeImpact(changedFiles); const affectedNames = new Set(impact.affectedWorkspaces.map(ws => ws.name)); const nodes = Array.from(this.dependencyGraph.nodes.values()).map(ws => ({ id: ws.name, label: ws.name, type: ws.type, affected: affectedNames.has(ws.name) })); const edges = []; for (const [from, targets] of this.dependencyGraph.edges) { for (const to of targets) { edges.push({ from, to, type: 'dependency' }); } } const legend = { app: 'Application', package: 'NPM Package', lib: 'Library', tool: 'Tool/Script', dependency: 'Depends on' }; return { nodes, edges, legend }; } // Build workspace dependency graph async buildDependencyGraph() { const workspaces = await this.discoverWorkspaces(); // Add nodes for (const workspace of workspaces) { this.dependencyGraph.nodes.set(workspace.name, workspace); this.dependencyGraph.edges.set(workspace.name, []); this.dependencyGraph.reverseEdges.set(workspace.name, []); } // Add edges based on dependencies for (const workspace of workspaces) { const deps = this.options.includeDevDependencies ? [...workspace.dependencies, ...workspace.devDependencies] : workspace.dependencies; for (const dep of deps) { if (this.dependencyGraph.nodes.has(dep)) { // Add edge: workspace depends on dep this.dependencyGraph.edges.get(workspace.name).push(dep); this.dependencyGraph.reverseEdges.get(dep).push(workspace.name); } } } } // Discover all workspaces in the monorepo async discoverWorkspaces() { const workspaces = []; const workspaceDirs = ['apps', 'packages', 'libs', 'tools']; for (const dir of workspaceDirs) { const dirPath = path.join(this.rootPath, dir); if (await fs.pathExists(dirPath)) { const entries = await fs.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory()) { const workspacePath = path.join(dirPath, entry.name); const packageJsonPath = path.join(workspacePath, 'package.json'); if (await fs.pathExists(packageJsonPath)) { try { const packageJson = await fs.readJson(packageJsonPath); const workspace = { name: entry.name, path: workspacePath, type: this.inferWorkspaceType(dir), dependencies: this.extractWorkspaceDependencies(packageJson.dependencies || {}), devDependencies: this.extractWorkspaceDependencies(packageJson.devDependencies || {}), framework: this.detectFramework(packageJson), buildScript: packageJson.scripts?.build, testScript: packageJson.scripts?.test }; workspaces.push(workspace); } catch (error) { console.warn(`Failed to read package.json for ${entry.name}: ${error}`); } } } } } } return workspaces; } // Extract workspace dependencies (filter out external packages) extractWorkspaceDependencies(deps) { return Object.keys(deps).filter(dep => { // Check if it's a workspace dependency (starts with workspace name pattern) return this.dependencyGraph.nodes.has(dep) || dep.startsWith('@re-shell/'); }); } // Infer workspace type from directory inferWorkspaceType(dir) { switch (dir) { case 'apps': return 'app'; case 'packages': return 'package'; case 'libs': return 'lib'; case 'tools': return 'tool'; default: return 'package'; } } // Detect framework from package.json detectFramework(packageJson) { const deps = { ...packageJson.dependencies, ...packageJson.devDependencies }; if (deps.react) return 'react'; if (deps.vue) return 'vue'; if (deps.svelte) return 'svelte'; if (deps['@angular/core']) return 'angular'; return undefined; } // Find which workspace owns a file findFileOwnerWorkspace(filePath) { const absolutePath = path.resolve(this.rootPath, filePath); for (const [name, workspace] of this.dependencyGraph.nodes) { if (absolutePath.startsWith(workspace.path)) { return name; } } return undefined; } // Find workspaces that depend on a given workspace findDependentWorkspaces(workspaceName) { return this.dependencyGraph.reverseEdges.get(workspaceName) || []; } // Calculate optimal build order using topological sort calculateBuildOrder(workspaces) { const visited = new Set(); const visiting = new Set(); const result = []; const visit = (workspace) => { if (visiting.has(workspace)) { throw new error_handler_1.ValidationError(`Circular dependency detected involving ${workspace}`); } if (visited.has(workspace)) { return; } visiting.add(workspace); // Visit dependencies first const deps = this.dependencyGraph.edges.get(workspace) || []; for (const dep of deps) { if (workspaces.includes(dep)) { visit(dep); } } visiting.delete(workspace); visited.add(workspace); result.push(workspace); }; for (const workspace of workspaces) { if (!visited.has(workspace)) { visit(workspace); } } return result; } // Calculate test order (reverse of build order for most cases) calculateTestOrder(workspaces) { const buildOrder = this.calculateBuildOrder(workspaces); // For testing, we usually want to test dependencies first, then dependents // But for integration tests, we might want the reverse return this.options.includeTests ? buildOrder : buildOrder.reverse(); } // Find critical path in dependency graph findCriticalPath(workspaces) { // Find the workspace with the most dependents let maxDependents = 0; let criticalWorkspace = ''; for (const workspace of workspaces) { const dependents = this.findDependentWorkspaces(workspace); if (dependents.length > maxDependents) { maxDependents = dependents.length; criticalWorkspace = workspace; } } if (!criticalWorkspace) { return workspaces.slice(0, 1); // Return first workspace if no clear critical path } // Build path from critical workspace const path = [criticalWorkspace]; const visited = new Set([criticalWorkspace]); // Follow dependency chain let current = criticalWorkspace; while (true) { const deps = this.dependencyGraph.edges.get(current) || []; const nextDep = deps.find(dep => workspaces.includes(dep) && !visited.has(dep)); if (!nextDep) break; path.unshift(nextDep); // Add to beginning to maintain dependency order visited.add(nextDep); current = nextDep; } return path; } // Generate actionable recommendations generateRecommendations(changedFiles, affectedWorkspaces, criticalChanges) { const recommendations = []; if (changedFiles.length === 0) { recommendations.push('No changes detected - no action required'); return recommendations; } if (affectedWorkspaces.length === 0) { recommendations.push('Changes detected but no workspace impact - verify file locations'); return recommendations; } // Critical changes if (criticalChanges.length > 0) { recommendations.push(`๐Ÿšจ Critical changes detected in ${criticalChanges.length} files - full rebuild recommended`); } // Build recommendations const appsAffected = affectedWorkspaces.filter(ws => ws.type === 'app').length; const packagesAffected = affectedWorkspaces.filter(ws => ws.type === 'package').length; if (packagesAffected > 0) { recommendations.push(`๐Ÿ“ฆ ${packagesAffected} package(s) affected - rebuild and test packages first`); } if (appsAffected > 0) { recommendations.push(`๐Ÿ”ง ${appsAffected} app(s) affected - rebuild applications after packages`); } // Performance recommendations if (affectedWorkspaces.length > 5) { recommendations.push('โšก Consider parallel builds for better performance with many affected workspaces'); } // Test recommendations if (this.options.includeTests) { const hasTests = affectedWorkspaces.some(ws => ws.testScript); if (hasTests) { recommendations.push('๐Ÿงช Run tests in dependency order to catch issues early'); } } // Framework-specific recommendations const frameworks = new Set(affectedWorkspaces.map(ws => ws.framework).filter(Boolean)); if (frameworks.size > 1) { recommendations.push('๐Ÿ”„ Multiple frameworks affected - consider framework-specific optimization'); } return recommendations; } // Get default impact rules getDefaultImpactRules() { return [ { pattern: /package\.json$/, affects: ['*'], severity: 'critical', action: 'rebuild', description: 'Package.json changes affect all workspaces' }, { pattern: /tsconfig.*\.json$/, affects: ['*'], severity: 'high', action: 'rebuild', description: 'TypeScript configuration changes require rebuild' }, { pattern: /\.config\.(js|ts)$/, affects: ['*'], severity: 'high', action: 'rebuild', description: 'Configuration file changes require rebuild' }, { pattern: /packages\/.*\/src\//, affects: ['*'], severity: 'high', action: 'rebuild', description: 'Shared package changes affect all consumers' }, { pattern: /libs\/.*\/src\//, affects: ['*'], severity: 'medium', action: 'rebuild', description: 'Library changes affect dependent workspaces' }, { pattern: /apps\/.*\/src\//, affects: [], severity: 'low', action: 'rebuild', description: 'App-specific changes have isolated impact' }, { pattern: /\.test\.(js|ts|jsx|tsx)$/, affects: [], severity: 'low', action: 'test', description: 'Test file changes only require test runs' }, { pattern: /README\.md$/, affects: [], severity: 'low', action: 'lint', description: 'Documentation changes require minimal action' } ]; } // Convert severity to numeric level for comparison severityLevel(severity) { switch (severity) { case 'low': return 1; case 'medium': return 2; case 'high': return 3; case 'critical': return 4; default: return 0; } } // Add custom impact rule addImpactRule(rule) { this.impactRules.push(rule); } // Get dependency graph information getDependencyGraph() { return this.dependencyGraph; } // Get workspace information getWorkspaceInfo(name) { return this.dependencyGraph.nodes.get(name); } // Get all workspaces getAllWorkspaces() { return Array.from(this.dependencyGraph.nodes.values()); } } exports.ChangeImpactAnalyzer = ChangeImpactAnalyzer; // Utility functions async function createChangeImpactAnalyzer(rootPath, options) { const analyzer = new ChangeImpactAnalyzer(rootPath, options); await analyzer.initialize(); return analyzer; } async function analyzeChangeImpact(rootPath, changedFiles, options) { const analyzer = await createChangeImpactAnalyzer(rootPath, options); return await analyzer.analyzeChangeImpact(changedFiles); }