neex 0.6.47 → 0.6.51

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.
@@ -4,118 +4,453 @@ 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 application runner
7
+ // src/start-manager.ts - Production start manager with clustering and monitoring
8
8
  const child_process_1 = require("child_process");
9
+ const chokidar_1 = require("chokidar");
9
10
  const logger_manager_js_1 = require("./logger-manager.js");
10
11
  const chalk_1 = __importDefault(require("chalk"));
11
- const figures_1 = __importDefault(require("figures"));
12
- const fs_1 = __importDefault(require("fs"));
13
12
  const path_1 = __importDefault(require("path"));
13
+ const fs_1 = __importDefault(require("fs"));
14
+ const http_1 = __importDefault(require("http"));
15
+ const lodash_1 = require("lodash");
14
16
  class StartManager {
15
17
  constructor(options) {
16
- this.process = null;
17
- this.isStopping = false;
18
+ this.workers = new Map();
19
+ this.masterProcess = null;
20
+ this.watcher = null;
21
+ this.healthServer = null;
22
+ this.isShuttingDown = false;
23
+ this.logStream = null;
24
+ this.totalRestarts = 0;
18
25
  this.options = options;
26
+ this.startTime = new Date();
27
+ this.debouncedRestart = (0, lodash_1.debounce)(this.restartAll.bind(this), options.restartDelay);
28
+ this.setupLogging();
19
29
  }
20
- async startProcess() {
21
- var _a, _b;
22
- if (this.process) {
23
- return;
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
+ }
47
+ 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');
24
52
  }
25
- const nodeArgs = this.options.nodeArgs || [];
26
- const args = [...nodeArgs, this.options.entry];
27
- if (this.options.verbose) {
28
- logger_manager_js_1.loggerManager.printLine(`Executing: node ${args.join(' ')}`, 'info');
29
- }
30
- this.process = (0, child_process_1.spawn)('node', args, {
31
- stdio: ['ignore', 'pipe', 'pipe'],
32
- shell: false,
33
- env: {
34
- ...process.env,
35
- NODE_ENV: 'production',
36
- FORCE_COLOR: this.options.color ? '1' : '0'
37
- },
38
- detached: true
39
- });
40
- const appName = this.options.name || path_1.default.basename(this.options.entry);
41
53
  if (!this.options.quiet) {
42
- logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.green(figures_1.default.play)} Starting ${chalk_1.default.cyan(appName)} in production mode...`, 'info');
54
+ logger_manager_js_1.loggerManager.printLine(message, level);
55
+ }
56
+ }
57
+ loadEnvFile() {
58
+ if (this.options.envFile && fs_1.default.existsSync(this.options.envFile)) {
59
+ try {
60
+ const envContent = fs_1.default.readFileSync(this.options.envFile, 'utf8');
61
+ const lines = envContent.split('\n');
62
+ for (const line of lines) {
63
+ const trimmed = line.trim();
64
+ if (trimmed && !trimmed.startsWith('#')) {
65
+ const [key, ...values] = trimmed.split('=');
66
+ if (key && values.length > 0) {
67
+ const value = values.join('=').trim();
68
+ // Remove quotes if present
69
+ const cleanValue = value.replace(/^["']|["']$/g, '');
70
+ process.env[key.trim()] = cleanValue;
71
+ }
72
+ }
73
+ }
74
+ if (this.options.verbose) {
75
+ this.log(`Loaded environment variables from ${this.options.envFile}`);
76
+ }
77
+ }
78
+ catch (error) {
79
+ this.log(`Failed to load environment file: ${error.message}`, 'warn');
80
+ }
81
+ }
82
+ }
83
+ parseMemoryLimit(limit) {
84
+ var _a;
85
+ if (!limit)
86
+ return undefined;
87
+ const match = limit.match(/^(\d+)([KMGT]?)$/i);
88
+ if (!match)
89
+ return undefined;
90
+ const value = parseInt(match[1]);
91
+ const unit = ((_a = match[2]) === null || _a === void 0 ? void 0 : _a.toUpperCase()) || '';
92
+ const multipliers = { K: 1024, M: 1024 * 1024, G: 1024 * 1024 * 1024, T: 1024 * 1024 * 1024 * 1024 };
93
+ return value * (multipliers[unit] || 1);
94
+ }
95
+ getNodeArgs() {
96
+ const args = [];
97
+ if (this.options.memoryLimit) {
98
+ args.push(`--max-old-space-size=${this.parseMemoryLimit(this.options.memoryLimit) / (1024 * 1024)}`);
99
+ }
100
+ if (this.options.inspect) {
101
+ args.push('--inspect');
102
+ }
103
+ if (this.options.inspectBrk) {
104
+ args.push('--inspect-brk');
43
105
  }
44
- (_a = this.process.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (data) => {
106
+ if (this.options.nodeArgs) {
107
+ args.push(...this.options.nodeArgs.split(' '));
108
+ }
109
+ return args;
110
+ }
111
+ async startWorker(workerId) {
112
+ var _a, _b;
113
+ const nodeArgs = this.getNodeArgs();
114
+ const env = {
115
+ ...process.env,
116
+ NODE_ENV: process.env.NODE_ENV || 'production',
117
+ WORKER_ID: workerId.toString(),
118
+ CLUSTER_WORKER: 'true',
119
+ FORCE_COLOR: this.options.color ? '1' : '0'
120
+ };
121
+ if (this.options.port) {
122
+ env.PORT = this.options.port.toString();
123
+ }
124
+ const workerProcess = (0, child_process_1.fork)(this.options.file, [], {
125
+ cwd: this.options.workingDir,
126
+ env,
127
+ execArgv: nodeArgs,
128
+ silent: true
129
+ });
130
+ const workerInfo = {
131
+ process: workerProcess,
132
+ pid: workerProcess.pid,
133
+ restarts: 0,
134
+ startTime: new Date(),
135
+ memoryUsage: 0,
136
+ cpuUsage: 0
137
+ };
138
+ this.workers.set(workerId, workerInfo);
139
+ // Handle worker output
140
+ (_a = workerProcess.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (data) => {
45
141
  if (!this.options.quiet) {
46
- process.stdout.write(data);
142
+ const message = data.toString().trim();
143
+ if (message) {
144
+ console.log(chalk_1.default.dim(`[Worker ${workerId}]`) + ' ' + message);
145
+ }
47
146
  }
48
147
  });
49
- (_b = this.process.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (data) => {
148
+ (_b = workerProcess.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (data) => {
50
149
  if (!this.options.quiet) {
51
- process.stderr.write(data);
150
+ const message = data.toString().trim();
151
+ if (message) {
152
+ console.error(chalk_1.default.dim(`[Worker ${workerId}]`) + ' ' + chalk_1.default.red(message));
153
+ }
52
154
  }
53
155
  });
54
- this.process.on('error', (error) => {
55
- logger_manager_js_1.loggerManager.printLine(`Application error: ${error.message}`, 'error');
156
+ workerProcess.on('error', (error) => {
157
+ this.log(`Worker ${workerId} error: ${error.message}`, 'error');
56
158
  });
57
- this.process.on('exit', (code) => {
58
- this.process = null;
59
- if (!this.isStopping) {
60
- logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.red(figures_1.default.cross)} Application ${appName} exited with code ${code}`, 'error');
61
- if (this.options.watch) {
62
- logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.yellow(figures_1.default.arrowRight)} Restarting application...`, 'info');
63
- this.startProcess();
159
+ workerProcess.on('exit', (code, signal) => {
160
+ this.workers.delete(workerId);
161
+ if (!this.isShuttingDown) {
162
+ if (code !== 0) {
163
+ this.log(`Worker ${workerId} exited with code ${code}`, 'error');
164
+ this.restartWorker(workerId);
64
165
  }
166
+ else {
167
+ this.log(`Worker ${workerId} exited gracefully`);
168
+ }
169
+ }
170
+ });
171
+ workerProcess.on('message', (message) => {
172
+ if (this.options.verbose) {
173
+ this.log(`Worker ${workerId} message: ${JSON.stringify(message)}`);
65
174
  }
66
175
  });
176
+ this.log(`Started worker ${workerId} (PID: ${workerProcess.pid})`);
177
+ return workerInfo;
67
178
  }
68
- async start() {
69
- if (!fs_1.default.existsSync(this.options.entry)) {
70
- throw new Error(`Entry file not found: ${this.options.entry}`);
179
+ async restartWorker(workerId) {
180
+ const workerInfo = this.workers.get(workerId);
181
+ if (workerInfo) {
182
+ workerInfo.restarts++;
183
+ this.totalRestarts++;
184
+ if (workerInfo.restarts >= this.options.maxCrashes) {
185
+ this.log(`Worker ${workerId} reached max crashes (${this.options.maxCrashes}), not restarting`, 'error');
186
+ return;
187
+ }
188
+ this.log(`Restarting worker ${workerId} (restart #${workerInfo.restarts})`);
189
+ // Graceful shutdown
190
+ try {
191
+ workerInfo.process.kill('SIGTERM');
192
+ await this.waitForProcessExit(workerInfo.process, 5000);
193
+ }
194
+ catch (error) {
195
+ workerInfo.process.kill('SIGKILL');
196
+ }
197
+ // Start new worker
198
+ setTimeout(() => {
199
+ this.startWorker(workerId);
200
+ }, this.options.restartDelay);
71
201
  }
72
- await this.startProcess();
73
- if (!this.options.quiet) {
74
- logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.green(figures_1.default.tick)} Application is running.`, 'info');
202
+ }
203
+ async waitForProcessExit(process, timeout) {
204
+ return new Promise((resolve, reject) => {
205
+ const timer = setTimeout(() => {
206
+ reject(new Error('Process exit timeout'));
207
+ }, timeout);
208
+ process.on('exit', () => {
209
+ clearTimeout(timer);
210
+ resolve();
211
+ });
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
+ }
251
+ }
252
+ });
253
+ this.log(`Started process (PID: ${this.masterProcess.pid})`);
254
+ }
255
+ async startCluster() {
256
+ this.log(`Starting cluster with ${this.options.workers} workers`);
257
+ for (let i = 0; i < this.options.workers; i++) {
258
+ await this.startWorker(i + 1);
75
259
  }
76
260
  }
77
- async stop() {
78
- this.isStopping = true;
79
- const proc = this.process;
80
- if (!proc) {
261
+ setupHealthCheck() {
262
+ if (!this.options.healthCheck)
81
263
  return;
264
+ this.healthServer = http_1.default.createServer((req, res) => {
265
+ if (req.url === '/health') {
266
+ const stats = {
267
+ status: 'ok',
268
+ uptime: Date.now() - this.startTime.getTime(),
269
+ workers: this.workers.size,
270
+ totalRestarts: this.totalRestarts,
271
+ memoryUsage: process.memoryUsage(),
272
+ cpuUsage: process.cpuUsage()
273
+ };
274
+ res.writeHead(200, { 'Content-Type': 'application/json' });
275
+ res.end(JSON.stringify(stats, null, 2));
276
+ }
277
+ else {
278
+ res.writeHead(404);
279
+ res.end('Not Found');
280
+ }
281
+ });
282
+ this.healthServer.listen(this.options.healthPort, () => {
283
+ this.log(`Health check server listening on port ${this.options.healthPort}`);
284
+ });
285
+ }
286
+ setupWatcher() {
287
+ if (!this.options.watch)
288
+ return;
289
+ const watchPatterns = [
290
+ `${this.options.workingDir}/**/*.js`,
291
+ `${this.options.workingDir}/**/*.json`,
292
+ `${this.options.workingDir}/**/*.env*`
293
+ ];
294
+ this.watcher = (0, chokidar_1.watch)(watchPatterns, {
295
+ ignored: [
296
+ '**/node_modules/**',
297
+ '**/.git/**',
298
+ '**/logs/**',
299
+ '**/*.log'
300
+ ],
301
+ ignoreInitial: true,
302
+ followSymlinks: false,
303
+ usePolling: false,
304
+ atomic: 300
305
+ });
306
+ this.watcher.on('change', (filePath) => {
307
+ if (this.options.verbose) {
308
+ this.log(`File changed: ${path_1.default.relative(this.options.workingDir, filePath)}`);
309
+ }
310
+ this.debouncedRestart();
311
+ });
312
+ this.watcher.on('error', (error) => {
313
+ this.log(`Watcher error: ${error.message}`, 'error');
314
+ });
315
+ this.log('File watcher enabled');
316
+ }
317
+ async restartAll() {
318
+ if (this.isShuttingDown)
319
+ 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
+ try {
325
+ workerInfo.process.kill('SIGTERM');
326
+ await this.waitForProcessExit(workerInfo.process, 5000);
327
+ }
328
+ catch (error) {
329
+ workerInfo.process.kill('SIGKILL');
330
+ }
331
+ // Start new worker
332
+ setTimeout(() => {
333
+ this.startWorker(workerId);
334
+ }, this.options.restartDelay);
335
+ }
82
336
  }
83
- this.process = null;
84
- return new Promise((resolve) => {
85
- const appName = this.options.name || path_1.default.basename(this.options.entry);
86
- logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.yellow(figures_1.default.warning)} Stopping application ${appName}...`, 'info');
87
- proc.on('exit', () => {
88
- logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.yellow(figures_1.default.square)} Application stopped.`, 'info');
89
- resolve();
90
- });
91
- proc.on('error', () => {
92
- // Handle errors during shutdown, e.g., if the process is already gone
93
- logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.yellow(figures_1.default.square)} Application stopped.`, 'info');
94
- resolve();
95
- });
337
+ else if (this.masterProcess) {
338
+ // Restart single process
96
339
  try {
97
- if (proc.pid) {
98
- const pid = proc.pid;
99
- // Kill the entire process group
100
- process.kill(-pid, 'SIGTERM');
101
- // Set a timeout to force kill if it doesn't terminate gracefully
102
- setTimeout(() => {
103
- if (!proc.killed) {
104
- try {
105
- process.kill(-pid, 'SIGKILL');
106
- }
107
- catch (e) {
108
- // Ignore errors if the process is already gone
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);
349
+ }
350
+ }
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);
109
363
  }
110
364
  }
111
- }, 5000).unref(); // .unref() allows the main process to exit if this is the only thing running
112
- }
365
+ }
366
+ catch (error) {
367
+ // Ignore monitoring errors
368
+ }
369
+ });
113
370
  }
114
- catch (e) {
115
- // This can happen if the process is already dead
116
- resolve();
371
+ }, 10000); // Check every 10 seconds
372
+ }
373
+ async start() {
374
+ try {
375
+ this.loadEnvFile();
376
+ this.setupHealthCheck();
377
+ 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();
117
383
  }
118
- });
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
+ }
397
+ }
398
+ catch (error) {
399
+ this.log(`Failed to start application: ${error.message}`, 'error');
400
+ throw error;
401
+ }
402
+ }
403
+ async stop() {
404
+ if (this.isShuttingDown)
405
+ return;
406
+ this.isShuttingDown = true;
407
+ this.log('Stopping application...');
408
+ // Stop watcher
409
+ if (this.watcher) {
410
+ await this.watcher.close();
411
+ this.watcher = null;
412
+ }
413
+ // Stop health server
414
+ if (this.healthServer) {
415
+ this.healthServer.close();
416
+ this.healthServer = null;
417
+ }
418
+ // Stop all workers
419
+ const shutdownPromises = [];
420
+ for (const [workerId, workerInfo] of this.workers.entries()) {
421
+ shutdownPromises.push((async () => {
422
+ try {
423
+ workerInfo.process.kill('SIGTERM');
424
+ await this.waitForProcessExit(workerInfo.process, this.options.gracefulTimeout);
425
+ }
426
+ catch (error) {
427
+ workerInfo.process.kill('SIGKILL');
428
+ }
429
+ })());
430
+ }
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
+ 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
+ this.workers.clear();
453
+ this.masterProcess = null;
119
454
  }
120
455
  }
121
456
  exports.StartManager = StartManager;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neex",
3
- "version": "0.6.47",
3
+ "version": "0.6.51",
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",