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
JavaScript
;
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;