diff --git a/.changeset/cache-regular-stylesheets.md b/.changeset/cache-regular-stylesheets.md deleted file mode 100644 index abab851b14e..00000000000 --- a/.changeset/cache-regular-stylesheets.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@remix-run/dev": patch ---- - -Add caching to regular stylesheet compilation diff --git a/.changeset/chilly-shoes-scream.md b/.changeset/chilly-shoes-scream.md deleted file mode 100644 index e3775b6b088..00000000000 --- a/.changeset/chilly-shoes-scream.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@remix-run/dev": patch ---- - -Rename `Architect (AWS Lambda)` -> `Architect` in the `create-remix` CLI to avoid confusion for other methods of deploying to AWS (i.e., SST) diff --git a/.changeset/clever-ways-love.md b/.changeset/clever-ways-love.md deleted file mode 100644 index b7ffe0a67df..00000000000 --- a/.changeset/clever-ways-love.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@remix-run/dev": minor -"@remix-run/react": minor -"@remix-run/serve": minor -"@remix-run/server-runtime": minor -"@remix-run/testing": minor ---- - -stabilize v2 dev server diff --git a/.changeset/css-bundle-skip-node-polyfills.md b/.changeset/css-bundle-skip-node-polyfills.md deleted file mode 100644 index 8e7ab433e61..00000000000 --- a/.changeset/css-bundle-skip-node-polyfills.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@remix-run/dev": patch ---- - -Improve CSS bundle build performance by skipping unused Node polyfills diff --git a/.changeset/css-bundle-skip-packages.md b/.changeset/css-bundle-skip-packages.md deleted file mode 100644 index 9afad9e2e9d..00000000000 --- a/.changeset/css-bundle-skip-packages.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@remix-run/dev": patch ---- - -Improve performance of CSS bundle build by skipping compilation of Remix/React packages that are known not to contain CSS imports diff --git a/.changeset/css-side-effect-imports-hmr-cache.md b/.changeset/css-side-effect-imports-hmr-cache.md deleted file mode 100644 index 8bfede70936..00000000000 --- a/.changeset/css-side-effect-imports-hmr-cache.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@remix-run/dev": patch ---- - -Cache CSS side-effect imports transform when using HMR diff --git a/.changeset/dry-items-deliver.md b/.changeset/dry-items-deliver.md new file mode 100644 index 00000000000..b4cc90a3d91 --- /dev/null +++ b/.changeset/dry-items-deliver.md @@ -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 diff --git a/.changeset/dual-forward-bug.md b/.changeset/dual-forward-bug.md deleted file mode 100644 index d66edae9327..00000000000 --- a/.changeset/dual-forward-bug.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@remix-run/react": patch ---- - -Detect mismatches between the initially loaded URL and the URL at the time we hydrate and trigger a hard reload if they do not match. This is an edge-case that can happen when the network is slowish and the user clicks forward into a Remix app and then clicks forward again while the initial JS chunks are loading. diff --git a/.changeset/famous-games-film.md b/.changeset/famous-games-film.md new file mode 100644 index 00000000000..2cfe70daf40 --- /dev/null +++ b/.changeset/famous-games-film.md @@ -0,0 +1,5 @@ +--- +"@remix-run/dev": patch +--- + +ignore missing react-dom/client for react 17 diff --git a/.changeset/five-zoos-fix.md b/.changeset/five-zoos-fix.md deleted file mode 100644 index 1963e3f1ee7..00000000000 --- a/.changeset/five-zoos-fix.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@remix-run/dev": patch ---- - -Fix bug with pathless layout routes beneath nested path segments diff --git a/.changeset/fix-react-types.md b/.changeset/fix-react-types.md deleted file mode 100644 index 1516e515817..00000000000 --- a/.changeset/fix-react-types.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@remix-run/server-runtime": patch ---- - -Fix typing issues when using React 17 stemming from `@remix/server-runtime` including `@types/react` as a `devDependency` when it doesn't actually do anything React-specific and was just re-exporting `ComponentType` in values such as `CatchBoundaryComponent`/`ErrorBoundaryComponent`/`V2_ErrorBoundaryComponent`. These types are more correctly exported from `@remix-run/react` which is React-aware so that is the correct place to be importing those types from. In order to avoid breaking existing builds, the types in `@remix/server-runtime` have been loosened to `any` and `@deprecated` warnings have been added informing users to switch to the corresponding types in `@remix-run/react`. diff --git a/.changeset/hydrate-error-type.md b/.changeset/hydrate-error-type.md new file mode 100644 index 00000000000..2dd2052a75c --- /dev/null +++ b/.changeset/hydrate-error-type.md @@ -0,0 +1,6 @@ +--- +"@remix-run/react": patch +"@remix-run/server-runtime": patch +--- + +Support proper hydration of `Error` subclasses such as `ReferenceError`/`TypeError` in development mode diff --git a/.changeset/json-text-encoding.md b/.changeset/json-text-encoding.md deleted file mode 100644 index 2def7b4345f..00000000000 --- a/.changeset/json-text-encoding.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@remix-run/react": minor ---- - -Support `application/json` and `text/plain` submission encodings in `useSubmit`/`fetcher.submit` diff --git a/.changeset/kind-coins-attack.md b/.changeset/kind-coins-attack.md new file mode 100644 index 00000000000..73d23ab1b79 --- /dev/null +++ b/.changeset/kind-coins-attack.md @@ -0,0 +1,5 @@ +--- +"@remix-run/dev": patch +--- + +ignore errors when killing already dead processes diff --git a/.changeset/neat-poets-scream.md b/.changeset/neat-poets-scream.md deleted file mode 100644 index 282498d5a26..00000000000 --- a/.changeset/neat-poets-scream.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"remix": patch -"@remix-run/serve": patch -"@remix-run/server-runtime": patch ---- - -fix(types): better tuple serialization types diff --git a/.changeset/nine-islands-mate.md b/.changeset/nine-islands-mate.md new file mode 100644 index 00000000000..a031e7da1db --- /dev/null +++ b/.changeset/nine-islands-mate.md @@ -0,0 +1,5 @@ +--- +"@remix-run/dev": patch +--- + +fix sourcemaps for v2_dev diff --git a/.changeset/orange-beds-explode.md b/.changeset/orange-beds-explode.md deleted file mode 100644 index 5fa009adcd8..00000000000 --- a/.changeset/orange-beds-explode.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@remix-run/dev": minor ---- - -improved logging for `remix build` and `remix dev` diff --git a/.changeset/postcss-cache-css-modules.md b/.changeset/postcss-cache-css-modules.md deleted file mode 100644 index a2f467531ec..00000000000 --- a/.changeset/postcss-cache-css-modules.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@remix-run/dev": patch ---- - -Add caching to PostCSS for CSS Modules diff --git a/.changeset/postcss-cache-side-effect-imports.md b/.changeset/postcss-cache-side-effect-imports.md deleted file mode 100644 index 36ff0756e9b..00000000000 --- a/.changeset/postcss-cache-side-effect-imports.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@remix-run/dev": patch ---- - -Add caching to PostCSS for side-effect imports diff --git a/.changeset/prefetch-viewport.md b/.changeset/prefetch-viewport.md deleted file mode 100644 index 139e10676d2..00000000000 --- a/.changeset/prefetch-viewport.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"remix": minor -"@remix-run/react": minor ---- - -Add support for `` to prefetch links when they enter the viewport via an [Intersection Observer](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) diff --git a/.changeset/red-bananas-design.md b/.changeset/red-bananas-design.md new file mode 100644 index 00000000000..43dd88ff405 --- /dev/null +++ b/.changeset/red-bananas-design.md @@ -0,0 +1,13 @@ +--- +"@remix-run/dev": minor +--- + +Output esbuild metafiles for bundle analysis + +Written to server build directory (`build/` by default): + +- `metafile.css.json` +- `metafile.js.json` (browser JS) +- `metafile.server.json` (server JS) + +Metafiles can be uploaded to https://esbuild.github.io/analyze/ for analysis. diff --git a/.changeset/rude-meals-move.md b/.changeset/rude-meals-move.md deleted file mode 100644 index ce5b4c112eb..00000000000 --- a/.changeset/rude-meals-move.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@remix-run/dev": patch ---- - -cache getRouteModuleExports calls to significantly speed up build and HMR rebuild times diff --git a/.changeset/tasty-bats-turn.md b/.changeset/tasty-bats-turn.md new file mode 100644 index 00000000000..6e2319859e3 --- /dev/null +++ b/.changeset/tasty-bats-turn.md @@ -0,0 +1,11 @@ +--- +"@remix-run/dev": patch +--- + +Do not clear screen when dev server starts + +On some terminal emulators, "clearing" only scrolls the next line to the +top. on others, it erases the scrollback. + +Instead, let users call `clear` themselves (`clear && remix dev`) if +they want to clear. diff --git a/.changeset/thin-rings-sparkle.md b/.changeset/thin-rings-sparkle.md deleted file mode 100644 index 6f01d8bf645..00000000000 --- a/.changeset/thin-rings-sparkle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@remix-run/dev": patch ---- - -group rebuild logs with surrounding whitespace diff --git a/.changeset/two-cobras-float.md b/.changeset/two-cobras-float.md deleted file mode 100644 index 108d59b7605..00000000000 --- a/.changeset/two-cobras-float.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@remix-run/react": patch ---- - -properly pass props to inline script tags for deferred data diff --git a/.changeset/type-userouteloaderdata.md b/.changeset/type-userouteloaderdata.md new file mode 100644 index 00000000000..d6aa1b2061d --- /dev/null +++ b/.changeset/type-userouteloaderdata.md @@ -0,0 +1,5 @@ +--- +"@remix-run/react": patch +--- + +Add generic type for `useRouteLoaderData()` diff --git a/.changeset/types-cookie.md b/.changeset/types-cookie.md deleted file mode 100644 index 069abbd2310..00000000000 --- a/.changeset/types-cookie.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@remix-run/server-runtime": patch ---- - -Move `@types/cookie` to `dependencies` since we re-export types from there diff --git a/.changeset/typescript-eslint-recommended.md b/.changeset/typescript-eslint-recommended.md deleted file mode 100644 index 84c8ccac0ff..00000000000 --- a/.changeset/typescript-eslint-recommended.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@remix-run/eslint-config": minor ---- - -Update `@remix-run/eslint-config` to inherit rules from `@typescript-eslint/recommended` diff --git a/.changeset/update-polyfills-plugin.md b/.changeset/update-polyfills-plugin.md deleted file mode 100644 index 90d8441a3a8..00000000000 --- a/.changeset/update-polyfills-plugin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@remix-run/dev": patch ---- - -Update minimum version of `esbuild-plugins-node-modules-polyfill` to 1.0.16 to ensure that the plugin is cached diff --git a/contributors.yml b/contributors.yml index cc11f989e3f..ecb1d1a4cca 100644 --- a/contributors.yml +++ b/contributors.yml @@ -417,6 +417,7 @@ - plastic041 - plondon - pmbanugo +- pratikdevdas - princerajroy - prvnbist - ptitFicus @@ -429,6 +430,7 @@ - reggie3 - reichhartd - remix-run-bot +- richardhunghhw - riencoertjens - rkulinski - rlfarman @@ -552,3 +554,4 @@ - amir-ziaei - mrkhosravian - tanerijun +- ally1002 diff --git a/docs/file-conventions/entry.server.md b/docs/file-conventions/entry.server.md index 3a5a1abdb7e..9b9262d87bc 100644 --- a/docs/file-conventions/entry.server.md +++ b/docs/file-conventions/entry.server.md @@ -39,10 +39,10 @@ export function handleError( ### Streaming Rendering Errors -When you are streaming your HTML responses via [`renderToPipeableStream`][rendertopipeablestream] or [`renderToReadableStream`][rendertoreadablestream], your own `handleError` implementation will only handle errors encountered uring the initial shell render. If you encounter a rendering error during subsequent streamed rendering you will need handle these errors manually since the Remix server has already sent the Response by that point. +When you are streaming your HTML responses via [`renderToPipeableStream`][rendertopipeablestream] or [`renderToReadableStream`][rendertoreadablestream], your own `handleError` implementation will only handle errors encountered during the initial shell render. If you encounter a rendering error during subsequent streamed rendering you will need handle these errors manually since the Remix server has already sent the Response by that point. - For `renderToPipeableStream`, you can handle these errors in the `onError` callback function. You will need to toggle a boolean when the in `onShellReady` so you know if the error was a shell rendering error (and can be ignored) or an async rendering error (and must be handled). - - For an exmaple, please see the default [`entry.server.tsx`][node-streaming-entry-server] for Node. + - For an example, please see the default [`entry.server.tsx`][node-streaming-entry-server] for Node. - For `renderToReadableStream`, you can handle these errors in the `onError` callback function - For an example, please see the default [`entry.server.tsx`][cloudflare-streaming-entry-server] for Cloudflare diff --git a/docs/guides/routing.md b/docs/guides/routing.md index 373ed591711..471446aee74 100644 --- a/docs/guides/routing.md +++ b/docs/guides/routing.md @@ -203,7 +203,7 @@ You may notice an `?index` query parameter showing up on your URLs from time to ├── root.tsx └── routes ├── sales.invoices._index.tsx <-- /sales/invoices?index - ├── sales.invoices.invoices.tsx <-- /sales/invoices + └── sales.invoices.tsx <-- /sales/invoices ``` This is handled automatically for you when you submit from a `
` contained within either the layout route or the index route. But if you are submitting forms to different routes, or using `fetcher.submit`/`fetcher.load` you may need to be aware of this URL pattern, so you can target the correct route. diff --git a/docs/other-api/dev-v2.md b/docs/other-api/dev-v2.md index f6adb19c88c..2b5fc18fbcb 100644 --- a/docs/other-api/dev-v2.md +++ b/docs/other-api/dev-v2.md @@ -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 ` | -| 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 ` | +| 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 | -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. @@ -237,9 +236,47 @@ import { remember } from "~/utils/remember"; export const db = remember("db", new PrismaClient()); ``` +### How to set up MSW + +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_ORIGIN` environment variable: + +```json filename=package.json +{ + "scripts": { + "dev": "remix dev -c 'npm run dev:app'", + "dev:app": "binode --require ./mocks -- @remix-run/serve:remix-serve ./build" + } +} +``` + +Next, you can use `REMIX_DEV_ORIGIN` to let MSW forward internal "dev ready" messages on `/ping`: + +```ts +import { rest } from "msw"; + +const REMIX_DEV_PING = new URL( + process.env.REMIX_DEV_ORIGIN +); +REMIX_DEV_PING.pathname = "/ping"; + +export const server = setupServer( + rest.post(REMIX_DEV_PING.href, (req) => req.passthrough()) + // ... other request handlers go here ... +); +``` + ### How to set up local HTTPS -For this example, let's use \[mkcert]\[mkcert]. +For this example, let's use [mkcert][mkcert]. After you have it installed, make sure to: - Create a local Certificate Authority if you haven't already done so @@ -294,35 +331,50 @@ server.listen(port, () => { }); ``` -### Troubleshooting +Now that the app server is set up, you should be able to build and run your app in production mode with TLS. +To get the dev server to interop with TLS, you'll need to specify the TLS cert and key you created: -#### Using MSW with `v2_dev` +```sh +remix dev --tls-key=key.pem --tls-cert=cert.pem -c 'node ./server.js' +``` -The dev server uses the `REMIX_DEV_HTTP_ORIGIN` environment variable to communicate its origin to the app server. -You can use that to mock out the `/ping` endpoint used for hot update coordination: +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! -```ts -import { rest } from "msw"; +### How to integrate with a reverse proxy -export const server = setupServer( - rest.post( - `${process.env.REMIX_DEV_HTTP_ORIGIN}/ping`, - (req) => { - return req.passthrough(); - } - ) - // ... other request handlers go here ... -); +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 Hot Module Replacement is supposed to keep your app's state around between hot updates. 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. @@ -365,3 +417,6 @@ While the initial build slowdown is inherently a cost for HDR, we plan to optimi [jenseng-talk]: https://www.youtube.com/watch?v=lbzNnN0F67Y [react-keys]: https://react.dev/learn/rendering-lists#why-does-react-need-keys [react-refresh]: https://github.com/facebook/react/tree/main/packages/react-refresh +[binode]: https://github.com/kentcdodds/binode +[msw]: https://mswjs.io/ +[mkcert]: https://github.com/FiloSottile/mkcert diff --git a/docs/route/error-boundary-v2.md b/docs/route/error-boundary-v2.md index 19cf7f71a77..6937bf28504 100644 --- a/docs/route/error-boundary-v2.md +++ b/docs/route/error-boundary-v2.md @@ -64,4 +64,4 @@ export function ErrorBoundary() { [error-boundaries]: https://reactjs.org/docs/error-boundaries.html [rr-error-boundary]: https://reactrouter.com/en/main/route/error-element [use-route-error]: ../hooks/use-route-error -[is-route-error-response]: ../utils/is-route-error-response.md +[is-route-error-response]: ../utils/is-route-error-response diff --git a/docs/tutorials/jokes.md b/docs/tutorials/jokes.md index 20435cd89b7..3ee82deabd5 100644 --- a/docs/tutorials/jokes.md +++ b/docs/tutorials/jokes.md @@ -7740,7 +7740,7 @@ Phew! And there we have it. If you made it through this whole thing then I'm rea [the-http-api]: https://developer.mozilla.org/en-US/docs/Web/HTTP [the-basic-example]: https://codesandbox.io/s/github/remix-run/examples/tree/main/basic [express]: https://expressjs.com -[hydrate]: https://reactjs.org/docs/react-dom.html#hydrate +[hydrate]: https://react.dev/reference/react-dom/client/hydrateRoot [http-localhost-3000]: http://localhost:3000 [bare-bones-hello-world-app]: /jokes-tutorial/img/bare-bones.png [remix-config-js]: ../file-conventions/remix-config diff --git a/integration/error-sanitization-test.ts b/integration/error-sanitization-test.ts index 34afa0f8a45..a575557469b 100644 --- a/integration/error-sanitization-test.ts +++ b/integration/error-sanitization-test.ts @@ -1,8 +1,9 @@ import { test, expect } from "@playwright/test"; import { ServerMode } from "@remix-run/server-runtime/mode"; -import { createFixture, js } from "./helpers/create-fixture"; import type { Fixture } from "./helpers/create-fixture"; +import { createAppFixture, createFixture, js } from "./helpers/create-fixture"; +import { PlaywrightFixture } from "./helpers/playwright-fixture"; const routeFiles = { "app/root.jsx": js` @@ -33,6 +34,10 @@ const routeFiles = { if (new URL(request.url).searchParams.has('loader')) { throw new Error("Loader Error"); } + if (new URL(request.url).searchParams.has('subclass')) { + // This will throw a ReferenceError + console.log(thisisnotathing); + } return "LOADER" } @@ -58,6 +63,7 @@ const routeFiles = { <>

Index Error

{"MESSAGE:" + error.message}

+

{"NAME:" + error.name}

{error.stack ?

{"STACK:" + error.stack}

: null} ); @@ -279,6 +285,21 @@ test.describe("Error Sanitization", () => { ); expect(errorLogs[0][0].stack).toMatch(" at "); }); + + test("does not support hydration of Error subclasses", async ({ page }) => { + let response = await fixture.requestDocument("/?subclass"); + let html = await response.text(); + expect(html).toMatch("

MESSAGE:Unexpected Server Error"); + expect(html).toMatch("

NAME:Error"); + + // Hydration + let appFixture = await createAppFixture(fixture); + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/?subclass", true); + html = await app.getHtml(); + expect(html).toMatch("

MESSAGE:Unexpected Server Error"); + expect(html).toMatch("

NAME:Error"); + }); }); test.describe("serverMode=development", () => { @@ -428,6 +449,27 @@ test.describe("Error Sanitization", () => { ); expect(errorLogs[0][0].stack).toMatch(" at "); }); + + test("supports hydration of Error subclasses", async ({ page }) => { + let response = await fixture.requestDocument("/?subclass"); + let html = await response.text(); + expect(html).toMatch("

MESSAGE:thisisnotathing is not defined"); + expect(html).toMatch("

NAME:ReferenceError"); + expect(html).toMatch( + "

STACK:ReferenceError: thisisnotathing is not defined" + ); + + // Hydration + let appFixture = await createAppFixture(fixture, ServerMode.Development); + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/?subclass", true); + html = await app.getHtml(); + expect(html).toMatch("

MESSAGE:thisisnotathing is not defined"); + expect(html).toMatch("

NAME:ReferenceError"); + expect(html).toMatch( + "STACK:ReferenceError: thisisnotathing is not defined" + ); + }); }); test.describe("serverMode=production (user-provided handleError)", () => { diff --git a/integration/helpers/playwright-fixture.ts b/integration/helpers/playwright-fixture.ts index 35c5f013950..c5adb1511d4 100644 --- a/integration/helpers/playwright-fixture.ts +++ b/integration/helpers/playwright-fixture.ts @@ -281,6 +281,27 @@ async function doAndWait( console.log(`action done, ${requestCounter} requests pending`); } await networkSettledPromise; + + // I wish I knew why but Safari seems to get all screwed up without this. + // When you run doAndWait (via clicking a blink or submitting a form) and + // then waitForSelector(). It finds the selector element but thinks it's + // hidden for some unknown reason. It's intermittent, but waiting for the + // next animation frame delaying slightly before the waitForSelector() calls + // seems to fix it 🤷‍♂️ + // + // Test timeout of 30000ms exceeded. + // + // Error: page.waitForSelector: Target closed + // =========================== logs =========================== + // waiting for locator('text=ROOT_BOUNDARY_TEXT') to be visible + // locator resolved to hidden

ROOT_BOUNDARY_TEXT
+ // locator resolved to hidden
ROOT_BOUNDARY_TEXT
+ // ... and so on until the test times out + let userAgent = await page.evaluate(() => navigator.userAgent); + if (/Safari\//i.test(userAgent) && !/Chrome\//i.test(userAgent)) { + await page.evaluate(() => new Promise((r) => requestAnimationFrame(r))); + } + if (DEBUG) { console.log(`action done, network settled`); } diff --git a/package.json b/package.json index 2a88b40c1d1..eddc1a65d5f 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "@octokit/graphql": "^4.8.0", "@octokit/plugin-paginate-rest": "^2.17.0", "@octokit/rest": "^18.12.0", - "@playwright/test": "^1.35.1", + "@playwright/test": "1.33.0", "@remix-run/changelog-github": "^0.0.5", "@rollup/plugin-babel": "^5.2.2", "@rollup/plugin-json": "^4.1.0", diff --git a/packages/create-remix/CHANGELOG.md b/packages/create-remix/CHANGELOG.md index c197af746ad..86c13a88203 100644 --- a/packages/create-remix/CHANGELOG.md +++ b/packages/create-remix/CHANGELOG.md @@ -1,5 +1,19 @@ # `create-remix` +## 1.18.1 + +### Patch Changes + +- Updated dependencies: + - `@remix-run/dev@1.18.1` + +## 1.18.0 + +### Patch Changes + +- Updated dependencies: + - `@remix-run/dev@1.18.0` + ## 1.17.1 ### Patch Changes diff --git a/packages/create-remix/package.json b/packages/create-remix/package.json index 249cc8ea285..15104484a0b 100644 --- a/packages/create-remix/package.json +++ b/packages/create-remix/package.json @@ -1,6 +1,6 @@ { "name": "create-remix", - "version": "1.17.1", + "version": "1.18.1", "description": "Create a new Remix app", "homepage": "https://remix.run", "bugs": { @@ -17,7 +17,7 @@ "create-remix": "dist/cli.js" }, "dependencies": { - "@remix-run/dev": "1.17.1" + "@remix-run/dev": "1.18.1" }, "engines": { "node": ">=14.0.0" diff --git a/packages/remix-architect/CHANGELOG.md b/packages/remix-architect/CHANGELOG.md index e91eb6f9acb..a9818980b48 100644 --- a/packages/remix-architect/CHANGELOG.md +++ b/packages/remix-architect/CHANGELOG.md @@ -1,5 +1,19 @@ # `@remix-run/architect` +## 1.18.1 + +### Patch Changes + +- Updated dependencies: + - `@remix-run/node@1.18.1` + +## 1.18.0 + +### Patch Changes + +- Updated dependencies: + - `@remix-run/node@1.18.0` + ## 1.17.1 ### Patch Changes diff --git a/packages/remix-architect/package.json b/packages/remix-architect/package.json index 9aac9559cce..324210eed5e 100644 --- a/packages/remix-architect/package.json +++ b/packages/remix-architect/package.json @@ -1,6 +1,6 @@ { "name": "@remix-run/architect", - "version": "1.17.1", + "version": "1.18.1", "description": "Architect server request handler for Remix", "bugs": { "url": "https://github.com/remix-run/remix/issues" @@ -15,7 +15,7 @@ "typings": "dist/index.d.ts", "dependencies": { "@architect/functions": "^5.2.0", - "@remix-run/node": "1.17.1", + "@remix-run/node": "1.18.1", "@types/aws-lambda": "^8.10.82" }, "devDependencies": { diff --git a/packages/remix-cloudflare-pages/CHANGELOG.md b/packages/remix-cloudflare-pages/CHANGELOG.md index ffce6d23dca..a1358bdd0a4 100644 --- a/packages/remix-cloudflare-pages/CHANGELOG.md +++ b/packages/remix-cloudflare-pages/CHANGELOG.md @@ -1,5 +1,19 @@ # `@remix-run/cloudflare-pages` +## 1.18.1 + +### Patch Changes + +- Updated dependencies: + - `@remix-run/cloudflare@1.18.1` + +## 1.18.0 + +### Patch Changes + +- Updated dependencies: + - `@remix-run/cloudflare@1.18.0` + ## 1.17.1 ### Patch Changes diff --git a/packages/remix-cloudflare-pages/package.json b/packages/remix-cloudflare-pages/package.json index 51b9e068a47..e11282e9b89 100644 --- a/packages/remix-cloudflare-pages/package.json +++ b/packages/remix-cloudflare-pages/package.json @@ -1,6 +1,6 @@ { "name": "@remix-run/cloudflare-pages", - "version": "1.17.1", + "version": "1.18.1", "description": "Cloudflare Pages request handler for Remix", "bugs": { "url": "https://github.com/remix-run/remix/issues" @@ -15,7 +15,7 @@ "typings": "dist/index.d.ts", "module": "dist/esm/index.js", "dependencies": { - "@remix-run/cloudflare": "1.17.1" + "@remix-run/cloudflare": "1.18.1" }, "devDependencies": { "@cloudflare/workers-types": "^3.4.0", diff --git a/packages/remix-cloudflare-workers/CHANGELOG.md b/packages/remix-cloudflare-workers/CHANGELOG.md index a721b41c330..2a4aa01b3a2 100644 --- a/packages/remix-cloudflare-workers/CHANGELOG.md +++ b/packages/remix-cloudflare-workers/CHANGELOG.md @@ -1,5 +1,19 @@ # `@remix-run/cloudflare-workers` +## 1.18.1 + +### Patch Changes + +- Updated dependencies: + - `@remix-run/cloudflare@1.18.1` + +## 1.18.0 + +### Patch Changes + +- Updated dependencies: + - `@remix-run/cloudflare@1.18.0` + ## 1.17.1 ### Patch Changes diff --git a/packages/remix-cloudflare-workers/package.json b/packages/remix-cloudflare-workers/package.json index 5bc10232d7b..a63a2aedf95 100644 --- a/packages/remix-cloudflare-workers/package.json +++ b/packages/remix-cloudflare-workers/package.json @@ -1,6 +1,6 @@ { "name": "@remix-run/cloudflare-workers", - "version": "1.17.1", + "version": "1.18.1", "description": "Cloudflare worker request handler for Remix", "bugs": { "url": "https://github.com/remix-run/remix/issues" @@ -16,7 +16,7 @@ "module": "dist/esm/index.js", "dependencies": { "@cloudflare/kv-asset-handler": "^0.1.3", - "@remix-run/cloudflare": "1.17.1" + "@remix-run/cloudflare": "1.18.1" }, "devDependencies": { "@cloudflare/workers-types": "^3.4.0" diff --git a/packages/remix-cloudflare/CHANGELOG.md b/packages/remix-cloudflare/CHANGELOG.md index 2bf36a1b0a4..7bb845beeb7 100644 --- a/packages/remix-cloudflare/CHANGELOG.md +++ b/packages/remix-cloudflare/CHANGELOG.md @@ -1,5 +1,19 @@ # `@remix-run/cloudflare` +## 1.18.1 + +### Patch Changes + +- Updated dependencies: + - `@remix-run/server-runtime@1.18.1` + +## 1.18.0 + +### Patch Changes + +- Updated dependencies: + - `@remix-run/server-runtime@1.18.0` + ## 1.17.1 ### Patch Changes diff --git a/packages/remix-cloudflare/package.json b/packages/remix-cloudflare/package.json index b647b897eb9..1d81d9bc7f3 100644 --- a/packages/remix-cloudflare/package.json +++ b/packages/remix-cloudflare/package.json @@ -1,6 +1,6 @@ { "name": "@remix-run/cloudflare", - "version": "1.17.1", + "version": "1.18.1", "description": "Cloudflare platform abstractions for Remix", "bugs": { "url": "https://github.com/remix-run/remix/issues" @@ -15,7 +15,7 @@ "typings": "dist/index.d.ts", "dependencies": { "@cloudflare/kv-asset-handler": "^0.1.3", - "@remix-run/server-runtime": "1.17.1" + "@remix-run/server-runtime": "1.18.1" }, "devDependencies": { "@cloudflare/workers-types": "^3.4.0" diff --git a/packages/remix-css-bundle/CHANGELOG.md b/packages/remix-css-bundle/CHANGELOG.md index a74e396afb1..5e4009c4f83 100644 --- a/packages/remix-css-bundle/CHANGELOG.md +++ b/packages/remix-css-bundle/CHANGELOG.md @@ -1,5 +1,9 @@ # @remix-run/css-bundle +## 1.18.1 + +## 1.18.0 + ## 1.17.1 ### Patch Changes diff --git a/packages/remix-css-bundle/package.json b/packages/remix-css-bundle/package.json index cb7beab89d3..8cfb43c9f6c 100644 --- a/packages/remix-css-bundle/package.json +++ b/packages/remix-css-bundle/package.json @@ -1,6 +1,6 @@ { "name": "@remix-run/css-bundle", - "version": "1.17.1", + "version": "1.18.1", "description": "CSS bundle href when using CSS bundling features in Remix", "homepage": "https://remix.run", "bugs": { diff --git a/packages/remix-deno/CHANGELOG.md b/packages/remix-deno/CHANGELOG.md index c9f2808a9ce..af356222fe6 100644 --- a/packages/remix-deno/CHANGELOG.md +++ b/packages/remix-deno/CHANGELOG.md @@ -1,5 +1,19 @@ # `@remix-run/deno` +## 1.18.1 + +### Patch Changes + +- Updated dependencies: + - `@remix-run/server-runtime@1.18.1` + +## 1.18.0 + +### Patch Changes + +- Updated dependencies: + - `@remix-run/server-runtime@1.18.0` + ## 1.17.1 ### Patch Changes diff --git a/packages/remix-deno/package.json b/packages/remix-deno/package.json index 445938db66a..d29881dde2c 100644 --- a/packages/remix-deno/package.json +++ b/packages/remix-deno/package.json @@ -1,6 +1,6 @@ { "name": "@remix-run/deno", - "version": "1.17.1", + "version": "1.18.1", "description": "Deno platform abstractions for Remix", "homepage": "https://remix.run", "main": "./index.ts", @@ -15,7 +15,7 @@ "license": "MIT", "sideEffects": false, "dependencies": { - "@remix-run/server-runtime": "1.17.1", + "@remix-run/server-runtime": "1.18.1", "mime": "^3.0.0" }, "engines": { diff --git a/packages/remix-dev/CHANGELOG.md b/packages/remix-dev/CHANGELOG.md index 95649715619..43498a3f76f 100644 --- a/packages/remix-dev/CHANGELOG.md +++ b/packages/remix-dev/CHANGELOG.md @@ -1,5 +1,39 @@ # `@remix-run/dev` +## 1.18.1 + +### Patch Changes + +- Ignore missing `react-dom/client` for React 17 ([#6725](https://github.com/remix-run/remix/pull/6725)) +- Updated dependencies: + - `@remix-run/server-runtime@1.18.1` + +## 1.18.0 + +### Minor Changes + +- stabilize v2 dev server ([#6615](https://github.com/remix-run/remix/pull/6615)) +- improved logging for `remix build` and `remix dev` ([#6596](https://github.com/remix-run/remix/pull/6596)) + +### Patch Changes + +- fix docs links for msw and mkcert ([#6672](https://github.com/remix-run/remix/pull/6672)) +- fix `remix dev -c`: kill all descendant processes of specified command when restarting ([#6663](https://github.com/remix-run/remix/pull/6663)) +- Add caching to regular stylesheet compilation ([#6638](https://github.com/remix-run/remix/pull/6638)) +- Rename `Architect (AWS Lambda)` -> `Architect` in the `create-remix` CLI to avoid confusion for other methods of deploying to AWS (i.e., SST) ([#6484](https://github.com/remix-run/remix/pull/6484)) +- Improve CSS bundle build performance by skipping unused Node polyfills ([#6639](https://github.com/remix-run/remix/pull/6639)) +- Improve performance of CSS bundle build by skipping compilation of Remix/React packages that are known not to contain CSS imports ([#6654](https://github.com/remix-run/remix/pull/6654)) +- Cache CSS side-effect imports transform when using HMR ([#6622](https://github.com/remix-run/remix/pull/6622)) +- Fix bug with pathless layout routes beneath nested path segments ([#6649](https://github.com/remix-run/remix/pull/6649)) +- Add caching to PostCSS for CSS Modules ([#6604](https://github.com/remix-run/remix/pull/6604)) +- Add caching to PostCSS for side-effect imports ([#6554](https://github.com/remix-run/remix/pull/6554)) +- cache getRouteModuleExports calls to significantly speed up build and HMR rebuild times ([#6629](https://github.com/remix-run/remix/pull/6629)) +- group rebuild logs with surrounding whitespace ([#6607](https://github.com/remix-run/remix/pull/6607)) +- instructions for integrating with msw ([#6669](https://github.com/remix-run/remix/pull/6669)) +- Update minimum version of `esbuild-plugins-node-modules-polyfill` to 1.0.16 to ensure that the plugin is cached ([#6652](https://github.com/remix-run/remix/pull/6652)) +- Updated dependencies: + - `@remix-run/server-runtime@1.18.0` + ## 1.17.1 ### Patch Changes diff --git a/packages/remix-dev/__tests__/cli-test.ts b/packages/remix-dev/__tests__/cli-test.ts index 0c1fe283f07..75160fd011b 100644 --- a/packages/remix-dev/__tests__/cli-test.ts +++ b/packages/remix-dev/__tests__/cli-test.ts @@ -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) @@ -230,7 +228,7 @@ describe("remix CLI", () => { await interactWithShell(proc, [ { question: /Where.*create.*app/i, type: [projectDir, ENTER] }, { question: /What type of app/i, answer: /basics/i }, - { question: /Where.*deploy/i, answer: /express/i }, + { question: /Where.*deploy/i, answer: /remix/i }, { question: /typescript or javascript/i, answer: /typescript/i }, { question: /install/i, type: ["n", ENTER] }, ]); @@ -255,7 +253,7 @@ describe("remix CLI", () => { await interactWithShell(proc, [ { question: /Where.*create.*app/i, type: [projectDir, ENTER] }, { question: /What type of app/i, answer: /basics/i }, - { question: /Where.*deploy/i, answer: /express/i }, + { question: /Where.*deploy/i, answer: /remix/i }, { question: /typescript or javascript/i, answer: /javascript/i }, { question: /install/i, type: ["n", ENTER] }, ]); diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts index bdfc8d275c1..0280942ad37 100644 --- a/packages/remix-dev/cli/commands.ts +++ b/packages/remix-dev/cli/commands.ts @@ -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(); @@ -223,8 +223,6 @@ export async function dev( tlsCert?: string; } = {} ) { - // clear screen - process.stdout.write("\x1Bc"); console.log(`\n 💿 remix dev\n`); if (process.env.NODE_ENV && process.env.NODE_ENV !== "development") { @@ -240,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( @@ -469,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 & { + flags: { + port?: number; tlsKey?: string; tlsCert?: string; + /** @deprecated */ + scheme?: string; // TODO: remove in v2 + /** @deprecated */ + host?: string; // TODO: remove in v2 } = {} -): Promise => { +) => { 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 = {} -): Promise => { + 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 = @@ -530,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, }; }; diff --git a/packages/remix-dev/cli/run.ts b/packages/remix-dev/cli/run.ts index df1cdcaa4ef..7f14c1f5bc9 100644 --- a/packages/remix-dev/cli/run.ts +++ b/packages/remix-dev/cli/run.ts @@ -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( @@ -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) @@ -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, @@ -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"]; diff --git a/packages/remix-dev/compiler/analysis.ts b/packages/remix-dev/compiler/analysis.ts new file mode 100644 index 00000000000..73381afbb71 --- /dev/null +++ b/packages/remix-dev/compiler/analysis.ts @@ -0,0 +1,17 @@ +import fs from "node:fs"; +import path from "node:path"; +import type { Metafile } from "esbuild"; + +import type { Context } from "./context"; + +export let writeMetafile = ( + ctx: Context, + filename: string, + metafile: Metafile +) => { + let buildDir = path.dirname(ctx.config.serverBuildPath); + if (!fs.existsSync(buildDir)) { + fs.mkdirSync(buildDir, { recursive: true }); + } + fs.writeFileSync(path.join(buildDir, filename), JSON.stringify(metafile)); +}; diff --git a/packages/remix-dev/compiler/css/compiler.ts b/packages/remix-dev/compiler/css/compiler.ts index cc56b0dc7fb..16f184c9aea 100644 --- a/packages/remix-dev/compiler/css/compiler.ts +++ b/packages/remix-dev/compiler/css/compiler.ts @@ -18,6 +18,7 @@ import { } from "./plugins/bundleEntry"; import type { Context } from "../context"; import { isBundle } from "./bundle"; +import { writeMetafile } from "../analysis"; const getExternals = (config: RemixConfig): string[] => { // For the browser build, exclude node built-ins that don't have a @@ -96,9 +97,11 @@ export let create = async (ctx: Context) => { let compiler = await esbuild.context({ ...createEsbuildConfig(ctx), write: false, + metafile: true, }); let compile = async () => { - let { outputFiles } = await compiler.rebuild(); + let { outputFiles, metafile } = await compiler.rebuild(); + writeMetafile(ctx, "metafile.css.json", metafile); let bundleOutputFile = outputFiles.find((outputFile) => isBundle(ctx, outputFile, ".css") ); diff --git a/packages/remix-dev/compiler/js/compiler.ts b/packages/remix-dev/compiler/js/compiler.ts index b8c7b265752..5cac49348c0 100644 --- a/packages/remix-dev/compiler/js/compiler.ts +++ b/packages/remix-dev/compiler/js/compiler.ts @@ -22,6 +22,7 @@ import invariant from "../../invariant"; import { hmrPlugin } from "./plugins/hmr"; import type { LazyValue } from "../lazyValue"; import type { Context } from "../context"; +import { writeMetafile } from "../analysis"; type Compiler = { // produce ./public/build/ @@ -136,6 +137,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 ), @@ -160,6 +165,7 @@ export const create = async ( let compile = async () => { let { metafile } = await compiler.rebuild(); + writeMetafile(ctx, "metafile.js.json", metafile); let hmr: Manifest["hmr"] | undefined = undefined; if (ctx.options.mode === "development" && ctx.config.future.v2_dev) { diff --git a/packages/remix-dev/compiler/js/plugins/hmr.ts b/packages/remix-dev/compiler/js/plugins/hmr.ts index eac32d2cedc..31f035c0b0c 100644 --- a/packages/remix-dev/compiler/js/plugins/hmr.ts +++ b/packages/remix-dev/compiler/js/plugins/hmr.ts @@ -1,6 +1,6 @@ import * as fs from "node:fs"; import * as path from "node:path"; -import * as esbuild from "esbuild"; +import type * as esbuild from "esbuild"; import type { RemixConfig } from "../../../config"; import type { Context } from "../../context"; @@ -161,6 +161,10 @@ export async function applyHMR( ) { let babel = await import("@babel/core"); // @ts-expect-error + let babelPresetTypescript = await import("@babel/preset-typescript"); + // @ts-expect-error + let babelJsx = await import("@babel/plugin-syntax-jsx"); + // @ts-expect-error let reactRefresh = await import("react-refresh/babel"); let IS_FAST_REFRESH_ENABLED = /\$RefreshReg\$\(/; @@ -177,54 +181,45 @@ import.meta.hot = __hmr__.createHotContext( $id$ ); ${lastModified ? `import.meta.hot.lastModified = "${lastModified}";` : ""} -}`.replace(/\$id\$/g, hmrId); +} +// REMIX HMR END +\n`.replace(/\$id\$/g, hmrId); let sourceCodeWithHMR = hmrPrefix + sourceCode; - // turn the source code into JS for babel - let jsWithHMR = esbuild.transformSync(sourceCodeWithHMR, { - loader: argsPath.endsWith("x") ? "tsx" : "ts", - format: args.pluginData?.format || "esm", - jsx: "automatic", - }).code; - let resultCode = jsWithHMR; - // run babel to add react-refresh - let transformResult = babel.transformSync(jsWithHMR, { + let transformResult = babel.transformSync(sourceCodeWithHMR, { filename: argsPath, ast: false, compact: false, sourceMaps: sourcemap, configFile: false, babelrc: false, - plugins: [[reactRefresh.default, { skipEnvCheck: true }]], + presets: [babelPresetTypescript.default], + plugins: [babelJsx.default, [reactRefresh.default, { skipEnvCheck: true }]], }); - let jsWithReactRefresh = transformResult?.code || jsWithHMR; + let jsWithReactRefresh = transformResult?.code ?? sourceCodeWithHMR; // auto opt-in to accepting fast refresh updates if the module // has react components - if (IS_FAST_REFRESH_ENABLED.test(jsWithReactRefresh)) { - resultCode = - ` - if (!window.$RefreshReg$ || !window.$RefreshSig$ || !window.$RefreshRuntime$) { - console.warn('remix:hmr: React Fast Refresh only works when the Remix compiler is running in development mode.'); - } else { - var prevRefreshReg = window.$RefreshReg$; - var prevRefreshSig = window.$RefreshSig$; - window.$RefreshReg$ = (type, id) => { - window.$RefreshRuntime$.register(type, ${JSON.stringify( - hmrId - )} + id); - } - window.$RefreshSig$ = window.$RefreshRuntime$.createSignatureFunctionForTransform; - } - ` + - jsWithReactRefresh + - ` - window.$RefreshReg$ = prevRefreshReg; - window.$RefreshSig$ = prevRefreshSig; - `; + if (!IS_FAST_REFRESH_ENABLED.test(jsWithReactRefresh)) { + return "// REMIX HMR BEGIN\n" + sourceCodeWithHMR; } - - return resultCode; + return ( + `// REMIX HMR BEGIN +if (!window.$RefreshReg$ || !window.$RefreshSig$ || !window.$RefreshRuntime$) { + console.warn('remix:hmr: React Fast Refresh only works when the Remix compiler is running in development mode.'); +} else { + var prevRefreshReg = window.$RefreshReg$; + var prevRefreshSig = window.$RefreshSig$; + window.$RefreshReg$ = (type, id) => { + window.$RefreshRuntime$.register(type, ${JSON.stringify(hmrId)} + id); + } + window.$RefreshSig$ = window.$RefreshRuntime$.createSignatureFunctionForTransform; +}\n` + + jsWithReactRefresh + + `\n +window.$RefreshReg$ = prevRefreshReg; +window.$RefreshSig$ = prevRefreshSig;` + ); } diff --git a/packages/remix-dev/compiler/options.ts b/packages/remix-dev/compiler/options.ts index 072f16ce1e6..2f85f6160e9 100644 --- a/packages/remix-dev/compiler/options.ts +++ b/packages/remix-dev/compiler/options.ts @@ -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 }; diff --git a/packages/remix-dev/compiler/server/compiler.ts b/packages/remix-dev/compiler/server/compiler.ts index 4fb76bdad2d..fd99c4c4ac0 100644 --- a/packages/remix-dev/compiler/server/compiler.ts +++ b/packages/remix-dev/compiler/server/compiler.ts @@ -20,6 +20,7 @@ import type * as Channel from "../../channel"; import type { Context } from "../context"; import type { LazyValue } from "../lazyValue"; import { cssBundlePlugin } from "../plugins/cssBundlePlugin"; +import { writeMetafile } from "../analysis"; type Compiler = { // produce ./build/index.js @@ -104,12 +105,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", @@ -128,9 +133,11 @@ export const create = async ( let compiler = await esbuild.context({ ...createEsbuildConfig(ctx, refs), write: false, + metafile: true, }); let compile = async () => { - let { outputFiles } = await compiler.rebuild(); + let { outputFiles, metafile } = await compiler.rebuild(); + writeMetafile(ctx, "metafile.server.json", metafile); return outputFiles; }; return { diff --git a/packages/remix-dev/compiler/server/plugins/entry.ts b/packages/remix-dev/compiler/server/plugins/entry.ts index 4966dd3ad93..edb4fb60b08 100644 --- a/packages/remix-dev/compiler/server/plugins/entry.ts +++ b/packages/remix-dev/compiler/server/plugins/entry.ts @@ -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) => { diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts index 73abaaa16f2..f6084a07501 100644 --- a/packages/remix-dev/config.ts +++ b/packages/remix-dev/config.ts @@ -38,12 +38,15 @@ export type ServerPlatform = "node" | "neutral"; type Dev = { command?: string; - scheme?: string; - host?: string; port?: number; restart?: boolean; tlsKey?: string; tlsCert?: string; + + /** @deprecated remove in v2 */ + scheme?: string; + /** @deprecated remove in v2 */ + host?: string; }; interface FutureConfig { diff --git a/packages/remix-dev/config/defaults/entry.dev.ts b/packages/remix-dev/config/defaults/entry.dev.ts index 036c6c04a9f..20d5e95db04 100644 --- a/packages/remix-dev/config/defaults/entry.dev.ts +++ b/packages/remix-dev/config/defaults/entry.dev.ts @@ -6,7 +6,7 @@ export default () => { import("react/jsx-dev-runtime"); import("react/jsx-runtime"); import("react-dom"); - import("react-dom/client"); + import("react-dom/client").catch(); import("react-refresh/runtime"); import("@remix-run/react"); import("remix:hmr"); diff --git a/packages/remix-dev/devServer_unstable/index.ts b/packages/remix-dev/devServer_unstable/index.ts index d01d0aaa616..1abeb364142 100644 --- a/packages/remix-dev/devServer_unstable/index.ts +++ b/packages/remix-dev/devServer_unstable/index.ts @@ -22,14 +22,7 @@ import type { Result } from "../result"; import { err, ok } from "../result"; import invariant from "../invariant"; import { logger } from "../tux"; - -type Origin = { - scheme: string; - host: string; - port: number; -}; - -let stringifyOrigin = (o: Origin) => `${o.scheme}://${o.host}:${o.port}`; +import { kill, killtree } from "./proc"; let detectBin = async (): Promise => { let pkgManager = detectPackageManager() ?? "npm"; @@ -46,12 +39,11 @@ export let serve = async ( initialConfig: RemixConfig, options: { command?: string; - scheme: string; - host: string; port: number; - restart: boolean; tlsKey?: string; tlsCert?: string; + REMIX_DEV_ORIGIN: URL; + restart: boolean; } ) => { await loadEnv(initialConfig.rootDirectory); @@ -91,12 +83,6 @@ export let serve = async ( : 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) => { let cmd = @@ -112,7 +98,8 @@ export let serve = async ( NODE_ENV: "development", PATH: bin + (process.platform === "win32" ? ";" : ":") + process.env.PATH, - REMIX_DEV_HTTP_ORIGIN: stringifyOrigin(origin), + REMIX_DEV_ORIGIN: options.REMIX_DEV_ORIGIN.href, + REMIX_DEV_HTTP_ORIGIN: options.REMIX_DEV_ORIGIN.href, // TODO: remove in v2 FORCE_COLOR: process.env.NO_COLOR === undefined ? "1" : "0", }, // https://github.com/sindresorhus/execa/issues/433 @@ -177,7 +164,7 @@ export let serve = async ( options: { mode: "development", sourcemap: true, - devOrigin: origin, + REMIX_DEV_ORIGIN: options.REMIX_DEV_ORIGIN, }, fileWatchCache, logger, @@ -214,7 +201,9 @@ export let serve = async ( try { let start = Date.now(); if (state.appServer === undefined || options.restart) { - await kill(state.appServer); + if (state.appServer?.pid) { + await killtree(state.appServer.pid); + } state.appServer = startAppServer(options.command); } let appReady = await state.appReady!.result; @@ -273,10 +262,10 @@ export let serve = async ( } ); - server.listen(origin.port); + server.listen(options.port); return new Promise(() => {}).finally(async () => { - await kill(state.appServer); + state.appServer?.pid && (await kill(state.appServer.pid)); websocket.close(); server.close(); await dispose(); @@ -290,25 +279,3 @@ let clean = (config: RemixConfig) => { }; let relativePath = (file: string) => path.relative(process.cwd(), file); - -let kill = async (p?: execa.ExecaChildProcess) => { - if (p === undefined) return; - let channel = Channel.create(); - p.on("exit", channel.ok); - - // https://github.com/nodejs/node/issues/12378 - if (process.platform === "win32") { - try { - await execa("taskkill", ["/pid", String(p.pid), "/f", "/t"]); - } catch (error) { - // if exit code is 128, app server process is already dead - if (!(error instanceof Error)) throw error; - if (!("exitCode" in error)) throw error; - if (error.exitCode !== 128) throw error; - } - } else { - p.kill("SIGTERM", { forceKillAfterTimeout: 1_000 }); - } - - await channel.result; -}; diff --git a/packages/remix-dev/devServer_unstable/proc.ts b/packages/remix-dev/devServer_unstable/proc.ts new file mode 100644 index 00000000000..6f9f6ea9c67 --- /dev/null +++ b/packages/remix-dev/devServer_unstable/proc.ts @@ -0,0 +1,51 @@ +import execa from "execa"; +import pidtree from "pidtree"; + +let isWindows = process.platform === "win32"; + +export let kill = async (pid: number) => { + if (isWindows) { + await execa("taskkill", ["/F", "/PID", pid.toString()]).catch((error) => { + // taskkill 128 -> the process is already dead + if (error.exitCode !== 128) throw error; + }); + return; + } + await execa("kill", ["-9", pid.toString()]).catch((error) => { + // process is already dead + if (!/No such process/.test(error.message)) throw error; + }); +}; + +let isAlive = (pid: number) => { + try { + process.kill(pid, 0); + return true; + } catch (error) { + return false; + } +}; + +export let killtree = async (pid: number) => { + let descendants = await pidtree(pid); + let pids = [pid, ...descendants]; + + await Promise.all(pids.map(kill)); + + return new Promise((resolve, reject) => { + let check = setInterval(() => { + let alive = pids.filter(isAlive); + if (alive.length === 0) { + clearInterval(check); + resolve(); + } + }, 50); + + setTimeout(() => { + clearInterval(check); + reject( + new Error("Timeout: Processes did not exit within the specified time.") + ); + }, 2000); + }); +}; diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 3d457c5ae54..186a8739b41 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,6 +1,6 @@ { "name": "@remix-run/dev", - "version": "1.17.1", + "version": "1.18.1", "description": "Dev tools and CLI for Remix", "homepage": "https://remix.run", "bugs": { @@ -28,7 +28,7 @@ "@babel/traverse": "^7.21.5", "@babel/types": "^7.21.5", "@npmcli/package-json": "^2.0.0", - "@remix-run/server-runtime": "1.17.1", + "@remix-run/server-runtime": "1.18.1", "@vanilla-extract/integration": "^6.2.0", "arg": "^5.0.1", "cacache": "^15.0.5", @@ -36,7 +36,7 @@ "chokidar": "^3.5.1", "dotenv": "^16.0.0", "esbuild": "0.17.6", - "esbuild-plugins-node-modules-polyfill": "^1.0.16", + "esbuild-plugins-node-modules-polyfill": "^1.1.0", "execa": "5.1.1", "exit-hook": "2.2.1", "express": "^4.17.1", @@ -54,6 +54,7 @@ "ora": "^5.4.1", "picocolors": "^1.0.0", "picomatch": "^2.3.1", + "pidtree": "^0.6.0", "postcss": "^8.4.19", "postcss-discard-duplicates": "^5.1.0", "postcss-load-config": "^4.0.1", @@ -73,7 +74,7 @@ "xdm": "^2.0.0" }, "devDependencies": { - "@remix-run/serve": "1.17.1", + "@remix-run/serve": "1.18.1", "@types/cacache": "^15.0.0", "@types/gunzip-maybe": "^1.4.0", "@types/inquirer": "^8.2.0", @@ -93,7 +94,7 @@ "type-fest": "^2.16.0" }, "peerDependencies": { - "@remix-run/serve": "^1.17.1" + "@remix-run/serve": "^1.18.1" }, "peerDependenciesMeta": { "@remix-run/serve": { diff --git a/packages/remix-dev/server-build.ts b/packages/remix-dev/server-build.ts index 2098896e6f5..028c75c8024 100644 --- a/packages/remix-dev/server-build.ts +++ b/packages/remix-dev/server-build.ts @@ -10,7 +10,6 @@ export const assets: ServerBuild["assets"] = undefined!; export const entry: ServerBuild["entry"] = undefined!; export const routes: ServerBuild["routes"] = undefined!; export const future: ServerBuild["future"] = undefined!; -export const dev: ServerBuild["dev"] = undefined!; export const publicPath: ServerBuild["publicPath"] = undefined!; // prettier-ignore export const assetsBuildDirectory: ServerBuild["assetsBuildDirectory"] = undefined!; diff --git a/packages/remix-eslint-config/CHANGELOG.md b/packages/remix-eslint-config/CHANGELOG.md index 793087a0540..8d2f5ee2b18 100644 --- a/packages/remix-eslint-config/CHANGELOG.md +++ b/packages/remix-eslint-config/CHANGELOG.md @@ -1,5 +1,13 @@ # `@remix-run/eslint-config` +## 1.18.1 + +No significant changes to this package were made in this release. [See the releases page on GitHub](https://github.com/remix-run/remix/releases/tag/remix%401.18.1) for an overview of all changes in v1.18.1. + +## 1.18.0 + +No significant changes to this package were made in this release. [See the releases page on GitHub](https://github.com/remix-run/remix/releases/tag/remix%401.18.0) for an overview of all changes in v1.18.0. + ## 1.17.1 No significant changes to this package were made in this release. [See the releases page on GitHub](https://github.com/remix-run/remix/releases/tag/remix%401.17.1) for an overview of all changes in v1.17.1. diff --git a/packages/remix-eslint-config/package.json b/packages/remix-eslint-config/package.json index 65f97da9398..16e832d5133 100644 --- a/packages/remix-eslint-config/package.json +++ b/packages/remix-eslint-config/package.json @@ -1,6 +1,6 @@ { "name": "@remix-run/eslint-config", - "version": "1.17.1", + "version": "1.18.1", "description": "ESLint configuration for Remix projects", "bugs": { "url": "https://github.com/remix-run/remix/issues" diff --git a/packages/remix-express/CHANGELOG.md b/packages/remix-express/CHANGELOG.md index b39e3b5bae3..7b8ce700762 100644 --- a/packages/remix-express/CHANGELOG.md +++ b/packages/remix-express/CHANGELOG.md @@ -1,5 +1,19 @@ # `@remix-run/express` +## 1.18.1 + +### Patch Changes + +- Updated dependencies: + - `@remix-run/node@1.18.1` + +## 1.18.0 + +### Patch Changes + +- Updated dependencies: + - `@remix-run/node@1.18.0` + ## 1.17.1 ### Patch Changes diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 98f87fa8144..dd92ea2c029 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,6 +1,6 @@ { "name": "@remix-run/express", - "version": "1.17.1", + "version": "1.18.1", "description": "Express server request handler for Remix", "bugs": { "url": "https://github.com/remix-run/remix/issues" @@ -14,7 +14,7 @@ "main": "dist/index.js", "typings": "dist/index.d.ts", "dependencies": { - "@remix-run/node": "1.17.1" + "@remix-run/node": "1.18.1" }, "devDependencies": { "@types/express": "^4.17.9", diff --git a/packages/remix-netlify/CHANGELOG.md b/packages/remix-netlify/CHANGELOG.md index 9d6b6f267b6..d065b8ab9ac 100644 --- a/packages/remix-netlify/CHANGELOG.md +++ b/packages/remix-netlify/CHANGELOG.md @@ -1,5 +1,19 @@ # `@remix-run/netlify` +## 1.18.1 + +### Patch Changes + +- Updated dependencies: + - `@remix-run/node@1.18.1` + +## 1.18.0 + +### Patch Changes + +- Updated dependencies: + - `@remix-run/node@1.18.0` + ## 1.17.1 ### Patch Changes diff --git a/packages/remix-netlify/package.json b/packages/remix-netlify/package.json index 55084f56e39..71ce689852e 100644 --- a/packages/remix-netlify/package.json +++ b/packages/remix-netlify/package.json @@ -1,6 +1,6 @@ { "name": "@remix-run/netlify", - "version": "1.17.1", + "version": "1.18.1", "description": "Netlify server request handler for Remix", "bugs": { "url": "https://github.com/remix-run/remix/issues" @@ -14,7 +14,7 @@ "main": "dist/index.js", "typings": "dist/index.d.ts", "dependencies": { - "@remix-run/node": "1.17.1" + "@remix-run/node": "1.18.1" }, "devDependencies": { "@netlify/functions": "^1.0.0" diff --git a/packages/remix-node/CHANGELOG.md b/packages/remix-node/CHANGELOG.md index 52ae8be7e41..4a0a887b631 100644 --- a/packages/remix-node/CHANGELOG.md +++ b/packages/remix-node/CHANGELOG.md @@ -1,5 +1,19 @@ # `@remix-run/node` +## 1.18.1 + +### Patch Changes + +- Updated dependencies: + - `@remix-run/server-runtime@1.18.1` + +## 1.18.0 + +### Patch Changes + +- Updated dependencies: + - `@remix-run/server-runtime@1.18.0` + ## 1.17.1 ### Patch Changes diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 001096c2473..3c6f4b275db 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,6 +1,6 @@ { "name": "@remix-run/node", - "version": "1.17.1", + "version": "1.18.1", "description": "Node.js platform abstractions for Remix", "bugs": { "url": "https://github.com/remix-run/remix/issues" @@ -17,7 +17,7 @@ "./install.js" ], "dependencies": { - "@remix-run/server-runtime": "1.17.1", + "@remix-run/server-runtime": "1.18.1", "@remix-run/web-fetch": "^4.3.4", "@remix-run/web-file": "^3.0.2", "@remix-run/web-stream": "^1.0.3", diff --git a/packages/remix-react/CHANGELOG.md b/packages/remix-react/CHANGELOG.md index 727db216582..7d392452b23 100644 --- a/packages/remix-react/CHANGELOG.md +++ b/packages/remix-react/CHANGELOG.md @@ -1,5 +1,29 @@ # `@remix-run/react` +## 1.18.1 + +### Patch Changes + +- Fix reload loops in scenarios where CDNs ignore search params ([#6707](https://github.com/remix-run/remix/pull/6707)) +- Updated dependencies: + - [`react-router-dom@6.14.1`](https://github.com/remix-run/react-router/releases/tag/react-router%406.14.1) + - [`@remix-run/router@1.7.1`](https://github.com/remix-run/react-router/blob/main/packages/router/CHANGELOG.md#171) + +## 1.18.0 + +### Minor Changes + +- stabilize v2 dev server ([#6615](https://github.com/remix-run/remix/pull/6615)) +- Support `application/json` and `text/plain` submission encodings in `useSubmit`/`fetcher.submit` ([#6570](https://github.com/remix-run/remix/pull/6570)) +- Add support for `` to prefetch links when they enter the viewport via an [Intersection Observer](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) ([#6433](https://github.com/remix-run/remix/pull/6433)) + +### Patch Changes + +- Bump router 6.14.0-pre.1 ([#6662](https://github.com/remix-run/remix/pull/6662)) +- Detect mismatches between the initially loaded URL and the URL at the time we hydrate and trigger a hard reload if they do not match. This is an edge-case that can happen when the network is slowish and the user clicks forward into a Remix app and then clicks forward again while the initial JS chunks are loading. ([#6409](https://github.com/remix-run/remix/pull/6409)) +- Lock in react router 6.14.0 ([#6677](https://github.com/remix-run/remix/pull/6677)) +- properly pass props to inline script tags for deferred data ([#6389](https://github.com/remix-run/remix/pull/6389)) + ## 1.17.1 ### Patch Changes diff --git a/packages/remix-react/__tests__/components-test.tsx b/packages/remix-react/__tests__/components-test.tsx index 6cb621925b1..748f4b12cf9 100644 --- a/packages/remix-react/__tests__/components-test.tsx +++ b/packages/remix-react/__tests__/components-test.tsx @@ -47,14 +47,14 @@ describe("", () => { LiveReload = require("../components").LiveReload; let { container } = render(); expect(container.querySelector("script")).toHaveTextContent( - "let port = undefined || (window.__remixContext && window.__remixContext.dev && window.__remixContext.dev.port) || 8002;" + "url.port = undefined || REMIX_DEV_ORIGIN ? new URL(REMIX_DEV_ORIGIN).port : Number(undefined) || 8002;" ); }); it("can set the port explicitly", () => { let { container } = render(); expect(container.querySelector("script")).toHaveTextContent( - "let port = 4321 || (window.__remixContext && window.__remixContext.dev && window.__remixContext.dev.port) || 8002;" + "url.port = 4321 || REMIX_DEV_ORIGIN ? new URL(REMIX_DEV_ORIGIN).port : Number(undefined) || 8002;" ); }); @@ -62,7 +62,7 @@ describe("", () => { process.env.REMIX_DEV_SERVER_WS_PORT = "1234"; let { container } = render(); expect(container.querySelector("script")).toHaveTextContent( - "let port = undefined || (window.__remixContext && window.__remixContext.dev && window.__remixContext.dev.port) || 1234;" + "url.port = undefined || REMIX_DEV_ORIGIN ? new URL(REMIX_DEV_ORIGIN).port : Number(1234) || 8002;" ); }); diff --git a/packages/remix-react/__tests__/hook-types-test.tsx b/packages/remix-react/__tests__/hook-types-test.tsx index c0bf59063b0..28ffd592f00 100644 --- a/packages/remix-react/__tests__/hook-types-test.tsx +++ b/packages/remix-react/__tests__/hook-types-test.tsx @@ -3,25 +3,30 @@ import type { TypedResponse, } from "@remix-run/server-runtime"; -import type { useLoaderData } from "../components"; +import type { useLoaderData, useRouteLoaderData } from "../components"; function isEqual( arg: A extends B ? (B extends A ? true : false) : false ): void {} type LoaderData = ReturnType>; +type RouteLoaderData = ReturnType>; describe("useLoaderData", () => { it("supports plain data type", () => { type AppData = { hello: string }; type response = LoaderData; + type routeResponse = RouteLoaderData; isEqual(true); + isEqual(true); }); it("supports plain Response", () => { type Loader = (args: any) => Response; type response = LoaderData; + type routeResponse = RouteLoaderData; isEqual(true); + isEqual(true); }); it("infers type regardless of redirect", () => { @@ -29,31 +34,41 @@ describe("useLoaderData", () => { args: any ) => TypedResponse<{ id: string }> | TypedResponse; type response = LoaderData; + type routeResponse = RouteLoaderData; isEqual(true); + isEqual(true); }); it("supports Response-returning loader", () => { type Loader = (args: any) => TypedResponse<{ hello: string }>; type response = LoaderData; + type routeResponse = RouteLoaderData; isEqual(true); + isEqual(true); }); it("supports async Response-returning loader", () => { type Loader = (args: any) => Promise>; type response = LoaderData; + type routeResponse = RouteLoaderData; isEqual(true); + isEqual(true); }); it("supports data-returning loader", () => { type Loader = (args: any) => { hello: string }; type response = LoaderData; + type routeResponse = RouteLoaderData; isEqual(true); + isEqual(true); }); it("supports async data-returning loader", () => { type Loader = (args: any) => Promise<{ hello: string }>; type response = LoaderData; + type routeResponse = RouteLoaderData; isEqual(true); + isEqual(true); }); }); @@ -61,41 +76,53 @@ describe("type serializer", () => { it("converts Date to string", () => { type AppData = { hello: Date }; type response = LoaderData; + type routeResponse = RouteLoaderData; isEqual(true); + isEqual(true); }); it("supports custom toJSON", () => { type AppData = { toJSON(): { data: string[] } }; type response = LoaderData; + type routeResponse = RouteLoaderData; isEqual(true); + isEqual(true); }); it("supports recursion", () => { type AppData = { dob: Date; parent: AppData }; type SerializedAppData = { dob: string; parent: SerializedAppData }; type response = LoaderData; + type routeResponse = RouteLoaderData; isEqual(true); + isEqual(true); }); it("supports tuples and arrays", () => { type AppData = { arr: Date[]; tuple: [string, number, Date]; empty: [] }; type response = LoaderData; + type routeResponse = RouteLoaderData; isEqual< response, { arr: string[]; tuple: [string, number, string]; empty: [] } >(true); + isEqual(true); }); it("transforms unserializables to null in arrays", () => { type AppData = [Function, symbol, undefined]; type response = LoaderData; + type routeResponse = RouteLoaderData; isEqual(true); + isEqual(true); }); it("transforms unserializables to never in objects", () => { type AppData = { arg1: Function; arg2: symbol; arg3: undefined }; type response = LoaderData; + type routeResponse = RouteLoaderData; isEqual(true); + isEqual(true); }); it("supports class instances", () => { @@ -105,7 +132,9 @@ describe("type serializer", () => { } type Loader = (args: any) => TypedResponse; type response = LoaderData; + type routeResponse = RouteLoaderData; isEqual(true); + isEqual(true); }); it("makes keys optional if the value is undefined", () => { @@ -115,7 +144,9 @@ describe("type serializer", () => { arg3: undefined; }; type response = LoaderData; + type routeResponse = RouteLoaderData; isEqual(true); + isEqual(true); }); it("allows data key in value", () => { @@ -131,7 +162,9 @@ describe("deferred type serializer", () => { args: any ) => TypedDeferredData<{ hello: string; lazy: Promise }>; type response = LoaderData; + type routeResponse = RouteLoaderData; isEqual }>(true); + isEqual(true); }); it("supports asynchronous loader", () => { @@ -139,7 +172,9 @@ describe("deferred type serializer", () => { args: any ) => Promise }>>; type response = LoaderData; + type routeResponse = RouteLoaderData; isEqual }>(true); + isEqual(true); }); it("supports synchronous loader with deferred object result", () => { @@ -147,7 +182,9 @@ describe("deferred type serializer", () => { args: any ) => TypedDeferredData<{ hello: string; lazy: Promise<{ a: number }> }>; type response = LoaderData; + type routeResponse = RouteLoaderData; isEqual }>(true); + isEqual(true); }); it("supports asynchronous loader with deferred object result", () => { @@ -157,7 +194,9 @@ describe("deferred type serializer", () => { TypedDeferredData<{ hello: string; lazy: Promise<{ a: number }> }> >; type response = LoaderData; + type routeResponse = RouteLoaderData; isEqual }>(true); + isEqual(true); }); it("converts Date to string", () => { @@ -167,7 +206,9 @@ describe("deferred type serializer", () => { TypedDeferredData<{ hello: Date; lazy: Promise<{ a: Date }> }> >; type response = LoaderData; + type routeResponse = RouteLoaderData; isEqual }>(true); + isEqual(true); }); it("supports custom toJSON", () => { @@ -178,10 +219,12 @@ describe("deferred type serializer", () => { TypedDeferredData<{ hello: AppData; lazy: Promise<{ a: AppData }> }> >; type response = LoaderData; + type routeResponse = RouteLoaderData; isEqual< response, { hello: { data: string[] }; lazy: Promise<{ a: { data: string[] } }> } >(true); + isEqual(true); }); it("supports recursion", () => { @@ -191,10 +234,12 @@ describe("deferred type serializer", () => { args: any ) => Promise }>>; type response = LoaderData; + type routeResponse = RouteLoaderData; isEqual< response, { hello: SerializedAppData; lazy: Promise } >(true); + isEqual(true); }); it("supports tuples and arrays", () => { @@ -208,10 +253,12 @@ describe("deferred type serializer", () => { args: any ) => Promise }>>; type response = LoaderData; + type routeResponse = RouteLoaderData; isEqual< response, { hello: SerializedAppData; lazy: Promise } >(true); + isEqual(true); }); it("transforms unserializables to null in arrays", () => { @@ -221,10 +268,12 @@ describe("deferred type serializer", () => { args: any ) => Promise }>>; type response = LoaderData; + type routeResponse = RouteLoaderData; isEqual< response, { hello: SerializedAppData; lazy: Promise } >(true); + isEqual(true); }); it("transforms unserializables to never in objects", () => { @@ -233,7 +282,9 @@ describe("deferred type serializer", () => { args: any ) => Promise }>>; type response = LoaderData; + type routeResponse = RouteLoaderData; isEqual }>(true); + isEqual(true); }); it("supports class instances", () => { @@ -245,10 +296,12 @@ describe("deferred type serializer", () => { args: any ) => Promise }>>; type response = LoaderData; + type routeResponse = RouteLoaderData; isEqual< response, { hello: { arg: string }; lazy: Promise<{ arg: string }> } >(true); + isEqual(true); }); it("makes keys optional if the value is undefined", () => { @@ -262,10 +315,12 @@ describe("deferred type serializer", () => { args: any ) => Promise }>>; type response = LoaderData; + type routeResponse = RouteLoaderData; isEqual< response, { hello: SerializedAppData; lazy: Promise } >(true); + isEqual(true); }); it("allows data key in value", () => { diff --git a/packages/remix-react/browser.tsx b/packages/remix-react/browser.tsx index c566a14ee57..3bad7c70855 100644 --- a/packages/remix-react/browser.tsx +++ b/packages/remix-react/browser.tsx @@ -181,17 +181,19 @@ export function RemixBrowser(_props: RemixBrowserProps): ReactElement { }, }); - // Hard reload if the URL we tried to load is not the current URL. - // This is usually the result of 2 rapid backwards/forward clicks from an + // Hard reload if the path we tried to load is not the current path. + // This is usually the result of 2 rapid back/forward clicks from an // external site into a Remix app, where we initially start the load for // one URL and while the JS chunks are loading a second forward click moves - // us to a new URL - let initialUrl = window.__remixContext.url; - let hydratedUrl = window.location.pathname + window.location.search; - if (initialUrl !== hydratedUrl) { + // us to a new URL. Avoid comparing search params because of CDNs which + // can be configured to ignore certain params and only pathname is relevant + // towards determining the route matches. + let initialPathname = window.__remixContext.url; + let hydratedPathname = window.location.pathname; + if (initialPathname !== hydratedPathname) { let errorMsg = - `Initial URL (${initialUrl}) does not match URL at time of hydration ` + - `(${hydratedUrl}), reloading page...`; + `Initial URL (${initialPathname}) does not match URL at time of hydration ` + + `(${hydratedPathname}), reloading page...`; console.error(errorMsg); window.location.reload(); } diff --git a/packages/remix-react/components.tsx b/packages/remix-react/components.tsx index c77492a548d..039d8211f46 100644 --- a/packages/remix-react/components.tsx +++ b/packages/remix-react/components.tsx @@ -31,6 +31,7 @@ import { useFetchers as useFetchersRR, useActionData as useActionDataRR, useLoaderData as useLoaderDataRR, + useRouteLoaderData as useRouteLoaderDataRR, useMatches as useMatchesRR, useLocation, useNavigation, @@ -1289,6 +1290,17 @@ export function useLoaderData(): SerializeFrom { return useLoaderDataRR() as SerializeFrom; } +/** + * Returns the loaderData for the given routeId. + * + * @see https://remix.run/hooks/use-route-loader-data + */ +export function useRouteLoaderData( + routeId: string +): SerializeFrom | undefined { + return useRouteLoaderDataRR(routeId) as SerializeFrom | undefined; +} + /** * Returns the JSON parsed data from the current route's `action`. * @@ -1782,7 +1794,6 @@ export const LiveReload = process.env.NODE_ENV !== "development" ? () => null : function LiveReload({ - // TODO: remove REMIX_DEV_SERVER_WS_PORT in v2 port, timeoutMs = 1000, nonce = undefined, @@ -1799,13 +1810,25 @@ export const LiveReload = dangerouslySetInnerHTML={{ __html: js` function remixLiveReloadConnect(config) { - let protocol = location.protocol === "https:" ? "wss:" : "ws:"; - let host = location.hostname; - 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); + let REMIX_DEV_ORIGIN = ${JSON.stringify( + process.env.REMIX_DEV_ORIGIN + )}; + let protocol = + REMIX_DEV_ORIGIN ? new URL(REMIX_DEV_ORIGIN).protocol.replace(/^http/, "ws") : + location.protocol === "https:" ? "wss:" : "ws:"; // remove in v2? + let hostname = location.hostname; + let url = new URL(protocol + "//" + hostname + "/socket"); + + url.port = + ${port} || + REMIX_DEV_ORIGIN ? new URL(REMIX_DEV_ORIGIN).port : + Number(${ + // TODO: remove in v2 + process.env.REMIX_DEV_SERVER_WS_PORT + }) || + 8002; + + let ws = new WebSocket(url.href); ws.onmessage = async (message) => { let event = JSON.parse(message.data); if (event.type === "LOG") { diff --git a/packages/remix-react/errors.ts b/packages/remix-react/errors.ts index d8605ccbd4a..80bee97af66 100644 --- a/packages/remix-react/errors.ts +++ b/packages/remix-react/errors.ts @@ -33,9 +33,26 @@ export function deserializeErrors( val.internal === true ); } else if (val && val.__type === "Error") { - let error = new Error(val.message); - error.stack = val.stack; - serialized[key] = error; + // Attempt to reconstruct the right type of Error (i.e., ReferenceError) + if (val.__subType) { + let ErrorConstructor = window[val.__subType]; + if (typeof ErrorConstructor === "function") { + try { + // @ts-expect-error + let error = new ErrorConstructor(val.message); + error.stack = val.stack; + serialized[key] = error; + } catch (e) { + // no-op - fall through and create a normal Error + } + } + } + + if (serialized[key] == null) { + let error = new Error(val.message); + error.stack = val.stack; + serialized[key] = error; + } } else { serialized[key] = val; } diff --git a/packages/remix-react/index.tsx b/packages/remix-react/index.tsx index 54b17227d33..5402414995a 100644 --- a/packages/remix-react/index.tsx +++ b/packages/remix-react/index.tsx @@ -34,7 +34,6 @@ export { useResolvedPath, useRevalidator, useRouteError, - useRouteLoaderData, useSearchParams, useSubmit, unstable_useBlocker, @@ -61,6 +60,7 @@ export { useFetcher, useFetchers, useLoaderData, + useRouteLoaderData, useMatches, useActionData, RemixContext as UNSAFE_RemixContext, diff --git a/packages/remix-react/package.json b/packages/remix-react/package.json index 28de8216fba..ee8706cb148 100644 --- a/packages/remix-react/package.json +++ b/packages/remix-react/package.json @@ -1,6 +1,6 @@ { "name": "@remix-run/react", - "version": "1.17.1", + "version": "1.18.1", "description": "React DOM bindings for Remix", "bugs": { "url": "https://github.com/remix-run/remix/issues" @@ -16,11 +16,11 @@ "typings": "dist/index.d.ts", "module": "dist/esm/index.js", "dependencies": { - "@remix-run/router": "1.7.0-pre.0", - "react-router-dom": "6.14.0-pre.0" + "@remix-run/router": "1.7.1", + "react-router-dom": "6.14.1" }, "devDependencies": { - "@remix-run/server-runtime": "1.17.1", + "@remix-run/server-runtime": "1.18.1", "@testing-library/jest-dom": "^5.16.2", "@testing-library/react": "^13.3.0", "@types/react": "^18.0.15", diff --git a/packages/remix-serve/CHANGELOG.md b/packages/remix-serve/CHANGELOG.md index fa4b178c129..7ccbb5fa034 100644 --- a/packages/remix-serve/CHANGELOG.md +++ b/packages/remix-serve/CHANGELOG.md @@ -1,5 +1,26 @@ # `@remix-run/serve` +## 1.18.1 + +### Patch Changes + +- Updated dependencies: + - `@remix-run/node@1.18.1` + - `@remix-run/express@1.18.1` + +## 1.18.0 + +### Minor Changes + +- stabilize v2 dev server ([#6615](https://github.com/remix-run/remix/pull/6615)) + +### Patch Changes + +- fix(types): better tuple serialization types ([#6616](https://github.com/remix-run/remix/pull/6616)) +- Updated dependencies: + - `@remix-run/node@1.18.0` + - `@remix-run/express@1.18.0` + ## 1.17.1 ### Patch Changes diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 19649c4e6eb..f7e3fabe395 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,6 +1,6 @@ { "name": "@remix-run/serve", - "version": "1.17.1", + "version": "1.18.1", "description": "Production application server for Remix", "bugs": { "url": "https://github.com/remix-run/remix/issues" @@ -17,8 +17,8 @@ "remix-serve": "dist/cli.js" }, "dependencies": { - "@remix-run/express": "1.17.1", - "@remix-run/node": "1.17.1", + "@remix-run/express": "1.18.1", + "@remix-run/node": "1.18.1", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/CHANGELOG.md b/packages/remix-server-runtime/CHANGELOG.md index ef35c1fbe37..ce703a8d859 100644 --- a/packages/remix-server-runtime/CHANGELOG.md +++ b/packages/remix-server-runtime/CHANGELOG.md @@ -1,5 +1,28 @@ # `@remix-run/server-runtime` +## 1.18.1 + +### Patch Changes + +- Fix reload loops in scenarios where CDNs ignore search params ([#6707](https://github.com/remix-run/remix/pull/6707)) +- Avoid circular references and infinite recursion in types ([#6736](https://github.com/remix-run/remix/pull/6736)) + - "Pretty" or simplified Typescript types are evaluated by eagerly resolving types. For complex types with circular references, this can cause TS to recurse infinitely. + - To fix this, pretty types are reverted as a built-in DX feature of `useLoaderData`, `useActionData`, etc... +- Updated dependencies: + - [`@remix-run/router@1.7.1`](https://github.com/remix-run/react-router/blob/main/packages/router/CHANGELOG.md#171) + +## 1.18.0 + +### Minor Changes + +- stabilize v2 dev server ([#6615](https://github.com/remix-run/remix/pull/6615)) + +### Patch Changes + +- Fix typing issues when using React 17 stemming from `@remix/server-runtime` including `@types/react` as a `devDependency` when it doesn't actually do anything React-specific and was just re-exporting `ComponentType` in values such as `CatchBoundaryComponent`/`ErrorBoundaryComponent`/`V2_ErrorBoundaryComponent`. These types are more correctly exported from `@remix-run/react` which is React-aware so that is the correct place to be importing those types from. In order to avoid breaking existing builds, the types in `@remix/server-runtime` have been loosened to `any` and `@deprecated` warnings have been added informing users to switch to the corresponding types in `@remix-run/react`. ([#5713](https://github.com/remix-run/remix/pull/5713)) +- fix(types): better tuple serialization types ([#6616](https://github.com/remix-run/remix/pull/6616)) +- Move `@types/cookie` to `dependencies` since we re-export types from there ([#5713](https://github.com/remix-run/remix/pull/5713)) + ## 1.17.1 No significant changes to this package were made in this release. [See the releases page on GitHub](https://github.com/remix-run/remix/releases/tag/remix%401.17.1) for an overview of all changes in v1.17.1. diff --git a/packages/remix-server-runtime/build.ts b/packages/remix-server-runtime/build.ts index 7a5cbc5c606..00c5e82980a 100644 --- a/packages/remix-server-runtime/build.ts +++ b/packages/remix-server-runtime/build.ts @@ -15,7 +15,6 @@ export interface ServerBuild { publicPath: string; assetsBuildDirectory: string; future: FutureConfig; - dev?: { port: number }; } export interface HandleDocumentRequestFunction { diff --git a/packages/remix-server-runtime/dev.ts b/packages/remix-server-runtime/dev.ts index 2163e36b4d3..f6da046a524 100644 --- a/packages/remix-server-runtime/dev.ts +++ b/packages/remix-server-runtime/dev.ts @@ -1,16 +1,25 @@ import type { ServerBuild } from "./build"; -export function broadcastDevReady(build: ServerBuild, origin?: string) { +export async function broadcastDevReady(build: ServerBuild, origin?: string) { origin ??= process.env.REMIX_DEV_HTTP_ORIGIN; if (!origin) throw Error("Dev server origin not set"); + let url = new URL(origin); + url.pathname = "ping"; - fetch(`${origin}/ping`, { + let response = await fetch(url.href, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ buildHash: build.assets.version }), }).catch((error) => { - console.error(`Could not reach Remix dev server at ${origin}`); + console.error(`Could not reach Remix dev server at ${url}`); + throw error; }); + if (!response.ok) { + console.error( + `Could not reach Remix dev server at ${url} (${response.status})` + ); + throw Error(await response.text()); + } } export function logDevReady(build: ServerBuild) { diff --git a/packages/remix-server-runtime/errors.ts b/packages/remix-server-runtime/errors.ts index 1422f8c5e74..95fb143c499 100644 --- a/packages/remix-server-runtime/errors.ts +++ b/packages/remix-server-runtime/errors.ts @@ -109,6 +109,15 @@ export function serializeErrors( message: sanitized.message, stack: sanitized.stack, __type: "Error", + // If this is a subclass (i.e., ReferenceError), send up the type so we + // can re-create the same type during hydration. This will only apply + // in dev mode since all production errors are sanitized to normal + // Error instances + ...(sanitized.name !== "Error" + ? { + __subType: sanitized.name, + } + : {}), }; } else { serialized[key] = val; diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 222e27a0d57..8ed5adf6cc2 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,6 +1,6 @@ { "name": "@remix-run/server-runtime", - "version": "1.17.1", + "version": "1.18.1", "description": "Server runtime for Remix", "bugs": { "url": "https://github.com/remix-run/remix/issues" @@ -16,7 +16,7 @@ "typings": "dist/index.d.ts", "module": "dist/esm/index.js", "dependencies": { - "@remix-run/router": "1.7.0-pre.0", + "@remix-run/router": "1.7.1", "@types/cookie": "^0.4.1", "@web3-storage/multipart-parser": "^1.0.0", "cookie": "^0.4.1", diff --git a/packages/remix-server-runtime/serialize.ts b/packages/remix-server-runtime/serialize.ts index a7ecbf1e4fb..6fd357b315c 100644 --- a/packages/remix-server-runtime/serialize.ts +++ b/packages/remix-server-runtime/serialize.ts @@ -1,10 +1,6 @@ import type { AppData } from "./data"; import type { TypedDeferredData, TypedResponse } from "./responses"; -// force Typescript to simplify the type -type Pretty = { [K in keyof T]: T[K] } & {}; -type PrettyTransform = [T] extends [U] ? T : Pretty; - type JsonPrimitive = | string | number @@ -29,9 +25,9 @@ type Serialize = T extends NonJsonPrimitive ? never : T extends { toJSON(): infer U } ? U : T extends [] ? [] : - T extends [unknown, ...unknown[]] ? PrettyTransform> : + T extends [unknown, ...unknown[]] ? SerializeTuple : T extends ReadonlyArray ? (U extends NonJsonPrimitive ? null : Serialize)[] : - T extends object ? PrettyTransform>> : + T extends object ? SerializeObject> : never ; diff --git a/packages/remix-server-runtime/server.ts b/packages/remix-server-runtime/server.ts index e26d4edb08b..eb30cb36f18 100644 --- a/packages/remix-server-runtime/server.ts +++ b/packages/remix-server-runtime/server.ts @@ -289,14 +289,13 @@ async function handleDocumentRequestRR( routeModules: createEntryRouteModules(build.routes), staticHandlerContext: context, serverHandoffString: createServerHandoffString({ - url: context.location.pathname + context.location.search, + url: context.location.pathname, state: { loaderData: context.loaderData, actionData: context.actionData, errors: serializeErrors(context.errors, serverMode), }, future: build.future, - dev: build.dev, }), future: build.future, }; @@ -335,7 +334,7 @@ async function handleDocumentRequestRR( ...entryContext, staticHandlerContext: context, serverHandoffString: createServerHandoffString({ - url: context.location.pathname + context.location.search, + url: context.location.pathname, state: { loaderData: context.loaderData, actionData: context.actionData, diff --git a/packages/remix-server-runtime/serverHandoff.ts b/packages/remix-server-runtime/serverHandoff.ts index 07bdee23107..583f2a21db1 100644 --- a/packages/remix-server-runtime/serverHandoff.ts +++ b/packages/remix-server-runtime/serverHandoff.ts @@ -21,7 +21,6 @@ export function createServerHandoffString(serverHandoff: { state: ValidateShape; url: string; future: FutureConfig; - 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 `` element. diff --git a/packages/remix-testing/CHANGELOG.md b/packages/remix-testing/CHANGELOG.md index b95f589baa3..97c92fa91e9 100644 --- a/packages/remix-testing/CHANGELOG.md +++ b/packages/remix-testing/CHANGELOG.md @@ -1,5 +1,27 @@ # `@remix-run/testing` +## 1.18.1 + +### Patch Changes + +- Updated dependencies: + - `@remix-run/react@1.18.1` + - `@remix-run/node@1.18.1` + - [`react-router-dom@6.14.1`](https://github.com/remix-run/react-router/releases/tag/react-router%406.14.1) + - [`@remix-run/router@1.7.1`](https://github.com/remix-run/react-router/blob/main/packages/router/CHANGELOG.md#171) + +## 1.18.0 + +### Minor Changes + +- stabilize v2 dev server ([#6615](https://github.com/remix-run/remix/pull/6615)) + +### Patch Changes + +- Updated dependencies: + - `@remix-run/react@1.18.0` + - `@remix-run/node@1.18.0` + ## 1.17.1 ### Patch Changes diff --git a/packages/remix-testing/package.json b/packages/remix-testing/package.json index 776a02d0424..3f03c752429 100644 --- a/packages/remix-testing/package.json +++ b/packages/remix-testing/package.json @@ -1,6 +1,6 @@ { "name": "@remix-run/testing", - "version": "1.17.1", + "version": "1.18.1", "description": "Testing utilities for Remix apps", "homepage": "https://remix.run", "bugs": { @@ -16,10 +16,10 @@ "typings": "./dist/index.d.ts", "module": "./dist/esm/index.js", "dependencies": { - "@remix-run/node": "1.17.1", - "@remix-run/react": "1.17.1", - "@remix-run/router": "1.7.0-pre.0", - "react-router-dom": "6.14.0-pre.0" + "@remix-run/node": "1.18.1", + "@remix-run/react": "1.18.1", + "@remix-run/router": "1.7.1", + "react-router-dom": "6.14.1" }, "devDependencies": { "@types/node": "^18.11.9", diff --git a/packages/remix-vercel/CHANGELOG.md b/packages/remix-vercel/CHANGELOG.md index 71dc405aab1..4fd22f34afc 100644 --- a/packages/remix-vercel/CHANGELOG.md +++ b/packages/remix-vercel/CHANGELOG.md @@ -1,5 +1,20 @@ # `@remix-run/vercel` +## 1.18.1 + +### Patch Changes + +- Updated dependencies: + - `@remix-run/node@1.18.1` + +## 1.18.0 + +### Patch Changes + +- Show deprecation warning when using `@remix-run/vercel`, since Vercel now provides built-in support for Remix apps ([#5964](https://github.com/remix-run/remix/pull/5964)) ([`e222ded5c`](https://github.com/remix-run/remix/commit/e222ded5c46feca1ba47585ed8608dd2fb5a876b)) +- Updated dependencies: + - `@remix-run/node@1.18.0` + ## 1.17.1 ### Patch Changes diff --git a/packages/remix-vercel/package.json b/packages/remix-vercel/package.json index 4e21b6061bb..f441c6c8d24 100644 --- a/packages/remix-vercel/package.json +++ b/packages/remix-vercel/package.json @@ -1,6 +1,6 @@ { "name": "@remix-run/vercel", - "version": "1.17.1", + "version": "1.18.1", "description": "Vercel server request handler for Remix", "bugs": { "url": "https://github.com/remix-run/remix/issues" @@ -14,7 +14,7 @@ "main": "dist/index.js", "typings": "dist/index.d.ts", "dependencies": { - "@remix-run/node": "1.17.1" + "@remix-run/node": "1.18.1" }, "devDependencies": { "@types/supertest": "^2.0.10", diff --git a/packages/remix/package.json b/packages/remix/package.json index c3523744ed3..ff9a20b0823 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -1,6 +1,6 @@ { "name": "remix", - "version": "1.17.1", + "version": "1.18.1", "description": "A framework for building better websites", "homepage": "https://remix.run", "bugs": { diff --git a/scripts/bump-router-versions.sh b/scripts/bump-router-versions.sh index fae157781fe..f4226ae6c47 100755 --- a/scripts/bump-router-versions.sh +++ b/scripts/bump-router-versions.sh @@ -1,13 +1,20 @@ #!/bin/bash -VERSION="${1}" - -if [ "${VERSION}" == "" ]; then - VERSION="latest" +ROUTER_VERSION="${1}" +RR_VERSION="${2}" + +if [ "${ROUTER_VERSION}" == "" ]; then + ROUTER_VERSION="latest" + RR_VERSION="latest" +elif [ "${RR_VERSION}" == "" ]; then + RR_VERSION="${ROUTER_VERSION}" fi -echo "Updating all router versions to ${VERSION}" +echo "Updating the React Router dependencies to the following versions:" +echo " @remix-run/router -> ${ROUTER_VERSION}" +echo " react-router-dom -> ${RR_VERSION}" +echo "" if [ ! -d "packages/remix-server-runtime" ]; then echo "Must be run from the remix repository" @@ -17,19 +24,19 @@ fi set -x cd packages/remix-server-runtime -yarn add -E @remix-run/router@${VERSION} +yarn add -E @remix-run/router@${ROUTER_VERSION} cd ../.. # cd packages/remix-express -# yarn add -E @remix-run/router@${VERSION} +# yarn add -E @remix-run/router@${ROUTER_VERSION} # cd ../.. cd packages/remix-react -yarn add -E @remix-run/router@${VERSION} react-router-dom@${VERSION} +yarn add -E @remix-run/router@${ROUTER_VERSION} react-router-dom@${RR_VERSION} cd ../.. cd packages/remix-testing -yarn add -E @remix-run/router@${VERSION} react-router-dom@${VERSION} +yarn add -E @remix-run/router@${ROUTER_VERSION} react-router-dom@${RR_VERSION} cd ../.. set +x \ No newline at end of file diff --git a/templates/arc/.eslintrc.js b/templates/arc/.eslintrc.cjs similarity index 100% rename from templates/arc/.eslintrc.js rename to templates/arc/.eslintrc.cjs diff --git a/templates/arc/.gitignore b/templates/arc/.gitignore index 592d769e5d8..c8f4835d63e 100644 --- a/templates/arc/.gitignore +++ b/templates/arc/.gitignore @@ -1,7 +1,9 @@ +.DS_Store node_modules /.cache -/server/index.js +/server/index.mjs +/server/index.mjs.map /public/build preferences.arc sam.json diff --git a/templates/arc/README.md b/templates/arc/README.md index bf6f90a08d3..2c43f6611c6 100644 --- a/templates/arc/README.md +++ b/templates/arc/README.md @@ -4,6 +4,18 @@ ## Development +Create a `preferences.arc` file in the root with the following contents: + +``` +@sandbox +livereload false + +# NODE_ENV development is required when running the dev server +@env +testing + NODE_ENV development +``` + The following command will run two processes during development when using Architect as your server. - Your Architect server sandbox diff --git a/templates/arc/app.arc b/templates/arc/app.arc index 4e472fca56b..79428bd94fe 100644 --- a/templates/arc/app.arc +++ b/templates/arc/app.arc @@ -8,6 +8,10 @@ remix-architect-app @static +@plugins +plugin-remix + src plugin-remix.js + # @aws # profile default # region us-west-1 diff --git a/templates/arc/app/root.tsx b/templates/arc/app/root.tsx index 8cb74a167f8..f10a63cf481 100644 --- a/templates/arc/app/root.tsx +++ b/templates/arc/app/root.tsx @@ -19,6 +19,7 @@ export default function App() { + diff --git a/templates/arc/package.json b/templates/arc/package.json index e2d5ad3866b..90233f2cab3 100644 --- a/templates/arc/package.json +++ b/templates/arc/package.json @@ -1,11 +1,10 @@ { "private": true, "sideEffects": false, + "type": "module", "scripts": { "build": "remix build", - "dev:remix": "remix watch", - "dev:arc": "cross-env NODE_ENV=development arc sandbox", - "dev": "npm-run-all build --parallel \"dev:*\"", + "dev": "remix dev -c \"arc sandbox -e testing\"", "start": "cross-env NODE_ENV=production arc sandbox", "typecheck": "tsc" }, @@ -20,13 +19,12 @@ "react-dom": "^18.2.0" }, "devDependencies": { - "@architect/architect": "^10.11.2", + "@architect/architect": "^10.12.1", "@remix-run/dev": "*", "@remix-run/eslint-config": "*", "@types/react": "^18.0.35", "@types/react-dom": "^18.0.11", "eslint": "^8.38.0", - "npm-run-all": "^4.1.5", "typescript": "^5.0.4" }, "engines": { diff --git a/templates/arc/plugin-remix.js b/templates/arc/plugin-remix.js new file mode 100644 index 00000000000..acbadff18e4 --- /dev/null +++ b/templates/arc/plugin-remix.js @@ -0,0 +1,38 @@ +// This should eventually be a npm package, but for now it lives here. +// It's job is to notify the remix dev server of the version of the running +// app to trigger HMR / HDR. + +import * as fs from "node:fs"; +import * as path from "node:path"; + +import { logDevReady } from "@remix-run/node"; + +const buildPath = "server/index.mjs"; + +let lastTimeout; + +export default { + sandbox: { + async watcher() { + if (lastTimeout) { + clearTimeout(lastTimeout); + } + + lastTimeout = setTimeout(async () => { + const contents = fs.readFileSync( + path.resolve(process.cwd(), buildPath), + "utf8" + ); + const manifestMatches = contents.matchAll(/manifest-([A-f0-9]+)\.js/g); + const sent = new Set(); + for (const match of manifestMatches) { + const buildHash = match[1]; + if (!sent.has(buildHash)) { + sent.add(buildHash); + logDevReady({ assets: { version: buildHash } }); + } + } + }, 300); + }, + }, +}; diff --git a/templates/arc/remix.config.js b/templates/arc/remix.config.js index b76efda0f3a..c0345763aae 100644 --- a/templates/arc/remix.config.js +++ b/templates/arc/remix.config.js @@ -1,12 +1,14 @@ /** @type {import('@remix-run/dev').AppConfig} */ -module.exports = { +export default { ignoredRouteFiles: ["**/.*"], publicPath: "/_static/build/", - server: "./server.ts", - serverBuildPath: "server/index.js", + server: "server.ts", + serverBuildPath: "server/index.mjs", // appDirectory: "app", // assetsBuildDirectory: "public/build", + serverModuleFormat: "esm", future: { + v2_dev: true, v2_errorBoundary: true, v2_headers: true, v2_meta: true, diff --git a/templates/arc/server/config.arc b/templates/arc/server/config.arc index 3d11ce0fc30..990ffe2fbe5 100644 --- a/templates/arc/server/config.arc +++ b/templates/arc/server/config.arc @@ -1,5 +1,5 @@ @aws -runtime nodejs14.x +runtime nodejs16.x # memory 1152 # timeout 30 # concurrency 1 diff --git a/templates/cloudflare-pages/.eslintrc.js b/templates/cloudflare-pages/.eslintrc.cjs similarity index 100% rename from templates/cloudflare-pages/.eslintrc.js rename to templates/cloudflare-pages/.eslintrc.cjs diff --git a/templates/cloudflare-pages/.gitignore b/templates/cloudflare-pages/.gitignore index 7c0736ebf5a..e542b26b368 100644 --- a/templates/cloudflare-pages/.gitignore +++ b/templates/cloudflare-pages/.gitignore @@ -1,3 +1,4 @@ +.DS_Store node_modules /.cache diff --git a/templates/cloudflare-pages/package.json b/templates/cloudflare-pages/package.json index 7e91782b468..982c8cd68ce 100644 --- a/templates/cloudflare-pages/package.json +++ b/templates/cloudflare-pages/package.json @@ -1,21 +1,18 @@ { "private": true, "sideEffects": false, + "type": "module", "scripts": { "build": "remix build", - "dev:remix": "remix watch", - "dev:wrangler": "cross-env NODE_ENV=development npm run wrangler", - "dev": "npm-run-all build --parallel \"dev:*\"", - "start": "cross-env NODE_ENV=production npm run wrangler", - "typecheck": "tsc", - "wrangler": "wrangler pages dev ./public" + "dev": "remix dev --no-restart -c \"npm run start\"", + "start": "wrangler pages dev --compatibility-date=2023-06-21 ./public", + "typecheck": "tsc" }, "dependencies": { "@remix-run/cloudflare": "*", "@remix-run/cloudflare-pages": "*", "@remix-run/css-bundle": "*", "@remix-run/react": "*", - "cross-env": "^7.0.3", "isbot": "^3.6.8", "react": "^18.2.0", "react-dom": "^18.2.0" @@ -27,9 +24,8 @@ "@types/react": "^18.0.35", "@types/react-dom": "^18.0.11", "eslint": "^8.38.0", - "npm-run-all": "^4.1.5", "typescript": "^5.0.4", - "wrangler": "^2.15.1" + "wrangler": "^3.1.1" }, "engines": { "node": ">=16.13.0" diff --git a/templates/cloudflare-pages/public/_headers b/templates/cloudflare-pages/public/_headers index dd69b93b392..c5129f35cd3 100644 --- a/templates/cloudflare-pages/public/_headers +++ b/templates/cloudflare-pages/public/_headers @@ -1,2 +1,4 @@ +/favicon.ico + Cache-Control: public, max-age=3600, s-maxage=3600 /build/* Cache-Control: public, max-age=31536000, immutable diff --git a/templates/cloudflare-pages/public/_routes.json b/templates/cloudflare-pages/public/_routes.json index 5826b059c08..4b57270dae9 100644 --- a/templates/cloudflare-pages/public/_routes.json +++ b/templates/cloudflare-pages/public/_routes.json @@ -1,5 +1,5 @@ { "version": 1, "include": ["/*"], - "exclude": ["/build/*"] + "exclude": ["/favicon.ico", "/build/*"] } diff --git a/templates/cloudflare-pages/remix.config.js b/templates/cloudflare-pages/remix.config.js index 6f5ffe0fffe..d31efea86fe 100644 --- a/templates/cloudflare-pages/remix.config.js +++ b/templates/cloudflare-pages/remix.config.js @@ -1,5 +1,5 @@ /** @type {import('@remix-run/dev').AppConfig} */ -module.exports = { +export default { devServerBroadcastDelay: 1000, ignoredRouteFiles: ["**/.*"], server: "./server.ts", @@ -14,6 +14,7 @@ module.exports = { // assetsBuildDirectory: "public/build", // publicPath: "/build/", future: { + v2_dev: true, v2_errorBoundary: true, v2_headers: true, v2_meta: true, diff --git a/templates/cloudflare-pages/server.ts b/templates/cloudflare-pages/server.ts index b1681c47979..d8f4dcf1425 100644 --- a/templates/cloudflare-pages/server.ts +++ b/templates/cloudflare-pages/server.ts @@ -1,8 +1,13 @@ +import { logDevReady } from "@remix-run/cloudflare"; import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages"; import * as build from "@remix-run/dev/server-build"; +if (process.env.NODE_ENV === "development") { + logDevReady(build); +} + export const onRequest = createPagesFunctionHandler({ build, - getLoadContext: (context) => context.env, + getLoadContext: (context) => ({ env: context.env }), mode: process.env.NODE_ENV, }); diff --git a/templates/cloudflare-pages/wrangler.toml b/templates/cloudflare-pages/wrangler.toml deleted file mode 100644 index ecd695e5a65..00000000000 --- a/templates/cloudflare-pages/wrangler.toml +++ /dev/null @@ -1,2 +0,0 @@ -compatibility_date = "2022-04-05" -compatibility_flags = ["streams_enable_constructors"] diff --git a/templates/cloudflare-workers/.eslintrc.js b/templates/cloudflare-workers/.eslintrc.cjs similarity index 100% rename from templates/cloudflare-workers/.eslintrc.js rename to templates/cloudflare-workers/.eslintrc.cjs diff --git a/templates/cloudflare-workers/.gitignore b/templates/cloudflare-workers/.gitignore index f0421bd7025..7c29f45a232 100644 --- a/templates/cloudflare-workers/.gitignore +++ b/templates/cloudflare-workers/.gitignore @@ -1,3 +1,4 @@ +.DS_Store node_modules /.cache diff --git a/templates/cloudflare-workers/package.json b/templates/cloudflare-workers/package.json index 9ca72d4bfcb..7cca279a472 100644 --- a/templates/cloudflare-workers/package.json +++ b/templates/cloudflare-workers/package.json @@ -1,21 +1,18 @@ { "private": true, "sideEffects": false, + "type": "module", "scripts": { "build": "remix build", - "deploy": "wrangler publish", - "dev:remix": "remix watch", - "dev:miniflare": "cross-env NODE_ENV=development miniflare ./build/index.js --watch", - "dev": "npm-run-all build --parallel \"dev:*\"", - "start": "cross-env NODE_ENV=production miniflare ./build/index.js", + "deploy": "remix build && wrangler publish", + "dev": "remix dev --no-restart -c \"npm start\"", + "start": "wrangler dev ./build/index.js", "typecheck": "tsc" }, "dependencies": { "@remix-run/cloudflare": "*", - "@remix-run/cloudflare-workers": "*", "@remix-run/css-bundle": "*", "@remix-run/react": "*", - "cross-env": "^7.0.3", "isbot": "^3.6.8", "react": "^18.2.0", "react-dom": "^18.2.0" @@ -27,10 +24,8 @@ "@types/react": "^18.0.35", "@types/react-dom": "^18.0.11", "eslint": "^8.38.0", - "miniflare": "^2.13.0", - "npm-run-all": "^4.1.5", "typescript": "^5.0.4", - "wrangler": "^2.15.1" + "wrangler": "^3.1.1" }, "engines": { "node": ">=16.13.0" diff --git a/templates/cloudflare-workers/remix.config.js b/templates/cloudflare-workers/remix.config.js index c4210dd979a..b971b575863 100644 --- a/templates/cloudflare-workers/remix.config.js +++ b/templates/cloudflare-workers/remix.config.js @@ -1,10 +1,12 @@ /** @type {import('@remix-run/dev').AppConfig} */ -module.exports = { - devServerBroadcastDelay: 1000, +export default { ignoredRouteFiles: ["**/.*"], server: "./server.ts", serverConditions: ["worker"], - serverDependenciesToBundle: "all", + serverDependenciesToBundle: [ + // bundle verything except the virtual module for the static content manifest provided by wrangler + /^(?!.*\b__STATIC_CONTENT_MANIFEST\b).*$/, + ], serverMainFields: ["browser", "module", "main"], serverMinify: true, serverModuleFormat: "esm", @@ -14,6 +16,7 @@ module.exports = { // serverBuildPath: "build/index.js", // publicPath: "/build/", future: { + v2_dev: true, v2_errorBoundary: true, v2_headers: true, v2_meta: true, diff --git a/templates/cloudflare-workers/remix.env.d.ts b/templates/cloudflare-workers/remix.env.d.ts index 425870ae632..b5be9ba3bbe 100644 --- a/templates/cloudflare-workers/remix.env.d.ts +++ b/templates/cloudflare-workers/remix.env.d.ts @@ -1,3 +1,8 @@ /// /// /// + +declare module "__STATIC_CONTENT_MANIFEST" { + const manifest: string; + export default manifest; +} diff --git a/templates/cloudflare-workers/server.ts b/templates/cloudflare-workers/server.ts index 4f4b2ad0aff..d80fdf4cd2c 100644 --- a/templates/cloudflare-workers/server.ts +++ b/templates/cloudflare-workers/server.ts @@ -1,7 +1,53 @@ -import { createEventHandler } from "@remix-run/cloudflare-workers"; +import { getAssetFromKV } from "@cloudflare/kv-asset-handler"; +import type { AppLoadContext } from "@remix-run/cloudflare"; +import { createRequestHandler, logDevReady } from "@remix-run/cloudflare"; import * as build from "@remix-run/dev/server-build"; +import __STATIC_CONTENT_MANIFEST from "__STATIC_CONTENT_MANIFEST"; -addEventListener( - "fetch", - createEventHandler({ build, mode: process.env.NODE_ENV }) -); +const MANIFEST = JSON.parse(__STATIC_CONTENT_MANIFEST); +const handleRemixRequest = createRequestHandler(build, process.env.NODE_ENV); + +if (build.dev) { + logDevReady(build); +} + +export default { + async fetch( + request: Request, + env: { + __STATIC_CONTENT: Fetcher; + }, + ctx: ExecutionContext + ): Promise { + try { + const url = new URL(request.url); + const ttl = url.pathname.startsWith("/build/") + ? 60 * 60 * 24 * 365 // 1 year + : 60 * 5; // 5 minutes + return await getAssetFromKV( + { + request, + waitUntil: ctx.waitUntil.bind(ctx), + } as FetchEvent, + { + ASSET_NAMESPACE: env.__STATIC_CONTENT, + ASSET_MANIFEST: MANIFEST, + cacheControl: { + browserTTL: ttl, + edgeTTL: ttl, + }, + } + ); + } catch (error) {} + + try { + const loadContext: AppLoadContext = { + env, + }; + return await handleRemixRequest(request, loadContext); + } catch (error) { + console.log(error); + return new Response("An unexpected error occurred", { status: 500 }); + } + }, +}; diff --git a/templates/cloudflare-workers/wrangler.toml b/templates/cloudflare-workers/wrangler.toml index 6dae0da58da..b4ddc4387f8 100644 --- a/templates/cloudflare-workers/wrangler.toml +++ b/templates/cloudflare-workers/wrangler.toml @@ -3,12 +3,7 @@ name = "remix-cloudflare-workers" workers_dev = true main = "./build/index.js" # https://developers.cloudflare.com/workers/platform/compatibility-dates -compatibility_date = "2022-04-05" -compatibility_flags = ["streams_enable_constructors"] +compatibility_date = "2023-04-20" [site] bucket = "./public" - -[build] - command = "npm run build" - diff --git a/templates/express/.eslintrc.js b/templates/express/.eslintrc.cjs similarity index 100% rename from templates/express/.eslintrc.js rename to templates/express/.eslintrc.cjs diff --git a/templates/express/package.json b/templates/express/package.json index e13c9097316..486f497a099 100644 --- a/templates/express/package.json +++ b/templates/express/package.json @@ -1,11 +1,10 @@ { "private": true, "sideEffects": false, + "type": "module", "scripts": { "build": "remix build", - "dev": "npm-run-all build --parallel \"dev:*\"", - "dev:node": "cross-env NODE_ENV=development nodemon --require dotenv/config ./server.js --watch ./server.js", - "dev:remix": "remix watch", + "dev": "remix dev --no-restart -c \"node server.js\"", "start": "cross-env NODE_ENV=production node ./server.js", "typecheck": "tsc" }, @@ -30,10 +29,8 @@ "@types/morgan": "^1.9.4", "@types/react": "^18.0.35", "@types/react-dom": "^18.0.11", - "dotenv": "^16.0.3", + "chokidar": "^3.5.3", "eslint": "^8.38.0", - "nodemon": "^2.0.22", - "npm-run-all": "^4.1.5", "typescript": "^5.0.4" }, "engines": { diff --git a/templates/express/remix.config.js b/templates/express/remix.config.js index 29287a0d890..378d0fc051f 100644 --- a/templates/express/remix.config.js +++ b/templates/express/remix.config.js @@ -1,12 +1,13 @@ /** @type {import('@remix-run/dev').AppConfig} */ -module.exports = { +export default { ignoredRouteFiles: ["**/.*"], // appDirectory: "app", // assetsBuildDirectory: "public/build", // serverBuildPath: "build/index.js", // publicPath: "/build/", - serverModuleFormat: "cjs", + serverModuleFormat: "esm", future: { + v2_dev: true, v2_errorBoundary: true, v2_headers: true, v2_meta: true, diff --git a/templates/express/server.js b/templates/express/server.js index 8d3d41a639d..10b59924ea4 100644 --- a/templates/express/server.js +++ b/templates/express/server.js @@ -1,14 +1,19 @@ -const path = require("path"); +import * as fs from "node:fs"; -const { createRequestHandler } = require("@remix-run/express"); -const { installGlobals } = require("@remix-run/node"); -const compression = require("compression"); -const express = require("express"); -const morgan = require("morgan"); +import { createRequestHandler } from "@remix-run/express"; +import { broadcastDevReady, installGlobals } from "@remix-run/node"; +import chokidar from "chokidar"; +import compression from "compression"; +import express from "express"; +import morgan from "morgan"; installGlobals(); -const BUILD_DIR = path.join(process.cwd(), "build"); +const BUILD_PATH = "./build/index.js"; +/** + * @type { import('@remix-run/node').ServerBuild | Promise } + */ +let build = await import(BUILD_PATH); const app = express(); @@ -32,34 +37,42 @@ app.use(morgan("tiny")); app.all( "*", process.env.NODE_ENV === "development" - ? (req, res, next) => { - purgeRequireCache(); - - return createRequestHandler({ - build: require(BUILD_DIR), - mode: process.env.NODE_ENV, - })(req, res, next); - } + ? createDevRequestHandler() : createRequestHandler({ - build: require(BUILD_DIR), + build, mode: process.env.NODE_ENV, }) ); -const port = process.env.PORT || 3000; -app.listen(port, () => { +const port = process.env.PORT || 3000; +app.listen(port, async () => { console.log(`Express server listening on port ${port}`); + + if (process.env.NODE_ENV === "development") { + broadcastDevReady(build); + } }); -function purgeRequireCache() { - // purge require cache on requests for "server side HMR" this won't let - // you have in-memory objects between requests in development, - // alternatively you can set up nodemon/pm2-dev to restart the server on - // file changes, but then you'll have to reconnect to databases/etc on each - // change. We prefer the DX of this, so we've included it for you by default - for (const key in require.cache) { - if (key.startsWith(BUILD_DIR)) { - delete require.cache[key]; +function createDevRequestHandler() { + const watcher = chokidar.watch(BUILD_PATH, { ignoreInitial: true }); + + watcher.on("all", async () => { + // 1. purge require cache && load updated server build + const stat = fs.statSync(BUILD_PATH); + build = import(BUILD_PATH + "?t=" + stat.mtimeMs); + // 2. tell dev server that this app server is now ready + broadcastDevReady(await build); + }); + + return async (req, res, next) => { + try { + // + return createRequestHandler({ + build: await build, + mode: "development", + })(req, res, next); + } catch (error) { + next(error); } - } + }; } diff --git a/templates/fly/remix.config.js b/templates/fly/remix.config.js index cf359205c5e..60aa1cad1d6 100644 --- a/templates/fly/remix.config.js +++ b/templates/fly/remix.config.js @@ -5,7 +5,9 @@ module.exports = { // assetsBuildDirectory: "public/build", // serverBuildPath: "build/index.js", // publicPath: "/build/", + serverModuleFormat: "cjs", future: { + v2_dev: true, v2_errorBoundary: true, v2_headers: true, v2_meta: true, diff --git a/templates/netlify/netlify.toml b/templates/netlify/netlify.toml index 6e33122159d..2ec142b0b1f 100644 --- a/templates/netlify/netlify.toml +++ b/templates/netlify/netlify.toml @@ -2,10 +2,6 @@ command = "remix build" publish = "public" -[dev] - command = "remix watch" - port = 3000 - [[redirects]] from = "/*" to = "/.netlify/functions/server" diff --git a/templates/netlify/package.json b/templates/netlify/package.json index 904d242a7ea..16d1d91e585 100644 --- a/templates/netlify/package.json +++ b/templates/netlify/package.json @@ -4,7 +4,7 @@ "scripts": { "build": "remix build", "dev": "remix dev", - "start": "cross-env NODE_ENV=production netlify dev", + "start": "netlify serve", "typecheck": "tsc" }, "dependencies": { diff --git a/templates/netlify/remix.config.js b/templates/netlify/remix.config.js index c510d3cc2d2..f54a5a8d198 100644 --- a/templates/netlify/remix.config.js +++ b/templates/netlify/remix.config.js @@ -9,7 +9,9 @@ module.exports = { // appDirectory: "app", // assetsBuildDirectory: "public/build", // publicPath: "/build/", + serverModuleFormat: "cjs", future: { + v2_dev: true, v2_errorBoundary: true, v2_headers: true, v2_meta: true, diff --git a/templates/remix/remix.config.js b/templates/remix/remix.config.js index 29287a0d890..60aa1cad1d6 100644 --- a/templates/remix/remix.config.js +++ b/templates/remix/remix.config.js @@ -7,6 +7,7 @@ module.exports = { // publicPath: "/build/", serverModuleFormat: "cjs", future: { + v2_dev: true, v2_errorBoundary: true, v2_headers: true, v2_meta: true, diff --git a/templates/vercel/remix.config.js b/templates/vercel/remix.config.js index 79e2b95fd7e..f688ad4857b 100644 --- a/templates/vercel/remix.config.js +++ b/templates/vercel/remix.config.js @@ -9,7 +9,9 @@ module.exports = { // appDirectory: "app", // assetsBuildDirectory: "public/build", // publicPath: "/build/", + serverModuleFormat: "cjs", future: { + v2_dev: true, v2_errorBoundary: true, v2_headers: true, v2_meta: true, diff --git a/yarn.lock b/yarn.lock index e226ee9eb70..742c0423a7b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2569,13 +2569,13 @@ tiny-glob "^0.2.9" tslib "^2.4.0" -"@playwright/test@^1.35.1": - version "1.35.1" - resolved "https://registry.npmjs.org/@playwright/test/-/test-1.35.1.tgz#a596b61e15b980716696f149cc7a2002f003580c" - integrity sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA== +"@playwright/test@1.33.0": + version "1.33.0" + resolved "https://registry.npmjs.org/@playwright/test/-/test-1.33.0.tgz#669ef859efb81b143dfc624eef99d1dd92a81b67" + integrity sha512-YunBa2mE7Hq4CfPkGzQRK916a4tuZoVx/EpLjeWlTVOnD4S2+fdaQZE0LJkbfhN5FTSKNLdcl7MoT5XB37bTkg== dependencies: "@types/node" "*" - playwright-core "1.35.1" + playwright-core "1.33.0" optionalDependencies: fsevents "2.3.2" @@ -2589,10 +2589,10 @@ "@changesets/types" "^5.0.0" dotenv "^8.1.0" -"@remix-run/router@1.7.0-pre.0": - version "1.7.0-pre.0" - resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.7.0-pre.0.tgz#913e0803ac70291c47537eb20397e01532dbc1a2" - integrity sha512-i0sJFZG0glzgAt6fXfkSHk5rsKOM8DSSH063gKO5sKUHvfQJZn8av4d/BMrwymr3KaFLqkl9m3FXepbEk7XzqA== +"@remix-run/router@1.7.1": + version "1.7.1" + resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.7.1.tgz#fea7ac35ae4014637c130011f59428f618730498" + integrity sha512-bgVQM4ZJ2u2CM8k1ey70o1ePFXsEzYVZoWghh6WjM8p59jQ7HxzbHW4SbnWFG7V9ig9chLawQxDTZ3xzOF8MkQ== "@remix-run/web-blob@^3.0.3", "@remix-run/web-blob@^3.0.4": version "3.0.4" @@ -2688,7 +2688,7 @@ estree-walker "^2.0.1" picomatch "^2.2.2" -"@rollup/pluginutils@^5.0.1", "@rollup/pluginutils@^5.0.2": +"@rollup/pluginutils@^5.0.1": version "5.0.2" resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz#012b8f53c71e4f6f9cb317e311df1404f56e7a33" integrity sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA== @@ -6132,12 +6132,14 @@ esbuild-openbsd-64@0.14.47: resolved "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.47.tgz#309af806db561aa886c445344d1aacab850dbdc5" integrity sha512-QpgN8ofL7B9z8g5zZqJE+eFvD1LehRlxr25PBkjyyasakm4599iroUpaj96rdqRlO2ShuyqwJdr+oNqWwTUmQw== -esbuild-plugins-node-modules-polyfill@^1.0.16: - version "1.0.16" - resolved "https://registry.npmjs.org/esbuild-plugins-node-modules-polyfill/-/esbuild-plugins-node-modules-polyfill-1.0.16.tgz#c904592749efcd185df0245f9f9386ee0b371f4f" - integrity sha512-l2kfXhuqez0HnmzDjUh1y4fwDjAP8f2nW3u145AGp8kz7tQkgVKUb4yWeKbMJvbsHcpyMzguCOkOJD/+6SGvZw== +esbuild-plugins-node-modules-polyfill@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/esbuild-plugins-node-modules-polyfill/-/esbuild-plugins-node-modules-polyfill-1.1.0.tgz#9b701b0bff618ce24ac262fd49aa49dd3da58197" + integrity sha512-pfJAbt00Luc9uuYtXGlaUrcTzf4h95Cr9Lfw+7smTFmZWtbwbrN5Hsf+La4lfD6OygHvZeefZFILOGK1ZnuyjA== dependencies: - modern-node-polyfills "^1.0.0" + "@jspm/core" "^2.0.1" + local-pkg "^0.4.3" + resolve.exports "^2.0.2" esbuild-register@^3.3.2: version "3.3.2" @@ -10212,15 +10214,6 @@ mlly@^1.0.0, mlly@^1.1.0: pkg-types "^1.0.1" ufo "^1.0.1" -modern-node-polyfills@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/modern-node-polyfills/-/modern-node-polyfills-1.0.0.tgz#f0ac8eddb49dd18f51a9f6e72d2934392bbc9720" - integrity sha512-w1yb6ae5qSUJJ2u41krkUAxs+L7i9143Qam8EuXwDMeZHxl1JN8RfTSXG4S2bt0RHIRMeoWm/HCeO0pNIHmIYQ== - dependencies: - "@jspm/core" "^2.0.1" - "@rollup/pluginutils" "^5.0.2" - local-pkg "^0.4.3" - morgan@^1.10.0: version "1.10.0" resolved "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz" @@ -10947,6 +10940,11 @@ pidtree@^0.3.0: resolved "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz" integrity sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA== +pidtree@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz#90ad7b6d42d5841e69e0a2419ef38f8883aa057c" + integrity sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g== + pify@^2.2.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz" @@ -10983,10 +10981,10 @@ pkg-types@^1.0.1: mlly "^1.0.0" pathe "^1.0.0" -playwright-core@1.35.1: - version "1.35.1" - resolved "https://registry.npmjs.org/playwright-core/-/playwright-core-1.35.1.tgz#52c1e6ffaa6a8c29de1a5bdf8cce0ce290ffb81d" - integrity sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg== +playwright-core@1.33.0: + version "1.33.0" + resolved "https://registry.npmjs.org/playwright-core/-/playwright-core-1.33.0.tgz#269efe29a927cd6d144d05f3c2d2f72bd72447a1" + integrity sha512-aizyPE1Cj62vAECdph1iaMILpT0WUDCq3E6rW6I+dleSbBoGbktvJtzS6VHkZ4DKNEOG9qJpiom/ZxO+S15LAw== pointer-symbol@^1.0.0: version "1.0.0" @@ -11437,20 +11435,20 @@ react-refresh@^0.14.0: resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz" integrity sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ== -react-router-dom@6.14.0-pre.0: - version "6.14.0-pre.0" - resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.14.0-pre.0.tgz#8e106739c776f48c0d3710aae69bac42ddf09a3f" - integrity sha512-NndsLa0FHP8BDUePOnpiQqunDaoY1e0PljjqTQDxHMeCZqf1148S8hl54ZF6rXoA4EuACszVSrURIrAhZ++ljg== +react-router-dom@6.14.1: + version "6.14.1" + resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.14.1.tgz#0ad7ba7abdf75baa61169d49f096f0494907a36f" + integrity sha512-ssF6M5UkQjHK70fgukCJyjlda0Dgono2QGwqGvuk7D+EDGHdacEN3Yke2LTMjkrpHuFwBfDFsEjGVXBDmL+bWw== dependencies: - "@remix-run/router" "1.7.0-pre.0" - react-router "6.14.0-pre.0" + "@remix-run/router" "1.7.1" + react-router "6.14.1" -react-router@6.14.0-pre.0: - version "6.14.0-pre.0" - resolved "https://registry.npmjs.org/react-router/-/react-router-6.14.0-pre.0.tgz#38d8ace45232b5d6cbcaf8b68d6af76a584f5e02" - integrity sha512-xNXcEDQ7pjKpw4ueFhmbksnVaRuyXng4OXXBg9RsM16FyJDSUUKXruygjrDiVeyGeAGjIiDDNUX/G6EwF3aS9w== +react-router@6.14.1: + version "6.14.1" + resolved "https://registry.npmjs.org/react-router/-/react-router-6.14.1.tgz#5e82bcdabf21add859dc04b1859f91066b3a5810" + integrity sha512-U4PfgvG55LdvbQjg5Y9QRWyVxIdO1LlpYT7x+tMAxd9/vmiPuJhIwdxZuIQLN/9e3O4KFDHYfR9gzGeYMasW8g== dependencies: - "@remix-run/router" "1.7.0-pre.0" + "@remix-run/router" "1.7.1" react@^18.2.0: version "18.2.0" @@ -11822,6 +11820,11 @@ resolve.exports@^1.1.0: resolved "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.0.tgz" integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ== +resolve.exports@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800" + integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg== + resolve@^1.1.6, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0: version "1.22.0" resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz"