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

607 lines (606 loc) 23.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; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.analyzeProject = analyzeProject; const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); const chalk_1 = __importDefault(require("chalk")); const child_process_1 = require("child_process"); const monorepo_1 = require("../utils/monorepo"); async function analyzeProject(options = {}) { try { const monorepoRoot = await (0, monorepo_1.findMonorepoRoot)(process.cwd()); if (!monorepoRoot) { throw new Error('Not in a Re-Shell monorepo. Run this command from within a monorepo.'); } if (options.spinner) { options.spinner.text = 'Starting analysis...'; } const workspaces = options.workspace ? [options.workspace] : await getWorkspaces(monorepoRoot); const results = { timestamp: new Date().toISOString(), monorepo: path.basename(monorepoRoot), workspaces: workspaces.length, analysis: {} }; // Run different types of analysis based on options const analysisTypes = options.type === 'all' ? ['bundle', 'dependencies', 'performance', 'security'] : [options.type || 'all']; for (const workspace of workspaces) { const workspacePath = path.join(monorepoRoot, workspace); if (!await fs.pathExists(path.join(workspacePath, 'package.json'))) { continue; } results.analysis[workspace] = {}; for (const analysisType of analysisTypes) { if (options.spinner) { options.spinner.text = `Analyzing ${workspace} (${analysisType})...`; } switch (analysisType) { case 'bundle': results.analysis[workspace].bundle = await analyzeBundleSize(workspacePath, workspace, options); break; case 'dependencies': results.analysis[workspace].dependencies = await analyzeDependencies(workspacePath, workspace, options); break; case 'performance': results.analysis[workspace].performance = await analyzePerformance(workspacePath, workspace, options); break; case 'security': results.analysis[workspace].security = await analyzeSecurityIssues(workspacePath, workspace, options); break; case 'all': results.analysis[workspace].bundle = await analyzeBundleSize(workspacePath, workspace, options); results.analysis[workspace].dependencies = await analyzeDependencies(workspacePath, workspace, options); results.analysis[workspace].performance = await analyzePerformance(workspacePath, workspace, options); results.analysis[workspace].security = await analyzeSecurityIssues(workspacePath, workspace, options); break; } } } // Save results if output specified if (options.output) { await fs.writeJson(options.output, results, { spaces: 2 }); console.log(chalk_1.default.green(`Analysis results saved to: ${options.output}`)); } return displayAnalysisResults(results, options); } catch (error) { if (options.spinner) { options.spinner.fail(chalk_1.default.red('Analysis failed')); } throw error; } } async function analyzeBundleSize(workspacePath, workspace, options) { try { const packageJsonPath = path.join(workspacePath, 'package.json'); const packageJson = await fs.readJson(packageJsonPath); // Check if workspace has build script if (!packageJson.scripts?.build) { return { workspace, size: { total: 'N/A', gzipped: 'N/A', assets: [] }, chunks: [], treeshaking: { unusedExports: [], deadCode: 0 } }; } // Try to build and analyze const distPath = path.join(workspacePath, 'dist'); const buildPath = path.join(workspacePath, 'build'); let outputPath = distPath; if (await fs.pathExists(buildPath)) { outputPath = buildPath; } else if (!await fs.pathExists(distPath)) { // Try to build first try { (0, child_process_1.execSync)('npm run build', { cwd: workspacePath, stdio: 'pipe' }); } catch (error) { // Build failed, return empty analysis return { workspace, size: { total: 'Build failed', gzipped: 'N/A', assets: [] }, chunks: [], treeshaking: { unusedExports: [], deadCode: 0 } }; } } // Analyze build output const assets = await analyzeBuildAssets(outputPath); const totalSize = assets.reduce((sum, asset) => sum + parseInt(asset.size) || 0, 0); // Try to detect webpack/vite stats const statsPath = path.join(workspacePath, 'stats.json'); const webpackStatsPath = path.join(workspacePath, 'webpack-stats.json'); let chunks = []; let treeshaking = { unusedExports: [], deadCode: 0 }; if (await fs.pathExists(statsPath)) { const stats = await fs.readJson(statsPath); chunks = extractChunksFromStats(stats); treeshaking = extractTreeshakingInfo(stats); } else if (await fs.pathExists(webpackStatsPath)) { const stats = await fs.readJson(webpackStatsPath); chunks = extractChunksFromStats(stats); treeshaking = extractTreeshakingInfo(stats); } return { workspace, size: { total: formatBytes(totalSize), gzipped: formatBytes(Math.floor(totalSize * 0.3)), // Estimate assets }, chunks, treeshaking }; } catch (error) { return { workspace, size: { total: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`, gzipped: 'N/A', assets: [] }, chunks: [], treeshaking: { unusedExports: [], deadCode: 0 } }; } } async function analyzeDependencies(workspacePath, workspace, options) { try { const packageJsonPath = path.join(workspacePath, 'package.json'); const packageJson = await fs.readJson(packageJsonPath); const deps = packageJson.dependencies || {}; const devDeps = packageJson.devDependencies || {}; // Get outdated packages let outdated = []; try { const outdatedOutput = (0, child_process_1.execSync)('npm outdated --json', { cwd: workspacePath, stdio: 'pipe', encoding: 'utf8' }); const outdatedData = JSON.parse(outdatedOutput); outdated = Object.entries(outdatedData).map(([name, info]) => ({ name, current: info.current, wanted: info.wanted, latest: info.latest })); } catch (error) { // npm outdated exits with code 1 when packages are outdated if (error.stdout) { try { const outdatedData = JSON.parse(error.stdout); outdated = Object.entries(outdatedData).map(([name, info]) => ({ name, current: info.current, wanted: info.wanted, latest: info.latest })); } catch { // Ignore parsing errors } } } // Check for vulnerabilities let vulnerabilities = []; try { const auditOutput = (0, child_process_1.execSync)('npm audit --json', { cwd: workspacePath, stdio: 'pipe', encoding: 'utf8' }); const auditData = JSON.parse(auditOutput); if (auditData.metadata?.vulnerabilities) { vulnerabilities = Object.entries(auditData.metadata.vulnerabilities) .filter(([key]) => key !== 'total') .map(([severity, count]) => ({ severity, count: count })); } } catch (error) { // Audit might fail, continue without vulnerability data } // Analyze licenses (simplified) const licenses = await analyzeLicenses(deps, devDeps); // Find duplicates (simplified check) const duplicates = findDuplicateDependencies(packageJson); return { workspace, total: Object.keys(deps).length + Object.keys(devDeps).length, production: Object.keys(deps).length, development: Object.keys(devDeps).length, outdated: outdated.slice(0, 10), // Limit to top 10 duplicates, vulnerabilities, licenses }; } catch (error) { return { workspace, total: 0, production: 0, development: 0, outdated: [], duplicates: [], vulnerabilities: [], licenses: [] }; } } async function analyzePerformance(workspacePath, workspace, options) { try { const packageJsonPath = path.join(workspacePath, 'package.json'); const packageJson = await fs.readJson(packageJsonPath); // Measure build time let buildTime = 0; if (packageJson.scripts?.build) { const startTime = Date.now(); try { (0, child_process_1.execSync)('npm run build', { cwd: workspacePath, stdio: 'pipe' }); buildTime = Date.now() - startTime; } catch (error) { buildTime = -1; // Build failed } } // Get bundle size const distPath = path.join(workspacePath, 'dist'); const buildPath = path.join(workspacePath, 'build'); let bundleSize = 'N/A'; if (await fs.pathExists(distPath)) { bundleSize = await getDirectorySize(distPath); } else if (await fs.pathExists(buildPath)) { bundleSize = await getDirectorySize(buildPath); } // Performance suggestions based on analysis const suggestions = generatePerformanceSuggestions(packageJson, bundleSize, buildTime); return { workspace, buildTime, bundleSize, loadTime: { ttfb: 0, // Would need actual measurement fcp: 0, lcp: 0 }, suggestions }; } catch (error) { return { workspace, buildTime: -1, bundleSize: 'Error', loadTime: { ttfb: 0, fcp: 0, lcp: 0 }, suggestions: [`Performance analysis failed: ${error instanceof Error ? error.message : 'Unknown error'}`] }; } } async function analyzeSecurityIssues(workspacePath, workspace, options) { try { // Run security audit let auditResults = {}; try { const auditOutput = (0, child_process_1.execSync)('npm audit --json', { cwd: workspacePath, stdio: 'pipe', encoding: 'utf8' }); auditResults = JSON.parse(auditOutput); } catch (error) { if (error.stdout) { try { auditResults = JSON.parse(error.stdout); } catch { // Ignore parsing errors } } } // Check for sensitive files const sensitiveFiles = await checkSensitiveFiles(workspacePath); // Check for hardcoded secrets (basic patterns) const secretPatterns = await scanForSecrets(workspacePath); return { workspace, audit: auditResults, sensitiveFiles, secretPatterns, recommendations: generateSecurityRecommendations(auditResults, sensitiveFiles, secretPatterns) }; } catch (error) { return { workspace, audit: {}, sensitiveFiles: [], secretPatterns: [], recommendations: [`Security analysis failed: ${error instanceof Error ? error.message : 'Unknown error'}`] }; } } // Helper functions async function getWorkspaces(monorepoRoot) { try { const packageJsonPath = path.join(monorepoRoot, 'package.json'); const packageJson = await fs.readJson(packageJsonPath); if (packageJson.workspaces) { if (Array.isArray(packageJson.workspaces)) { return packageJson.workspaces; } else if (packageJson.workspaces.packages) { return packageJson.workspaces.packages; } } // Fallback: scan for package.json files const workspaces = []; const scanDir = async (dir, depth = 0) => { if (depth > 2) return; const items = await fs.readdir(dir, { withFileTypes: true }); for (const item of items) { if (item.isDirectory() && !item.name.startsWith('.') && item.name !== 'node_modules') { const pkgPath = path.join(dir, item.name, 'package.json'); if (await fs.pathExists(pkgPath)) { workspaces.push(path.relative(monorepoRoot, path.join(dir, item.name))); } else { await scanDir(path.join(dir, item.name), depth + 1); } } } }; await scanDir(monorepoRoot); return workspaces; } catch (error) { return []; } } async function analyzeBuildAssets(outputPath) { try { const assets = []; const items = await fs.readdir(outputPath, { withFileTypes: true }); for (const item of items) { if (item.isFile()) { const filePath = path.join(outputPath, item.name); const stats = await fs.stat(filePath); const ext = path.extname(item.name); assets.push({ name: item.name, size: formatBytes(stats.size), type: getFileType(ext) }); } } return assets.sort((a, b) => parseInt(b.size) - parseInt(a.size)); } catch (error) { return []; } } function extractChunksFromStats(stats) { try { if (stats.chunks) { return stats.chunks.map((chunk) => ({ name: chunk.names?.[0] || chunk.id, size: formatBytes(chunk.size || 0), modules: chunk.modules?.length || 0 })); } return []; } catch (error) { return []; } } function extractTreeshakingInfo(stats) { try { const unusedExports = []; let deadCode = 0; if (stats.modules) { for (const module of stats.modules) { if (module.usedExports === false) { unusedExports.push(module.name); } if (module.providedExports && module.usedExports) { deadCode += module.providedExports.length - module.usedExports.length; } } } return { unusedExports: unusedExports.slice(0, 10), deadCode }; } catch (error) { return { unusedExports: [], deadCode: 0 }; } } async function analyzeLicenses(deps, devDeps) { // Simplified license analysis - would need actual package resolution const commonLicenses = ['MIT', 'Apache-2.0', 'BSD-3-Clause', 'ISC', 'GPL-3.0']; return commonLicenses.map(license => ({ license, packages: [] })); } function findDuplicateDependencies(packageJson) { // Simplified duplicate detection return []; } async function getDirectorySize(dirPath) { try { let totalSize = 0; const items = await fs.readdir(dirPath, { withFileTypes: true }); for (const item of items.slice(0, 20)) { // Limit for performance if (item.isFile()) { const stats = await fs.stat(path.join(dirPath, item.name)); totalSize += stats.size; } } return formatBytes(totalSize); } catch (error) { return 'Unknown'; } } function generatePerformanceSuggestions(packageJson, bundleSize, buildTime) { const suggestions = []; if (buildTime > 30000) { suggestions.push('Consider using faster build tools like esbuild or swc'); } if (bundleSize && parseInt(bundleSize) > 1000000) { suggestions.push('Bundle size is large, consider code splitting'); } const deps = packageJson.dependencies || {}; if (deps.lodash) { suggestions.push('Consider using lodash-es for better tree shaking'); } if (!packageJson.type || packageJson.type !== 'module') { suggestions.push('Consider using ES modules for better tree shaking'); } return suggestions; } async function checkSensitiveFiles(workspacePath) { const sensitivePatterns = ['.env', '.env.local', '.env.production', 'secrets.json', 'private.key']; const sensitiveFiles = []; for (const pattern of sensitivePatterns) { const filePath = path.join(workspacePath, pattern); if (await fs.pathExists(filePath)) { sensitiveFiles.push(pattern); } } return sensitiveFiles; } async function scanForSecrets(workspacePath) { // Basic secret pattern detection const secretPatterns = []; const patterns = [ /api[_-]?key/i, /secret[_-]?key/i, /password/i, /token/i ]; try { const srcPath = path.join(workspacePath, 'src'); if (await fs.pathExists(srcPath)) { // This would need more sophisticated scanning in a real implementation // For now, just return empty array } } catch (error) { // Ignore errors } return secretPatterns; } function generateSecurityRecommendations(audit, sensitiveFiles, secrets) { const recommendations = []; if (sensitiveFiles.length > 0) { recommendations.push('Add sensitive files to .gitignore'); } if (secrets.length > 0) { recommendations.push('Use environment variables for sensitive data'); } if (audit.metadata?.vulnerabilities?.total > 0) { recommendations.push('Run npm audit fix to address vulnerabilities'); } recommendations.push('Enable dependabot for automatic security updates'); recommendations.push('Use npm audit in CI/CD pipeline'); return recommendations; } function formatBytes(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } function getFileType(ext) { const types = { '.js': 'JavaScript', '.ts': 'TypeScript', '.css': 'Stylesheet', '.html': 'HTML', '.json': 'JSON', '.png': 'Image', '.jpg': 'Image', '.svg': 'SVG', '.woff': 'Font', '.woff2': 'Font' }; return types[ext] || 'Other'; } function displayAnalysisResults(results, options) { if (options.json) { console.log(JSON.stringify(results, null, 2)); return; } if (options.spinner) { options.spinner.stop(); } console.log('\n' + chalk_1.default.bold('📊 Re-Shell Project Analysis\n')); console.log(chalk_1.default.bold('Summary:')); console.log(` Monorepo: ${results.monorepo}`); console.log(` Workspaces analyzed: ${results.workspaces}`); console.log(` Generated: ${new Date(results.timestamp).toLocaleString()}`); console.log(); // Display results for each workspace for (const [workspace, analysis] of Object.entries(results.analysis)) { const workspaceAnalysis = analysis; console.log(chalk_1.default.bold(`📦 ${workspace}`)); if (workspaceAnalysis.bundle) { console.log(chalk_1.default.blue(' Bundle Analysis:')); console.log(` Total size: ${workspaceAnalysis.bundle.size.total}`); console.log(` Assets: ${workspaceAnalysis.bundle.size.assets.length}`); console.log(` Chunks: ${workspaceAnalysis.bundle.chunks.length}`); } if (workspaceAnalysis.dependencies) { console.log(chalk_1.default.blue(' Dependencies:')); console.log(` Total: ${workspaceAnalysis.dependencies.total}`); console.log(` Outdated: ${workspaceAnalysis.dependencies.outdated.length}`); console.log(` Vulnerabilities: ${workspaceAnalysis.dependencies.vulnerabilities.reduce((sum, v) => sum + v.count, 0)}`); } if (workspaceAnalysis.performance) { console.log(chalk_1.default.blue(' Performance:')); console.log(` Build time: ${workspaceAnalysis.performance.buildTime > 0 ? workspaceAnalysis.performance.buildTime + 'ms' : 'N/A'}`); console.log(` Bundle size: ${workspaceAnalysis.performance.bundleSize}`); console.log(` Suggestions: ${workspaceAnalysis.performance.suggestions.length}`); } if (workspaceAnalysis.security) { console.log(chalk_1.default.blue(' Security:')); console.log(` Sensitive files: ${workspaceAnalysis.security.sensitiveFiles.length}`); console.log(` Recommendations: ${workspaceAnalysis.security.recommendations.length}`); } console.log(); } console.log(chalk_1.default.dim('Use --verbose for detailed breakdown')); console.log(chalk_1.default.dim('Use --output <file> to save results')); }