diff --git a/scripts/lib/dev-share.test.ts b/scripts/lib/dev-share.test.ts index b2dfba3585eb..bfecc525e7ac 100644 --- a/scripts/lib/dev-share.test.ts +++ b/scripts/lib/dev-share.test.ts @@ -26,8 +26,13 @@ const encode = (value: string) => Stream.make(new TextEncoder().encode(value)); * Answers `tailscale status --json` with a valid tailnet name, and lets each * test set the outcome of the `off` (pre-clear) and `serve` calls separately — * they are the same subcommand and are told apart by the trailing `off`. + * `onServeArgs` observes each non-status, non-off invocation. */ -const spawnerLayer = (input: { readonly off?: CallResult; readonly serve?: CallResult }) => +const spawnerLayer = (input: { + readonly off?: CallResult; + readonly serve?: CallResult; + readonly onServeArgs?: (args: ReadonlyArray) => void; +}) => Layer.succeed( ChildProcessSpawner.ChildProcessSpawner, ChildProcessSpawner.make((command) => { @@ -36,7 +41,7 @@ const spawnerLayer = (input: { readonly off?: CallResult; readonly serve?: CallR ? { exitCode: 0 } : args.includes("off") ? (input.off ?? { exitCode: 0 }) - : (input.serve ?? { exitCode: 0 }); + : (input.onServeArgs?.(args), input.serve ?? { exitCode: 0 }); return Effect.succeed( ChildProcessSpawner.makeHandle({ @@ -102,6 +107,19 @@ describe("shareDevServer", () => { }), ); + it.effect("points the serve mapping at the host Vite actually bound", () => + Effect.gen(function* () { + const serveArgs: ReadonlyArray[] = []; + yield* shareDevServer({ webPort: 5788 }).pipe( + Effect.provide(spawnerLayer({ onServeArgs: (args) => serveArgs.push([...args]) })), + ); + + // Vite binds `localhost`, which resolves IPv6-first on some OSes; a + // hardcoded 127.0.0.1 target would answer 502 there. + assert.deepEqual(serveArgs, [["serve", "--bg", "--https=5788", "http://localhost:5788"]]); + }), + ); + // The stale-mapping clear runs before serve, so a failure here leaves the // port serving nothing. Saying only "serve failed" would let an operator // assume their previous mapping survived. diff --git a/scripts/lib/dev-share.ts b/scripts/lib/dev-share.ts index 0f843b3ba91e..f9e781ee6249 100644 --- a/scripts/lib/dev-share.ts +++ b/scripts/lib/dev-share.ts @@ -195,7 +195,16 @@ export const shareDevServer = Effect.fn("devShare.shareDevServer")(function* (in }); } - yield* ensureTailscaleServe({ localPort: input.webPort, servePort: input.webPort }).pipe( + // "localhost", not "127.0.0.1": Vite binds `host: "localhost"` (see + // apps/web/vite.config.ts), which resolves to whichever loopback family this + // OS prefers — ::1 on modern macOS. Naming the same host here lets + // tailscaled resolve it exactly like the dev server did, so the mapping and + // the listener always agree. + yield* ensureTailscaleServe({ + localPort: input.webPort, + servePort: input.webPort, + localHost: "localhost", + }).pipe( Effect.mapError((error) => { const explanation = explainCommandFailure(error); return new DevServeFailedError({