portless 0.15.0 → 0.15.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
@@ -34,7 +34,7 @@ HTTPS with HTTP/2 is enabled by default. On first run, portless generates a loca
34
34
 
35
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
- When auto-starting, portless reuses the configuration (port, TLS, TLD) from the most recent proxy run, so a restart or reboot does not silently revert to defaults. Explicit env vars (`PORTLESS_PORT`, `PORTLESS_HTTPS`, etc.) always take priority.
37
+ When auto-starting, portless reuses the configuration (port, TLS, TLDs) from the most recent proxy run, so a restart or reboot does not silently revert to defaults. Explicit env vars (`PORTLESS_PORT`, `PORTLESS_HTTPS`, etc.) always take priority.
38
38
 
39
39
  In non-interactive environments (no TTY, or `CI=1`), portless exits with a descriptive error instead of prompting, so task runners like turborepo and CI scripts fail early with a clear message.
40
40
 
@@ -210,6 +210,17 @@ portless myapp next dev
210
210
 
211
211
  The proxy auto-syncs `/etc/hosts` for route hostnames (including `.test`), so those domains resolve on your machine.
212
212
 
213
+ Repeat `--tld` to serve the same app names under multiple TLDs from one proxy:
214
+
215
+ ```bash
216
+ portless proxy start --tld localhost --tld test
217
+ portless myapp next dev
218
+ # -> https://myapp.localhost
219
+ # -> https://myapp.test
220
+ ```
221
+
222
+ When multiple TLDs are configured, `PORTLESS_URL` uses the first TLD. `PORTLESS_TLD` also accepts a comma separated list, e.g. `PORTLESS_TLD=localhost,test`.
223
+
213
224
  Recommended: `.test` (IANA-reserved, no collision risk). Avoid `.local` (conflicts with mDNS/Bonjour) and `.dev` (Google-owned, forces HTTPS via HSTS).
214
225
 
215
226
  ## How it works
@@ -264,7 +275,7 @@ portless service uninstall
264
275
 
265
276
  The service uses portless defaults unless install options or `PORTLESS_*` environment variables are provided: HTTPS on port 443 with `.localhost` names. `service install` accepts the proxy options you would use with `proxy start`, including `--port`, `--no-tls`, `--lan`, `--ip`, `--tld`, `--wildcard`, `--cert`, and `--key`. Use `--state-dir <path>` or `PORTLESS_STATE_DIR=<path>` to choose where service state and logs are written.
266
277
 
267
- The chosen service configuration is written into launchd, systemd, or Task Scheduler and reused after reboot. `portless service status` reports the installed port, HTTPS mode, TLD, LAN mode, wildcard mode, and state directory. macOS and Linux install a root-owned service so port 443 can bind at boot. Windows installs a Task Scheduler startup task that runs as SYSTEM. Installation and removal may require administrator privileges. `portless clean` automatically removes the service.
278
+ The chosen service configuration is written into launchd, systemd, or Task Scheduler and reused after reboot. `portless service status` reports the installed port, HTTPS mode, TLDs, LAN mode, wildcard mode, and state directory. macOS and Linux install a root-owned service so port 443 can bind at boot. Windows installs a Task Scheduler startup task that runs as SYSTEM. Installation and removal may require administrator privileges. `portless clean` automatically removes the service.
268
279
 
269
280
  ## LAN mode
270
281
 
@@ -276,7 +287,7 @@ portless proxy start --lan --ip 192.168.1.42
276
287
 
277
288
  `--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.
278
289
 
279
- Portless remembers LAN mode via `proxy.lan`, so if you stop a LAN proxy and start it again, it stays in LAN mode. All proxy settings (port, TLS, TLD, LAN) are persisted and reused on auto-start unless overridden by explicit flags or 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.
290
+ Portless remembers LAN mode via `proxy.lan`, so if you stop a LAN proxy and start it again, it stays in LAN mode. All proxy settings (port, TLS, TLDs, LAN) are persisted and reused on auto-start unless overridden by explicit flags or 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.
280
291
 
281
292
  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.
282
293
 
@@ -386,7 +397,7 @@ portless service uninstall # Remove the startup service
386
397
  --cert <path> Use a custom TLS certificate
387
398
  --key <path> Use a custom TLS private key
388
399
  --foreground Run proxy in foreground instead of daemon
389
- --tld <tld> Use a custom TLD instead of .localhost (e.g. test)
400
+ --tld <tld> Use a custom TLD instead of .localhost; repeat for more
390
401
  --wildcard Allow unregistered subdomains to fall back to parent route
391
402
  --state-dir <path> Use a custom state directory with service install
392
403
  --script <name> Run a specific package.json script (default: dev)
@@ -407,7 +418,7 @@ PORTLESS_APP_PORT=<number> Use a fixed port for the app (same as --app-por
407
418
  PORTLESS_HTTPS=0 Disable HTTPS (same as --no-tls)
408
419
  PORTLESS_LAN=1 Enable LAN mode when set to 1 (auto-detects LAN IP)
409
420
  PORTLESS_LAN_IP=<address> Pin a specific LAN IP for LAN mode
410
- PORTLESS_TLD=<tld> Use a custom TLD (e.g. test; default: localhost)
421
+ PORTLESS_TLD=<tld>[,<tld>] Use one or more TLDs (e.g. localhost,test)
411
422
  PORTLESS_WILDCARD=1 Allow unregistered subdomains to fall back to parent route
412
423
  PORTLESS_SYNC_HOSTS=0 Disable auto-sync of /etc/hosts (on by default)
413
424
  PORTLESS_TAILSCALE=1 Share apps on your Tailscale network (same as --tailscale)
@@ -418,7 +429,7 @@ PORTLESS_STATE_DIR=<path> Override the state directory
418
429
  # Injected into child processes
419
430
  PORT Ephemeral port the child should listen on
420
431
  HOST Usually 127.0.0.1 (omitted for Expo in LAN mode)
421
- PORTLESS_URL Public URL (e.g. https://myapp.localhost)
432
+ PORTLESS_URL Primary public URL (e.g. https://myapp.localhost)
422
433
  PORTLESS_TAILSCALE_URL Tailscale URL of the app (when --tailscale is active)
423
434
  PORTLESS_NGROK_URL ngrok URL of the app (when --ngrok is active)
424
435
  NODE_EXTRA_CA_CERTS Path to the portless CA (when HTTPS is active)
@@ -65,6 +65,18 @@ function parseHostname(input, tld = "localhost") {
65
65
  }
66
66
  return hostname;
67
67
  }
68
+ function parseHostnames(input, tlds = ["localhost"]) {
69
+ const uniqueTlds = [...new Set(tlds)];
70
+ let baseInput = input.trim().replace(/^https?:\/\//, "").split("/")[0].toLowerCase();
71
+ for (const tld of uniqueTlds) {
72
+ const suffix = `.${tld}`;
73
+ if (baseInput.endsWith(suffix)) {
74
+ baseInput = baseInput.slice(0, -suffix.length);
75
+ break;
76
+ }
77
+ }
78
+ return uniqueTlds.map((tld) => parseHostname(baseInput, tld));
79
+ }
68
80
 
69
81
  // src/proxy.ts
70
82
  import * as http from "http";
@@ -322,11 +334,13 @@ function createProxyServer(options) {
322
334
  getRoutes,
323
335
  proxyPort,
324
336
  tld = "localhost",
337
+ tlds = [tld],
325
338
  strict = true,
326
339
  onError = (msg) => console.error(msg),
327
340
  tls
328
341
  } = options;
329
- const tldSuffix = `.${tld}`;
342
+ const tldSuffixes = [...new Set(tlds.length > 0 ? tlds : [tld])].map((value) => `.${value}`);
343
+ const primaryTldSuffix = tldSuffixes[0] ?? ".localhost";
330
344
  const handleRequest = (req, res) => {
331
345
  const reqTls = isEncrypted(req);
332
346
  res.setHeader(PORTLESS_HEADER, "1");
@@ -349,7 +363,7 @@ function createProxyServer(options) {
349
363
  "Loop Detected",
350
364
  `<div class="content"><p class="desc">This request has passed through portless ${hops} times. This usually means a dev server (Vite, webpack, etc.) is proxying requests back through portless without rewriting the Host header.</p><div class="section"><p class="label">Fix: add changeOrigin to your proxy config</p><pre class="terminal">proxy: {
351
365
  "/api": {
352
- target: "${reqTls ? "https" : "http"}://&lt;backend&gt;${escapeHtml(tldSuffix)}${reqTls ? "" : ":&lt;port&gt;"}",
366
+ target: "${reqTls ? "https" : "http"}://&lt;backend&gt;${escapeHtml(primaryTldSuffix)}${reqTls ? "" : ":&lt;port&gt;"}",
353
367
  changeOrigin: true,
354
368
  },
355
369
  }</pre></div></div>`
@@ -360,7 +374,8 @@ function createProxyServer(options) {
360
374
  const route = findRoute(routes, host, strict);
361
375
  if (!route) {
362
376
  const safeHost = escapeHtml(host);
363
- const strippedHost = host.endsWith(tldSuffix) ? host.slice(0, -tldSuffix.length) : host;
377
+ const matchedSuffix = tldSuffixes.find((suffix) => host.endsWith(suffix));
378
+ const strippedHost = matchedSuffix ? host.slice(0, -matchedSuffix.length) : host;
364
379
  const safeSuggestion = escapeHtml(strippedHost);
365
380
  const routesList = routes.length > 0 ? `<div class="section"><p class="label">Active apps</p><ul class="card">${routes.map((r) => `<li><a href="${escapeHtml(formatUrl(r.hostname, proxyPort, reqTls))}" class="card-link"><span class="name">${escapeHtml(r.hostname)}</span><span class="meta"><code class="port">127.0.0.1:${escapeHtml(String(r.port))}</code><span class="arrow">${ARROW_SVG}</span></span></a></li>`).join("")}</ul></div>` : '<p class="empty">No apps running.</p>';
366
381
  res.writeHead(404, { "Content-Type": "text/html" });
@@ -694,9 +709,9 @@ function checkHostResolution(hostname) {
694
709
  import * as fs3 from "fs";
695
710
  import * as path2 from "path";
696
711
  var STALE_LOCK_THRESHOLD_MS = 1e4;
697
- var LOCK_TIMEOUT_MS = 5e3;
712
+ var LOCK_TIMEOUT_MS = 15e3;
698
713
  var LOCK_RETRY_BASE_MS = 10;
699
- var LOCK_RETRY_CAP_MS = 500;
714
+ var LOCK_RETRY_CAP_MS = 100;
700
715
  var FILE_MODE = 420;
701
716
  var DIR_MODE = 493;
702
717
  function isValidRoute(value) {
@@ -995,6 +1010,7 @@ export {
995
1010
  escapeHtml,
996
1011
  formatUrl,
997
1012
  parseHostname,
1013
+ parseHostnames,
998
1014
  PORTLESS_HEADER,
999
1015
  createProxyServer,
1000
1016
  createHttpRedirectServer,