@noego/app 0.0.11 โ 0.0.12
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/.claude/settings.local.json +17 -0
- package/package.json +1 -1
- package/src/args.js +17 -2
- package/src/cli.js +6 -0
- package/src/commands/dev.js +80 -16
- package/src/commands/test-live.js +187 -0
- package/src/config.js +7 -0
- package/src/runtime/runtime.js +4 -1
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(cat:*)",
|
|
5
|
+
"Bash(curl:*)",
|
|
6
|
+
"Bash(noego dev)",
|
|
7
|
+
"WebSearch",
|
|
8
|
+
"WebFetch(domain:mattkentzia.com)",
|
|
9
|
+
"Bash(node --test:*)",
|
|
10
|
+
"WebFetch(domain:svelte.dev)",
|
|
11
|
+
"WebFetch(domain:github.com)",
|
|
12
|
+
"Bash(tree:*)"
|
|
13
|
+
],
|
|
14
|
+
"deny": [],
|
|
15
|
+
"ask": []
|
|
16
|
+
}
|
|
17
|
+
}
|
package/package.json
CHANGED
package/src/args.js
CHANGED
|
@@ -27,11 +27,17 @@ const FLAG_MAP = new Map([
|
|
|
27
27
|
['mode', 'mode'],
|
|
28
28
|
['verbose', 'verbose'],
|
|
29
29
|
['help', 'help'],
|
|
30
|
-
['version', 'version']
|
|
30
|
+
['version', 'version'],
|
|
31
|
+
['ci-server', 'testServer'],
|
|
32
|
+
['ci-test', 'testCommand'],
|
|
33
|
+
['ci-status', 'testStatus'],
|
|
34
|
+
['ci-port', 'testPort'],
|
|
35
|
+
['ci-timeout', 'testTimeout'],
|
|
36
|
+
['ci-visible', 'testVisible']
|
|
31
37
|
]);
|
|
32
38
|
|
|
33
39
|
const MULTI_VALUE_FLAGS = new Set(['sqlGlob', 'assets', 'clientExclude', 'watchPath']);
|
|
34
|
-
const BOOLEAN_FLAGS = new Set(['watch', 'splitServe', 'verbose']);
|
|
40
|
+
const BOOLEAN_FLAGS = new Set(['watch', 'splitServe', 'verbose', 'testVisible']);
|
|
35
41
|
|
|
36
42
|
export function parseCliArgs(argv) {
|
|
37
43
|
const result = {
|
|
@@ -129,6 +135,15 @@ export function printHelpAndExit({ stdout = process.stdout } = {}) {
|
|
|
129
135
|
app dev [options]
|
|
130
136
|
app serve [options]
|
|
131
137
|
app preview [options]
|
|
138
|
+
app ci [options]
|
|
139
|
+
|
|
140
|
+
CI Testing Options:
|
|
141
|
+
--ci-server <cmd> Server start command (default: npm run dev)
|
|
142
|
+
--ci-test <cmd> Test command (default: npm run test:live)
|
|
143
|
+
--ci-status <path> Health check endpoint (default: /api/status)
|
|
144
|
+
--ci-port <number> Server port (default: random 4000-8000)
|
|
145
|
+
--ci-timeout <seconds> Health check timeout (default: 60)
|
|
146
|
+
--ci-visible Run browser in visible mode
|
|
132
147
|
|
|
133
148
|
Options (shared):
|
|
134
149
|
--root <dir> Project root (default: .)
|
package/src/cli.js
CHANGED
|
@@ -8,6 +8,7 @@ import { runBuild } from './commands/build.js';
|
|
|
8
8
|
import { runServe } from './commands/serve.js';
|
|
9
9
|
import { runPreview } from './commands/preview.js';
|
|
10
10
|
import { runDev } from './commands/dev.js';
|
|
11
|
+
import { runTestLive } from './commands/test-live.js';
|
|
11
12
|
|
|
12
13
|
export async function runCli(argv = process.argv.slice(2)) {
|
|
13
14
|
try {
|
|
@@ -43,6 +44,11 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
43
44
|
await runPreview(config);
|
|
44
45
|
break;
|
|
45
46
|
}
|
|
47
|
+
case 'ci': {
|
|
48
|
+
const config = await loadBuildConfig(options, { cwd: process.cwd() });
|
|
49
|
+
await runTestLive(config);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
46
52
|
default:
|
|
47
53
|
throw cliError(`Unknown command "${command}"`);
|
|
48
54
|
}
|
package/src/commands/dev.js
CHANGED
|
@@ -449,52 +449,115 @@ async function runSplitServeWithWatch(context, tsxExecutable, tsxArgs, baseEnv,
|
|
|
449
449
|
let frontendRestartCount = 0;
|
|
450
450
|
let isShuttingDown = false;
|
|
451
451
|
|
|
452
|
+
// Crash restart tracking
|
|
453
|
+
const MAX_CRASH_RESTARTS = 3;
|
|
454
|
+
const CRASH_RESTART_DELAY = 2000; // 2 seconds between crash restarts
|
|
455
|
+
const STABILITY_THRESHOLD = 30000; // Reset crash counter if running 30+ seconds
|
|
456
|
+
let backendCrashRestarts = 0;
|
|
457
|
+
let frontendCrashRestarts = 0;
|
|
458
|
+
let backendStartTime = 0;
|
|
459
|
+
let frontendStartTime = 0;
|
|
460
|
+
|
|
452
461
|
const startBackend = () => {
|
|
453
462
|
backendRestartCount++;
|
|
463
|
+
backendStartTime = Date.now();
|
|
454
464
|
logger.info(`๐ [RESTART #${backendRestartCount}] Starting backend on port ${backendPort}...`);
|
|
455
465
|
const backendEnv = {
|
|
456
466
|
...baseEnv,
|
|
457
467
|
NOEGO_SERVICE: 'backend',
|
|
458
468
|
NOEGO_PORT: String(backendPort)
|
|
459
469
|
};
|
|
460
|
-
|
|
470
|
+
|
|
461
471
|
backendProc = spawn(tsxExecutable, tsxArgs, {
|
|
462
472
|
cwd: context.config.rootDir,
|
|
463
473
|
env: backendEnv,
|
|
464
474
|
stdio: 'inherit',
|
|
465
475
|
detached: false
|
|
466
476
|
});
|
|
467
|
-
|
|
468
|
-
backendProc.on('exit', (code) => {
|
|
477
|
+
|
|
478
|
+
backendProc.on('exit', (code, signal) => {
|
|
479
|
+
if (isShuttingDown) return;
|
|
480
|
+
|
|
481
|
+
// Check if process was stable (ran for a while) - reset crash counter
|
|
482
|
+
const runDuration = Date.now() - backendStartTime;
|
|
483
|
+
if (runDuration > STABILITY_THRESHOLD) {
|
|
484
|
+
backendCrashRestarts = 0;
|
|
485
|
+
}
|
|
486
|
+
|
|
469
487
|
if (code !== null && code !== 0) {
|
|
470
|
-
logger.error(`
|
|
488
|
+
logger.error(`[BACKEND] Exited with code ${code}, signal ${signal}`);
|
|
489
|
+
|
|
490
|
+
// Auto-restart on crash if under limit
|
|
491
|
+
if (backendCrashRestarts < MAX_CRASH_RESTARTS) {
|
|
492
|
+
backendCrashRestarts++;
|
|
493
|
+
logger.warn(`[BACKEND] Crash detected. Auto-restart ${backendCrashRestarts}/${MAX_CRASH_RESTARTS} in ${CRASH_RESTART_DELAY}ms...`);
|
|
494
|
+
setTimeout(() => {
|
|
495
|
+
if (!isShuttingDown) {
|
|
496
|
+
startBackend();
|
|
497
|
+
}
|
|
498
|
+
}, CRASH_RESTART_DELAY);
|
|
499
|
+
} else {
|
|
500
|
+
logger.error(`[BACKEND] Exceeded max crash restarts (${MAX_CRASH_RESTARTS}). Shutting down...`);
|
|
501
|
+
shutdown('backend-exceeded-restarts', 1, 'backend-crash');
|
|
502
|
+
}
|
|
471
503
|
}
|
|
472
504
|
});
|
|
505
|
+
|
|
506
|
+
backendProc.on('error', (err) => {
|
|
507
|
+
logger.error(`[BACKEND] Spawn error: ${err.message}`);
|
|
508
|
+
});
|
|
473
509
|
};
|
|
474
|
-
|
|
510
|
+
|
|
475
511
|
const startFrontend = () => {
|
|
476
512
|
frontendRestartCount++;
|
|
513
|
+
frontendStartTime = Date.now();
|
|
477
514
|
logger.info(`๐ [RESTART #${frontendRestartCount}] Starting frontend on port ${frontendPort}...`);
|
|
478
515
|
const frontendEnv = {
|
|
479
516
|
...baseEnv,
|
|
480
517
|
NOEGO_SERVICE: 'frontend',
|
|
481
518
|
NOEGO_PORT: String(frontendPort)
|
|
482
519
|
};
|
|
483
|
-
|
|
520
|
+
|
|
484
521
|
frontendProc = spawn(tsxExecutable, tsxArgs, {
|
|
485
522
|
cwd: context.config.rootDir,
|
|
486
523
|
env: frontendEnv,
|
|
487
524
|
stdio: 'inherit',
|
|
488
525
|
detached: false
|
|
489
526
|
});
|
|
490
|
-
|
|
491
|
-
frontendProc.on('exit', (code) => {
|
|
527
|
+
|
|
528
|
+
frontendProc.on('exit', (code, signal) => {
|
|
529
|
+
if (isShuttingDown) return;
|
|
530
|
+
|
|
531
|
+
// Check if process was stable (ran for a while) - reset crash counter
|
|
532
|
+
const runDuration = Date.now() - frontendStartTime;
|
|
533
|
+
if (runDuration > STABILITY_THRESHOLD) {
|
|
534
|
+
frontendCrashRestarts = 0;
|
|
535
|
+
}
|
|
536
|
+
|
|
492
537
|
if (code !== null && code !== 0) {
|
|
493
|
-
logger.error(`
|
|
538
|
+
logger.error(`[FRONTEND] Exited with code ${code}, signal ${signal}`);
|
|
539
|
+
|
|
540
|
+
// Auto-restart on crash if under limit
|
|
541
|
+
if (frontendCrashRestarts < MAX_CRASH_RESTARTS) {
|
|
542
|
+
frontendCrashRestarts++;
|
|
543
|
+
logger.warn(`[FRONTEND] Crash detected. Auto-restart ${frontendCrashRestarts}/${MAX_CRASH_RESTARTS} in ${CRASH_RESTART_DELAY}ms...`);
|
|
544
|
+
setTimeout(() => {
|
|
545
|
+
if (!isShuttingDown) {
|
|
546
|
+
startFrontend();
|
|
547
|
+
}
|
|
548
|
+
}, CRASH_RESTART_DELAY);
|
|
549
|
+
} else {
|
|
550
|
+
logger.error(`[FRONTEND] Exceeded max crash restarts (${MAX_CRASH_RESTARTS}). Shutting down...`);
|
|
551
|
+
shutdown('frontend-exceeded-restarts', 1, 'frontend-crash');
|
|
552
|
+
}
|
|
494
553
|
}
|
|
495
554
|
});
|
|
555
|
+
|
|
556
|
+
frontendProc.on('error', (err) => {
|
|
557
|
+
logger.error(`[FRONTEND] Spawn error: ${err.message}`);
|
|
558
|
+
});
|
|
496
559
|
};
|
|
497
|
-
|
|
560
|
+
|
|
498
561
|
const stopBackend = () =>
|
|
499
562
|
new Promise((resolve) => {
|
|
500
563
|
if (!backendProc) return resolve();
|
|
@@ -549,10 +612,11 @@ async function runSplitServeWithWatch(context, tsxExecutable, tsxArgs, baseEnv,
|
|
|
549
612
|
}
|
|
550
613
|
});
|
|
551
614
|
|
|
552
|
-
async function shutdown(signal = 'SIGTERM', exitCode = 0) {
|
|
615
|
+
async function shutdown(signal = 'SIGTERM', exitCode = 0, source = 'unknown') {
|
|
553
616
|
if (isShuttingDown) return;
|
|
554
617
|
isShuttingDown = true;
|
|
555
618
|
|
|
619
|
+
logger.info(`[SHUTDOWN] source=${source}, signal=${signal}, exitCode=${exitCode}`);
|
|
556
620
|
logger.info(`Shutting down split-serve processes (signal: ${signal})...`);
|
|
557
621
|
|
|
558
622
|
try {
|
|
@@ -577,7 +641,7 @@ async function runSplitServeWithWatch(context, tsxExecutable, tsxArgs, baseEnv,
|
|
|
577
641
|
}
|
|
578
642
|
logger.error('Watcher error:', error);
|
|
579
643
|
logger.error('File watching failed; shutting down dev server.');
|
|
580
|
-
await shutdown('watcherError', 1);
|
|
644
|
+
await shutdown('watcherError', 1, 'watcher-error');
|
|
581
645
|
}
|
|
582
646
|
|
|
583
647
|
const handleFileChange = async (reason, file) => {
|
|
@@ -699,8 +763,8 @@ async function runSplitServeWithWatch(context, tsxExecutable, tsxArgs, baseEnv,
|
|
|
699
763
|
await import(runtimeEntryPath);
|
|
700
764
|
|
|
701
765
|
// Handle graceful shutdown signals
|
|
702
|
-
process.on('SIGINT', () => shutdown('SIGINT', 0));
|
|
703
|
-
process.on('SIGTERM', () => shutdown('SIGTERM', 0));
|
|
766
|
+
process.on('SIGINT', () => shutdown('SIGINT', 0, 'signal-handler'));
|
|
767
|
+
process.on('SIGTERM', () => shutdown('SIGTERM', 0, 'signal-handler'));
|
|
704
768
|
|
|
705
769
|
// Handle process exit - this ensures children are killed even if parent crashes
|
|
706
770
|
process.on('exit', () => {
|
|
@@ -717,13 +781,13 @@ async function runSplitServeWithWatch(context, tsxExecutable, tsxArgs, baseEnv,
|
|
|
717
781
|
// Handle uncaught exceptions
|
|
718
782
|
process.on('uncaughtException', async (error) => {
|
|
719
783
|
logger.error('Uncaught exception:', error);
|
|
720
|
-
await shutdown('uncaughtException', 1);
|
|
784
|
+
await shutdown('uncaughtException', 1, 'uncaught-exception');
|
|
721
785
|
});
|
|
722
786
|
|
|
723
787
|
// Handle unhandled promise rejections
|
|
724
788
|
process.on('unhandledRejection', async (reason, promise) => {
|
|
725
789
|
logger.error('Unhandled rejection at:', promise, 'reason:', reason);
|
|
726
|
-
await shutdown('unhandledRejection', 1);
|
|
790
|
+
await shutdown('unhandledRejection', 1, 'unhandled-rejection');
|
|
727
791
|
});
|
|
728
792
|
|
|
729
793
|
// Keep process alive for file watching
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { createBuildContext } from '../build/context.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Polls health check endpoint until server is ready
|
|
6
|
+
*/
|
|
7
|
+
async function waitForServer(port, statusPath, timeoutSec, logger) {
|
|
8
|
+
const url = `http://localhost:${port}${statusPath}`;
|
|
9
|
+
const maxAttempts = timeoutSec;
|
|
10
|
+
|
|
11
|
+
logger.info(`Waiting for server at ${url}...`);
|
|
12
|
+
|
|
13
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
14
|
+
try {
|
|
15
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(2000) });
|
|
16
|
+
|
|
17
|
+
if (response.ok) {
|
|
18
|
+
const data = await response.json();
|
|
19
|
+
|
|
20
|
+
// For /status/deep - check both status and database
|
|
21
|
+
if (statusPath.includes('deep')) {
|
|
22
|
+
if (data.status === 'OK' && data.database === 'connected') {
|
|
23
|
+
logger.info('Server is ready!');
|
|
24
|
+
await new Promise(r => setTimeout(r, 2000)); // Extra 2s for full init
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
} else if (data.status === 'OK' || data.status === 'ok') {
|
|
28
|
+
logger.info('Server is ready!');
|
|
29
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
} catch (err) {
|
|
34
|
+
// Connection refused or timeout - server not ready yet
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (attempt < maxAttempts) {
|
|
38
|
+
await new Promise(r => setTimeout(r, 1000)); // Wait 1s between attempts
|
|
39
|
+
if (attempt % 5 === 0) {
|
|
40
|
+
logger.info(`Still waiting... (${attempt}/${maxAttempts})`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Spawns a command and returns a process handle
|
|
50
|
+
*/
|
|
51
|
+
function spawnCommand(command, env, logger) {
|
|
52
|
+
logger.info(`Spawning: ${command}`);
|
|
53
|
+
|
|
54
|
+
const child = spawn(command, {
|
|
55
|
+
shell: true,
|
|
56
|
+
stdio: 'inherit',
|
|
57
|
+
env: { ...process.env, ...env },
|
|
58
|
+
detached: process.platform !== 'win32', // Process group for Unix
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return child;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Kills process tree (cross-platform)
|
|
66
|
+
*/
|
|
67
|
+
function killProcessTree(pid, logger) {
|
|
68
|
+
try {
|
|
69
|
+
if (process.platform === 'win32') {
|
|
70
|
+
spawn('taskkill', ['/pid', pid.toString(), '/T', '/F'], { stdio: 'ignore' });
|
|
71
|
+
} else {
|
|
72
|
+
process.kill(-pid, 'SIGKILL'); // Negative PID kills process group
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
logger.debug(`Process ${pid} already terminated`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Main test:live command implementation
|
|
81
|
+
*/
|
|
82
|
+
export async function runTestLive(config) {
|
|
83
|
+
const context = createBuildContext(config);
|
|
84
|
+
const { logger } = context;
|
|
85
|
+
|
|
86
|
+
// Parse options - no defaults, must be specified
|
|
87
|
+
const serverCmd = config.testServer;
|
|
88
|
+
const testCmd = config.testCommand;
|
|
89
|
+
const statusPath = config.testStatus;
|
|
90
|
+
const port = config.testPort || Math.floor(Math.random() * 4000) + 4000;
|
|
91
|
+
const timeout = config.testTimeout || 60;
|
|
92
|
+
const visible = config.testVisible || false;
|
|
93
|
+
|
|
94
|
+
// Validate required options
|
|
95
|
+
if (!serverCmd) {
|
|
96
|
+
logger.error('โ Missing required option: --ci-server');
|
|
97
|
+
logger.info('Example: noego ci --ci-server "npm run dev" --ci-test "npm run test:live" --ci-status "/api/status"');
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!testCmd) {
|
|
102
|
+
logger.error('โ Missing required option: --ci-test');
|
|
103
|
+
logger.info('Example: noego ci --ci-server "npm run dev" --ci-test "npm run test:live" --ci-status "/api/status"');
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!statusPath) {
|
|
108
|
+
logger.error('โ Missing required option: --ci-status');
|
|
109
|
+
logger.info('Example: noego ci --ci-server "npm run dev" --ci-test "npm run test:live" --ci-status "/api/status"');
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
logger.info('๐งช Live Test Runner');
|
|
114
|
+
logger.info(` Server: ${serverCmd}`);
|
|
115
|
+
logger.info(` Tests: ${testCmd}`);
|
|
116
|
+
logger.info(` Port: ${port}`);
|
|
117
|
+
logger.info(` Health: ${statusPath}`);
|
|
118
|
+
|
|
119
|
+
let serverProcess = null;
|
|
120
|
+
let exitCode = 1;
|
|
121
|
+
|
|
122
|
+
const cleanup = async () => {
|
|
123
|
+
if (serverProcess && !serverProcess.killed) {
|
|
124
|
+
logger.info('Shutting down server...');
|
|
125
|
+
killProcessTree(serverProcess.pid, logger);
|
|
126
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Handle interrupts
|
|
131
|
+
process.on('SIGINT', async () => {
|
|
132
|
+
logger.info('Interrupted by user');
|
|
133
|
+
await cleanup();
|
|
134
|
+
process.exit(130);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
process.on('SIGTERM', async () => {
|
|
138
|
+
await cleanup();
|
|
139
|
+
process.exit(143);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
// Start server
|
|
144
|
+
serverProcess = spawnCommand(serverCmd, { PORT: port.toString() }, logger);
|
|
145
|
+
|
|
146
|
+
// Wait for health check
|
|
147
|
+
logger.info('Waiting for server to be ready...');
|
|
148
|
+
const ready = await waitForServer(port, statusPath, timeout, logger);
|
|
149
|
+
|
|
150
|
+
if (!ready) {
|
|
151
|
+
logger.error(`โ Server failed to become healthy within ${timeout} seconds`);
|
|
152
|
+
await cleanup();
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Run tests
|
|
157
|
+
logger.info('โ
Server ready, running tests...');
|
|
158
|
+
const testEnv = {
|
|
159
|
+
PORT: port.toString(),
|
|
160
|
+
HEADLESS: visible ? 'false' : 'true',
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const testProcess = spawnCommand(testCmd, testEnv, logger);
|
|
164
|
+
|
|
165
|
+
// Wait for tests to complete
|
|
166
|
+
exitCode = await new Promise((resolve) => {
|
|
167
|
+
testProcess.on('exit', (code) => resolve(code || 0));
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (exitCode === 0) {
|
|
171
|
+
logger.info('โ
Tests passed!');
|
|
172
|
+
} else {
|
|
173
|
+
logger.error(`โ Tests failed with exit code ${exitCode}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
} catch (err) {
|
|
177
|
+
logger.error('Test runner failed:', err.message);
|
|
178
|
+
if (err.stack) {
|
|
179
|
+
logger.debug(err.stack);
|
|
180
|
+
}
|
|
181
|
+
exitCode = 1;
|
|
182
|
+
|
|
183
|
+
} finally {
|
|
184
|
+
await cleanup();
|
|
185
|
+
process.exit(exitCode);
|
|
186
|
+
}
|
|
187
|
+
}
|
package/src/config.js
CHANGED
|
@@ -71,6 +71,13 @@ export async function loadBuildConfig(cliOptions = {}, { cwd = process.cwd() } =
|
|
|
71
71
|
...config,
|
|
72
72
|
rootDir: config.root,
|
|
73
73
|
verbose: cliOptions.verbose || false,
|
|
74
|
+
// CI test options from CLI
|
|
75
|
+
testServer: cliOptions.testServer,
|
|
76
|
+
testCommand: cliOptions.testCommand,
|
|
77
|
+
testStatus: cliOptions.testStatus,
|
|
78
|
+
testPort: cliOptions.testPort,
|
|
79
|
+
testTimeout: cliOptions.testTimeout,
|
|
80
|
+
testVisible: cliOptions.testVisible,
|
|
74
81
|
layout,
|
|
75
82
|
server: config.server ? {
|
|
76
83
|
rootDir: config.server.main_abs ? path.dirname(config.server.main_abs) : config.root,
|
package/src/runtime/runtime.js
CHANGED
|
@@ -97,12 +97,15 @@ async function setupProxyFirst(app, backendPort, config) {
|
|
|
97
97
|
return resolve(cached.canHandle);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
// Configurable timeout - default 500ms is enough for GC pauses and startup
|
|
101
|
+
const checkTimeout = parseInt(process.env.NOEGO_BACKEND_CHECK_TIMEOUT) || 500;
|
|
102
|
+
|
|
100
103
|
const options = {
|
|
101
104
|
hostname: 'localhost',
|
|
102
105
|
port: backendPort,
|
|
103
106
|
path: pathname,
|
|
104
107
|
method: 'GET', // Use GET instead of HEAD since some backends don't support HEAD
|
|
105
|
-
timeout:
|
|
108
|
+
timeout: checkTimeout,
|
|
106
109
|
headers: {
|
|
107
110
|
'X-Proxy-Check': 'true' // Indicate this is just a check
|
|
108
111
|
}
|