UNPKG

neex

Version:

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

402 lines (401 loc) 17.6 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.Runner = void 0; // src/runner.ts - Fixed version const child_process_1 = require("child_process"); const fsPromises = __importStar(require("fs/promises")); const path = __importStar(require("path")); const chalk_1 = __importDefault(require("chalk")); const logger_1 = __importDefault(require("./logger")); const p_map_1 = __importDefault(require("p-map")); const npm_run_path_1 = require("npm-run-path"); const fs = __importStar(require("fs")); class Runner { constructor(options) { this.activeProcesses = new Map(); this.serverInfo = new Map(); this.portRegex = /listening on (?:port |http:\/\/localhost:|https:\/\/localhost:)(\d+)/i; this.urlRegex = /(https?:\/\/localhost:[0-9]+(?:\/[^\s]*)?)/i; this.isCleaningUp = false; this.options = options; this.activeProcesses = new Map(); this.setupSignalHandlers(); } setupSignalHandlers() { const signals = ['SIGINT', 'SIGTERM', 'SIGQUIT']; signals.forEach((signal) => { process.on(signal, () => { if (!this.isCleaningUp) { this.isCleaningUp = true; logger_1.default.printLine(`\nReceived ${signal}. Cleaning up...`, 'warn'); this.cleanup(signal); process.exit(0); } }); }); // Handle unexpected exits process.on('beforeExit', () => { if (!this.isCleaningUp) { this.cleanup(); } }); } async expandWildcardCommands(commands) { const expandedCommands = []; let packageJson; try { const packageJsonPath = path.join(process.cwd(), 'package.json'); if (fs.existsSync(packageJsonPath)) { const packageJsonContent = await fsPromises.readFile(packageJsonPath, 'utf-8'); packageJson = JSON.parse(packageJsonContent); } } catch (error) { logger_1.default.printLine(`Could not read or parse package.json: ${error.message}`, 'warn'); packageJson = { scripts: {} }; } for (const command of commands) { if (command.includes('*') && packageJson && packageJson.scripts) { const pattern = new RegExp(`^${command.replace(/\*/g, '.*')}$`); let foundMatch = false; for (const scriptName in packageJson.scripts) { if (pattern.test(scriptName)) { expandedCommands.push(scriptName); foundMatch = true; } } if (!foundMatch) { logger_1.default.printLine(`No scripts found in package.json matching wildcard: ${command}`, 'warn'); expandedCommands.push(command); } } else { expandedCommands.push(command); } } return expandedCommands; } async resolveScriptAndCwd(scriptNameOrCommand, baseDir) { try { const packageJsonPath = path.join(baseDir, 'package.json'); const packageJsonContent = await fsPromises.readFile(packageJsonPath, 'utf-8'); const packageJson = JSON.parse(packageJsonContent); if (packageJson.scripts && packageJson.scripts[scriptNameOrCommand]) { const scriptValue = packageJson.scripts[scriptNameOrCommand]; const cdMatch = scriptValue.match(/^cd\s+([^&]+)\s+&&\s+(.*)$/); if (cdMatch) { const dir = cdMatch[1]; const commandToExecute = cdMatch[2]; const targetCwd = path.resolve(baseDir, dir); return { executableCommand: commandToExecute, executionCwd: targetCwd }; } else { return { executableCommand: scriptValue, executionCwd: baseDir }; } } } catch (error) { // Will treat as direct command } return { executableCommand: scriptNameOrCommand, executionCwd: undefined }; } detectServerInfo(command, data) { if (!this.options.isServerMode) return; let serverInfo = this.serverInfo.get(command); if (!serverInfo) { serverInfo = { name: command, status: 'starting' }; this.serverInfo.set(command, serverInfo); } const portMatch = data.match(this.portRegex); if (portMatch && portMatch[1]) { serverInfo.port = parseInt(portMatch[1], 10); serverInfo.status = 'running'; if (!serverInfo.url) { logger_1.default.printLine(`Server ${command} running on port ${serverInfo.port}`, 'info'); } } const urlMatch = data.match(this.urlRegex); if (urlMatch && urlMatch[1]) { serverInfo.url = urlMatch[1]; serverInfo.status = 'running'; logger_1.default.printLine(`Server ${command} available at ${chalk_1.default.cyan(serverInfo.url)}`, 'info'); } this.serverInfo.set(command, serverInfo); } async runCommand(originalCommand, currentRetry = 0) { const { executableCommand: command, executionCwd: cwd } = await this.resolveScriptAndCwd(originalCommand, process.cwd()); const startTime = new Date(); const result = { command: originalCommand, success: false, code: null, startTime, endTime: null, output: [], stderr: [] }; if (this.options.printOutput) { logger_1.default.printStart(originalCommand); } return new Promise(async (resolve) => { var _a, _b; const [cmd, ...args] = command.split(' '); const env = { ...process.env, ...(0, npm_run_path_1.npmRunPathEnv)(), FORCE_COLOR: this.options.color ? '1' : '0' }; // Fix: Remove detached and handle process groups properly const proc = (0, child_process_1.spawn)(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'], shell: true, env, cwd }); this.activeProcesses.set(originalCommand, proc); if (this.options.isServerMode) { this.serverInfo.set(originalCommand, { name: originalCommand, status: 'starting', pid: proc.pid, startTime: new Date() }); } (_a = proc.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (data) => { const output = { command: originalCommand, type: 'stdout', data: data.toString(), timestamp: new Date() }; if (this.options.isServerMode) this.detectServerInfo(originalCommand, data.toString()); if (result.output) result.output.push(output); logger_1.default.bufferOutput(output); if (!this.options.groupOutput && this.options.printOutput) logger_1.default.printBuffer(originalCommand); }); (_b = proc.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (data) => { const output = { command: originalCommand, type: 'stderr', data: data.toString(), timestamp: new Date() }; if (result.output) result.output.push(output); logger_1.default.bufferOutput(output); if (!this.options.groupOutput && this.options.printOutput) logger_1.default.printBuffer(originalCommand); }); proc.on('error', async (err) => { result.endTime = new Date(); result.error = err; result.success = false; result.duration = result.endTime.getTime() - startTime.getTime(); this.activeProcesses.delete(originalCommand); if (this.options.isServerMode) { const serverInfo = this.serverInfo.get(originalCommand); if (serverInfo) { serverInfo.status = 'error'; this.serverInfo.set(originalCommand, serverInfo); } logger_1.default.printLine(`Command "${originalCommand}" failed to start: ${err.message}`, 'error'); } logger_1.default.printBuffer(originalCommand); if (this.options.printOutput) logger_1.default.printEnd(result, this.options.minimalOutput); if (this.options.retry && this.options.retry > 0 && currentRetry < this.options.retry) { logger_1.default.printLine(`Command "${originalCommand}" failed with error. Retrying (${currentRetry + 1}/${this.options.retry})...`, 'warn'); if (this.options.retryDelay && this.options.retryDelay > 0) { await new Promise(res => setTimeout(res, this.options.retryDelay)); } logger_1.default.clearBuffer(originalCommand); resolve(this.runCommand(originalCommand, currentRetry + 1)); } else { resolve(result); } }); proc.on('close', async (code) => { result.code = code; result.success = code === 0; result.endTime = new Date(); result.duration = result.endTime.getTime() - startTime.getTime(); this.activeProcesses.delete(originalCommand); if (this.options.isServerMode) { const serverInfo = this.serverInfo.get(originalCommand); if (serverInfo) { serverInfo.status = code === 0 ? 'stopped' : 'error'; this.serverInfo.set(originalCommand, serverInfo); } if (code !== 0) { logger_1.default.printLine(`Server "${originalCommand}" exited with code ${code}`, 'error'); } } logger_1.default.printBuffer(originalCommand); if (this.options.printOutput) logger_1.default.printEnd(result, this.options.minimalOutput); if (!result.success && this.options.retry && this.options.retry > 0 && currentRetry < this.options.retry) { logger_1.default.printLine(`Command "${originalCommand}" failed with code ${code}. Retrying (${currentRetry + 1}/${this.options.retry})...`, 'warn'); if (this.options.retryDelay && this.options.retryDelay > 0) { await new Promise(res => setTimeout(res, this.options.retryDelay)); } logger_1.default.clearBuffer(originalCommand); resolve(this.runCommand(originalCommand, currentRetry + 1)); } else { resolve(result); } }); }); } async runSequential(commands) { const results = []; for (const cmd of commands) { const result = await this.runCommand(cmd); results.push(result); if (!result.success && this.options.stopOnError) { break; } } return results; } async runParallel(commands) { const concurrency = this.options.maxParallel || commands.length; const mapper = async (cmd) => { return this.runCommand(cmd); }; try { return await (0, p_map_1.default)(commands, mapper, { concurrency, stopOnError: this.options.stopOnError }); } catch (error) { if (this.options.isServerMode) { logger_1.default.printLine('One or more servers failed to start. Stopping all servers.', 'error'); } return []; } } async run(initialCommands) { const commands = await this.expandWildcardCommands(initialCommands); if (commands.length === 0) { logger_1.default.printLine('No commands to run after wildcard expansion.', 'warn'); return []; } logger_1.default.setCommands(commands); if (this.options.parallel) { if (this.options.isServerMode) { logger_1.default.printLine('Starting servers in parallel mode', 'info'); } return this.runParallel(commands); } else { if (this.options.isServerMode) { logger_1.default.printLine('Starting servers in sequential mode', 'info'); } return this.runSequential(commands); } } cleanup(signal = 'SIGTERM') { if (this.isCleaningUp) return; this.isCleaningUp = true; logger_1.default.printLine('Cleaning up child processes...', 'warn'); const promises = []; this.activeProcesses.forEach((proc, command) => { if (proc.pid && !proc.killed) { promises.push(this.killProcess(proc, command, signal)); } }); // Wait for all processes to be killed Promise.all(promises).then(() => { this.activeProcesses.clear(); if (this.options.isServerMode && this.serverInfo.size > 0) { logger_1.default.printLine('Server shutdown summary:', 'info'); this.serverInfo.forEach((info, command) => { const statusColor = info.status === 'running' ? chalk_1.default.green : info.status === 'error' ? chalk_1.default.red : chalk_1.default.yellow; logger_1.default.printLine(` ${command}: ${statusColor(info.status)}`, 'info'); }); } }).catch((error) => { logger_1.default.printLine(`Error during cleanup: ${error.message}`, 'error'); }); } async killProcess(proc, command, signal) { return new Promise((resolve) => { const timeout = setTimeout(() => { if (proc.pid && !proc.killed) { try { proc.kill('SIGKILL'); logger_1.default.printLine(`Force killed process ${proc.pid} (${command}) with SIGKILL`, 'warn'); } catch (error) { logger_1.default.printLine(`Failed to force kill process ${proc.pid} (${command}): ${error.message}`, 'error'); } } resolve(); }, 5000); // 5 second timeout proc.on('exit', () => { clearTimeout(timeout); logger_1.default.printLine(`Process ${proc.pid} (${command}) exited gracefully`, 'info'); resolve(); }); try { // Try to kill the process gracefully first proc.kill(signal); logger_1.default.printLine(`Sent ${signal} to process ${proc.pid} (${command})`, 'info'); } catch (error) { logger_1.default.printLine(`Failed to send ${signal} to process ${proc.pid} (${command}): ${error.message}`, 'error'); clearTimeout(timeout); resolve(); } }); } } exports.Runner = Runner;