diff --git a/.changeset/bright-rabbits-attend.md b/.changeset/bright-rabbits-attend.md deleted file mode 100644 index fc6e428fc28..00000000000 --- a/.changeset/bright-rabbits-attend.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@remix-run/server-runtime": patch ---- - -Single Fetch: Do not try to encode a `turbo-stream` body into 304 responses diff --git a/.changeset/dirty-adults-sparkle.md b/.changeset/dirty-adults-sparkle.md deleted file mode 100644 index 3db0a14dbe1..00000000000 --- a/.changeset/dirty-adults-sparkle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@remix-run/react": patch ---- - -Fog of War: Sort `/__manifest` query parameters for better caching diff --git a/.changeset/flat-wasps-heal.md b/.changeset/flat-wasps-heal.md deleted file mode 100644 index 327702b00aa..00000000000 --- a/.changeset/flat-wasps-heal.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -"@remix-run/react": patch -"@remix-run/server-runtime": patch ---- - -Single Fetch - fix revalidation behavior bugs - -- With Single Fetch, existing routes revalidate by default -- This means requests do not need special query params for granular route revalidations out of the box - i.e., `GET /a/b/c.data` -- There are two conditions that will trigger granular revalidation: - - If a route opts out of revalidation via `shouldRevalidate`, it will be excluded from the single fetch call - - If a route defines a `clientLoader` then it will be excluded from the single fetch call and if you call `serverLoader()` from your `clientLoader`, that will make a separarte HTTP call for just that route loader - i.e., `GET /a/b/c.data?_routes=routes/a` for a `clientLoader` in `routes/a.tsx` -- When one or more routes are excluded from the single fetch call, the remaining routes that have loaders are included as query params: - - For example, if A was excluded, and the `root` route and `routes/b` had a `loader` but `routes/c` did not, the single fetch request would be `GET /a/b/c.data?_routes=root,routes/a` diff --git a/.changeset/friendly-walls-brake.md b/.changeset/friendly-walls-brake.md deleted file mode 100644 index 14754c7524d..00000000000 --- a/.changeset/friendly-walls-brake.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@remix-run/dev": patch ---- - -Handle circular dependencies in modulepreload manifest generation. diff --git a/.changeset/hot-dogs-applaud.md b/.changeset/hot-dogs-applaud.md deleted file mode 100644 index 7e41800566a..00000000000 --- a/.changeset/hot-dogs-applaud.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -"@remix-run/dev": patch ---- - -(unstable) Automatic dependency optimization - -You can now opt-in to automatic dependency optimization during development by using the `future.unstable_optimizeDeps` future flag. -For details, check out the docs at [`Guides` > `Dependency optimization`](https://remix.run/docs/en/main/guides/dependency-optimization). - -For users who were previously working around this limiation, you no longer need to explicitly add routes to Vite's `optimizeDeps.entries` nor do you need to disable the `remix-dot-server` plugin. diff --git a/.changeset/khaki-ads-buy.md b/.changeset/khaki-ads-buy.md deleted file mode 100644 index 485360f60bf..00000000000 --- a/.changeset/khaki-ads-buy.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -"@remix-run/react": patch -"@remix-run/server-runtime": patch ---- - -Remove hydration URL check that was originally added for React 17 hydration issues and we no longer support React 17 - -- Reverts the logic originally added in Remix `v1.18.0` via https://github.com/remix-run/remix/pull/6409 -- This was added to resolve an issue that could arise when doing quick back/forward history navigations while JS was loading which would cause a mismatch between the server matches and client matches: https://github.com/remix-run/remix/issues/1757 -- This specific hydration issue would then cause this React v17 only looping issue: https://github.com/remix-run/remix/issues/1678 -- The URL comparison that we added in `1.18.0` turned out to be subject to false positives of it's own which could also put the user in looping scenarios -- Remix v2 upgraded it's minimal React version to v18 which eliminated the v17 hydration error loop -- React v18 handles this hydration error like any other error and does not result in a loop -- So we can remove our check and thus avoid the false-positive scenarios in which it may also trigger a loop diff --git a/.changeset/moody-cups-give.md b/.changeset/moody-cups-give.md deleted file mode 100644 index 56385725027..00000000000 --- a/.changeset/moody-cups-give.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -"@remix-run/cloudflare": patch -"@remix-run/deno": patch -"@remix-run/node": patch -"@remix-run/react": patch -"@remix-run/server-runtime": patch ---- - -(unstable) Improved typesafety for single-fetch - -If you were already using single-fetch types: - -- Remove `"@remix-run/react/future/single-fetch.d.ts"` override from `tsconfig.json` > `compilerOptions` > `types` -- Remove `defineLoader`, `defineAction`, `defineClientLoader`, `defineClientAction` helpers from your route modules -- Replace `UIMatch_SingleFetch` type helper with `UIMatch` -- Replace `MetaArgs_SingleFetch` type helper with `MetaArgs` - -Then you are ready for the new typesafety setup: - -```ts -// vite.config.ts - -declare module "@remix-run/server-runtime" { - interface Future { - unstable_singleFetch: true; // 👈 enable _types_ for single-fetch - } -} - -export default defineConfig({ - plugins: [ - remix({ - future: { - unstable_singleFetch: true, // 👈 enable single-fetch - }, - }), - ], -}); -``` - -For more information, see [Guides > Single Fetch](https://remix.run/docs/en/dev/guides/single-fetch) in our docs. diff --git a/.changeset/popular-meals-hide.md b/.changeset/popular-meals-hide.md deleted file mode 100644 index 3d2cd1cc734..00000000000 --- a/.changeset/popular-meals-hide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@remix-run/server-runtime": patch ---- - -Single Fetch: Change content type on `.data` requests to `text/x-script` to allow Cloudflare compression diff --git a/.changeset/tiny-crabs-deliver.md b/.changeset/tiny-crabs-deliver.md deleted file mode 100644 index 790961e2522..00000000000 --- a/.changeset/tiny-crabs-deliver.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@remix-run/dev": patch ---- - -Fix `dest already exists` build errors by only moving SSR assets to the client build directory when they're not already present on disk diff --git a/.changeset/two-chicken-provide.md b/.changeset/two-chicken-provide.md deleted file mode 100644 index 3bd7ff47a66..00000000000 --- a/.changeset/two-chicken-provide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@remix-run/react": patch ---- - -Clarify wording in default `HydrateFallback` console warning diff --git a/CHANGELOG.md b/CHANGELOG.md index 305c1ba26a7..7c3c644c692 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,142 +13,151 @@ We manage release notes in this file instead of the paginated Github Releases Pa Table of Contents - [Remix Releases](#remix-releases) - - [v2.11.2](#v2112) + - [v2.12.0](#v2120) + - [What's Changed](#whats-changed) + - [Future Flag for Automatic Dependency Optimization (unstable)](#future-flag-for-automatic-dependency-optimization-unstable) + - [Improved Single Fetch Type Safety (unstable)](#improved-single-fetch-type-safety-unstable) + - [Updates to Single Fetch Revalidation Behavior (unstable)](#updates-to-single-fetch-revalidation-behavior-unstable) + - [Minor Changes](#minor-changes) - [Patch Changes](#patch-changes) - [Updated Dependencies](#updated-dependencies) - [Changes by Package](#changes-by-package) - - [v2.11.1](#v2111) + - [v2.11.2](#v2112) - [Patch Changes](#patch-changes-1) + - [Updated Dependencies](#updated-dependencies-1) - [Changes by Package](#changes-by-package-1) + - [v2.11.1](#v2111) + - [Patch Changes](#patch-changes-2) + - [Changes by Package](#changes-by-package-2) - [v2.11.0](#v2110) - - [What's Changed](#whats-changed) + - [What's Changed](#whats-changed-1) - [Renamed `unstable_fogOfWar` future flag to `unstable_lazyRouteDiscovery` (unstable)](#renamed-unstable_fogofwar-future-flag-to-unstable_lazyroutediscovery-unstable) - [Removed `response` stub in Single Fetch (unstable)](#removed-response-stub-in-single-fetch-unstable) - - [Minor Changes](#minor-changes) - - [Patch Changes](#patch-changes-2) - - [Updated Dependencies](#updated-dependencies-1) - - [Changes by Package](#changes-by-package-2) - - [v2.10.3](#v2103) + - [Minor Changes](#minor-changes-1) - [Patch Changes](#patch-changes-3) - [Updated Dependencies](#updated-dependencies-2) - [Changes by Package](#changes-by-package-3) - - [v2.10.2](#v2102) + - [v2.10.3](#v2103) - [Patch Changes](#patch-changes-4) + - [Updated Dependencies](#updated-dependencies-3) - [Changes by Package](#changes-by-package-4) - - [v2.10.1](#v2101) + - [v2.10.2](#v2102) - [Patch Changes](#patch-changes-5) - - [Updated Dependencies](#updated-dependencies-3) - [Changes by Package](#changes-by-package-5) - - [v2.10.0](#v2100) - - [What's Changed](#whats-changed-1) - - [Lazy Route Discovery (a.k.a. "Fog of War")](#lazy-route-discovery-aka-fog-of-war) - - [Minor Changes](#minor-changes-1) + - [v2.10.1](#v2101) - [Patch Changes](#patch-changes-6) - [Updated Dependencies](#updated-dependencies-4) - [Changes by Package](#changes-by-package-6) - - [v2.9.2](#v292) + - [v2.10.0](#v2100) - [What's Changed](#whats-changed-2) - - [Updated Type-Safety for Single Fetch](#updated-type-safety-for-single-fetch) + - [Lazy Route Discovery (a.k.a. "Fog of War")](#lazy-route-discovery-aka-fog-of-war) + - [Minor Changes](#minor-changes-2) - [Patch Changes](#patch-changes-7) - [Updated Dependencies](#updated-dependencies-5) - [Changes by Package](#changes-by-package-7) - - [v2.9.1](#v291) + - [v2.9.2](#v292) + - [What's Changed](#whats-changed-3) + - [Updated Type-Safety for Single Fetch](#updated-type-safety-for-single-fetch) - [Patch Changes](#patch-changes-8) + - [Updated Dependencies](#updated-dependencies-6) - [Changes by Package](#changes-by-package-8) + - [v2.9.1](#v291) + - [Patch Changes](#patch-changes-9) + - [Changes by Package](#changes-by-package-9) - [v2.9.0](#v290) - - [What's Changed](#whats-changed-3) + - [What's Changed](#whats-changed-4) - [Single Fetch (unstable)](#single-fetch-unstable) - [Undici](#undici) - - [Minor Changes](#minor-changes-2) - - [Patch Changes](#patch-changes-9) - - [Updated Dependencies](#updated-dependencies-6) - - [Changes by Package](#changes-by-package-9) - - [v2.8.1](#v281) + - [Minor Changes](#minor-changes-3) - [Patch Changes](#patch-changes-10) - [Updated Dependencies](#updated-dependencies-7) - [Changes by Package](#changes-by-package-10) - - [v2.8.0](#v280) - - [Minor Changes](#minor-changes-3) + - [v2.8.1](#v281) - [Patch Changes](#patch-changes-11) - [Updated Dependencies](#updated-dependencies-8) - [Changes by Package](#changes-by-package-11) - - [2.7.2](#272) + - [v2.8.0](#v280) + - [Minor Changes](#minor-changes-4) - [Patch Changes](#patch-changes-12) - - [2.7.1](#271) + - [Updated Dependencies](#updated-dependencies-9) + - [Changes by Package](#changes-by-package-12) + - [2.7.2](#272) - [Patch Changes](#patch-changes-13) + - [2.7.1](#271) + - [Patch Changes](#patch-changes-14) - [v2.7.0](#v270) - - [What's Changed](#whats-changed-4) + - [What's Changed](#whats-changed-5) - [Stabilized Vite Plugin](#stabilized-vite-plugin) - [New `Layout` Export](#new-layout-export) - [Basename support](#basename-support) - [Cloudflare Proxy as a Vite Plugin](#cloudflare-proxy-as-a-vite-plugin) - - [Minor Changes](#minor-changes-4) - - [Patch Changes](#patch-changes-14) - - [Updated Dependencies](#updated-dependencies-9) - - [Changes by Package](#changes-by-package-12) - - [v2.6.0](#v260) - - [What's Changed](#whats-changed-5) - - [Unstable Vite Plugin updates](#unstable-vite-plugin-updates) - [Minor Changes](#minor-changes-5) - [Patch Changes](#patch-changes-15) - [Updated Dependencies](#updated-dependencies-10) - [Changes by Package](#changes-by-package-13) - - [v2.5.1](#v251) + - [v2.6.0](#v260) + - [What's Changed](#whats-changed-6) + - [Unstable Vite Plugin updates](#unstable-vite-plugin-updates) + - [Minor Changes](#minor-changes-6) - [Patch Changes](#patch-changes-16) - [Updated Dependencies](#updated-dependencies-11) - [Changes by Package](#changes-by-package-14) - - [v2.5.0](#v250) - - [What's Changed](#whats-changed-6) - - [SPA Mode (unstable)](#spa-mode-unstable) - - [Server Bundles (unstable)](#server-bundles-unstable) - - [Minor Changes](#minor-changes-6) + - [v2.5.1](#v251) - [Patch Changes](#patch-changes-17) - [Updated Dependencies](#updated-dependencies-12) - [Changes by Package](#changes-by-package-15) - - [v2.4.1](#v241) + - [v2.5.0](#v250) + - [What's Changed](#whats-changed-7) + - [SPA Mode (unstable)](#spa-mode-unstable) + - [Server Bundles (unstable)](#server-bundles-unstable) + - [Minor Changes](#minor-changes-7) - [Patch Changes](#patch-changes-18) - [Updated Dependencies](#updated-dependencies-13) - [Changes by Package](#changes-by-package-16) + - [v2.4.1](#v241) + - [Patch Changes](#patch-changes-19) + - [Updated Dependencies](#updated-dependencies-14) + - [Changes by Package](#changes-by-package-17) - [v2.4.0](#v240) - - [What's Changed](#whats-changed-7) + - [What's Changed](#whats-changed-8) - [Client Data](#client-data) - [`future.v3_relativeSplatPath`](#futurev3_relativesplatpath) - [Vite Updates (Unstable)](#vite-updates-unstable) - - [Minor Changes](#minor-changes-7) - - [Patch Changes](#patch-changes-19) - - [Updated Dependencies](#updated-dependencies-14) - - [Changes by Package](#changes-by-package-17) - - [v2.3.1](#v231) + - [Minor Changes](#minor-changes-8) - [Patch Changes](#patch-changes-20) - [Updated Dependencies](#updated-dependencies-15) - [Changes by Package](#changes-by-package-18) - - [v2.3.0](#v230) - - [What's Changed](#whats-changed-8) - - [Stabilized `useBlocker`](#stabilized-useblocker) - - [`unstable_flushSync` API](#unstable_flushsync-api) - - [Minor Changes](#minor-changes-8) + - [v2.3.1](#v231) - [Patch Changes](#patch-changes-21) - [Updated Dependencies](#updated-dependencies-16) - [Changes by Package](#changes-by-package-19) - - [v2.2.0](#v220) + - [v2.3.0](#v230) - [What's Changed](#whats-changed-9) - - [Vite!](#vite) - - [New Fetcher APIs](#new-fetcher-apis) - - [Persistence Future Flag](#persistence-future-flag) + - [Stabilized `useBlocker`](#stabilized-useblocker) + - [`unstable_flushSync` API](#unstable_flushsync-api) - [Minor Changes](#minor-changes-9) - [Patch Changes](#patch-changes-22) - [Updated Dependencies](#updated-dependencies-17) - [Changes by Package](#changes-by-package-20) - - [v2.1.0](#v210) + - [v2.2.0](#v220) - [What's Changed](#whats-changed-10) - - [View Transitions](#view-transitions) - - [Stable `createRemixStub`](#stable-createremixstub) + - [Vite!](#vite) + - [New Fetcher APIs](#new-fetcher-apis) + - [Persistence Future Flag](#persistence-future-flag) - [Minor Changes](#minor-changes-10) - [Patch Changes](#patch-changes-23) - [Updated Dependencies](#updated-dependencies-18) - [Changes by Package](#changes-by-package-21) - - [v2.0.1](#v201) + - [v2.1.0](#v210) + - [What's Changed](#whats-changed-11) + - [View Transitions](#view-transitions) + - [Stable `createRemixStub`](#stable-createremixstub) + - [Minor Changes](#minor-changes-11) - [Patch Changes](#patch-changes-24) + - [Updated Dependencies](#updated-dependencies-19) + - [Changes by Package](#changes-by-package-22) + - [v2.0.1](#v201) + - [Patch Changes](#patch-changes-25) - [Changes by Package 🔗](#changes-by-package-) - [v2.0.0](#v200) - [Breaking Changes](#breaking-changes) @@ -160,8 +169,8 @@ We manage release notes in this file instead of the paginated Github Releases Pa - [Breaking Type Changes](#breaking-type-changes) - [New Features](#new-features) - [Other Notable Changes](#other-notable-changes) - - [Updated Dependencies](#updated-dependencies-19) - - [Changes by Package](#changes-by-package-22) + - [Updated Dependencies](#updated-dependencies-20) + - [Changes by Package](#changes-by-package-23) @@ -209,6 +218,101 @@ Date: YYYY-MM-DD --> +## v2.12.0 + +Date: 2024-09-09 + +### What's Changed + +#### Future Flag for Automatic Dependency Optimization (unstable) + +You can now opt-in to automatic dependency optimization during development by using the `future.unstable_optimizeDeps` future flag. For details, check out the docs at [Guides > Dependency optimization](https://remix.run/docs/en/main/guides/dependency-optimization). For users who were previously working around this limitation, you no longer need to explicitly add routes to Vite's `optimizeDeps.entries` nor do you need to disable the `remix-dot-server` plugin. + +#### Improved Single Fetch Type Safety (unstable) + +- If you were already using single-fetch types: + - Remove the `"@remix-run/react/future/single-fetch.d.ts"` override from `tsconfig.json` > `compilerOptions` > `types` + - Remove `defineLoader`, `defineAction`, `defineClientLoader`, `defineClientAction` helpers from your route modules + - Replace `UIMatch_SingleFetch` type helper with the original `UIMatch` + - Replace `MetaArgs_SingleFetch` type helper with the original `MetaArgs` + +Then you are ready for the new type safety setup: + +```ts +// vite.config.ts + +declare module "@remix-run/server-runtime" { + interface Future { + unstable_singleFetch: true; // 👈 enable _types_ for single-fetch + } +} + +export default defineConfig({ + plugins: [ + remix({ + future: { + unstable_singleFetch: true, // 👈 enable single-fetch + }, + }), + ], +}); +``` + +For more information, see [Guides > Single Fetch](https://remix.run/docs/guides/single-fetch) in our docs. + +#### Updates to Single Fetch Revalidation Behavior (unstable) + +With Single Fetch, re-used routes will now revalidate by default on `GET` navigations. This is aimed at improving caching of Single Fetch calls in the simple case while still allowing users to opt-into the previous behavior for more advanced use cases. + +With this new behavior, requests do not need special query params for granular route revalidations out of the box - i.e., `GET /a/b/c.data` + +There are two conditions that will trigger granular revalidation and will exclude certain routes from the single fetch call: + +- If a route opts out of revalidation via `shouldRevalidate` +- If a route defines a `clientLoader` + - If you call `serverLoader()` from your `clientLoader`, that will make a separate HTTP call for just that route loader - i.e., `GET /a/b/c.data?_routes=routes/a` for a `clientLoader` in `routes/a.tsx` + +When one or more routes are excluded from the Single Fetch call, the remaining routes that have loaders are included as query params. For example, when navigating to `/a/b/c`, if A was excluded, and the `root` route and `routes/b` had a `loader` but `routes/c` did not, the Single Fetch request would be `GET /a/b/c.data?_routes=root,routes/b`. + +For more information, see [Guides > Single Fetch](https://remix.run/docs/guides/single-fetch) in our docs. + +### Minor Changes + +- `@remix-run/dev` - New `future.unstable_optimizeDeps` flag for automatic dependency optimization ([#9921](https://github.com/remix-run/remix/pull/9921)) + +### Patch Changes + +- `@remix-run/dev` - Handle circular dependencies in modulepreload manifest generation ([#9917](https://github.com/remix-run/remix/pull/9917)) +- `@remix-run/dev` - Fix `dest already exists` build errors by only moving SSR assets to the client build directory when they're not already present on disk ([#9901](https://github.com/remix-run/remix/pull/9901)) +- `@remix-run/react` - Clarify wording in default `HydrateFallback` console warning ([#9899](https://github.com/remix-run/remix/pull/9899)) +- `@remix-run/react` - Remove hydration URL check that was originally added for React 17 hydration issues and we no longer support React 17 ([#9890](https://github.com/remix-run/remix/pull/9890)) + - Reverts the logic originally added in Remix `v1.18.0` via [#6409](https://github.com/remix-run/remix/pull/6409) + - This was added to resolve an issue that could arise when doing quick back/forward history navigations while JS was loading which would cause a mismatch between the server matches and client matches: [#1757](https://github.com/remix-run/remix/issues/1757) + - This specific hydration issue would then cause this React v17 only looping issue: [#1678](https://github.com/remix-run/remix/issues/1678) + - The URL comparison that we added in `1.18.0` turned out to be subject to false positives of it's own which could also put the user in looping scenarios + - Remix v2 upgraded it's minimal React version to v18 which eliminated the v17 hydration error loop + - React v18 handles this hydration error like any other error and does not result in a loop + - So we can remove our check and thus avoid the false-positive scenarios in which it may also trigger a loop +- `@remix-run/react` - Lazy Route Discovery: Sort `/__manifest` query parameters for better caching ([#9888](https://github.com/remix-run/remix/pull/9888)) +- `@remix-run/react` - Single Fetch: Improved type safety ([#9893](https://github.com/remix-run/remix/pull/9893)) +- `@remix-run/react` - Single Fetch: Fix revalidation behavior bugs ([#9938](https://github.com/remix-run/remix/pull/9938)) +- `@remix-run/server-runtime` - Do not render or try to include a body for 304 responses on document requests ([#9955](https://github.com/remix-run/remix/pull/9955)) +- `@remix-run/server-runtime` - Single Fetch: Do not try to encode a `turbo-stream` body into 304 responses ([#9941](https://github.com/remix-run/remix/pull/9941)) +- `@remix-run/server-runtime` - Single Fetch: Change content type on `.data` requests to `text/x-script` to allow Cloudflare compression ([#9889](https://github.com/remix-run/remix/pull/9889)) + +### Updated Dependencies + +- [`react-router-dom@6.26.2`](https://github.com/remix-run/react-router/releases/tag/react-router%406.26.2) +- [`@remix-run/router@1.19.2`](https://github.com/remix-run/react-router/blob/main/packages/router/CHANGELOG.md#1192) + +### Changes by Package + +- [`@remix-run/dev`](https://github.com/remix-run/remix/blob/remix%402.12.0/packages/remix-dev/CHANGELOG.md#2120) +- [`@remix-run/react`](https://github.com/remix-run/remix/blob/remix%402.12.0/packages/remix-react/CHANGELOG.md#2120) +- [`@remix-run/server-runtime`](https://github.com/remix-run/remix/blob/remix%402.12.0/packages/remix-server-runtime/CHANGELOG.md#2120) + +**Full Changelog**: [`v2.11.2...v2.12.0`](https://github.com/remix-run/remix/compare/remix@2.11.2...remix@2.12.0) + ## v2.11.2 Date: 2024-08-15 diff --git a/contributors.yml b/contributors.yml index c39c25716ea..5a88c93ed6d 100644 --- a/contributors.yml +++ b/contributors.yml @@ -366,6 +366,7 @@ - kishanhitk - kiyadotdev - klauspaiva +- klirium - knowler - konradkalemba - krolebord @@ -484,6 +485,7 @@ - n8agrin - na2hiro - nareshbhatia +- nauvalazhar - naveed-fida - navid-kalaei - nexxeln @@ -547,6 +549,7 @@ - remix-run-bot - richardhunghhw - riencoertjens +- risv1 - rkulinski - rlfarman - roachjc diff --git a/docs/file-conventions/routes.md b/docs/file-conventions/routes.md index e33004d18d5..4c01fed5c2a 100644 --- a/docs/file-conventions/routes.md +++ b/docs/file-conventions/routes.md @@ -246,7 +246,7 @@ Sometimes you want to share a layout with a group of routes without adding any p | `/` | `app/routes/_index.tsx` | `app/root.tsx` | | `/login` | `app/routes/_auth.login.tsx` | `app/routes/_auth.tsx` | | `/register` | `app/routes/_auth.register.tsx` | `app/routes/_auth.tsx` | -| `/concerts` | `app/routes/concerts.tsx` | `app/routes/concerts.tsx` | +| `/concerts` | `app/routes/concerts.tsx` | `app/root.tsx` | | `/concerts/salt-lake-city` | `app/routes/concerts.$city.tsx` | `app/routes/concerts.tsx` | Think of the `_leading` underscore as a blanket you're pulling over the filename, hiding the filename from the URL. diff --git a/docs/guides/single-fetch.md b/docs/guides/single-fetch.md index d52d7648e07..c3d0f09bc77 100644 --- a/docs/guides/single-fetch.md +++ b/docs/guides/single-fetch.md @@ -492,7 +492,3 @@ Revalidation is handled via a `?_routes` query string parameter on the single fe [compatibility-flag]: https://developers.cloudflare.com/workers/configuration/compatibility-dates [data-utility]: ../utils/data [augment]: https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation - -``` - -``` diff --git a/docs/start/tutorial.md b/docs/start/tutorial.md index 418cdac49d1..625f94d1e65 100644 --- a/docs/start/tutorial.md +++ b/docs/start/tutorial.md @@ -785,7 +785,7 @@ export const action = async ({ Without client side routing, if a server redirected after a `POST` request, the new page would fetch the latest data and render. As we learned before, Remix emulates this model and automatically revalidates the data on the page after the `action` call. That's why the sidebar automatically updates when we save the form. The extra revalidation code doesn't exist without client side routing, so it doesn't need to exist with client side routing in Remix either! -One last thing. Without JavaScript, the [`redirect`][redirect] would be a normal redirect. However, with JavaScript it's a clientside redirect, so the user doesn't lose client state like scroll positions or component state. +One last thing. Without JavaScript, the [`redirect`][redirect] would be a normal redirect. However, with JavaScript it's a client-side redirect, so the user doesn't lose client state like scroll positions or component state. ## Redirecting new records to the edit page diff --git a/docs/utils/sessions.md b/docs/utils/sessions.md index 240a8617a91..647289fd31b 100644 --- a/docs/utils/sessions.md +++ b/docs/utils/sessions.md @@ -218,7 +218,7 @@ Remix makes it easy to store sessions in your own database if needed. The `creat - `createData` will be called from `commitSession` on the initial session creation when no session ID exists in the cookie - `readData` will be called from `getSession` when a session ID exists in the cookie - `updateData` will be called from `commitSession` when a session ID already exists in the cookie -- `deleteData` is called from `destorySession` +- `deleteData` is called from `destroySession` The following example shows how you could do this using a generic database client: diff --git a/integration/helpers/vite-cloudflare-template/package.json b/integration/helpers/vite-cloudflare-template/package.json index 1f7b245e474..84c8085bf46 100644 --- a/integration/helpers/vite-cloudflare-template/package.json +++ b/integration/helpers/vite-cloudflare-template/package.json @@ -11,9 +11,9 @@ "typecheck": "tsc" }, "dependencies": { - "@remix-run/cloudflare": "2.11.2", - "@remix-run/cloudflare-pages": "2.11.2", - "@remix-run/react": "2.11.2", + "@remix-run/cloudflare": "2.12.0", + "@remix-run/cloudflare-pages": "2.12.0", + "@remix-run/react": "2.12.0", "isbot": "^4.1.0", "miniflare": "^3.20231030.4", "react": "^18.2.0", diff --git a/integration/package.json b/integration/package.json index 90c677e4844..bd94ebf2813 100644 --- a/integration/package.json +++ b/integration/package.json @@ -14,7 +14,7 @@ "@remix-run/dev": "workspace:*", "@remix-run/express": "workspace:*", "@remix-run/node": "workspace:*", - "@remix-run/router": "1.19.2-pre.0", + "@remix-run/router": "1.19.2", "@remix-run/server-runtime": "workspace:*", "@types/express": "^4.17.9", "@vanilla-extract/css": "^1.10.0", diff --git a/integration/single-fetch-test.ts b/integration/single-fetch-test.ts index 870a97b94c6..79d1a860d5e 100644 --- a/integration/single-fetch-test.ts +++ b/integration/single-fetch-test.ts @@ -182,17 +182,14 @@ test.describe("single-fetch", () => { }); test("loads proper data on single fetch loader requests", async () => { - let fixture = await createFixture( - { - config: { - future: { - unstable_singleFetch: true, - }, + let fixture = await createFixture({ + config: { + future: { + unstable_singleFetch: true, }, - files, }, - ServerMode.Development - ); + files, + }); let res = await fixture.requestSingleFetchData("/_root.data"); expect(res.data).toEqual({ root: { @@ -258,17 +255,14 @@ test.describe("single-fetch", () => { }); test("loads proper data on single fetch action requests", async () => { - let fixture = await createFixture( - { - config: { - future: { - unstable_singleFetch: true, - }, + let fixture = await createFixture({ + config: { + future: { + unstable_singleFetch: true, }, - files, }, - ServerMode.Development - ); + files, + }); let postBody = new URLSearchParams(); postBody.set("key", "value"); let res = await fixture.requestSingleFetchData("/data.data", { @@ -379,18 +373,15 @@ test.describe("single-fetch", () => { test("loads proper data on client side action navigation", async ({ page, }) => { - let fixture = await createFixture( - { - config: { - future: { - unstable_singleFetch: true, - }, + let fixture = await createFixture({ + config: { + future: { + unstable_singleFetch: true, }, - files, }, - ServerMode.Development - ); - let appFixture = await createAppFixture(fixture, ServerMode.Development); + files, + }); + let appFixture = await createAppFixture(fixture); let app = new PlaywrightFixture(appFixture, page); await app.goto("/"); await app.clickSubmitButton("/data"); @@ -1116,33 +1107,30 @@ test.describe("single-fetch", () => { }); test("processes thrown action redirects via Response", async ({ page }) => { - let fixture = await createFixture( - { - config: { - future: { - unstable_singleFetch: true, - }, - }, - files: { - ...files, - "app/routes/data.tsx": js` - import { redirect } from '@remix-run/node'; - export function action() { - throw redirect('/target'); - } - export default function Component() { - return null - } - `, - "app/routes/target.tsx": js` - export default function Component() { - return

Target

- } - `, + let fixture = await createFixture({ + config: { + future: { + unstable_singleFetch: true, }, }, - ServerMode.Development - ); + files: { + ...files, + "app/routes/data.tsx": js` + import { redirect } from '@remix-run/node'; + export function action() { + throw redirect('/target'); + } + export default function Component() { + return null + } + `, + "app/routes/target.tsx": js` + export default function Component() { + return

Target

+ } + `, + }, + }); console.error = () => {}; @@ -1167,7 +1155,7 @@ test.describe("single-fetch", () => { }); expect(status).toBe(202); - let appFixture = await createAppFixture(fixture, ServerMode.Development); + let appFixture = await createAppFixture(fixture); let app = new PlaywrightFixture(appFixture, page); await app.goto("/"); await app.clickSubmitButton("/data"); @@ -1176,33 +1164,30 @@ test.describe("single-fetch", () => { }); test("processes returned action redirects via Response", async ({ page }) => { - let fixture = await createFixture( - { - config: { - future: { - unstable_singleFetch: true, - }, - }, - files: { - ...files, - "app/routes/data.tsx": js` - import { redirect } from '@remix-run/node'; - export function action() { - return redirect('/target'); - } - export default function Component() { - return null - } - `, - "app/routes/target.tsx": js` - export default function Component() { - return

Target

- } - `, + let fixture = await createFixture({ + config: { + future: { + unstable_singleFetch: true, }, }, - ServerMode.Development - ); + files: { + ...files, + "app/routes/data.tsx": js` + import { redirect } from '@remix-run/node'; + export function action() { + return redirect('/target'); + } + export default function Component() { + return null + } + `, + "app/routes/target.tsx": js` + export default function Component() { + return

Target

+ } + `, + }, + }); let res = await fixture.requestDocument("/data", { method: "post", @@ -1225,7 +1210,7 @@ test.describe("single-fetch", () => { }); expect(status).toBe(202); - let appFixture = await createAppFixture(fixture, ServerMode.Development); + let appFixture = await createAppFixture(fixture); let app = new PlaywrightFixture(appFixture, page); await app.goto("/"); await app.clickSubmitButton("/data"); @@ -1681,24 +1666,21 @@ test.describe("single-fetch", () => { let warnLogs: unknown[] = []; console.warn = (...args) => warnLogs.push(...args); - let fixture = await createFixture( - { - config: { - future: { - unstable_singleFetch: true, - }, - }, - files: { - ...files, - "app/routes/resource.tsx": js` - export function loader() { - return { message: "RESOURCE" }; - } - `, + let fixture = await createFixture({ + config: { + future: { + unstable_singleFetch: true, }, }, - ServerMode.Development - ); + files: { + ...files, + "app/routes/resource.tsx": js` + export function loader() { + return { message: "RESOURCE" }; + } + `, + }, + }); let res = await fixture.requestResource("/resource"); expect(await res.json()).toEqual({ message: "RESOURCE", @@ -1718,16 +1700,15 @@ test.describe("single-fetch", () => { test("allows fetcher to hit resource route and return via turbo stream", async ({ page, }) => { - let fixture = await createFixture( - { - config: { - future: { - unstable_singleFetch: true, - }, + let fixture = await createFixture({ + config: { + future: { + unstable_singleFetch: true, }, - files: { - ...files, - "app/routes/_index.tsx": js` + }, + files: { + ...files, + "app/routes/_index.tsx": js` import { useFetcher } from "@remix-run/react"; export default function Component() { @@ -1742,7 +1723,7 @@ test.describe("single-fetch", () => { ); } `, - "app/routes/resource.tsx": js` + "app/routes/resource.tsx": js` export function loader() { // Fetcher calls to resource routes will append ".data" and we'll go through // the turbo-stream flow. If a user were to curl this endpoint they'd go @@ -1753,11 +1734,9 @@ test.describe("single-fetch", () => { }; } `, - }, }, - ServerMode.Development - ); - let appFixture = await createAppFixture(fixture, ServerMode.Development); + }); + let appFixture = await createAppFixture(fixture); let app = new PlaywrightFixture(appFixture, page); await app.goto("/"); await app.clickElement("#load"); @@ -1770,22 +1749,21 @@ test.describe("single-fetch", () => { test("Strips ?_routes query param from loader/action requests", async ({ page, }) => { - let fixture = await createFixture( - { - config: { - future: { - unstable_singleFetch: true, - }, + let fixture = await createFixture({ + config: { + future: { + unstable_singleFetch: true, }, - files: { - ...files, - "app/routes/_index.tsx": js` + }, + files: { + ...files, + "app/routes/_index.tsx": js` import { Link } from '@remix-run/react'; export default function Component() { return Go to /parent/a; } `, - "app/routes/parent.tsx": js` + "app/routes/parent.tsx": js` import { Link, Outlet, useLoaderData } from '@remix-run/react'; export function loader({ request }) { return { url: request.url }; @@ -1799,7 +1777,7 @@ test.describe("single-fetch", () => { ); } `, - "app/routes/parent.a.tsx": js` + "app/routes/parent.a.tsx": js` import { useLoaderData } from '@remix-run/react'; export function loader({ request }) { return { url: request.url }; @@ -1821,11 +1799,9 @@ test.describe("single-fetch", () => { ); } `, - }, }, - ServerMode.Development - ); - let appFixture = await createAppFixture(fixture, ServerMode.Development); + }); + let appFixture = await createAppFixture(fixture); let app = new PlaywrightFixture(appFixture, page); let urls: string[] = []; @@ -1864,43 +1840,40 @@ test.describe("single-fetch", () => { test("Action requests do not use _routes and do not call loaders on the server", async ({ page, }) => { - let fixture = await createFixture( - { - config: { - future: { - unstable_singleFetch: true, - }, - }, - files: { - ...files, - "app/routes/page.tsx": js` - import { Form, useActionData, useLoaderData } from '@remix-run/react'; - let count = 0; - export function loader({ request }) { - return { count: ++count }; - } - export function action({ request }) { - return { message: "ACTION" }; - } - export default function Component() { - let data = useLoaderData(); - let actionData = useActionData(); - return ( - <> -

{"Count:" + data.count}

-
- - {actionData ?

{actionData.message}

: null} -
- - ) - } - `, + let fixture = await createFixture({ + config: { + future: { + unstable_singleFetch: true, }, }, - ServerMode.Development - ); - let appFixture = await createAppFixture(fixture, ServerMode.Development); + files: { + ...files, + "app/routes/page.tsx": js` + import { Form, useActionData, useLoaderData } from '@remix-run/react'; + let count = 0; + export function loader({ request }) { + return { count: ++count }; + } + export function action({ request }) { + return { message: "ACTION" }; + } + export default function Component() { + let data = useLoaderData(); + let actionData = useActionData(); + return ( + <> +

{"Count:" + data.count}

+
+ + {actionData ?

{actionData.message}

: null} +
+ + ) + } + `, + }, + }); + let appFixture = await createAppFixture(fixture); let app = new PlaywrightFixture(appFixture, page); let urls: string[] = []; @@ -1952,8 +1925,23 @@ test.describe("single-fetch", () => { `, }, }); - let res = await fixture.requestSingleFetchData("/_root.data"); - expect(res.data).toEqual({ + + // Document requests + let documentRes = await fixture.requestDocument("/"); + let html = await documentRes.text(); + expect(html).toContain(""); + expect(html).toContain("

Hello from the loader!

"); + documentRes = await fixture.requestDocument("/", { + headers: { + "If-None-Match": "1234", + }, + }); + expect(documentRes.status).toBe(304); + expect(await documentRes.text()).toBe(""); + + // Data requests + let dataRes = await fixture.requestSingleFetchData("/_root.data"); + expect(dataRes.data).toEqual({ root: { data: { message: "ROOT", @@ -1965,28 +1953,27 @@ test.describe("single-fetch", () => { }, }, }); - res = await fixture.requestSingleFetchData("/_root.data", { + dataRes = await fixture.requestSingleFetchData("/_root.data", { headers: { "If-None-Match": "1234", }, }); - expect(res.status).toBe(304); - expect(res.data).toBeNull(); + expect(dataRes.status).toBe(304); + expect(dataRes.data).toBeNull(); }); test.describe("revalidations/_routes param", () => { test("does not make a server call if no loaders need to run", async ({ page, }) => { - let fixture = await createFixture( - { - config: { - future: { - unstable_singleFetch: true, - }, + let fixture = await createFixture({ + config: { + future: { + unstable_singleFetch: true, }, - files: { - "app/root.tsx": js` + }, + files: { + "app/root.tsx": js` import { Link, Links, Meta, Outlet, Scripts } from "@remix-run/react"; export default function Root() { @@ -2006,22 +1993,20 @@ test.describe("single-fetch", () => { ); } `, - "app/routes/a.tsx": js` + "app/routes/a.tsx": js` import { Outlet } from "@remix-run/react"; export default function Root() { return ; } `, - "app/routes/a.b.tsx": js` + "app/routes/a.b.tsx": js` export default function Root() { return

B

; } `, - }, }, - ServerMode.Development - ); + }); let urls: string[] = []; page.on("request", (req) => { @@ -2030,7 +2015,7 @@ test.describe("single-fetch", () => { } }); - let appFixture = await createAppFixture(fixture, ServerMode.Development); + let appFixture = await createAppFixture(fixture); let app = new PlaywrightFixture(appFixture, page); await app.goto("/"); @@ -2050,48 +2035,48 @@ test.describe("single-fetch", () => { files: { ...files, "app/routes/_index.tsx": js` - import { Link } from '@remix-run/react'; - export default function Component() { - return Go to /parent/a; - } - `, + import { Link } from '@remix-run/react'; + export default function Component() { + return Go to /parent/a; + } + `, "app/routes/parent.tsx": js` - import { Link, Outlet, useLoaderData } from '@remix-run/react'; - let count = 0; - export function loader({ request }) { - return { count: ++count }; - } - export default function Component() { - return ( - <> -

Parent Count: {useLoaderData().count}

- Go to /parent/a - Go to /parent/b - - - ); - } - `, + import { Link, Outlet, useLoaderData } from '@remix-run/react'; + let count = 0; + export function loader({ request }) { + return { count: ++count }; + } + export default function Component() { + return ( + <> +

Parent Count: {useLoaderData().count}

+ Go to /parent/a + Go to /parent/b + + + ); + } + `, "app/routes/parent.a.tsx": js` - import { useLoaderData } from '@remix-run/react'; - let count = 0; - export function loader({ request }) { - return { count: ++count }; - } - export default function Component() { - return

A Count: {useLoaderData().count}

; - } - `, + import { useLoaderData } from '@remix-run/react'; + let count = 0; + export function loader({ request }) { + return { count: ++count }; + } + export default function Component() { + return

A Count: {useLoaderData().count}

; + } + `, "app/routes/parent.b.tsx": js` - import { useLoaderData } from '@remix-run/react'; - let count = 0; - export function loader({ request }) { - return { count: ++count }; - } - export default function Component() { - return

B Count: {useLoaderData().count}

; - } - `, + import { useLoaderData } from '@remix-run/react'; + let count = 0; + export function loader({ request }) { + return { count: ++count }; + } + export default function Component() { + return

B Count: {useLoaderData().count}

; + } + `, }, }); let appFixture = await createAppFixture(fixture); @@ -2430,72 +2415,69 @@ test.describe("single-fetch", () => { test.describe("client loaders", () => { test("when no routes have client loaders", async ({ page }) => { - let fixture = await createFixture( - { - config: { - future: { - unstable_singleFetch: true, - }, + let fixture = await createFixture({ + config: { + future: { + unstable_singleFetch: true, }, - files: { - ...files, - "app/routes/a.tsx": js` - import { Outlet, useLoaderData } from '@remix-run/react'; + }, + files: { + ...files, + "app/routes/a.tsx": js` + import { Outlet, useLoaderData } from '@remix-run/react'; - export function loader() { - return { message: "A server loader" }; - } + export function loader() { + return { message: "A server loader" }; + } - export default function Comp() { - let data = useLoaderData(); - return ( - <> -

A

-

{data.message}

- - - ); - } - `, - "app/routes/a.b.tsx": js` - import { Outlet, useLoaderData } from '@remix-run/react'; + export default function Comp() { + let data = useLoaderData(); + return ( + <> +

A

+

{data.message}

+ + + ); + } + `, + "app/routes/a.b.tsx": js` + import { Outlet, useLoaderData } from '@remix-run/react'; - export function loader() { - return { message: "B server loader" }; - } + export function loader() { + return { message: "B server loader" }; + } - export default function Comp() { - let data = useLoaderData(); - return ( - <> -

B

-

{data.message}

- - - ); - } - `, - "app/routes/a.b.c.tsx": js` - import { useLoaderData } from '@remix-run/react'; + export default function Comp() { + let data = useLoaderData(); + return ( + <> +

B

+

{data.message}

+ + + ); + } + `, + "app/routes/a.b.c.tsx": js` + import { useLoaderData } from '@remix-run/react'; - export function loader() { - return { message: "C server loader" }; - } + export function loader() { + return { message: "C server loader" }; + } - export default function Comp() { - let data = useLoaderData(); - return ( - <> -

C

-

{data.message}

- - ); - } - `, - }, + export default function Comp() { + let data = useLoaderData(); + return ( + <> +

C

+

{data.message}

+ + ); + } + `, }, - ServerMode.Development - ); + }); let urls: string[] = []; page.on("request", (req) => { @@ -2504,7 +2486,7 @@ test.describe("single-fetch", () => { } }); - let appFixture = await createAppFixture(fixture, ServerMode.Development); + let appFixture = await createAppFixture(fixture); let app = new PlaywrightFixture(appFixture, page); await app.goto("/"); await app.clickLink("/a/b/c"); @@ -2518,77 +2500,74 @@ test.describe("single-fetch", () => { }); test("when one route has a client loader", async ({ page }) => { - let fixture = await createFixture( - { - config: { - future: { - unstable_singleFetch: true, - }, + let fixture = await createFixture({ + config: { + future: { + unstable_singleFetch: true, }, - files: { - ...files, - "app/routes/a.tsx": js` - import { Outlet, useLoaderData } from '@remix-run/react'; + }, + files: { + ...files, + "app/routes/a.tsx": js` + import { Outlet, useLoaderData } from '@remix-run/react'; - export function loader() { - return { message: "A server loader" }; - } + export function loader() { + return { message: "A server loader" }; + } - export default function Comp() { - let data = useLoaderData(); - return ( - <> -

A

-

{data.message}

- - - ); - } - `, - "app/routes/a.b.tsx": js` - import { Outlet, useLoaderData } from '@remix-run/react'; + export default function Comp() { + let data = useLoaderData(); + return ( + <> +

A

+

{data.message}

+ + + ); + } + `, + "app/routes/a.b.tsx": js` + import { Outlet, useLoaderData } from '@remix-run/react'; - export function loader() { - return { message: "B server loader" }; - } + export function loader() { + return { message: "B server loader" }; + } - export default function Comp() { - let data = useLoaderData(); - return ( - <> -

B

-

{data.message}

- - - ); - } - `, - "app/routes/a.b.c.tsx": js` - import { useLoaderData } from '@remix-run/react'; + export default function Comp() { + let data = useLoaderData(); + return ( + <> +

B

+

{data.message}

+ + + ); + } + `, + "app/routes/a.b.c.tsx": js` + import { useLoaderData } from '@remix-run/react'; - export function loader() { - return { message: "C server loader" }; - } + export function loader() { + return { message: "C server loader" }; + } - export async function clientLoader({ serverLoader }) { - let data = await serverLoader(); - return { message: data.message + " (C client loader)" }; - } + export async function clientLoader({ serverLoader }) { + let data = await serverLoader(); + return { message: data.message + " (C client loader)" }; + } - export default function Comp() { - let data = useLoaderData(); - return ( - <> -

C

-

{data.message}

- - ); - } - `, - }, + export default function Comp() { + let data = useLoaderData(); + return ( + <> +

C

+

{data.message}

+ + ); + } + `, }, - ServerMode.Development - ); + }); let urls: string[] = []; page.on("request", (req) => { @@ -2597,7 +2576,7 @@ test.describe("single-fetch", () => { } }); - let appFixture = await createAppFixture(fixture, ServerMode.Development); + let appFixture = await createAppFixture(fixture); let app = new PlaywrightFixture(appFixture, page); await app.goto("/"); await app.clickLink("/a/b/c"); @@ -2618,82 +2597,79 @@ test.describe("single-fetch", () => { }); test("when multiple routes have client loaders", async ({ page }) => { - let fixture = await createFixture( - { - config: { - future: { - unstable_singleFetch: true, - }, + let fixture = await createFixture({ + config: { + future: { + unstable_singleFetch: true, }, - files: { - ...files, - "app/routes/a.tsx": js` - import { Outlet, useLoaderData } from '@remix-run/react'; + }, + files: { + ...files, + "app/routes/a.tsx": js` + import { Outlet, useLoaderData } from '@remix-run/react'; - export function loader() { - return { message: "A server loader" }; - } + export function loader() { + return { message: "A server loader" }; + } - export default function Comp() { - let data = useLoaderData(); - return ( - <> -

A

-

{data.message}

- - - ); - } - `, - "app/routes/a.b.tsx": js` - import { Outlet, useLoaderData } from '@remix-run/react'; + export default function Comp() { + let data = useLoaderData(); + return ( + <> +

A

+

{data.message}

+ + + ); + } + `, + "app/routes/a.b.tsx": js` + import { Outlet, useLoaderData } from '@remix-run/react'; - export function loader() { - return { message: "B server loader" }; - } + export function loader() { + return { message: "B server loader" }; + } - export async function clientLoader({ serverLoader }) { - let data = await serverLoader(); - return { message: data.message + " (B client loader)" }; - } + export async function clientLoader({ serverLoader }) { + let data = await serverLoader(); + return { message: data.message + " (B client loader)" }; + } - export default function Comp() { - let data = useLoaderData(); - return ( - <> -

B

-

{data.message}

- - - ); - } - `, - "app/routes/a.b.c.tsx": js` - import { useLoaderData } from '@remix-run/react'; + export default function Comp() { + let data = useLoaderData(); + return ( + <> +

B

+

{data.message}

+ + + ); + } + `, + "app/routes/a.b.c.tsx": js` + import { useLoaderData } from '@remix-run/react'; - export function loader() { - return { message: "C server loader" }; - } + export function loader() { + return { message: "C server loader" }; + } - export async function clientLoader({ serverLoader }) { - let data = await serverLoader(); - return { message: data.message + " (C client loader)" }; - } + export async function clientLoader({ serverLoader }) { + let data = await serverLoader(); + return { message: data.message + " (C client loader)" }; + } - export default function Comp() { - let data = useLoaderData(); - return ( - <> -

C

-

{data.message}

- - ); - } - `, - }, + export default function Comp() { + let data = useLoaderData(); + return ( + <> +

C

+

{data.message}

+ + ); + } + `, }, - ServerMode.Development - ); + }); let urls: string[] = []; page.on("request", (req) => { @@ -2702,7 +2678,7 @@ test.describe("single-fetch", () => { } }); - let appFixture = await createAppFixture(fixture, ServerMode.Development); + let appFixture = await createAppFixture(fixture); let app = new PlaywrightFixture(appFixture, page); await app.goto("/"); await app.clickLink("/a/b/c"); @@ -2724,87 +2700,84 @@ test.describe("single-fetch", () => { }); test("when all routes have client loaders", async ({ page }) => { - let fixture = await createFixture( - { - config: { - future: { - unstable_singleFetch: true, - }, + let fixture = await createFixture({ + config: { + future: { + unstable_singleFetch: true, }, - files: { - ...files, - "app/routes/a.tsx": js` - import { Outlet, useLoaderData } from '@remix-run/react'; + }, + files: { + ...files, + "app/routes/a.tsx": js` + import { Outlet, useLoaderData } from '@remix-run/react'; - export function loader() { - return { message: "A server loader" }; - } + export function loader() { + return { message: "A server loader" }; + } - export async function clientLoader({ serverLoader }) { - let data = await serverLoader(); - return { message: data.message + " (A client loader)" }; - } + export async function clientLoader({ serverLoader }) { + let data = await serverLoader(); + return { message: data.message + " (A client loader)" }; + } - export default function Comp() { - let data = useLoaderData(); - return ( - <> -

A

-

{data.message}

- - - ); - } - `, - "app/routes/a.b.tsx": js` - import { Outlet, useLoaderData } from '@remix-run/react'; + export default function Comp() { + let data = useLoaderData(); + return ( + <> +

A

+

{data.message}

+ + + ); + } + `, + "app/routes/a.b.tsx": js` + import { Outlet, useLoaderData } from '@remix-run/react'; - export function loader() { - return { message: "B server loader" }; - } + export function loader() { + return { message: "B server loader" }; + } - export async function clientLoader({ serverLoader }) { - let data = await serverLoader(); - return { message: data.message + " (B client loader)" }; - } + export async function clientLoader({ serverLoader }) { + let data = await serverLoader(); + return { message: data.message + " (B client loader)" }; + } - export default function Comp() { - let data = useLoaderData(); - return ( - <> -

B

-

{data.message}

- - - ); - } - `, - "app/routes/a.b.c.tsx": js` - import { useLoaderData } from '@remix-run/react'; + export default function Comp() { + let data = useLoaderData(); + return ( + <> +

B

+

{data.message}

+ + + ); + } + `, + "app/routes/a.b.c.tsx": js` + import { useLoaderData } from '@remix-run/react'; - export function loader() { - return { message: "C server loader" }; - } + export function loader() { + return { message: "C server loader" }; + } - export async function clientLoader({ serverLoader }) { - let data = await serverLoader(); - return { message: data.message + " (C client loader)" }; - } + export async function clientLoader({ serverLoader }) { + let data = await serverLoader(); + return { message: data.message + " (C client loader)" }; + } - export default function Comp() { - let data = useLoaderData(); - return ( - <> -

C

-

{data.message}

- - ); - } - `, - }, + export default function Comp() { + let data = useLoaderData(); + return ( + <> +

C

+

{data.message}

+ + ); + } + `, }, - ServerMode.Development - ); + }); let urls: string[] = []; page.on("request", (req) => { @@ -2813,7 +2786,7 @@ test.describe("single-fetch", () => { } }); - let appFixture = await createAppFixture(fixture, ServerMode.Development); + let appFixture = await createAppFixture(fixture); let app = new PlaywrightFixture(appFixture, page); await app.goto("/"); await app.clickLink("/a/b/c"); @@ -2840,22 +2813,21 @@ test.describe("single-fetch", () => { test.describe("fetchers", () => { test("Fetcher loaders call singular routes", async ({ page }) => { - let fixture = await createFixture( - { - config: { - future: { - unstable_singleFetch: true, - }, + let fixture = await createFixture({ + config: { + future: { + unstable_singleFetch: true, }, - files: { - ...files, - "app/routes/a.tsx": js` + }, + files: { + ...files, + "app/routes/a.tsx": js` import { Outlet } from '@remix-run/react'; export default function Comp() { return ; } `, - "app/routes/a.b.tsx": js` + "app/routes/a.b.tsx": js` import { useFetcher } from '@remix-run/react'; export function loader() { @@ -2872,10 +2844,8 @@ test.describe("single-fetch", () => { ); } `, - }, }, - ServerMode.Development - ); + }); let urls: string[] = []; page.on("request", (req) => { @@ -2884,7 +2854,7 @@ test.describe("single-fetch", () => { } }); - let appFixture = await createAppFixture(fixture, ServerMode.Development); + let appFixture = await createAppFixture(fixture); let app = new PlaywrightFixture(appFixture, page); await app.goto("/a/b"); await app.clickElement("#load"); @@ -2897,49 +2867,46 @@ test.describe("single-fetch", () => { }); test("Fetcher actions call singular routes", async ({ page }) => { - let fixture = await createFixture( - { - config: { - future: { - unstable_singleFetch: true, - }, + let fixture = await createFixture({ + config: { + future: { + unstable_singleFetch: true, }, - files: { - ...files, - "app/routes/a.tsx": js` - import { Outlet } from '@remix-run/react'; - export default function Comp() { - return ; - } - `, - "app/routes/a.b.tsx": js` - import { useFetcher } from '@remix-run/react'; + }, + files: { + ...files, + "app/routes/a.tsx": js` + import { Outlet } from '@remix-run/react'; + export default function Comp() { + return ; + } + `, + "app/routes/a.b.tsx": js` + import { useFetcher } from '@remix-run/react'; - export function action() { - return { message: 'ACTION' }; - } + export function action() { + return { message: 'ACTION' }; + } - export default function Comp() { - let fetcher = useFetcher(); - return ( - <> - - {fetcher.data ?

{fetcher.data.message}

: null} - - ); - } - `, - }, + export default function Comp() { + let fetcher = useFetcher(); + return ( + <> + + {fetcher.data ?

{fetcher.data.message}

: null} + + ); + } + `, }, - ServerMode.Development - ); + }); let urls: string[] = []; page.on("request", (req) => { @@ -2948,7 +2915,7 @@ test.describe("single-fetch", () => { } }); - let appFixture = await createAppFixture(fixture, ServerMode.Development); + let appFixture = await createAppFixture(fixture); let app = new PlaywrightFixture(appFixture, page); await app.goto("/a/b"); await app.clickElement("#submit"); @@ -2963,16 +2930,15 @@ test.describe("single-fetch", () => { test("Fetcher loads do not revalidate on GET navigations by default", async ({ page, }) => { - let fixture = await createFixture( - { - config: { - future: { - unstable_singleFetch: true, - }, + let fixture = await createFixture({ + config: { + future: { + unstable_singleFetch: true, }, - files: { - ...files, - "app/routes/parent.tsx": js` + }, + files: { + ...files, + "app/routes/parent.tsx": js` import { Link, Outlet, useFetcher } from '@remix-run/react'; export default function Component() { let fetcher = useFetcher(); @@ -2989,17 +2955,17 @@ test.describe("single-fetch", () => { ); } `, - "app/routes/parent.a.tsx": js` + "app/routes/parent.a.tsx": js` export default function Component() { return

A

; } `, - "app/routes/parent.b.tsx": js` + "app/routes/parent.b.tsx": js` export default function Component() { return

B

; } `, - "app/routes/fetch.tsx": js` + "app/routes/fetch.tsx": js` let count = 0; export function loader({ request }) { return { count: ++count }; @@ -3008,11 +2974,9 @@ test.describe("single-fetch", () => { return

Fetch

; } `, - }, }, - ServerMode.Development - ); - let appFixture = await createAppFixture(fixture, ServerMode.Development); + }); + let appFixture = await createAppFixture(fixture); let app = new PlaywrightFixture(appFixture, page); let urls: string[] = []; @@ -3040,16 +3004,15 @@ test.describe("single-fetch", () => { test("Fetcher loads can opt into revalidation on GET navigations", async ({ page, }) => { - let fixture = await createFixture( - { - config: { - future: { - unstable_singleFetch: true, - }, + let fixture = await createFixture({ + config: { + future: { + unstable_singleFetch: true, }, - files: { - ...files, - "app/routes/parent.tsx": js` + }, + files: { + ...files, + "app/routes/parent.tsx": js` import { Link, Outlet, useFetcher } from '@remix-run/react'; export default function Component() { let fetcher = useFetcher(); @@ -3066,17 +3029,17 @@ test.describe("single-fetch", () => { ); } `, - "app/routes/parent.a.tsx": js` + "app/routes/parent.a.tsx": js` export default function Component() { return

A

; } `, - "app/routes/parent.b.tsx": js` + "app/routes/parent.b.tsx": js` export default function Component() { return

B

; } `, - "app/routes/fetch.tsx": js` + "app/routes/fetch.tsx": js` let count = 0; export function loader({ request }) { return { count: ++count }; @@ -3088,11 +3051,9 @@ test.describe("single-fetch", () => { return

Fetch

; } `, - }, }, - ServerMode.Development - ); - let appFixture = await createAppFixture(fixture, ServerMode.Development); + }); + let appFixture = await createAppFixture(fixture); let app = new PlaywrightFixture(appFixture, page); let urls: string[] = []; @@ -3121,85 +3082,82 @@ test.describe("single-fetch", () => { test.describe("prefetching", () => { test("when no routes have client loaders", async ({ page }) => { - let fixture = await createFixture( - { - config: { - future: { - unstable_singleFetch: true, - }, + let fixture = await createFixture({ + config: { + future: { + unstable_singleFetch: true, }, - files: { - ...files, - "app/routes/_index.tsx": js` - import { Link } from "@remix-run/react"; + }, + files: { + ...files, + "app/routes/_index.tsx": js` + import { Link } from "@remix-run/react"; - export default function Index() { - return ( - - ); - } - `, - "app/routes/a.tsx": js` - import { Outlet, useLoaderData } from '@remix-run/react'; + export default function Index() { + return ( + + ); + } + `, + "app/routes/a.tsx": js` + import { Outlet, useLoaderData } from '@remix-run/react'; - export function loader() { - return { message: "A server loader" }; - } + export function loader() { + return { message: "A server loader" }; + } - export default function Comp() { - let data = useLoaderData(); - return ( - <> -

A

-

{data.message}

- - - ); - } - `, - "app/routes/a.b.tsx": js` - import { Outlet, useLoaderData } from '@remix-run/react'; + export default function Comp() { + let data = useLoaderData(); + return ( + <> +

A

+

{data.message}

+ + + ); + } + `, + "app/routes/a.b.tsx": js` + import { Outlet, useLoaderData } from '@remix-run/react'; - export function loader() { - return { message: "B server loader" }; - } + export function loader() { + return { message: "B server loader" }; + } - export default function Comp() { - let data = useLoaderData(); - return ( - <> -

B

-

{data.message}

- - - ); - } - `, - "app/routes/a.b.c.tsx": js` - import { useLoaderData } from '@remix-run/react'; + export default function Comp() { + let data = useLoaderData(); + return ( + <> +

B

+

{data.message}

+ + + ); + } + `, + "app/routes/a.b.c.tsx": js` + import { useLoaderData } from '@remix-run/react'; - export function loader() { - return { message: "C server loader" }; - } + export function loader() { + return { message: "C server loader" }; + } - export default function Comp() { - let data = useLoaderData(); - return ( - <> -

C

-

{data.message}

- - ); - } - `, - }, + export default function Comp() { + let data = useLoaderData(); + return ( + <> +

C

+

{data.message}

+ + ); + } + `, }, - ServerMode.Development - ); + }); - let appFixture = await createAppFixture(fixture, ServerMode.Development); + let appFixture = await createAppFixture(fixture); let app = new PlaywrightFixture(appFixture, page); await app.goto("/", true); // No clientLoaders so we can make a single parameter-less fetch @@ -3211,303 +3169,498 @@ test.describe("single-fetch", () => { }); test("when one route has a client loader", async ({ page }) => { - let fixture = await createFixture( - { - config: { - future: { - unstable_singleFetch: true, - }, + let fixture = await createFixture({ + config: { + future: { + unstable_singleFetch: true, }, - files: { - ...files, - "app/routes/_index.tsx": js` - import { Link } from "@remix-run/react"; + }, + files: { + ...files, + "app/routes/_index.tsx": js` + import { Link } from "@remix-run/react"; - export default function Index() { - return ( - - ); - } - `, - "app/routes/a.tsx": js` - import { Outlet, useLoaderData } from '@remix-run/react'; + export default function Index() { + return ( + + ); + } + `, + "app/routes/a.tsx": js` + import { Outlet, useLoaderData } from '@remix-run/react'; - export function loader() { - return { message: "A server loader" }; - } + export function loader() { + return { message: "A server loader" }; + } - export default function Comp() { - let data = useLoaderData(); - return ( - <> -

A

-

{data.message}

- - - ); - } - `, - "app/routes/a.b.tsx": js` - import { Outlet, useLoaderData } from '@remix-run/react'; + export default function Comp() { + let data = useLoaderData(); + return ( + <> +

A

+

{data.message}

+ + + ); + } + `, + "app/routes/a.b.tsx": js` + import { Outlet, useLoaderData } from '@remix-run/react'; - export function loader() { - return { message: "B server loader" }; - } + export function loader() { + return { message: "B server loader" }; + } - export default function Comp() { - let data = useLoaderData(); - return ( - <> -

B

-

{data.message}

- - - ); - } - `, - "app/routes/a.b.c.tsx": js` - import { useLoaderData } from '@remix-run/react'; + export default function Comp() { + let data = useLoaderData(); + return ( + <> +

B

+

{data.message}

+ + + ); + } + `, + "app/routes/a.b.c.tsx": js` + import { useLoaderData } from '@remix-run/react'; - export function loader() { - return { message: "C server loader" }; - } + export function loader() { + return { message: "C server loader" }; + } - export async function clientLoader({ serverLoader }) { - let data = await serverLoader(); - return { message: data.message + " (C client loader)" }; - } + export async function clientLoader({ serverLoader }) { + let data = await serverLoader(); + return { message: data.message + " (C client loader)" }; + } - export default function Comp() { - let data = useLoaderData(); - return ( - <> -

C

-

{data.message}

- - ); - } - `, - }, + export default function Comp() { + let data = useLoaderData(); + return ( + <> +

C

+

{data.message}

+ + ); + } + `, }, - ServerMode.Development - ); + }); - let appFixture = await createAppFixture(fixture, ServerMode.Development); + let appFixture = await createAppFixture(fixture); let app = new PlaywrightFixture(appFixture, page); await app.goto("/", true); - // A/B can be prefetched, C doesn't get prefetched due to its `clientLoader` + // root/A/B can be prefetched, C doesn't get prefetched due to its `clientLoader` await page.waitForSelector( - "nav link[rel='prefetch'][as='fetch'][href='/a/b/c.data?_routes=routes%2Fa%2Croutes%2Fa.b']", + "nav link[rel='prefetch'][as='fetch'][href='/a/b/c.data?_routes=root%2Croutes%2Fa%2Croutes%2Fa.b']", { state: "attached" } ); expect(await app.page.locator("nav link[as='fetch']").count()).toEqual(1); }); test("when multiple routes have client loaders", async ({ page }) => { - let fixture = await createFixture( - { - config: { - future: { - unstable_singleFetch: true, - }, + let fixture = await createFixture({ + config: { + future: { + unstable_singleFetch: true, }, - files: { - ...files, - "app/routes/_index.tsx": js` - import { Link } from "@remix-run/react"; + }, + files: { + ...files, + "app/routes/_index.tsx": js` + import { Link } from "@remix-run/react"; - export default function Index() { - return ( - - ); - } - `, - "app/routes/a.tsx": js` - import { Outlet, useLoaderData } from '@remix-run/react'; + export default function Index() { + return ( + + ); + } + `, + "app/routes/a.tsx": js` + import { Outlet, useLoaderData } from '@remix-run/react'; - export function loader() { - return { message: "A server loader" }; - } + export function loader() { + return { message: "A server loader" }; + } - export default function Comp() { - let data = useLoaderData(); - return ( - <> -

A

-

{data.message}

- - - ); - } - `, - "app/routes/a.b.tsx": js` - import { Outlet, useLoaderData } from '@remix-run/react'; + export default function Comp() { + let data = useLoaderData(); + return ( + <> +

A

+

{data.message}

+ + + ); + } + `, + "app/routes/a.b.tsx": js` + import { Outlet, useLoaderData } from '@remix-run/react'; - export function loader() { - return { message: "B server loader" }; - } + export function loader() { + return { message: "B server loader" }; + } - export async function clientLoader({ serverLoader }) { - let data = await serverLoader(); - return { message: data.message + " (B client loader)" }; - } + export async function clientLoader({ serverLoader }) { + let data = await serverLoader(); + return { message: data.message + " (B client loader)" }; + } - export default function Comp() { - let data = useLoaderData(); - return ( - <> -

B

-

{data.message}

- - - ); - } - `, - "app/routes/a.b.c.tsx": js` - import { useLoaderData } from '@remix-run/react'; + export default function Comp() { + let data = useLoaderData(); + return ( + <> +

B

+

{data.message}

+ + + ); + } + `, + "app/routes/a.b.c.tsx": js` + import { useLoaderData } from '@remix-run/react'; - export function loader() { - return { message: "C server loader" }; - } + export function loader() { + return { message: "C server loader" }; + } - export async function clientLoader({ serverLoader }) { - let data = await serverLoader(); - return { message: data.message + " (C client loader)" }; - } + export async function clientLoader({ serverLoader }) { + let data = await serverLoader(); + return { message: data.message + " (C client loader)" }; + } - export default function Comp() { - let data = useLoaderData(); - return ( - <> -

C

-

{data.message}

- - ); - } - `, - }, + export default function Comp() { + let data = useLoaderData(); + return ( + <> +

C

+

{data.message}

+ + ); + } + `, }, - ServerMode.Development - ); + }); - let appFixture = await createAppFixture(fixture, ServerMode.Development); + let appFixture = await createAppFixture(fixture); let app = new PlaywrightFixture(appFixture, page); await app.goto("/", true); - // Only A can get prefetched, B/C can't due to `clientLoader` + // root/A can get prefetched, B/C can't due to `clientLoader` await page.waitForSelector( - "nav link[rel='prefetch'][as='fetch'][href='/a/b/c.data?_routes=routes%2Fa']", + "nav link[rel='prefetch'][as='fetch'][href='/a/b/c.data?_routes=root%2Croutes%2Fa']", { state: "attached" } ); expect(await app.page.locator("nav link[as='fetch']").count()).toEqual(1); }); test("when all routes have client loaders", async ({ page }) => { - let fixture = await createFixture( - { - config: { - future: { - unstable_singleFetch: true, - }, + let fixture = await createFixture({ + config: { + future: { + unstable_singleFetch: true, }, - files: { - ...files, - "app/routes/_index.tsx": js` - import { Link } from "@remix-run/react"; + }, + files: { + ...files, + "app/root.tsx": js` + import { Links, Meta, Outlet, Scripts } from "@remix-run/react"; - export default function Index() { - return ( - - ); - } - `, - "app/routes/a.tsx": js` - import { Outlet, useLoaderData } from '@remix-run/react'; + export function loader() { + return { + message: "ROOT", + }; + } - export function loader() { - return { message: "A server loader" }; - } + export async function clientLoader({ serverLoader }) { + let data = await serverLoader(); + return { message: data.message + " (root client loader)" }; + } - export async function clientLoader({ serverLoader }) { - let data = await serverLoader(); - return { message: data.message + " (A client loader)" }; - } + export default function Root() { + return ( + + + + + + + + + + + ); + } + `, + "app/routes/_index.tsx": js` + import { Link } from "@remix-run/react"; - export default function Comp() { - let data = useLoaderData(); - return ( - <> -

A

-

{data.message}

- - - ); - } - `, - "app/routes/a.b.tsx": js` - import { Outlet, useLoaderData } from '@remix-run/react'; + export default function Index() { + return ( + + ); + } + `, + "app/routes/a.tsx": js` + import { Outlet, useLoaderData } from '@remix-run/react'; - export function loader() { - return { message: "B server loader" }; - } + export function loader() { + return { message: "A server loader" }; + } - export async function clientLoader({ serverLoader }) { - let data = await serverLoader(); - return { message: data.message + " (B client loader)" }; - } + export async function clientLoader({ serverLoader }) { + let data = await serverLoader(); + return { message: data.message + " (A client loader)" }; + } - export default function Comp() { - let data = useLoaderData(); - return ( - <> -

B

-

{data.message}

- - - ); - } - `, - "app/routes/a.b.c.tsx": js` - import { useLoaderData } from '@remix-run/react'; + export default function Comp() { + let data = useLoaderData(); + return ( + <> +

A

+

{data.message}

+ + + ); + } + `, + "app/routes/a.b.tsx": js` + import { Outlet, useLoaderData } from '@remix-run/react'; - export function loader() { - return { message: "C server loader" }; - } + export function loader() { + return { message: "B server loader" }; + } - export async function clientLoader({ serverLoader }) { - let data = await serverLoader(); - return { message: data.message + " (C client loader)" }; - } + export async function clientLoader({ serverLoader }) { + let data = await serverLoader(); + return { message: data.message + " (B client loader)" }; + } - export default function Comp() { - let data = useLoaderData(); - return ( - <> -

C

-

{data.message}

- - ); - } - `, - }, + export default function Comp() { + let data = useLoaderData(); + return ( + <> +

B

+

{data.message}

+ + + ); + } + `, + "app/routes/a.b.c.tsx": js` + import { useLoaderData } from '@remix-run/react'; + + export function loader() { + return { message: "C server loader" }; + } + + export async function clientLoader({ serverLoader }) { + let data = await serverLoader(); + return { message: data.message + " (C client loader)" }; + } + + export default function Comp() { + let data = useLoaderData(); + return ( + <> +

C

+

{data.message}

+ + ); + } + `, }, - ServerMode.Development - ); + }); - let appFixture = await createAppFixture(fixture, ServerMode.Development); + let appFixture = await createAppFixture(fixture); let app = new PlaywrightFixture(appFixture, page); await app.goto("/", true); // No prefetching due to clientLoaders expect(await app.page.locator("nav link[as='fetch']").count()).toEqual(0); }); + + test("when a reused route opts out of revalidation", async ({ page }) => { + let fixture = await createFixture({ + config: { + future: { + unstable_singleFetch: true, + }, + }, + files: { + ...files, + "app/routes/a.tsx": js` + import { Link, Outlet, useLoaderData } from '@remix-run/react'; + + export function loader() { + return { message: "A server loader" }; + } + + export function shouldRevalidate() { + return false; + } + + export default function Comp() { + let data = useLoaderData(); + return ( + <> +

A

+

{data.message}

+ + + + ); + } + `, + "app/routes/a.b.tsx": js` + import { Outlet, useLoaderData } from '@remix-run/react'; + + export function loader() { + return { message: "B server loader" }; + } + + export default function Comp() { + let data = useLoaderData(); + return ( + <> +

B

+

{data.message}

+ + + ); + } + `, + "app/routes/a.b.c.tsx": js` + import { useLoaderData } from '@remix-run/react'; + + export function loader() { + return { message: "C server loader" }; + } + + export default function Comp() { + let data = useLoaderData(); + return ( + <> +

C

+

{data.message}

+ + ); + } + `, + }, + }); + + let appFixture = await createAppFixture(fixture); + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/a", true); + + // A opted out of revalidation + await page.waitForSelector( + "link[rel='prefetch'][as='fetch'][href='/a/b/c.data?_routes=root%2Croutes%2Fa.b%2Croutes%2Fa.b.c']", + { state: "attached" } + ); + expect(await app.page.locator("nav link[as='fetch']").count()).toEqual(1); + }); + + test("when a reused route opts out of revalidation and another route has a clientLoader", async ({ + page, + }) => { + let fixture = await createFixture({ + config: { + future: { + unstable_singleFetch: true, + }, + }, + files: { + ...files, + "app/routes/a.tsx": js` + import { Link, Outlet, useLoaderData } from '@remix-run/react'; + + export function loader() { + return { message: "A server loader" }; + } + + export function shouldRevalidate() { + return false; + } + + export default function Comp() { + let data = useLoaderData(); + return ( + <> +

A

+

{data.message}

+ + + + ); + } + `, + "app/routes/a.b.tsx": js` + import { Outlet, useLoaderData } from '@remix-run/react'; + + export function loader() { + return { message: "B server loader" }; + } + + export default function Comp() { + let data = useLoaderData(); + return ( + <> +

B

+

{data.message}

+ + + ); + } + `, + "app/routes/a.b.c.tsx": js` + import { useLoaderData } from '@remix-run/react'; + + export function loader() { + return { message: "C server loader" }; + } + + export async function clientLoader({ serverLoader }) { + let data = await serverLoader(); + return { message: data.message + " (C client loader)" }; + } + + export default function Comp() { + let data = useLoaderData(); + return ( + <> +

C

+

{data.message}

+ + ); + } + `, + }, + }); + + let appFixture = await createAppFixture(fixture); + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/a", true); + + // A opted out of revalidation + await page.waitForSelector( + "nav link[rel='prefetch'][as='fetch'][href='/a/b/c.data?_routes=root%2Croutes%2Fa.b']", + { state: "attached" } + ); + expect(await app.page.locator("nav link[as='fetch']").count()).toEqual(1); + }); }); test("supports nonce on streaming script tags", async ({ page }) => { diff --git a/integration/vite-hmr-hdr-test.ts b/integration/vite-hmr-hdr-test.ts index 68003e673a9..7a0f536b6f9 100644 --- a/integration/vite-hmr-hdr-test.ts +++ b/integration/vite-hmr-hdr-test.ts @@ -10,6 +10,7 @@ import { EXPRESS_SERVER, viteConfig, } from "./helpers/vite.js"; +import { js } from "./helpers/create-fixture.js"; const indexRoute = ` // imports @@ -112,6 +113,61 @@ test("Vite / HMR & HDR / mdx", async ({ page, viteDev }) => { expect(page.errors).toEqual([]); }); +test.describe("single fetch", () => { + test("Vite / HMR & HDR / vite dev", async ({ + page, + browserName, + viteDev, + }) => { + let files: Files = async ({ port }) => ({ + "vite.config.js": js` + import { vitePlugin as remix } from "@remix-run/dev"; + + export default { + ${await viteConfig.server({ port })} + plugins: [ + remix({ + future: { + unstable_singleFetch: true + }, + }) + ] + } + `, + "app/routes/_index.tsx": indexRoute, + }); + let { cwd, port } = await viteDev(files); + await workflow({ page, browserName, cwd, port }); + }); + + test("Vite / HMR & HDR / express", async ({ + page, + browserName, + customDev, + }) => { + let files: Files = async ({ port }) => ({ + "vite.config.js": js` + import { vitePlugin as remix } from "@remix-run/dev"; + + export default { + ${await viteConfig.server({ port })} + plugins: [ + remix({ + future: { + unstable_singleFetch: true + }, + }) + ] + } + `, + "server.mjs": EXPRESS_SERVER({ port }), + "app/routes/_index.tsx": indexRoute, + }); + let { cwd, port } = await customDev(files); + await workflow({ page, browserName, cwd, port }); + }); +}); + async function workflow({ page, browserName, diff --git a/packages/create-remix/CHANGELOG.md b/packages/create-remix/CHANGELOG.md index 0b297c517b8..845fff30086 100644 --- a/packages/create-remix/CHANGELOG.md +++ b/packages/create-remix/CHANGELOG.md @@ -1,5 +1,9 @@ # `create-remix` +## 2.12.0 + +No significant changes to this package were made in this release. [See the repo `CHANGELOG.md`](https://github.com/remix-run/remix/blob/main/CHANGELOG.md) for an overview of all changes in v2.12.0. + ## 2.11.2 No significant changes to this package were made in this release. [See the repo `CHANGELOG.md`](https://github.com/remix-run/remix/blob/main/CHANGELOG.md) for an overview of all changes in v2.11.2. diff --git a/packages/create-remix/package.json b/packages/create-remix/package.json index 8d13800eca4..727c5babaf1 100644 --- a/packages/create-remix/package.json +++ b/packages/create-remix/package.json @@ -1,6 +1,6 @@ { "name": "create-remix", - "version": "2.11.2", + "version": "2.12.0", "description": "Create a new Remix app", "homepage": "https://remix.run", "bugs": { diff --git a/packages/remix-architect/CHANGELOG.md b/packages/remix-architect/CHANGELOG.md index 7c38a5bb93b..696b907da26 100644 --- a/packages/remix-architect/CHANGELOG.md +++ b/packages/remix-architect/CHANGELOG.md @@ -1,5 +1,12 @@ # `@remix-run/architect` +## 2.12.0 + +### Patch Changes + +- Updated dependencies: + - `@remix-run/node@2.12.0` + ## 2.11.2 ### Patch Changes diff --git a/packages/remix-architect/package.json b/packages/remix-architect/package.json index e0213431353..4023d9edcb3 100644 --- a/packages/remix-architect/package.json +++ b/packages/remix-architect/package.json @@ -1,6 +1,6 @@ { "name": "@remix-run/architect", - "version": "2.11.2", + "version": "2.12.0", "description": "Architect server request handler for Remix", "bugs": { "url": "https://github.com/remix-run/remix/issues" diff --git a/packages/remix-cloudflare-pages/CHANGELOG.md b/packages/remix-cloudflare-pages/CHANGELOG.md index 7944c07a0ed..7f9fdeb1ea4 100644 --- a/packages/remix-cloudflare-pages/CHANGELOG.md +++ b/packages/remix-cloudflare-pages/CHANGELOG.md @@ -1,5 +1,12 @@ # `@remix-run/cloudflare-pages` +## 2.12.0 + +### Patch Changes + +- Updated dependencies: + - `@remix-run/cloudflare@2.12.0` + ## 2.11.2 ### Patch Changes diff --git a/packages/remix-cloudflare-pages/package.json b/packages/remix-cloudflare-pages/package.json index 70038b118a7..ae13051720a 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": "2.11.2", + "version": "2.12.0", "description": "Cloudflare Pages request handler for Remix", "bugs": { "url": "https://github.com/remix-run/remix/issues" diff --git a/packages/remix-cloudflare-workers/CHANGELOG.md b/packages/remix-cloudflare-workers/CHANGELOG.md index b48f2f776fb..6930c40c9a9 100644 --- a/packages/remix-cloudflare-workers/CHANGELOG.md +++ b/packages/remix-cloudflare-workers/CHANGELOG.md @@ -1,5 +1,12 @@ # `@remix-run/cloudflare-workers` +## 2.12.0 + +### Patch Changes + +- Updated dependencies: + - `@remix-run/cloudflare@2.12.0` + ## 2.11.2 ### Patch Changes diff --git a/packages/remix-cloudflare-workers/package.json b/packages/remix-cloudflare-workers/package.json index bdc342d0c24..9e59528b1e5 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": "2.11.2", + "version": "2.12.0", "description": "Cloudflare worker request handler for Remix", "bugs": { "url": "https://github.com/remix-run/remix/issues" diff --git a/packages/remix-cloudflare/CHANGELOG.md b/packages/remix-cloudflare/CHANGELOG.md index 12b7800d9f3..4825be0a57b 100644 --- a/packages/remix-cloudflare/CHANGELOG.md +++ b/packages/remix-cloudflare/CHANGELOG.md @@ -1,5 +1,45 @@ # `@remix-run/cloudflare` +## 2.12.0 + +### Patch Changes + +- Single Fetch: Improved typesafety ([#9893](https://github.com/remix-run/remix/pull/9893)) + + If you were already using previously released unstable single-fetch types: + + - Remove `"@remix-run/react/future/single-fetch.d.ts"` override from `tsconfig.json` > `compilerOptions` > `types` + - Remove `defineLoader`, `defineAction`, `defineClientLoader`, `defineClientAction` helpers from your route modules + - Replace `UIMatch_SingleFetch` type helper with `UIMatch` + - Replace `MetaArgs_SingleFetch` type helper with `MetaArgs` + + Then you are ready for the new typesafety setup: + + ```ts + // vite.config.ts + + declare module "@remix-run/server-runtime" { + interface Future { + unstable_singleFetch: true; // 👈 enable _types_ for single-fetch + } + } + + export default defineConfig({ + plugins: [ + remix({ + future: { + unstable_singleFetch: true, // 👈 enable single-fetch + }, + }), + ], + }); + ``` + + For more information, see [Guides > Single Fetch](https://remix.run/docs/en/dev/guides/single-fetch) in our docs. + +- Updated dependencies: + - `@remix-run/server-runtime@2.12.0` + ## 2.11.2 ### Patch Changes diff --git a/packages/remix-cloudflare/package.json b/packages/remix-cloudflare/package.json index 1ad526c9a03..6e65754e8cc 100644 --- a/packages/remix-cloudflare/package.json +++ b/packages/remix-cloudflare/package.json @@ -1,6 +1,6 @@ { "name": "@remix-run/cloudflare", - "version": "2.11.2", + "version": "2.12.0", "description": "Cloudflare platform abstractions for Remix", "bugs": { "url": "https://github.com/remix-run/remix/issues" diff --git a/packages/remix-css-bundle/CHANGELOG.md b/packages/remix-css-bundle/CHANGELOG.md index 30fd4082b8a..25de667e8b9 100644 --- a/packages/remix-css-bundle/CHANGELOG.md +++ b/packages/remix-css-bundle/CHANGELOG.md @@ -1,5 +1,9 @@ # @remix-run/css-bundle +## 2.12.0 + +No significant changes to this package were made in this release. [See the repo `CHANGELOG.md`](https://github.com/remix-run/remix/blob/main/CHANGELOG.md) for an overview of all changes in v2.12.0. + ## 2.11.2 No significant changes to this package were made in this release. [See the repo `CHANGELOG.md`](https://github.com/remix-run/remix/blob/main/CHANGELOG.md) for an overview of all changes in v2.11.2. diff --git a/packages/remix-css-bundle/package.json b/packages/remix-css-bundle/package.json index 5eee7c7d4a5..5d62a44d4f0 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": "2.11.2", + "version": "2.12.0", "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 39b40a427c3..c2191665f3f 100644 --- a/packages/remix-deno/CHANGELOG.md +++ b/packages/remix-deno/CHANGELOG.md @@ -1,5 +1,45 @@ # `@remix-run/deno` +## 2.12.0 + +### Patch Changes + +- Single Fetch: Improved typesafety ([#9893](https://github.com/remix-run/remix/pull/9893)) + + If you were already using previously released unstable single-fetch types: + + - Remove `"@remix-run/react/future/single-fetch.d.ts"` override from `tsconfig.json` > `compilerOptions` > `types` + - Remove `defineLoader`, `defineAction`, `defineClientLoader`, `defineClientAction` helpers from your route modules + - Replace `UIMatch_SingleFetch` type helper with `UIMatch` + - Replace `MetaArgs_SingleFetch` type helper with `MetaArgs` + + Then you are ready for the new typesafety setup: + + ```ts + // vite.config.ts + + declare module "@remix-run/server-runtime" { + interface Future { + unstable_singleFetch: true; // 👈 enable _types_ for single-fetch + } + } + + export default defineConfig({ + plugins: [ + remix({ + future: { + unstable_singleFetch: true, // 👈 enable single-fetch + }, + }), + ], + }); + ``` + + For more information, see [Guides > Single Fetch](https://remix.run/docs/en/dev/guides/single-fetch) in our docs. + +- Updated dependencies: + - `@remix-run/server-runtime@2.12.0` + ## 2.11.2 ### Patch Changes diff --git a/packages/remix-deno/package.json b/packages/remix-deno/package.json index 864eadcd91f..f9b46f44db2 100644 --- a/packages/remix-deno/package.json +++ b/packages/remix-deno/package.json @@ -1,6 +1,6 @@ { "name": "@remix-run/deno", - "version": "2.11.2", + "version": "2.12.0", "description": "Deno platform abstractions for Remix", "homepage": "https://remix.run", "main": "./index.ts", diff --git a/packages/remix-dev/CHANGELOG.md b/packages/remix-dev/CHANGELOG.md index 12354b7deb8..b7cbb9ad54d 100644 --- a/packages/remix-dev/CHANGELOG.md +++ b/packages/remix-dev/CHANGELOG.md @@ -1,5 +1,22 @@ # `@remix-run/dev` +## 2.12.0 + +### Minor Changes + +- New `future.unstable_optimizeDeps` flag for automatic dependency optimization ([#9921](https://github.com/remix-run/remix/pull/9921)) + - You can now opt-in to automatic dependency optimization during development by using the `future.unstable_optimizeDeps` future flag + - For details, check out the docs at [`Guides` > `Dependency optimization`](https://remix.run/docs/en/main/guides/dependency-optimization) + - For users who were previously working around this limitation, you no longer need to explicitly add routes to Vite's `optimizeDeps.entries` nor do you need to disable the `remix-dot-server` plugin + +### Patch Changes + +- Handle circular dependencies in modulepreload manifest generation ([#9917](https://github.com/remix-run/remix/pull/9917)) +- Fix `dest already exists` build errors by only moving SSR assets to the client build directory when they're not already present on disk ([#9901](https://github.com/remix-run/remix/pull/9901)) +- Updated dependencies: + - `@remix-run/server-runtime@2.12.0` + - `@remix-run/node@2.12.0` + ## 2.11.2 ### Patch Changes diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 8e3e6db0d0c..81a939b29d6 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,6 +1,6 @@ { "name": "@remix-run/dev", - "version": "2.11.2", + "version": "2.12.0", "description": "Dev tools and CLI for Remix", "homepage": "https://remix.run", "bugs": { @@ -32,7 +32,7 @@ "@mdx-js/mdx": "^2.3.0", "@npmcli/package-json": "^4.0.1", "@remix-run/node": "workspace:*", - "@remix-run/router": "1.19.2-pre.0", + "@remix-run/router": "1.19.2", "@remix-run/server-runtime": "workspace:*", "@types/mdx": "^2.0.5", "@vanilla-extract/integration": "^6.2.0", @@ -106,8 +106,8 @@ "wrangler": "^3.28.2" }, "peerDependencies": { - "@remix-run/react": "^2.11.2", - "@remix-run/serve": "^2.11.2", + "@remix-run/react": "^2.12.0", + "@remix-run/serve": "^2.12.0", "typescript": "^5.1.0", "vite": "^5.1.0", "wrangler": "^3.28.2" diff --git a/packages/remix-dev/vite/static/refresh-utils.cjs b/packages/remix-dev/vite/static/refresh-utils.cjs index a2891438a0c..6d2db1ef007 100644 --- a/packages/remix-dev/vite/static/refresh-utils.cjs +++ b/packages/remix-dev/vite/static/refresh-utils.cjs @@ -58,7 +58,13 @@ const enqueueUpdate = debounce(async () => { window.__remixRouteModuleUpdates.clear(); } - await revalidate(); + try { + window.__remixHdrActive = true; + await revalidate(); + } finally { + window.__remixHdrActive = false; + } + if (manifest) { Object.assign(window.__remixManifest, manifest); } diff --git a/packages/remix-eslint-config/CHANGELOG.md b/packages/remix-eslint-config/CHANGELOG.md index f708370c3f6..03fbed7f466 100644 --- a/packages/remix-eslint-config/CHANGELOG.md +++ b/packages/remix-eslint-config/CHANGELOG.md @@ -1,5 +1,9 @@ # `@remix-run/eslint-config` +## 2.12.0 + +No significant changes to this package were made in this release. [See the repo `CHANGELOG.md`](https://github.com/remix-run/remix/blob/main/CHANGELOG.md) for an overview of all changes in v2.12.0. + ## 2.11.2 No significant changes to this package were made in this release. [See the repo `CHANGELOG.md`](https://github.com/remix-run/remix/blob/main/CHANGELOG.md) for an overview of all changes in v2.11.2. diff --git a/packages/remix-eslint-config/package.json b/packages/remix-eslint-config/package.json index 02a74260069..6a520758fde 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": "2.11.2", + "version": "2.12.0", "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 b685717a35b..daee7517143 100644 --- a/packages/remix-express/CHANGELOG.md +++ b/packages/remix-express/CHANGELOG.md @@ -1,5 +1,12 @@ # `@remix-run/express` +## 2.12.0 + +### Patch Changes + +- Updated dependencies: + - `@remix-run/node@2.12.0` + ## 2.11.2 ### Patch Changes diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 4bed3d973d4..9a2681edc01 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,6 +1,6 @@ { "name": "@remix-run/express", - "version": "2.11.2", + "version": "2.12.0", "description": "Express server request handler for Remix", "bugs": { "url": "https://github.com/remix-run/remix/issues" diff --git a/packages/remix-node/CHANGELOG.md b/packages/remix-node/CHANGELOG.md index b179612806f..86ad76d41c4 100644 --- a/packages/remix-node/CHANGELOG.md +++ b/packages/remix-node/CHANGELOG.md @@ -1,5 +1,45 @@ # `@remix-run/node` +## 2.12.0 + +### Patch Changes + +- Single Fetch: Improved typesafety ([#9893](https://github.com/remix-run/remix/pull/9893)) + + If you were already using previously released unstable single-fetch types: + + - Remove `"@remix-run/react/future/single-fetch.d.ts"` override from `tsconfig.json` > `compilerOptions` > `types` + - Remove `defineLoader`, `defineAction`, `defineClientLoader`, `defineClientAction` helpers from your route modules + - Replace `UIMatch_SingleFetch` type helper with `UIMatch` + - Replace `MetaArgs_SingleFetch` type helper with `MetaArgs` + + Then you are ready for the new typesafety setup: + + ```ts + // vite.config.ts + + declare module "@remix-run/server-runtime" { + interface Future { + unstable_singleFetch: true; // 👈 enable _types_ for single-fetch + } + } + + export default defineConfig({ + plugins: [ + remix({ + future: { + unstable_singleFetch: true, // 👈 enable single-fetch + }, + }), + ], + }); + ``` + + For more information, see [Guides > Single Fetch](https://remix.run/docs/en/dev/guides/single-fetch) in our docs. + +- Updated dependencies: + - `@remix-run/server-runtime@2.12.0` + ## 2.11.2 ### Patch Changes diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 3e92e8f9bec..62eeb2d36c7 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,6 +1,6 @@ { "name": "@remix-run/node", - "version": "2.11.2", + "version": "2.12.0", "description": "Node.js platform abstractions for Remix", "bugs": { "url": "https://github.com/remix-run/remix/issues" diff --git a/packages/remix-react/CHANGELOG.md b/packages/remix-react/CHANGELOG.md index 6ee1d05ea57..88d564335ec 100644 --- a/packages/remix-react/CHANGELOG.md +++ b/packages/remix-react/CHANGELOG.md @@ -1,5 +1,69 @@ # `@remix-run/react` +## 2.12.0 + +### Patch Changes + +- Lazy Route Discovery: Sort `/__manifest` query parameters for better caching ([#9888](https://github.com/remix-run/remix/pull/9888)) + +- Single Fetch: fix revalidation behavior bugs ([#9938](https://github.com/remix-run/remix/pull/9938)) + + - With Single Fetch, existing routes revalidate by default + - This means requests do not need special query params for granular route revalidations out of the box - i.e., `GET /a/b/c.data` + - There are two conditions that will trigger granular revalidation: + - If a route opts out of revalidation via `shouldRevalidate`, it will be excluded from the single fetch call + - If a route defines a `clientLoader` then it will be excluded from the single fetch call and if you call `serverLoader()` from your `clientLoader`, that will make a separarte HTTP call for just that route loader - i.e., `GET /a/b/c.data?_routes=routes/a` for a `clientLoader` in `routes/a.tsx` + - When one or more routes are excluded from the single fetch call, the remaining routes that have loaders are included as query params: + - For example, if A was excluded, and the `root` route and `routes/b` had a `loader` but `routes/c` did not, the single fetch request would be `GET /a/b/c.data?_routes=root,routes/a` + +- Remove hydration URL check that was originally added for React 17 hydration issues and we no longer support React 17 ([#9890](https://github.com/remix-run/remix/pull/9890)) + + - Reverts the logic originally added in Remix `v1.18.0` via + - This was added to resolve an issue that could arise when doing quick back/forward history navigations while JS was loading which would cause a mismatch between the server matches and client matches: + - This specific hydration issue would then cause this React v17 only looping issue: + - The URL comparison that we added in `1.18.0` turned out to be subject to false positives of it's own which could also put the user in looping scenarios + - Remix v2 upgraded it's minimal React version to v18 which eliminated the v17 hydration error loop + - React v18 handles this hydration error like any other error and does not result in a loop + - So we can remove our check and thus avoid the false-positive scenarios in which it may also trigger a loop + +- Single Fetch: Improved typesafety ([#9893](https://github.com/remix-run/remix/pull/9893)) + + If you were already using previously released unstable single-fetch types: + + - Remove `"@remix-run/react/future/single-fetch.d.ts"` override from `tsconfig.json` > `compilerOptions` > `types` + - Remove `defineLoader`, `defineAction`, `defineClientLoader`, `defineClientAction` helpers from your route modules + - Replace `UIMatch_SingleFetch` type helper with `UIMatch` + - Replace `MetaArgs_SingleFetch` type helper with `MetaArgs` + + Then you are ready for the new typesafety setup: + + ```ts + // vite.config.ts + + declare module "@remix-run/server-runtime" { + interface Future { + unstable_singleFetch: true; // 👈 enable _types_ for single-fetch + } + } + + export default defineConfig({ + plugins: [ + remix({ + future: { + unstable_singleFetch: true, // 👈 enable single-fetch + }, + }), + ], + }); + ``` + + For more information, see [Guides > Single Fetch](https://remix.run/docs/en/dev/guides/single-fetch) in our docs. + +- Clarify wording in default `HydrateFallback` console warning ([#9899](https://github.com/remix-run/remix/pull/9899)) + +- Updated dependencies: + - `@remix-run/server-runtime@2.12.0` + ## 2.11.2 ### Patch Changes diff --git a/packages/remix-react/browser.tsx b/packages/remix-react/browser.tsx index 1ee298c23f0..39e27f11844 100644 --- a/packages/remix-react/browser.tsx +++ b/packages/remix-react/browser.tsx @@ -46,6 +46,7 @@ declare global { var __remixRouteModules: RouteModules; var __remixManifest: AssetsManifest; var __remixRevalidation: number | undefined; + var __remixHdrActive: boolean; var __remixClearCriticalCss: (() => void) | undefined; var $RefreshRuntime$: { performReactRefresh: () => void; diff --git a/packages/remix-react/components.tsx b/packages/remix-react/components.tsx index 0f8725b824e..5fccf3ba25d 100644 --- a/packages/remix-react/components.tsx +++ b/packages/remix-react/components.tsx @@ -56,7 +56,7 @@ import type { MetaMatches, RouteHandle, } from "./routeModules"; -import { addRevalidationParam, singleFetchUrl } from "./single-fetch"; +import { singleFetchUrl } from "./single-fetch"; import { getPartialManifest, isFogOfWarEnabled } from "./fog-of-war"; function useDataRouterContext() { @@ -449,7 +449,7 @@ function PrefetchPageLinksImpl({ }) { let location = useLocation(); let { future, manifest, routeModules } = useRemixContext(); - let { matches } = useDataRouterStateContext(); + let { loaderData, matches } = useDataRouterStateContext(); let newMatchesForData = React.useMemo( () => @@ -464,6 +464,69 @@ function PrefetchPageLinksImpl({ [page, nextMatches, matches, manifest, location] ); + let dataHrefs = React.useMemo(() => { + if (!future.unstable_singleFetch) { + return getDataLinkHrefs(page, newMatchesForData, manifest); + } + + if (page === location.pathname + location.search + location.hash) { + // Because we opt-into revalidation, don't compute this for the current page + // since it would always trigger a prefetch of the existing loaders + return []; + } + + // Single-fetch is harder :) + // This parallels the logic in the single fetch data strategy + let routesParams = new Set(); + let foundOptOutRoute = false; + nextMatches.forEach((m) => { + if (!manifest.routes[m.route.id].hasLoader) { + return; + } + + if ( + !newMatchesForData.some((m2) => m2.route.id === m.route.id) && + m.route.id in loaderData && + routeModules[m.route.id]?.shouldRevalidate + ) { + foundOptOutRoute = true; + } else if (manifest.routes[m.route.id].hasClientLoader) { + foundOptOutRoute = true; + } else { + routesParams.add(m.route.id); + } + }); + + if (routesParams.size === 0) { + return []; + } + + let url = singleFetchUrl(page); + // When one or more routes have opted out, we add a _routes param to + // limit the loaders to those that have a server loader and did not + // opt out + if (foundOptOutRoute && routesParams.size > 0) { + url.searchParams.set( + "_routes", + nextMatches + .filter((m) => routesParams.has(m.route.id)) + .map((m) => m.route.id) + .join(",") + ); + } + + return [url.pathname + url.search]; + }, [ + future.unstable_singleFetch, + loaderData, + location, + manifest, + newMatchesForData, + nextMatches, + page, + routeModules, + ]); + let newMatchesForAssets = React.useMemo( () => getNewMatchesForLinks( @@ -477,11 +540,6 @@ function PrefetchPageLinksImpl({ [page, nextMatches, matches, manifest, location] ); - let dataHrefs = React.useMemo( - () => getDataLinkHrefs(page, newMatchesForData, manifest), - [newMatchesForData, page, manifest] - ); - let moduleHrefs = React.useMemo( () => getModuleLinkHrefs(newMatchesForAssets, manifest), [newMatchesForAssets, manifest] @@ -491,41 +549,11 @@ function PrefetchPageLinksImpl({ // just the manifest like the other links in here. let keyedPrefetchLinks = useKeyedPrefetchLinks(newMatchesForAssets); - let linksToRender: React.ReactNode | React.ReactNode[] | null = null; - if (!future.unstable_singleFetch) { - // Non-single-fetch prefetching - linksToRender = dataHrefs.map((href) => ( - - )); - } else if (newMatchesForData.length > 0) { - // Single-fetch with routes that require data - let url = addRevalidationParam( - manifest, - routeModules, - nextMatches.map((m) => m.route), - newMatchesForData.map((m) => m.route), - singleFetchUrl(page) - ); - if (url.searchParams.get("_routes") !== "") { - linksToRender = ( - - ); - } else { - // No single-fetch prefetching if _routes param is empty due to `clientLoader`'s - } - } else { - // No single-fetch prefetching if there are no new matches for data - } - return ( <> - {linksToRender} + {dataHrefs.map((href) => ( + + ))} {moduleHrefs.map((href) => ( ))} diff --git a/packages/remix-react/package.json b/packages/remix-react/package.json index c450e161fc2..798318f9438 100644 --- a/packages/remix-react/package.json +++ b/packages/remix-react/package.json @@ -1,6 +1,6 @@ { "name": "@remix-run/react", - "version": "2.11.2", + "version": "2.12.0", "description": "React DOM bindings for Remix", "bugs": { "url": "https://github.com/remix-run/remix/issues" @@ -19,10 +19,10 @@ "tsc": "tsc" }, "dependencies": { - "@remix-run/router": "1.19.2-pre.0", + "@remix-run/router": "1.19.2", "@remix-run/server-runtime": "workspace:*", - "react-router": "6.26.2-pre.0", - "react-router-dom": "6.26.2-pre.0", + "react-router": "6.26.2", + "react-router-dom": "6.26.2", "turbo-stream": "2.4.0" }, "devDependencies": { diff --git a/packages/remix-react/single-fetch.tsx b/packages/remix-react/single-fetch.tsx index e10da5f199f..f390413fcec 100644 --- a/packages/remix-react/single-fetch.tsx +++ b/packages/remix-react/single-fetch.tsx @@ -16,10 +16,7 @@ import type { UNSAFE_SingleFetchResults as SingleFetchResults, } from "@remix-run/server-runtime"; import { UNSAFE_SingleFetchRedirectSymbol as SingleFetchRedirectSymbol } from "@remix-run/server-runtime"; -import type { - DataRouteObject, - unstable_DataStrategyFunctionArgs as DataStrategyFunctionArgs, -} from "react-router-dom"; +import type { unstable_DataStrategyFunctionArgs as DataStrategyFunctionArgs } from "react-router-dom"; import { decode } from "turbo-stream"; import { createRequestInit, isResponse } from "./data"; @@ -261,35 +258,29 @@ async function singleFetchLoaderNavigationStrategy( results[m.route.id] = { type: "error", result: e }; } return; - } else if (!manifest.routes[m.route.id].hasLoader) { - // If we don't have a server loader, then we don't care about the HTTP - // call and can just send back a `null` - because we _do_ have a `loader` - // in the client router handling route module/styles loads - results[m.route.id] = { - type: "data", - result: null, - }; - return; } - // Otherwise, we want to load this route on the server and can lump this - // it in with the others on a singular promise - routesParams.add(m.route.id); + // Load this route on the server if it has a loader + if (manifest.routes[m.route.id].hasLoader) { + routesParams.add(m.route.id); + } - await handler(async () => { - try { + // Lump this match in with the others on a singular promise + try { + let result = await handler(async () => { let data = await singleFetchDfd.promise; - results[m.route.id] = { - type: "data", - result: unwrapSingleFetchResults(data, m.route.id), - }; - } catch (e) { - results[m.route.id] = { - type: "error", - result: e, - }; - } - }); + return unwrapSingleFetchResults(data, m.route.id); + }); + results[m.route.id] = { + type: "data", + result, + }; + } catch (e) { + results[m.route.id] = { + type: "error", + result: e, + }; + } }) ) ); @@ -297,10 +288,17 @@ async function singleFetchLoaderNavigationStrategy( // Wait for all routes to resolve above before we make the HTTP call await routesLoadedPromise; - // Don't make any single fetch server calls: + // We can skip the server call: // - On initial hydration - only clientLoaders can pass through via `clientLoader.hydrate` // - If there are no routes to fetch from the server - if (!router.state.initialized || routesParams.size === 0) { + // + // One exception - if we are performing an HDR revalidation we have to call + // the server in case a new loader has shown up that the manifest doesn't yet + // know about + if ( + (!router.state.initialized || routesParams.size === 0) && + !window.__remixHdrActive + ) { singleFetchDfd.resolve({}); } else { try { @@ -376,56 +374,6 @@ function stripIndexParam(url: URL) { return url; } -// Determine which routes we want to load so we can add a `?_routes` search param -// for fine-grained revalidation if necessary. There's some nuance to this decision: -// -// - The presence of `shouldRevalidate` and `clientLoader` functions are the only -// way to trigger fine-grained single fetch loader calls. without either of -// these on the route matches we just always ask for the full `.data` request. -// - If any routes have a `shouldRevalidate` or `clientLoader` then we do a -// comparison of the routes we matched and the routes we're aiming to load -// - If they don't match up, then we add the `_routes` param or fine-grained -// loading -// - This is used by the single fetch implementation above and by the -// `` component so we can prefetch routes using the -// same logic -export function addRevalidationParam( - manifest: AssetsManifest, - routeModules: RouteModules, - matchedRoutes: DataRouteObject[], - loadRoutes: DataRouteObject[], - url: URL -) { - let genRouteIds = (arr: string[]) => - arr.filter((id) => manifest.routes[id].hasLoader).join(","); - - // Look at the `routeModules` for `shouldRevalidate` here instead of the manifest - // since HDR adds a wrapper for `shouldRevalidate` even if the route didn't have one - // initially. - // TODO: We probably can get rid of that wrapper once we're strictly on on - // single-fetch in v3 and just leverage a needsRevalidation data structure here - // to determine what to fetch - let needsParam = matchedRoutes.some( - (r) => - routeModules[r.id]?.shouldRevalidate || - manifest.routes[r.id]?.hasClientLoader - ); - if (!needsParam) { - return url; - } - - let matchedIds = genRouteIds(matchedRoutes.map((r) => r.id)); - let loadIds = genRouteIds( - loadRoutes - .filter((r) => !manifest.routes[r.id]?.hasClientLoader) - .map((r) => r.id) - ); - if (matchedIds !== loadIds) { - url.searchParams.set("_routes", loadIds); - } - return url; -} - export function singleFetchUrl(reqUrl: URL | string) { let url = typeof reqUrl === "string" diff --git a/packages/remix-serve/CHANGELOG.md b/packages/remix-serve/CHANGELOG.md index 0f0a2d60459..5ea3f9af956 100644 --- a/packages/remix-serve/CHANGELOG.md +++ b/packages/remix-serve/CHANGELOG.md @@ -1,5 +1,13 @@ # `@remix-run/serve` +## 2.12.0 + +### Patch Changes + +- Updated dependencies: + - `@remix-run/node@2.12.0` + - `@remix-run/express@2.12.0` + ## 2.11.2 ### Patch Changes diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 9aa07aca725..df5babd952b 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,6 +1,6 @@ { "name": "@remix-run/serve", - "version": "2.11.2", + "version": "2.12.0", "description": "Production application server for Remix", "bugs": { "url": "https://github.com/remix-run/remix/issues" diff --git a/packages/remix-server-runtime/CHANGELOG.md b/packages/remix-server-runtime/CHANGELOG.md index 7ef0650750e..8f339348193 100644 --- a/packages/remix-server-runtime/CHANGELOG.md +++ b/packages/remix-server-runtime/CHANGELOG.md @@ -1,5 +1,65 @@ # `@remix-run/server-runtime` +## 2.12.0 + +### Patch Changes + +- Single Fetch: Do not try to encode a `turbo-stream` body into 304 responses ([#9941](https://github.com/remix-run/remix/pull/9941)) +- Single Fetch: fix revalidation behavior bugs ([#9938](https://github.com/remix-run/remix/pull/9938)) + - With Single Fetch, existing routes revalidate by default + - This means requests do not need special query params for granular route revalidations out of the box - i.e., `GET /a/b/c.data` + - There are two conditions that will trigger granular revalidation: + - If a route opts out of revalidation via `shouldRevalidate`, it will be excluded from the single fetch call + - If a route defines a `clientLoader` then it will be excluded from the single fetch call and if you call `serverLoader()` from your `clientLoader`, that will make a separarte HTTP call for just that route loader - i.e., `GET /a/b/c.data?_routes=routes/a` for a `clientLoader` in `routes/a.tsx` + - When one or more routes are excluded from the single fetch call, the remaining routes that have loaders are included as query params: + - For example, if A was excluded, and the `root` route and `routes/b` had a `loader` but `routes/c` did not, the single fetch request would be `GET /a/b/c.data?_routes=root,routes/a` +- Remove hydration URL check that was originally added for React 17 hydration issues and we no longer support React 17 ([#9890](https://github.com/remix-run/remix/pull/9890)) + + - Reverts the logic originally added in Remix `v1.18.0` via + - This was added to resolve an issue that could arise when doing quick back/forward history navigations while JS was loading which would cause a mismatch between the server matches and client matches: + - This specific hydration issue would then cause this React v17 only looping issue: + - The URL comparison that we added in `1.18.0` turned out to be subject to false positives of it's own which could also put the user in looping scenarios + - Remix v2 upgraded it's minimal React version to v18 which eliminated the v17 hydration error loop + - React v18 handles this hydration error like any other error and does not result in a loop + - So we can remove our check and thus avoid the false-positive scenarios in which it may also trigger a loop + +- Single Fetch: Improved typesafety ([#9893](https://github.com/remix-run/remix/pull/9893)) + + If you were already using previously released unstable single-fetch types: + + - Remove `"@remix-run/react/future/single-fetch.d.ts"` override from `tsconfig.json` > `compilerOptions` > `types` + - Remove `defineLoader`, `defineAction`, `defineClientLoader`, `defineClientAction` helpers from your route modules + - Replace `UIMatch_SingleFetch` type helper with `UIMatch` + - Replace `MetaArgs_SingleFetch` type helper with `MetaArgs` + + Then you are ready for the new typesafety setup: + + ```ts + // vite.config.ts + + declare module "@remix-run/server-runtime" { + interface Future { + unstable_singleFetch: true; // 👈 enable _types_ for single-fetch + } + } + + export default defineConfig({ + plugins: [ + remix({ + future: { + unstable_singleFetch: true, // 👈 enable single-fetch + }, + }), + ], + }); + ``` + + For more information, see [Guides > Single Fetch](https://remix.run/docs/en/dev/guides/single-fetch) in our docs. + +- Single Fetch: Change content type on `.data` requests to `text/x-script` to allow Cloudflare compression ([#9889](https://github.com/remix-run/remix/pull/9889)) + +- Support 304 responses on document requests ([#9955](https://github.com/remix-run/remix/pull/9955)) + ## 2.11.2 ### Patch Changes diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 4cff98475ce..b7cd15393d7 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": "2.11.2", + "version": "2.12.0", "description": "Server runtime for Remix", "bugs": { "url": "https://github.com/remix-run/remix/issues" @@ -19,7 +19,7 @@ "tsc": "tsc" }, "dependencies": { - "@remix-run/router": "1.19.2-pre.0", + "@remix-run/router": "1.19.2", "@types/cookie": "^0.6.0", "@web3-storage/multipart-parser": "^1.0.0", "cookie": "^0.6.0", diff --git a/packages/remix-server-runtime/serialize.ts b/packages/remix-server-runtime/serialize.ts index f66f315469c..836e4e14ee8 100644 --- a/packages/remix-server-runtime/serialize.ts +++ b/packages/remix-server-runtime/serialize.ts @@ -23,11 +23,7 @@ type SingleFetchEnabled = * `type LoaderData = SerializeFrom` */ export type SerializeFrom = - SingleFetchEnabled extends true ? - T extends (...args: any[]) => unknown ? - SingleFetch_SerializeFrom : - never - : + SingleFetchEnabled extends true ? SingleFetch_SerializeFrom : T extends (...args: any[]) => infer Output ? Parameters extends [ClientLoaderFunctionArgs | ClientActionFunctionArgs] ? // Client data functions may not serialize diff --git a/packages/remix-server-runtime/server.ts b/packages/remix-server-runtime/server.ts index a28aff97369..14a7f680893 100644 --- a/packages/remix-server-runtime/server.ts +++ b/packages/remix-server-runtime/server.ts @@ -465,6 +465,11 @@ async function handleDocumentRequest( let headers = getDocumentHeaders(build, context); + // 304 responses should not have a body or a content-type + if (context.statusCode === 304) { + return new Response(null, { status: 304, headers }); + } + // Sanitize errors outside of development environments if (context.errors) { Object.values(context.errors).forEach((err) => { diff --git a/packages/remix-server-runtime/single-fetch.ts b/packages/remix-server-runtime/single-fetch.ts index 8e917e28593..222f74bb4e2 100644 --- a/packages/remix-server-runtime/single-fetch.ts +++ b/packages/remix-server-runtime/single-fetch.ts @@ -18,7 +18,7 @@ import type { AppLoadContext } from "./data"; import { sanitizeError, sanitizeErrors } from "./errors"; import { getDocumentHeaders } from "./headers"; import { ServerMode } from "./mode"; -import type { TypedResponse } from "./responses"; +import type { TypedDeferredData, TypedResponse } from "./responses"; import { isRedirectStatusCode, isResponse } from "./responses"; import type { Jsonify } from "./jsonify"; import type { @@ -418,21 +418,24 @@ type Serialize = undefined -type Fn = (...args: any[]) => unknown; - // Backwards-compatible type for Remix v2 where json/defer still use the old types, // and only non-json/defer returns use the new types. This allows for incremental // migration of loaders to return naked objects. In the next major version, // json/defer will be removed so everything will use the new simplified typings. // prettier-ignore -export type SerializeFrom = - Parameters extends [ClientLoaderFunctionArgs | ClientActionFunctionArgs] ? - ReturnType extends TypedResponse ? Jsonify : - Awaited> +export type SerializeFrom = + T extends (...args: infer Args) => infer Return ? + Args extends [ClientLoaderFunctionArgs | ClientActionFunctionArgs] ? + Awaited extends TypedResponse ? Jsonify : + Awaited extends TypedDeferredData ? U : + Awaited + : + Awaited extends TypedResponse ? Jsonify : + Awaited extends TypedDeferredData ? Serialize : + Awaited extends DataWithResponseInit ? Serialize : + Serialize>> : - Awaited> extends TypedResponse> ? Jsonify : - Awaited> extends DataWithResponseInit ? Serialize : - Serialize>>; + T type ServerLoader = (args: LoaderFunctionArgs) => T; type ClientLoader = (args: ClientLoaderFunctionArgs) => T; @@ -470,6 +473,8 @@ type Recursive = { recursive?: Recursive; }; +type Pretty = { [K in keyof T]: T[K] } & {}; + // prettier-ignore // eslint-disable-next-line type _tests = [ @@ -597,4 +602,10 @@ type _tests = [ function: () => void, class: TestClass }>>, + + Expect>>>, { a: string, b: string }>>, + Expect}>>>>, { a: string, b: Promise }>>, + + // non-function backcompat + Expect, {a: string, b: Date}>> ] diff --git a/packages/remix-testing/CHANGELOG.md b/packages/remix-testing/CHANGELOG.md index 13e4cc2948c..968b691be03 100644 --- a/packages/remix-testing/CHANGELOG.md +++ b/packages/remix-testing/CHANGELOG.md @@ -1,5 +1,13 @@ # `@remix-run/testing` +## 2.12.0 + +### Patch Changes + +- Updated dependencies: + - `@remix-run/react@2.12.0` + - `@remix-run/node@2.12.0` + ## 2.11.2 ### Patch Changes diff --git a/packages/remix-testing/package.json b/packages/remix-testing/package.json index 7929a470d07..0bcf7bd3e6e 100644 --- a/packages/remix-testing/package.json +++ b/packages/remix-testing/package.json @@ -1,6 +1,6 @@ { "name": "@remix-run/testing", - "version": "2.11.2", + "version": "2.12.0", "description": "Testing utilities for Remix apps", "homepage": "https://remix.run", "bugs": { @@ -21,8 +21,8 @@ "dependencies": { "@remix-run/node": "workspace:*", "@remix-run/react": "workspace:*", - "@remix-run/router": "1.19.2-pre.0", - "react-router-dom": "6.26.2-pre.0" + "@remix-run/router": "1.19.2", + "react-router-dom": "6.26.2" }, "devDependencies": { "@remix-run/server-runtime": "workspace:*", diff --git a/packages/remix/package.json b/packages/remix/package.json index 0b0a0f44a0b..7dc1af11b01 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -1,6 +1,6 @@ { "name": "remix", - "version": "2.11.2", + "version": "2.12.0", "description": "A framework for building better websites", "homepage": "https://remix.run", "bugs": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0343a5da291..c8be1d96c04 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -323,8 +323,8 @@ importers: specifier: workspace:* version: link:../packages/remix-node '@remix-run/router': - specifier: 1.19.2-pre.0 - version: 1.19.2-pre.0 + specifier: 1.19.2 + version: 1.19.2 '@remix-run/server-runtime': specifier: workspace:* version: link:../packages/remix-server-runtime @@ -548,13 +548,13 @@ importers: integration/helpers/vite-cloudflare-template: dependencies: '@remix-run/cloudflare': - specifier: 2.11.2 + specifier: 2.12.0 version: link:../../../packages/remix-cloudflare '@remix-run/cloudflare-pages': - specifier: 2.11.2 + specifier: 2.12.0 version: link:../../../packages/remix-cloudflare-pages '@remix-run/react': - specifier: 2.11.2 + specifier: 2.12.0 version: link:../../../packages/remix-react isbot: specifier: ^4.1.0 @@ -868,11 +868,11 @@ importers: specifier: workspace:* version: link:../remix-node '@remix-run/react': - specifier: ^2.11.2 + specifier: ^2.12.0 version: link:../remix-react '@remix-run/router': - specifier: 1.19.2-pre.0 - version: 1.19.2-pre.0 + specifier: 1.19.2 + version: 1.19.2 '@remix-run/server-runtime': specifier: workspace:* version: link:../remix-server-runtime @@ -1217,17 +1217,17 @@ importers: packages/remix-react: dependencies: '@remix-run/router': - specifier: 1.19.2-pre.0 - version: 1.19.2-pre.0 + specifier: 1.19.2 + version: 1.19.2 '@remix-run/server-runtime': specifier: workspace:* version: link:../remix-server-runtime react-router: - specifier: 6.26.2-pre.0 - version: 6.26.2-pre.0(react@18.2.0) + specifier: 6.26.2 + version: 6.26.2(react@18.2.0) react-router-dom: - specifier: 6.26.2-pre.0 - version: 6.26.2-pre.0(react-dom@18.2.0)(react@18.2.0) + specifier: 6.26.2 + version: 6.26.2(react-dom@18.2.0)(react@18.2.0) turbo-stream: specifier: 2.4.0 version: 2.4.0 @@ -1303,8 +1303,8 @@ importers: packages/remix-server-runtime: dependencies: '@remix-run/router': - specifier: 1.19.2-pre.0 - version: 1.19.2-pre.0 + specifier: 1.19.2 + version: 1.19.2 '@types/cookie': specifier: ^0.6.0 version: 0.6.0 @@ -1340,11 +1340,11 @@ importers: specifier: workspace:* version: link:../remix-react '@remix-run/router': - specifier: 1.19.2-pre.0 - version: 1.19.2-pre.0 + specifier: 1.19.2 + version: 1.19.2 react-router-dom: - specifier: 6.26.2-pre.0 - version: 6.26.2-pre.0(react-dom@18.2.0)(react@18.2.0) + specifier: 6.26.2 + version: 6.26.2(react-dom@18.2.0)(react@18.2.0) devDependencies: '@remix-run/server-runtime': specifier: workspace:* @@ -4206,8 +4206,8 @@ packages: - encoding dev: false - /@remix-run/router@1.19.2-pre.0: - resolution: {integrity: sha512-6hY0ygbEysBa8BWmO2uJbOjll+1kCHjJTg5qqCH2T+VVkGTjiZFd/Veruj4smF9rZP2PmZoFxSpZbWDr+xVieA==} + /@remix-run/router@1.19.2: + resolution: {integrity: sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA==} engines: {node: '>=14.0.0'} dev: false @@ -12800,26 +12800,26 @@ packages: engines: {node: '>=0.10.0'} dev: false - /react-router-dom@6.26.2-pre.0(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-oHlxmc/5SufjxHI96rT1xGII8eKRlIn8juA86YK/2OUbs4HnV+8khNjakJSzTyLx86RY/kc09lDrfpbCWspSpg==} + /react-router-dom@6.26.2(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-z7YkaEW0Dy35T3/QKPYB1LjMK2R1fxnHO8kWpUMTBdfVzZrWOiY9a7CtN8HqdWtDUWd5FY6Dl8HFsqVwH4uOtQ==} engines: {node: '>=14.0.0'} peerDependencies: react: '>=16.8' react-dom: '>=16.8' dependencies: - '@remix-run/router': 1.19.2-pre.0 + '@remix-run/router': 1.19.2 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-router: 6.26.2-pre.0(react@18.2.0) + react-router: 6.26.2(react@18.2.0) dev: false - /react-router@6.26.2-pre.0(react@18.2.0): - resolution: {integrity: sha512-nO4Kv9HNXZyaVzKxRcmgDn0tVHEs2pNN3lrTEbxYupDa/6igNHnKOe1TXlc13D9FMRUpjlpGhd/Ork/kcQ1PVw==} + /react-router@6.26.2(react@18.2.0): + resolution: {integrity: sha512-tvN1iuT03kHgOFnLPfLJ8V95eijteveqdOSk+srqfePtQvqCExB8eHOYnlilbOcyJyKnYkr1vJvf7YqotAJu1A==} engines: {node: '>=14.0.0'} peerDependencies: react: '>=16.8' dependencies: - '@remix-run/router': 1.19.2-pre.0 + '@remix-run/router': 1.19.2 react: 18.2.0 dev: false