Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(dev): better networking options #6724

Merged
merged 5 commits into from
Jul 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/dry-items-deliver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@remix-run/dev": minor
"@remix-run/react": minor
"@remix-run/server-runtime": minor
---

improved networking options for v2_dev

deprecate the `--scheme` and `--host` options and replace them with the `REMIX_DEV_ORIGIN` environment variable
67 changes: 46 additions & 21 deletions docs/other-api/dev-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,24 +136,23 @@ but CloudFlare does not support async I/O like `fetch` outside of request handli

Options priority order is: 1. flags, 2. config, 3. defaults.

| Option | flag | config | default |
| --------------- | ------------------ | ---------------- | ------------------------------------------------- |
| Command | `-c` / `--command` | `command` | `remix-serve <server build path>` |
| No restart | `--no-restart` | `restart: false` | `restart: true` |
| Scheme | `--scheme` | `scheme` | `https` if TLS key/cert are set, otherwise `http` |
| Host | `--host` | `host` | `localhost` |
| Port | `--port` | `port` | Dynamically chosen open port |
| TLS key | `--tls-key` | `tlsKey` | N/A |
| TLS certificate | `--tls-cert` | `tlsCert` | N/A |
| Option | flag | config | default |
| --------------- | ------------------ | ---------------- | --------------------------------- |
| Command | `-c` / `--command` | `command` | `remix-serve <server build path>` |
| No restart | `--no-restart` | `restart: false` | `restart: true` |
| Port | `--port` | `port` | Dynamically chosen open port |
| TLS key | `--tls-key` | `tlsKey` | N/A |
| TLS certificate | `--tls-cert` | `tlsCert` | N/A |

<docs-info>

The scheme/host/port options only affect the Remix dev server, and **do not affect your app server**.
The port option only affects the Remix dev server, and **does not affect your app server**.
Your app will run on your app server's normal URL.

You most likely won't want to configure the scheme/host/port for the dev server,
as those are implementation details used internally for hot updates.
They exist in case you need fine-grain control, for example Docker networking or using specific open ports.
You probably don't want to configure the port for the dev server,
as it is an implementation detail used internally for hot updates.
The port option exists in case you need fine-grain networking control,
for example to setup Docker networking or use a specific open port for security purposes.

</docs-info>

Expand Down Expand Up @@ -244,9 +243,11 @@ To use [Mock Service Worker][msw] in development, you'll need to:
1. Run MSW as part of your app server
2. Configure MSW to not mock internal "dev ready" messages to the dev server

`remix dev` will provide the `REMIX_DEV_ORIGIN` environment variable for use in your app server.

For example, if you are using [binode][binode] to integrate with MSW,
make sure that the call to `binode` is within the `remix dev -c` subcommand.
That way, the MSW server will have access to the `REMIX_DEV_HTTP_ORIGIN` environment variable:
That way, the MSW server will have access to the `REMIX_DEV_ORIGIN` environment variable:

```json filename=package.json
{
Expand All @@ -257,16 +258,16 @@ That way, the MSW server will have access to the `REMIX_DEV_HTTP_ORIGIN` environ
}
```

Next, you can use `REMIX_DEV_HTTP_ORIGIN` to let MSW forward internal "dev ready" messages on `/ping`:
Next, you can use `REMIX_DEV_ORIGIN` to let MSW forward internal "dev ready" messages on `/ping`:

```ts
import { rest } from "msw";

let REMIX_DEV_PING = new URL(process.env.REMIX_DEV_ORIGIN);
REMIX_DEV_PING.pathname = "/ping";

export const server = setupServer(
rest.post(
`${process.env.REMIX_DEV_HTTP_ORIGIN}/ping`,
(req) => req.passthrough()
)
rest.post(REMIX_DEV_PING.href, (req) => req.passthrough())
// ... other request handlers go here ...
);
```
Expand Down Expand Up @@ -338,6 +339,31 @@ remix dev --tls-key=key.pem --tls-cert=cert.pem -c 'node ./server.js'
Alternatively, you can specify the TLS key and cert via the `v2_dev.tlsCert` and `v2_dev.tlsKey` config options.
Now your app server and dev server are TLS ready!

### How to integrate with a reverse proxy

Let's say you have the app server and dev server both running on the same machine:

- App server 👉 `http://localhost:1234`
- Dev server 👉 `http://localhost:5678`

Then, you setup a reverse proxy in front of the app server and dev server:

- Reverse proxy 👉 `https://myhost`

But the internal HTTP and WebSocket connections to support hot updates will still try to reach the dev server's unproxied origin:

- Hot updates 👉 `http://localhost:5678` / `ws://localhost:5678` ❌

To get the internal connections to point to the reverse proxy, you can use the `REMIX_DEV_ORIGIN` environment variable:

```sh
REMIX_DEV_ORIGIN=https://myhost remix dev
```

Now, hot updates will be sent correctly to the proxy:

- Hot updates 👉 `https://myhost` / `wss://myhost` ✅

### Troubleshooting

#### HMR: hot updates losing app state
Expand All @@ -346,8 +372,7 @@ Hot Module Replacement is supposed to keep your app's state around between hot u
But in some cases React cannot distinguish between existing components being changed and new components being added.
[React needs `key`s][react-keys] to disambiguate these cases and track changes when sibling elements are modified.

Additionally, when adding or removing hooks, React Refresh treats that as a brand new component.
So if you add `useLoaderData` to your component, you may lose state local to that component.
Additionally, when adding or removing hooks, React Refresh treats that as a brand new component. So if you add `useLoaderData` to your component, you may lose state local to that component.

These are limitations of React and [React Refresh][react-refresh], not Remix.

Expand Down
2 changes: 0 additions & 2 deletions packages/remix-dev/__tests__/cli-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,6 @@ describe("remix CLI", () => {

[v2_dev]
--command, -c Command used to run your app server
--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.
--tls-key Path to TLS key (key.pem)
Expand Down
88 changes: 48 additions & 40 deletions packages/remix-dev/cli/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,8 @@ export async function build(
sourcemap,
};
if (mode === "development" && config.future.v2_dev) {
let origin = await resolveDevOrigin(config);
options.devOrigin = origin;
let resolved = await resolveDev(config);
options.REMIX_DEV_ORIGIN = resolved.REMIX_DEV_ORIGIN;
}

let fileWatchCache = createFileWatchCache();
Expand Down Expand Up @@ -238,7 +238,8 @@ export async function dev(
return await new Promise(() => {});
}

await devServer_unstable.serve(config, await resolveDevServe(config, flags));
let resolved = await resolveDevServe(config, flags);
await devServer_unstable.serve(config, resolved);
}

export async function codemod(
Expand Down Expand Up @@ -467,58 +468,72 @@ let parseMode = (

let findPort = async () => getPort({ port: makeRange(3001, 3100) });

type DevOrigin = {
scheme: string;
host: string;
port: number;
};
let resolveDevOrigin = async (
let resolveDev = async (
config: RemixConfig,
flags: Partial<DevOrigin> & {
flags: {
port?: number;
tlsKey?: string;
tlsCert?: string;
/** @deprecated */
scheme?: string; // TODO: remove in v2
/** @deprecated */
host?: string; // TODO: remove in v2
} = {}
): Promise<DevOrigin> => {
) => {
let dev = config.future.v2_dev;
if (dev === false) throw Error("This should never happen");

// prettier-ignore
let scheme =
flags.scheme ??
(dev === true ? undefined : dev.scheme) ??
(flags.tlsKey && flags.tlsCert) ? "https": "http";
// prettier-ignore
let host =
flags.host ??
(dev === true ? undefined : dev.host) ??
"localhost";
// prettier-ignore
let port =
flags.port ??
(dev === true ? undefined : dev.port) ??
(await findPort());

let tlsKey = flags.tlsKey ?? (dev === true ? undefined : dev.tlsKey);
if (tlsKey) tlsKey = path.resolve(tlsKey);
let tlsCert = flags.tlsCert ?? (dev === true ? undefined : dev.tlsCert);
if (tlsCert) tlsCert = path.resolve(tlsCert);
let isTLS = tlsKey && tlsCert;

let REMIX_DEV_ORIGIN = process.env.REMIX_DEV_ORIGIN;
if (REMIX_DEV_ORIGIN === undefined) {
// prettier-ignore
let scheme =
flags.scheme ?? // TODO: remove in v2
(dev === true ? undefined : dev.scheme) ?? // TODO: remove in v2
isTLS ? "https" : "http";
// prettier-ignore
let hostname =
flags.host ?? // TODO: remove in v2
(dev === true ? undefined : dev.host) ?? // TODO: remove in v2
"localhost";
REMIX_DEV_ORIGIN = `${scheme}://${hostname}:${port}`;
}

return {
scheme,
host,
port,
tlsKey,
tlsCert,
REMIX_DEV_ORIGIN: new URL(REMIX_DEV_ORIGIN),
};
};

type DevServeFlags = DevOrigin & {
command?: string;
restart: boolean;
tlsKey?: string;
tlsCert?: string;
};
let resolveDevServe = async (
config: RemixConfig,
flags: Partial<DevServeFlags> = {}
): Promise<DevServeFlags> => {
flags: {
port?: number;
tlsKey?: string;
tlsCert?: string;
scheme?: string; // TODO: remove in v2
host?: string; // TODO: remove in v2
command?: string;
restart?: boolean;
} = {}
) => {
let dev = config.future.v2_dev;
if (dev === false) throw Error("Cannot resolve dev options");

let origin = await resolveDevOrigin(config, flags);
let resolved = await resolveDev(config, flags);

// prettier-ignore
let command =
Expand All @@ -528,16 +543,9 @@ let resolveDevServe = async (
let restart =
flags.restart ?? (dev === true ? undefined : dev.restart) ?? true;

let tlsKey = flags.tlsKey ?? (dev === true ? undefined : dev.tlsKey);
if (tlsKey) tlsKey = path.resolve(tlsKey);
let tlsCert = flags.tlsCert ?? (dev === true ? undefined : dev.tlsCert);
if (tlsCert) tlsCert = path.resolve(tlsCert);

return {
...resolved,
command,
...origin,
restart,
tlsKey,
tlsCert,
};
};
28 changes: 24 additions & 4 deletions packages/remix-dev/cli/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as colors from "../colors";
import * as commands from "./commands";
import { validateNewProjectPath, validateTemplate } from "./create";
import { detectPackageManager } from "./detectPackageManager";
import { logger } from "../tux";

const helpText = `
${colors.logoBlue("R")} ${colors.logoGreen("E")} ${colors.logoYellow(
Expand Down Expand Up @@ -44,8 +45,6 @@ ${colors.logoBlue("R")} ${colors.logoGreen("E")} ${colors.logoYellow(

[v2_dev]
--command, -c Command used to run your app server
--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.
--tls-key Path to TLS key (key.pem)
Expand Down Expand Up @@ -183,13 +182,15 @@ export async function run(argv: string[] = process.argv.slice(2)) {
// dev server
"--command": String,
"-c": "--command",
"--scheme": String,
"--host": String,
"--port": Number,
"-p": "--port",
"--no-restart": Boolean,
"--tls-key": String,
"--tls-cert": String,

// deprecated, remove in v2
"--scheme": String,
"--host": String,
},
{
argv,
Expand All @@ -214,6 +215,25 @@ export async function run(argv: string[] = process.argv.slice(2)) {
return;
}

// TODO: remove in v2
if (flags["scheme"]) {
logger.warn("`--scheme` flag is deprecated", {
details: [
"Use `REMIX_DEV_ORIGIN` instead",
"-> https://remix.run/docs/en/main/other-api/dev-v2#how-to-integrate-with-a-reverse-proxy",
],
});
}
// TODO: remove in v2
if (flags["host"]) {
logger.warn("`--host` flag is deprecated", {
details: [
"Use `REMIX_DEV_ORIGIN` instead",
"-> https://remix.run/docs/en/main/other-api/dev-v2#how-to-integrate-with-a-reverse-proxy",
],
});
}

if (flags["tls-key"]) {
flags.tlsKey = flags["tls-key"];
delete flags["tls-key"];
Expand Down
4 changes: 4 additions & 0 deletions packages/remix-dev/compiler/js/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ const createEsbuildConfig = (
publicPath: ctx.config.publicPath,
define: {
"process.env.NODE_ENV": JSON.stringify(ctx.options.mode),
"process.env.REMIX_DEV_ORIGIN": JSON.stringify(
ctx.options.REMIX_DEV_ORIGIN ?? ""
),
// TODO: remove in v2
"process.env.REMIX_DEV_SERVER_WS_PORT": JSON.stringify(
ctx.config.devServerPort
),
Expand Down
7 changes: 1 addition & 6 deletions packages/remix-dev/compiler/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,5 @@ export type Options = {
mode: Mode;
sourcemap: boolean;

// TODO: required in v2
devOrigin?: {
scheme: string;
host: string;
port: number;
};
REMIX_DEV_ORIGIN?: URL; // TODO: required in v2
};
8 changes: 6 additions & 2 deletions packages/remix-dev/compiler/server/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,16 @@ const createEsbuildConfig = (
publicPath: ctx.config.publicPath,
define: {
"process.env.NODE_ENV": JSON.stringify(ctx.options.mode),
// TODO: remove REMIX_DEV_SERVER_WS_PORT in v2
// TODO: remove in v2
"process.env.REMIX_DEV_SERVER_WS_PORT": JSON.stringify(
ctx.config.devServerPort
),
"process.env.REMIX_DEV_ORIGIN": JSON.stringify(
ctx.options.REMIX_DEV_ORIGIN ?? ""
),
// TODO: remove in v2
"process.env.REMIX_DEV_HTTP_ORIGIN": JSON.stringify(
ctx.options.devOrigin ?? "" // TODO: remove nullish check in v2
ctx.options.REMIX_DEV_ORIGIN ?? ""
),
},
jsx: "automatic",
Expand Down
7 changes: 0 additions & 7 deletions packages/remix-dev/compiler/server/plugins/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,6 @@ ${Object.keys(config.routes)
export const future = ${JSON.stringify(config.future)};
export const publicPath = ${JSON.stringify(config.publicPath)};
export const entry = { module: entryServer };
${
options.devOrigin
? `export const dev = ${JSON.stringify({
port: options.devOrigin.port,
})}`
: ""
}
export const routes = {
${Object.keys(config.routes)
.map((key, index) => {
Expand Down
Loading