portless 0.9.6 → 0.10.1

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/README.md CHANGED
@@ -32,7 +32,7 @@ portless myapp next dev
32
32
 
33
33
  HTTPS with HTTP/2 is enabled by default. On first run, portless generates a local CA, trusts it, and binds port 443 (auto-elevates with sudo on macOS/Linux). Use `--no-tls` for plain HTTP.
34
34
 
35
- The proxy auto-starts when you run an app. A random port (4000--4999) is assigned via the `PORT` environment variable. Most frameworks (Next.js, Express, Nuxt, etc.) respect this automatically. For frameworks that ignore `PORT` (Vite, Astro, React Router, Angular, Expo, React Native), portless auto-injects `--port` and `--host` flags.
35
+ The proxy auto-starts when you run an app. A random port (4000--4999) is assigned via the `PORT` environment variable. Most frameworks (Next.js, Express, Nuxt, etc.) respect this automatically. For frameworks that ignore `PORT` (Vite, VitePlus, Astro, React Router, Angular, Expo, React Native), portless auto-injects the right `--port` flag and, when needed, a matching `--host` flag.
36
36
 
37
37
  ## Use in package.json
38
38
 
@@ -88,7 +88,7 @@ portless myapp next dev
88
88
  # -> https://myapp.test
89
89
  ```
90
90
 
91
- The proxy auto-syncs `/etc/hosts` for custom TLDs, so `.test` domains resolve correctly.
91
+ The proxy auto-syncs `/etc/hosts` for route hostnames (including `.test`), so those domains resolve on your machine.
92
92
 
93
93
  Recommended: `.test` (IANA-reserved, no collision risk). Avoid `.local` (conflicts with mDNS/Bonjour) and `.dev` (Google-owned, forces HTTPS via HSTS).
94
94
 
@@ -129,6 +129,33 @@ portless trust
129
129
 
130
130
  On Linux, `portless trust` supports Debian/Ubuntu, Arch, Fedora/RHEL/CentOS, and openSUSE (via `update-ca-certificates` or `update-ca-trust`). On Windows, it uses `certutil` to add the CA to the system trust store.
131
131
 
132
+ ## LAN mode
133
+
134
+ ```bash
135
+ portless proxy start --lan
136
+ portless proxy start --lan --https
137
+ portless proxy start --lan --ip 192.168.1.42
138
+ ```
139
+
140
+ `--lan` switches the proxy to mDNS discovery: services are advertised as `<name>.local` and reachable from any device on the same network. Portless auto-detects your LAN IP and follows Wi-Fi/IP changes automatically, but you can pin another address with `--ip <address>` or by exporting `PORTLESS_LAN_IP`. Set `PORTLESS_LAN=1` in your shell (0/1 boolean) to make LAN mode the default whenever the proxy starts.
141
+
142
+ Portless remembers LAN mode via `proxy.lan`, so if you stop a LAN proxy and start it again, it stays in LAN mode. Other proxy settings still follow the current flags and env vars. Use `PORTLESS_LAN=0` for one start to switch back to `.localhost` mode. If a proxy is already running with different explicit LAN/TLS/TLD settings, portless warns and asks you to stop it first.
143
+
144
+ LAN mode depends on the system mDNS tools that portless already spawns: macOS ships with `dns-sd`, while Linux uses `avahi-publish-address` from `avahi-utils` (install via `sudo apt install avahi-utils` or your distro’s equivalent). If the command is missing or your network isn’t reachable, `portless proxy start --lan` prints the relevant error and exits.
145
+
146
+ ### Framework notes
147
+
148
+ - **Next.js**: add your `.local` hostnames to `allowedDevOrigins`:
149
+
150
+ ```js
151
+ // next.config.js
152
+ module.exports = {
153
+ allowedDevOrigins: ["myapp.local", "*.myapp.local"],
154
+ };
155
+ ```
156
+
157
+ - **Expo / React Native**: portless always injects `--port`. React Native also gets `--host 127.0.0.1`. Expo gets `--host localhost` outside LAN mode, but in LAN mode portless leaves Metro on its default LAN host behavior instead of forcing `--host` or `HOST`.
158
+
132
159
  ## Commands
133
160
 
134
161
  ```bash
@@ -139,6 +166,7 @@ portless alias <name> <port> --force # Overwrite an existing route
139
166
  portless alias --remove <name> # Remove a static route
140
167
  portless list # Show active routes
141
168
  portless trust # Add local CA to system trust store
169
+ portless clean # Remove state, CA trust entry, and hosts block
142
170
  portless hosts sync # Add routes to /etc/hosts (fixes Safari)
143
171
  portless hosts clean # Remove portless entries from /etc/hosts
144
172
 
@@ -148,6 +176,7 @@ PORTLESS=0 pnpm dev # Bypasses proxy, uses default port
148
176
  # Proxy control
149
177
  portless proxy start # Start the HTTPS proxy (port 443, daemon)
150
178
  portless proxy start --no-tls # Start without HTTPS (port 80)
179
+ portless proxy start --lan # Start in LAN mode (mDNS .local for devices)
151
180
  portless proxy start -p 1355 # Start on a custom port (no sudo)
152
181
  portless proxy start --foreground # Start in foreground (for debugging)
153
182
  portless proxy start --wildcard # Allow unregistered subdomains to fall back to parent
@@ -160,6 +189,8 @@ portless proxy stop # Stop the proxy
160
189
  -p, --port <number> Port for the proxy (default: 443, or 80 with --no-tls)
161
190
  --no-tls Disable HTTPS (use plain HTTP on port 80)
162
191
  --https Enable HTTPS (default, accepted for compatibility)
192
+ --lan Enable LAN mode (mDNS .local for real devices)
193
+ --ip <address> Pin a specific LAN IP (disables auto-follow; use with --lan)
163
194
  --cert <path> Use a custom TLS certificate
164
195
  --key <path> Use a custom TLS private key
165
196
  --foreground Run proxy in foreground instead of daemon
@@ -176,19 +207,30 @@ portless proxy stop # Stop the proxy
176
207
  # Configuration
177
208
  PORTLESS_PORT=<number> Override the default proxy port
178
209
  PORTLESS_APP_PORT=<number> Use a fixed port for the app (same as --app-port)
179
- PORTLESS_HTTPS HTTPS on by default; set to 0 to disable (same as --no-tls)
210
+ PORTLESS_HTTPS=0 Disable HTTPS (same as --no-tls)
211
+ PORTLESS_LAN=1 Enable LAN mode when set to 1 (auto-detects LAN IP)
180
212
  PORTLESS_TLD=<tld> Use a custom TLD (e.g. test; default: localhost)
181
213
  PORTLESS_WILDCARD=1 Allow unregistered subdomains to fall back to parent route
182
- PORTLESS_SYNC_HOSTS=1 Auto-sync /etc/hosts (auto-enabled for custom TLDs)
214
+ PORTLESS_SYNC_HOSTS=0 Disable auto-sync of /etc/hosts (on by default)
183
215
  PORTLESS_STATE_DIR=<path> Override the state directory
184
216
 
185
217
  # Injected into child processes
186
218
  PORT Ephemeral port the child should listen on
187
- HOST Always 127.0.0.1
219
+ HOST Usually 127.0.0.1 (omitted for Expo in LAN mode)
188
220
  PORTLESS_URL Public URL (e.g. https://myapp.localhost)
189
221
  ```
190
222
 
191
- > **Reserved names:** `run`, `get`, `alias`, `hosts`, `list`, `trust`, and `proxy` are subcommands and cannot be used as app names directly. Use `portless run <cmd>` to infer the name from your project, or `portless --name <name> <cmd>` to force any name including reserved ones.
223
+ > **Reserved names:** `run`, `get`, `alias`, `hosts`, `list`, `trust`, `clean`, and `proxy` are subcommands and cannot be used as app names directly. Use `portless run <cmd>` to infer the name from your project, or `portless --name <name> <cmd>` to force any name including reserved ones.
224
+
225
+ ## Uninstall / reset
226
+
227
+ To remove portless data from your machine (proxy state under `~/.portless` and the system state directory, the local CA from the OS trust store when portless installed it, and the portless block in `/etc/hosts`):
228
+
229
+ ```bash
230
+ portless clean
231
+ ```
232
+
233
+ macOS/Linux may prompt for `sudo`. Custom certificate paths passed with `--cert` and `--key` are not deleted.
192
234
 
193
235
  ## Safari / DNS
194
236
 
@@ -201,7 +243,7 @@ portless hosts sync # Add current routes to /etc/hosts
201
243
  portless hosts clean # Clean up later
202
244
  ```
203
245
 
204
- Auto-syncs `/etc/hosts` for custom TLDs (e.g. `--tld test`). For `.localhost`, set `PORTLESS_SYNC_HOSTS=1` to enable. Disable with `PORTLESS_SYNC_HOSTS=0`.
246
+ Auto-syncs `/etc/hosts` for route hostnames by default (`.localhost`, custom TLDs, LAN `.local`). Set `PORTLESS_SYNC_HOSTS=0` to disable.
205
247
 
206
248
  ## Proxying Between Portless Apps
207
249
 
@@ -614,6 +614,9 @@ function buildBlock(hostnames) {
614
614
  ${entries}
615
615
  ${MARKER_END}`;
616
616
  }
617
+ function shouldAutoSyncHosts(syncVal) {
618
+ return syncVal !== "0" && syncVal !== "false";
619
+ }
617
620
  function syncHostsFile(hostnames) {
618
621
  try {
619
622
  const content = readHostsFile();
@@ -670,6 +673,8 @@ import { execSync, spawn } from "child_process";
670
673
  var isWindows2 = process.platform === "win32";
671
674
  var FALLBACK_PROXY_PORT = 1355;
672
675
  var PRIVILEGED_PORT_THRESHOLD = 1024;
676
+ var INTERNAL_LAN_IP_ENV = "PORTLESS_INTERNAL_LAN_IP";
677
+ var INTERNAL_LAN_IP_FLAG = "--lan-ip-auto";
673
678
  var SYSTEM_STATE_DIR = isWindows2 ? path2.join(os.tmpdir(), "portless") : "/tmp/portless";
674
679
  var USER_STATE_DIR = path2.join(os.homedir(), ".portless");
675
680
  var MIN_APP_PORT = 4e3;
@@ -816,6 +821,26 @@ function writeTlsMarker(dir, enabled) {
816
821
  }
817
822
  }
818
823
  }
824
+ var LAN_MARKER_FILE = "proxy.lan";
825
+ function readLanMarker(dir) {
826
+ try {
827
+ const raw = fs3.readFileSync(path2.join(dir, LAN_MARKER_FILE), "utf-8").trim();
828
+ return raw || null;
829
+ } catch {
830
+ return null;
831
+ }
832
+ }
833
+ function writeLanMarker(dir, ip) {
834
+ const markerPath = path2.join(dir, LAN_MARKER_FILE);
835
+ if (!ip) {
836
+ try {
837
+ fs3.unlinkSync(markerPath);
838
+ } catch {
839
+ }
840
+ } else {
841
+ fs3.writeFileSync(markerPath, ip, { mode: 420 });
842
+ }
843
+ }
819
844
  var DEFAULT_TLD = "localhost";
820
845
  var RISKY_TLDS = /* @__PURE__ */ new Map([
821
846
  ["local", "conflicts with mDNS/Bonjour on macOS"],
@@ -872,20 +897,78 @@ function isWildcardEnvEnabled() {
872
897
  const val = process.env.PORTLESS_WILDCARD;
873
898
  return val === "1" || val === "true";
874
899
  }
900
+ function isLanEnvEnabled() {
901
+ const val = process.env.PORTLESS_LAN;
902
+ return val === "1" || val === "true";
903
+ }
904
+ function buildProxyStartConfig(options) {
905
+ const effectiveTld = options.lanMode ? "local" : options.tld;
906
+ const args = [];
907
+ if (options.foreground) {
908
+ args.push("--foreground");
909
+ }
910
+ if (options.includePort && options.proxyPort !== void 0) {
911
+ args.push("--port", options.proxyPort.toString());
912
+ }
913
+ if (options.useHttps) {
914
+ if (options.customCertPath && options.customKeyPath) {
915
+ args.push("--cert", options.customCertPath, "--key", options.customKeyPath);
916
+ } else {
917
+ args.push("--https");
918
+ }
919
+ } else {
920
+ args.push("--no-tls");
921
+ }
922
+ if (options.lanMode) {
923
+ args.push("--lan");
924
+ if (options.lanIp) {
925
+ if (options.lanIpExplicit) {
926
+ args.push("--ip", options.lanIp);
927
+ } else {
928
+ args.push(INTERNAL_LAN_IP_FLAG, options.lanIp);
929
+ }
930
+ }
931
+ } else if (effectiveTld !== DEFAULT_TLD) {
932
+ args.push("--tld", effectiveTld);
933
+ }
934
+ if (options.useWildcard) {
935
+ args.push("--wildcard");
936
+ }
937
+ return { effectiveTld, args };
938
+ }
875
939
  async function discoverState() {
876
940
  if (process.env.PORTLESS_STATE_DIR) {
877
- const dir = process.env.PORTLESS_STATE_DIR;
878
- const port = readPortFromDir(dir) ?? getDefaultPort();
879
- const tls = readTlsMarker(dir);
880
- const tld = readTldFromDir(dir);
881
- return { dir, port, tls, tld };
941
+ const dir2 = process.env.PORTLESS_STATE_DIR;
942
+ const port = readPortFromDir(dir2) ?? getDefaultPort();
943
+ const lanIp = readLanMarker(dir2);
944
+ if (await isProxyRunning(port) || await isPortListening(port)) {
945
+ const tls = readTlsMarker(dir2);
946
+ const tld = readTldFromDir(dir2);
947
+ return { dir: dir2, port, tls, tld, lanMode: lanIp !== null || tld === "local", lanIp };
948
+ }
949
+ return {
950
+ dir: dir2,
951
+ port,
952
+ tls: readTlsMarker(dir2),
953
+ tld: readTldFromDir(dir2),
954
+ lanMode: lanIp !== null,
955
+ lanIp: null
956
+ };
882
957
  }
883
958
  const userPort = readPortFromDir(USER_STATE_DIR);
884
959
  if (userPort !== null) {
885
960
  if (await isProxyRunning(userPort)) {
886
961
  const tls = readTlsMarker(USER_STATE_DIR);
887
962
  const tld = readTldFromDir(USER_STATE_DIR);
888
- return { dir: USER_STATE_DIR, port: userPort, tls, tld };
963
+ const lanIp = readLanMarker(USER_STATE_DIR);
964
+ return {
965
+ dir: USER_STATE_DIR,
966
+ port: userPort,
967
+ tls,
968
+ tld,
969
+ lanMode: lanIp !== null || tld === "local",
970
+ lanIp
971
+ };
889
972
  }
890
973
  }
891
974
  const systemPort = readPortFromDir(SYSTEM_STATE_DIR);
@@ -893,24 +976,36 @@ async function discoverState() {
893
976
  if (await isProxyRunning(systemPort)) {
894
977
  const tls = readTlsMarker(SYSTEM_STATE_DIR);
895
978
  const tld = readTldFromDir(SYSTEM_STATE_DIR);
896
- return { dir: SYSTEM_STATE_DIR, port: systemPort, tls, tld };
979
+ const lanIp = readLanMarker(SYSTEM_STATE_DIR);
980
+ return {
981
+ dir: SYSTEM_STATE_DIR,
982
+ port: systemPort,
983
+ tls,
984
+ tld,
985
+ lanMode: lanIp !== null || tld === "local",
986
+ lanIp
987
+ };
897
988
  }
898
989
  }
899
990
  const configuredPort = getDefaultPort();
900
991
  const probePorts = /* @__PURE__ */ new Set([443, 80, FALLBACK_PROXY_PORT, configuredPort]);
901
992
  for (const port of probePorts) {
902
993
  if (await isProxyRunning(port)) {
903
- const dir = resolveStateDir(port);
904
- const tls = readTlsMarker(dir);
905
- const tld = readTldFromDir(dir);
906
- return { dir, port, tls, tld };
994
+ const dir2 = resolveStateDir(port);
995
+ const tls = readTlsMarker(dir2);
996
+ const tld = readTldFromDir(dir2);
997
+ const lanIp = readLanMarker(dir2);
998
+ return { dir: dir2, port, tls, tld, lanMode: lanIp !== null || tld === "local", lanIp };
907
999
  }
908
1000
  }
1001
+ const dir = resolveStateDir(configuredPort);
909
1002
  return {
910
- dir: resolveStateDir(configuredPort),
1003
+ dir,
911
1004
  port: configuredPort,
912
1005
  tls: false,
913
- tld: getDefaultTld()
1006
+ tld: getDefaultTld(),
1007
+ lanMode: readLanMarker(dir) !== null,
1008
+ lanIp: null
914
1009
  };
915
1010
  }
916
1011
  async function findFreePort(minPort = MIN_APP_PORT, maxPort = MAX_APP_PORT) {
@@ -964,6 +1059,22 @@ function isProxyRunning(port, tls = false) {
964
1059
  req.end();
965
1060
  });
966
1061
  }
1062
+ function isPortListening(port) {
1063
+ return new Promise((resolve) => {
1064
+ const socket = net2.createConnection({ host: "127.0.0.1", port });
1065
+ let settled = false;
1066
+ const finish = (result) => {
1067
+ if (settled) return;
1068
+ settled = true;
1069
+ socket.destroy();
1070
+ resolve(result);
1071
+ };
1072
+ socket.setTimeout(SOCKET_TIMEOUT_MS);
1073
+ socket.once("connect", () => finish(true));
1074
+ socket.once("error", () => finish(false));
1075
+ socket.once("timeout", () => finish(false));
1076
+ });
1077
+ }
967
1078
  function parsePidFromNetstat(output, port) {
968
1079
  for (const line of output.split(/\r?\n/)) {
969
1080
  if (!line.includes("LISTENING")) continue;
@@ -1091,6 +1202,7 @@ function spawnCommand(commandArgs, options) {
1091
1202
  }
1092
1203
  var FRAMEWORKS_NEEDING_PORT = {
1093
1204
  vite: { strictPort: true },
1205
+ vp: { strictPort: true },
1094
1206
  "react-router": { strictPort: true },
1095
1207
  astro: { strictPort: false },
1096
1208
  ng: { strictPort: false },
@@ -1136,6 +1248,8 @@ function injectFrameworkFlags(commandArgs, port) {
1136
1248
  }
1137
1249
  }
1138
1250
  if (!commandArgs.includes("--host")) {
1251
+ const isExpoLan = basename2 === "expo" && isLanEnvEnabled();
1252
+ if (isExpoLan) return;
1139
1253
  const hostValue = basename2 === "expo" ? "localhost" : "127.0.0.1";
1140
1254
  commandArgs.push("--host", hostValue);
1141
1255
  }
@@ -1364,6 +1478,7 @@ export {
1364
1478
  extractManagedBlock,
1365
1479
  removeBlock,
1366
1480
  buildBlock,
1481
+ shouldAutoSyncHosts,
1367
1482
  syncHostsFile,
1368
1483
  cleanHostsFile,
1369
1484
  getManagedHostnames,
@@ -1371,22 +1486,32 @@ export {
1371
1486
  isWindows2 as isWindows,
1372
1487
  FALLBACK_PROXY_PORT,
1373
1488
  PRIVILEGED_PORT_THRESHOLD,
1489
+ INTERNAL_LAN_IP_ENV,
1490
+ INTERNAL_LAN_IP_FLAG,
1491
+ SYSTEM_STATE_DIR,
1492
+ USER_STATE_DIR,
1374
1493
  WAIT_FOR_PROXY_MAX_ATTEMPTS,
1375
1494
  WAIT_FOR_PROXY_INTERVAL_MS,
1376
- getProtocolPort,
1377
1495
  getDefaultPort,
1378
1496
  resolveStateDir,
1497
+ readTlsMarker,
1379
1498
  writeTlsMarker,
1499
+ readLanMarker,
1500
+ writeLanMarker,
1380
1501
  DEFAULT_TLD,
1381
1502
  RISKY_TLDS,
1382
1503
  validateTld,
1504
+ readTldFromDir,
1383
1505
  writeTldFile,
1384
1506
  getDefaultTld,
1385
1507
  isHttpsEnvDisabled,
1386
1508
  isWildcardEnvEnabled,
1509
+ isLanEnvEnabled,
1510
+ buildProxyStartConfig,
1387
1511
  discoverState,
1388
1512
  findFreePort,
1389
1513
  isProxyRunning,
1514
+ isPortListening,
1390
1515
  findPidOnPort,
1391
1516
  waitForProxy,
1392
1517
  spawnCommand,