Skip to content

Commit 28cdbf2

Browse files
authored
feat(dev): stabilize v2 dev server (#6615)
1 parent 3c4a6a4 commit 28cdbf2

File tree

20 files changed

+492
-31
lines changed

20 files changed

+492
-31
lines changed

.changeset/clever-ways-love.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@remix-run/dev": minor
3+
"@remix-run/react": minor
4+
"@remix-run/serve": minor
5+
"@remix-run/server-runtime": minor
6+
"@remix-run/testing": minor
7+
---
8+
9+
stabilize v2 dev server

docs/other-api/adapter.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: "@remix-run/{adapter}"
3-
order: 2
3+
order: 3
44
---
55

66
# Server Adapters

docs/other-api/dev-v2.md

+350
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
---
2+
title: "@remix-run/dev CLI (v2)"
3+
order: 2
4+
new: true
5+
---
6+
7+
The Remix CLI comes from the `@remix-run/dev` package. It also includes the compiler. Make sure it is in your `package.json` `devDependencies` so it doesn't get deployed to your server.
8+
9+
To get a full list of available commands and flags, run:
10+
11+
```sh
12+
npx @remix-run/dev -h
13+
```
14+
15+
## `remix build`
16+
17+
Builds your app for production. This command will set `process.env.NODE_ENV` to `production` and minify the output for deployment.
18+
19+
```sh
20+
remix build
21+
```
22+
23+
### Options
24+
25+
| Option | flag | config | default |
26+
| ---------------------------------------- | ------------- | ------ | ------- |
27+
| Generate sourcemaps for production build | `--sourcemap` | N/A | `false` |
28+
29+
## `remix dev`
30+
31+
Builds your app and spins up the Remix dev server alongside your app server.
32+
33+
The dev server will:
34+
35+
1. Set `NODE_ENV` to `development`
36+
2. Watch your app code for changes and trigger rebuilds
37+
3. Restart your app server whenever rebuilds succeed
38+
4. Send code updates to the browser via Live Reload and HMR + Hot Data Revalidation
39+
40+
### With `remix-serve`
41+
42+
Enable the v2 dev server:
43+
44+
```js filename=remix.config.js
45+
module.exports = {
46+
future: {
47+
v2_dev: true,
48+
},
49+
};
50+
```
51+
52+
That's it!
53+
54+
### With custom app server
55+
56+
If you used a template to get started, hopefully it has integration with the v2 dev server out-of-the-box.
57+
If not, you can follow these steps to integrate your project with `v2_dev`:
58+
59+
1. Enable the v2 dev server:
60+
61+
```js filename=remix.config.js
62+
module.exports = {
63+
future: {
64+
v2_dev: true,
65+
},
66+
};
67+
```
68+
69+
2. Replace your dev scripts in `package.json` and use `-c` to specify your app server command:
70+
71+
```json
72+
{
73+
"dev": "remix dev -c 'node ./server.js'"
74+
}
75+
```
76+
77+
3. Ensure `broadcastDevReady` is called when your app server is up and running:
78+
79+
```js filename=server.js lines=[12,25-27]
80+
import path from "node:path";
81+
82+
import { broadcastDevReady } from "@remix-run/node";
83+
import express from "express";
84+
85+
const BUILD_DIR = path.resolve(__dirname, "build");
86+
const build = require(BUILD_DIR);
87+
88+
const app = express();
89+
90+
// ... code for setting up your express app goes here ...
91+
92+
app.all(
93+
"*",
94+
createRequestHandler({
95+
build,
96+
mode: process.env.NODE_ENV,
97+
})
98+
);
99+
100+
const port = 3000;
101+
app.listen(port, () => {
102+
console.log(`👉 http://localhost:${port}`);
103+
104+
if (process.env.NODE_ENV === "development") {
105+
broadcastDevReady(build);
106+
}
107+
});
108+
```
109+
110+
<docs-info>
111+
112+
For CloudFlare, use `logDevReady` instead of `broadcastDevReady`.
113+
114+
Why? `broadcastDevReady` uses `fetch` to send a ready message to the dev server,
115+
but CloudFlare does not support async I/O like `fetch` outside of request handling.
116+
117+
</docs-info>
118+
119+
### Options
120+
121+
Options priority order is: 1. flags, 2. config, 3. defaults.
122+
123+
| Option | flag | config | default |
124+
| --------------- | ------------------ | ---------------- | ------------------------------------------------- |
125+
| Command | `-c` / `--command` | `command` | `remix-serve <server build path>` |
126+
| No restart | `--no-restart` | `restart: false` | `restart: true` |
127+
| Scheme | `--scheme` | `scheme` | `https` if TLS key/cert are set, otherwise `http` |
128+
| Host | `--host` | `host` | `localhost` |
129+
| Port | `--port` | `port` | Dynamically chosen open port |
130+
| TLS key | `--tls-key` | `tlsKey` | N/A |
131+
| TLS certificate | `--tls-cert` | `tlsCert` | N/A |
132+
133+
<docs-info>
134+
135+
The scheme/host/port options only affect the Remix dev server, and **do not affect your app server**.
136+
Your app will run on your app server's normal URL.
137+
138+
You most likely won't want to configure the scheme/host/port for the dev server,
139+
as those are implementation details used internally for hot updates.
140+
They exist in case you need fine-grain control, for example Docker networking or using specific open ports.
141+
142+
</docs-info>
143+
144+
For example, to override the port used by the dev server via config:
145+
146+
```js filename=remix.config.js
147+
module.exports = {
148+
future: {
149+
v2_dev: {
150+
port: 8001,
151+
},
152+
},
153+
};
154+
```
155+
156+
### Keep app server running across rebuilds
157+
158+
By default, the Remix dev server restarts your app server when rebuilds occur.
159+
This is a simple way to ensure that your app server is up-to-date with the latest code changes.
160+
161+
If you'd like to opt-out of this behavior use the `--no-restart` flag:
162+
163+
```sh
164+
remix dev --no-restart -c 'node ./server.js'
165+
```
166+
167+
🚨 BUT that means you are now on the hook for applying changes to your running app server _and_ telling the dev server when those changes have been applied.
168+
169+
> With great power comes great responsibility.
170+
171+
Check out our [templates][templates] for examples on how to use `import` cache busting to apply code changes to your app server while it keeps running.
172+
173+
If you're using CJS but looking at an ESM template, you'll need to swap out `import` cache busting with `require` cache busting:
174+
175+
```diff
176+
- const stat = fs.statSync(BUILD_DIR);
177+
- build = import(BUILD_DIR + "?t=" + stat.mtimeMs);
178+
+ for (const key in require.cache) {
179+
+ if (key.startsWith(BUILD_DIR)) {
180+
+ delete require.cache[key];
181+
+ }
182+
+ }
183+
+ build = require(BUILD_DIR)
184+
```
185+
186+
#### Pick up changes from other packages
187+
188+
If you are using a monorepo, you might want Remix to perform hot updates not only when your app code changes, but whenever you change code in any of your apps dependencies.
189+
190+
For example, you could have a UI library package (`packages/ui`) that is used within your Remix app (`packages/app`).
191+
To pick up changes in `packages/ui`, you can configure [watchPaths][watch-paths] to include your packages.
192+
193+
#### Keep in-memory data and connections across rebuilds
194+
195+
Every time you re-import code to apply changes to your app server, that code will be run.
196+
Rerunning each changed module works great in most cases, but sometimes you want to want to keep stuff around.
197+
198+
For example, it'd be nice if your app only connected to your database once and kept that connection around across rebuilds.
199+
But since the connection is held in-memory, re-imports will wipe those out and cause your app to reconnect.
200+
201+
Luckily, there's a trick to get around this: use `global` as a cache for keeping things in-memory across rebuilds!
202+
Here's a nifty utility adapted from [Jon Jensen's code][jenseng-code] for [his Remix Conf 2023 talk][jenseng-talk]:
203+
204+
```ts filename=app/utils/remember.ts
205+
export function remember<T>(key: string, value: T) {
206+
const g = global as any;
207+
g.__singletons ??= {};
208+
g.__singletons[key] ??= value;
209+
return g.__singletons[key];
210+
}
211+
```
212+
213+
And here's how to use it to keep stuff around across rebuilds:
214+
215+
```ts filename=app/utils/db.server.ts
216+
import { PrismaClient } from "@prisma/client";
217+
218+
import { remember } from "~/utils/remember";
219+
220+
// hard-code a unique key so we can look up the client when this module gets re-imported
221+
export const db = remember("db", new PrismaClient());
222+
```
223+
224+
### How to set up local HTTPS
225+
226+
For this example, let's use [mkcert][mkcert].
227+
After you have it installed, make sure to:
228+
229+
- Create a local Certificate Authority if you haven't already done so
230+
- Use `NODE_EXTRA_CA_CERTS` for Node compatibility
231+
232+
```sh
233+
mkcert -install # create a local CA
234+
export NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem" # tell Node to use our local CA
235+
```
236+
237+
Now, create the TLS key and certificate:
238+
239+
```sh
240+
mkcert -key-file key.pem -cert-file cert.pem localhost
241+
```
242+
243+
👆 You can change `localhost` to something else if you are using custom hostnames.
244+
245+
Next, use the `key.pem` and `cert.pem` to get HTTPS working locally with your app server.
246+
This depends on what you are using for your app server.
247+
For example, here's how you could use HTTPS with an Express server:
248+
249+
```ts filename=server.js
250+
import fs from "node:fs";
251+
import https from "node:https";
252+
import path from "node:path";
253+
254+
import express from "express";
255+
256+
const BUILD_DIR = path.resolve(__dirname, "build");
257+
const build = require(BUILD_DIR);
258+
259+
const app = express();
260+
261+
// ... code setting up your express app goes here ...
262+
263+
let server = https.createServer(
264+
{
265+
key: fs.readFileSync("path/to/key.pem"),
266+
cert: fs.readFileSync("path/to/cert.pem"),
267+
},
268+
app
269+
);
270+
271+
let port = 3000;
272+
server.listen(port, () => {
273+
console.log(`👉 https://localhost:${port}`);
274+
275+
if (process.env.NODE_ENV === "development") {
276+
broadcastDevReady(build);
277+
}
278+
});
279+
```
280+
281+
### Troubleshooting
282+
283+
#### Using MSW with `v2_dev`
284+
285+
The dev server uses the `REMIX_DEV_HTTP_ORIGIN` environment variable to communicate its origin to the app server.
286+
You can use that to mock out the `/ping` endpoint used for hot update coordination:
287+
288+
```ts
289+
import { rest } from "msw";
290+
291+
export const server = setupServer(
292+
rest.post(
293+
`${process.env.REMIX_DEV_HTTP_ORIGIN}/ping`,
294+
(req) => {
295+
return req.passthrough();
296+
}
297+
)
298+
// ... other request handlers go here ...
299+
);
300+
```
301+
302+
#### HMR: hot updates losing app state
303+
304+
Hot Module Replacement is supposed to keep your app's state around between hot updates.
305+
But in some cases React cannot distinguish between existing components being changed and new components being added.
306+
[React needs `key`s][react-keys] to disambiguate these cases and track changes when sibling elements are modified.
307+
308+
Additionally, when adding or removing hooks, React Refresh treats that as a brand new component.
309+
So if you add `useLoaderData` to your component, you may lose state local to that component.
310+
311+
These are limitations of React and [React Refresh][react-refresh], not Remix.
312+
313+
#### HDR: every code change triggers HDR
314+
315+
Hot Data Revalidation detects loader changes by trying to bundle each loader and then fingerprinting the content for each.
316+
It relies on treeshaking to determine whether your changes affect each loader or not.
317+
318+
To ensure that treeshaking can reliably detect changes to loaders, make sure you declare that your app's package is side-effect free:
319+
320+
```json filename=package.json
321+
{
322+
"sideEffects": false
323+
}
324+
```
325+
326+
#### HDR: harmless console errors when loader data is removed
327+
328+
When you delete a loader or remove some of the data being returned by that loader, your app should be hot updated correctly.
329+
But you may notice console errors logged in your browser.
330+
331+
React strict-mode and React Suspense can cause multiple renders when hot updates are applied.
332+
Most of these render correctly, including the final render that is visible to you.
333+
But intermediate renders can sometimes use new loader data with old React components, which is where those errors come from.
334+
335+
We are continuing to investigate the underlying race condition to see if we can smooth that over.
336+
In the meantime, if those console errors bother you, you can refresh the page whenever they occur.
337+
338+
#### HDR: performance
339+
340+
When the v2 dev server builds (and rebuilds) your app, you may notice a slight slowdown as the dev server needs to crawl the dependencies for each loader.
341+
That way the dev server can detect loader changes on rebuilds.
342+
343+
While the initial build slowdown is inherently a cost for HDR, we plan to optimize rebuilds so that there is no perceivable slowdown for HDR rebuilds.
344+
345+
[templates]: https://github.com/remix-run/remix/tree/main/templates
346+
[watch-paths]: https://remix.run/docs/en/1.17.1/file-conventions/remix-config#watchpaths
347+
[jenseng-code]: https://github.com/jenseng/abuse-the-platform/blob/main/app/utils/singleton.ts
348+
[jenseng-talk]: https://www.youtube.com/watch?v=lbzNnN0F67Y
349+
[react-keys]: https://react.dev/learn/rendering-lists#why-does-react-need-keys
350+
[react-refresh]: https://github.com/facebook/react/tree/main/packages/react-refresh

docs/other-api/dev.md

+9
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@ title: "@remix-run/dev (CLI)"
33
order: 1
44
---
55

6+
<docs-warning>
7+
8+
The Remix CLI is changing in v2.
9+
You can prepare for this change at your convenience with the `v2_dev` future flag.
10+
For instructions on making this change see the [v2 guide][v2-guide].
11+
12+
</docs-warning>
13+
614
# Remix CLI
715

816
The Remix CLI comes from the `@remix-run/dev` package. It also includes the compiler. Make sure it is in your `package.json` `devDependencies` so it doesn't get deployed to your server.
@@ -113,3 +121,4 @@ Skip deleting the `remix.init` folder after initialization has been run. Useful
113121
[remix-app-server]: ./serve
114122
[node-inspector]: https://nodejs.org/en/docs/guides/debugging-getting-started
115123
[templates-folder-of-the-remix-repository]: https://github.com/remix-run/remix/tree/main/templates
124+
[v2-guide]: ../pages/v2

docs/pages/api-development-strategy.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ The lifecycle is thus either:
5252

5353
| Flag | Description |
5454
| ------------------------ | --------------------------------------------------------------------- |
55-
| `unstable_dev` | Enable the new development server (including HMR/HDR support) |
55+
| `v2_dev` | Enable the new development server (including HMR/HDR support) |
5656
| `v2_errorBoundary` | Combine `ErrorBoundary`/`CatchBoundary` into a single `ErrorBoundary` |
5757
| `v2_headers` | Leverage ancestor `headers` if children do not export `headers` |
5858
| `v2_meta` | Enable the new API for your `meta` functions |

0 commit comments

Comments
 (0)