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