|
| 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 |
0 commit comments