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