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