neex 0.6.51 → 0.6.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,29 +10,22 @@ const chalk_1 = __importDefault(require("chalk"));
10
10
  const figures_1 = __importDefault(require("figures"));
11
11
  const path_1 = __importDefault(require("path"));
12
12
  const fs_1 = __importDefault(require("fs"));
13
+ const os_1 = __importDefault(require("os"));
13
14
  function addStartCommands(program) {
14
15
  let startManager = null;
15
- // Start command for production execution
16
+ // Single optimized start command for production
16
17
  program
17
18
  .command('start [file]')
18
- .description('Start production application (default: dist/index.js)')
19
+ .description('Start production application with automatic optimization')
19
20
  .option('-d, --dir <directory>', 'Working directory', process.cwd())
20
21
  .option('-e, --env <file>', 'Environment file to load', '.env')
21
- .option('-p, --port <port>', 'Port number (if applicable)', parseInt)
22
- .option('-w, --workers <count>', 'Number of worker processes', parseInt, 1)
23
- .option('-c, --cluster', 'Enable cluster mode')
24
- .option('-m, --memory <limit>', 'Memory limit (e.g., 512M, 1G)')
25
- .option('-l, --log-level <level>', 'Log level (error, warn, info, debug)', 'info')
26
- .option('-f, --log-file <file>', 'Log file path')
27
- .option('--no-color', 'Disable colored output')
28
- .option('-q, --quiet', 'Quiet mode')
22
+ .option('-p, --port <port>', 'Port number', parseInt)
23
+ .option('-w, --workers <count>', 'Number of worker processes (default: auto)', parseInt)
29
24
  .option('-v, --verbose', 'Verbose output')
30
- .option('--watch', 'Watch for changes and restart')
31
- .option('--max-memory <limit>', 'Maximum memory before restart (e.g., 1G)')
32
- .option('--max-crashes <count>', 'Maximum crashes before giving up', parseInt, 5)
33
- .option('--restart-delay <ms>', 'Delay between restarts (ms)', parseInt, 1000)
34
- .option('--health-check', 'Enable health check monitoring')
25
+ .option('--watch', 'Watch for changes and restart (development mode)')
26
+ .option('--no-health', 'Disable health check endpoint')
35
27
  .option('--health-port <port>', 'Health check port', parseInt, 3001)
28
+ .option('--max-memory <limit>', 'Maximum memory before restart (e.g., 1G)')
36
29
  .option('--graceful-timeout <ms>', 'Graceful shutdown timeout (ms)', parseInt, 30000)
37
30
  .option('--inspect', 'Enable Node.js inspector')
38
31
  .option('--inspect-brk', 'Enable Node.js inspector with break')
@@ -40,49 +33,109 @@ function addStartCommands(program) {
40
33
  .action(async (file, options) => {
41
34
  try {
42
35
  const targetFile = file || 'dist/index.js';
43
- const resolvedFile = path_1.default.resolve(options.dir, targetFile);
44
- // Check if file exists
36
+ let resolvedFile = path_1.default.resolve(options.dir, targetFile);
37
+ // Auto-detect main file if not found
45
38
  if (!fs_1.default.existsSync(resolvedFile)) {
46
- // Try to find main file from package.json
47
39
  const packageJsonPath = path_1.default.join(options.dir, 'package.json');
48
40
  if (fs_1.default.existsSync(packageJsonPath)) {
49
- const packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath, 'utf8'));
50
- const mainFile = packageJson.main || 'index.js';
51
- const alternativeFile = path_1.default.resolve(options.dir, mainFile);
52
- if (fs_1.default.existsSync(alternativeFile)) {
53
- if (options.verbose) {
54
- logger_manager_js_1.loggerManager.printLine(`Using main file from package.json: ${mainFile}`, 'info');
41
+ try {
42
+ const packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath, 'utf8'));
43
+ const mainFile = packageJson.main || 'index.js';
44
+ const alternativeFile = path_1.default.resolve(options.dir, mainFile);
45
+ if (fs_1.default.existsSync(alternativeFile)) {
46
+ resolvedFile = alternativeFile;
47
+ if (options.verbose) {
48
+ logger_manager_js_1.loggerManager.printLine(`Using main file: ${mainFile}`, 'info');
49
+ }
50
+ }
51
+ else {
52
+ // Try common locations
53
+ const commonLocations = [
54
+ 'dist/server.js',
55
+ 'dist/app.js',
56
+ 'build/index.js',
57
+ 'build/server.js',
58
+ 'src/index.js',
59
+ 'src/server.js',
60
+ 'index.js',
61
+ 'server.js'
62
+ ];
63
+ let found = false;
64
+ for (const location of commonLocations) {
65
+ const testPath = path_1.default.resolve(options.dir, location);
66
+ if (fs_1.default.existsSync(testPath)) {
67
+ resolvedFile = testPath;
68
+ found = true;
69
+ if (options.verbose) {
70
+ logger_manager_js_1.loggerManager.printLine(`Found application file: ${location}`, 'info');
71
+ }
72
+ break;
73
+ }
74
+ }
75
+ if (!found) {
76
+ throw new Error(`Application file not found. Tried: ${targetFile}, ${mainFile}, and common locations.`);
77
+ }
55
78
  }
56
79
  }
57
- else {
58
- throw new Error(`Neither ${targetFile} nor ${mainFile} found. Please build your project first.`);
80
+ catch (parseError) {
81
+ throw new Error(`Failed to parse package.json: ${parseError.message}`);
59
82
  }
60
83
  }
61
84
  else {
62
- throw new Error(`File not found: ${resolvedFile}. Please build your project first.`);
85
+ throw new Error(`Application file not found: ${resolvedFile}`);
86
+ }
87
+ }
88
+ // Detect environment and set optimal configuration
89
+ const isDevelopment = options.watch || process.env.NODE_ENV === 'development';
90
+ const isProduction = !isDevelopment;
91
+ const cpuCount = os_1.default.cpus().length;
92
+ // Smart worker allocation
93
+ let workers = options.workers;
94
+ if (!workers) {
95
+ if (isDevelopment) {
96
+ workers = 1; // Single worker for development
97
+ }
98
+ else {
99
+ // For production, use CPU count but cap at 8 for most applications
100
+ workers = Math.min(cpuCount, 8);
63
101
  }
64
102
  }
65
- if (!options.quiet) {
66
- logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.blue(figures_1.default.info)} Starting production server: ${chalk_1.default.cyan(targetFile)}`, 'info');
103
+ const healthCheck = options.health !== false && isProduction;
104
+ const defaultPort = parseInt(process.env.PORT || '8000');
105
+ const port = options.port || defaultPort;
106
+ // Environment setup
107
+ if (!process.env.NODE_ENV) {
108
+ process.env.NODE_ENV = isProduction ? 'production' : 'development';
109
+ }
110
+ // Startup logging
111
+ const mode = isDevelopment ? 'development' : 'production';
112
+ const clusterInfo = workers > 1 ? ` (${workers} workers)` : '';
113
+ logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.green(figures_1.default.play)} Starting ${mode} server${clusterInfo}`, 'info');
114
+ if (options.verbose) {
115
+ logger_manager_js_1.loggerManager.printLine(`File: ${path_1.default.relative(process.cwd(), resolvedFile)}`);
116
+ logger_manager_js_1.loggerManager.printLine(`Working Directory: ${options.dir}`);
117
+ logger_manager_js_1.loggerManager.printLine(`Environment: ${process.env.NODE_ENV}`);
118
+ logger_manager_js_1.loggerManager.printLine(`Port: ${port}`);
119
+ logger_manager_js_1.loggerManager.printLine(`Workers: ${workers}`);
120
+ if (healthCheck) {
121
+ logger_manager_js_1.loggerManager.printLine(`Health Check: http://localhost:${options.healthPort}/health`);
122
+ }
67
123
  }
68
124
  startManager = new start_manager_js_1.StartManager({
69
125
  file: resolvedFile,
70
126
  workingDir: options.dir,
71
127
  envFile: options.env,
72
- port: options.port,
73
- workers: options.workers,
74
- cluster: options.cluster,
75
- memoryLimit: options.memory,
76
- logLevel: options.logLevel,
77
- logFile: options.logFile,
78
- color: options.color,
79
- quiet: options.quiet,
128
+ port,
129
+ workers,
130
+ memoryLimit: isProduction ? '512M' : undefined,
131
+ logLevel: options.verbose ? 'debug' : 'info',
132
+ color: process.stdout.isTTY,
80
133
  verbose: options.verbose,
81
134
  watch: options.watch,
82
- maxMemory: options.maxMemory,
83
- maxCrashes: options.maxCrashes,
84
- restartDelay: options.restartDelay,
85
- healthCheck: options.healthCheck,
135
+ maxMemory: options.maxMemory || (isProduction ? '1G' : undefined),
136
+ maxCrashes: isProduction ? 3 : 10,
137
+ restartDelay: isProduction ? 2000 : 1000,
138
+ healthCheck,
86
139
  healthPort: options.healthPort,
87
140
  gracefulTimeout: options.gracefulTimeout,
88
141
  inspect: options.inspect,
@@ -93,113 +146,10 @@ function addStartCommands(program) {
93
146
  }
94
147
  catch (error) {
95
148
  if (error instanceof Error) {
96
- logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.red(figures_1.default.cross)} Start failed: ${error.message}`, 'error');
97
- }
98
- else {
99
- logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.red(figures_1.default.cross)} An unknown start error occurred`, 'error');
100
- }
101
- process.exit(1);
102
- }
103
- });
104
- // Quick start command
105
- program
106
- .command('run [file]')
107
- .description('Quick start without options (alias for start)')
108
- .action(async (file) => {
109
- try {
110
- const targetFile = file || 'dist/index.js';
111
- if (!fs_1.default.existsSync(targetFile)) {
112
- throw new Error(`File not found: ${targetFile}. Please build your project first.`);
113
- }
114
- logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.blue(figures_1.default.info)} Running: ${chalk_1.default.cyan(targetFile)}`, 'info');
115
- startManager = new start_manager_js_1.StartManager({
116
- file: path_1.default.resolve(targetFile),
117
- workingDir: process.cwd(),
118
- envFile: '.env',
119
- port: undefined,
120
- workers: 1,
121
- cluster: false,
122
- memoryLimit: undefined,
123
- logLevel: 'info',
124
- logFile: undefined,
125
- color: true,
126
- quiet: false,
127
- verbose: false,
128
- watch: false,
129
- maxMemory: undefined,
130
- maxCrashes: 5,
131
- restartDelay: 1000,
132
- healthCheck: false,
133
- healthPort: 3001,
134
- gracefulTimeout: 30000,
135
- inspect: false,
136
- inspectBrk: false,
137
- nodeArgs: undefined
138
- });
139
- await startManager.start();
140
- }
141
- catch (error) {
142
- if (error instanceof Error) {
143
- logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.red(figures_1.default.cross)} Run failed: ${error.message}`, 'error');
144
- }
145
- else {
146
- logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.red(figures_1.default.cross)} An unknown run error occurred`, 'error');
147
- }
148
- process.exit(1);
149
- }
150
- });
151
- // Production command with clustering
152
- program
153
- .command('prod [file]')
154
- .description('Start in production mode with clustering')
155
- .option('-w, --workers <count>', 'Number of workers (default: CPU cores)', parseInt)
156
- .option('-p, --port <port>', 'Port number', parseInt)
157
- .option('-e, --env <file>', 'Environment file', '.env.production')
158
- .option('-l, --log-file <file>', 'Log file path')
159
- .option('-q, --quiet', 'Quiet mode')
160
- .action(async (file, options) => {
161
- try {
162
- const targetFile = file || 'dist/index.js';
163
- const resolvedFile = path_1.default.resolve(targetFile);
164
- if (!fs_1.default.existsSync(resolvedFile)) {
165
- throw new Error(`File not found: ${resolvedFile}. Please build your project first.`);
166
- }
167
- const workers = options.workers || require('os').cpus().length;
168
- if (!options.quiet) {
169
- logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.blue(figures_1.default.info)} Starting production server with ${workers} workers`, 'info');
170
- }
171
- startManager = new start_manager_js_1.StartManager({
172
- file: resolvedFile,
173
- workingDir: process.cwd(),
174
- envFile: options.env,
175
- port: options.port,
176
- workers,
177
- cluster: true,
178
- memoryLimit: undefined,
179
- logLevel: 'info',
180
- logFile: options.logFile,
181
- color: false,
182
- quiet: options.quiet,
183
- verbose: false,
184
- watch: false,
185
- maxMemory: '1G',
186
- maxCrashes: 3,
187
- restartDelay: 2000,
188
- healthCheck: true,
189
- healthPort: 3001,
190
- gracefulTimeout: 30000,
191
- inspect: false,
192
- inspectBrk: false,
193
- nodeArgs: undefined
194
- });
195
- await startManager.start();
196
- }
197
- catch (error) {
198
- if (error instanceof Error) {
199
- logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.red(figures_1.default.cross)} Production start failed: ${error.message}`, 'error');
149
+ logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.red(figures_1.default.cross)} ${error.message}`, 'error');
200
150
  }
201
151
  else {
202
- logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.red(figures_1.default.cross)} An unknown production start error occurred`, 'error');
152
+ logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.red(figures_1.default.cross)} Startup failed`, 'error');
203
153
  }
204
154
  process.exit(1);
205
155
  }
@@ -212,14 +162,17 @@ function addStartCommands(program) {
212
162
  startManager = null;
213
163
  }
214
164
  catch (error) {
215
- // Ignore cleanup errors
165
+ // Ignore cleanup errors but log in verbose mode
166
+ if (process.env.VERBOSE) {
167
+ console.error('Cleanup error:', error);
168
+ }
216
169
  }
217
170
  }
218
171
  };
219
- // Handle process termination
172
+ // Enhanced signal handling
220
173
  const handleExit = (signal) => {
221
174
  if (startManager) {
222
- logger_manager_js_1.loggerManager.printLine(`\n${chalk_1.default.yellow(figures_1.default.warning)} Received ${signal}, stopping server...`, 'info');
175
+ console.log(`\n${chalk_1.default.yellow(figures_1.default.warning)} Received ${signal}, shutting down gracefully...`);
223
176
  cleanupStart().then(() => {
224
177
  process.exit(0);
225
178
  }).catch(() => {
@@ -233,11 +186,19 @@ function addStartCommands(program) {
233
186
  // Register signal handlers
234
187
  process.on('SIGINT', () => handleExit('SIGINT'));
235
188
  process.on('SIGTERM', () => handleExit('SIGTERM'));
236
- process.on('SIGUSR2', () => handleExit('SIGUSR2')); // PM2 restart
237
- process.on('exit', () => {
238
- if (startManager) {
239
- cleanupStart();
240
- }
189
+ process.on('SIGUSR2', () => handleExit('SIGUSR2'));
190
+ // Handle uncaught exceptions
191
+ process.on('uncaughtException', (error) => {
192
+ console.error(`${chalk_1.default.red(figures_1.default.cross)} Uncaught Exception:`, error);
193
+ cleanupStart().then(() => {
194
+ process.exit(1);
195
+ });
196
+ });
197
+ process.on('unhandledRejection', (reason, promise) => {
198
+ console.error(`${chalk_1.default.red(figures_1.default.cross)} Unhandled Rejection at:`, promise, 'reason:', reason);
199
+ cleanupStart().then(() => {
200
+ process.exit(1);
201
+ });
241
202
  });
242
203
  return { cleanupStart };
243
204
  }
@@ -4,79 +4,63 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.StartManager = void 0;
7
- // src/start-manager.ts - Production start manager with clustering and monitoring
7
+ // src/start-manager.ts - Fixed and optimized production start manager
8
8
  const child_process_1 = require("child_process");
9
9
  const chokidar_1 = require("chokidar");
10
10
  const logger_manager_js_1 = require("./logger-manager.js");
11
11
  const chalk_1 = __importDefault(require("chalk"));
12
+ const figures_1 = __importDefault(require("figures"));
12
13
  const path_1 = __importDefault(require("path"));
13
14
  const fs_1 = __importDefault(require("fs"));
14
15
  const http_1 = __importDefault(require("http"));
15
16
  const lodash_1 = require("lodash");
17
+ const os_1 = __importDefault(require("os"));
16
18
  class StartManager {
17
19
  constructor(options) {
18
20
  this.workers = new Map();
19
- this.masterProcess = null;
20
21
  this.watcher = null;
21
22
  this.healthServer = null;
22
23
  this.isShuttingDown = false;
23
- this.logStream = null;
24
24
  this.totalRestarts = 0;
25
+ this.envLoaded = false;
25
26
  this.options = options;
26
27
  this.startTime = new Date();
27
28
  this.debouncedRestart = (0, lodash_1.debounce)(this.restartAll.bind(this), options.restartDelay);
28
- this.setupLogging();
29
- }
30
- setupLogging() {
31
- if (this.options.logFile) {
32
- try {
33
- const logDir = path_1.default.dirname(this.options.logFile);
34
- if (!fs_1.default.existsSync(logDir)) {
35
- fs_1.default.mkdirSync(logDir, { recursive: true });
36
- }
37
- this.logStream = fs_1.default.createWriteStream(this.options.logFile, { flags: 'a' });
38
- if (this.options.verbose) {
39
- logger_manager_js_1.loggerManager.printLine(`Logging to: ${this.options.logFile}`, 'info');
40
- }
41
- }
42
- catch (error) {
43
- logger_manager_js_1.loggerManager.printLine(`Failed to setup logging: ${error.message}`, 'warn');
44
- }
45
- }
46
29
  }
47
30
  log(message, level = 'info') {
48
- const timestamp = new Date().toISOString();
49
- const logMessage = `[${timestamp}] [${level.toUpperCase()}] ${message}`;
50
- if (this.logStream) {
51
- this.logStream.write(logMessage + '\n');
52
- }
53
- if (!this.options.quiet) {
54
- logger_manager_js_1.loggerManager.printLine(message, level);
55
- }
31
+ logger_manager_js_1.loggerManager.printLine(message, level);
56
32
  }
57
33
  loadEnvFile() {
34
+ if (this.envLoaded)
35
+ return;
58
36
  if (this.options.envFile && fs_1.default.existsSync(this.options.envFile)) {
59
37
  try {
60
38
  const envContent = fs_1.default.readFileSync(this.options.envFile, 'utf8');
61
39
  const lines = envContent.split('\n');
40
+ let loadedCount = 0;
62
41
  for (const line of lines) {
63
42
  const trimmed = line.trim();
64
43
  if (trimmed && !trimmed.startsWith('#')) {
65
44
  const [key, ...values] = trimmed.split('=');
66
45
  if (key && values.length > 0) {
67
46
  const value = values.join('=').trim();
68
- // Remove quotes if present
69
47
  const cleanValue = value.replace(/^["']|["']$/g, '');
70
- process.env[key.trim()] = cleanValue;
48
+ if (!process.env[key.trim()]) {
49
+ process.env[key.trim()] = cleanValue;
50
+ loadedCount++;
51
+ }
71
52
  }
72
53
  }
73
54
  }
74
- if (this.options.verbose) {
75
- this.log(`Loaded environment variables from ${this.options.envFile}`);
55
+ if (this.options.verbose && loadedCount > 0) {
56
+ this.log(`Loaded ${loadedCount} environment variables from ${this.options.envFile}`);
76
57
  }
58
+ this.envLoaded = true;
77
59
  }
78
60
  catch (error) {
79
- this.log(`Failed to load environment file: ${error.message}`, 'warn');
61
+ if (this.options.verbose) {
62
+ this.log(`Failed to load environment file: ${error.message}`, 'warn');
63
+ }
80
64
  }
81
65
  }
82
66
  }
@@ -95,7 +79,11 @@ class StartManager {
95
79
  getNodeArgs() {
96
80
  const args = [];
97
81
  if (this.options.memoryLimit) {
98
- args.push(`--max-old-space-size=${this.parseMemoryLimit(this.options.memoryLimit) / (1024 * 1024)}`);
82
+ const memoryBytes = this.parseMemoryLimit(this.options.memoryLimit);
83
+ if (memoryBytes) {
84
+ const memoryMB = Math.floor(memoryBytes / (1024 * 1024));
85
+ args.push(`--max-old-space-size=${memoryMB}`);
86
+ }
99
87
  }
100
88
  if (this.options.inspect) {
101
89
  args.push('--inspect');
@@ -104,53 +92,63 @@ class StartManager {
104
92
  args.push('--inspect-brk');
105
93
  }
106
94
  if (this.options.nodeArgs) {
107
- args.push(...this.options.nodeArgs.split(' '));
95
+ args.push(...this.options.nodeArgs.split(' ').filter(arg => arg.trim()));
108
96
  }
109
97
  return args;
110
98
  }
111
99
  async startWorker(workerId) {
112
100
  var _a, _b;
113
101
  const nodeArgs = this.getNodeArgs();
102
+ const basePort = this.options.port || 8000;
103
+ const workerPort = this.options.workers > 1 ? basePort + workerId - 1 : basePort;
114
104
  const env = {
115
105
  ...process.env,
116
106
  NODE_ENV: process.env.NODE_ENV || 'production',
117
107
  WORKER_ID: workerId.toString(),
108
+ PORT: workerPort.toString(),
118
109
  CLUSTER_WORKER: 'true',
119
- FORCE_COLOR: this.options.color ? '1' : '0'
110
+ FORCE_COLOR: this.options.color ? '1' : '0',
111
+ // Prevent dotenv from loading again in worker processes
112
+ DOTENV_CONFIG_PATH: '',
113
+ NODE_OPTIONS: '--no-deprecation'
120
114
  };
121
- if (this.options.port) {
122
- env.PORT = this.options.port.toString();
123
- }
124
115
  const workerProcess = (0, child_process_1.fork)(this.options.file, [], {
125
116
  cwd: this.options.workingDir,
126
117
  env,
127
118
  execArgv: nodeArgs,
128
- silent: true
119
+ silent: true // Capture output to avoid duplication
129
120
  });
130
121
  const workerInfo = {
131
122
  process: workerProcess,
132
123
  pid: workerProcess.pid,
133
124
  restarts: 0,
134
125
  startTime: new Date(),
135
- memoryUsage: 0,
136
- cpuUsage: 0
126
+ id: workerId
137
127
  };
138
128
  this.workers.set(workerId, workerInfo);
139
- // Handle worker output
129
+ // Handle worker output with proper prefixing
140
130
  (_a = workerProcess.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (data) => {
141
- if (!this.options.quiet) {
142
- const message = data.toString().trim();
143
- if (message) {
144
- console.log(chalk_1.default.dim(`[Worker ${workerId}]`) + ' ' + message);
131
+ const message = data.toString().trim();
132
+ if (message) {
133
+ // Filter out repetitive dotenv messages
134
+ if (message.includes('[dotenv@') || message.includes('injecting env')) {
135
+ return;
145
136
  }
137
+ const prefix = this.options.verbose ?
138
+ chalk_1.default.dim(`[Worker ${workerId}:${workerPort}] `) : '';
139
+ console.log(prefix + message);
146
140
  }
147
141
  });
148
142
  (_b = workerProcess.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (data) => {
149
- if (!this.options.quiet) {
150
- const message = data.toString().trim();
151
- if (message) {
152
- console.error(chalk_1.default.dim(`[Worker ${workerId}]`) + ' ' + chalk_1.default.red(message));
143
+ const message = data.toString().trim();
144
+ if (message) {
145
+ // Filter out non-critical warnings
146
+ if (message.includes('[dotenv@') || message.includes('injecting env')) {
147
+ return;
153
148
  }
149
+ const prefix = this.options.verbose ?
150
+ chalk_1.default.dim(`[Worker ${workerId}:${workerPort}] `) : '';
151
+ console.error(prefix + chalk_1.default.red(message));
154
152
  }
155
153
  });
156
154
  workerProcess.on('error', (error) => {
@@ -159,21 +157,42 @@ class StartManager {
159
157
  workerProcess.on('exit', (code, signal) => {
160
158
  this.workers.delete(workerId);
161
159
  if (!this.isShuttingDown) {
162
- if (code !== 0) {
163
- this.log(`Worker ${workerId} exited with code ${code}`, 'error');
160
+ if (code !== 0 && signal !== 'SIGTERM') {
161
+ this.log(`Worker ${workerId} crashed (code: ${code}, signal: ${signal})`, 'error');
164
162
  this.restartWorker(workerId);
165
163
  }
166
- else {
167
- this.log(`Worker ${workerId} exited gracefully`);
168
- }
169
164
  }
170
165
  });
171
- workerProcess.on('message', (message) => {
172
- if (this.options.verbose) {
173
- this.log(`Worker ${workerId} message: ${JSON.stringify(message)}`);
174
- }
166
+ // Wait for worker to be ready
167
+ await new Promise((resolve, reject) => {
168
+ const timeout = setTimeout(() => {
169
+ reject(new Error(`Worker ${workerId} failed to start within timeout`));
170
+ }, 10000);
171
+ workerProcess.on('message', (message) => {
172
+ if (message && message.type === 'ready') {
173
+ clearTimeout(timeout);
174
+ resolve();
175
+ }
176
+ });
177
+ workerProcess.on('error', (error) => {
178
+ clearTimeout(timeout);
179
+ reject(error);
180
+ });
181
+ workerProcess.on('exit', (code) => {
182
+ if (code !== 0) {
183
+ clearTimeout(timeout);
184
+ reject(new Error(`Worker ${workerId} exited with code ${code}`));
185
+ }
186
+ });
187
+ // Fallback - assume ready after 2 seconds if no message
188
+ setTimeout(() => {
189
+ clearTimeout(timeout);
190
+ resolve();
191
+ }, 2000);
175
192
  });
176
- this.log(`Started worker ${workerId} (PID: ${workerProcess.pid})`);
193
+ if (this.options.verbose) {
194
+ this.log(`Worker ${workerId} started (PID: ${workerProcess.pid}, Port: ${workerPort})`);
195
+ }
177
196
  return workerInfo;
178
197
  }
179
198
  async restartWorker(workerId) {
@@ -185,8 +204,9 @@ class StartManager {
185
204
  this.log(`Worker ${workerId} reached max crashes (${this.options.maxCrashes}), not restarting`, 'error');
186
205
  return;
187
206
  }
188
- this.log(`Restarting worker ${workerId} (restart #${workerInfo.restarts})`);
189
- // Graceful shutdown
207
+ if (this.options.verbose) {
208
+ this.log(`Restarting worker ${workerId} (attempt ${workerInfo.restarts})`);
209
+ }
190
210
  try {
191
211
  workerInfo.process.kill('SIGTERM');
192
212
  await this.waitForProcessExit(workerInfo.process, 5000);
@@ -194,7 +214,6 @@ class StartManager {
194
214
  catch (error) {
195
215
  workerInfo.process.kill('SIGKILL');
196
216
  }
197
- // Start new worker
198
217
  setTimeout(() => {
199
218
  this.startWorker(workerId);
200
219
  }, this.options.restartDelay);
@@ -209,78 +228,64 @@ class StartManager {
209
228
  clearTimeout(timer);
210
229
  resolve();
211
230
  });
212
- });
213
- }
214
- async startSingleProcess() {
215
- const nodeArgs = this.getNodeArgs();
216
- const env = {
217
- ...process.env,
218
- NODE_ENV: process.env.NODE_ENV || 'production',
219
- FORCE_COLOR: this.options.color ? '1' : '0'
220
- };
221
- if (this.options.port) {
222
- env.PORT = this.options.port.toString();
223
- }
224
- this.masterProcess = (0, child_process_1.spawn)('node', [...nodeArgs, this.options.file], {
225
- cwd: this.options.workingDir,
226
- env,
227
- stdio: this.options.quiet ? 'ignore' : 'inherit'
228
- });
229
- this.masterProcess.on('error', (error) => {
230
- this.log(`Process error: ${error.message}`, 'error');
231
- });
232
- this.masterProcess.on('exit', (code, signal) => {
233
- if (!this.isShuttingDown) {
234
- if (code !== 0) {
235
- this.log(`Process exited with code ${code}`, 'error');
236
- if (this.totalRestarts < this.options.maxCrashes) {
237
- this.totalRestarts++;
238
- this.log(`Restarting process (restart #${this.totalRestarts})`);
239
- setTimeout(() => {
240
- this.startSingleProcess();
241
- }, this.options.restartDelay);
242
- }
243
- else {
244
- this.log(`Reached max crashes (${this.options.maxCrashes}), giving up`, 'error');
245
- process.exit(1);
246
- }
247
- }
248
- else {
249
- this.log('Process exited gracefully');
250
- }
231
+ if (process.killed) {
232
+ clearTimeout(timer);
233
+ resolve();
251
234
  }
252
235
  });
253
- this.log(`Started process (PID: ${this.masterProcess.pid})`);
254
236
  }
255
237
  async startCluster() {
256
- this.log(`Starting cluster with ${this.options.workers} workers`);
238
+ const startPromises = [];
257
239
  for (let i = 0; i < this.options.workers; i++) {
258
- await this.startWorker(i + 1);
240
+ startPromises.push(this.startWorker(i + 1));
241
+ }
242
+ try {
243
+ await Promise.all(startPromises);
244
+ }
245
+ catch (error) {
246
+ this.log(`Failed to start some workers: ${error.message}`, 'error');
247
+ // Continue with successfully started workers
259
248
  }
260
249
  }
261
250
  setupHealthCheck() {
262
251
  if (!this.options.healthCheck)
263
252
  return;
264
253
  this.healthServer = http_1.default.createServer((req, res) => {
254
+ res.setHeader('Access-Control-Allow-Origin', '*');
255
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
256
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
257
+ if (req.method === 'OPTIONS') {
258
+ res.writeHead(200);
259
+ res.end();
260
+ return;
261
+ }
265
262
  if (req.url === '/health') {
266
263
  const stats = {
267
264
  status: 'ok',
268
265
  uptime: Date.now() - this.startTime.getTime(),
269
266
  workers: this.workers.size,
267
+ activeWorkers: Array.from(this.workers.values()).map(w => ({
268
+ id: w.id,
269
+ pid: w.pid,
270
+ restarts: w.restarts,
271
+ uptime: Date.now() - w.startTime.getTime()
272
+ })),
270
273
  totalRestarts: this.totalRestarts,
271
- memoryUsage: process.memoryUsage(),
272
- cpuUsage: process.cpuUsage()
274
+ memory: process.memoryUsage(),
275
+ cpu: os_1.default.loadavg()
273
276
  };
274
277
  res.writeHead(200, { 'Content-Type': 'application/json' });
275
278
  res.end(JSON.stringify(stats, null, 2));
276
279
  }
277
280
  else {
278
- res.writeHead(404);
281
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
279
282
  res.end('Not Found');
280
283
  }
281
284
  });
282
285
  this.healthServer.listen(this.options.healthPort, () => {
283
- this.log(`Health check server listening on port ${this.options.healthPort}`);
286
+ if (this.options.verbose) {
287
+ this.log(`Health endpoint: http://localhost:${this.options.healthPort}/health`);
288
+ }
284
289
  });
285
290
  }
286
291
  setupWatcher() {
@@ -292,16 +297,13 @@ class StartManager {
292
297
  `${this.options.workingDir}/**/*.env*`
293
298
  ];
294
299
  this.watcher = (0, chokidar_1.watch)(watchPatterns, {
295
- ignored: [
296
- '**/node_modules/**',
297
- '**/.git/**',
298
- '**/logs/**',
299
- '**/*.log'
300
- ],
300
+ ignored: ['**/node_modules/**', '**/.git/**', '**/logs/**', '**/tmp/**'],
301
301
  ignoreInitial: true,
302
- followSymlinks: false,
303
- usePolling: false,
304
- atomic: 300
302
+ atomic: 300,
303
+ awaitWriteFinish: {
304
+ stabilityThreshold: 2000,
305
+ pollInterval: 100
306
+ }
305
307
  });
306
308
  this.watcher.on('change', (filePath) => {
307
309
  if (this.options.verbose) {
@@ -312,15 +314,17 @@ class StartManager {
312
314
  this.watcher.on('error', (error) => {
313
315
  this.log(`Watcher error: ${error.message}`, 'error');
314
316
  });
315
- this.log('File watcher enabled');
317
+ if (this.options.verbose) {
318
+ this.log('File watching enabled');
319
+ }
316
320
  }
317
321
  async restartAll() {
318
322
  if (this.isShuttingDown)
319
323
  return;
320
- this.log('Restarting all processes due to file changes');
321
- if (this.options.cluster) {
322
- // Graceful restart of workers
323
- for (const [workerId, workerInfo] of this.workers.entries()) {
324
+ this.log('Restarting all workers due to file changes...');
325
+ const restartPromises = [];
326
+ for (const [workerId, workerInfo] of this.workers.entries()) {
327
+ restartPromises.push((async () => {
324
328
  try {
325
329
  workerInfo.process.kill('SIGTERM');
326
330
  await this.waitForProcessExit(workerInfo.process, 5000);
@@ -328,75 +332,59 @@ class StartManager {
328
332
  catch (error) {
329
333
  workerInfo.process.kill('SIGKILL');
330
334
  }
331
- // Start new worker
332
- setTimeout(() => {
333
- this.startWorker(workerId);
334
- }, this.options.restartDelay);
335
- }
336
- }
337
- else if (this.masterProcess) {
338
- // Restart single process
339
- try {
340
- this.masterProcess.kill('SIGTERM');
341
- await this.waitForProcessExit(this.masterProcess, 5000);
342
- }
343
- catch (error) {
344
- this.masterProcess.kill('SIGKILL');
345
- }
346
- setTimeout(() => {
347
- this.startSingleProcess();
348
- }, this.options.restartDelay);
335
+ })());
349
336
  }
337
+ await Promise.allSettled(restartPromises);
338
+ // Wait a bit before restarting
339
+ setTimeout(() => {
340
+ this.startCluster();
341
+ }, this.options.restartDelay);
350
342
  }
351
- setupMonitoring() {
352
- setInterval(() => {
353
- if (this.options.verbose) {
354
- this.workers.forEach((workerInfo, workerId) => {
355
- try {
356
- const usage = process.memoryUsage();
357
- workerInfo.memoryUsage = usage.heapUsed;
358
- if (this.options.maxMemory) {
359
- const maxMemory = this.parseMemoryLimit(this.options.maxMemory);
360
- if (maxMemory && usage.heapUsed > maxMemory) {
361
- this.log(`Worker ${workerId} exceeded memory limit, restarting`, 'warn');
362
- this.restartWorker(workerId);
363
- }
364
- }
365
- }
366
- catch (error) {
367
- // Ignore monitoring errors
368
- }
369
- });
343
+ setupMemoryMonitoring() {
344
+ if (!this.options.maxMemory)
345
+ return;
346
+ const maxMemory = this.parseMemoryLimit(this.options.maxMemory);
347
+ if (!maxMemory)
348
+ return;
349
+ const checkInterval = setInterval(() => {
350
+ if (this.isShuttingDown) {
351
+ clearInterval(checkInterval);
352
+ return;
370
353
  }
371
- }, 10000); // Check every 10 seconds
354
+ this.workers.forEach((workerInfo, workerId) => {
355
+ try {
356
+ // This is a simplified check - in practice, you'd need to get actual worker memory usage
357
+ const usage = process.memoryUsage();
358
+ if (usage.heapUsed > maxMemory) {
359
+ this.log(`Worker ${workerId} may have exceeded memory limit, restarting`, 'warn');
360
+ this.restartWorker(workerId);
361
+ }
362
+ }
363
+ catch (error) {
364
+ // Ignore monitoring errors
365
+ }
366
+ });
367
+ }, 30000); // Check every 30 seconds
372
368
  }
373
369
  async start() {
374
370
  try {
371
+ // Load environment variables only once in the main process
375
372
  this.loadEnvFile();
373
+ // Set up monitoring and health checks
376
374
  this.setupHealthCheck();
377
375
  this.setupWatcher();
378
- this.setupMonitoring();
379
- const runtime = Date.now() - this.startTime.getTime();
380
- this.log(`Starting application in ${runtime}ms`);
381
- if (this.options.cluster) {
382
- await this.startCluster();
383
- }
384
- else {
385
- await this.startSingleProcess();
386
- }
387
- this.log(`Application started successfully`);
388
- if (this.options.verbose) {
389
- this.log(`Configuration: ${JSON.stringify({
390
- workers: this.options.workers,
391
- cluster: this.options.cluster,
392
- watch: this.options.watch,
393
- healthCheck: this.options.healthCheck,
394
- port: this.options.port
395
- })}`);
396
- }
376
+ this.setupMemoryMonitoring();
377
+ // Start worker cluster
378
+ await this.startCluster();
379
+ // Success message
380
+ const basePort = this.options.port || 8000;
381
+ const portInfo = this.options.workers > 1 ?
382
+ ` on ports ${basePort}-${basePort + this.options.workers - 1}` :
383
+ ` on port ${basePort}`;
384
+ logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.green(figures_1.default.tick)} Server ready${portInfo} (${this.workers.size} workers)`, 'info');
397
385
  }
398
386
  catch (error) {
399
- this.log(`Failed to start application: ${error.message}`, 'error');
387
+ this.log(`Failed to start server: ${error.message}`, 'error');
400
388
  throw error;
401
389
  }
402
390
  }
@@ -404,18 +392,13 @@ class StartManager {
404
392
  if (this.isShuttingDown)
405
393
  return;
406
394
  this.isShuttingDown = true;
407
- this.log('Stopping application...');
408
- // Stop watcher
395
+ this.log('Shutting down gracefully...');
409
396
  if (this.watcher) {
410
397
  await this.watcher.close();
411
- this.watcher = null;
412
398
  }
413
- // Stop health server
414
399
  if (this.healthServer) {
415
400
  this.healthServer.close();
416
- this.healthServer = null;
417
401
  }
418
- // Stop all workers
419
402
  const shutdownPromises = [];
420
403
  for (const [workerId, workerInfo] of this.workers.entries()) {
421
404
  shutdownPromises.push((async () => {
@@ -428,29 +411,9 @@ class StartManager {
428
411
  }
429
412
  })());
430
413
  }
431
- // Stop master process
432
- if (this.masterProcess) {
433
- shutdownPromises.push((async () => {
434
- try {
435
- this.masterProcess.kill('SIGTERM');
436
- await this.waitForProcessExit(this.masterProcess, this.options.gracefulTimeout);
437
- }
438
- catch (error) {
439
- this.masterProcess.kill('SIGKILL');
440
- }
441
- })());
442
- }
443
- // Wait for all processes to shut down
444
414
  await Promise.allSettled(shutdownPromises);
445
- // Close log stream
446
- if (this.logStream) {
447
- this.logStream.end();
448
- this.logStream = null;
449
- }
450
- const uptime = Date.now() - this.startTime.getTime();
451
- this.log(`Application stopped after ${uptime}ms uptime`);
452
415
  this.workers.clear();
453
- this.masterProcess = null;
416
+ this.log('Server stopped');
454
417
  }
455
418
  }
456
419
  exports.StartManager = StartManager;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neex",
3
- "version": "0.6.51",
3
+ "version": "0.6.54",
4
4
  "description": "The Modern Build System for Polyrepo-in-Monorepo Architecture",
5
5
  "main": "dist/src/index.js",
6
6
  "types": "dist/src/index.d.ts",