@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.
Files changed (2) hide show
  1. package/dist/cli.js +126 -22
  2. 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
- syncHostsFile(cachedRoutes.map((route) => route.hostname));
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
- syncHostsFile(cachedRoutes.map((route) => route.hostname));
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, { pid: process.pid, ...runtime });
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 childArgs = [process.execPath, process.argv[1], "proxy", "start"];
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 childArgs = [process.argv[1], "proxy", "start"];
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(process.execPath, childArgs, {
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 daemonArgs = [process.argv[1], "proxy", "start", "--foreground"];
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(process.execPath, daemonArgs, {
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.1.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",