UNPKG

neex

Version:

Neex - Modern Fullstack Framework Built on Express and Next.js. Fast to Start, Easy to Build, Ready to Deploy

368 lines (367 loc) 15.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.BuildManager = void 0; // src/build-manager.ts - Build manager for TypeScript projects using tsc const child_process_1 = require("child_process"); const chokidar_1 = require("chokidar"); const logger_manager_js_1 = require("./logger-manager.js"); const chalk_1 = __importDefault(require("chalk")); const figures_1 = __importDefault(require("figures")); const path_1 = __importDefault(require("path")); const promises_1 = __importDefault(require("fs/promises")); const fs_1 = require("fs"); class BuildManager { constructor(options) { this.watcher = null; this.buildProcess = null; this.isBuilding = false; this.buildCount = 0; this.debouncedBuild = this.debounce(this.runBuild.bind(this), 300); this.options = options; } async cleanOutputDirectory() { if (this.options.clean && (0, fs_1.existsSync)(this.options.output)) { try { await promises_1.default.rm(this.options.output, { recursive: true, force: true }); if (this.options.verbose) { logger_manager_js_1.loggerManager.printLine(`Cleaned output directory: ${this.options.output}`, 'info'); } } catch (error) { logger_manager_js_1.loggerManager.printLine(`Failed to clean output directory: ${error.message}`, 'warn'); } } } async ensureOutputDirectory() { try { await promises_1.default.mkdir(this.options.output, { recursive: true }); } catch (error) { throw new Error(`Failed to create output directory: ${error.message}`); } } async validateTsConfig() { if (!(0, fs_1.existsSync)(this.options.tsconfig)) { throw new Error(`TypeScript config file not found: ${this.options.tsconfig}`); } } async copyPackageJson() { var _a; const packageJsonPath = path_1.default.join(process.cwd(), 'package.json'); const outputPackageJsonPath = path_1.default.join(this.options.output, 'package.json'); if ((0, fs_1.existsSync)(packageJsonPath)) { try { const packageJson = JSON.parse(await promises_1.default.readFile(packageJsonPath, 'utf8')); // Create production package.json const prodPackageJson = { name: packageJson.name, version: packageJson.version, description: packageJson.description, main: ((_a = packageJson.main) === null || _a === void 0 ? void 0 : _a.replace(/^src\//, '')) || 'index.js', type: this.options.format === 'esm' ? 'module' : 'commonjs', scripts: { start: 'node index.js' }, dependencies: packageJson.dependencies || {}, engines: packageJson.engines }; await promises_1.default.writeFile(outputPackageJsonPath, JSON.stringify(prodPackageJson, null, 2)); if (this.options.verbose) { logger_manager_js_1.loggerManager.printLine('Generated production package.json', 'info'); } } catch (error) { logger_manager_js_1.loggerManager.printLine(`Failed to copy package.json: ${error.message}`, 'warn'); } } } getTscCommand() { const args = [ '--project', this.options.tsconfig, '--outDir', this.options.output, '--target', this.options.target, '--declaration' ]; if (this.options.sourcemap) { args.push('--sourceMap'); } if (this.options.format === 'esm') { args.push('--module', 'es2020', '--moduleResolution', 'node'); } else { args.push('--module', 'commonjs'); } // Always include these for better compatibility args.push('--esModuleInterop', '--allowSyntheticDefaultImports', '--strict'); return { command: 'tsc', args }; } async runBuild() { if (this.isBuilding) { return; } this.isBuilding = true; this.buildCount++; const startTime = Date.now(); if (!this.options.quiet) { const buildNumber = this.options.watch ? ` #${this.buildCount}` : ''; logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.green(figures_1.default.play)} Building${buildNumber}...`, 'info'); } try { await this.ensureOutputDirectory(); const { command, args } = this.getTscCommand(); if (this.options.verbose) { logger_manager_js_1.loggerManager.printLine(`Executing: ${command} ${args.join(' ')}`, 'info'); } return new Promise((resolve, reject) => { var _a, _b; this.buildProcess = (0, child_process_1.spawn)(command, args, { stdio: ['ignore', 'pipe', 'pipe'], // Capture stdout and stderr shell: false, env: { ...process.env, FORCE_COLOR: '0' // Disable TSC colors to avoid log pollution } }); let stdout = ''; let stderr = ''; // Capture all output but don't display TSC logs (_a = this.buildProcess.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (data) => { stdout += data.toString(); }); (_b = this.buildProcess.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (data) => { stderr += data.toString(); }); this.buildProcess.on('error', (error) => { this.buildProcess = null; this.isBuilding = false; reject(new Error(`Build process error: ${error.message}`)); }); this.buildProcess.on('exit', async (code) => { this.buildProcess = null; this.isBuilding = false; const duration = Date.now() - startTime; if (code === 0) { // Copy package.json after successful build await this.copyPackageJson(); if (!this.options.quiet) { logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.green(figures_1.default.tick)} Build completed in ${duration}ms`, 'info'); } if (this.options.analyze) { await this.analyzeBuild(); } resolve(); } else { // Only show meaningful errors, filter out TSC verbosity const meaningfulErrors = this.filterTscErrors(stderr); if (meaningfulErrors) { logger_manager_js_1.loggerManager.printLine(`Build failed:\n${meaningfulErrors}`, 'error'); } else { logger_manager_js_1.loggerManager.printLine(`Build failed with code ${code}`, 'error'); } reject(new Error(`Build failed with code ${code}`)); } }); }); } catch (error) { this.isBuilding = false; throw error; } } filterTscErrors(stderr) { if (!stderr) return ''; const lines = stderr.split('\n'); const meaningfulLines = lines.filter(line => { const trimmed = line.trim(); // Filter out TSC verbose output, keep only actual errors return trimmed && !trimmed.includes('message TS') && !trimmed.includes('Found 0 errors') && !trimmed.match(/^\s*\d+\s*$/) && // Filter line numbers !trimmed.includes('Watching for file changes'); }); return meaningfulLines.join('\n').trim(); } async analyzeBuild() { try { const files = await promises_1.default.readdir(this.options.output, { withFileTypes: true }); let totalSize = 0; const fileStats = []; for (const file of files) { if (file.isFile() && (file.name.endsWith('.js') || file.name.endsWith('.d.ts'))) { const filePath = path_1.default.join(this.options.output, file.name); const stat = await promises_1.default.stat(filePath); totalSize += stat.size; fileStats.push({ name: file.name, size: stat.size }); } } fileStats.sort((a, b) => b.size - a.size); logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.blue(figures_1.default.info)} Build Analysis:`, 'info'); logger_manager_js_1.loggerManager.printLine(`Total size: ${chalk_1.default.cyan(this.formatBytes(totalSize))}`, 'info'); logger_manager_js_1.loggerManager.printLine(`Generated files: ${fileStats.length}`, 'info'); if (this.options.verbose && fileStats.length > 0) { const topFiles = fileStats.slice(0, 5); logger_manager_js_1.loggerManager.printLine('Largest files:', 'info'); topFiles.forEach(file => { logger_manager_js_1.loggerManager.printLine(` ${file.name}: ${this.formatBytes(file.size)}`, 'info'); }); } } catch (error) { logger_manager_js_1.loggerManager.printLine(`Failed to analyze build: ${error.message}`, 'warn'); } } async stopProcess() { if (!this.buildProcess) { return; } return new Promise((resolve) => { if (!this.buildProcess) { resolve(); return; } const proc = this.buildProcess; this.buildProcess = null; const cleanup = () => { if (!this.options.quiet) { logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.yellow(figures_1.default.square)} Build process stopped`, 'info'); } resolve(); }; proc.on('exit', cleanup); proc.on('error', cleanup); try { if (proc.pid) { // Kill process group process.kill(-proc.pid, 'SIGTERM'); // Fallback after timeout setTimeout(() => { if (proc.pid && !proc.killed) { try { process.kill(-proc.pid, 'SIGKILL'); } catch (e) { // Ignore } } }, 3000); } } catch (error) { // Process might already be dead cleanup(); } }); } 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]; } setupWatcher() { const watchPatterns = [ `${this.options.source}/**/*.ts`, `${this.options.source}/**/*.tsx`, `${this.options.source}/**/*.js`, `${this.options.source}/**/*.jsx`, this.options.tsconfig ]; this.watcher = (0, chokidar_1.watch)(watchPatterns, { ignoreInitial: true, followSymlinks: false, usePolling: false, atomic: 200, ignored: [ '**/node_modules/**', '**/.git/**', `**/${this.options.output}/**`, '**/*.log', '**/*.map' ] }); this.watcher.on('change', (filePath) => { if (this.options.verbose) { logger_manager_js_1.loggerManager.printLine(`File changed: ${path_1.default.relative(process.cwd(), filePath)}`, 'info'); } this.debouncedBuild(); }); this.watcher.on('add', (filePath) => { if (this.options.verbose) { logger_manager_js_1.loggerManager.printLine(`File added: ${path_1.default.relative(process.cwd(), filePath)}`, 'info'); } this.debouncedBuild(); }); this.watcher.on('unlink', (filePath) => { if (this.options.verbose) { logger_manager_js_1.loggerManager.printLine(`File removed: ${path_1.default.relative(process.cwd(), filePath)}`, 'info'); } this.debouncedBuild(); }); this.watcher.on('error', (error) => { logger_manager_js_1.loggerManager.printLine(`Watcher error: ${error.message}`, 'error'); }); if (this.options.verbose) { logger_manager_js_1.loggerManager.printLine(`Watching: ${watchPatterns.join(', ')}`, 'info'); } } debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } async build() { // Check if source directory exists if (!(0, fs_1.existsSync)(this.options.source)) { throw new Error(`Source directory not found: ${this.options.source}`); } try { await this.validateTsConfig(); await this.cleanOutputDirectory(); await this.runBuild(); if (this.options.watch) { this.setupWatcher(); if (!this.options.quiet) { logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.blue(figures_1.default.info)} Watching for changes...`, 'info'); } } } catch (error) { logger_manager_js_1.loggerManager.printLine(error.message, 'error'); if (!this.options.watch) { process.exit(1); } } } async stop() { if (!this.options.quiet) { logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.yellow('⏹')} Stopping build process...`, 'info'); } if (this.watcher) { await this.watcher.close(); this.watcher = null; } await this.stopProcess(); if (this.buildCount > 0 && !this.options.quiet) { logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.blue('ℹ')} Build process stopped after ${this.buildCount} build(s).`, 'info'); } } } exports.BuildManager = BuildManager;