@elisoncampos/local-router 0.1.0 → 0.2.0
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/dist/cli.js +126 -22
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import chalk from "chalk";
|
|
|
5
5
|
import * as fs8 from "fs";
|
|
6
6
|
import * as path7 from "path";
|
|
7
7
|
import { spawn as spawn2, spawnSync } from "child_process";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
8
9
|
|
|
9
10
|
// src/certs.ts
|
|
10
11
|
import * as crypto from "crypto";
|
|
@@ -830,6 +831,9 @@ function spawnCommand(commandArgs, options) {
|
|
|
830
831
|
const cleanup = () => {
|
|
831
832
|
process.removeListener("SIGINT", onSigInt);
|
|
832
833
|
process.removeListener("SIGTERM", onSigTerm);
|
|
834
|
+
process.removeListener("SIGHUP", onSigHup);
|
|
835
|
+
process.removeListener("uncaughtException", onUncaughtException);
|
|
836
|
+
process.removeListener("unhandledRejection", onUnhandledRejection);
|
|
833
837
|
options?.onCleanup?.();
|
|
834
838
|
};
|
|
835
839
|
const handleSignal = (signal) => {
|
|
@@ -841,8 +845,28 @@ function spawnCommand(commandArgs, options) {
|
|
|
841
845
|
};
|
|
842
846
|
const onSigInt = () => handleSignal("SIGINT");
|
|
843
847
|
const onSigTerm = () => handleSignal("SIGTERM");
|
|
848
|
+
const onSigHup = () => handleSignal("SIGHUP");
|
|
849
|
+
const onUncaughtException = (error) => {
|
|
850
|
+
if (exiting) return;
|
|
851
|
+
exiting = true;
|
|
852
|
+
console.error(error.stack || error.message);
|
|
853
|
+
child.kill("SIGTERM");
|
|
854
|
+
cleanup();
|
|
855
|
+
process.exit(1);
|
|
856
|
+
};
|
|
857
|
+
const onUnhandledRejection = (reason) => {
|
|
858
|
+
if (exiting) return;
|
|
859
|
+
exiting = true;
|
|
860
|
+
console.error(reason instanceof Error ? reason.stack || reason.message : String(reason));
|
|
861
|
+
child.kill("SIGTERM");
|
|
862
|
+
cleanup();
|
|
863
|
+
process.exit(1);
|
|
864
|
+
};
|
|
844
865
|
process.on("SIGINT", onSigInt);
|
|
845
866
|
process.on("SIGTERM", onSigTerm);
|
|
867
|
+
process.on("SIGHUP", onSigHup);
|
|
868
|
+
process.on("uncaughtException", onUncaughtException);
|
|
869
|
+
process.on("unhandledRejection", onUnhandledRejection);
|
|
846
870
|
child.on("error", (error) => {
|
|
847
871
|
if (exiting) return;
|
|
848
872
|
exiting = true;
|
|
@@ -1309,6 +1333,27 @@ var DEBOUNCE_MS = 100;
|
|
|
1309
1333
|
var POLL_INTERVAL_MS = 3e3;
|
|
1310
1334
|
var EXIT_TIMEOUT_MS = 2e3;
|
|
1311
1335
|
var START_TIMEOUT_MS = 3e4;
|
|
1336
|
+
var AUTO_STOP_IDLE_MS = 3e3;
|
|
1337
|
+
var CLI_ENTRY_PATH = fileURLToPath(import.meta.url);
|
|
1338
|
+
function getSelfInvocation(args) {
|
|
1339
|
+
if (CLI_ENTRY_PATH.endsWith(".ts")) {
|
|
1340
|
+
const projectRoot = path7.dirname(path7.dirname(CLI_ENTRY_PATH));
|
|
1341
|
+
const tsxBin = path7.join(
|
|
1342
|
+
projectRoot,
|
|
1343
|
+
"node_modules",
|
|
1344
|
+
".bin",
|
|
1345
|
+
isWindows2 ? "tsx.cmd" : "tsx"
|
|
1346
|
+
);
|
|
1347
|
+
return {
|
|
1348
|
+
command: tsxBin,
|
|
1349
|
+
args: [CLI_ENTRY_PATH, ...args]
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
return {
|
|
1353
|
+
command: process.execPath,
|
|
1354
|
+
args: [CLI_ENTRY_PATH, ...args]
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1312
1357
|
function printHelp() {
|
|
1313
1358
|
console.log(`
|
|
1314
1359
|
${chalk.bold("local-router")} - Stable local domains with HTTP and HTTPS on the real hostnames you want.
|
|
@@ -1438,7 +1483,8 @@ function parseProxyArgs(args) {
|
|
|
1438
1483
|
httpsPort: getDefaultHttpsPort(),
|
|
1439
1484
|
httpEnabled: true,
|
|
1440
1485
|
httpsEnabled: true,
|
|
1441
|
-
foreground: false
|
|
1486
|
+
foreground: false,
|
|
1487
|
+
autoStopWhenIdle: false
|
|
1442
1488
|
};
|
|
1443
1489
|
for (let index = 0; index < args.length; index += 1) {
|
|
1444
1490
|
const token = args[index];
|
|
@@ -1446,6 +1492,10 @@ function parseProxyArgs(args) {
|
|
|
1446
1492
|
options.foreground = true;
|
|
1447
1493
|
continue;
|
|
1448
1494
|
}
|
|
1495
|
+
if (token === "--auto-stop-when-idle") {
|
|
1496
|
+
options.autoStopWhenIdle = true;
|
|
1497
|
+
continue;
|
|
1498
|
+
}
|
|
1449
1499
|
if (token === "--http-port") {
|
|
1450
1500
|
options.httpPort = parseNumberFlag("--http-port", args[index + 1]);
|
|
1451
1501
|
index += 1;
|
|
@@ -1567,6 +1617,15 @@ function writeEmptyRoutesIfNeeded(store) {
|
|
|
1567
1617
|
fixOwnership(store.routesPath);
|
|
1568
1618
|
}
|
|
1569
1619
|
}
|
|
1620
|
+
function syncHostsFromStore(store) {
|
|
1621
|
+
const activeRoutes = store.loadRoutes(true);
|
|
1622
|
+
const hostnames = activeRoutes.map((route) => route.hostname);
|
|
1623
|
+
if (hostnames.length === 0) {
|
|
1624
|
+
cleanHostsFile();
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
syncHostsFile(hostnames);
|
|
1628
|
+
}
|
|
1570
1629
|
function needsPrivileges(runtime) {
|
|
1571
1630
|
if (isWindows2) return false;
|
|
1572
1631
|
return runtime.httpEnabled && runtime.httpPort < PRIVILEGED_PORT_THRESHOLD || runtime.httpsEnabled && runtime.httpsPort < PRIVILEGED_PORT_THRESHOLD;
|
|
@@ -1574,14 +1633,37 @@ function needsPrivileges(runtime) {
|
|
|
1574
1633
|
function startProxyServer(store, runtime, stateDir) {
|
|
1575
1634
|
store.ensureDir();
|
|
1576
1635
|
writeEmptyRoutesIfNeeded(store);
|
|
1577
|
-
let cachedRoutes = store.loadRoutes();
|
|
1636
|
+
let cachedRoutes = store.loadRoutes(true);
|
|
1578
1637
|
let debounceTimer = null;
|
|
1579
1638
|
let watcher = null;
|
|
1580
1639
|
let poller = null;
|
|
1640
|
+
let idleTimer = null;
|
|
1641
|
+
let cleaningUp = false;
|
|
1642
|
+
const clearIdleTimer = () => {
|
|
1643
|
+
if (!idleTimer) return;
|
|
1644
|
+
clearTimeout(idleTimer);
|
|
1645
|
+
idleTimer = null;
|
|
1646
|
+
};
|
|
1647
|
+
const scheduleIdleShutdown = () => {
|
|
1648
|
+
if (!runtime.autoStopWhenIdle || idleTimer || cachedRoutes.length > 0) return;
|
|
1649
|
+
idleTimer = setTimeout(() => {
|
|
1650
|
+
idleTimer = null;
|
|
1651
|
+
cachedRoutes = store.loadRoutes(true);
|
|
1652
|
+
if (cachedRoutes.length === 0) {
|
|
1653
|
+
cleanup();
|
|
1654
|
+
}
|
|
1655
|
+
}, AUTO_STOP_IDLE_MS);
|
|
1656
|
+
idleTimer.unref();
|
|
1657
|
+
};
|
|
1581
1658
|
const reloadRoutes = () => {
|
|
1582
1659
|
try {
|
|
1583
|
-
cachedRoutes = store.loadRoutes();
|
|
1584
|
-
|
|
1660
|
+
cachedRoutes = store.loadRoutes(true);
|
|
1661
|
+
syncHostsFromStore(store);
|
|
1662
|
+
if (cachedRoutes.length > 0) {
|
|
1663
|
+
clearIdleTimer();
|
|
1664
|
+
} else {
|
|
1665
|
+
scheduleIdleShutdown();
|
|
1666
|
+
}
|
|
1585
1667
|
} catch {
|
|
1586
1668
|
}
|
|
1587
1669
|
};
|
|
@@ -1591,9 +1673,11 @@ function startProxyServer(store, runtime, stateDir) {
|
|
|
1591
1673
|
debounceTimer = setTimeout(reloadRoutes, DEBOUNCE_MS);
|
|
1592
1674
|
});
|
|
1593
1675
|
} catch {
|
|
1594
|
-
poller = setInterval(reloadRoutes, POLL_INTERVAL_MS);
|
|
1595
1676
|
}
|
|
1596
|
-
|
|
1677
|
+
poller = setInterval(reloadRoutes, POLL_INTERVAL_MS);
|
|
1678
|
+
poller.unref();
|
|
1679
|
+
syncHostsFromStore(store);
|
|
1680
|
+
scheduleIdleShutdown();
|
|
1597
1681
|
let tlsOptions;
|
|
1598
1682
|
if (runtime.httpsEnabled) {
|
|
1599
1683
|
const certs = ensureCerts(stateDir);
|
|
@@ -1626,7 +1710,14 @@ function startProxyServer(store, runtime, stateDir) {
|
|
|
1626
1710
|
pendingListeners -= 1;
|
|
1627
1711
|
if (pendingListeners > 0) return;
|
|
1628
1712
|
fs8.writeFileSync(store.pidPath, String(process.pid), { mode: FILE_MODE });
|
|
1629
|
-
writeProxyState(stateDir, {
|
|
1713
|
+
writeProxyState(stateDir, {
|
|
1714
|
+
pid: process.pid,
|
|
1715
|
+
httpPort: runtime.httpPort,
|
|
1716
|
+
httpsPort: runtime.httpsPort,
|
|
1717
|
+
httpEnabled: runtime.httpEnabled,
|
|
1718
|
+
httpsEnabled: runtime.httpsEnabled,
|
|
1719
|
+
autoStopWhenIdle: runtime.autoStopWhenIdle
|
|
1720
|
+
});
|
|
1630
1721
|
fixOwnership(store.pidPath, store.statePath);
|
|
1631
1722
|
console.log(chalk.green("local-router proxy is listening."));
|
|
1632
1723
|
if (runtime.httpEnabled) {
|
|
@@ -1657,8 +1748,11 @@ function startProxyServer(store, runtime, stateDir) {
|
|
|
1657
1748
|
servers.httpsServer.listen(runtime.httpsPort, onListening);
|
|
1658
1749
|
}
|
|
1659
1750
|
const cleanup = () => {
|
|
1751
|
+
if (cleaningUp) return;
|
|
1752
|
+
cleaningUp = true;
|
|
1660
1753
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
1661
1754
|
if (poller) clearInterval(poller);
|
|
1755
|
+
clearIdleTimer();
|
|
1662
1756
|
watcher?.close();
|
|
1663
1757
|
try {
|
|
1664
1758
|
fs8.unlinkSync(store.pidPath);
|
|
@@ -1683,7 +1777,8 @@ async function ensureProxy(runtime) {
|
|
|
1683
1777
|
const { dir, state } = await discoverState();
|
|
1684
1778
|
const effectiveState = state ?? {
|
|
1685
1779
|
pid: 0,
|
|
1686
|
-
...runtime
|
|
1780
|
+
...runtime,
|
|
1781
|
+
autoStopWhenIdle: true
|
|
1687
1782
|
};
|
|
1688
1783
|
const stateDir = resolveStateDir(runtime);
|
|
1689
1784
|
const store = new RouteStore(stateDir, {
|
|
@@ -1707,12 +1802,13 @@ async function ensureProxy(runtime) {
|
|
|
1707
1802
|
if (answer === "s" || answer === "skip") {
|
|
1708
1803
|
throw new Error("Skipping the proxy is not supported for this command.");
|
|
1709
1804
|
}
|
|
1710
|
-
const
|
|
1805
|
+
const invocation = getSelfInvocation(["proxy", "start"]);
|
|
1806
|
+
const childArgs = [...invocation.args, "--auto-stop-when-idle"];
|
|
1711
1807
|
if (!runtime.httpEnabled) childArgs.push("--no-http");
|
|
1712
1808
|
if (!runtime.httpsEnabled) childArgs.push("--no-https");
|
|
1713
1809
|
if (runtime.httpPort !== getDefaultHttpPort()) childArgs.push("--http-port", String(runtime.httpPort));
|
|
1714
1810
|
if (runtime.httpsPort !== getDefaultHttpsPort()) childArgs.push("--https-port", String(runtime.httpsPort));
|
|
1715
|
-
const result = spawnSync("sudo", childArgs, {
|
|
1811
|
+
const result = spawnSync("sudo", [invocation.command, ...childArgs], {
|
|
1716
1812
|
stdio: "inherit",
|
|
1717
1813
|
timeout: START_TIMEOUT_MS
|
|
1718
1814
|
});
|
|
@@ -1721,12 +1817,13 @@ async function ensureProxy(runtime) {
|
|
|
1721
1817
|
process.exit(1);
|
|
1722
1818
|
}
|
|
1723
1819
|
} else {
|
|
1724
|
-
const
|
|
1820
|
+
const invocation = getSelfInvocation(["proxy", "start"]);
|
|
1821
|
+
const childArgs = [...invocation.args, "--auto-stop-when-idle"];
|
|
1725
1822
|
if (!runtime.httpEnabled) childArgs.push("--no-http");
|
|
1726
1823
|
if (!runtime.httpsEnabled) childArgs.push("--no-https");
|
|
1727
1824
|
if (runtime.httpPort !== getDefaultHttpPort()) childArgs.push("--http-port", String(runtime.httpPort));
|
|
1728
1825
|
if (runtime.httpsPort !== getDefaultHttpsPort()) childArgs.push("--https-port", String(runtime.httpsPort));
|
|
1729
|
-
const result = spawnSync(
|
|
1826
|
+
const result = spawnSync(invocation.command, childArgs, {
|
|
1730
1827
|
stdio: "inherit",
|
|
1731
1828
|
timeout: START_TIMEOUT_MS
|
|
1732
1829
|
});
|
|
@@ -1772,6 +1869,18 @@ async function handleRun(args) {
|
|
|
1772
1869
|
isSystemDir: dir === SYSTEM_STATE_DIR,
|
|
1773
1870
|
onWarning: (message) => console.warn(chalk.yellow(message))
|
|
1774
1871
|
});
|
|
1872
|
+
const cleanupRoutes = () => {
|
|
1873
|
+
for (const hostname of resolved.hostnames) {
|
|
1874
|
+
try {
|
|
1875
|
+
store.removeRoute(hostname);
|
|
1876
|
+
} catch {
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
try {
|
|
1880
|
+
syncHostsFromStore(store);
|
|
1881
|
+
} catch {
|
|
1882
|
+
}
|
|
1883
|
+
};
|
|
1775
1884
|
const port = parsed.appPort ?? await findFreePort();
|
|
1776
1885
|
console.log(chalk.green(`App port: ${port}`));
|
|
1777
1886
|
try {
|
|
@@ -1798,14 +1907,7 @@ async function handleRun(args) {
|
|
|
1798
1907
|
LOCAL_ROUTER_URLS_HTTP: allHttpUrls,
|
|
1799
1908
|
__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS: ".localhost"
|
|
1800
1909
|
},
|
|
1801
|
-
onCleanup:
|
|
1802
|
-
for (const hostname of resolved.hostnames) {
|
|
1803
|
-
try {
|
|
1804
|
-
store.removeRoute(hostname);
|
|
1805
|
-
} catch {
|
|
1806
|
-
}
|
|
1807
|
-
}
|
|
1808
|
-
}
|
|
1910
|
+
onCleanup: cleanupRoutes
|
|
1809
1911
|
});
|
|
1810
1912
|
}
|
|
1811
1913
|
async function handleHosts(args) {
|
|
@@ -1887,12 +1989,14 @@ ${chalk.bold("Usage:")}
|
|
|
1887
1989
|
} catch {
|
|
1888
1990
|
}
|
|
1889
1991
|
fixOwnership(logPath, stateDir);
|
|
1890
|
-
const
|
|
1992
|
+
const invocation = getSelfInvocation(["proxy", "start", "--foreground"]);
|
|
1993
|
+
const daemonArgs = [...invocation.args];
|
|
1994
|
+
if (options.autoStopWhenIdle) daemonArgs.push("--auto-stop-when-idle");
|
|
1891
1995
|
if (!options.httpEnabled) daemonArgs.push("--no-http");
|
|
1892
1996
|
if (!options.httpsEnabled) daemonArgs.push("--no-https");
|
|
1893
1997
|
if (options.httpPort !== getDefaultHttpPort()) daemonArgs.push("--http-port", String(options.httpPort));
|
|
1894
1998
|
if (options.httpsPort !== getDefaultHttpsPort()) daemonArgs.push("--https-port", String(options.httpsPort));
|
|
1895
|
-
const child = spawn2(
|
|
1999
|
+
const child = spawn2(invocation.command, daemonArgs, {
|
|
1896
2000
|
detached: true,
|
|
1897
2001
|
stdio: ["ignore", logFd, logFd],
|
|
1898
2002
|
env: process.env,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elisoncampos/local-router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Named local domains with automatic HTTP/HTTPS routing for development.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"json5": "^2.2.3"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
+
"@changesets/changelog-github": "^0.5.1",
|
|
43
44
|
"@changesets/cli": "^2.29.5",
|
|
44
45
|
"@types/node": "^24.0.0",
|
|
45
46
|
"tsup": "^8.3.5",
|