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

665 lines (664 loc) • 26.5 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.IncrementalBuilder = void 0; exports.createIncrementalBuilder = createIncrementalBuilder; exports.runIncrementalBuild = runIncrementalBuild; const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); const child_process_1 = require("child_process"); const error_handler_1 = require("./error-handler"); const change_detector_1 = require("./change-detector"); const change_impact_analyzer_1 = require("./change-impact-analyzer"); // Incremental build optimizer class IncrementalBuilder { constructor(rootPath, options = {}) { this.rootPath = path.resolve(rootPath); this.changeDetector = new change_detector_1.ChangeDetector(rootPath); this.impactAnalyzer = new change_impact_analyzer_1.ChangeImpactAnalyzer(rootPath); this.buildCache = { version: '1.0', builds: {} }; this.options = { maxParallelBuilds: Math.max(1, Math.floor(require('os').cpus().length / 2)), enableCache: true, cacheLocation: path.join(rootPath, '.re-shell', 'build-cache.json'), cleanBuild: false, dryRun: false, verbose: false, skipTests: false, failFast: true, buildTimeout: 300000, // 5 minutes ...options }; } // Initialize the incremental builder async initialize() { await this.changeDetector.initialize(); await this.impactAnalyzer.initialize(); await this.loadBuildCache(); } // Create optimized build plan based on changes async createBuildPlan(changedFiles) { const targets = await this.discoverBuildTargets(); // Get changed files if not provided let files = changedFiles; if (!files) { const changeResult = await this.changeDetector.detectChanges(); files = [...changeResult.added, ...changeResult.modified]; } // Analyze impact to determine which targets need rebuilding const impact = await this.impactAnalyzer.analyzeChangeImpact(files); const affectedTargets = new Set(impact.affectedWorkspaces.map(ws => ws.name)); // Filter targets that need rebuilding const targetsToRebuild = targets.filter(target => { // Always rebuild if clean build is requested if (this.options.cleanBuild) { return true; } // Rebuild if target is affected by changes if (affectedTargets.has(target.name)) { return true; } // Rebuild if cache is invalid if (!this.isCacheValid(target)) { return true; } return false; }); // Calculate build order considering dependencies const buildOrder = this.calculateOptimalBuildOrder(targetsToRebuild); // Group targets for parallel execution const parallelGroups = this.createParallelGroups(targetsToRebuild, buildOrder); // Estimate build time const totalEstimatedTime = this.estimateBuildTime(targetsToRebuild); // Generate optimization suggestions const optimizations = this.generateOptimizations(targetsToRebuild, impact); return { targets: targetsToRebuild, buildOrder, parallelGroups, totalEstimatedTime, optimizations }; } // Execute incremental build plan async executeBuildPlan(plan) { if (this.options.dryRun) { console.log('šŸ” Dry run - showing what would be built:'); plan.targets.forEach(target => { console.log(` • ${target.name} (${target.type})`); }); return []; } const results = []; const startTime = Date.now(); console.log(`šŸš€ Starting incremental build (${plan.targets.length} targets)`); console.log(`šŸ“Š Estimated time: ${Math.round(plan.totalEstimatedTime / 1000)}s`); if (plan.optimizations.length > 0) { console.log('šŸ’” Optimizations applied:'); plan.optimizations.forEach(opt => console.log(` • ${opt}`)); } // Execute parallel groups sequentially for (let i = 0; i < plan.parallelGroups.length; i++) { const group = plan.parallelGroups[i]; console.log(`\nšŸ“¦ Building group ${i + 1}/${plan.parallelGroups.length} (${group.length} targets)`); // Build targets in parallel within the group const groupPromises = group.map(async (targetName) => { const target = plan.targets.find(t => t.name === targetName); return await this.buildTarget(target); }); const groupResults = await Promise.all(groupPromises); results.push(...groupResults); // Check for failures if fail-fast is enabled if (this.options.failFast) { const failures = groupResults.filter(r => !r.success); if (failures.length > 0) { console.log(`āŒ Build failed (fail-fast enabled)`); throw new error_handler_1.ValidationError(`Build failed for targets: ${failures.map(f => f.target).join(', ')}`); } } } const totalTime = Date.now() - startTime; const successful = results.filter(r => r.success).length; const failed = results.length - successful; console.log(`\nāœ… Build completed in ${Math.round(totalTime / 1000)}s`); console.log(`šŸ“Š Results: ${successful} successful, ${failed} failed`); // Update build cache if (this.options.enableCache) { await this.updateBuildCache(results); } return results; } // Build a specific target async buildTarget(target) { const startTime = Date.now(); // Check cache first if (this.options.enableCache && this.isCacheValid(target)) { if (this.options.verbose) { console.log(`šŸŽÆ ${target.name}: Using cached build`); } return { target: target.name, success: true, duration: 0, output: 'Cached build', cacheHit: true }; } if (this.options.verbose) { console.log(`šŸ”Ø Building ${target.name}...`); } try { const buildResult = await this.executeBuildScript(target); const duration = Date.now() - startTime; // Calculate output size const outputSize = await this.calculateOutputSize(target); const result = { target: target.name, success: buildResult.success, duration, output: buildResult.output, error: buildResult.error, outputSize, cacheHit: false }; if (result.success && this.options.verbose) { console.log(`āœ… ${target.name}: Built in ${Math.round(duration / 1000)}s`); } else if (!result.success) { console.log(`āŒ ${target.name}: Build failed`); if (this.options.verbose && result.error) { console.log(` Error: ${result.error}`); } } return result; } catch (error) { const duration = Date.now() - startTime; return { target: target.name, success: false, duration, output: '', error: error instanceof Error ? error.message : String(error) }; } } // Execute build script for a target async executeBuildScript(target) { return new Promise((resolve) => { const cwd = target.path; const command = target.buildScript; // Detect package manager const packageManager = this.detectPackageManager(target.path); const [cmd, ...args] = command.split(' '); const fullCommand = packageManager === 'npm' ? `npm run ${cmd}` : packageManager === 'yarn' ? `yarn ${cmd}` : packageManager === 'pnpm' ? `pnpm run ${cmd}` : command; const [finalCmd, ...finalArgs] = fullCommand.split(' '); const child = (0, child_process_1.spawn)(finalCmd, finalArgs, { cwd, stdio: ['ignore', 'pipe', 'pipe'], shell: true }); let output = ''; let error = ''; child.stdout?.on('data', (data) => { output += data.toString(); }); child.stderr?.on('data', (data) => { error += data.toString(); }); const timeout = setTimeout(() => { child.kill('SIGTERM'); resolve({ success: false, output, error: `Build timeout after ${this.options.buildTimeout}ms` }); }, this.options.buildTimeout); child.on('close', (code) => { clearTimeout(timeout); resolve({ success: code === 0, output, error: code !== 0 ? error : undefined }); }); child.on('error', (err) => { clearTimeout(timeout); resolve({ success: false, output, error: err.message }); }); }); } // Discover all build targets in the project async discoverBuildTargets() { const targets = []; const workspaces = this.impactAnalyzer.getAllWorkspaces(); for (const workspace of workspaces) { const packageJsonPath = path.join(workspace.path, 'package.json'); if (await fs.pathExists(packageJsonPath)) { try { const packageJson = await fs.readJson(packageJsonPath); const scripts = packageJson.scripts || {}; if (scripts.build) { const target = { name: workspace.name, path: workspace.path, type: workspace.type, buildScript: scripts.build, testScript: scripts.test, dependencies: workspace.dependencies, outputs: await this.detectOutputPaths(workspace.path), inputs: await this.detectInputPaths(workspace.path), lastBuildTime: await this.getLastBuildTime(workspace.path), buildHash: await this.calculateBuildHash(workspace.path) }; targets.push(target); } } catch (error) { console.warn(`Failed to process ${workspace.name}: ${error}`); } } } return targets; } // Calculate optimal build order calculateOptimalBuildOrder(targets) { const visited = new Set(); const visiting = new Set(); const result = []; const targetMap = new Map(targets.map(t => [t.name, t])); const visit = (targetName) => { if (visiting.has(targetName)) { throw new error_handler_1.ValidationError(`Circular dependency detected involving ${targetName}`); } if (visited.has(targetName)) { return; } const target = targetMap.get(targetName); if (!target) return; visiting.add(targetName); // Visit dependencies first for (const dep of target.dependencies) { if (targetMap.has(dep)) { visit(dep); } } visiting.delete(targetName); visited.add(targetName); result.push(targetName); }; for (const target of targets) { if (!visited.has(target.name)) { visit(target.name); } } return result; } // Create parallel execution groups createParallelGroups(targets, buildOrder) { const groups = []; const targetMap = new Map(targets.map(t => [t.name, t])); const built = new Set(); for (const targetName of buildOrder) { const target = targetMap.get(targetName); if (!target) continue; // Check if all dependencies are built const dependenciesBuilt = target.dependencies.every(dep => !targetMap.has(dep) || built.has(dep)); if (dependenciesBuilt) { // Add to existing group or create new one let addedToGroup = false; for (const group of groups) { if (group.length < this.options.maxParallelBuilds) { // Check if this target can be built in parallel with group members const canParallelize = group.every(groupMember => { const groupTarget = targetMap.get(groupMember); return groupTarget && !target.dependencies.includes(groupMember) && !groupTarget.dependencies.includes(targetName); }); if (canParallelize) { group.push(targetName); addedToGroup = true; break; } } } if (!addedToGroup) { groups.push([targetName]); } built.add(targetName); } } return groups; } // Estimate total build time estimateBuildTime(targets) { let totalTime = 0; for (const target of targets) { // Use historical data if available const cacheEntry = this.buildCache.builds[target.name]; if (cacheEntry) { totalTime += cacheEntry.duration; } else { // Estimate based on target type and size const estimatedTime = this.estimateTargetBuildTime(target); totalTime += estimatedTime; } } // Account for parallelization const parallelizationFactor = Math.min(this.options.maxParallelBuilds, targets.length); return Math.ceil(totalTime / parallelizationFactor); } // Estimate build time for a single target estimateTargetBuildTime(target) { // Base estimates in milliseconds const baseTime = { app: 60000, // 1 minute package: 30000, // 30 seconds lib: 20000, // 20 seconds tool: 10000 // 10 seconds }; let estimate = baseTime[target.type] || 30000; // Adjust based on input size const inputCount = target.inputs.length; if (inputCount > 100) { estimate *= 1.5; } else if (inputCount > 50) { estimate *= 1.2; } return estimate; } // Generate optimization suggestions generateOptimizations(targets, impact) { const optimizations = []; if (targets.length === 0) { optimizations.push('No targets need rebuilding - all caches are valid'); return optimizations; } // Cache optimizations const cacheHits = targets.filter(t => this.isCacheValid(t)).length; if (cacheHits > 0) { optimizations.push(`${cacheHits} targets using cached builds`); } // Parallel build optimization if (this.options.maxParallelBuilds > 1) { optimizations.push(`Parallel builds enabled (max ${this.options.maxParallelBuilds})`); } // Change-based optimization if (impact.totalImpact < targets.length) { optimizations.push(`Smart rebuilds: only ${impact.totalImpact} of ${targets.length} workspaces affected`); } // Type-based optimization const typeGroups = targets.reduce((acc, t) => { acc[t.type] = (acc[t.type] || 0) + 1; return acc; }, {}); if (typeGroups.package && typeGroups.app) { optimizations.push('Building packages before apps for optimal dependency resolution'); } return optimizations; } // Check if build cache is valid for target isCacheValid(target) { if (!this.options.enableCache) { return false; } const cacheEntry = this.buildCache.builds[target.name]; if (!cacheEntry) { return false; } // Check if hash matches if (cacheEntry.hash !== target.buildHash) { return false; } // Check if outputs exist return target.outputs.every(output => { const outputPath = path.resolve(target.path, output); return fs.existsSync(outputPath); }); } // Detect package manager for target detectPackageManager(targetPath) { if (fs.existsSync(path.join(targetPath, 'pnpm-lock.yaml'))) { return 'pnpm'; } if (fs.existsSync(path.join(targetPath, 'yarn.lock'))) { return 'yarn'; } return 'npm'; } // Detect output paths for target async detectOutputPaths(targetPath) { const commonOutputs = ['dist', 'build', 'lib', 'out']; const outputs = []; for (const output of commonOutputs) { const outputPath = path.join(targetPath, output); if (await fs.pathExists(outputPath)) { outputs.push(output); } } return outputs.length > 0 ? outputs : ['dist']; // Default to dist } // Detect input paths for target async detectInputPaths(targetPath) { const inputs = []; const srcPath = path.join(targetPath, 'src'); if (await fs.pathExists(srcPath)) { const srcFiles = await this.getFilesRecursive(srcPath); inputs.push(...srcFiles.map(f => path.relative(targetPath, f))); } // Add package.json and config files const configFiles = ['package.json', 'tsconfig.json', 'vite.config.ts', 'webpack.config.js']; for (const configFile of configFiles) { const configPath = path.join(targetPath, configFile); if (await fs.pathExists(configPath)) { inputs.push(configFile); } } return inputs; } // Get files recursively from directory async getFilesRecursive(dirPath) { const files = []; const entries = await fs.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dirPath, entry.name); if (entry.isDirectory()) { if (!['node_modules', '.git', 'dist', 'build'].includes(entry.name)) { files.push(...await this.getFilesRecursive(fullPath)); } } else { files.push(fullPath); } } return files; } // Get last build time for target async getLastBuildTime(targetPath) { const outputDirs = ['dist', 'build', 'lib']; for (const outputDir of outputDirs) { const outputPath = path.join(targetPath, outputDir); if (await fs.pathExists(outputPath)) { const stats = await fs.stat(outputPath); return stats.mtime.getTime(); } } return undefined; } // Calculate build hash for target async calculateBuildHash(targetPath) { const inputs = await this.detectInputPaths(targetPath); const hashes = []; for (const input of inputs) { const inputPath = path.join(targetPath, input); if (await fs.pathExists(inputPath)) { const fileHash = await this.changeDetector.getFileHash(input); if (fileHash) { hashes.push(fileHash.hash); } } } return require('crypto').createHash('md5').update(hashes.join('')).digest('hex'); } // Calculate output size for target async calculateOutputSize(target) { let totalSize = 0; for (const output of target.outputs) { const outputPath = path.resolve(target.path, output); if (await fs.pathExists(outputPath)) { const stats = await fs.stat(outputPath); if (stats.isDirectory()) { totalSize += await this.getDirectorySize(outputPath); } else { totalSize += stats.size; } } } return totalSize; } // Get directory size recursively async getDirectorySize(dirPath) { let totalSize = 0; const entries = await fs.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dirPath, entry.name); if (entry.isDirectory()) { totalSize += await this.getDirectorySize(fullPath); } else { const stats = await fs.stat(fullPath); totalSize += stats.size; } } return totalSize; } // Load build cache from disk async loadBuildCache() { if (!this.options.enableCache) { return; } try { if (await fs.pathExists(this.options.cacheLocation)) { this.buildCache = await fs.readJson(this.options.cacheLocation); } } catch (error) { console.warn(`Failed to load build cache: ${error}`); this.buildCache = { version: '1.0', builds: {} }; } } // Save build cache to disk async saveBuildCache() { if (!this.options.enableCache) { return; } try { await fs.ensureDir(path.dirname(this.options.cacheLocation)); await fs.writeJson(this.options.cacheLocation, this.buildCache, { spaces: 2 }); } catch (error) { console.warn(`Failed to save build cache: ${error}`); } } // Update build cache with results async updateBuildCache(results) { for (const result of results) { if (result.success && !result.cacheHit) { const target = (await this.discoverBuildTargets()).find(t => t.name === result.target); if (target) { this.buildCache.builds[result.target] = { hash: target.buildHash, timestamp: Date.now(), duration: result.duration, success: result.success, outputSize: result.outputSize || 0 }; } } } await this.saveBuildCache(); } // Get build statistics getBuildStats() { const builds = Object.values(this.buildCache.builds); const totalBuilds = builds.length; const successfulBuilds = builds.filter(b => b.success); const averageBuildTime = successfulBuilds.length > 0 ? successfulBuilds.reduce((sum, b) => sum + b.duration, 0) / successfulBuilds.length : 0; const totalCacheSize = builds.reduce((sum, b) => sum + b.outputSize, 0); return { totalBuilds, cacheHitRate: totalBuilds > 0 ? (successfulBuilds.length / totalBuilds) * 100 : 0, averageBuildTime, totalCacheSize }; } // Clear build cache async clearCache() { this.buildCache = { version: '1.0', builds: {} }; if (await fs.pathExists(this.options.cacheLocation)) { await fs.remove(this.options.cacheLocation); } } } exports.IncrementalBuilder = IncrementalBuilder; // Utility functions async function createIncrementalBuilder(rootPath, options) { const builder = new IncrementalBuilder(rootPath, options); await builder.initialize(); return builder; } async function runIncrementalBuild(rootPath, changedFiles, options) { const builder = await createIncrementalBuilder(rootPath, options); const plan = await builder.createBuildPlan(changedFiles); return await builder.executeBuildPlan(plan); }