portless 0.7.1 → 0.8.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
@@ -29,7 +29,7 @@ portless myapp next dev
29
29
  # -> http://myapp.localhost:1355
30
30
  ```
31
31
 
32
- 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), portless auto-injects `--port` and `--host` flags.
32
+ 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.
33
33
 
34
34
  ## Use in package.json
35
35
 
@@ -53,7 +53,7 @@ portless docs.myapp next dev
53
53
  # -> http://docs.myapp.localhost:1355
54
54
  ```
55
55
 
56
- Wildcard subdomain routing: any subdomain of a registered route routes to that app automatically (e.g. `tenant1.myapp.localhost:1355` routes to the `myapp` app without extra registration).
56
+ By default, only explicitly registered subdomains are routed (strict mode). Use `--wildcard` when starting the proxy to allow any subdomain of a registered route to fall back to that app (e.g. `tenant1.myapp.localhost:1355` routes to the `myapp` app without extra registration).
57
57
 
58
58
  ## Git Worktrees
59
59
 
@@ -93,10 +93,10 @@ Recommended: `.test` (IANA-reserved, no collision risk). Avoid `.local` (conflic
93
93
 
94
94
  ```mermaid
95
95
  flowchart TD
96
- Browser["Browser\nmyapp.localhost:1355"]
97
- Proxy["portless proxy\n(port 1355)"]
98
- App1[":4123\nmyapp"]
99
- App2[":4567\napi"]
96
+ Browser["Browser<br>myapp.localhost:1355"]
97
+ Proxy["portless proxy<br>(port 1355)"]
98
+ App1[":4123<br>myapp"]
99
+ App2[":4567<br>api"]
100
100
 
101
101
  Browser -->|port 1355| Proxy
102
102
  Proxy --> App1
@@ -152,6 +152,7 @@ portless proxy start # Start the proxy (port 1355, daemon)
152
152
  portless proxy start --https # Start with HTTP/2 + TLS
153
153
  portless proxy start -p 80 # Start on port 80 (requires sudo)
154
154
  portless proxy start --foreground # Start in foreground (for debugging)
155
+ portless proxy start --wildcard # Allow unregistered subdomains to fall back to parent
155
156
  portless proxy stop # Stop the proxy
156
157
  ```
157
158
 
@@ -165,6 +166,7 @@ portless proxy stop # Stop the proxy
165
166
  --no-tls Disable HTTPS (overrides PORTLESS_HTTPS)
166
167
  --foreground Run proxy in foreground instead of daemon
167
168
  --tld <tld> Use a custom TLD instead of .localhost (e.g. test)
169
+ --wildcard Allow unregistered subdomains to fall back to parent route
168
170
  --app-port <number> Use a fixed port for the app (skip auto-assignment)
169
171
  --force Override a route registered by another process
170
172
  --name <name> Use <name> as the app name
@@ -178,6 +180,7 @@ PORTLESS_PORT=<number> Override the default proxy port
178
180
  PORTLESS_APP_PORT=<number> Use a fixed port for the app (same as --app-port)
179
181
  PORTLESS_HTTPS=1 Always enable HTTPS
180
182
  PORTLESS_TLD=<tld> Use a custom TLD (e.g. test; default: localhost)
183
+ PORTLESS_WILDCARD=1 Allow unregistered subdomains to fall back to parent route
181
184
  PORTLESS_SYNC_HOSTS=1 Auto-sync /etc/hosts (auto-enabled for custom TLDs)
182
185
  PORTLESS_STATE_DIR=<path> Override the state directory
183
186
 
@@ -300,14 +300,15 @@ function buildForwardedHeaders(req, tls) {
300
300
  }
301
301
  var PORTLESS_HOPS_HEADER = "x-portless-hops";
302
302
  var MAX_PROXY_HOPS = 5;
303
- function findRoute(routes, host) {
304
- return routes.find((r) => r.hostname === host) || routes.find((r) => host.endsWith("." + r.hostname));
303
+ function findRoute(routes, host, strict) {
304
+ return routes.find((r) => r.hostname === host) || (strict ? void 0 : routes.find((r) => host.endsWith("." + r.hostname)));
305
305
  }
306
306
  function createProxyServer(options) {
307
307
  const {
308
308
  getRoutes,
309
309
  proxyPort,
310
310
  tld = "localhost",
311
+ strict = true,
311
312
  onError = (msg) => console.error(msg),
312
313
  tls
313
314
  } = options;
@@ -342,7 +343,7 @@ function createProxyServer(options) {
342
343
  );
343
344
  return;
344
345
  }
345
- const route = findRoute(routes, host);
346
+ const route = findRoute(routes, host, strict);
346
347
  if (!route) {
347
348
  const safeHost = escapeHtml(host);
348
349
  const strippedHost = host.endsWith(tldSuffix) ? host.slice(0, -tldSuffix.length) : host;
@@ -436,7 +437,7 @@ function createProxyServer(options) {
436
437
  }
437
438
  const routes = getRoutes();
438
439
  const host = getRequestHost(req).split(":")[0];
439
- const route = findRoute(routes, host);
440
+ const route = findRoute(routes, host, strict);
440
441
  if (!route) {
441
442
  socket.destroy();
442
443
  return;
@@ -515,6 +516,9 @@ function createProxyServer(options) {
515
516
  const plainServer = http.createServer(handleRequest);
516
517
  plainServer.on("upgrade", handleUpgrade);
517
518
  const wrapper = net.createServer((socket) => {
519
+ socket.on("error", () => {
520
+ socket.destroy();
521
+ });
518
522
  socket.once("readable", () => {
519
523
  const buf = socket.read(1);
520
524
  if (!buf) {
@@ -745,6 +749,10 @@ function isHttpsEnvEnabled() {
745
749
  const val = process.env.PORTLESS_HTTPS;
746
750
  return val === "1" || val === "true";
747
751
  }
752
+ function isWildcardEnvEnabled() {
753
+ const val = process.env.PORTLESS_WILDCARD;
754
+ return val === "1" || val === "true";
755
+ }
748
756
  async function discoverState() {
749
757
  if (process.env.PORTLESS_STATE_DIR) {
750
758
  const dir = process.env.PORTLESS_STATE_DIR;
@@ -894,16 +902,28 @@ function collectBinPaths(cwd) {
894
902
  return dirs;
895
903
  }
896
904
  function augmentedPath(env) {
897
- const base = (env ?? process.env).PATH ?? "";
905
+ const source = env ?? process.env;
906
+ const base = source.PATH ?? source.Path ?? "";
898
907
  const bins = collectBinPaths(process.cwd());
899
- return bins.length > 0 ? bins.join(path2.delimiter) + path2.delimiter + base : base;
908
+ const nodeBin = path2.dirname(process.execPath);
909
+ const allBins = [...bins, nodeBin];
910
+ return allBins.join(path2.delimiter) + path2.delimiter + base;
900
911
  }
901
912
  function spawnCommand(commandArgs, options) {
902
- const env = { ...options?.env ?? process.env, PATH: augmentedPath(options?.env) };
903
- const child = isWindows2 ? spawn(commandArgs[0], commandArgs.slice(1), {
913
+ const env = {
914
+ ...options?.env ?? process.env,
915
+ PATH: augmentedPath(options?.env)
916
+ };
917
+ if (isWindows2) {
918
+ for (const key of Object.keys(env)) {
919
+ if (key !== "PATH" && key.toUpperCase() === "PATH") {
920
+ delete env[key];
921
+ }
922
+ }
923
+ }
924
+ const child = isWindows2 ? spawn("cmd.exe", ["/d", "/s", "/c", commandArgs.join(" ")], {
904
925
  stdio: "inherit",
905
- env,
906
- shell: true
926
+ env
907
927
  }) : spawn("/bin/sh", ["-c", commandArgs.map(shellEscape).join(" ")], {
908
928
  stdio: "inherit",
909
929
  env
@@ -953,12 +973,38 @@ var FRAMEWORKS_NEEDING_PORT = {
953
973
  "react-native": { strictPort: false },
954
974
  expo: { strictPort: false }
955
975
  };
976
+ var PACKAGE_RUNNERS = {
977
+ npx: [],
978
+ bunx: [],
979
+ pnpx: [],
980
+ yarn: ["dlx", "exec"],
981
+ pnpm: ["dlx", "exec"]
982
+ };
983
+ function findFrameworkBasename(commandArgs) {
984
+ if (commandArgs.length === 0) return null;
985
+ const first = path2.basename(commandArgs[0]);
986
+ if (FRAMEWORKS_NEEDING_PORT[first]) return first;
987
+ const subcommands = PACKAGE_RUNNERS[first];
988
+ if (!subcommands) return null;
989
+ let i = 1;
990
+ if (subcommands.length > 0) {
991
+ while (i < commandArgs.length && commandArgs[i].startsWith("-")) i++;
992
+ if (i >= commandArgs.length) return null;
993
+ if (!subcommands.includes(commandArgs[i])) {
994
+ const name2 = path2.basename(commandArgs[i]);
995
+ return FRAMEWORKS_NEEDING_PORT[name2] ? name2 : null;
996
+ }
997
+ i++;
998
+ }
999
+ while (i < commandArgs.length && commandArgs[i].startsWith("-")) i++;
1000
+ if (i >= commandArgs.length) return null;
1001
+ const name = path2.basename(commandArgs[i]);
1002
+ return FRAMEWORKS_NEEDING_PORT[name] ? name : null;
1003
+ }
956
1004
  function injectFrameworkFlags(commandArgs, port) {
957
- const cmd = commandArgs[0];
958
- if (!cmd) return;
959
- const basename2 = path2.basename(cmd);
1005
+ const basename2 = findFrameworkBasename(commandArgs);
1006
+ if (!basename2) return;
960
1007
  const framework = FRAMEWORKS_NEEDING_PORT[basename2];
961
- if (!framework) return;
962
1008
  if (!commandArgs.includes("--port")) {
963
1009
  commandArgs.push("--port", port.toString());
964
1010
  if (framework.strictPort) {
@@ -1193,6 +1239,7 @@ export {
1193
1239
  writeTldFile,
1194
1240
  getDefaultTld,
1195
1241
  isHttpsEnvEnabled,
1242
+ isWildcardEnvEnabled,
1196
1243
  discoverState,
1197
1244
  findFreePort,
1198
1245
  isProxyRunning,