147 lines
4.8 KiB
JavaScript
147 lines
4.8 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
// Wrapper around `ng serve` that resolves the best binding:
|
|
// 1. https://stella-ops.local (port 443) — if hostname resolves and port is free
|
|
// 2. https://localhost:10000 — always available fallback
|
|
//
|
|
// Additionally binds http://stella-ops.local (port 80) as a redirect to HTTPS
|
|
// when the hostname resolves and port 80 is available.
|
|
|
|
const { spawn } = require('child_process');
|
|
const http = require('http');
|
|
const dns = require('dns');
|
|
const net = require('net');
|
|
const path = require('path');
|
|
|
|
const HOSTNAME = 'stella-ops.local';
|
|
const HTTPS_PORT = 443;
|
|
const HTTP_PORT = 80;
|
|
const DEV_PORT = 10000;
|
|
const SETUP_DOC = 'docs/technical/architecture/port-registry.md';
|
|
|
|
function isWindows() {
|
|
return process.platform === 'win32';
|
|
}
|
|
|
|
function hostsFilePath() {
|
|
return isWindows()
|
|
? 'C:\\Windows\\System32\\drivers\\etc\\hosts'
|
|
: '/etc/hosts';
|
|
}
|
|
|
|
function resolveHostnameIp(hostname) {
|
|
return new Promise((resolve) => {
|
|
dns.lookup(hostname, { family: 4 }, (err, address) => {
|
|
if (err) return resolve(null);
|
|
resolve(address);
|
|
});
|
|
});
|
|
}
|
|
|
|
function isPortAvailable(port, ip) {
|
|
return new Promise((resolve) => {
|
|
const server = net.createServer();
|
|
server.once('error', () => resolve(false));
|
|
server.once('listening', () => {
|
|
server.close(() => resolve(true));
|
|
});
|
|
server.listen(port, ip);
|
|
});
|
|
}
|
|
|
|
function startHttpRedirect(httpsHost, httpsPort, bindIp) {
|
|
return new Promise((resolve) => {
|
|
const server = http.createServer((req, res) => {
|
|
const location = `https://${httpsHost}${httpsPort === 443 ? '' : ':' + httpsPort}${req.url}`;
|
|
res.writeHead(301, { Location: location });
|
|
res.end();
|
|
});
|
|
|
|
server.on('error', (err) => {
|
|
console.warn(` HTTP redirect on port ${HTTP_PORT} failed: ${err.message}`);
|
|
resolve(false);
|
|
});
|
|
|
|
server.listen(HTTP_PORT, bindIp, () => {
|
|
console.log(` HTTP redirect active: http://${HOSTNAME} -> https://${httpsHost}${httpsPort === 443 ? '' : ':' + httpsPort}`);
|
|
console.log(` Bound to ${bindIp}:${HTTP_PORT}`);
|
|
resolve(true);
|
|
});
|
|
});
|
|
}
|
|
|
|
async function main() {
|
|
const extraArgs = process.argv.slice(2);
|
|
const resolvedIp = await resolveHostnameIp(HOSTNAME);
|
|
const hostnameOk = !!resolvedIp;
|
|
const port443Free = hostnameOk ? await isPortAvailable(HTTPS_PORT, resolvedIp) : false;
|
|
const port80Free = hostnameOk ? await isPortAvailable(HTTP_PORT, resolvedIp) : false;
|
|
|
|
let host, port;
|
|
|
|
if (hostnameOk && port443Free) {
|
|
host = HOSTNAME;
|
|
port = HTTPS_PORT;
|
|
console.log('');
|
|
console.log(` ${HOSTNAME} resolves to ${resolvedIp}; port ${HTTPS_PORT} is available.`);
|
|
console.log(` Dev server binding to https://${HOSTNAME}`);
|
|
console.log(` Also accessible at https://localhost:${DEV_PORT}`);
|
|
|
|
if (port80Free) {
|
|
const redirectOk = await startHttpRedirect(HOSTNAME, HTTPS_PORT, resolvedIp);
|
|
if (redirectOk) {
|
|
console.log(` Also accessible at http://${HOSTNAME} (redirects to HTTPS)`);
|
|
} else {
|
|
console.warn(` Failed to start HTTP redirect on ${resolvedIp}:${HTTP_PORT}`);
|
|
}
|
|
} else {
|
|
console.warn(` Port ${HTTP_PORT} on ${resolvedIp} is unavailable; skipping http://${HOSTNAME} redirect.`);
|
|
}
|
|
|
|
console.log('');
|
|
} else if (hostnameOk) {
|
|
host = HOSTNAME;
|
|
port = DEV_PORT;
|
|
console.warn('');
|
|
console.warn(` ${HOSTNAME} resolves to ${resolvedIp} but port ${HTTPS_PORT} is unavailable`);
|
|
console.warn(` (requires elevated privileges or is already in use).`);
|
|
console.warn(` Dev server binding to https://${HOSTNAME}:${DEV_PORT}`);
|
|
console.warn('');
|
|
} else {
|
|
host = 'localhost';
|
|
port = DEV_PORT;
|
|
console.warn('');
|
|
console.warn(` WARNING: ${HOSTNAME} does not resolve.`);
|
|
console.warn(` Dev server binding to https://localhost:${DEV_PORT}`);
|
|
console.warn('');
|
|
console.warn(` To use https://${HOSTNAME}, add to ${hostsFilePath()}:`);
|
|
console.warn('');
|
|
console.warn(` 127.1.0.1 ${HOSTNAME} # or your preferred IP`);
|
|
console.warn('');
|
|
console.warn(` See ${SETUP_DOC} for the full list of hostnames.`);
|
|
console.warn('');
|
|
}
|
|
|
|
runNgServe(host, port, extraArgs);
|
|
}
|
|
|
|
function runNgServe(host, port, extraArgs) {
|
|
const cwd = path.resolve(__dirname, '..');
|
|
const ngCli = path.resolve(cwd, 'node_modules', '@angular', 'cli', 'bin', 'ng.js');
|
|
// Pass the hostname (not IP) so Vite resolves it for both HTTPS and HMR websocket.
|
|
// The hostname resolves to a unique loopback IP via the hosts file, so ports
|
|
// won't collide with other services.
|
|
const args = ['serve', '--host', host, '--port', String(port), '--ssl', ...extraArgs];
|
|
|
|
console.log(` ng serve binding to ${host}:${port}`);
|
|
|
|
const child = spawn(process.execPath, [ngCli, ...args], {
|
|
stdio: 'inherit',
|
|
cwd,
|
|
});
|
|
|
|
child.on('exit', (code) => process.exit(code ?? 1));
|
|
}
|
|
|
|
main();
|