@noego/app 0.0.16 → 0.0.17
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.
- package/cjs/client.cjs +35 -0
- package/cjs/config.cjs +24 -0
- package/cjs/index.cjs +54 -0
- package/package.json +5 -2
- package/src/commands/dev.js +254 -11
- package/src/runtime/runtime.js +49 -2
package/cjs/client.cjs
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// CJS wrapper for ESM client module
|
|
4
|
+
|
|
5
|
+
let modulePromise = null;
|
|
6
|
+
|
|
7
|
+
function getModule() {
|
|
8
|
+
if (!modulePromise) {
|
|
9
|
+
modulePromise = import('../src/client.js');
|
|
10
|
+
}
|
|
11
|
+
return modulePromise;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Export functions that return promises
|
|
15
|
+
module.exports = {
|
|
16
|
+
setContext: async (...args) => {
|
|
17
|
+
const mod = await getModule();
|
|
18
|
+
return mod.setContext(...args);
|
|
19
|
+
},
|
|
20
|
+
boot: async (...args) => {
|
|
21
|
+
const mod = await getModule();
|
|
22
|
+
return mod.boot(...args);
|
|
23
|
+
},
|
|
24
|
+
clientBoot: async (...args) => {
|
|
25
|
+
const mod = await getModule();
|
|
26
|
+
return mod.clientBoot(...args);
|
|
27
|
+
},
|
|
28
|
+
clientInit: async (...args) => {
|
|
29
|
+
const mod = await getModule();
|
|
30
|
+
return mod.clientInit(...args);
|
|
31
|
+
},
|
|
32
|
+
get client() {
|
|
33
|
+
return getModule().then(mod => mod.client);
|
|
34
|
+
}
|
|
35
|
+
};
|
package/cjs/config.cjs
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// CJS wrapper for ESM config module
|
|
4
|
+
|
|
5
|
+
let modulePromise = null;
|
|
6
|
+
|
|
7
|
+
function getModule() {
|
|
8
|
+
if (!modulePromise) {
|
|
9
|
+
modulePromise = import('../src/runtime/config.js');
|
|
10
|
+
}
|
|
11
|
+
return modulePromise;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Export functions that return promises
|
|
15
|
+
module.exports = {
|
|
16
|
+
buildConfig: async (...args) => {
|
|
17
|
+
const mod = await getModule();
|
|
18
|
+
return mod.buildConfig(...args);
|
|
19
|
+
},
|
|
20
|
+
getConfig: async (...args) => {
|
|
21
|
+
const mod = await getModule();
|
|
22
|
+
return mod.getConfig(...args);
|
|
23
|
+
}
|
|
24
|
+
};
|
package/cjs/index.cjs
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// CJS wrapper for ESM module
|
|
4
|
+
// Uses dynamic import to load ESM and caches the result
|
|
5
|
+
|
|
6
|
+
let modulePromise = null;
|
|
7
|
+
|
|
8
|
+
function getModule() {
|
|
9
|
+
if (!modulePromise) {
|
|
10
|
+
modulePromise = import('../src/index.js');
|
|
11
|
+
}
|
|
12
|
+
return modulePromise;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = new Proxy({}, {
|
|
16
|
+
get(_, prop) {
|
|
17
|
+
if (prop === 'then' || prop === 'catch' || prop === 'finally') {
|
|
18
|
+
return undefined; // Prevent treating as thenable
|
|
19
|
+
}
|
|
20
|
+
return async (...args) => {
|
|
21
|
+
const mod = await getModule();
|
|
22
|
+
const fn = mod[prop];
|
|
23
|
+
if (typeof fn === 'function') {
|
|
24
|
+
return fn(...args);
|
|
25
|
+
}
|
|
26
|
+
return fn;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Also export named exports as promises for destructuring
|
|
32
|
+
Object.defineProperties(module.exports, {
|
|
33
|
+
runCombinedServices: {
|
|
34
|
+
get() {
|
|
35
|
+
return async (...args) => {
|
|
36
|
+
const mod = await getModule();
|
|
37
|
+
return mod.runCombinedServices(...args);
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
boot: {
|
|
42
|
+
get() {
|
|
43
|
+
return async (...args) => {
|
|
44
|
+
const mod = await getModule();
|
|
45
|
+
return mod.boot(...args);
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
client: {
|
|
50
|
+
get() {
|
|
51
|
+
return getModule().then(mod => mod.client);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noego/app",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.17",
|
|
4
4
|
"description": "Production build tool for Dinner/Forge apps.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,14 +9,17 @@
|
|
|
9
9
|
},
|
|
10
10
|
"exports": {
|
|
11
11
|
".": {
|
|
12
|
-
"import": "./src/index.js"
|
|
12
|
+
"import": "./src/index.js",
|
|
13
|
+
"require": "./cjs/index.cjs"
|
|
13
14
|
},
|
|
14
15
|
"./client": {
|
|
15
16
|
"import": "./src/client.js",
|
|
17
|
+
"require": "./cjs/client.cjs",
|
|
16
18
|
"types": "./types/client.d.ts"
|
|
17
19
|
},
|
|
18
20
|
"./config": {
|
|
19
21
|
"import": "./src/runtime/config.js",
|
|
22
|
+
"require": "./cjs/config.cjs",
|
|
20
23
|
"types": "./types/config.d.ts"
|
|
21
24
|
}
|
|
22
25
|
},
|
package/src/commands/dev.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import { spawn } from 'node:child_process';
|
|
3
|
+
import { spawn, execSync } from 'node:child_process';
|
|
4
4
|
import { createBuildContext } from '../build/context.js';
|
|
5
5
|
import { findConfigFile } from '../runtime/index.js';
|
|
6
6
|
import { loadConfig } from '../runtime/config-loader.js';
|
|
@@ -28,6 +28,90 @@ function killProcessTree(pid, signal = 'SIGKILL') {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Recovery utility for EADDRINUSE errors
|
|
33
|
+
* Uses lsof to detect what process is using a port, kills it, and waits for the port to be free
|
|
34
|
+
*
|
|
35
|
+
* @param {number} port - The port to recover
|
|
36
|
+
* @param {object} logger - Logger instance for output
|
|
37
|
+
* @returns {boolean} - True if recovery was successful, false otherwise
|
|
38
|
+
*/
|
|
39
|
+
function recoverFromPortInUse(port, logger) {
|
|
40
|
+
try {
|
|
41
|
+
logger.warn(`[PORT RECOVERY] Attempting to recover port ${port}...`);
|
|
42
|
+
|
|
43
|
+
// Run lsof to find what's using the port
|
|
44
|
+
let lsofOutput;
|
|
45
|
+
try {
|
|
46
|
+
lsofOutput = execSync(`lsof -i :${port}`, { encoding: 'utf-8' });
|
|
47
|
+
} catch (err) {
|
|
48
|
+
// lsof returns exit code 1 if no processes found
|
|
49
|
+
if (err.status === 1) {
|
|
50
|
+
logger.info(`[PORT RECOVERY] Port ${port} appears to be free (no process found)`);
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Parse lsof output to extract PIDs
|
|
57
|
+
// Output format: COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
|
|
58
|
+
// Skip header line, extract second column (PID)
|
|
59
|
+
const lines = lsofOutput.trim().split('\n');
|
|
60
|
+
const pidsToKill = new Set();
|
|
61
|
+
|
|
62
|
+
for (let i = 1; i < lines.length; i++) { // Skip header
|
|
63
|
+
const columns = lines[i].split(/\s+/);
|
|
64
|
+
if (columns.length >= 2) {
|
|
65
|
+
const pid = parseInt(columns[1], 10);
|
|
66
|
+
if (!isNaN(pid) && pid !== process.pid) { // Don't kill ourselves
|
|
67
|
+
pidsToKill.add(pid);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (pidsToKill.size === 0) {
|
|
73
|
+
logger.info(`[PORT RECOVERY] No killable processes found on port ${port}`);
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Kill each process holding the port
|
|
78
|
+
for (const pid of pidsToKill) {
|
|
79
|
+
try {
|
|
80
|
+
logger.warn(`[PORT RECOVERY] Killing stale process PID ${pid} holding port ${port}`);
|
|
81
|
+
execSync(`kill -9 ${pid}`, { encoding: 'utf-8' });
|
|
82
|
+
} catch (killErr) {
|
|
83
|
+
// Process might already be dead
|
|
84
|
+
logger.debug(`[PORT RECOVERY] Could not kill PID ${pid}: ${killErr.message}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Brief wait for OS to release the port
|
|
89
|
+
// execSync is synchronous so we use a simple busy-wait
|
|
90
|
+
const waitStart = Date.now();
|
|
91
|
+
const maxWait = 2000; // 2 seconds max
|
|
92
|
+
while (Date.now() - waitStart < maxWait) {
|
|
93
|
+
try {
|
|
94
|
+
// Check if port is now free by running lsof again
|
|
95
|
+
execSync(`lsof -i :${port}`, { encoding: 'utf-8' });
|
|
96
|
+
// If lsof succeeds, port is still in use - keep waiting
|
|
97
|
+
// Small synchronous delay
|
|
98
|
+
const spinStart = Date.now();
|
|
99
|
+
while (Date.now() - spinStart < 100) { /* busy wait 100ms */ }
|
|
100
|
+
} catch {
|
|
101
|
+
// lsof failed = port is free
|
|
102
|
+
logger.info(`[PORT RECOVERY] Port ${port} is now free`);
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
logger.warn(`[PORT RECOVERY] Port ${port} may still be in use after recovery attempt`);
|
|
108
|
+
return false;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
logger.error(`[PORT RECOVERY] Failed to recover port ${port}: ${error.message}`);
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
31
115
|
export async function runDev(config) {
|
|
32
116
|
const context = createBuildContext(config);
|
|
33
117
|
const { logger } = context;
|
|
@@ -448,7 +532,25 @@ async function runSplitServeWithWatch(context, tsxExecutable, tsxArgs, baseEnv,
|
|
|
448
532
|
let backendStartTime = 0;
|
|
449
533
|
let frontendStartTime = 0;
|
|
450
534
|
|
|
451
|
-
|
|
535
|
+
// Port recovery retry tracking
|
|
536
|
+
const MAX_PORT_RECOVERY_RETRIES = 3;
|
|
537
|
+
let backendPortRetries = 0;
|
|
538
|
+
let frontendPortRetries = 0;
|
|
539
|
+
|
|
540
|
+
const startBackend = (retryAfterRecovery = false) => {
|
|
541
|
+
if (retryAfterRecovery) {
|
|
542
|
+
backendPortRetries++;
|
|
543
|
+
if (backendPortRetries > MAX_PORT_RECOVERY_RETRIES) {
|
|
544
|
+
logger.error(`[BACKEND] Exceeded max port recovery retries (${MAX_PORT_RECOVERY_RETRIES}). Shutting down...`);
|
|
545
|
+
shutdown('backend-port-recovery-exceeded', 1, 'backend-port-recovery');
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
logger.warn(`[BACKEND] Port recovery retry ${backendPortRetries}/${MAX_PORT_RECOVERY_RETRIES}...`);
|
|
549
|
+
} else {
|
|
550
|
+
// Reset retry counter on normal start
|
|
551
|
+
backendPortRetries = 0;
|
|
552
|
+
}
|
|
553
|
+
|
|
452
554
|
backendRestartCount++;
|
|
453
555
|
backendStartTime = Date.now();
|
|
454
556
|
logger.info(`🚀 [RESTART #${backendRestartCount}] Starting backend on port ${backendPort}...`);
|
|
@@ -458,13 +560,23 @@ async function runSplitServeWithWatch(context, tsxExecutable, tsxArgs, baseEnv,
|
|
|
458
560
|
NOEGO_PORT: String(backendPort)
|
|
459
561
|
};
|
|
460
562
|
|
|
563
|
+
// Capture stderr to differentiate between port errors and boot errors
|
|
564
|
+
let stderrBuffer = '';
|
|
565
|
+
|
|
461
566
|
backendProc = spawn(tsxExecutable, tsxArgs, {
|
|
462
567
|
cwd: context.config.rootDir,
|
|
463
568
|
env: backendEnv,
|
|
464
|
-
stdio: 'inherit',
|
|
569
|
+
stdio: ['inherit', 'inherit', 'pipe'], // Pipe stderr to capture errors
|
|
465
570
|
detached: false
|
|
466
571
|
});
|
|
467
572
|
|
|
573
|
+
// Capture stderr while also logging it to console
|
|
574
|
+
backendProc.stderr.on('data', (data) => {
|
|
575
|
+
const text = data.toString();
|
|
576
|
+
stderrBuffer += text;
|
|
577
|
+
process.stderr.write(text); // Still show errors to user
|
|
578
|
+
});
|
|
579
|
+
|
|
468
580
|
backendProc.on('exit', (code, signal) => {
|
|
469
581
|
if (isShuttingDown) return;
|
|
470
582
|
|
|
@@ -472,12 +584,33 @@ async function runSplitServeWithWatch(context, tsxExecutable, tsxArgs, baseEnv,
|
|
|
472
584
|
const runDuration = Date.now() - backendStartTime;
|
|
473
585
|
if (runDuration > STABILITY_THRESHOLD) {
|
|
474
586
|
backendCrashRestarts = 0;
|
|
587
|
+
backendPortRetries = 0; // Also reset port retries on stability
|
|
475
588
|
}
|
|
476
589
|
|
|
590
|
+
// Quick exit (< 5 seconds) might be port conflict OR boot error
|
|
591
|
+
const isQuickExit = runDuration < 5000;
|
|
592
|
+
|
|
593
|
+
// Check stderr for EADDRINUSE to determine error type
|
|
594
|
+
const isPortError = stderrBuffer.includes('EADDRINUSE') ||
|
|
595
|
+
stderrBuffer.includes('address already in use') ||
|
|
596
|
+
stderrBuffer.includes('listen EADDRINUSE');
|
|
597
|
+
|
|
477
598
|
if (code !== null && code !== 0) {
|
|
478
|
-
logger.error(`[BACKEND] Exited with code ${code}, signal ${signal}`);
|
|
599
|
+
logger.error(`[BACKEND] Exited with code ${code}, signal ${signal}, ran for ${runDuration}ms`);
|
|
479
600
|
|
|
480
|
-
//
|
|
601
|
+
// Differentiate between port errors and other boot errors
|
|
602
|
+
if (isQuickExit && !isPortError && stderrBuffer.length > 0) {
|
|
603
|
+
// Boot error (syntax error, missing module, etc.) - don't retry
|
|
604
|
+
logger.error(`[BACKEND] Boot error detected (not a port conflict). Not retrying.`);
|
|
605
|
+
logger.error(`[BACKEND] Fix the error and save a file to trigger a new restart attempt.`);
|
|
606
|
+
backendProc = null;
|
|
607
|
+
// Reset crash counter so next file change gets a fresh attempt
|
|
608
|
+
backendCrashRestarts = 0;
|
|
609
|
+
backendPortRetries = 0;
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Auto-restart on crash if under limit (for port errors or runtime crashes)
|
|
481
614
|
if (backendCrashRestarts < MAX_CRASH_RESTARTS) {
|
|
482
615
|
backendCrashRestarts++;
|
|
483
616
|
logger.warn(`[BACKEND] Crash detected. Auto-restart ${backendCrashRestarts}/${MAX_CRASH_RESTARTS}...`);
|
|
@@ -492,7 +625,24 @@ async function runSplitServeWithWatch(context, tsxExecutable, tsxArgs, baseEnv,
|
|
|
492
625
|
}
|
|
493
626
|
},
|
|
494
627
|
error: (err) => {
|
|
495
|
-
logger.warn(`[BACKEND] Port wait
|
|
628
|
+
logger.warn(`[BACKEND] Port wait timed out: ${err.message}`);
|
|
629
|
+
|
|
630
|
+
// Quick exit + port not free + port error = likely EADDRINUSE from stale process
|
|
631
|
+
if (isQuickExit && isPortError) {
|
|
632
|
+
logger.warn(`[BACKEND] Port conflict detected - attempting port recovery...`);
|
|
633
|
+
const recovered = recoverFromPortInUse(backendPort, logger);
|
|
634
|
+
if (recovered && !isShuttingDown) {
|
|
635
|
+
// Wait a moment for OS to fully release the port
|
|
636
|
+
setTimeout(() => {
|
|
637
|
+
if (!isShuttingDown) {
|
|
638
|
+
startBackend(true); // Mark as recovery retry
|
|
639
|
+
}
|
|
640
|
+
}, 500);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Fallback: try restart anyway
|
|
496
646
|
if (!isShuttingDown) {
|
|
497
647
|
startBackend();
|
|
498
648
|
}
|
|
@@ -507,10 +657,36 @@ async function runSplitServeWithWatch(context, tsxExecutable, tsxArgs, baseEnv,
|
|
|
507
657
|
|
|
508
658
|
backendProc.on('error', (err) => {
|
|
509
659
|
logger.error(`[BACKEND] Spawn error: ${err.message}`);
|
|
660
|
+
|
|
661
|
+
// Check if this is an EADDRINUSE-like error at spawn level
|
|
662
|
+
if (err.code === 'EADDRINUSE' || err.message.includes('EADDRINUSE')) {
|
|
663
|
+
logger.warn(`[BACKEND] EADDRINUSE detected at spawn - attempting port recovery...`);
|
|
664
|
+
const recovered = recoverFromPortInUse(backendPort, logger);
|
|
665
|
+
if (recovered && backendPortRetries < MAX_PORT_RECOVERY_RETRIES && !isShuttingDown) {
|
|
666
|
+
setTimeout(() => {
|
|
667
|
+
if (!isShuttingDown) {
|
|
668
|
+
startBackend(true);
|
|
669
|
+
}
|
|
670
|
+
}, 500);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
510
673
|
});
|
|
511
674
|
};
|
|
512
675
|
|
|
513
|
-
const startFrontend = () => {
|
|
676
|
+
const startFrontend = (retryAfterRecovery = false) => {
|
|
677
|
+
if (retryAfterRecovery) {
|
|
678
|
+
frontendPortRetries++;
|
|
679
|
+
if (frontendPortRetries > MAX_PORT_RECOVERY_RETRIES) {
|
|
680
|
+
logger.error(`[FRONTEND] Exceeded max port recovery retries (${MAX_PORT_RECOVERY_RETRIES}). Shutting down...`);
|
|
681
|
+
shutdown('frontend-port-recovery-exceeded', 1, 'frontend-port-recovery');
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
logger.warn(`[FRONTEND] Port recovery retry ${frontendPortRetries}/${MAX_PORT_RECOVERY_RETRIES}...`);
|
|
685
|
+
} else {
|
|
686
|
+
// Reset retry counter on normal start
|
|
687
|
+
frontendPortRetries = 0;
|
|
688
|
+
}
|
|
689
|
+
|
|
514
690
|
frontendRestartCount++;
|
|
515
691
|
frontendStartTime = Date.now();
|
|
516
692
|
logger.info(`🚀 [RESTART #${frontendRestartCount}] Starting frontend on port ${frontendPort}...`);
|
|
@@ -520,13 +696,23 @@ async function runSplitServeWithWatch(context, tsxExecutable, tsxArgs, baseEnv,
|
|
|
520
696
|
NOEGO_PORT: String(frontendPort)
|
|
521
697
|
};
|
|
522
698
|
|
|
699
|
+
// Capture stderr to differentiate between port errors and boot errors
|
|
700
|
+
let stderrBuffer = '';
|
|
701
|
+
|
|
523
702
|
frontendProc = spawn(tsxExecutable, tsxArgs, {
|
|
524
703
|
cwd: context.config.rootDir,
|
|
525
704
|
env: frontendEnv,
|
|
526
|
-
stdio: 'inherit',
|
|
705
|
+
stdio: ['inherit', 'inherit', 'pipe'], // Pipe stderr to capture errors
|
|
527
706
|
detached: false
|
|
528
707
|
});
|
|
529
708
|
|
|
709
|
+
// Capture stderr while also logging it to console
|
|
710
|
+
frontendProc.stderr.on('data', (data) => {
|
|
711
|
+
const text = data.toString();
|
|
712
|
+
stderrBuffer += text;
|
|
713
|
+
process.stderr.write(text); // Still show errors to user
|
|
714
|
+
});
|
|
715
|
+
|
|
530
716
|
frontendProc.on('exit', (code, signal) => {
|
|
531
717
|
if (isShuttingDown) return;
|
|
532
718
|
|
|
@@ -534,12 +720,33 @@ async function runSplitServeWithWatch(context, tsxExecutable, tsxArgs, baseEnv,
|
|
|
534
720
|
const runDuration = Date.now() - frontendStartTime;
|
|
535
721
|
if (runDuration > STABILITY_THRESHOLD) {
|
|
536
722
|
frontendCrashRestarts = 0;
|
|
723
|
+
frontendPortRetries = 0; // Also reset port retries on stability
|
|
537
724
|
}
|
|
538
725
|
|
|
726
|
+
// Quick exit (< 5 seconds) might be port conflict OR boot error
|
|
727
|
+
const isQuickExit = runDuration < 5000;
|
|
728
|
+
|
|
729
|
+
// Check stderr for EADDRINUSE to determine error type
|
|
730
|
+
const isPortError = stderrBuffer.includes('EADDRINUSE') ||
|
|
731
|
+
stderrBuffer.includes('address already in use') ||
|
|
732
|
+
stderrBuffer.includes('listen EADDRINUSE');
|
|
733
|
+
|
|
539
734
|
if (code !== null && code !== 0) {
|
|
540
|
-
logger.error(`[FRONTEND] Exited with code ${code}, signal ${signal}`);
|
|
735
|
+
logger.error(`[FRONTEND] Exited with code ${code}, signal ${signal}, ran for ${runDuration}ms`);
|
|
736
|
+
|
|
737
|
+
// Differentiate between port errors and other boot errors
|
|
738
|
+
if (isQuickExit && !isPortError && stderrBuffer.length > 0) {
|
|
739
|
+
// Boot error (syntax error, missing module, etc.) - don't retry
|
|
740
|
+
logger.error(`[FRONTEND] Boot error detected (not a port conflict). Not retrying.`);
|
|
741
|
+
logger.error(`[FRONTEND] Fix the error and save a file to trigger a new restart attempt.`);
|
|
742
|
+
frontendProc = null;
|
|
743
|
+
// Reset crash counter so next file change gets a fresh attempt
|
|
744
|
+
frontendCrashRestarts = 0;
|
|
745
|
+
frontendPortRetries = 0;
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
541
748
|
|
|
542
|
-
// Auto-restart on crash if under limit
|
|
749
|
+
// Auto-restart on crash if under limit (for port errors or runtime crashes)
|
|
543
750
|
if (frontendCrashRestarts < MAX_CRASH_RESTARTS) {
|
|
544
751
|
frontendCrashRestarts++;
|
|
545
752
|
logger.warn(`[FRONTEND] Crash detected. Auto-restart ${frontendCrashRestarts}/${MAX_CRASH_RESTARTS}...`);
|
|
@@ -554,7 +761,24 @@ async function runSplitServeWithWatch(context, tsxExecutable, tsxArgs, baseEnv,
|
|
|
554
761
|
}
|
|
555
762
|
},
|
|
556
763
|
error: (err) => {
|
|
557
|
-
logger.warn(`[FRONTEND] Port wait
|
|
764
|
+
logger.warn(`[FRONTEND] Port wait timed out: ${err.message}`);
|
|
765
|
+
|
|
766
|
+
// Quick exit + port not free + port error = likely EADDRINUSE from stale process
|
|
767
|
+
if (isQuickExit && isPortError) {
|
|
768
|
+
logger.warn(`[FRONTEND] Port conflict detected - attempting port recovery...`);
|
|
769
|
+
const recovered = recoverFromPortInUse(frontendPort, logger);
|
|
770
|
+
if (recovered && !isShuttingDown) {
|
|
771
|
+
// Wait a moment for OS to fully release the port
|
|
772
|
+
setTimeout(() => {
|
|
773
|
+
if (!isShuttingDown) {
|
|
774
|
+
startFrontend(true); // Mark as recovery retry
|
|
775
|
+
}
|
|
776
|
+
}, 500);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Fallback: try restart anyway
|
|
558
782
|
if (!isShuttingDown) {
|
|
559
783
|
startFrontend();
|
|
560
784
|
}
|
|
@@ -569,6 +793,19 @@ async function runSplitServeWithWatch(context, tsxExecutable, tsxArgs, baseEnv,
|
|
|
569
793
|
|
|
570
794
|
frontendProc.on('error', (err) => {
|
|
571
795
|
logger.error(`[FRONTEND] Spawn error: ${err.message}`);
|
|
796
|
+
|
|
797
|
+
// Check if this is an EADDRINUSE-like error at spawn level
|
|
798
|
+
if (err.code === 'EADDRINUSE' || err.message.includes('EADDRINUSE')) {
|
|
799
|
+
logger.warn(`[FRONTEND] EADDRINUSE detected at spawn - attempting port recovery...`);
|
|
800
|
+
const recovered = recoverFromPortInUse(frontendPort, logger);
|
|
801
|
+
if (recovered && frontendPortRetries < MAX_PORT_RECOVERY_RETRIES && !isShuttingDown) {
|
|
802
|
+
setTimeout(() => {
|
|
803
|
+
if (!isShuttingDown) {
|
|
804
|
+
startFrontend(true);
|
|
805
|
+
}
|
|
806
|
+
}, 500);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
572
809
|
});
|
|
573
810
|
};
|
|
574
811
|
|
|
@@ -893,6 +1130,12 @@ async function runSplitServeWithWatch(context, tsxExecutable, tsxArgs, baseEnv,
|
|
|
893
1130
|
})
|
|
894
1131
|
.on('error', handleWatcherError);
|
|
895
1132
|
|
|
1133
|
+
// Proactive port recovery before initial startup
|
|
1134
|
+
// This handles the case where stale processes from a previous dev session are holding the ports
|
|
1135
|
+
logger.info(`[STARTUP] Checking ports ${backendPort} and ${frontendPort} for stale processes...`);
|
|
1136
|
+
recoverFromPortInUse(backendPort, logger);
|
|
1137
|
+
recoverFromPortInUse(frontendPort, logger);
|
|
1138
|
+
|
|
896
1139
|
// Start initial processes
|
|
897
1140
|
startBackend();
|
|
898
1141
|
startFrontend();
|
package/src/runtime/runtime.js
CHANGED
|
@@ -390,10 +390,23 @@ async function runBackendService(config) {
|
|
|
390
390
|
console.log('[backend] config.dev.backendPort:', config.dev.backendPort);
|
|
391
391
|
console.log('[backend] Using port:', backendPort);
|
|
392
392
|
|
|
393
|
-
|
|
393
|
+
const httpServer = http.createServer(backendApp);
|
|
394
|
+
|
|
395
|
+
httpServer.on('error', (err) => {
|
|
396
|
+
if (err.code === 'EADDRINUSE') {
|
|
397
|
+
console.error(`[ERROR] Port ${backendPort} is already in use (EADDRINUSE)`);
|
|
398
|
+
console.error(`[ERROR] This likely means a stale process is still running.`);
|
|
399
|
+
console.error(`[ERROR] The dev server will attempt to recover...`);
|
|
400
|
+
} else {
|
|
401
|
+
console.error(`[ERROR] Server error on port ${backendPort}:`, err.message);
|
|
402
|
+
}
|
|
403
|
+
process.exit(1);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
httpServer.listen(backendPort, '0.0.0.0', () => {
|
|
394
407
|
console.log(`Backend server running on http://localhost:${backendPort}`);
|
|
395
408
|
});
|
|
396
|
-
|
|
409
|
+
|
|
397
410
|
return backendApp;
|
|
398
411
|
}
|
|
399
412
|
|
|
@@ -441,6 +454,17 @@ async function runFrontendService(config) {
|
|
|
441
454
|
console.log('[frontend] config.dev.port:', config.dev.port);
|
|
442
455
|
console.log('[frontend] Using port:', frontendPort);
|
|
443
456
|
|
|
457
|
+
httpServer.on('error', (err) => {
|
|
458
|
+
if (err.code === 'EADDRINUSE') {
|
|
459
|
+
console.error(`[ERROR] Port ${frontendPort} is already in use (EADDRINUSE)`);
|
|
460
|
+
console.error(`[ERROR] This likely means a stale process is still running.`);
|
|
461
|
+
console.error(`[ERROR] The dev server will attempt to recover...`);
|
|
462
|
+
} else {
|
|
463
|
+
console.error(`[ERROR] Server error on port ${frontendPort}:`, err.message);
|
|
464
|
+
}
|
|
465
|
+
process.exit(1);
|
|
466
|
+
});
|
|
467
|
+
|
|
444
468
|
httpServer.listen(frontendPort, '0.0.0.0', () => {
|
|
445
469
|
console.log(`Frontend server running on http://localhost:${frontendPort}`);
|
|
446
470
|
});
|
|
@@ -669,6 +693,17 @@ async function runRouterService(config) {
|
|
|
669
693
|
try { socket?.destroy?.(); } catch {}
|
|
670
694
|
});
|
|
671
695
|
|
|
696
|
+
httpServer.on('error', (err) => {
|
|
697
|
+
if (err.code === 'EADDRINUSE') {
|
|
698
|
+
console.error(`[ERROR] Port ${routerPort} is already in use (EADDRINUSE)`);
|
|
699
|
+
console.error(`[ERROR] This likely means a stale process is still running.`);
|
|
700
|
+
console.error(`[ERROR] The dev server will attempt to recover...`);
|
|
701
|
+
} else {
|
|
702
|
+
console.error(`[ERROR] Server error on port ${routerPort}:`, err.message);
|
|
703
|
+
}
|
|
704
|
+
process.exit(1);
|
|
705
|
+
});
|
|
706
|
+
|
|
672
707
|
httpServer.listen(routerPort, '0.0.0.0', () => {
|
|
673
708
|
console.log(`Router server running on http://localhost:${routerPort}`);
|
|
674
709
|
console.log(` Proxying to frontend on port ${frontendPort}`);
|
|
@@ -724,6 +759,18 @@ export async function runCombinedServices(config, options = {}) {
|
|
|
724
759
|
|
|
725
760
|
// Allow PORT env var to override config
|
|
726
761
|
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : (config.dev?.port || 3000);
|
|
762
|
+
|
|
763
|
+
httpServer.on('error', (err) => {
|
|
764
|
+
if (err.code === 'EADDRINUSE') {
|
|
765
|
+
console.error(`[ERROR] Port ${port} is already in use (EADDRINUSE)`);
|
|
766
|
+
console.error(`[ERROR] This likely means a stale process is still running.`);
|
|
767
|
+
console.error(`[ERROR] The dev server will attempt to recover...`);
|
|
768
|
+
} else {
|
|
769
|
+
console.error(`[ERROR] Server error on port ${port}:`, err.message);
|
|
770
|
+
}
|
|
771
|
+
process.exit(1);
|
|
772
|
+
});
|
|
773
|
+
|
|
727
774
|
httpServer.listen(port, '0.0.0.0', () => {
|
|
728
775
|
const services = [];
|
|
729
776
|
if (hasBackend && config.server?.main_abs) services.push('Backend');
|