portless 0.13.1 → 0.15.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 +24 -1
- package/dist/{chunk-3WLVQXFE.js → chunk-PCBKLZK2.js} +53 -7
- package/dist/cli.js +744 -44
- package/dist/index.d.ts +20 -3
- package/dist/index.js +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -323,6 +323,20 @@ Set `PORTLESS_TAILSCALE=1` in your shell profile or `.env` to share every app by
|
|
|
323
323
|
|
|
324
324
|
Requires the Tailscale CLI to be installed and connected (`tailscale up`), with Tailscale HTTPS certificates enabled.
|
|
325
325
|
|
|
326
|
+
## ngrok sharing
|
|
327
|
+
|
|
328
|
+
Expose your dev server to the public internet with [ngrok](https://ngrok.com):
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
portless myapp --ngrok next dev
|
|
332
|
+
# -> https://myapp.localhost (local)
|
|
333
|
+
# -> https://abc123.ngrok.app (public)
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Set `PORTLESS_NGROK=1` in your shell profile or `.env` to enable ngrok by default when portless runs an app. `portless list` shows both local and ngrok URLs. The ngrok tunnel is cleaned up automatically when the app exits.
|
|
337
|
+
|
|
338
|
+
Requires the ngrok CLI to be installed and authenticated. If ngrok reports an authentication error, run `ngrok config add-authtoken <token>` and try again.
|
|
339
|
+
|
|
326
340
|
## Commands
|
|
327
341
|
|
|
328
342
|
```bash
|
|
@@ -334,6 +348,7 @@ portless alias <name> <port> # Register a static route (e.g. for Docker)
|
|
|
334
348
|
portless alias <name> <port> --force # Overwrite an existing route
|
|
335
349
|
portless alias --remove <name> # Remove a static route
|
|
336
350
|
portless list # Show active routes
|
|
351
|
+
portless doctor # Check proxy, routes, DNS, and CA trust
|
|
337
352
|
portless trust # Add local CA to system trust store
|
|
338
353
|
portless clean # Remove state, CA trust entry, and hosts block
|
|
339
354
|
portless prune # Kill orphaned dev servers from crashed sessions
|
|
@@ -378,6 +393,7 @@ portless service uninstall # Remove the startup service
|
|
|
378
393
|
--app-port <number> Use a fixed port for the app (skip auto-assignment)
|
|
379
394
|
--tailscale Share the app on your Tailscale network (tailnet)
|
|
380
395
|
--funnel Share the app publicly via Tailscale Funnel
|
|
396
|
+
--ngrok Share the app publicly via ngrok
|
|
381
397
|
--force Kill the existing process and take over its route
|
|
382
398
|
--name <name> Use <name> as the app name
|
|
383
399
|
```
|
|
@@ -396,6 +412,7 @@ PORTLESS_WILDCARD=1 Allow unregistered subdomains to fall back to p
|
|
|
396
412
|
PORTLESS_SYNC_HOSTS=0 Disable auto-sync of /etc/hosts (on by default)
|
|
397
413
|
PORTLESS_TAILSCALE=1 Share apps on your Tailscale network (same as --tailscale)
|
|
398
414
|
PORTLESS_FUNNEL=1 Share apps publicly via Tailscale Funnel (same as --funnel)
|
|
415
|
+
PORTLESS_NGROK=1 Share apps publicly via ngrok (same as --ngrok)
|
|
399
416
|
PORTLESS_STATE_DIR=<path> Override the state directory
|
|
400
417
|
|
|
401
418
|
# Injected into child processes
|
|
@@ -403,10 +420,11 @@ PORT Ephemeral port the child should listen on
|
|
|
403
420
|
HOST Usually 127.0.0.1 (omitted for Expo in LAN mode)
|
|
404
421
|
PORTLESS_URL Public URL (e.g. https://myapp.localhost)
|
|
405
422
|
PORTLESS_TAILSCALE_URL Tailscale URL of the app (when --tailscale is active)
|
|
423
|
+
PORTLESS_NGROK_URL ngrok URL of the app (when --ngrok is active)
|
|
406
424
|
NODE_EXTRA_CA_CERTS Path to the portless CA (when HTTPS is active)
|
|
407
425
|
```
|
|
408
426
|
|
|
409
|
-
> **Reserved names:** `run`, `get`, `alias`, `hosts`, `list`, `trust`, `clean`, `prune`, `proxy`, and `service` 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.
|
|
427
|
+
> **Reserved names:** `run`, `get`, `alias`, `hosts`, `list`, `doctor`, `trust`, `clean`, `prune`, `proxy`, and `service` 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.
|
|
410
428
|
|
|
411
429
|
## Uninstall / reset
|
|
412
430
|
|
|
@@ -431,6 +449,10 @@ portless hosts clean # Clean up later
|
|
|
431
449
|
|
|
432
450
|
Auto-syncs `/etc/hosts` for route hostnames by default (`.localhost`, custom TLDs, LAN `.local`). Set `PORTLESS_SYNC_HOSTS=0` to disable.
|
|
433
451
|
|
|
452
|
+
## Troubleshooting
|
|
453
|
+
|
|
454
|
+
Run `portless doctor` to inspect local health without changing state. It checks Node.js, the state directory, proxy liveness, route entries, HTTPS CA trust, hostname resolution, and LAN mode prerequisites, then prints suggested fixes.
|
|
455
|
+
|
|
434
456
|
## Proxying Between Portless Apps
|
|
435
457
|
|
|
436
458
|
If your frontend dev server (e.g. Vite, webpack) proxies API requests to another portless app, make sure the proxy rewrites the `Host` header. Without this, portless routes the request back to the frontend in an infinite loop.
|
|
@@ -486,3 +508,4 @@ pnpm format # Format all files with Prettier
|
|
|
486
508
|
- Node.js 24+
|
|
487
509
|
- macOS, Linux, or Windows
|
|
488
510
|
- Tailscale CLI (optional, for `--tailscale` and `--funnel`)
|
|
511
|
+
- ngrok CLI (optional, for `--ngrok`)
|
|
@@ -17,6 +17,15 @@ function fixOwnership(...paths) {
|
|
|
17
17
|
function isErrnoException(err) {
|
|
18
18
|
return err instanceof Error && "code" in err && typeof err.code === "string";
|
|
19
19
|
}
|
|
20
|
+
function isProcessAlive(pid) {
|
|
21
|
+
if (pid <= 0) return false;
|
|
22
|
+
try {
|
|
23
|
+
process.kill(pid, 0);
|
|
24
|
+
return true;
|
|
25
|
+
} catch (err) {
|
|
26
|
+
return isErrnoException(err) && err.code === "EPERM";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
20
29
|
function escapeHtml(str) {
|
|
21
30
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
22
31
|
}
|
|
@@ -375,6 +384,9 @@ function createProxyServer(options) {
|
|
|
375
384
|
delete proxyReqHeaders[key];
|
|
376
385
|
}
|
|
377
386
|
}
|
|
387
|
+
if (!proxyReqHeaders.host) {
|
|
388
|
+
proxyReqHeaders.host = getRequestHost(req);
|
|
389
|
+
}
|
|
378
390
|
const proxyReq = http.request(
|
|
379
391
|
{
|
|
380
392
|
hostname: "127.0.0.1",
|
|
@@ -460,6 +472,9 @@ function createProxyServer(options) {
|
|
|
460
472
|
delete proxyReqHeaders[key];
|
|
461
473
|
}
|
|
462
474
|
}
|
|
475
|
+
if (!proxyReqHeaders.host) {
|
|
476
|
+
proxyReqHeaders.host = getRequestHost(req);
|
|
477
|
+
}
|
|
463
478
|
const proxyReq = http.request({
|
|
464
479
|
hostname: "127.0.0.1",
|
|
465
480
|
port: route.port,
|
|
@@ -869,13 +884,17 @@ var RouteStore = class _RouteStore {
|
|
|
869
884
|
try {
|
|
870
885
|
parsed = JSON.parse(raw);
|
|
871
886
|
} catch {
|
|
887
|
+
this.onWarning?.(`Corrupted routes file (invalid JSON): ${this.routesPath}`);
|
|
872
888
|
return [];
|
|
873
889
|
}
|
|
874
890
|
if (!Array.isArray(parsed)) {
|
|
891
|
+
this.onWarning?.(`Corrupted routes file (expected array): ${this.routesPath}`);
|
|
875
892
|
return [];
|
|
876
893
|
}
|
|
877
894
|
return parsed.filter(isValidRoute);
|
|
878
|
-
} catch {
|
|
895
|
+
} catch (err) {
|
|
896
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
897
|
+
this.onWarning?.(`Could not read routes file: ${message}`);
|
|
879
898
|
return [];
|
|
880
899
|
}
|
|
881
900
|
}
|
|
@@ -920,22 +939,48 @@ var RouteStore = class _RouteStore {
|
|
|
920
939
|
const routes = this.loadRoutes(true);
|
|
921
940
|
const route = routes.find((r) => r.hostname === hostname);
|
|
922
941
|
if (!route) return;
|
|
923
|
-
if (
|
|
924
|
-
|
|
925
|
-
route.
|
|
926
|
-
|
|
942
|
+
if ("tailscaleUrl" in fields) {
|
|
943
|
+
if (fields.tailscaleUrl === null) delete route.tailscaleUrl;
|
|
944
|
+
else if (fields.tailscaleUrl !== void 0) route.tailscaleUrl = fields.tailscaleUrl;
|
|
945
|
+
}
|
|
946
|
+
if ("tailscaleHttpsPort" in fields) {
|
|
947
|
+
if (fields.tailscaleHttpsPort === null) delete route.tailscaleHttpsPort;
|
|
948
|
+
else if (fields.tailscaleHttpsPort !== void 0)
|
|
949
|
+
route.tailscaleHttpsPort = fields.tailscaleHttpsPort;
|
|
950
|
+
}
|
|
951
|
+
if ("tailscaleFunnel" in fields) {
|
|
952
|
+
if (fields.tailscaleFunnel === null) delete route.tailscaleFunnel;
|
|
953
|
+
else if (fields.tailscaleFunnel !== void 0)
|
|
954
|
+
route.tailscaleFunnel = fields.tailscaleFunnel;
|
|
955
|
+
}
|
|
956
|
+
if ("ngrokUrl" in fields) {
|
|
957
|
+
if (fields.ngrokUrl === null) delete route.ngrokUrl;
|
|
958
|
+
else if (fields.ngrokUrl !== void 0) route.ngrokUrl = fields.ngrokUrl;
|
|
959
|
+
}
|
|
960
|
+
if ("ngrokPid" in fields) {
|
|
961
|
+
if (fields.ngrokPid === null) delete route.ngrokPid;
|
|
962
|
+
else if (fields.ngrokPid !== void 0) route.ngrokPid = fields.ngrokPid;
|
|
963
|
+
}
|
|
927
964
|
this.saveRoutes(routes);
|
|
928
965
|
} finally {
|
|
929
966
|
this.releaseLock();
|
|
930
967
|
}
|
|
931
968
|
}
|
|
932
|
-
|
|
969
|
+
/**
|
|
970
|
+
* Remove a route by hostname. When `ownerPid` is provided, the entry is
|
|
971
|
+
* only removed while it is still owned by that pid. Exit cleanups must
|
|
972
|
+
* pass their own pid: after a `--force` takeover the killed process would
|
|
973
|
+
* otherwise deregister the route the new owner just registered.
|
|
974
|
+
*/
|
|
975
|
+
removeRoute(hostname, ownerPid) {
|
|
933
976
|
this.ensureDir();
|
|
934
977
|
if (!this.acquireLock()) {
|
|
935
978
|
throw new Error("Failed to acquire route lock");
|
|
936
979
|
}
|
|
937
980
|
try {
|
|
938
|
-
const routes = this.loadRoutes(true).filter(
|
|
981
|
+
const routes = this.loadRoutes(true).filter(
|
|
982
|
+
(r) => r.hostname !== hostname || ownerPid !== void 0 && r.pid !== ownerPid
|
|
983
|
+
);
|
|
939
984
|
this.saveRoutes(routes);
|
|
940
985
|
} finally {
|
|
941
986
|
this.releaseLock();
|
|
@@ -946,6 +991,7 @@ var RouteStore = class _RouteStore {
|
|
|
946
991
|
export {
|
|
947
992
|
fixOwnership,
|
|
948
993
|
isErrnoException,
|
|
994
|
+
isProcessAlive,
|
|
949
995
|
escapeHtml,
|
|
950
996
|
formatUrl,
|
|
951
997
|
parseHostname,
|