Repository: remix-run/remix (ts)

Issue:
<LiveReload port={12345} /> doesn't change port
### What version of Remix are you using?

1.16

### Are all your remix dependencies & dev-dependencies using the same version?

- [X] Yes

### Steps to Reproduce

<LiveReload port={12345} />

### Expected Behavior

port must change

### Actual Behavior

port is always taken from remix.config file instead of supplied port to component


### Findings

https://github.com/remix-run/remix/blob/ea7ab3adf1cc38a3ee40261f8d40d9b2535455ba/packages/remix-react/components.tsx#L1740

here supplied port is grabbed last but `window.__remixContext.dev.websocketPort` this is always set so port supplied to component is ignored

Pull request description:
feat(dev): reuse dev server port for websocket
Fixes #6319 #6419 

Patch:
diff --git a/.changeset/wicked-pandas-give.md b/.changeset/wicked-pandas-give.md
new file mode 100644
index 000000000..0a998218e
--- /dev/null
+++ b/.changeset/wicked-pandas-give.md
@@ -0,0 +1,23 @@
+---
+"@remix-run/dev": minor
+"@remix-run/react": minor
+"@remix-run/server-runtime": minor
+---
+
+Reuse dev server port for WebSocket (Live Reload,HMR,HDR)
+
+As a result the `webSocketPort`/`--websocket-port` option has been obsoleted.
+Additionally, scheme/host/port options for the dev server have been renamed.
+
+Available options are:
+
+| Option         | flag               | config           | default                           |
+| -------------- | ------------------ | ---------------- | --------------------------------- |
+| Command        | `-c` / `--command` | `command`        | `remix-serve <server build path>` |
+| Scheme         | `--scheme`         | `scheme`         | `http`                            |
+| Host           | `--host`           | `host`           | `localhost`                       |
+| Port           | `--port`           | `port`           | Dynamically chosen open port      |
+| No restart     | `--no-restart`     | `restart: false` | `restart: true`                   |
+
+Note that scheme/host/port options are for the _dev server_, not your app server.
+You probably don't need to use scheme/host/port option if you aren't configuring networking (e.g. for Docker or SSL).
diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts
index 200a7a81d..95bec84c0 100644
--- a/packages/remix-dev/cli/commands.ts
+++ b/packages/remix-dev/cli/commands.ts
@@ -177,13 +177,8 @@ export async function build(
     onWarning: warnOnce,
   };
   if (mode === "development" && config.future.unstable_dev) {
-    let dev = await resolveDevBuild(config);
-    options.devHttpOrigin = {
-      scheme: dev.httpScheme,
-      host: dev.httpHost,
-      port: dev.httpPort,
-    };
-    options.devWebSocketPort = dev.webSocketPort;
+    let origin = await resolveDevOrigin(config);
+    options.devOrigin = origin;
   }
 
   fse.emptyDirSync(config.assetsBuildDirectory);
@@ -216,20 +211,20 @@ export async function dev(
   remixRoot: string,
   flags: {
     debug?: boolean;
-    port?: number; // TODO: remove for v2
 
     // unstable_dev
     command?: string;
-    httpScheme?: string;
-    httpHost?: string;
-    httpPort?: number;
+    scheme?: string;
+    host?: string;
+    port?: number;
     restart?: boolean;
-    websocketPort?: number;
   } = {}
 ) {
   if (process.env.NODE_ENV && process.env.NODE_ENV !== "development") {
     console.warn(
-      `Forcing NODE_ENV to be 'development'. Was: ${process.env.NODE_ENV}`
+      `Forcing NODE_ENV to be 'development'. Was: ${JSON.stringify(
+        process.env.NODE_ENV
+      )}`
     );
   }
   process.env.NODE_ENV = "development";
@@ -471,49 +466,42 @@ let parseMode = (
 
 let findPort = async () => getPort({ port: makeRange(3001, 3100) });
 
-type DevBuildFlags = {
-  httpScheme: string;
-  httpHost: string;
-  httpPort: number;
-  webSocketPort: number;
+type DevOrigin = {
+  scheme: string;
+  host: string;
+  port: number;
 };
-let resolveDevBuild = async (
+let resolveDevOrigin = async (
   config: RemixConfig,
-  flags: Partial<DevBuildFlags> = {}
-): Promise<DevBuildFlags> => {
+  flags: Partial<DevOrigin> = {}
+): Promise<DevOrigin> => {
   let dev = config.future.unstable_dev;
   if (dev === false) throw Error("This should never happen");
 
   // prettier-ignore
-  let httpScheme =
-    flags.httpScheme ??
-    (dev === true ? undefined : dev.httpScheme) ??
+  let scheme =
+    flags.scheme ??
+    (dev === true ? undefined : dev.scheme) ??
     "http";
   // prettier-ignore
-  let httpHost =
-    flags.httpHost ??
-    (dev === true ? undefined : dev.httpHost) ??
+  let host =
+    flags.host ??
+    (dev === true ? undefined : dev.host) ??
     "localhost";
   // prettier-ignore
-  let httpPort =
-    flags.httpPort ??
-    (dev === true ? undefined : dev.httpPort) ??
-    (await findPort());
-  // prettier-ignore
-  let webSocketPort =
-    flags.webSocketPort ??
-    (dev === true ? undefined : dev.webSocketPort) ??
+  let port =
+    flags.port ??
+    (dev === true ? undefined : dev.port) ??
     (await findPort());
 
   return {
-    httpScheme,
-    httpHost,
-    httpPort,
-    webSocketPort,
+    scheme,
+    host,
+    port,
   };
 };
 
-type DevServeFlags = DevBuildFlags & {
+type DevServeFlags = DevOrigin & {
   command: string;
   restart: boolean;
 };
@@ -524,10 +512,7 @@ let resolveDevServe = async (
   let dev = config.future.unstable_dev;
   if (dev === false) throw Error("Cannot resolve dev options");
 
-  let { httpScheme, httpHost, httpPort, webSocketPort } = await resolveDevBuild(
-    config,
-    flags
-  );
+  let origin = await resolveDevOrigin(config, flags);
 
   // prettier-ignore
   let command =
@@ -558,10 +543,7 @@ let resolveDevServe = async (
 
   return {
     command,
-    httpScheme,
-    httpHost,
-    httpPort,
-    webSocketPort,
+    ...origin,
     restart,
   };
 };
diff --git a/packages/remix-dev/cli/run.ts b/packages/remix-dev/cli/run.ts
index 524b1bdc8..f1d8bcde7 100644
--- a/packages/remix-dev/cli/run.ts
+++ b/packages/remix-dev/cli/run.ts
@@ -44,11 +44,10 @@ ${colors.logoBlue("R")} ${colors.logoGreen("E")} ${colors.logoYellow(
 
     [unstable_dev]
     --command, -c       Command used to run your app server
-    --http-scheme       HTTP(S) scheme for the dev server. Default: http
-    --http-host         HTTP(S) host for the dev server. Default: localhost
-    --http-port         HTTP(S) port for the dev server. Default: any open port
+    --scheme            Scheme for the dev server. Default: http
+    --host              Host for the dev server. Default: localhost
+    --port              Port for the dev server. Default: any open port
     --no-restart        Do not restart the app server when rebuilds occur.
-    --websocket-port    WebSocket port for the dev server. Default: any open port
   \`init\` Options:
     --no-delete         Skip deleting the \`remix.init\` script
   \`routes\` Options:
@@ -170,8 +169,6 @@ export async function run(argv: string[] = process.argv.slice(2)) {
       "--interactive": Boolean,
       "--no-interactive": Boolean,
       "--json": Boolean,
-      "--port": Number,
-      "-p": "--port",
       "--remix-version": String,
       "--sourcemap": Boolean,
       "--template": String,
@@ -184,11 +181,11 @@ export async function run(argv: string[] = process.argv.slice(2)) {
       // dev server
       "--command": String,
       "-c": "--command",
-      "--http-scheme": String,
-      "--http-host": String,
-      "--http-port": Number,
+      "--scheme": String,
+      "--host": String,
+      "--port": Number,
+      "-p": "--port",
       "--no-restart": Boolean,
-      "--websocket-port": Number,
     },
     {
       argv,
@@ -213,23 +210,6 @@ export async function run(argv: string[] = process.argv.slice(2)) {
     return;
   }
 
-  if (flags["http-scheme"]) {
-    flags.httpScheme = flags["http-scheme"];
-    delete flags["http-scheme"];
-  }
-  if (flags["http-host"]) {
-    flags.httpHost = flags["http-host"];
-    delete flags["http-host"];
-  }
-  if (flags["http-port"]) {
-    flags.httpPort = flags["http-port"];
-    delete flags["http-port"];
-  }
-  if (flags["websocket-port"]) {
-    flags.webSocketPort = flags["websocket-port"];
-    delete flags["websocket-port"];
-  }
-
   if (args["--no-delete"]) {
     flags.delete = false;
   }
diff --git a/packages/remix-dev/compiler/options.ts b/packages/remix-dev/compiler/options.ts
index 165b89cba..d128116d7 100644
--- a/packages/remix-dev/compiler/options.ts
+++ b/packages/remix-dev/compiler/options.ts
@@ -6,10 +6,9 @@ export type Options = {
   onWarning?: (message: string, key: string) => void;
 
   // TODO: required in v2
-  devHttpOrigin?: {
+  devOrigin?: {
     scheme: string;
     host: string;
     port: number;
   };
-  devWebSocketPort?: number;
 };
diff --git a/packages/remix-dev/compiler/server/compiler.ts b/packages/remix-dev/compiler/server/compiler.ts
index 3dd99e80f..47af42302 100644
--- a/packages/remix-dev/compiler/server/compiler.ts
+++ b/packages/remix-dev/compiler/server/compiler.ts
@@ -103,7 +103,7 @@ const createEsbuildConfig = (
         ctx.config.devServerPort
       ),
       "process.env.REMIX_DEV_HTTP_ORIGIN": JSON.stringify(
-        ctx.options.devHttpOrigin ?? "" // TODO: remove nullish check in v2
+        ctx.options.devOrigin ?? "" // TODO: remove nullish check in v2
       ),
     },
     jsx: "automatic",
diff --git a/packages/remix-dev/compiler/server/plugins/entry.ts b/packages/remix-dev/compiler/server/plugins/entry.ts
index 3d64193e3..4966dd3ad 100644
--- a/packages/remix-dev/compiler/server/plugins/entry.ts
+++ b/packages/remix-dev/compiler/server/plugins/entry.ts
@@ -51,9 +51,9 @@ ${Object.keys(config.routes)
   export const publicPath = ${JSON.stringify(config.publicPath)};
   export const entry = { module: entryServer };
   ${
-    options.devWebSocketPort
+    options.devOrigin
       ? `export const dev = ${JSON.stringify({
-          websocketPort: options.devWebSocketPort,
+          port: options.devOrigin.port,
         })}`
       : ""
   }
diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts
index ed90d95a1..e431e14d9 100644
--- a/packages/remix-dev/config.ts
+++ b/packages/remix-dev/config.ts
@@ -37,15 +37,11 @@ export type ServerModuleFormat = "esm" | "cjs";
 export type ServerPlatform = "node" | "neutral";
 
 type Dev = {
-  port?: number; // TODO: remove in v2
-
   command?: string;
-  httpScheme?: string;
-  httpHost?: string;
-  httpPort?: number;
-  webSocketPort?: number;
+  scheme?: string;
+  host?: string;
+  port?: number;
   restart?: boolean;
-  publicDirectory?: string;
 };
 
 interface FutureConfig {
diff --git a/packages/remix-dev/devServer_unstable/index.ts b/packages/remix-dev/devServer_unstable/index.ts
index e4492d350..01c29d8b7 100644
--- a/packages/remix-dev/devServer_unstable/index.ts
+++ b/packages/remix-dev/devServer_unstable/index.ts
@@ -1,5 +1,6 @@
 import * as path from "node:path";
 import * as stream from "node:stream";
+import * as http from "node:http";
 import fs from "fs-extra";
 import prettyMs from "pretty-ms";
 import execa from "execa";
@@ -41,21 +42,13 @@ export let serve = async (
   initialConfig: RemixConfig,
   options: {
     command: string;
-    httpScheme: string;
-    httpHost: string;
-    httpPort: number;
-    webSocketPort: number;
+    scheme: string;
+    host: string;
+    port: number;
     restart: boolean;
   }
 ) => {
   await loadEnv(initialConfig.rootDirectory);
-  let websocket = Socket.serve({ port: options.webSocketPort });
-  let httpOrigin: Origin = {
-    scheme: options.httpScheme,
-    host: options.httpHost,
-    port: options.httpPort,
-  };
-
   let state: {
     appServer?: execa.ExecaChildProcess;
     manifest?: Manifest;
@@ -65,6 +58,30 @@ export let serve = async (
     prevLoaderHashes?: Record<string, string>;
   } = {};
 
+  let app = express()
+    // handle `broadcastDevReady` messages
+    .use(express.json())
+    .post("/ping", (req, res) => {
+      let { buildHash } = req.body;
+      if (typeof buildHash !== "string") {
+        console.warn(`Unrecognized payload: ${req.body}`);
+        res.sendStatus(400);
+      }
+      if (buildHash === state.manifest?.version) {
+        state.appReady?.ok();
+      }
+      res.sendStatus(200);
+    });
+
+  let server = http.createServer(app);
+  let websocket = Socket.serve(server);
+
+  let origin: Origin = {
+    scheme: options.scheme,
+    host: options.host,
+    port: options.port,
+  };
+
   let bin = await detectBin();
   let startAppServer = (command: string) => {
     console.log(`> ${command}`);
@@ -74,7 +91,7 @@ export let serve = async (
         NODE_ENV: "development",
         PATH:
           bin + (process.platform === "win32" ? ";" : ":") + process.env.PATH,
-        REMIX_DEV_HTTP_ORIGIN: stringifyOrigin(httpOrigin),
+        REMIX_DEV_HTTP_ORIGIN: stringifyOrigin(origin),
       },
       // https://github.com/sindresorhus/execa/issues/433
       windowsHide: false,
@@ -118,8 +135,7 @@ export let serve = async (
         mode: "development",
         sourcemap: true,
         onWarning: warnOnce,
-        devHttpOrigin: httpOrigin,
-        devWebSocketPort: options.webSocketPort,
+        devOrigin: origin,
       },
     },
     {
@@ -198,28 +214,14 @@ export let serve = async (
     }
   );
 
-  let httpServer = express()
-    // handle `broadcastDevReady` messages
-    .use(express.json())
-    .post("/ping", (req, res) => {
-      let { buildHash } = req.body;
-      if (typeof buildHash !== "string") {
-        console.warn(`Unrecognized payload: ${req.body}`);
-        res.sendStatus(400);
-      }
-      if (buildHash === state.manifest?.version) {
-        state.appReady?.ok();
-      }
-      res.sendStatus(200);
-    })
-    .listen(httpOrigin.port, () => {
-      console.log("Remix dev server ready");
-    });
+  server.listen(origin.port, () => {
+    console.log("Remix dev server ready");
+  });
 
   return new Promise(() => {}).finally(async () => {
     await kill(state.appServer);
     websocket.close();
-    httpServer.close();
+    server.close();
     await dispose();
   });
 };
diff --git a/packages/remix-dev/devServer_unstable/socket.ts b/packages/remix-dev/devServer_unstable/socket.ts
index 83ea95fb5..a66ca95a0 100644
--- a/packages/remix-dev/devServer_unstable/socket.ts
+++ b/packages/remix-dev/devServer_unstable/socket.ts
@@ -1,4 +1,5 @@
 import WebSocket from "ws";
+import type { Server as HTTPServer } from "http";
 
 import { type Manifest } from "../manifest";
 import type * as HMR from "./hmr";
@@ -14,8 +15,8 @@ type Message =
 
 type Broadcast = (message: Message) => void;
 
-export let serve = (options: { port: number }) => {
-  let wss = new WebSocket.Server({ port: options.port });
+export let serve = (server: HTTPServer) => {
+  let wss = new WebSocket.Server({ server });
 
   let broadcast: Broadcast = (message) => {
     wss.clients.forEach((client) => {
diff --git a/packages/remix-react/browser.tsx b/packages/remix-react/browser.tsx
index c81a6ed80..821731a3d 100644
--- a/packages/remix-react/browser.tsx
+++ b/packages/remix-react/browser.tsx
@@ -24,7 +24,7 @@ declare global {
     // The number of active deferred keys rendered on the server
     a?: number;
     dev?: {
-      websocketPort?: number;
+      port?: number;
       hmrRuntime?: string;
     };
   };
diff --git a/packages/remix-react/components.tsx b/packages/remix-react/components.tsx
index cd5fbf8e8..661447f5c 100644
--- a/packages/remix-react/components.tsx
+++ b/packages/remix-react/components.tsx
@@ -1714,7 +1714,7 @@ export const LiveReload =
     ? () => null
     : function LiveReload({
         // TODO: remove REMIX_DEV_SERVER_WS_PORT in v2
-        port = Number(process.env.REMIX_DEV_SERVER_WS_PORT || 8002),
+        port,
         timeoutMs = 1000,
         nonce = undefined,
       }: {
@@ -1732,9 +1732,9 @@ export const LiveReload =
                 function remixLiveReloadConnect(config) {
                   let protocol = location.protocol === "https:" ? "wss:" : "ws:";
                   let host = location.hostname;
-                  let port = (window.__remixContext && window.__remixContext.dev && window.__remixContext.dev.websocketPort) || ${String(
-                    port
-                  )};
+                  let port = ${port} || (window.__remixContext && window.__remixContext.dev && window.__remixContext.dev.port) || ${Number(
+                process.env.REMIX_DEV_SERVER_WS_PORT || 8002
+              )};
                   let socketPath = protocol + "//" + host + ":" + port + "/socket";
                   let ws = new WebSocket(socketPath);
                   ws.onmessage = async (message) => {
diff --git a/packages/remix-react/entry.ts b/packages/remix-react/entry.ts
index d2a2ab2f2..cfef8123a 100644
--- a/packages/remix-react/entry.ts
+++ b/packages/remix-react/entry.ts
@@ -10,7 +10,7 @@ export interface RemixContextObject {
   serverHandoffString?: string;
   future: FutureConfig;
   abortDelay?: number;
-  dev?: { websocketPort: number };
+  dev?: { port: number };
 }
 
 // Additional React-Router information needed at runtime, but not hydrated
diff --git a/packages/remix-server-runtime/build.ts b/packages/remix-server-runtime/build.ts
index 4e1b831b1..dfa5e2810 100644
--- a/packages/remix-server-runtime/build.ts
+++ b/packages/remix-server-runtime/build.ts
@@ -15,7 +15,7 @@ export interface ServerBuild {
   publicPath: string;
   assetsBuildDirectory: string;
   future: FutureConfig;
-  dev?: { websocketPort: number };
+  dev?: { port: number };
 }
 
 export interface HandleDocumentRequestFunction {
diff --git a/packages/remix-server-runtime/serverHandoff.ts b/packages/remix-server-runtime/serverHandoff.ts
index edc4d4b30..0ea04f244 100644
--- a/packages/remix-server-runtime/serverHandoff.ts
+++ b/packages/remix-server-runtime/serverHandoff.ts
@@ -20,7 +20,7 @@ export function createServerHandoffString<T>(serverHandoff: {
   // we'd end up including duplicate info
   state: ValidateShape<T, HydrationState>;
   future: FutureConfig;
-  dev?: { websocketPort: number };
+  dev?: { port: number };
 }): string {
   // Uses faster alternative of jsesc to escape data returned from the loaders.
   // This string is inserted directly into the HTML in the `<Scripts>` element.


Test patch:
diff --git a/integration/hmr-log-test.ts b/integration/hmr-log-test.ts
index 1a9fe3af3..54ab8b6c0 100644
--- a/integration/hmr-log-test.ts
+++ b/integration/hmr-log-test.ts
@@ -9,11 +9,7 @@ import { createFixtureProject, css, js, json } from "./helpers/create-fixture";
 
 test.setTimeout(120_000);
 
-let fixture = (options: {
-  appServerPort: number;
-  httpPort: number;
-  webSocketPort: number;
-}) => ({
+let fixture = (options: { appPort: number; devPort: number }) => ({
   files: {
     "remix.config.js": js`
       module.exports = {
@@ -21,8 +17,7 @@ let fixture = (options: {
         tailwind: true,
         future: {
           unstable_dev: {
-            httpPort: ${options.httpPort},
-            webSocketPort: ${options.webSocketPort},
+            port: ${options.devPort},
           },
           v2_routeConvention: true,
           v2_errorBoundary: true,
@@ -80,7 +75,7 @@ let fixture = (options: {
         })
       );
 
-      let port = ${options.appServerPort};
+      let port = ${options.appPort};
       app.listen(port, () => {
         let build = require(BUILD_DIR);
         console.log('✅ app ready: http://localhost:' + port);
@@ -250,12 +245,9 @@ test("HMR", async ({ page }) => {
   });
 
   let portRange = makeRange(4080, 4099);
-  let appServerPort = await getPort({ port: portRange });
-  let httpPort = await getPort({ port: portRange });
-  let webSocketPort = await getPort({ port: portRange });
-  let projectDir = await createFixtureProject(
-    fixture({ appServerPort, httpPort, webSocketPort })
-  );
+  let appPort = await getPort({ port: portRange });
+  let devPort = await getPort({ port: portRange });
+  let projectDir = await createFixtureProject(fixture({ appPort, devPort }));
 
   // spin up dev server
   let dev = execa("npm", ["run", "dev"], { cwd: projectDir });
@@ -270,7 +262,7 @@ test("HMR", async ({ page }) => {
       { timeoutMs: 10_000 }
     );
 
-    await page.goto(`http://localhost:${appServerPort}`, {
+    await page.goto(`http://localhost:${appPort}`, {
       waitUntil: "networkidle",
     });
 
@@ -470,7 +462,7 @@ whatsup
     await page.getByText("Hello, planet").waitFor({ timeout: HMR_TIMEOUT_MS });
     await page.waitForLoadState("networkidle");
 
-    expect(devStderr()).toBe("");
+    let stderr = devStderr();
     let withSyntaxError = `
       import { useLoaderData } from "@remix-run/react";
       export function shouldRevalidate(args) {
@@ -486,9 +478,15 @@ whatsup
       }
     `;
     fs.writeFileSync(indexPath, withSyntaxError);
-    await wait(() => devStderr().includes('Expected ";" but found "efault"'), {
-      timeoutMs: HMR_TIMEOUT_MS,
-    });
+    await wait(
+      () =>
+        devStderr()
+          .replace(stderr, "")
+          .includes('Expected ";" but found "efault"'),
+      {
+        timeoutMs: HMR_TIMEOUT_MS,
+      }
+    );
 
     let withFix = `
       import { useLoaderData } from "@remix-run/react";
diff --git a/integration/hmr-test.ts b/integration/hmr-test.ts
index 6f6b63fd2..45aa771ec 100644
--- a/integration/hmr-test.ts
+++ b/integration/hmr-test.ts
@@ -9,11 +9,7 @@ import { createFixtureProject, css, js, json } from "./helpers/create-fixture";
 
 test.setTimeout(120_000);
 
-let fixture = (options: {
-  appServerPort: number;
-  httpPort: number;
-  webSocketPort: number;
-}) => ({
+let fixture = (options: { appPort: number; devPort: number }) => ({
   files: {
     "remix.config.js": js`
       module.exports = {
@@ -21,8 +17,7 @@ let fixture = (options: {
         tailwind: true,
         future: {
           unstable_dev: {
-            httpPort: ${options.httpPort},
-            webSocketPort: ${options.webSocketPort},
+            port: ${options.devPort},
           },
           v2_routeConvention: true,
           v2_errorBoundary: true,
@@ -69,18 +64,17 @@ let fixture = (options: {
       const app = express();
       app.use(express.static("public", { immutable: true, maxAge: "1y" }));
 
-      const MODE = process.env.NODE_ENV;
       const BUILD_DIR = path.join(process.cwd(), "build");
 
       app.all(
         "*",
         createRequestHandler({
           build: require(BUILD_DIR),
-          mode: MODE,
+          mode: process.env.NODE_ENV,
         })
       );
 
-      let port = ${options.appServerPort};
+      let port = ${options.appPort};
       app.listen(port, () => {
         let build = require(BUILD_DIR);
         console.log('✅ app ready: http://localhost:' + port);
@@ -249,12 +243,9 @@ test("HMR", async ({ page }) => {
   });
 
   let portRange = makeRange(3080, 3099);
-  let appServerPort = await getPort({ port: portRange });
-  let httpPort = await getPort({ port: portRange });
-  let webSocketPort = await getPort({ port: portRange });
-  let projectDir = await createFixtureProject(
-    fixture({ appServerPort, httpPort, webSocketPort })
-  );
+  let appPort = await getPort({ port: portRange });
+  let devPort = await getPort({ port: portRange });
+  let projectDir = await createFixtureProject(fixture({ appPort, devPort }));
 
   // spin up dev server
   let dev = execa("npm", ["run", "dev"], { cwd: projectDir });
@@ -269,7 +260,7 @@ test("HMR", async ({ page }) => {
       { timeoutMs: 10_000 }
     );
 
-    await page.goto(`http://localhost:${appServerPort}`, {
+    await page.goto(`http://localhost:${appPort}`, {
       waitUntil: "networkidle",
     });
 
@@ -469,7 +460,7 @@ whatsup
     await page.getByText("Hello, planet").waitFor({ timeout: HMR_TIMEOUT_MS });
     await page.waitForLoadState("networkidle");
 
-    expect(devStderr()).toBe("");
+    let stderr = devStderr();
     let withSyntaxError = `
       import { useLoaderData } from "@remix-run/react";
       export function shouldRevalidate(args) {
@@ -485,9 +476,15 @@ whatsup
       }
     `;
     fs.writeFileSync(indexPath, withSyntaxError);
-    await wait(() => devStderr().includes('Expected ";" but found "efault"'), {
-      timeoutMs: HMR_TIMEOUT_MS,
-    });
+    await wait(
+      () =>
+        devStderr()
+          .replace(stderr, "")
+          .includes('Expected ";" but found "efault"'),
+      {
+        timeoutMs: HMR_TIMEOUT_MS,
+      }
+    );
 
     let withFix = `
       import { useLoaderData } from "@remix-run/react";
diff --git a/packages/remix-dev/__tests__/cli-test.ts b/packages/remix-dev/__tests__/cli-test.ts
index 0ce720933..5e14cc52a 100644
--- a/packages/remix-dev/__tests__/cli-test.ts
+++ b/packages/remix-dev/__tests__/cli-test.ts
@@ -118,11 +118,10 @@ describe("remix CLI", () => {
 
             [unstable_dev]
             --command, -c       Command used to run your app server
-            --http-scheme       HTTP(S) scheme for the dev server. Default: http
-            --http-host         HTTP(S) host for the dev server. Default: localhost
-            --http-port         HTTP(S) port for the dev server. Default: any open port
+            --scheme            Scheme for the dev server. Default: http
+            --host              Host for the dev server. Default: localhost
+            --port              Port for the dev server. Default: any open port
             --no-restart        Do not restart the app server when rebuilds occur.
-            --websocket-port    WebSocket port for the dev server. Default: any open port
           \`init\` Options:
             --no-delete         Skip deleting the \`remix.init\` script
           \`routes\` Options:
diff --git a/packages/remix-react/__tests__/components-test.tsx b/packages/remix-react/__tests__/components-test.tsx
index b088805a4..6cb621925 100644
--- a/packages/remix-react/__tests__/components-test.tsx
+++ b/packages/remix-react/__tests__/components-test.tsx
@@ -47,14 +47,14 @@ describe("<LiveReload />", () => {
       LiveReload = require("../components").LiveReload;
       let { container } = render(<LiveReload />);
       expect(container.querySelector("script")).toHaveTextContent(
-        "let port = (window.__remixContext && window.__remixContext.dev && window.__remixContext.dev.websocketPort) || 8002;"
+        "let port = undefined || (window.__remixContext && window.__remixContext.dev && window.__remixContext.dev.port) || 8002;"
       );
     });
 
     it("can set the port explicitly", () => {
       let { container } = render(<LiveReload port={4321} />);
       expect(container.querySelector("script")).toHaveTextContent(
-        "let port = (window.__remixContext && window.__remixContext.dev && window.__remixContext.dev.websocketPort) || 4321;"
+        "let port = 4321 || (window.__remixContext && window.__remixContext.dev && window.__remixContext.dev.port) || 8002;"
       );
     });
 
@@ -62,7 +62,7 @@ describe("<LiveReload />", () => {
       process.env.REMIX_DEV_SERVER_WS_PORT = "1234";
       let { container } = render(<LiveReload />);
       expect(container.querySelector("script")).toHaveTextContent(
-        "let port = (window.__remixContext && window.__remixContext.dev && window.__remixContext.dev.websocketPort) || 1234;"
+        "let port = undefined || (window.__remixContext && window.__remixContext.dev && window.__remixContext.dev.port) || 1234;"
       );
     });