151 أسطر
3.6 KiB
JavaScript
151 أسطر
3.6 KiB
JavaScript
const { existsSync } = require('fs');
|
|
const { spawn } = require('child_process');
|
|
const { performance } = require('perf_hooks');
|
|
|
|
function parseArgs(argv) {
|
|
const options = {
|
|
entry: 'dist/main.js',
|
|
port: 4100,
|
|
timeout: 30000,
|
|
path: '/api/v1/health',
|
|
};
|
|
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const arg = argv[index];
|
|
const next = argv[index + 1];
|
|
|
|
if (arg === '--entry' && next) {
|
|
options.entry = next;
|
|
index += 1;
|
|
continue;
|
|
}
|
|
|
|
if (arg === '--port' && next) {
|
|
options.port = Number(next);
|
|
index += 1;
|
|
continue;
|
|
}
|
|
|
|
if (arg === '--timeout' && next) {
|
|
options.timeout = Number(next);
|
|
index += 1;
|
|
continue;
|
|
}
|
|
|
|
if (arg === '--path' && next) {
|
|
options.path = next.startsWith('/') ? next : `/${next}`;
|
|
index += 1;
|
|
}
|
|
}
|
|
|
|
if (!existsSync(options.entry)) {
|
|
throw new Error(`Entry file not found: ${options.entry}. Run "npm run build" first.`);
|
|
}
|
|
|
|
if (!Number.isInteger(options.port) || options.port <= 0) {
|
|
throw new Error('port must be a positive integer');
|
|
}
|
|
|
|
if (!Number.isFinite(options.timeout) || options.timeout <= 0) {
|
|
throw new Error('timeout must be a positive number of milliseconds');
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
async function waitForHealth(url, timeoutMs) {
|
|
const deadline = Date.now() + timeoutMs;
|
|
let lastError = null;
|
|
|
|
while (Date.now() < deadline) {
|
|
try {
|
|
const response = await fetch(url, { method: 'GET' });
|
|
if (response.ok) {
|
|
const body = await response.text();
|
|
return { status: response.status, body };
|
|
}
|
|
} catch (error) {
|
|
lastError = error;
|
|
}
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
}
|
|
|
|
if (lastError instanceof Error) {
|
|
throw new Error(`Startup timeout. Last error: ${lastError.message}`);
|
|
}
|
|
|
|
throw new Error('Startup timeout. Health endpoint did not become ready.');
|
|
}
|
|
|
|
async function terminate(child) {
|
|
if (child.exitCode !== null) {
|
|
return;
|
|
}
|
|
|
|
child.kill();
|
|
await new Promise((resolve) => {
|
|
child.once('exit', resolve);
|
|
setTimeout(resolve, 5000);
|
|
});
|
|
}
|
|
|
|
async function main() {
|
|
const options = parseArgs(process.argv.slice(2));
|
|
const url = `http://127.0.0.1:${options.port}${options.path}`;
|
|
const stdoutLines = [];
|
|
const stderrLines = [];
|
|
|
|
const startedAt = performance.now();
|
|
const child = spawn(process.execPath, [options.entry], {
|
|
cwd: process.cwd(),
|
|
env: {
|
|
...process.env,
|
|
PORT: String(options.port),
|
|
PUBLIC_BASE_URL: `http://127.0.0.1:${options.port}`,
|
|
},
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
|
|
child.stdout.on('data', (chunk) => {
|
|
stdoutLines.push(...String(chunk).split(/\r?\n/).filter(Boolean));
|
|
if (stdoutLines.length > 20) {
|
|
stdoutLines.splice(0, stdoutLines.length - 20);
|
|
}
|
|
});
|
|
|
|
child.stderr.on('data', (chunk) => {
|
|
stderrLines.push(...String(chunk).split(/\r?\n/).filter(Boolean));
|
|
if (stderrLines.length > 20) {
|
|
stderrLines.splice(0, stderrLines.length - 20);
|
|
}
|
|
});
|
|
|
|
try {
|
|
const healthResult = await waitForHealth(url, options.timeout);
|
|
const readyMs = performance.now() - startedAt;
|
|
|
|
console.log(
|
|
JSON.stringify(
|
|
{
|
|
entry: options.entry,
|
|
url,
|
|
startupMs: Number(readyMs.toFixed(2)),
|
|
healthStatus: healthResult.status,
|
|
recentStdout: stdoutLines,
|
|
recentStderr: stderrLines,
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
} finally {
|
|
await terminate(child);
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error instanceof Error ? error.message : String(error));
|
|
process.exitCode = 1;
|
|
});
|