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 +34 -3
- package/dist/{chunk-D3LR3J7L.js → chunk-WXEE5QH6.js} +143 -16
- package/dist/cli.js +757 -106
- package/dist/index.js +1 -1
- package/package.json +1 -1
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`
|
|
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
|
|
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
|
|
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
|
-
|
|
481
|
-
|
|
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
|
|
870
|
-
const port = readPortFromDir(
|
|
871
|
-
const
|
|
872
|
-
|
|
873
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
896
|
-
const tls = readTlsMarker(
|
|
897
|
-
const tld = readTldFromDir(
|
|
898
|
-
|
|
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
|
|
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,
|