From 0b866c43774a0f6eaec7f8bc88bc6ee1743fc084 Mon Sep 17 00:00:00 2001 From: th3nolo Date: Thu, 19 Mar 2026 13:10:47 -0400 Subject: [PATCH] fix(dev): keep host resolution consistent across the dev stack --- apps/desktop/scripts/dev-electron.mjs | 10 +++++++--- apps/web/vite.config.ts | 13 ++++++++++++- scripts/dev-runner.test.ts | 26 +++++++++++++++++++++++++- scripts/dev-runner.ts | 21 +++++++++++++++++++-- turbo.json | 1 + 5 files changed, 64 insertions(+), 7 deletions(-) diff --git a/apps/desktop/scripts/dev-electron.mjs b/apps/desktop/scripts/dev-electron.mjs index 12d4753509..bb1d4d5180 100644 --- a/apps/desktop/scripts/dev-electron.mjs +++ b/apps/desktop/scripts/dev-electron.mjs @@ -5,8 +5,12 @@ import waitOn from "wait-on"; import { desktopDir, resolveElectronPath } from "./electron-launcher.mjs"; -const port = Number(process.env.ELECTRON_RENDERER_PORT ?? 5733); -const devServerUrl = `http://localhost:${port}`; +const rendererPort = Number(process.env.ELECTRON_RENDERER_PORT ?? 5733); +const devServerUrl = process.env.VITE_DEV_SERVER_URL?.trim() || `http://localhost:${rendererPort}`; +const parsedDevServerUrl = new URL(devServerUrl); +const devServerPort = Number( + parsedDevServerUrl.port || (parsedDevServerUrl.protocol === "https:" ? 443 : 80), +); const requiredFiles = [ "dist-electron/main.js", "dist-electron/preload.js", @@ -21,7 +25,7 @@ const restartDebounceMs = 120; const childTreeGracePeriodMs = 1_200; await waitOn({ - resources: [`tcp:${port}`, ...requiredFiles.map((filePath) => `file:${filePath}`)], + resources: [`tcp:${devServerPort}`, ...requiredFiles.map((filePath) => `file:${filePath}`)], }); const childEnv = { ...process.env }; diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 56b138d331..cb0aaca9fc 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -6,6 +6,16 @@ import { defineConfig } from "vite"; import pkg from "./package.json" with { type: "json" }; const port = Number(process.env.PORT ?? 5733); +const DEFAULT_DEV_HOST = "127.0.0.1"; +const isWildcardHost = (host: string): boolean => + host === "0.0.0.0" || host === "::" || host === "[::]"; +const configuredHost = process.env.T3CODE_HOST?.trim(); +const resolvedHost = configuredHost === "[::]" ? "::" : configuredHost; +const bindHost = resolvedHost && resolvedHost !== "localhost" ? resolvedHost : DEFAULT_DEV_HOST; +const hmrHost = + !resolvedHost || resolvedHost === "localhost" || isWildcardHost(resolvedHost) + ? DEFAULT_DEV_HOST + : resolvedHost; const sourcemapEnv = process.env.T3CODE_WEB_SOURCEMAP?.trim().toLowerCase(); const buildSourcemap = @@ -41,6 +51,7 @@ export default defineConfig({ tsconfigPaths: true, }, server: { + host: bindHost, port, strictPort: true, hmr: { @@ -48,7 +59,7 @@ export default defineConfig({ // inside Electron's BrowserWindow. Vite 8 uses console.debug for // connection logs — enable "Verbose" in DevTools to see them. protocol: "ws", - host: "localhost", + host: hmrHost, }, }, build: { diff --git a/scripts/dev-runner.test.ts b/scripts/dev-runner.test.ts index 704a285414..97283a440a 100644 --- a/scripts/dev-runner.test.ts +++ b/scripts/dev-runner.test.ts @@ -67,6 +67,8 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { ]); assert.equal(env.T3CODE_STATE_DIR, defaultStateDir); + assert.equal(env.VITE_WS_URL, "ws://127.0.0.1:3773"); + assert.equal(env.VITE_DEV_SERVER_URL, "http://127.0.0.1:5733"); }), ); @@ -89,7 +91,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { assert.equal(env.T3CODE_STATE_DIR, resolve("/tmp/override-state")); assert.equal(env.T3CODE_PORT, "4222"); - assert.equal(env.VITE_WS_URL, "ws://localhost:4222"); + assert.equal(env.VITE_WS_URL, "ws://127.0.0.1:4222"); assert.equal(env.T3CODE_NO_BROWSER, "1"); assert.equal(env.T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD, "0"); assert.equal(env.T3CODE_LOG_WS_EVENTS, "1"); @@ -98,6 +100,28 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { }), ); + it.effect("formats IPv6 hosts correctly in dev URLs", () => + Effect.gen(function* () { + const env = yield* createDevRunnerEnv({ + mode: "dev", + baseEnv: {}, + serverOffset: 0, + webOffset: 0, + stateDir: undefined, + authToken: undefined, + noBrowser: undefined, + autoBootstrapProjectFromCwd: undefined, + logWebSocketEvents: undefined, + host: "::1", + port: 4222, + devUrl: undefined, + }); + + assert.equal(env.VITE_WS_URL, "ws://[::1]:4222"); + assert.equal(env.VITE_DEV_SERVER_URL, "http://[::1]:5733"); + }), + ); + it.effect("does not force websocket logging on in dev mode when unset", () => Effect.gen(function* () { const env = yield* createDevRunnerEnv({ diff --git a/scripts/dev-runner.ts b/scripts/dev-runner.ts index 3815c4a3b9..3617031c27 100644 --- a/scripts/dev-runner.ts +++ b/scripts/dev-runner.ts @@ -13,6 +13,7 @@ const BASE_SERVER_PORT = 3773; const BASE_WEB_PORT = 5733; const MAX_HASH_OFFSET = 3000; const MAX_PORT = 65535; +const DEFAULT_DEV_HOST = "127.0.0.1"; export const DEFAULT_DEV_STATE_DIR = Effect.map(Effect.service(Path.Path), (path) => path.join(homedir(), ".t3", "dev"), @@ -130,6 +131,21 @@ interface CreateDevRunnerEnvInput { readonly devUrl: URL | undefined; } +const isWildcardHost = (host: string): boolean => + host === "0.0.0.0" || host === "::" || host === "[::]"; + +const formatHostForUrl = (host: string): string => + host.includes(":") && !host.startsWith("[") ? `[${host}]` : host; + +const resolveDevHost = (host: string | undefined): string => { + const trimmedHost = host?.trim(); + if (!trimmedHost || trimmedHost === "localhost" || isWildcardHost(trimmedHost)) { + return DEFAULT_DEV_HOST; + } + + return trimmedHost; +}; + export function createDevRunnerEnv({ mode, baseEnv, @@ -148,14 +164,15 @@ export function createDevRunnerEnv({ const serverPort = port ?? BASE_SERVER_PORT + serverOffset; const webPort = BASE_WEB_PORT + webOffset; const resolvedStateDir = yield* resolveStateDir(stateDir); + const webHost = resolveDevHost(host); const output: NodeJS.ProcessEnv = { ...baseEnv, T3CODE_PORT: String(serverPort), PORT: String(webPort), ELECTRON_RENDERER_PORT: String(webPort), - VITE_WS_URL: `ws://localhost:${serverPort}`, - VITE_DEV_SERVER_URL: devUrl?.toString() ?? `http://localhost:${webPort}`, + VITE_WS_URL: `ws://${formatHostForUrl(webHost)}:${serverPort}`, + VITE_DEV_SERVER_URL: devUrl?.toString() ?? `http://${formatHostForUrl(webHost)}:${webPort}`, T3CODE_STATE_DIR: resolvedStateDir, }; diff --git a/turbo.json b/turbo.json index 671336afae..3cb2cc17f1 100644 --- a/turbo.json +++ b/turbo.json @@ -10,6 +10,7 @@ "T3CODE_PORT", "T3CODE_NO_BROWSER", "T3CODE_STATE_DIR", + "T3CODE_HOST", "T3CODE_AUTH_TOKEN", "T3CODE_DESKTOP_WS_URL" ],