UNPKG

accs-cli

Version:

ACCS CLI — Full-featured developer tool for scaffolding, running, building, and managing multi-language projects

322 lines (274 loc) 10.6 kB
/** * Build command - Compile and optimize project files */ import path from 'path'; import chalk from 'chalk'; import { logger } from '../utils/logger.js'; import { FileUtils } from '../utils/file-utils.js'; import { configManager } from '../config/config-manager.js'; import babel from '@babel/core'; export function buildCommand(program) { program .command('build') .option('-m, --minify', 'Minify output files') .option('-s, --sourcemap', 'Generate source maps') .option('-o, --output <dir>', 'Output directory', 'dist') .option('-c, --clean', 'Clean output directory first') .option('-v, --verbose', 'Verbose output') .description('Build project for production') .action(async (options) => { try { await buildProject(options); } catch (error) { logger.error('Build failed:', error.message); process.exit(1); } }); } async function buildProject(options) { const projectRoot = FileUtils.getProjectRoot(); const srcDir = path.join(projectRoot, configManager.get('srcDir', 'src')); const outputDir = path.join(projectRoot, options.output || configManager.get('buildDir', 'dist')); logger.info(`Building project from ${chalk.cyan(path.relative(projectRoot, srcDir))} to ${chalk.cyan(path.relative(projectRoot, outputDir))}`); // Clean output directory if requested if (options.clean && FileUtils.exists(outputDir)) { logger.startSpinner('Cleaning output directory...'); await FileUtils.remove(outputDir); logger.succeedSpinner('Output directory cleaned'); } // Create output directory await FileUtils.createDir(outputDir); // Check if it's a Node.js project with build scripts const packageJsonPath = path.join(projectRoot, 'package.json'); if (FileUtils.exists(packageJsonPath)) { const packageJson = await FileUtils.readJson(packageJsonPath); // Run existing build scripts if available if (packageJson.scripts?.build && packageJson.scripts.build !== 'accs build') { await runExistingBuildScript(projectRoot, options); return; } } // Default build process const buildStats = { filesProcessed: 0, errors: 0, startTime: Date.now() }; logger.startSpinner('Processing files...'); try { if (FileUtils.exists(srcDir)) { await processSrcDirectory(srcDir, outputDir, options, buildStats, projectRoot); } else { // Fallback: build from root directory await processRootDirectory(projectRoot, outputDir, options, buildStats, projectRoot); } const duration = Date.now() - buildStats.startTime; logger.succeedSpinner(`Build completed in ${duration}ms`); // Show build statistics logger.separator(); logger.success(`Files processed: ${buildStats.filesProcessed}`); if (buildStats.errors > 0) { logger.warn(`Errors: ${buildStats.errors}`); } logger.info(`Output: ${chalk.cyan(path.relative(process.cwd(), outputDir))}`); } catch (error) { logger.failSpinner('Build failed'); throw error; } } async function runExistingBuildScript(projectRoot, options) { logger.info('Running existing build script...'); try { const { execa } = await import('execa'); const subprocess = execa('npm', ['run', 'build'], { cwd: projectRoot, stdio: options.verbose ? 'inherit' : 'pipe' }); if (!options.verbose) { logger.startSpinner('Building...'); await subprocess; logger.succeedSpinner('Build completed using npm script'); } else { await subprocess; logger.success('Build completed using npm script'); } } catch (error) { throw new Error(`Build script failed: ${error.message}`); } } async function processSrcDirectory(srcDir, outputDir, options, stats, projectRoot) { const entries = await import('fs').then(fs => fs.promises.readdir(srcDir)); for (const entry of entries) { const srcPath = path.join(srcDir, entry); const destPath = path.join(outputDir, entry); if (FileUtils.isDirectory(srcPath)) { await FileUtils.createDir(destPath); await processSrcDirectory(srcPath, destPath, options, stats, projectRoot); } else { await processFile(srcPath, destPath, options, stats, projectRoot); } } } async function processRootDirectory(rootDir, outputDir, options, stats, projectRoot) { const entries = await import('fs').then(fs => fs.promises.readdir(rootDir)); const excludeFiles = [ 'node_modules', '.git', '.DS_Store', 'package.json', 'package-lock.json', '.gitignore', 'README.md', 'LICENSE', outputDir.split(path.sep).pop() ]; for (const entry of entries) { if (excludeFiles.includes(entry)) continue; const srcPath = path.join(rootDir, entry); const destPath = path.join(outputDir, entry); if (FileUtils.isDirectory(srcPath)) { await FileUtils.createDir(destPath); await processRootDirectory(srcPath, destPath, options, stats, projectRoot); } else { await processFile(srcPath, destPath, options, stats, projectRoot); } } } async function processFile(srcPath, destPath, options, stats, projectRoot) { try { const extension = FileUtils.getExtension(srcPath); // Process based on file type switch (extension) { case '.js': case '.mjs': await processJavaScript(srcPath, destPath, options); break; case '.ts': case '.tsx': case '.jsx': await processBabel(srcPath, destPath, options, stats, projectRoot); break; case '.css': await processCSS(srcPath, destPath, options); break; case '.html': await processHTML(srcPath, destPath, options); break; case '.json': await processJSON(srcPath, destPath, options); break; default: // Copy as-is for other file types await FileUtils.copy(srcPath, destPath); } stats.filesProcessed++; if (options.verbose) { logger.debug(`Processed: ${path.relative(process.cwd(), srcPath)}`, true); } } catch (error) { stats.errors++; logger.error(`Failed to process ${path.relative(process.cwd(), srcPath)}: ${error.message}`); } } async function processJavaScript(srcPath, destPath, options) { let content = await import('fs').then(fs => fs.promises.readFile(srcPath, 'utf8')); if (options.minify) { try { // Simple minification (remove comments and extra whitespace) content = content .replace(/\/\*[\s\S]*?\*\//g, '') // Remove block comments .replace(/\/\/.*$/gm, '') // Remove line comments .replace(/\s+/g, ' ') // Collapse whitespace .trim(); } catch (error) { logger.warn(`Failed to minify ${srcPath}, copying as-is`); } } await import('fs').then(fs => fs.promises.writeFile(destPath, content, 'utf8')); } async function processBabel(srcPath, destPath, options, stats, projectRoot, retryCount = 0) { const MAX_RETRIES = 1; try { const content = await import('fs').then(fs => fs.promises.readFile(srcPath, 'utf8')); const isTsx = srcPath.endsWith('.tsx'); const isTs = srcPath.endsWith('.ts'); const presets = [ '@babel/preset-env', '@babel/preset-react', ]; if (isTs || isTsx) { presets.push('@babel/preset-typescript'); } const result = await babel.transformAsync(content, { presets, sourceMaps: options.sourcemap, filename: srcPath, root: projectRoot, }); if (result.code) { const jsDestPath = destPath.replace(/\.(ts|tsx|jsx)$/, '.js'); await import('fs').then(fs => fs.promises.writeFile(jsDestPath, result.code, 'utf8')); if (options.sourcemap && result.map) { await import('fs').then(fs => fs.promises.writeFile(`${jsDestPath}.map`, JSON.stringify(result.map), 'utf8')); } } } catch (error) { if (error.message.includes("Cannot find package") && retryCount < MAX_RETRIES) { const packageNameMatch = error.message.match(/Cannot find package '([^']*)'/); if (packageNameMatch && packageNameMatch[1]) { const packageName = packageNameMatch[1]; logger.warn(`Missing Babel preset '${packageName}'. Installing...`); try { const { execa } = await import('execa'); await execa('npm', ['install', '--save-dev', packageName], { cwd: projectRoot }); await new Promise(resolve => setTimeout(resolve, 1000)); // 1 second delay await execa('npm', ['rebuild'], { cwd: projectRoot }); logger.success(`Installed '${packageName}'. Retrying transpilation...`); await processBabel(srcPath, destPath, options, stats, projectRoot, retryCount + 1); } catch (installError) { logger.error(`Failed to install '${packageName}': ${installError.message}`); stats.errors++; } } } else { logger.error(`Failed to transpile ${srcPath}: ${error.message}`); stats.errors++; } } } async function processCSS(srcPath, destPath, options) { let content = await import('fs').then(fs => fs.promises.readFile(srcPath, 'utf8')); if (options.minify) { try { // Simple CSS minification content = content .replace(/\/\*[\s\S]*?\*\//g, '') // Remove comments .replace(/\s+/g, ' ') // Collapse whitespace .replace(/;\s*}/g, '}') // Remove last semicolon before } .replace(/\s*{\s*/g, '{') // Clean up braces .replace(/;\s*/g, ';') // Clean up semicolons .trim(); } catch (error) { logger.warn(`Failed to minify ${srcPath}, copying as-is`); } } await import('fs').then(fs => fs.promises.writeFile(destPath, content, 'utf8')); } async function processHTML(srcPath, destPath, options) { let content = await import('fs').then(fs => fs.promises.readFile(srcPath, 'utf8')); if (options.minify) { try { // Simple HTML minification content = content .replace(/<!--[\s\S]*?-->/g, '') // Remove comments .replace(/\s+/g, ' ') // Collapse whitespace .replace(/>\s+</g, '><') // Remove whitespace between tags .trim(); } catch (error) { logger.warn(`Failed to minify ${srcPath}, copying as-is`); } } await import('fs').then(fs => fs.promises.writeFile(destPath, content, 'utf8')); } async function processJSON(srcPath, destPath, options) { const data = await FileUtils.readJson(srcPath); if (options.minify) { // Minify JSON by removing extra whitespace await FileUtils.writeJson(destPath, data, 0); } else { await FileUtils.writeJson(destPath, data, 2); } }