portless 0.9.5 → 0.10.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/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
 
@@ -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
@@ -148,6 +175,7 @@ PORTLESS=0 pnpm dev # Bypasses proxy, uses default port
148
175
  # Proxy control
149
176
  portless proxy start # Start the HTTPS proxy (port 443, daemon)
150
177
  portless proxy start --no-tls # Start without HTTPS (port 80)
178
+ portless proxy start --lan # Start in LAN mode (mDNS .local for devices)
151
179
  portless proxy start -p 1355 # Start on a custom port (no sudo)
152
180
  portless proxy start --foreground # Start in foreground (for debugging)
153
181
  portless proxy start --wildcard # Allow unregistered subdomains to fall back to parent
@@ -160,6 +188,8 @@ portless proxy stop # Stop the proxy
160
188
  -p, --port <number> Port for the proxy (default: 443, or 80 with --no-tls)
161
189
  --no-tls Disable HTTPS (use plain HTTP on port 80)
162
190
  --https Enable HTTPS (default, accepted for compatibility)
191
+ --lan Enable LAN mode (mDNS .local for real devices)
192
+ --ip <address> Pin a specific LAN IP (disables auto-follow; use with --lan)
163
193
  --cert <path> Use a custom TLS certificate
164
194
  --key <path> Use a custom TLS private key
165
195
  --foreground Run proxy in foreground instead of daemon
@@ -176,7 +206,8 @@ portless proxy stop # Stop the proxy
176
206
  # Configuration
177
207
  PORTLESS_PORT=<number> Override the default proxy port
178
208
  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)
209
+ PORTLESS_HTTPS=0 Disable HTTPS (same as --no-tls)
210
+ PORTLESS_LAN=1 Enable LAN mode when set to 1 (auto-detects LAN IP)
180
211
  PORTLESS_TLD=<tld> Use a custom TLD (e.g. test; default: localhost)
181
212
  PORTLESS_WILDCARD=1 Allow unregistered subdomains to fall back to parent route
182
213
  PORTLESS_SYNC_HOSTS=1 Auto-sync /etc/hosts (auto-enabled for custom TLDs)
@@ -184,7 +215,7 @@ PORTLESS_STATE_DIR=<path> Override the state directory
184
215
 
185
216
  # Injected into child processes
186
217
  PORT Ephemeral port the child should listen on
187
- HOST Always 127.0.0.1
218
+ HOST Usually 127.0.0.1 (omitted for Expo in LAN mode)
188
219
  PORTLESS_URL Public URL (e.g. https://myapp.localhost)
189
220
  ```
190
221
 
@@ -477,8 +477,16 @@ function createProxyServer(options) {
477
477
  }
478
478
  proxySocket.pipe(socket);
479
479
  socket.pipe(proxySocket);
480
- proxySocket.on("error", () => socket.destroy());
481
- socket.on("error", () => proxySocket.destroy());
480
+ const cleanup = () => {
481
+ proxySocket.destroy();
482
+ socket.destroy();
483
+ };
484
+ proxySocket.on("error", cleanup);
485
+ socket.on("error", cleanup);
486
+ proxySocket.on("close", cleanup);
487
+ socket.on("close", cleanup);
488
+ proxySocket.on("end", cleanup);
489
+ socket.on("end", cleanup);
482
490
  });
483
491
  proxyReq.on("error", (err) => {
484
492
  onError(`WebSocket proxy error for ${getRequestHost(req)}: ${err.message}`);
@@ -662,6 +670,8 @@ import { execSync, spawn } from "child_process";
662
670
  var isWindows2 = process.platform === "win32";
663
671
  var FALLBACK_PROXY_PORT = 1355;
664
672
  var PRIVILEGED_PORT_THRESHOLD = 1024;
673
+ var INTERNAL_LAN_IP_ENV = "PORTLESS_INTERNAL_LAN_IP";
674
+ var INTERNAL_LAN_IP_FLAG = "--lan-ip-auto";
665
675
  var SYSTEM_STATE_DIR = isWindows2 ? path2.join(os.tmpdir(), "portless") : "/tmp/portless";
666
676
  var USER_STATE_DIR = path2.join(os.homedir(), ".portless");
667
677
  var MIN_APP_PORT = 4e3;
@@ -808,6 +818,26 @@ function writeTlsMarker(dir, enabled) {
808
818
  }
809
819
  }
810
820
  }
821
+ var LAN_MARKER_FILE = "proxy.lan";
822
+ function readLanMarker(dir) {
823
+ try {
824
+ const raw = fs3.readFileSync(path2.join(dir, LAN_MARKER_FILE), "utf-8").trim();
825
+ return raw || null;
826
+ } catch {
827
+ return null;
828
+ }
829
+ }
830
+ function writeLanMarker(dir, ip) {
831
+ const markerPath = path2.join(dir, LAN_MARKER_FILE);
832
+ if (!ip) {
833
+ try {
834
+ fs3.unlinkSync(markerPath);
835
+ } catch {
836
+ }
837
+ } else {
838
+ fs3.writeFileSync(markerPath, ip, { mode: 420 });
839
+ }
840
+ }
811
841
  var DEFAULT_TLD = "localhost";
812
842
  var RISKY_TLDS = /* @__PURE__ */ new Map([
813
843
  ["local", "conflicts with mDNS/Bonjour on macOS"],
@@ -864,20 +894,78 @@ function isWildcardEnvEnabled() {
864
894
  const val = process.env.PORTLESS_WILDCARD;
865
895
  return val === "1" || val === "true";
866
896
  }
897
+ function isLanEnvEnabled() {
898
+ const val = process.env.PORTLESS_LAN;
899
+ return val === "1" || val === "true";
900
+ }
901
+ function buildProxyStartConfig(options) {
902
+ const effectiveTld = options.lanMode ? "local" : options.tld;
903
+ const args = [];
904
+ if (options.foreground) {
905
+ args.push("--foreground");
906
+ }
907
+ if (options.includePort && options.proxyPort !== void 0) {
908
+ args.push("--port", options.proxyPort.toString());
909
+ }
910
+ if (options.useHttps) {
911
+ if (options.customCertPath && options.customKeyPath) {
912
+ args.push("--cert", options.customCertPath, "--key", options.customKeyPath);
913
+ } else {
914
+ args.push("--https");
915
+ }
916
+ } else {
917
+ args.push("--no-tls");
918
+ }
919
+ if (options.lanMode) {
920
+ args.push("--lan");
921
+ if (options.lanIp) {
922
+ if (options.lanIpExplicit) {
923
+ args.push("--ip", options.lanIp);
924
+ } else {
925
+ args.push(INTERNAL_LAN_IP_FLAG, options.lanIp);
926
+ }
927
+ }
928
+ } else if (effectiveTld !== DEFAULT_TLD) {
929
+ args.push("--tld", effectiveTld);
930
+ }
931
+ if (options.useWildcard) {
932
+ args.push("--wildcard");
933
+ }
934
+ return { effectiveTld, args };
935
+ }
867
936
  async function discoverState() {
868
937
  if (process.env.PORTLESS_STATE_DIR) {
869
- const dir = process.env.PORTLESS_STATE_DIR;
870
- const port = readPortFromDir(dir) ?? getDefaultPort();
871
- const tls = readTlsMarker(dir);
872
- const tld = readTldFromDir(dir);
873
- return { dir, port, tls, tld };
938
+ const dir2 = process.env.PORTLESS_STATE_DIR;
939
+ const port = readPortFromDir(dir2) ?? getDefaultPort();
940
+ const lanIp = readLanMarker(dir2);
941
+ if (await isProxyRunning(port) || await isPortListening(port)) {
942
+ const tls = readTlsMarker(dir2);
943
+ const tld = readTldFromDir(dir2);
944
+ return { dir: dir2, port, tls, tld, lanMode: lanIp !== null || tld === "local", lanIp };
945
+ }
946
+ return {
947
+ dir: dir2,
948
+ port,
949
+ tls: readTlsMarker(dir2),
950
+ tld: readTldFromDir(dir2),
951
+ lanMode: lanIp !== null,
952
+ lanIp: null
953
+ };
874
954
  }
875
955
  const userPort = readPortFromDir(USER_STATE_DIR);
876
956
  if (userPort !== null) {
877
957
  if (await isProxyRunning(userPort)) {
878
958
  const tls = readTlsMarker(USER_STATE_DIR);
879
959
  const tld = readTldFromDir(USER_STATE_DIR);
880
- return { dir: USER_STATE_DIR, port: userPort, tls, tld };
960
+ const lanIp = readLanMarker(USER_STATE_DIR);
961
+ return {
962
+ dir: USER_STATE_DIR,
963
+ port: userPort,
964
+ tls,
965
+ tld,
966
+ lanMode: lanIp !== null || tld === "local",
967
+ lanIp
968
+ };
881
969
  }
882
970
  }
883
971
  const systemPort = readPortFromDir(SYSTEM_STATE_DIR);
@@ -885,24 +973,36 @@ async function discoverState() {
885
973
  if (await isProxyRunning(systemPort)) {
886
974
  const tls = readTlsMarker(SYSTEM_STATE_DIR);
887
975
  const tld = readTldFromDir(SYSTEM_STATE_DIR);
888
- return { dir: SYSTEM_STATE_DIR, port: systemPort, tls, tld };
976
+ const lanIp = readLanMarker(SYSTEM_STATE_DIR);
977
+ return {
978
+ dir: SYSTEM_STATE_DIR,
979
+ port: systemPort,
980
+ tls,
981
+ tld,
982
+ lanMode: lanIp !== null || tld === "local",
983
+ lanIp
984
+ };
889
985
  }
890
986
  }
891
987
  const configuredPort = getDefaultPort();
892
988
  const probePorts = /* @__PURE__ */ new Set([443, 80, FALLBACK_PROXY_PORT, configuredPort]);
893
989
  for (const port of probePorts) {
894
990
  if (await isProxyRunning(port)) {
895
- const dir = resolveStateDir(port);
896
- const tls = readTlsMarker(dir);
897
- const tld = readTldFromDir(dir);
898
- return { dir, port, tls, tld };
991
+ const dir2 = resolveStateDir(port);
992
+ const tls = readTlsMarker(dir2);
993
+ const tld = readTldFromDir(dir2);
994
+ const lanIp = readLanMarker(dir2);
995
+ return { dir: dir2, port, tls, tld, lanMode: lanIp !== null || tld === "local", lanIp };
899
996
  }
900
997
  }
998
+ const dir = resolveStateDir(configuredPort);
901
999
  return {
902
- dir: resolveStateDir(configuredPort),
1000
+ dir,
903
1001
  port: configuredPort,
904
1002
  tls: false,
905
- tld: getDefaultTld()
1003
+ tld: getDefaultTld(),
1004
+ lanMode: readLanMarker(dir) !== null,
1005
+ lanIp: null
906
1006
  };
907
1007
  }
908
1008
  async function findFreePort(minPort = MIN_APP_PORT, maxPort = MAX_APP_PORT) {
@@ -956,6 +1056,22 @@ function isProxyRunning(port, tls = false) {
956
1056
  req.end();
957
1057
  });
958
1058
  }
1059
+ function isPortListening(port) {
1060
+ return new Promise((resolve) => {
1061
+ const socket = net2.createConnection({ host: "127.0.0.1", port });
1062
+ let settled = false;
1063
+ const finish = (result) => {
1064
+ if (settled) return;
1065
+ settled = true;
1066
+ socket.destroy();
1067
+ resolve(result);
1068
+ };
1069
+ socket.setTimeout(SOCKET_TIMEOUT_MS);
1070
+ socket.once("connect", () => finish(true));
1071
+ socket.once("error", () => finish(false));
1072
+ socket.once("timeout", () => finish(false));
1073
+ });
1074
+ }
959
1075
  function parsePidFromNetstat(output, port) {
960
1076
  for (const line of output.split(/\r?\n/)) {
961
1077
  if (!line.includes("LISTENING")) continue;
@@ -1083,6 +1199,7 @@ function spawnCommand(commandArgs, options) {
1083
1199
  }
1084
1200
  var FRAMEWORKS_NEEDING_PORT = {
1085
1201
  vite: { strictPort: true },
1202
+ vp: { strictPort: true },
1086
1203
  "react-router": { strictPort: true },
1087
1204
  astro: { strictPort: false },
1088
1205
  ng: { strictPort: false },
@@ -1128,6 +1245,8 @@ function injectFrameworkFlags(commandArgs, port) {
1128
1245
  }
1129
1246
  }
1130
1247
  if (!commandArgs.includes("--host")) {
1248
+ const isExpoLan = basename2 === "expo" && isLanEnvEnabled();
1249
+ if (isExpoLan) return;
1131
1250
  const hostValue = basename2 === "expo" ? "localhost" : "127.0.0.1";
1132
1251
  commandArgs.push("--host", hostValue);
1133
1252
  }
@@ -1363,22 +1482,30 @@ export {
1363
1482
  isWindows2 as isWindows,
1364
1483
  FALLBACK_PROXY_PORT,
1365
1484
  PRIVILEGED_PORT_THRESHOLD,
1485
+ INTERNAL_LAN_IP_ENV,
1486
+ INTERNAL_LAN_IP_FLAG,
1366
1487
  WAIT_FOR_PROXY_MAX_ATTEMPTS,
1367
1488
  WAIT_FOR_PROXY_INTERVAL_MS,
1368
- getProtocolPort,
1369
1489
  getDefaultPort,
1370
1490
  resolveStateDir,
1491
+ readTlsMarker,
1371
1492
  writeTlsMarker,
1493
+ readLanMarker,
1494
+ writeLanMarker,
1372
1495
  DEFAULT_TLD,
1373
1496
  RISKY_TLDS,
1374
1497
  validateTld,
1498
+ readTldFromDir,
1375
1499
  writeTldFile,
1376
1500
  getDefaultTld,
1377
1501
  isHttpsEnvDisabled,
1378
1502
  isWildcardEnvEnabled,
1503
+ isLanEnvEnabled,
1504
+ buildProxyStartConfig,
1379
1505
  discoverState,
1380
1506
  findFreePort,
1381
1507
  isProxyRunning,
1508
+ isPortListening,
1382
1509
  findPidOnPort,
1383
1510
  waitForProxy,
1384
1511
  spawnCommand,