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