#459

Proposal: appPortStrategy and appPortRange, so an app's port can be stable or sequential instead of random

OpenKnat-Devopened 1mo ago

The gap

findFreePort picks a random port in 4000-4999 on every run. The hostname hides that most of the time, but several things still see the port:

  • An SSO bookmark or OAuth redirect URI registered against one localhost:<port>. Strict providers accept only the URIs they were given, and a corporate SSO tile is configured once by an admin.
  • IDE and workspace tools that list listening ports and open localhost:<port> directly (#318).
  • Logs, HAR files and bug reports from agents, where "the app was on 4381 yesterday and 4907 today" makes runs hard to compare.
  • Frameworks that persist the port in their own cache or .env.

appPort pins one fixed port, but every checkout and worktree then collides on it, which is the opposite of what worktrees are for (the shape of #288 and #302 before #447).

Proposal: two app-level keys beside appPort

{ "appPortStrategy": "sequential", "appPortRange": [4200, 4999] }
appPortStrategyPicks
random (default, unchanged)any free port in the range, random first then a scan
stablea port derived from the app's hostname, so one checkout keeps its port across restarts and reboots while different checkouts spread across the range; probes forward on collision
sequentialthe lowest free port, so the first app on a machine gets exactly the bottom of the range, which is what a bookmark or redirect URI registered for one port needs

appPortRange is the inclusive range the strategy picks from, default [4000, 4999]. Both keys work at the top level and in apps entries, so a root can say sequential while one package pins appPort. PORTLESS_APP_PORT_STRATEGY and PORTLESS_APP_PORT_RANGE (min-max) are the env twins and beat the file, the same way PORTLESS_APP_PORT beats appPort.

Two rules that keep it from creating new problems:

  • A fixed port always wins: --app-port, PORTLESS_APP_PORT or appPort skip the strategy. appPort next to appPortStrategy in the same object is rejected when the file is read, since the strategy would have nothing to decide.
  • Ports that registered routes already own are skipped by every strategy. A bottom-first strategy would otherwise hand the same port to two apps started at the same moment, before the first has bound it.

Without either key nothing changes: the existing random-first behaviour is untouched.

Verified

Unit tests for each strategy (bottom port, next port when the bottom is held, same key same port, different keys spread, wraparound, blocked ports and registered ports skipped), the env and config resolution with env winning, and the config validation including the fixed-plus-strategy rejection. CLI tests: two apps started under sequential get the bottom port and the next one; stable gives the same name the same port after a restart; an exported strategy beats the file; a fixed port beats the strategy; the conflicting file is refused before anything runs. End to end with a local build from a git worktree on a feature branch: an Nx app got 4200, Storybook beside it got 4201, both routed through one proxy, banner Using port 4200 (sequential in 4200-4999).

PR follows.

Activity

0

No activity yet.