From 50121f3e5556c18d371422262b144afebc61043e Mon Sep 17 00:00:00 2001 From: Brooks Lybrand Date: Fri, 11 Oct 2024 12:43:53 -0500 Subject: [PATCH 01/33] Bump template to pre.1 --- templates/basic/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/basic/package.json b/templates/basic/package.json index 5466439857..033bf5dbdd 100644 --- a/templates/basic/package.json +++ b/templates/basic/package.json @@ -9,15 +9,15 @@ "typecheck": "react-router typegen && tsc" }, "dependencies": { - "@react-router/node": "7.0.0-pre.0", - "@react-router/serve": "7.0.0-pre.0", + "@react-router/node": "7.0.0-pre.1", + "@react-router/serve": "7.0.0-pre.1", "isbot": "^5.1.17", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router": "7.0.0-pre.0" + "react-router": "7.0.0-pre.1" }, "devDependencies": { - "@react-router/dev": "7.0.0-pre.0", + "@react-router/dev": "7.0.0-pre.1", "@types/react": "^18.3.9", "@types/react-dom": "^18.3.0", "autoprefixer": "^10.4.20", From bb9a10ec6c54e06c0222a1744080e1a2ebccd35e Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Fri, 11 Oct 2024 13:25:24 -0400 Subject: [PATCH 02/33] Add [PRERELEASE] prefix to 7.0.0-pre.1 changesets --- .changeset/little-cooks-pull.md | 2 +- .changeset/rare-plums-chew.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/little-cooks-pull.md b/.changeset/little-cooks-pull.md index 9159ca5938..a221a5b2eb 100644 --- a/.changeset/little-cooks-pull.md +++ b/.changeset/little-cooks-pull.md @@ -2,4 +2,4 @@ "react-router": patch --- -Fix typegen for routes with a client loader but no server loader +[PRERELEASE] Fix typegen for routes with a client loader but no server loader diff --git a/.changeset/rare-plums-chew.md b/.changeset/rare-plums-chew.md index 6624883cd6..7f071c8186 100644 --- a/.changeset/rare-plums-chew.md +++ b/.changeset/rare-plums-chew.md @@ -4,5 +4,5 @@ "react-router": patch --- -- Fix `react-router-serve` handling of prerendered HTML files by removing the `redirect: false` option so it now falls back on the default `redirect: true` behavior of redirecting from `/folder` -> `/folder/` which will then pick up `/folder/index.html` from disk. See https://expressjs.com/en/resources/middleware/serve-static.html -- Proxy prerendered loader data into prerender pass for HTML files to avoid double-invocations of the loader at build time +- [PRERELEASE] Fix `react-router-serve` handling of prerendered HTML files by removing the `redirect: false` option so it now falls back on the default `redirect: true` behavior of redirecting from `/folder` -> `/folder/` which will then pick up `/folder/index.html` from disk. See https://expressjs.com/en/resources/middleware/serve-static.html +- [PRERELEASE] Proxy prerendered loader data into prerender pass for HTML files to avoid double-invocations of the loader at build time From 0061b031c3c7be6f410f3caa5bdb5b71452ce5bc Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Fri, 11 Oct 2024 14:11:00 -0400 Subject: [PATCH 03/33] Bring in 6.26.2/6.27.0 release notes --- CHANGELOG.md | 260 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 167 insertions(+), 93 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25832711c1..eb859e858c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,175 +13,182 @@ We manage release notes in this file instead of the paginated Github Releases Pa Table of Contents - [React Router Releases](#react-router-releases) - - [v6.26.1](#v6261) - - [Patch Changes](#patch-changes) - - [v6.26.0](#v6260) + - [v6.27.0](#v6270) + - [What's Changed](#whats-changed) + - [Stabilized APIs](#stabilized-apis) - [Minor Changes](#minor-changes) + - [Patch Changes](#patch-changes) + - [v6.26.2](#v6262) - [Patch Changes](#patch-changes-1) - - [v6.25.1](#v6251) + - [v6.26.1](#v6261) - [Patch Changes](#patch-changes-2) - - [v6.25.0](#v6250) - - [What's Changed](#whats-changed) - - [Stabilized `v7_skipActionErrorRevalidation`](#stabilized-v7_skipactionerrorrevalidation) + - [v6.26.0](#v6260) - [Minor Changes](#minor-changes-1) - [Patch Changes](#patch-changes-3) - - [v6.24.1](#v6241) + - [v6.25.1](#v6251) - [Patch Changes](#patch-changes-4) - - [v6.24.0](#v6240) + - [v6.25.0](#v6250) - [What's Changed](#whats-changed-1) - - [Lazy Route Discovery (a.k.a. "Fog of War")](#lazy-route-discovery-aka-fog-of-war) + - [Stabilized `v7_skipActionErrorRevalidation`](#stabilized-v7_skipactionerrorrevalidation) - [Minor Changes](#minor-changes-2) - [Patch Changes](#patch-changes-5) - - [v6.23.1](#v6231) + - [v6.24.1](#v6241) - [Patch Changes](#patch-changes-6) - - [v6.23.0](#v6230) + - [v6.24.0](#v6240) - [What's Changed](#whats-changed-2) + - [Lazy Route Discovery (a.k.a. "Fog of War")](#lazy-route-discovery-aka-fog-of-war) + - [Minor Changes](#minor-changes-3) + - [Patch Changes](#patch-changes-7) + - [v6.23.1](#v6231) + - [Patch Changes](#patch-changes-8) + - [v6.23.0](#v6230) + - [What's Changed](#whats-changed-3) - [Data Strategy (unstable)](#data-strategy-unstable) - [Skip Action Error Revalidation (unstable)](#skip-action-error-revalidation-unstable) - - [Minor Changes](#minor-changes-3) + - [Minor Changes](#minor-changes-4) - [v6.22.3](#v6223) - - [Patch Changes](#patch-changes-7) + - [Patch Changes](#patch-changes-9) - [v6.22.2](#v6222) - - [Patch Changes](#patch-changes-8) + - [Patch Changes](#patch-changes-10) - [v6.22.1](#v6221) - - [Patch Changes](#patch-changes-9) + - [Patch Changes](#patch-changes-11) - [v6.22.0](#v6220) - - [What's Changed](#whats-changed-3) + - [What's Changed](#whats-changed-4) - [Core Web Vitals Technology Report Flag](#core-web-vitals-technology-report-flag) - - [Minor Changes](#minor-changes-4) - - [Patch Changes](#patch-changes-10) + - [Minor Changes](#minor-changes-5) + - [Patch Changes](#patch-changes-12) - [v6.21.3](#v6213) - - [Patch Changes](#patch-changes-11) + - [Patch Changes](#patch-changes-13) - [v6.21.2](#v6212) - - [Patch Changes](#patch-changes-12) + - [Patch Changes](#patch-changes-14) - [v6.21.1](#v6211) - - [Patch Changes](#patch-changes-13) + - [Patch Changes](#patch-changes-15) - [v6.21.0](#v6210) - - [What's Changed](#whats-changed-4) + - [What's Changed](#whats-changed-5) - [`future.v7_relativeSplatPath`](#futurev7_relativesplatpath) - [Partial Hydration](#partial-hydration) - - [Minor Changes](#minor-changes-5) - - [Patch Changes](#patch-changes-14) - - [v6.20.1](#v6201) - - [Patch Changes](#patch-changes-15) - - [v6.20.0](#v6200) - [Minor Changes](#minor-changes-6) - [Patch Changes](#patch-changes-16) + - [v6.20.1](#v6201) + - [Patch Changes](#patch-changes-17) + - [v6.20.0](#v6200) + - [Minor Changes](#minor-changes-7) + - [Patch Changes](#patch-changes-18) - [v6.19.0](#v6190) - - [What's Changed](#whats-changed-5) + - [What's Changed](#whats-changed-6) - [`unstable_flushSync` API](#unstable_flushsync-api) - - [Minor Changes](#minor-changes-7) - - [Patch Changes](#patch-changes-17) + - [Minor Changes](#minor-changes-8) + - [Patch Changes](#patch-changes-19) - [v6.18.0](#v6180) - - [What's Changed](#whats-changed-6) + - [What's Changed](#whats-changed-7) - [New Fetcher APIs](#new-fetcher-apis) - [Persistence Future Flag (`future.v7_fetcherPersist`)](#persistence-future-flag-futurev7_fetcherpersist) - - [Minor Changes](#minor-changes-8) - - [Patch Changes](#patch-changes-18) + - [Minor Changes](#minor-changes-9) + - [Patch Changes](#patch-changes-20) - [v6.17.0](#v6170) - - [What's Changed](#whats-changed-7) + - [What's Changed](#whats-changed-8) - [View Transitions πŸš€](#view-transitions-) - - [Minor Changes](#minor-changes-9) - - [Patch Changes](#patch-changes-19) - - [v6.16.0](#v6160) - [Minor Changes](#minor-changes-10) - - [Patch Changes](#patch-changes-20) - - [v6.15.0](#v6150) - - [Minor Changes](#minor-changes-11) - [Patch Changes](#patch-changes-21) - - [v6.14.2](#v6142) + - [v6.16.0](#v6160) + - [Minor Changes](#minor-changes-11) - [Patch Changes](#patch-changes-22) - - [v6.14.1](#v6141) - - [Patch Changes](#patch-changes-23) - - [v6.14.0](#v6140) - - [What's Changed](#whats-changed-8) - - [JSON/Text Submissions](#jsontext-submissions) + - [v6.15.0](#v6150) - [Minor Changes](#minor-changes-12) + - [Patch Changes](#patch-changes-23) + - [v6.14.2](#v6142) - [Patch Changes](#patch-changes-24) - - [v6.13.0](#v6130) + - [v6.14.1](#v6141) + - [Patch Changes](#patch-changes-25) + - [v6.14.0](#v6140) - [What's Changed](#whats-changed-9) - - [`future.v7_startTransition`](#futurev7_starttransition) + - [JSON/Text Submissions](#jsontext-submissions) - [Minor Changes](#minor-changes-13) - - [Patch Changes](#patch-changes-25) - - [v6.12.1](#v6121) - [Patch Changes](#patch-changes-26) - - [v6.12.0](#v6120) + - [v6.13.0](#v6130) - [What's Changed](#whats-changed-10) - - [`React.startTransition` support](#reactstarttransition-support) + - [`future.v7_startTransition`](#futurev7_starttransition) - [Minor Changes](#minor-changes-14) - [Patch Changes](#patch-changes-27) - - [v6.11.2](#v6112) + - [v6.12.1](#v6121) - [Patch Changes](#patch-changes-28) - - [v6.11.1](#v6111) - - [Patch Changes](#patch-changes-29) - - [v6.11.0](#v6110) + - [v6.12.0](#v6120) + - [What's Changed](#whats-changed-11) + - [`React.startTransition` support](#reactstarttransition-support) - [Minor Changes](#minor-changes-15) + - [Patch Changes](#patch-changes-29) + - [v6.11.2](#v6112) - [Patch Changes](#patch-changes-30) - - [v6.10.0](#v6100) - - [What's Changed](#whats-changed-11) + - [v6.11.1](#v6111) + - [Patch Changes](#patch-changes-31) + - [v6.11.0](#v6110) - [Minor Changes](#minor-changes-16) + - [Patch Changes](#patch-changes-32) + - [v6.10.0](#v6100) + - [What's Changed](#whats-changed-12) + - [Minor Changes](#minor-changes-17) - [`future.v7_normalizeFormMethod`](#futurev7_normalizeformmethod) - - [Patch Changes](#patch-changes-31) + - [Patch Changes](#patch-changes-33) - [v6.9.0](#v690) - - [What's Changed](#whats-changed-12) + - [What's Changed](#whats-changed-13) - [`Component`/`ErrorBoundary` route properties](#componenterrorboundary-route-properties) - [Introducing Lazy Route Modules](#introducing-lazy-route-modules) - - [Minor Changes](#minor-changes-17) - - [Patch Changes](#patch-changes-32) + - [Minor Changes](#minor-changes-18) + - [Patch Changes](#patch-changes-34) - [v6.8.2](#v682) - - [Patch Changes](#patch-changes-33) + - [Patch Changes](#patch-changes-35) - [v6.8.1](#v681) - - [Patch Changes](#patch-changes-34) + - [Patch Changes](#patch-changes-36) - [v6.8.0](#v680) - - [Minor Changes](#minor-changes-18) - - [Patch Changes](#patch-changes-35) - - [v6.7.0](#v670) - [Minor Changes](#minor-changes-19) - - [Patch Changes](#patch-changes-36) - - [v6.6.2](#v662) - [Patch Changes](#patch-changes-37) - - [v6.6.1](#v661) - - [Patch Changes](#patch-changes-38) - - [v6.6.0](#v660) - - [What's Changed](#whats-changed-13) + - [v6.7.0](#v670) - [Minor Changes](#minor-changes-20) + - [Patch Changes](#patch-changes-38) + - [v6.6.2](#v662) - [Patch Changes](#patch-changes-39) - - [v6.5.0](#v650) + - [v6.6.1](#v661) + - [Patch Changes](#patch-changes-40) + - [v6.6.0](#v660) - [What's Changed](#whats-changed-14) - [Minor Changes](#minor-changes-21) - - [Patch Changes](#patch-changes-40) - - [v6.4.5](#v645) - [Patch Changes](#patch-changes-41) - - [v6.4.4](#v644) + - [v6.5.0](#v650) + - [What's Changed](#whats-changed-15) + - [Minor Changes](#minor-changes-22) - [Patch Changes](#patch-changes-42) - - [v6.4.3](#v643) + - [v6.4.5](#v645) - [Patch Changes](#patch-changes-43) - - [v6.4.2](#v642) + - [v6.4.4](#v644) - [Patch Changes](#patch-changes-44) - - [v6.4.1](#v641) + - [v6.4.3](#v643) - [Patch Changes](#patch-changes-45) - - [v6.4.0](#v640) - - [What's Changed](#whats-changed-15) - - [Remix Data APIs](#remix-data-apis) + - [v6.4.2](#v642) - [Patch Changes](#patch-changes-46) - - [v6.3.0](#v630) - - [Minor Changes](#minor-changes-22) - - [v6.2.2](#v622) + - [v6.4.1](#v641) - [Patch Changes](#patch-changes-47) - - [v6.2.1](#v621) + - [v6.4.0](#v640) + - [What's Changed](#whats-changed-16) + - [Remix Data APIs](#remix-data-apis) - [Patch Changes](#patch-changes-48) - - [v6.2.0](#v620) + - [v6.3.0](#v630) - [Minor Changes](#minor-changes-23) + - [v6.2.2](#v622) - [Patch Changes](#patch-changes-49) - - [v6.1.1](#v611) + - [v6.2.1](#v621) - [Patch Changes](#patch-changes-50) - - [v6.1.0](#v610) + - [v6.2.0](#v620) - [Minor Changes](#minor-changes-24) - [Patch Changes](#patch-changes-51) - - [v6.0.2](#v602) + - [v6.1.1](#v611) - [Patch Changes](#patch-changes-52) - - [v6.0.1](#v601) + - [v6.1.0](#v610) + - [Minor Changes](#minor-changes-25) - [Patch Changes](#patch-changes-53) + - [v6.0.2](#v602) + - [Patch Changes](#patch-changes-54) + - [v6.0.1](#v601) + - [Patch Changes](#patch-changes-55) - [v6.0.0](#v600) @@ -205,6 +212,73 @@ Date: YYYY-MM-DD **Full Changelog**: [`v6.X.Y...v6.X.Y`](https://github.com/remix-run/react-router/compare/react-router@6.X.Y...react-router@6.X.Y) --> +## v6.27.0 + +Date: 2024-10-11 + +### What's Changed + +#### Stabilized APIs + +This release stabilizes a handful of "unstable" APIs in preparation for the [pending](https://x.com/remix_run/status/1841926034868077009) React Router v7 release (see [these](https://remix.run/blog/merging-remix-and-react-router) [posts](https://remix.run/blog/incremental-path-to-react-19) for more info): + +- `unstable_dataStrategy` β†’ `dataStrategy` (`createBrowserRouter` and friends) ([Docs](https://reactrouter.com/v6/routers/create-browser-router#optsdatastrategy)) +- `unstable_patchRoutesOnNavigation` β†’ `patchRoutesOnNavigation` (`createBrowserRouter` and friends) ([Docs](https://reactrouter.com/v6/routers/create-browser-router#optspatchroutesonnavigation)) +- `unstable_flushSync` β†’ `flushSync` (`useSubmit`, `fetcher.load`, `fetcher.submit`) ([Docs](https://reactrouter.com/v6/hooks/use-submit#optionsflushsync)) +- `unstable_viewTransition` β†’ `viewTransition` (``, `
`, `useNavigate`, `useSubmit`) ([Docs](https://reactrouter.com/v6/components/link#viewtransition)) + +### Minor Changes + +- Stabilize the `unstable_flushSync` option for navigations and fetchers ([#11989](https://github.com/remix-run/react-router/pull/11989)) +- Stabilize the `unstable_viewTransition` option for navigations and the corresponding `unstable_useViewTransitionState` hook ([#11989](https://github.com/remix-run/react-router/pull/11989)) +- Stabilize `unstable_dataStrategy` ([#11974](https://github.com/remix-run/react-router/pull/11974)) +- Stabilize `unstable_patchRoutesOnNavigation` ([#11973](https://github.com/remix-run/react-router/pull/11973)) + - Add new `PatchRoutesOnNavigationFunctionArgs` type for convenience ([#11967](https://github.com/remix-run/react-router/pull/11967)) + +### Patch Changes + +- Fix bug when submitting to the current contextual route (parent route with an index child) when an `?index` param already exists from a prior submission ([#12003](https://github.com/remix-run/react-router/pull/12003)) +- Fix `useFormAction` bug - when removing `?index` param it would not keep other non-Remix `index` params ([#12003](https://github.com/remix-run/react-router/pull/12003)) +- Fix bug with fetchers not persisting `preventScrollReset` through redirects during concurrent fetches ([#11999](https://github.com/remix-run/react-router/pull/11999)) +- Avoid unnecessary `console.error` on fetcher abort due to back-to-back revalidation calls ([#12050](https://github.com/remix-run/react-router/pull/12050)) +- Fix bugs with `partialHydration` when hydrating with errors ([#12070](https://github.com/remix-run/react-router/pull/12070)) +- Remove internal cache to fix issues with interrupted `patchRoutesOnNavigation` calls ([#12055](https://github.com/remix-run/react-router/pull/12055)) + - ⚠️ This may be a breaking change if you were relying on this behavior in the `unstable_` API + - We used to cache in-progress calls to `patchRoutesOnNavigation` internally so that multiple navigations with the same start/end would only execute the function once and use the same promise + - However, this approach was at odds with `patch` short circuiting if a navigation was interrupted (and the `request.signal` aborted) since the first invocation's `patch` would no-op + - This cache also made some assumptions as to what a valid cache key might be - and is oblivious to any other application-state changes that may have occurred + - So, the cache has been removed because in _most_ cases, repeated calls to something like `import()` for async routes will already be cached automatically - and if not it's easy enough for users to implement this cache in userland +- Remove internal `discoveredRoutes` FIFO queue from `unstable_patchRoutesOnNavigation` ([#11977](https://github.com/remix-run/react-router/pull/11977)) + - ⚠️ This may be a breaking change if you were relying on this behavior in the `unstable_` API + - This was originally implemented as an optimization but it proved to be a bit too limiting + - If you need this optimization you can implement your own cache inside `patchRoutesOnNavigation` +- Fix types for `RouteObject` within `PatchRoutesOnNavigationFunction`'s `patch` method so it doesn't expect agnostic route objects passed to `patch` ([#11967](https://github.com/remix-run/react-router/pull/11967)) +- Expose errors thrown from `patchRoutesOnNavigation` directly to `useRouteError` instead of wrapping them in a 400 `ErrorResponse` instance ([#12111](https://github.com/remix-run/react-router/pull/12111)) + +**Full Changelog**: [`v6.26.2...v6.27.0`](https://github.com/remix-run/react-router/compare/react-router@6.26.2...react-router@6.27.0) + +## v6.26.2 + +Date: 2024-09-09 + +### Patch Changes + +- Update the `unstable_dataStrategy` API to allow for more advanced implementations ([#11943](https://github.com/remix-run/react-router/pull/11943)) + - ⚠️ If you have already adopted `unstable_dataStrategy`, please review carefully as this includes breaking changes to this API + - Rename `unstable_HandlerResult` to `unstable_DataStrategyResult` + - Change the return signature of `unstable_dataStrategy` from a parallel array of `unstable_DataStrategyResult[]` (parallel to `matches`) to a key/value object of `routeId => unstable_DataStrategyResult` + - This allows more advanced control over revalidation behavior because you can opt-into or out-of revalidating data that may not have been revalidated by default (via `match.shouldLoad`) + - You should now return/throw a result from your `handlerOverride` instead of returning a `DataStrategyResult` + - The return value (or thrown error) from your `handlerOverride` will be wrapped up into a `DataStrategyResult` and returned fromm `match.resolve` + - Therefore, if you are aggregating the results of `match.resolve()` into a final results object you should not need to think about the `DataStrategyResult` type + - If you are manually filling your results object from within your `handlerOverride`, then you will need to assign a `DataStrategyResult` as the value so React Router knows if it's a successful execution or an error (see examples in the documentation for details) + - Added a new `fetcherKey` parameter to `unstable_dataStrategy` to allow differentiation from navigational and fetcher calls +- Preserve opted-in view transitions through redirects ([#11925](https://github.com/remix-run/react-router/pull/11925)) +- Preserve pending view transitions through a router revalidation call ([#11917](https://github.com/remix-run/react-router/pull/11917)) +- Fix blocker usage when `blocker.proceed` is called quickly/synchronously ([#11930](https://github.com/remix-run/react-router/pull/11930)) + +**Full Changelog**: [`v6.26.1...v6.26.2`](https://github.com/remix-run/react-router/compare/react-router@6.26.1...react-router@6.26.2) + ## v6.26.1 Date: 2024-08-15 From e62c7cf57af7fb01304e9c264266ac81bd1b62e0 Mon Sep 17 00:00:00 2001 From: Thomas Karlsson Date: Sun, 13 Oct 2024 18:13:36 +0200 Subject: [PATCH 04/33] Update installation.md (#12124) --- contributors.yml | 1 + docs/start/installation.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/contributors.yml b/contributors.yml index 5ffecc6f66..f01dd70c1a 100644 --- a/contributors.yml +++ b/contributors.yml @@ -284,3 +284,4 @@ - yuleicul - zeromask1337 - zheng-chuang +- Tumas2 diff --git a/docs/start/installation.md b/docs/start/installation.md index 82c1582718..75d123db3c 100644 --- a/docs/start/installation.md +++ b/docs/start/installation.md @@ -100,7 +100,7 @@ export default function Home() { ```ts filename=app/routes.js import { index } from "@react-router/dev/routes"; -export const routes = [index("./home.tsx")]; +export const routes = [index("./home.jsx")]; ``` ```tsx filename=vite.config.js From 2c12ed532c7ddbba01296df8ae1d8b99c4769925 Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Sun, 13 Oct 2024 16:14:11 +0000 Subject: [PATCH 05/33] chore: format --- contributors.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributors.yml b/contributors.yml index f01dd70c1a..182253523a 100644 --- a/contributors.yml +++ b/contributors.yml @@ -264,6 +264,7 @@ - TooTallNate - triangularcube - trungpv1601 +- Tumas2 - turansky - tyankatsu0105 - underager @@ -284,4 +285,3 @@ - yuleicul - zeromask1337 - zheng-chuang -- Tumas2 From ac199f4372aac9aaa3da63ef9e8303acf9594444 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Mon, 14 Oct 2024 15:23:37 +1100 Subject: [PATCH 06/33] Clean paths in relative route helper snapshots (#12126) --- .../__tests__/route-config-test.ts | 58 ++++++++++++------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/packages/react-router-dev/__tests__/route-config-test.ts b/packages/react-router-dev/__tests__/route-config-test.ts index 4fcdddbb4a..001f2698cd 100644 --- a/packages/react-router-dev/__tests__/route-config-test.ts +++ b/packages/react-router-dev/__tests__/route-config-test.ts @@ -1,3 +1,6 @@ +import path from "node:path"; +import { normalizePath } from "vite"; + import { validateRouteConfig, route, @@ -7,6 +10,15 @@ import { relative, } from "../config/routes"; +const cleanPathsForSnapshot = (obj: any): any => + JSON.parse( + JSON.stringify(obj, (_key, value) => + typeof value === "string" && path.isAbsolute(value) + ? normalizePath(value.replace(process.cwd(), "{{CWD}}")) + : value + ) + ); + describe("route config", () => { describe("validateRouteConfig", () => { it("validates a route config", () => { @@ -460,40 +472,43 @@ describe("route config", () => { describe("relative", () => { it("supports relative routes", () => { - let { route } = relative("/path/to/dirname"); + let { route } = relative(path.join(process.cwd(), "/path/to/dirname")); expect( - route("parent", "nested/parent.tsx", [ - route("child", "nested/child.tsx", { id: "child" }), - ]) + cleanPathsForSnapshot( + route("parent", "nested/parent.tsx", [ + route("child", "nested/child.tsx", { id: "child" }), + ]) + ) ).toMatchInlineSnapshot(` { "children": [ { - "children": undefined, - "file": "/path/to/dirname/nested/child.tsx", + "file": "{{CWD}}/path/to/dirname/nested/child.tsx", "id": "child", "path": "child", }, ], - "file": "/path/to/dirname/nested/parent.tsx", + "file": "{{CWD}}/path/to/dirname/nested/parent.tsx", "path": "parent", } `); }); it("supports relative index routes", () => { - let { index } = relative("/path/to/dirname"); - expect([ - index("nested/without-options.tsx"), - index("nested/with-options.tsx", { id: "with-options" }), - ]).toMatchInlineSnapshot(` + let { index } = relative(path.join(process.cwd(), "/path/to/dirname")); + expect( + cleanPathsForSnapshot([ + index("nested/without-options.tsx"), + index("nested/with-options.tsx", { id: "with-options" }), + ]) + ).toMatchInlineSnapshot(` [ { - "file": "/path/to/dirname/nested/without-options.tsx", + "file": "{{CWD}}/path/to/dirname/nested/without-options.tsx", "index": true, }, { - "file": "/path/to/dirname/nested/with-options.tsx", + "file": "{{CWD}}/path/to/dirname/nested/with-options.tsx", "id": "with-options", "index": true, }, @@ -502,21 +517,22 @@ describe("route config", () => { }); it("supports relative layout routes", () => { - let { layout } = relative("/path/to/dirname"); + let { layout } = relative(path.join(process.cwd(), "/path/to/dirname")); expect( - layout("nested/parent.tsx", [ - layout("nested/child.tsx", { id: "child" }), - ]) + cleanPathsForSnapshot( + layout("nested/parent.tsx", [ + layout("nested/child.tsx", { id: "child" }), + ]) + ) ).toMatchInlineSnapshot(` { "children": [ { - "children": undefined, - "file": "/path/to/dirname/nested/child.tsx", + "file": "{{CWD}}/path/to/dirname/nested/child.tsx", "id": "child", }, ], - "file": "/path/to/dirname/nested/parent.tsx", + "file": "{{CWD}}/path/to/dirname/nested/parent.tsx", } `); }); From e0361ff737159bde2f240f0b2dd7da2cc987318c Mon Sep 17 00:00:00 2001 From: Brooks Lybrand Date: Tue, 15 Oct 2024 10:49:12 -0500 Subject: [PATCH 07/33] Revert template logos --- templates/basic/public/logo-dark.svg | 40 +++++++++++---------------- templates/basic/public/logo-light.svg | 40 +++++++++++---------------- 2 files changed, 32 insertions(+), 48 deletions(-) diff --git a/templates/basic/public/logo-dark.svg b/templates/basic/public/logo-dark.svg index 41c7e4f85c..88ac8b77d8 100644 --- a/templates/basic/public/logo-dark.svg +++ b/templates/basic/public/logo-dark.svg @@ -1,25 +1,17 @@ - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + diff --git a/templates/basic/public/logo-light.svg b/templates/basic/public/logo-light.svg index 4e74342e6a..9f46b557bc 100644 --- a/templates/basic/public/logo-light.svg +++ b/templates/basic/public/logo-light.svg @@ -1,25 +1,17 @@ - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + From 13df3cec9068195e5f67d86dfc77c68e0367e7d8 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 15 Oct 2024 14:44:23 -0400 Subject: [PATCH 08/33] Add note on installing via @7 --- docs/upgrading/v6.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/upgrading/v6.md b/docs/upgrading/v6.md index 55d290f7e6..4a2290f3f6 100644 --- a/docs/upgrading/v6.md +++ b/docs/upgrading/v6.md @@ -279,6 +279,8 @@ npm install react-router-dom@7 Your app should continue to work but you'll get console warnings for importing from "react-router-dom". In v7 you can import everything directly from `"react-router"`. +_Note: If you have issues with the above command, you may need to use the full `7.0.0-pre.N` version number because package managers may not always resolve `@7` to a prerelease since there is no stable 7.x release yet._ + πŸ‘‰ **Uninstall react-router-dom, install react-router** ```shellscript nonumber From bdbb16984a54bea6c9dbd7ede05937e623155fab Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 16 Oct 2024 09:13:45 -0400 Subject: [PATCH 09/33] Update error messages to reference React Router instead of Remix --- packages/react-router-dev/invariant.ts | 2 +- packages/react-router/lib/server-runtime/invariant.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-router-dev/invariant.ts b/packages/react-router-dev/invariant.ts index 690ae3ffe4..4be3353dd2 100644 --- a/packages/react-router-dev/invariant.ts +++ b/packages/react-router-dev/invariant.ts @@ -11,7 +11,7 @@ export default function invariant( export default function invariant(value: any, message?: string) { if (value === false || value === null || typeof value === "undefined") { console.error( - "The following error is a bug in Remix; please open an issue! https://github.com/remix-run/remix/issues/new" + "The following error is a bug in React Router; please open an issue! https://github.com/remix-run/react-router/issues/new/choose" ); throw new Error(message); } diff --git a/packages/react-router/lib/server-runtime/invariant.ts b/packages/react-router/lib/server-runtime/invariant.ts index 123cc25cb4..e71b43a270 100644 --- a/packages/react-router/lib/server-runtime/invariant.ts +++ b/packages/react-router/lib/server-runtime/invariant.ts @@ -9,7 +9,7 @@ export default function invariant( export default function invariant(value: any, message?: string) { if (value === false || value === null || typeof value === "undefined") { console.error( - "The following error is a bug in Remix; please open an issue! https://github.com/remix-run/remix/issues/new" + "The following error is a bug in React Router; please open an issue! https://github.com/remix-run/react-router/issues/new/choose" ); throw new Error(message); } From f74bb20abcd1f264fc885838aebc4770ebada2b6 Mon Sep 17 00:00:00 2001 From: Alex Anderson Date: Wed, 16 Oct 2024 07:22:59 -0600 Subject: [PATCH 10/33] docs(misc/resource-routes): add new page (#12119) --- docs/misc/file-route-conventions.md | 5 +- docs/misc/resource-routes.md | 198 ++++++++++++++++++++++++++++ 2 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 docs/misc/resource-routes.md diff --git a/docs/misc/file-route-conventions.md b/docs/misc/file-route-conventions.md index e32a7f0aaf..d37775d427 100644 --- a/docs/misc/file-route-conventions.md +++ b/docs/misc/file-route-conventions.md @@ -281,7 +281,7 @@ export async function serverLoader({ params }) { ## Escaping Special Characters -If you want one of the special characters used for these route conventions to actually be a part of the URL, you can escape the conventions with `[]` characters. +If you want one of the special characters used for these route conventions to actually be a part of the URL, you can escape the conventions with `[]` characters. This can be especially helpful for [resource routes][resource_routes] that include an extension in the URL. | Filename | URL | | ----------------------------------- | ------------------- | @@ -290,6 +290,8 @@ If you want one of the special characters used for these route conventions to ac | `app/routes/weird-url.[_index].tsx` | `/weird-url/_index` | | `app/routes/dolla-bills-[$].tsx` | `/dolla-bills-$` | | `app/routes/[[so-weird]].tsx` | `/[so-weird]` | +| `app/routes/reports.$id[.pdf].ts | `/reports/123.pdf | +| `app/routes/reports.$id[.].ts | `/reports/123.pdf | ## Folders for Organization @@ -371,3 +373,4 @@ app/routes/app._index/route.tsx [nested_routes]: #nested-routes [dot_delimiters]: #dot-delimiters [dynamic_segments]: #dynamic-segments +[resource_routes]: ../misc/resource-routes \ No newline at end of file diff --git a/docs/misc/resource-routes.md b/docs/misc/resource-routes.md new file mode 100644 index 0000000000..b70c129f28 --- /dev/null +++ b/docs/misc/resource-routes.md @@ -0,0 +1,198 @@ +--- +title: Resource Routes +--- + +This feature is available when using the [React Router Vite Plugin][vite-plugin] + +# Resource Routes + +Resource Routes are not part of your application UI, but are still part of your application. They can send any kind of Response. + +Most routes in React Router are UI Routes, or routes that actually render a component. But routes don't always have to render components. There are a handful of cases where you want to use route as a general-purpose endpoint to your website. Here are a few examples: + +- JSON API for a mobile app that reuses server-side code with the React Router UI +- Dynamically generating PDFs +- Dynamically generating social images for blog posts or other pages +- Webhooks for other services like Stripe or GitHub +- a CSS file that dynamically renders custom properties for a user's preferred theme + +## Creating Resource Routes + +If a route doesn't export a default component, it can be used as a Resource Route. If called with `GET`, the loader's response is returned and none of the parent route loaders are called either (because those are needed for the UI, but this is not the UI). If called with `POST`, the action's response is returned. + +For example, consider a UI Route that renders a report, note the link: + +```tsx filename=app/routes/reports.$id.tsx lines=[12-14] +export async function loader({ + params, +}: LoaderFunctionArgs) { + return json(await getReport(params.id)); +} + +export default function Report() { + const report = useLoaderData(); + return ( +
+

{report.name}

+ + View as PDF + + {/* ... */} +
+ ); +} +``` + +It's linking to a PDF version of the page. To make this work we can create a Resource Route below it. Notice that it has no default export component: that makes it a Resource Route. + +```tsx filename=app/routes/reports.$id[.pdf].tsx +export async function loader({ + params, +}: LoaderFunctionArgs) { + const report = await getReport(params.id); + const pdf = await generateReportPDF(report); + return new Response(pdf, { + status: 200, + headers: { + "Content-Type": "application/pdf", + }, + }); +} +``` + +Note the filename uses [escaping patterns][escaping] to include the extension in the URL. + +When the user clicks the link from the UI route, they will navigate to the PDF. + +## Linking to Resource Routes + +It’s imperative that you use reloadDocument on any Links to Resource Routes + +There's a subtle detail to be aware of when linking to resource routes. You need to link to it with `` or a plain ``. If you link to it with a normal `` without `reloadDocument`, then the resource route will be treated as a UI route. React Router will try to get the data with `fetch` and render the component. Don't sweat it too much, you'll get a helpful error message if you make this mistake. + +## Handling different request methods + +To handle `GET` requests export a loader function: + +```tsx +import type * as Route from "./+types.resource"; + +export const loader = async ({ + request, +}: Route.LoaderArgs) => { + // handle "GET" request + + return {success: true} +}; +``` + +To handle `POST`, `PUT`, `PATCH` or `DELETE` requests export an action function: + +```tsx +import type * as Route from "./+types.resource"; + +export const action = async ({ + request, +}: Route.ActionArgs) => { + switch (request.method) { + case "POST": { + /* handle "POST" */ + } + case "PUT": { + /* handle "PUT" */ + } + case "PATCH": { + /* handle "PATCH" */ + } + case "DELETE": { + /* handle "DELETE" */ + } + } +}; +``` + +Resource Routes do not support [non-standard HTTP methods][nonstandard-http-methods] - those should be handled by the HTTP server that serves your React Router requests. + +## Turbo Stream + +Returning bare objects or using the [`data` util][data-util] from a Resource Route will automatically encode the responses as a [turbo-stream][turbo-stream], which automatically serializes Dates, Promises, and other objects over the network. React Router automatically deserializes turbo-stream data when you call resource routes from React Router APIs such as ``, `useSubmit`, or `useFetcher`. + +Third-party services, like webhooks, that call your resource routes likely can't decode turbo-stream data. You should use Response instances to respond with any other kind of data using `Response.json` or `new Response()` with the `Content-Type` header. + +```ts +export const action = async () => { + return Response.json({time: Date.now()}, { + status: 200, + }); +} +``` + +turbo-stream is an implementation detail of Resource Routes, and you shouldn't rely on it outside of the React Router APIs. If you want to manually decode turbo-stream responses outside of React Router, such as from a `fetch` call in a mobile app, it is best to manually encode the data using the `turbo-stream` package directly, return that using `new Response`, and have the client decode the response using `turbo-stream`. + + +## Client Loaders & Client Actions + +When calling Resource Routes using ``, `useSubmit`, or Fetchers, Client Loaders and Actions defined in the Resource Route will participate in the request lifecycle. + +```ts +import type * as Route from "./+types.github"; + +export const action = async () => { + return Response.json({time: Date.now()}, { + status: 200, + }); +} + +export const clientAction = async ({serverAction}:Route.ClientActionArgs) => { + return Promise.race([ + serverAction, + new Promise((resolve, reject) => setTimeout(reject, 5000)) + ]) +} +``` + + +## Webhooks + +Resource routes can be used to handle webhooks. For example, you can create a webhook that receives notifications from GitHub when a new commit is pushed to a repository: + +```tsx +import type * as Route from "./+types.github"; + +import crypto from "node:crypto"; + +export const action = async ({ + request, +}: Route.ActionArgs) => { + if (request.method !== "POST") { + return Response.json({ message: "Method not allowed" }, { + status: 405, + }); + } + const payload = await request.json(); + + /* Validate the webhook */ + const signature = request.headers.get( + "X-Hub-Signature-256" + ); + const generatedSignature = `sha256=${crypto + .createHmac("sha256", process.env.GITHUB_WEBHOOK_SECRET) + .update(JSON.stringify(payload)) + .digest("hex")}`; + if (signature !== generatedSignature) { + return Response.json({ message: "Signature mismatch" }, { + status: 401, + }); + } + + /* process the webhook (e.g. enqueue a background job) */ + + return Response.json({ success: true }); +}; +``` + +[vite-plugin]: ../start/rendering +[turbo-stream]: https://github.com/jacob-ebey/turbo-stream +[data-util]: ../../api/react-router/data +[nonstandard-http-methods]: https://github.com/remix-run/react-router/issues/11959 +[escaping]: ../misc/file-route-conventions#escaping-special-characters From 88ab824d824983b9143f85f14eeb32e1de0aa242 Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Wed, 16 Oct 2024 13:23:35 +0000 Subject: [PATCH 11/33] chore: format --- docs/misc/file-route-conventions.md | 2 +- docs/misc/resource-routes.md | 58 ++++++++++++++++++----------- 2 files changed, 37 insertions(+), 23 deletions(-) diff --git a/docs/misc/file-route-conventions.md b/docs/misc/file-route-conventions.md index d37775d427..7c2313fd58 100644 --- a/docs/misc/file-route-conventions.md +++ b/docs/misc/file-route-conventions.md @@ -373,4 +373,4 @@ app/routes/app._index/route.tsx [nested_routes]: #nested-routes [dot_delimiters]: #dot-delimiters [dynamic_segments]: #dynamic-segments -[resource_routes]: ../misc/resource-routes \ No newline at end of file +[resource_routes]: ../misc/resource-routes diff --git a/docs/misc/resource-routes.md b/docs/misc/resource-routes.md index b70c129f28..8dd66e8ec9 100644 --- a/docs/misc/resource-routes.md +++ b/docs/misc/resource-routes.md @@ -82,7 +82,7 @@ export const loader = async ({ }: Route.LoaderArgs) => { // handle "GET" request - return {success: true} + return { success: true }; }; ``` @@ -115,21 +115,23 @@ Resource Routes do not support [non-standard HTTP methods][nonstandard-http-meth ## Turbo Stream -Returning bare objects or using the [`data` util][data-util] from a Resource Route will automatically encode the responses as a [turbo-stream][turbo-stream], which automatically serializes Dates, Promises, and other objects over the network. React Router automatically deserializes turbo-stream data when you call resource routes from React Router APIs such as ``, `useSubmit`, or `useFetcher`. +Returning bare objects or using the [`data` util][data-util] from a Resource Route will automatically encode the responses as a [turbo-stream][turbo-stream], which automatically serializes Dates, Promises, and other objects over the network. React Router automatically deserializes turbo-stream data when you call resource routes from React Router APIs such as ``, `useSubmit`, or `useFetcher`. Third-party services, like webhooks, that call your resource routes likely can't decode turbo-stream data. You should use Response instances to respond with any other kind of data using `Response.json` or `new Response()` with the `Content-Type` header. ```ts export const action = async () => { - return Response.json({time: Date.now()}, { - status: 200, - }); -} + return Response.json( + { time: Date.now() }, + { + status: 200, + } + ); +}; ``` turbo-stream is an implementation detail of Resource Routes, and you shouldn't rely on it outside of the React Router APIs. If you want to manually decode turbo-stream responses outside of React Router, such as from a `fetch` call in a mobile app, it is best to manually encode the data using the `turbo-stream` package directly, return that using `new Response`, and have the client decode the response using `turbo-stream`. - ## Client Loaders & Client Actions When calling Resource Routes using ``, `useSubmit`, or Fetchers, Client Loaders and Actions defined in the Resource Route will participate in the request lifecycle. @@ -138,20 +140,26 @@ When calling Resource Routes using ``, `useSubmit`, or Fetchers, Client Lo import type * as Route from "./+types.github"; export const action = async () => { - return Response.json({time: Date.now()}, { - status: 200, - }); -} + return Response.json( + { time: Date.now() }, + { + status: 200, + } + ); +}; -export const clientAction = async ({serverAction}:Route.ClientActionArgs) => { +export const clientAction = async ({ + serverAction, +}: Route.ClientActionArgs) => { return Promise.race([ serverAction, - new Promise((resolve, reject) => setTimeout(reject, 5000)) - ]) -} + new Promise((resolve, reject) => + setTimeout(reject, 5000) + ), + ]); +}; ``` - ## Webhooks Resource routes can be used to handle webhooks. For example, you can create a webhook that receives notifications from GitHub when a new commit is pushed to a repository: @@ -165,9 +173,12 @@ export const action = async ({ request, }: Route.ActionArgs) => { if (request.method !== "POST") { - return Response.json({ message: "Method not allowed" }, { - status: 405, - }); + return Response.json( + { message: "Method not allowed" }, + { + status: 405, + } + ); } const payload = await request.json(); @@ -180,9 +191,12 @@ export const action = async ({ .update(JSON.stringify(payload)) .digest("hex")}`; if (signature !== generatedSignature) { - return Response.json({ message: "Signature mismatch" }, { - status: 401, - }); + return Response.json( + { message: "Signature mismatch" }, + { + status: 401, + } + ); } /* process the webhook (e.g. enqueue a background job) */ From d2d88f34487ed2e82770dcd03bc3adb7ce50bf89 Mon Sep 17 00:00:00 2001 From: Brooks Lybrand Date: Wed, 16 Oct 2024 13:02:59 -0500 Subject: [PATCH 12/33] Update React Router logo --- templates/basic/app/routes/home.tsx | 2 +- templates/basic/public/logo-dark.svg | 38 ++++++++++++++++----------- templates/basic/public/logo-light.svg | 38 ++++++++++++++++----------- 3 files changed, 45 insertions(+), 33 deletions(-) diff --git a/templates/basic/app/routes/home.tsx b/templates/basic/app/routes/home.tsx index 2fdc30dd86..7bf87bc3fa 100644 --- a/templates/basic/app/routes/home.tsx +++ b/templates/basic/app/routes/home.tsx @@ -15,7 +15,7 @@ export default function Index() {

Welcome to React Router

-
+
React Router - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/basic/public/logo-light.svg b/templates/basic/public/logo-light.svg index 9f46b557bc..73284929d3 100644 --- a/templates/basic/public/logo-light.svg +++ b/templates/basic/public/logo-light.svg @@ -1,17 +1,23 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + From 9317983012b5a62bbbca9dbce4c4478b3a3797b0 Mon Sep 17 00:00:00 2001 From: Brooks Lybrand Date: Wed, 16 Oct 2024 13:52:05 -0500 Subject: [PATCH 13/33] docs: remove incorrect data props --- docs/start/actions.md | 6 +++--- docs/start/data-loading.md | 10 ++++------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/docs/start/actions.md b/docs/start/actions.md index 7344d4491f..3b42de83c9 100644 --- a/docs/start/actions.md +++ b/docs/start/actions.md @@ -28,7 +28,7 @@ export async function clientAction({ } export default function Project({ - clientActionData, + actionData, }: Route.ComponentProps) { return (
@@ -37,8 +37,8 @@ export default function Project({ - {clientActionData ? ( -

{clientActionData.title} updated

+ {actionData ? ( +

{actionData.title} updated

) : null}
); diff --git a/docs/start/data-loading.md b/docs/start/data-loading.md index 92daa9465f..efe194894c 100644 --- a/docs/start/data-loading.md +++ b/docs/start/data-loading.md @@ -24,9 +24,9 @@ export async function clientLoader({ } export default function Product({ - clientLoaderData, + loaderData, }: Route.ComponentProps) { - const { name, description } = clientLoaderData; + const { name, description } = loaderData; return (

{name}

@@ -115,7 +115,7 @@ Note that when server rendering, any URLs that aren't pre-rendered will be serve ## Using Both Loaders -`loader` and `clientLoader` can be used together. The `loader` will be used on the server for initial SSR (or pre-rendering) and the `clientLoader` will be used on subsequent clientside navigations. +`loader` and `clientLoader` can be used together. The `loader` will be used on the server for initial SSR (or pre-rendering) and the `clientLoader` will be used on subsequent client-side navigations. ```tsx filename=app/product.tsx // route("products/:pid", "./product.tsx"); @@ -135,10 +135,8 @@ export async function clientLoader({ export default function Product({ loaderData, - clientLoaderData, }: Route.ComponentProps) { - const { name, description } = - clientLoaderData || loaderData; + const { name, description } = loaderData; return (
From cd473964a47806d37ed856ff4704492772f907a3 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 16 Oct 2024 16:35:40 -0400 Subject: [PATCH 14/33] Export proper dom versions (#12132) --- docs/upgrading/v6.md | 19 ++++++++++++++- .../react-router-architect/rollup.config.js | 7 +++--- .../react-router-cloudflare/rollup.config.js | 5 ++-- packages/react-router-dom/index.ts | 7 ++++++ packages/react-router-dom/tsconfig.json | 4 ++-- .../react-router-express/rollup.config.js | 1 + .../react-router-fs-routes/rollup.config.js | 3 +++ packages/react-router-node/package.json | 6 ++--- packages/react-router-node/rollup.config.js | 24 ++----------------- packages/react-router-node/tsconfig.json | 5 ++-- .../rollup.config.js | 3 +++ packages/react-router-serve/rollup.config.js | 1 + packages/react-router/rollup.config.js | 3 +++ packages/react-router/tsconfig.json | 4 ++-- rollup.config.js | 10 ++++---- rollup.utils.js | 9 ------- 16 files changed, 57 insertions(+), 54 deletions(-) diff --git a/docs/upgrading/v6.md b/docs/upgrading/v6.md index 4a2290f3f6..a1e4c2930f 100644 --- a/docs/upgrading/v6.md +++ b/docs/upgrading/v6.md @@ -277,12 +277,14 @@ Now that your app is caught up, you can simply update to v7 (theoretically!) wit npm install react-router-dom@7 ``` -Your app should continue to work but you'll get console warnings for importing from "react-router-dom". In v7 you can import everything directly from `"react-router"`. +Your app should continue to work but we've restructured in v7 so that you can import directly from `"react-router"` β€” we'll do that in the next step. _Note: If you have issues with the above command, you may need to use the full `7.0.0-pre.N` version number because package managers may not always resolve `@7` to a prerelease since there is no stable 7.x release yet._ πŸ‘‰ **Uninstall react-router-dom, install react-router** +In v7 we've also combined the `react-router` and `react-router-dom` packages and you can import everything directly from `"react-router"` (with one exception - see below): + ```shellscript nonumber npm uninstall react-router-dom npm install react-router @@ -290,6 +292,19 @@ npm install react-router πŸ‘‰ **Update imports** +Now you can update you imports to come from `react-router`: + +```diff +-import { useLocation } from "react-router-dom"; ++import { useLocation } from "react-router"; +``` + +The one exception to this rule is for exports that specifically require `react-dom` β€” namely `RouterProvider` and `HydratedRouter` which use [`ReactDOM.flushSync`][react-flushsync] internally. These need to come from a separate `package.json` export to avoid peer dependency issues in non-browser apps that don't install `react-dom`. If you're writing a browser-based app, you will want to those from `react-router/dom`: + +```js +import { RouterProvider } from "react-router/dom"; +``` + Instead of manually updating imports, you can use this command. Make sure your git working tree is clean though so you can revert if it doesn't work as expected. ```shellscript nonumber @@ -297,3 +312,5 @@ find ./path/to/src \( -name "*.tsx" -o -name "*.ts" -o -name "*.js" -o -name "*. ``` Congratulations, you're now on v7! + +[react-flushsync]: https://react.dev/reference/react-dom/flushSync diff --git a/packages/react-router-architect/rollup.config.js b/packages/react-router-architect/rollup.config.js index 4e48ff954e..666442b069 100644 --- a/packages/react-router-architect/rollup.config.js +++ b/packages/react-router-architect/rollup.config.js @@ -10,6 +10,7 @@ const { getBuildDirectories, createBanner, WATCH, + remixBabelConfig, } = require("../../rollup.utils"); const { name: packageName, version } = require("./package.json"); @@ -23,9 +24,7 @@ module.exports = function rollup() { return [ { - external(id) { - return isBareModuleId(id); - }, + external: (id) => isBareModuleId(id), input: `${SOURCE_DIR}/index.ts`, output: { banner: createBanner(packageName, version), @@ -39,11 +38,13 @@ module.exports = function rollup() { babelHelpers: "bundled", exclude: /node_modules/, extensions: [".ts"], + ...remixBabelConfig, }), typescript({ tsconfig: path.join(__dirname, "tsconfig.json"), exclude: ["__tests__"], noEmitOnError: !WATCH, + noForceEmit: true, }), nodeResolve({ extensions: [".ts"] }), copy({ diff --git a/packages/react-router-cloudflare/rollup.config.js b/packages/react-router-cloudflare/rollup.config.js index 82dad3a416..578f3ad892 100644 --- a/packages/react-router-cloudflare/rollup.config.js +++ b/packages/react-router-cloudflare/rollup.config.js @@ -24,9 +24,7 @@ module.exports = function rollup() { return [ { - external(id) { - return isBareModuleId(id); - }, + external: (id) => isBareModuleId(id), input: `${SOURCE_DIR}/index.ts`, output: { banner: createBanner(packageName, version), @@ -45,6 +43,7 @@ module.exports = function rollup() { typescript({ tsconfig: path.join(__dirname, "tsconfig.json"), noEmitOnError: !WATCH, + noForceEmit: true, }), nodeResolve({ extensions: [".ts"] }), copy({ diff --git a/packages/react-router-dom/index.ts b/packages/react-router-dom/index.ts index c4934c9ec5..957fe6bbd0 100644 --- a/packages/react-router-dom/index.ts +++ b/packages/react-router-dom/index.ts @@ -1,2 +1,9 @@ +import type { RouterProviderProps } from "react-router/dom"; +import { HydratedRouter, RouterProvider } from "react-router/dom"; + +// TODO: Confirm if this causes tree-shaking issues and if so, convert to named exports export type * from "react-router"; export * from "react-router"; + +export type { RouterProviderProps }; +export { HydratedRouter, RouterProvider }; diff --git a/packages/react-router-dom/tsconfig.json b/packages/react-router-dom/tsconfig.json index 4c00326035..45b8ec667f 100644 --- a/packages/react-router-dom/tsconfig.json +++ b/packages/react-router-dom/tsconfig.json @@ -3,8 +3,8 @@ "compilerOptions": { "lib": ["ES2020", "DOM", "DOM.Iterable"], "target": "ES2020", - "module": "ES2020", - "moduleResolution": "node", + "module": "Node16", + "moduleResolution": "Node16", "strict": true, "jsx": "react", diff --git a/packages/react-router-express/rollup.config.js b/packages/react-router-express/rollup.config.js index 156a6620e3..9508014d31 100644 --- a/packages/react-router-express/rollup.config.js +++ b/packages/react-router-express/rollup.config.js @@ -43,6 +43,7 @@ module.exports = function rollup() { tsconfig: path.join(__dirname, "tsconfig.json"), exclude: ["__tests__"], noEmitOnError: !WATCH, + noForceEmit: true, }), nodeResolve({ extensions: [".ts", ".tsx"] }), copy({ diff --git a/packages/react-router-fs-routes/rollup.config.js b/packages/react-router-fs-routes/rollup.config.js index b663bec580..ac59595db7 100644 --- a/packages/react-router-fs-routes/rollup.config.js +++ b/packages/react-router-fs-routes/rollup.config.js @@ -9,6 +9,7 @@ const { createBanner, getBuildDirectories, WATCH, + remixBabelConfig, } = require("../../rollup.utils"); const { name, version } = require("./package.json"); @@ -36,11 +37,13 @@ module.exports = function rollup() { babelHelpers: "bundled", exclude: /node_modules/, extensions: [".ts"], + ...remixBabelConfig, }), typescript({ tsconfig: path.join(__dirname, "tsconfig.json"), exclude: ["__tests__"], noEmitOnError: !WATCH, + noForceEmit: true, }), nodeResolve({ extensions: [".ts"] }), copy({ diff --git a/packages/react-router-node/package.json b/packages/react-router-node/package.json index c6d5ee06aa..e4640d532a 100644 --- a/packages/react-router-node/package.json +++ b/packages/react-router-node/package.json @@ -16,13 +16,11 @@ "exports": { ".": { "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "require": "./dist/index.js" + "default": "./dist/index.js" }, "./install": { "types": "./dist/install.d.ts", - "import": "./dist/install.mjs", - "require": "./dist/install.js" + "default": "./dist/install.js" }, "./package.json": "./package.json" }, diff --git a/packages/react-router-node/rollup.config.js b/packages/react-router-node/rollup.config.js index 7fa53e3690..49266fca02 100644 --- a/packages/react-router-node/rollup.config.js +++ b/packages/react-router-node/rollup.config.js @@ -30,8 +30,7 @@ module.exports = function rollup() { output: { banner: createBanner(name, version), dir: OUTPUT_DIR, - entryFileNames: "[name].mjs", - format: "esm", + format: "cjs", preserveModules: true, exports: "named", }, @@ -46,6 +45,7 @@ module.exports = function rollup() { tsconfig: path.join(__dirname, "tsconfig.json"), exclude: ["__tests__"], noEmitOnError: !WATCH, + noForceEmit: true, }), nodeResolve({ extensions: [".ts", ".tsx"] }), copy({ @@ -53,25 +53,5 @@ module.exports = function rollup() { }), ], }, - { - input, - external: (id) => isBareModuleId(id), - output: { - banner: createBanner(name, version), - dir: OUTPUT_DIR, - format: "cjs", - preserveModules: true, - exports: "named", - }, - plugins: [ - babel({ - babelHelpers: "bundled", - exclude: /node_modules/, - extensions: [".ts", ".tsx"], - ...remixBabelConfig, - }), - nodeResolve({ extensions: [".ts", ".tsx"] }), - ], - }, ]; }; diff --git a/packages/react-router-node/tsconfig.json b/packages/react-router-node/tsconfig.json index 91b81681b8..2ee2e1f13d 100644 --- a/packages/react-router-node/tsconfig.json +++ b/packages/react-router-node/tsconfig.json @@ -4,9 +4,8 @@ "compilerOptions": { "lib": ["DOM", "DOM.Iterable", "ES2022"], "target": "ES2022", - "module": "ES2022", - - "moduleResolution": "Bundler", + "module": "Node16", + "moduleResolution": "Node16", "allowSyntheticDefaultImports": true, "strict": true, "jsx": "react", diff --git a/packages/react-router-remix-config-routes-adapter/rollup.config.js b/packages/react-router-remix-config-routes-adapter/rollup.config.js index a263370d4c..0afe65b2b4 100644 --- a/packages/react-router-remix-config-routes-adapter/rollup.config.js +++ b/packages/react-router-remix-config-routes-adapter/rollup.config.js @@ -9,6 +9,7 @@ const { createBanner, getBuildDirectories, WATCH, + remixBabelConfig, } = require("../../rollup.utils"); const { name, version } = require("./package.json"); @@ -36,11 +37,13 @@ module.exports = function rollup() { babelHelpers: "bundled", exclude: /node_modules/, extensions: [".ts"], + ...remixBabelConfig, }), typescript({ tsconfig: path.join(__dirname, "tsconfig.json"), exclude: ["__tests__"], noEmitOnError: !WATCH, + noForceEmit: true, }), nodeResolve({ extensions: [".ts"] }), copy({ diff --git a/packages/react-router-serve/rollup.config.js b/packages/react-router-serve/rollup.config.js index 9f7cc10161..122390bd26 100644 --- a/packages/react-router-serve/rollup.config.js +++ b/packages/react-router-serve/rollup.config.js @@ -42,6 +42,7 @@ module.exports = function rollup() { tsconfig: path.join(__dirname, "tsconfig.json"), exclude: ["__tests__"], noEmitOnError: !WATCH, + noForceEmit: true, }), nodeResolve({ extensions: [".ts"] }), copy({ diff --git a/packages/react-router/rollup.config.js b/packages/react-router/rollup.config.js index 08b2c1ad0b..cb776f2983 100644 --- a/packages/react-router/rollup.config.js +++ b/packages/react-router/rollup.config.js @@ -53,6 +53,7 @@ module.exports = function rollup() { tsconfig: path.join(__dirname, "tsconfig.json"), exclude: ["__tests__"], noEmitOnError: !WATCH, + noForceEmit: true, }), copy({ targets: [{ src: "LICENSE.md", dest: SOURCE_DIR }], @@ -90,6 +91,7 @@ module.exports = function rollup() { // eslint-disable-next-line no-restricted-globals tsconfig: path.join(__dirname, "tsconfig.dom.json"), noEmitOnError: !WATCH, + noForceEmit: true, }), ].concat(PRETTY ? prettier({ parser: "babel" }) : []), }, @@ -121,6 +123,7 @@ module.exports = function rollup() { // eslint-disable-next-line no-restricted-globals tsconfig: path.join(__dirname, "tsconfig.dom.json"), noEmitOnError: !WATCH, + noForceEmit: true, }), ], }, diff --git a/packages/react-router/tsconfig.json b/packages/react-router/tsconfig.json index 302bedbf6b..9238ae0799 100644 --- a/packages/react-router/tsconfig.json +++ b/packages/react-router/tsconfig.json @@ -4,8 +4,8 @@ "compilerOptions": { "lib": ["ES2020", "DOM", "DOM.Iterable"], "target": "ES2020", - "module": "ES2020", - "moduleResolution": "node", + "module": "Node16", + "moduleResolution": "Node16", "strict": true, "jsx": "react", diff --git a/rollup.config.js b/rollup.config.js index 66a4af0937..2a6b16ee22 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -4,13 +4,13 @@ const path = require("path"); module.exports = function rollup(options) { return [ "react-router", + "react-router-dom", // depends on react-router + "react-router-node", // depends on react-router + "react-router-express", // depends on react-router-node + "react-router-serve", // depends on react-router-node/express + "react-router-dev", // depends on react-router-node/express/serve "react-router-architect", "react-router-cloudflare", - "react-router-dom", - "react-router-dev", - "react-router-express", - "react-router-node", - "react-router-serve", "react-router-fs-routes", "react-router-remix-config-routes-adapter", ] diff --git a/rollup.utils.js b/rollup.utils.js index 4d8d1fe83d..e131cc72df 100644 --- a/rollup.utils.js +++ b/rollup.utils.js @@ -150,15 +150,6 @@ const remixBabelConfig = { plugins: [ "@babel/plugin-proposal-export-namespace-from", "@babel/plugin-proposal-optional-chaining", - // Strip console.debug calls unless REACT_ROUTER_DEBUG=true - ...(process.env.REACT_ROUTER_DEBUG === "true" - ? [] - : [ - [ - "transform-remove-console", - { exclude: ["error", "warn", "log", "info"] }, - ], - ]), ], }; From 70073f800cbfb92370f74a3f6f84631fbe6847e7 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 16 Oct 2024 16:55:41 -0400 Subject: [PATCH 15/33] Update DEVELOPMENT.md --- DEVELOPMENT.md | 99 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 67 insertions(+), 32 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 78f28508d5..0022a023ce 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -4,25 +4,30 @@ New releases should be created from release branches originating from the `dev` branch. When you are ready to begin the release process: -- Make sure you've pulled all the changes from GitHub for both `dev` and `main` branches. -- Check out the `dev` branch. -- Create a new `release-next` branch (eg, `git checkout -b release-next`). +- Make sure you've pulled all the changes from GitHub for both `dev` and `main` branches +- Check out the `dev` branch +- Create a new `release-next` branch (eg, `git checkout -b release-next`) - Technically, any `release-*` branch name will work as this is what triggers our GitHub CI workflow that will ultimately publish the release - but we just always use `release-next` -- Merge `main` into the release branch. + - We are using `release-v6` for [ongoing v6 releases](#6x-releases-from-the-v6-branch) +- Merge `main` into the release branch Changesets will do most of the heavy lifting for our releases. When changes are made to the codebase, an accompanying changeset file should be included to document the change. Those files will dictate how Changesets will version our packages and what shows up in the changelogs. ### Starting a new pre-release -- Ensure you are on the new `release-next` branch. -- Enter Changesets pre-release mode using the `pre` tag: `pnpm changeset pre enter pre`. -- Commit the change and push the `release-next` branch to GitHub. -- Wait for the release workflow to finish. The Changesets action in the workflow will open a PR that will increment all versions and generate the changelogs. +- Ensure you are on the new `release-next` branch +- Enter Changesets pre-release mode using the `pre` tag: + - `pnpm changeset pre enter pre` +- Commit the change and push the `release-next` branch to GitHub + - `git commit -a -m "Enter prerelease mode"` + - `git push --set-upstream origin release-next` +- Wait for the release workflow to finish - the Changesets action will open a PR that will increment all versions and generate the changelogs - Check out the PR branch created by changesets locally -- Review the updated `CHANGELOG.md` files in the PR locally and make any adjustments necessary, then merge the PR into the `release-next` branch. +- _Optional:_ Review the updated `CHANGELOG.md` files in the PR locally and make any adjustments necessary, then merge the PR into the `release-next` branch. - `find packages -name 'CHANGELOG.md' -mindepth 2 -maxdepth 2 -exec code {} \;` + - Usually for prereleases there's not much to change here because the prerelease sections will be deleted prior to the final stable release anyway - Once the changesets files are in good shape, merge the PR to `release-next` -- Once the PR is merged, the release workflow will publish the updated `X.Y.Z-pre.*` packages to npm. +- Once the PR is merged, the release workflow will publish the updated `X.Y.Z-pre.*` packages to npm - At this point, you can begin crafting the release notes for the eventual stable release in the root `CHANGELOG.md` file in the repo - Copy the template for a new release and update the version numbers and links accordingly - Copy the relevant changelog entries from all packages into the release notes and adjust accordingly @@ -33,28 +38,30 @@ Changesets will do most of the heavy lifting for our releases. When changes are You may need to make changes to a pre-release prior to publishing a final stable release. To do so: - Branch off of `release-next` and make whatever changes you need -- Create a new changeset: `pnpm changeset`. - - **IMPORTANT:** This is required even if you ultimately don't want to include these changes in the logs. Remember, changelogs can be edited prior to publishing, but the Changeset version script needs to see new changesets in order to create a new version. -- Push your branch to GitHub and PR it to `release-next`. -- Once reviewed/approved, merge the PR to the `release-next` branch. -- Wait for the release workflow to finish and the Changesets action to open its PR that will increment all versions. -- Review the PR, make any adjustments necessary, and merge it into the `release-next` branch. -- Once the PR is merged, the release workflow will publish the updated `X.Y.Z-pre.*` packages to npm. +- Create a new changeset: `pnpm changeset` + - **IMPORTANT:** This is required even if you ultimately don't want to include these changes in the logs. Remember, changelogs can be edited prior to publishing, but the Changeset version script needs to see new changesets in order to create a new version +- Push your branch to GitHub and PR it to `release-next` +- Once reviewed/approved, merge the PR to the `release-next` branch +- Wait for the release workflow to finish and the Changesets action to open its PR that will increment all versions + - Note: If more changes are needed you can just merge them to `release-next` and this PR will automatically update in place +- Review the PR, make any adjustments necessary, and merge it into the `release-next` branch +- Once the PR is merged, the release workflow will publish the updated `X.Y.Z-pre.*` packages to npm - Make sure you copy over the new changeset contents into stable release notes in the root `CHANGELOG.md` file in the repo ### Publishing the stable release -- Exit Changesets pre-release mode in the `release-next` branch: `pnpm changeset pre exit`. -- Commit the edited pre-release file along with any unpublished changesets, and push the `release-next` branch to GitHub. -- Wait for the release workflow to finish. The Changesets action in the workflow will open a PR that will increment all versions and generate the changelogs for the stable release. -- Review the updated `CHANGELOG` files in the PR and make any adjustments necessary. +- Exit Changesets pre-release mode in the `release-next` branch: + - `pnpm changeset pre exit` +- Commit the edited pre-release file along with any unpublished changesets, and push the `release-next` branch to GitHub +- Wait for the release workflow to finish - the Changesets action in the workflow will open a PR that will increment all versions and generate the changelogs for the stable release +- Review the updated `CHANGELOG` files in the PR and make any adjustments necessary - `find packages -name 'CHANGELOG.md' -mindepth 2 -maxdepth 2 -exec code {} \;` - Our automated release process should have removed prerelease entries - Finalize the release notes - - This should already be in pretty good shape in the root `CHANGELOG.md` file in the repo + - This should already be in pretty good shape in the root `CHANGELOG.md` file in the repo because changes have been added with each prerelease - Do a quick double check that all iterated prerelease changesets got copied over -- Merge the PR into the `release-next` branch. -- Once the PR is merged, the release workflow will publish the updated packages to npm. +- Merge the PR into the `release-next` branch +- Once the PR is merged, the release workflow will publish the updated packages to npm - Once the release is published: - Pull the latest `release-next` branch containing the PR you just merged - Merge the `release-next` branch into `main` **using a non-fast-forward merge** and push it up to GitHub @@ -72,14 +79,42 @@ You may need to make changes to a pre-release prior to publishing a final stable Hotfix releases follow the same process as standard releases above, but the `release-next` branch should be branched off latest `main` instead of `dev`. Once the stable hotfix is published, the `release-next` branch should be merged back into both `main` and `dev` just like a normal release. +### 6.x releases from the `v6` branch + +After the `6.25.0` release, we branched off a `v6` branch for continued `6.x` work and merged the `v7` branch into `dev` to begin preparation for the `7.0.0` release. Until we launch `7.0.0`, we need to `6.x` releases in a slightly different manner. + +- Changes for 6.x should be PR'd to the `v6` branch with a changeset +- Once merged, cherry-pick or re-do those changes against the `dev` branch so that they show up in v7 + - This does not apply to things like adding deprecation warnings that should not land in v7 + - You should not include a changeset in your commit to `dev` +- Starting the release process for 6.x is the same as outlined above, with a few changes: + - Branch from `v6` instead of `dev` + - Use the name `release-v6` to avoid collisions with the ongoing v7 (pre)releases using `release-next` + - Do not merge `main` into the `release-v6` branch +- The process of the PRs and iterating on prereleases remains the same +- Once the stable release is out: + - Merge `release-v6` back to `v6` with a **Normal Merge** + - Copy the updated changelog entry for the `6.X.Y` version to `main` + - The _code_ changes should already be in the `dev` branch but confirm that the commits in this release are all included in `dev` already: + - I.e., https://github.com/remix-run/react-router/compare/react-router@6.26.1...react-router@6.26.2 + - If one or more are not, then you can manually bring them over by cherry-picking the commit (or re-doing the work) + - You should not include a changelog in your commit to `dev` + - Copy the updated changelogs from `release-next` over to `dev` so the changelogs continue to reflect this new 6x release into the v7 releases + +### Notes on 7.0.0-pre.N released during the v7 prerelease + +During the v7 prerelease, the process for iterating and shipping a new `7.0.0-pre.N` release is slightly more streamlined than the steps outlined [above](#iterating-a-pre-release). Because we want _everything_ in `dev` to ship in the prerelease, cutting a new prerelease is simply: + +- Merge `dev` -> `release-next` +- This will include the changesets for changes committed to `dev` since the last prerelease +- This will automatically open a new Changesets PR for the new prerelease version + ### Experimental releases -Experimental releases use a [manually-triggered Github Actions workflow](./.github/workflows/release-experimental.yml) and can be built from any existing branch. to build and publish an experimental release: +Experimental releases and hot-fixes do not need to be branched off of `dev`. Experimental releases can be branched from anywhere as they are not intended for general use. -- Commit your changes to a branch -- Push the branch to github -- Go to the Github Actions UI for the [release-experimental.yml workflow](https://github.com/remix-run/react-router/actions/workflows/release-experimental.yml) -- Click the `Run workflow` dropdown -- Leave the `Use workflow from` dropdown as `main` -- Enter your feature branch in the `branch` input -- Click the `Run workflow` button +- Create a new branch for the release: `git checkout -b release-experimental` +- Make whatever changes you need and commit them: `git add . && git commit "experimental changes!"` +- Update version numbers and create a release tag: `pnpm run version:experimental` +- Push to GitHub: `git push origin --follow-tags` +- The CI workflow should automatically trigger from the experimental tag to publish the release to npm From 2f58222bad4b1fa2cf01a061825004a0fff31cc3 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Thu, 17 Oct 2024 16:17:27 +1100 Subject: [PATCH 16/33] Use route helper in config integration test (#12137) --- integration/route-config-test.ts | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/integration/route-config-test.ts b/integration/route-config-test.ts index 32a77c0958..5d5c5151cf 100644 --- a/integration/route-config-test.ts +++ b/integration/route-config-test.ts @@ -57,13 +57,10 @@ test.describe("route config", () => { let files: Files = async ({ port }) => ({ "vite.config.js": await viteConfig.basic({ port }), "app/routes.ts": js` - import { type RouteConfig } from "@react-router/dev/routes"; + import { type RouteConfig, index } from "@react-router/dev/routes"; export const routes: RouteConfig = [ - { - file: "test-route-1.tsx", - index: true, - }, + index("test-route-1.tsx"), ]; `, "app/test-route-1.tsx": ` @@ -115,13 +112,10 @@ test.describe("route config", () => { export { routes } from "./actual-routes"; `, "app/actual-routes.ts": js` - import { type RouteConfig } from "@react-router/dev/routes"; + import { type RouteConfig, index } from "@react-router/dev/routes"; export const routes: RouteConfig = [ - { - file: "test-route-1.tsx", - index: true, - }, + index("test-route-1.tsx"), ]; `, "app/test-route-1.tsx": ` @@ -167,13 +161,10 @@ test.describe("route config", () => { let files: Files = async ({ port }) => ({ "vite.config.js": await viteConfig.basic({ port }), "app/routes.ts": js` - import { type RouteConfig } from "@react-router/dev/routes"; + import { type RouteConfig, index } from "@react-router/dev/routes"; export const routes: RouteConfig = [ - { - file: "test-route-1.tsx", - index: true, - }, + index("test-route-1.tsx"), ]; `, "app/test-route-1.tsx": ` @@ -231,13 +222,10 @@ test.describe("route config", () => { "vite.config.js": await viteConfig.basic({ port }), "app/routes.ts": js` import path from "node:path"; - import { type RouteConfig } from "@react-router/dev/routes"; + import { type RouteConfig, index } from "@react-router/dev/routes"; export const routes: RouteConfig = [ - { - file: path.resolve(import.meta.dirname, "test-route.tsx"), - index: true, - }, + index(path.resolve(import.meta.dirname, "test-route.tsx")), ]; `, "app/test-route.tsx": ` From 3f7c47b284d9499f1b683846665510121842c330 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Thu, 17 Oct 2024 08:19:37 -0600 Subject: [PATCH 17/33] wordsmithing --- docs/upgrading/v6.md | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/docs/upgrading/v6.md b/docs/upgrading/v6.md index a1e4c2930f..56be091e1d 100644 --- a/docs/upgrading/v6.md +++ b/docs/upgrading/v6.md @@ -271,25 +271,25 @@ function shouldRevalidate({ actionStatus, defaultShouldRevalidate }) { Now that your app is caught up, you can simply update to v7 (theoretically!) without issue. -πŸ‘‰ **install v7** +πŸ‘‰ **Install v7 pre-release** ```shellscript nonumber -npm install react-router-dom@7 +npm install react-router-dom@pre ``` -Your app should continue to work but we've restructured in v7 so that you can import directly from `"react-router"` β€” we'll do that in the next step. - -_Note: If you have issues with the above command, you may need to use the full `7.0.0-pre.N` version number because package managers may not always resolve `@7` to a prerelease since there is no stable 7.x release yet._ +Your app should continue to work but you will get console warnings about your imports from "react-router-dom", let's tackle that next. πŸ‘‰ **Uninstall react-router-dom, install react-router** -In v7 we've also combined the `react-router` and `react-router-dom` packages and you can import everything directly from `"react-router"` (with one exception - see below): +In v7 we no longer need `"react-router-dom"` as the packages have been simplified. You can import everything from `"react-router"`: ```shellscript nonumber npm uninstall react-router-dom npm install react-router ``` +Note you only need `"react-router"` in your package.json. + πŸ‘‰ **Update imports** Now you can update you imports to come from `react-router`: @@ -299,18 +299,21 @@ Now you can update you imports to come from `react-router`: +import { useLocation } from "react-router"; ``` -The one exception to this rule is for exports that specifically require `react-dom` β€” namely `RouterProvider` and `HydratedRouter` which use [`ReactDOM.flushSync`][react-flushsync] internally. These need to come from a separate `package.json` export to avoid peer dependency issues in non-browser apps that don't install `react-dom`. If you're writing a browser-based app, you will want to those from `react-router/dom`: - -```js -import { RouterProvider } from "react-router/dom"; -``` - Instead of manually updating imports, you can use this command. Make sure your git working tree is clean though so you can revert if it doesn't work as expected. ```shellscript nonumber find ./path/to/src \( -name "*.tsx" -o -name "*.ts" -o -name "*.js" -o -name "*.jsx" \) -type f -exec sed -i '' 's|from "react-router-dom"|from "react-router"|g' {} + ``` +πŸ‘‰ **Update DOM-specific imports** + +`RouterProvider` and `HydratedRouter` come from a deep import because they depend on `"react-dom"`: + +```diff +-import { RouterProvider } from "react-router-dom"; ++import { RouterProvider } from "react-router/dom"; +``` + Congratulations, you're now on v7! [react-flushsync]: https://react.dev/reference/react-dom/flushSync From 211f46d77ae80495df55da7f48f3fd4635ab3f9d Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Thu, 17 Oct 2024 08:21:27 -0600 Subject: [PATCH 18/33] docs: add pre to install --- docs/upgrading/v6.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/upgrading/v6.md b/docs/upgrading/v6.md index 56be091e1d..abe56d51d8 100644 --- a/docs/upgrading/v6.md +++ b/docs/upgrading/v6.md @@ -285,7 +285,7 @@ In v7 we no longer need `"react-router-dom"` as the packages have been simplifie ```shellscript nonumber npm uninstall react-router-dom -npm install react-router +npm install react-router@pre ``` Note you only need `"react-router"` in your package.json. From 66613c0c6d1b2ba4f3636b660ba55803286d695c Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 17 Oct 2024 16:46:34 -0400 Subject: [PATCH 19/33] Fix contxt types for Route.LoaderArgs, add upgrade docs (#12145) --- docs/upgrading/remix.md | 31 ++++++++++++++++++++++++++++++ packages/react-router/lib/types.ts | 15 +++++++++------ 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/docs/upgrading/remix.md b/docs/upgrading/remix.md index 3e48e13e0c..c868bb219c 100644 --- a/docs/upgrading/remix.md +++ b/docs/upgrading/remix.md @@ -96,6 +96,34 @@ If you have an `entry.server.tsx` and/or an `entry.client.tsx` file in your appl | `entry.server.tsx` | `` | ➑️ | `` | | `entry.client.stx` | `` | ➑️ | `` | +### Step 7 - Update types for `AppLoadContext` + +This is only applicable if you were using a custom server in Remix v2. If you were using `remix-serve` you can skip this step. + +If you were using `getLoadContext` in your Remix app, then you'll notice that the `LoaderFunctionArgs`/`ActionFunctionArgs` types now type the `context` parameter incorrectly (optional and typed as `any`). These types accept a generic for the `context` type but even that still leaves the property as optional because it does not exist in React Router SPA apps. + +The proper long term fix is to move to the new [`Route.LoaderArgs`][server-loaders]/[`Route.ActionArgs`][server-actions] types from the new typegen in React Router v7. + +However, the short-term solution to ease the upgrade is to use TypeScript's [module augmentation][ts-module-augmentation] feature to override the built in `LoaderFunctionArgs`/`ActionFunctionArgs` interfaces. + +You can do this with the following code in your `vite.config.ts`: + +```ts filename="vite.config.ts" +// Your AppLoadContext used in v2 +interface AppLoadContext { + whatever: string; +} + +// Tell v7 the type of the context and that it is non-optional +declare module "react-router" { + interface LoaderFunctionArgs { + context: AppLoadContext; + } +} +``` + +This should allow you to upgrade and ship your application on React Router v7, and then you can incrementally migrate routes to the new typegen approach. + ## Known Prerelease Issues ### Typesafety @@ -127,3 +155,6 @@ let data = useLoaderData(); [routing]: ../start/routing [fs-routing]: ../misc/file-route-conventions [v7-changelog-types]: https://github.com/remix-run/react-router/blob/release-next/CHANGELOG.md#typesafety-improvements +[server-loaders]: ../start/data-loading#server-data-loading +[server-actions]: ../start/actions#server-actions +[ts-module-augmentation]: https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation diff --git a/packages/react-router/lib/types.ts b/packages/react-router/lib/types.ts index 9e980d2957..9b0621b69a 100644 --- a/packages/react-router/lib/types.ts +++ b/packages/react-router/lib/types.ts @@ -88,10 +88,13 @@ type _CreateActionData = Awaited< undefined > -type DataFunctionArgs = { +type ClientDataFunctionArgs = { request: Request; params: Params; - context?: AppLoadContext; +}; + +type ServerDataFunctionArgs = ClientDataFunctionArgs & { + context: AppLoadContext; }; // prettier-ignore @@ -122,21 +125,21 @@ type Serialize = undefined -export type CreateServerLoaderArgs = DataFunctionArgs; +export type CreateServerLoaderArgs = ServerDataFunctionArgs; export type CreateClientLoaderArgs< Params, T extends RouteModule -> = DataFunctionArgs & { +> = ClientDataFunctionArgs & { serverLoader: () => Promise>; }; -export type CreateServerActionArgs = DataFunctionArgs; +export type CreateServerActionArgs = ServerDataFunctionArgs; export type CreateClientActionArgs< Params, T extends RouteModule -> = DataFunctionArgs & { +> = ClientDataFunctionArgs & { serverAction: () => Promise>; }; From 6bd83625b04000bb4979cb701d63802c9d4e81b4 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Fri, 18 Oct 2024 10:17:55 -0400 Subject: [PATCH 20/33] Remove json() utility (#12146) --- .changeset/strange-jeans-give.md | 7 + docs/misc/resource-routes.md | 3 +- integration/abort-signal-test.ts | 5 +- integration/bug-report-test.ts | 3 +- integration/catch-boundary-data-test.ts | 3 +- integration/catch-boundary-test.ts | 3 +- integration/client-data-test.ts | 36 +- integration/error-data-request-test.ts | 8 +- integration/fetcher-layout-test.ts | 22 +- integration/fetcher-test.ts | 5 +- integration/form-data-test.ts | 6 +- integration/form-test.ts | 746 +++++++++--------- integration/headers-test.ts | 25 +- integration/hook-useSubmit-test.ts | 7 +- integration/link-test.ts | 12 +- integration/loader-test.ts | 9 +- integration/matches-test.ts | 9 +- integration/multiple-cookies-test.ts | 6 +- integration/remix-serve-test.ts | 3 +- integration/request-test.ts | 5 +- integration/resource-routes-test.ts | 31 +- integration/revalidate-test.ts | 10 +- integration/set-cookie-revalidation-test.ts | 4 +- integration/transition-test.ts | 2 +- integration/vite-build-test.ts | 17 +- integration/vite-cloudflare-test.ts | 3 +- integration/vite-dev-test.ts | 16 +- integration/vite-dot-server-test.ts | 3 +- integration/vite-dotenv-test.ts | 5 +- integration/vite-hmr-hdr-test.ts | 8 +- integration/vite-loader-context-test.ts | 3 +- .../__tests__/data-router-no-dom-test.tsx | 10 + .../__tests__/dom/data-static-router-test.tsx | 5 +- .../react-router/__tests__/dom/stub-test.tsx | 13 +- .../__tests__/dom/use-blocker-test.tsx | 3 +- .../__tests__/router/data-strategy-test.ts | 11 +- .../__tests__/router/lazy-test.ts | 13 +- .../__tests__/router/navigation-test.ts | 7 +- .../react-router/__tests__/router/ssr-test.ts | 35 +- .../server-runtime/handle-error-test.ts | 7 +- .../__tests__/server-runtime/handler-test.ts | 4 +- .../server-runtime/responses-test.ts | 29 +- .../__tests__/server-runtime/server-test.ts | 13 +- packages/react-router/index.ts | 11 - packages/react-router/lib/dom/ssr/data.ts | 10 - .../react-router/lib/dom/ssr/single-fetch.tsx | 3 +- packages/react-router/lib/router/router.ts | 21 +- packages/react-router/lib/router/utils.ts | 24 +- .../react-router/lib/server-runtime/data.ts | 3 +- .../lib/server-runtime/responses.ts | 81 -- .../lib/server-runtime/routeModules.ts | 16 +- .../react-router/lib/server-runtime/server.ts | 15 +- .../lib/server-runtime/single-fetch.ts | 2 +- packages/react-router/lib/types.ts | 13 +- 54 files changed, 592 insertions(+), 782 deletions(-) create mode 100644 .changeset/strange-jeans-give.md delete mode 100644 packages/react-router/lib/server-runtime/responses.ts diff --git a/.changeset/strange-jeans-give.md b/.changeset/strange-jeans-give.md new file mode 100644 index 0000000000..a33c69d1a3 --- /dev/null +++ b/.changeset/strange-jeans-give.md @@ -0,0 +1,7 @@ +--- +"react-router": patch +--- + +Remove the deprecated `json` utility + +- You can use [`Response.json`](https://developer.mozilla.org/en-US/docs/Web/API/Response/json_static) if you still need to construct JSON responses in your app diff --git a/docs/misc/resource-routes.md b/docs/misc/resource-routes.md index 8dd66e8ec9..ced39187eb 100644 --- a/docs/misc/resource-routes.md +++ b/docs/misc/resource-routes.md @@ -26,7 +26,8 @@ For example, consider a UI Route that renders a report, note the link: export async function loader({ params, }: LoaderFunctionArgs) { - return json(await getReport(params.id)); + let report = await getReport(params.id); + return report; } export default function Report() { diff --git a/integration/abort-signal-test.ts b/integration/abort-signal-test.ts index 967f079b12..f650ec6f1f 100644 --- a/integration/abort-signal-test.ts +++ b/integration/abort-signal-test.ts @@ -15,17 +15,16 @@ test.beforeAll(async () => { fixture = await createFixture({ files: { "app/routes/_index.tsx": js` - import { json } from "react-router"; import { useActionData, useLoaderData, Form } from "react-router"; export async function action ({ request }) { // New event loop causes express request to close await new Promise(r => setTimeout(r, 0)); - return json({ aborted: request.signal.aborted }); + return { aborted: request.signal.aborted }; } export function loader({ request }) { - return json({ aborted: request.signal.aborted }); + return { aborted: request.signal.aborted }; } export default function Index() { diff --git a/integration/bug-report-test.ts b/integration/bug-report-test.ts index ccacf307fa..5e272b897a 100644 --- a/integration/bug-report-test.ts +++ b/integration/bug-report-test.ts @@ -59,11 +59,10 @@ test.beforeAll(async () => { //////////////////////////////////////////////////////////////////////////// files: { "app/routes/_index.tsx": js` - import { json } from "react-router"; import { useLoaderData, Link } from "react-router"; export function loader() { - return json("pizza"); + return "pizza"; } export default function Index() { diff --git a/integration/catch-boundary-data-test.ts b/integration/catch-boundary-data-test.ts index e99cc8f06c..7f6c89284e 100644 --- a/integration/catch-boundary-data-test.ts +++ b/integration/catch-boundary-data-test.ts @@ -40,7 +40,6 @@ test.describe("ErrorBoundary (thrown responses)", () => { fixture = await createFixture({ files: { "app/root.tsx": js` - import { json } from "react-router"; import { Links, Meta, @@ -50,7 +49,7 @@ test.describe("ErrorBoundary (thrown responses)", () => { useMatches, } from "react-router"; - export const loader = () => json("${ROOT_DATA}"); + export const loader = () => "${ROOT_DATA}"; export default function Root() { const data = useLoaderData(); diff --git a/integration/catch-boundary-test.ts b/integration/catch-boundary-test.ts index 1b1cb6475e..0cad7d7b5c 100644 --- a/integration/catch-boundary-test.ts +++ b/integration/catch-boundary-test.ts @@ -30,11 +30,10 @@ test.describe("ErrorBoundary (thrown responses)", () => { fixture = await createFixture({ files: { "app/root.tsx": js` - import { json } from "react-router"; import { Links, Meta, Outlet, Scripts, useMatches } from "react-router"; export function loader() { - return json({ data: "ROOT LOADER" }); + return { data: "ROOT LOADER" }; } export default function Root() { diff --git a/integration/client-data-test.ts b/integration/client-data-test.ts index 093135f3e3..57b6a083f9 100644 --- a/integration/client-data-test.ts +++ b/integration/client-data-test.ts @@ -49,10 +49,9 @@ function getFiles({ } `, "app/routes/parent.tsx": js` - import { json } from "react-router" import { Outlet, useLoaderData } from "react-router" export function loader() { - return json({ message: 'Parent Server Loader'}); + return { message: 'Parent Server Loader' }; } ${ parentClientLoader @@ -89,13 +88,12 @@ function getFiles({ } `, "app/routes/parent.child.tsx": js` - import { json } from "react-router" import { Form, Outlet, useActionData, useLoaderData } from "react-router" export function loader() { - return json({ message: 'Child Server Loader'}); + return { message: 'Child Server Loader' }; } export function action() { - return json({ message: 'Child Server Action'}); + return { message: 'Child Server Action' }; } ${ childClientLoader @@ -417,18 +415,13 @@ test.describe("Client Data", () => { }), "app/routes/parent.child.tsx": js` import * as React from 'react'; - import { json } from "react-router"; import { useLoaderData } from "react-router"; export function loader() { - return json({ - message: "Child Server Loader Data", - }); + return { message: "Child Server Loader Data" }; } export async function clientLoader({ serverLoader }) { await new Promise(r => setTimeout(r, 100)); - return { - message: "Child Client Loader Data", - }; + return { message: "Child Client Loader Data" }; } export function HydrateFallback() { return

SHOULD NOT SEE ME

@@ -600,19 +593,14 @@ test.describe("Client Data", () => { }), "app/routes/parent.child.tsx": js` import * as React from 'react'; - import { json } from "react-router"; import { useLoaderData, useRevalidator } from "react-router"; let isFirstCall = true; export async function loader({ serverLoader }) { if (isFirstCall) { isFirstCall = false - return json({ - message: "Child Server Loader Data (1)", - }); + return { message: "Child Server Loader Data (1)" }; } - return json({ - message: "Child Server Loader Data (2+)", - }); + return { message: "Child Server Loader Data (2+)" }; } export async function clientLoader({ serverLoader }) { await new Promise(r => setTimeout(r, 100)); @@ -671,13 +659,9 @@ test.describe("Client Data", () => { export async function loader({ serverLoader }) { if (isFirstCall) { isFirstCall = false - return json({ - message: "Child Server Loader Data (1)", - }); + return { message: "Child Server Loader Data (1)" }; } - return json({ - message: "Child Server Loader Data (2+)", - }); + return { message: "Child Server Loader Data (2+)" }; } let isFirstClientCall = true; export async function clientLoader({ serverLoader }) { @@ -799,7 +783,7 @@ test.describe("Client Data", () => { "app/routes/parent.tsx": js` import { Outlet, useLoaderData, useRouteLoaderData, useRouteError } from 'react-router' export function loader() { - return { message: 'Parent Server Loader'}; + return { message: 'Parent Server Loader' }; } export async function clientLoader({ serverLoader }) { console.log('running parent client loader') diff --git a/integration/error-data-request-test.ts b/integration/error-data-request-test.ts index bc5f0f64af..057cc6dd82 100644 --- a/integration/error-data-request-test.ts +++ b/integration/error-data-request-test.ts @@ -58,10 +58,8 @@ test.describe("ErrorBoundary", () => { `, [`app/routes/loader-return-json.jsx`]: js` - import { json } from "react-router"; - export async function loader() { - return json({ ok: true }); + return { ok: true }; } export default function () { @@ -80,10 +78,8 @@ test.describe("ErrorBoundary", () => { `, [`app/routes/action-return-json.jsx`]: js` - import { json } from "react-router"; - export async function action() { - return json({ ok: true }); + return { ok: true }; } export default function () { diff --git a/integration/fetcher-layout-test.ts b/integration/fetcher-layout-test.ts index 8e5bd67b88..b4113fd8a9 100644 --- a/integration/fetcher-layout-test.ts +++ b/integration/fetcher-layout-test.ts @@ -15,10 +15,9 @@ test.beforeAll(async () => { fixture = await createFixture({ files: { "app/routes/layout-action.tsx": js` - import { json } from "react-router"; import { Outlet, useFetcher, useFormAction } from "react-router"; - export let action = ({ params }) => json("layout action data"); + export let action = ({ params }) => "layout action data"; export default function ActionLayout() { let fetcher = useFetcher(); @@ -40,16 +39,15 @@ test.beforeAll(async () => { `, "app/routes/layout-action._index.tsx": js` - import { json } from "react-router"; import { useFetcher, useFormAction, useLoaderData, } from "react-router"; - export let loader = ({ params }) => json("index data"); + export let loader = ({ params }) => "index data"; - export let action = ({ params }) => json("index action data"); + export let action = ({ params }) => "index action data"; export default function ActionLayoutIndex() { let data = useLoaderData(); @@ -71,16 +69,15 @@ test.beforeAll(async () => { `, "app/routes/layout-action.$param.tsx": js` - import { json } from "react-router"; import { useFetcher, useFormAction, useLoaderData, } from "react-router"; - export let loader = ({ params }) => json(params.param); + export let loader = ({ params }) => params.param; - export let action = ({ params }) => json("param action data"); + export let action = ({ params }) => "param action data"; export default function ActionLayoutChild() { let data = useLoaderData(); @@ -102,10 +99,9 @@ test.beforeAll(async () => { `, "app/routes/layout-loader.tsx": js` - import { json } from "react-router"; import { Outlet, useFetcher, useFormAction } from "react-router"; - export let loader = () => json("layout loader data"); + export let loader = () => "layout loader data"; export default function LoaderLayout() { let fetcher = useFetcher(); @@ -127,14 +123,13 @@ test.beforeAll(async () => { `, "app/routes/layout-loader._index.tsx": js` - import { json } from "react-router"; import { useFetcher, useFormAction, useLoaderData, } from "react-router"; - export let loader = ({ params }) => json("index data"); + export let loader = ({ params }) => "index data"; export default function ActionLayoutIndex() { let fetcher = useFetcher(); @@ -154,14 +149,13 @@ test.beforeAll(async () => { `, "app/routes/layout-loader.$param.tsx": js` - import { json } from "react-router"; import { useFetcher, useFormAction, useLoaderData, } from "react-router"; - export let loader = ({ params }) => json(params.param); + export let loader = ({ params }) => params.param; export default function ActionLayoutChild() { let fetcher = useFetcher(); diff --git a/integration/fetcher-test.ts b/integration/fetcher-test.ts index b91ef8d8e1..138300cb25 100644 --- a/integration/fetcher-test.ts +++ b/integration/fetcher-test.ts @@ -149,7 +149,6 @@ test.describe("useFetcher", () => { `, "app/routes/fetcher-echo.tsx": js` - import { json } from "react-router"; import { useFetcher } from "react-router"; export async function action({ request }) { @@ -164,13 +163,13 @@ test.describe("useFetcher", () => { } else { value = (await request.formData()).get('value'); } - return json({ data: "ACTION (" + contentType + ") " + value }) + return { data: "ACTION (" + contentType + ") " + value } } export async function loader({ request }) { await new Promise(r => setTimeout(r, 1000)); let value = new URL(request.url).searchParams.get('value'); - return json({ data: "LOADER " + value }) + return { data: "LOADER " + value } } export default function Index() { diff --git a/integration/form-data-test.ts b/integration/form-data-test.ts index 988eaef1d0..3653f92850 100644 --- a/integration/form-data-test.ts +++ b/integration/form-data-test.ts @@ -9,15 +9,13 @@ test.beforeAll(async () => { fixture = await createFixture({ files: { "app/routes/_index.tsx": js` - import { json } from "react-router"; - export async function action({ request }) { try { await request.formData() } catch { - return json("no pizza"); + return new Response("no pizza"); } - return json("pizza"); + return new Response("pizza"); } `, }, diff --git a/integration/form-test.ts b/integration/form-test.ts index 7b6b86b689..243d7f427d 100644 --- a/integration/form-test.ts +++ b/integration/form-test.ts @@ -62,435 +62,433 @@ test.describe("Forms", () => { fixture = await createFixture({ files: { "app/routes/get-submission.tsx": js` - import { useLoaderData, Form } from "react-router"; + import { useLoaderData, Form } from "react-router"; - export function loader({ request }) { - let url = new URL(request.url); - return url.searchParams.toString() - } + export function loader({ request }) { + let url = new URL(request.url); + return url.searchParams.toString() + } - export default function() { - let data = useLoaderData(); - return ( - <> -
- - - -
- -
- - -
- -
- - -
+ export default function() { + let data = useLoaderData(); + return ( + <> +
+ + + +
+
+ + +
+ +
+ - - - - - - -
- -
{data}
- - ) - } - `, + value="${LAKSA}" + > + + + + + + + + +
+ + + + +
+ +
{data}
+ + ) + } + `, "app/routes/about.tsx": js` - export async function action({ request }) { - return json({ submitted: true }); - } - export default function () { - return

About

; - } - `, + export async function action({ request }) { + return { submitted: true }; + } + export default function () { + return

About

; + } + `, "app/routes/inbox.tsx": js` - import { Form } from "react-router"; - export default function() { - return ( - <> -
- -
-
- -
-
- -
-
- -
-
- -
- - ) - } - `, + import { Form } from "react-router"; + export default function() { + return ( + <> +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ + ) + } + `, "app/routes/blog.tsx": js` - import { Form, Outlet } from "react-router"; - export default function() { - return ( - <> -

Blog

-
- -
-
- -
-
- -
-
- -
-
- -
- - - ) - } - `, + import { Form, Outlet } from "react-router"; + export default function() { + return ( + <> +

Blog

+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ + + ) + } + `, "app/routes/blog._index.tsx": js` - import { Form } from "react-router"; - export function action() { - return { ok: true }; - } - export default function() { - return ( - <> -
- - -
-
- -
-
- -
-
- -
-
- -
- -
- - -
- - ) - } - `, + import { Form } from "react-router"; + export function action() { + return { ok: true }; + } + export default function() { + return ( + <> +
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+ + +
+ + ) + } + `, "app/routes/blog.$postId.tsx": js` - import { Form } from "react-router"; - export default function() { - return ( - <> -
- -
-
- -
-
- -
-
- -
-
- -
- - ) - } - `, + import { Form } from "react-router"; + export default function() { + return ( + <> +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ + ) + } + `, "app/routes/projects.tsx": js` - import { Form, Outlet } from "react-router"; - export default function() { - return ( - <> -

Projects

- - - ) - } - `, + import { Form, Outlet } from "react-router"; + export default function() { + return ( + <> +

Projects

+ + + ) + } + `, "app/routes/projects._index.tsx": js` - export default function() { - return

All projects

- } - `, + export default function() { + return

All projects

+ } + `, "app/routes/projects.$.tsx": js` - import { Form } from "react-router"; - export default function() { - return ( - <> -
- -
-
- -
-
- -
-
- -
-
- -
- - ) - } - `, + import { Form } from "react-router"; + export default function() { + return ( + <> +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ + ) + } + `, "app/routes/stop-propagation.tsx": js` - import { json } from "react-router"; - import { Form, useActionData } from "react-router"; + import { Form, useActionData } from "react-router"; - export async function action({ request }) { - let formData = await request.formData(); - return json(Object.fromEntries(formData)); - } + export async function action({ request }) { + let formData = await request.formData(); + return Object.fromEntries(formData); + } - export default function Index() { - let actionData = useActionData(); - return ( -
event.stopPropagation()}> - {actionData ?
{JSON.stringify(actionData)}
: null} -
- -
-
- ) - } - `, + export default function Index() { + let actionData = useActionData(); + return ( +
event.stopPropagation()}> + {actionData ?
{JSON.stringify(actionData)}
: null} +
+ +
+
+ ) + } + `, "app/routes/form-method.tsx": js` - import { Form, useActionData, useLoaderData, useSearchParams } from "react-router"; - import { json } from "react-router"; + import { Form, useActionData, useLoaderData, useSearchParams } from "react-router"; - export function action({ request }) { - return json(request.method) - } + export function action({ request }) { + return request.method + } - export function loader({ request }) { - return json(request.method) - } + export function loader({ request }) { + return request.method + } - export default function() { - let actionData = useActionData(); - let loaderData = useLoaderData(); - let [searchParams] = useSearchParams(); - let formMethod = searchParams.get('method') || 'GET'; - let submitterFormMethod = searchParams.get('submitterFormMethod') || 'GET'; - return ( - <> -
- - -
- {actionData ?
{actionData}
: null} -
{loaderData}
- - ) - } - `, + export default function() { + let actionData = useActionData(); + let loaderData = useLoaderData(); + let [searchParams] = useSearchParams(); + let formMethod = searchParams.get('method') || 'GET'; + let submitterFormMethod = searchParams.get('submitterFormMethod') || 'GET'; + return ( + <> +
+ + +
+ {actionData ?
{actionData}
: null} +
{loaderData}
+ + ) + } + `, "app/routes/submitter.tsx": js` - import { Form } from "react-router"; - - export default function() { - return ( - <> - -
- - - - - - - - - -
- - ) - } - `, + import { Form } from "react-router"; + + export default function() { + return ( + <> + +
+ + + + + + + + + +
+ + ) + } + `, "app/routes/file-upload.tsx": js` - import { Form, useSearchParams } from "react-router"; - - export default function() { - const [params] = useSearchParams(); - return ( -
- - - -
- {actionData ?

{JSON.stringify(actionData)}

: null} - - ) - } - `, + export default function() { + const actionData = useActionData(); + return ( +
+ + + + + {actionData ?

{JSON.stringify(actionData)}

: null} +
+ ) + } + `, // Generic route for outputting url-encoded form data (either from the request body or search params) // // TODO: refactor other tests to use this "app/routes/outputFormData.tsx": js` - import { useActionData, useSearchParams } from "react-router"; - - export async function action({ request }) { - const formData = await request.formData(); - const body = new URLSearchParams(); - for (let [key, value] of formData) { - body.append( - key, - value instanceof File ? await streamToString(value.stream()) : value - ); - } - return body.toString(); + import { useActionData, useSearchParams } from "react-router"; + + export async function action({ request }) { + const formData = await request.formData(); + const body = new URLSearchParams(); + for (let [key, value] of formData) { + body.append( + key, + value instanceof File ? await streamToString(value.stream()) : value + ); } + return body.toString(); + } - export default function OutputFormData() { - const requestBody = useActionData(); - const searchParams = useSearchParams()[0]; - return ; - } - `, + export default function OutputFormData() { + const requestBody = useActionData(); + const searchParams = useSearchParams()[0]; + return ; + } + `, "myfile.txt": "stuff", "app/routes/pathless-layout-parent.tsx": js` - import { json, Form, Outlet, useActionData } from "react-router" + import { Form, Outlet, useActionData } from "react-router" - export async function action({ request }) { - return json({ submitted: true }); - } - export default function () { - let data = useActionData(); - return ( - <> -
-

Pathless Layout Parent

- -
- -

{data?.submitted === true ? 'Submitted - Yes' : 'Submitted - No'}

- - ); - } - `, + export async function action({ request }) { + return { submitted: true }; + } + export default function () { + let data = useActionData(); + return ( + <> +
+

Pathless Layout Parent

+ +
+ +

{data?.submitted === true ? 'Submitted - Yes' : 'Submitted - No'}

+ + ); + } + `, "app/routes/pathless-layout-parent._pathless.nested.tsx": js` - import { Outlet } from "react-router"; - - export default function () { - return ( - <> -

Pathless Layout

- - - ); - } - `, + import { Outlet } from "react-router"; + + export default function () { + return ( + <> +

Pathless Layout

+ + + ); + } + `, "app/routes/pathless-layout-parent._pathless.nested._index.tsx": js` - export default function () { - return

Pathless Layout Index

- } - `, + export default function () { + return

Pathless Layout Index

+ } + `, }, }); diff --git a/integration/headers-test.ts b/integration/headers-test.ts index d51dc69d06..7571323c42 100644 --- a/integration/headers-test.ts +++ b/integration/headers-test.ts @@ -17,10 +17,9 @@ test.describe.skip("headers export", () => { { files: { "app/root.tsx": js` - import { json } from "react-router"; import { Links, Meta, Outlet, Scripts } from "react-router"; - export const loader = () => json({}); + export const loader = () => ({}); export default function Root() { return ( @@ -39,10 +38,10 @@ test.describe.skip("headers export", () => { `, "app/routes/_index.tsx": js` - import { json } from "react-router"; + import { data } from "react-router"; export function loader() { - return json(null, { + return data(null, { headers: { "${ROOT_HEADER_KEY}": "${ROOT_HEADER_VALUE}" } @@ -61,10 +60,10 @@ test.describe.skip("headers export", () => { `, "app/routes/action.tsx": js` - import { json } from "react-router"; + import { data } from "react-router"; export function action() { - return json(null, { + return data(null, { headers: { "${ACTION_HKEY}": "${ACTION_HVALUE}" } @@ -159,11 +158,11 @@ test.describe.skip("headers export", () => { `, "app/routes/cookie.tsx": js` - import { json, Outlet } from "react-router"; + import { data, Outlet } from "react-router"; export function loader({ request }) { if (new URL(request.url).searchParams.has("parent-throw")) { - throw json(null, { headers: { "Set-Cookie": "parent-thrown-cookie=true" } }); + throw data(null, { headers: { "Set-Cookie": "parent-thrown-cookie=true" } }); } return null }; @@ -178,13 +177,13 @@ test.describe.skip("headers export", () => { `, "app/routes/cookie.child.tsx": js` - import { json } from "react-router"; + import { data } from "react-router"; export function loader({ request }) { if (new URL(request.url).searchParams.has("throw")) { - throw json(null, { headers: { "Set-Cookie": "thrown-cookie=true" } }); + throw data(null, { headers: { "Set-Cookie": "thrown-cookie=true" } }); } - return json(null, { + return data(null, { headers: { "Set-Cookie": "normal-cookie=true" }, }); }; @@ -239,10 +238,10 @@ test.describe.skip("headers export", () => { `, "app/routes/_index.tsx": js` - import { json } from "react-router"; + import { data } from "react-router"; export function loader() { - return json(null, { + return data(null, { headers: { "${HEADER_KEY}": "${HEADER_VALUE}" } diff --git a/integration/hook-useSubmit-test.ts b/integration/hook-useSubmit-test.ts index ac7ec35bf1..b28cc172e4 100644 --- a/integration/hook-useSubmit-test.ts +++ b/integration/hook-useSubmit-test.ts @@ -45,19 +45,18 @@ test.describe("`useSubmit()` returned function", () => { } `, "app/routes/action.tsx": js` - import { json } from "react-router"; import { useActionData, useSubmit } from "react-router"; export async function action({ request }) { let contentType = request.headers.get('Content-Type'); if (contentType.includes('application/json')) { - return json({ value: await request.json() }); + return { value: await request.json() }; } if (contentType.includes('text/plain')) { - return json({ value: await request.text() }); + return { value: await request.text() }; } let fd = await request.formData(); - return json({ value: new URLSearchParams(fd.entries()).toString() }) + return { value: new URLSearchParams(fd.entries()).toString() } } export default function Component() { diff --git a/integration/link-test.ts b/integration/link-test.ts index 183c8bf709..1c7ffb3c23 100644 --- a/integration/link-test.ts +++ b/integration/link-test.ts @@ -308,20 +308,18 @@ test.describe("route module link export", () => { `, "app/routes/gists.tsx": js` - import { json } from "react-router"; - import { Link, Outlet, useLoaderData, useNavigation } from "react-router"; + import { data, Link, Outlet, useLoaderData, useNavigation } from "react-router"; import stylesHref from "~/gists.css?url"; export function links() { return [{ rel: "stylesheet", href: stylesHref }]; } export async function loader() { - let data = { + return data({ users: [ { id: "ryanflorence", name: "Ryan Florence" }, { id: "mjackson", name: "Michael Jackson" }, ], - }; - return json(data, { + }, { headers: { "Cache-Control": "public, max-age=60", }, @@ -359,7 +357,7 @@ test.describe("route module link export", () => { `, "app/routes/gists.$username.tsx": js` - import { json, redirect } from "react-router"; + import { data, redirect } from "react-router"; import { Link, useLoaderData, useParams } from "react-router"; export async function loader({ params }) { let { username } = params; @@ -367,7 +365,7 @@ test.describe("route module link export", () => { return redirect("/gists/mjackson", 302); } if (username === "_why") { - return json(null, { status: 404 }); + return data(null, { status: 404 }); } return ${JSON.stringify(fakeGists)}; } diff --git a/integration/loader-test.ts b/integration/loader-test.ts index a0c8a5c31a..9592d69563 100644 --- a/integration/loader-test.ts +++ b/integration/loader-test.ts @@ -18,10 +18,9 @@ test.describe("loader", () => { fixture = await createFixture({ files: { "app/root.tsx": js` - import { json } from "react-router"; import { Links, Meta, Outlet, Scripts } from "react-router"; - export const loader = () => json("${ROOT_DATA}"); + export const loader = () => "${ROOT_DATA}"; export default function Root() { return ( @@ -40,8 +39,6 @@ test.describe("loader", () => { `, "app/routes/_index.tsx": js` - import { json } from "react-router"; - export function loader() { return "${INDEX_DATA}" } @@ -103,10 +100,8 @@ test.describe("loader in an app", () => { `, "app/routes/fetch-target.tsx": js` - import { json } from "react-router"; - export function loader() { - return json({ message: "${FETCH_TARGET_TEXT}" }) + return Response.json({ message: "${FETCH_TARGET_TEXT}" }) } `, }, diff --git a/integration/matches-test.ts b/integration/matches-test.ts index 9dfafb1738..dbe292d7ca 100644 --- a/integration/matches-test.ts +++ b/integration/matches-test.ts @@ -17,10 +17,9 @@ test.describe("useMatches", () => { files: { "app/root.tsx": js` import * as React from 'react'; - import { json } from "react-router"; import { Link, Links, Meta, Outlet, Scripts, useMatches } from "react-router"; export const handle = { stuff: "root handle"}; - export const loader = () => json("ROOT"); + export const loader = () => "ROOT"; export default function Root() { let matches = useMatches(); let [matchesCount, setMatchesCount] = React.useState(0); @@ -47,20 +46,18 @@ test.describe("useMatches", () => { `, "app/routes/_index.tsx": js` - import { json } from "react-router"; export const handle = { stuff: "index handle"}; - export const loader = () => json("INDEX"); + export const loader = () => "INDEX"; export default function Index() { return

Index Page

} `, "app/routes/about.tsx": js` - import { json } from "react-router"; export const handle = { stuff: "about handle"}; export const loader = async () => { await new Promise(r => setTimeout(r, 100)); - return json("ABOUT"); + return "ABOUT"; } export default function About() { return

About Page

diff --git a/integration/multiple-cookies-test.ts b/integration/multiple-cookies-test.ts index 979a545987..96eabed064 100644 --- a/integration/multiple-cookies-test.ts +++ b/integration/multiple-cookies-test.ts @@ -16,21 +16,21 @@ test.describe("pathless layout routes", () => { await createFixture({ files: { "app/routes/_index.tsx": js` - import { redirect, json } from "react-router"; + import { data, redirect } from "react-router"; import { Form, useActionData } from "react-router"; export let loader = async () => { let headers = new Headers(); headers.append("Set-Cookie", "foo=bar"); headers.append("Set-Cookie", "bar=baz"); - return json({}, { headers }); + return data({}, { headers }); }; export let action = async () => { let headers = new Headers(); headers.append("Set-Cookie", "another=one"); headers.append("Set-Cookie", "how-about=two"); - return json({success: true}, { headers }); + return data({success: true}, { headers }); }; export default function MultipleSetCookiesPage() { diff --git a/integration/remix-serve-test.ts b/integration/remix-serve-test.ts index 6fb920740c..4850326e46 100644 --- a/integration/remix-serve-test.ts +++ b/integration/remix-serve-test.ts @@ -23,11 +23,10 @@ test.beforeAll(async () => { useReactRouterServe: true, files: { "app/routes/_index.tsx": js` - import { json } from "react-router"; import { useLoaderData, Link } from "react-router"; export function loader() { - return json("pizza"); + return "pizza"; } export default function Index() { diff --git a/integration/request-test.ts b/integration/request-test.ts index 88dc4825f1..e75c024edc 100644 --- a/integration/request-test.ts +++ b/integration/request-test.ts @@ -15,7 +15,6 @@ test.beforeAll(async () => { fixture = await createFixture({ files: { "app/routes/_index.tsx": js` - import { json } from "react-router"; import { Form, useLoaderData, useActionData } from "react-router"; async function requestToJson(request) { @@ -26,12 +25,12 @@ test.beforeAll(async () => { body = Object.fromEntries(fd.entries()); } - return json({ + return { method: request.method, url: request.url, headers: Object.fromEntries(request.headers.entries()), body, - }); + }; } export async function loader({ request }) { return requestToJson(request); diff --git a/integration/resource-routes-test.ts b/integration/resource-routes-test.ts index 842edd0108..52f4928866 100644 --- a/integration/resource-routes-test.ts +++ b/integration/resource-routes-test.ts @@ -61,25 +61,21 @@ test.describe("loader in an app", async () => { export let loader = () => ({ data: 'whatever' }); `, "app/routes/data[.]json.tsx": js` - import { json } from "react-router"; - export let loader = () => json({hello: "world"}); + export let loader = () => Response.json({ hello: "world" }); `, "app/assets/icon.svg": SVG_CONTENTS, "app/routes/[manifest.webmanifest].tsx": js` - import { json } from "react-router"; import iconUrl from "~/assets/icon.svg"; export function loader() { - return json( - { - icons: [ - { - src: iconUrl, - sizes: '48x48 72x72 96x96 128x128 192x192 256x256 512x512', - type: 'image/svg+xml', - }, - ], - }, - ); + return { + icons: [ + { + src: iconUrl, + sizes: '48x48 72x72 96x96 128x128 192x192 256x256 512x512', + type: 'image/svg+xml', + }, + ], + }; } `, "app/routes/throw-error.tsx": js` @@ -108,16 +104,17 @@ test.describe("loader in an app", async () => { } `, "app/routes/no-action.tsx": js` - import { json } from "react-router"; export let loader = () => { - return json({ ok: true }); + return { ok: true }; } `, "app/routes/$.tsx": js` import { json } from "react-router"; import { useRouteError } from "react-router"; export function loader({ request }) { - throw json({ message: new URL(request.url).pathname + ' not found' }, { + throw Response.json({ + message: new URL(request.url).pathname + ' not found' + }, { status: 404 }); } diff --git a/integration/revalidate-test.ts b/integration/revalidate-test.ts index 11c6bf330a..96f7ef6228 100644 --- a/integration/revalidate-test.ts +++ b/integration/revalidate-test.ts @@ -46,7 +46,7 @@ test.describe("Revalidation", () => { `, "app/routes/parent.tsx": js` - import { json } from "react-router"; + import { data } from "react-router"; import { Outlet, useLoaderData } from "react-router"; export async function loader({ request }) { @@ -57,7 +57,7 @@ test.describe("Revalidation", () => { .find(c => c.startsWith('parent=')) let strValue = (cookie || 'parent=0').split("=")[1]; let value = parseInt(strValue, 10) + 1; - return json({ value }, { + return data({ value }, { headers: { "Set-Cookie": "parent=" + value, } @@ -86,11 +86,11 @@ test.describe("Revalidation", () => { `, "app/routes/parent.child.tsx": js` - import { json } from "react-router"; + import { data } from "react-router"; import { Form, useLoaderData, useRevalidator } from "react-router"; export async function action() { - return json({ action: 'data' }) + return { action: 'data' } } export async function loader({ request }) { @@ -101,7 +101,7 @@ test.describe("Revalidation", () => { .find(c => c.startsWith('child=')) let strValue = (cookie || 'child=0').split("=")[1]; let value = parseInt(strValue, 10) + 1; - return json({ value }, { + return data({ value }, { headers: { "Set-Cookie": "child=" + value, } diff --git a/integration/set-cookie-revalidation-test.ts b/integration/set-cookie-revalidation-test.ts index c6a0f74925..b1c965d6c1 100644 --- a/integration/set-cookie-revalidation-test.ts +++ b/integration/set-cookie-revalidation-test.ts @@ -34,7 +34,7 @@ test.describe("set-cookie revalidation", () => { "app/root.tsx": js` import { - json, + data, Links, Meta, Outlet, @@ -48,7 +48,7 @@ test.describe("set-cookie revalidation", () => { let session = await sessionStorage.getSession(request.headers.get("Cookie")); let message = session.get(MESSAGE_KEY) || null; - return json(message, { + return data(message, { headers: { "Set-Cookie": await sessionStorage.commitSession(session), }, diff --git a/integration/transition-test.ts b/integration/transition-test.ts index a1dff0e991..1521257e6b 100644 --- a/integration/transition-test.ts +++ b/integration/transition-test.ts @@ -147,7 +147,7 @@ test.describe("rendering", () => { }; export const loader = async () => { - return json({}); + return {}; }; export default function GitHubIssue1691() { diff --git a/integration/vite-build-test.ts b/integration/vite-build-test.ts index 84ba75739c..27d7ab5914 100644 --- a/integration/vite-build-test.ts +++ b/integration/vite-build-test.ts @@ -63,12 +63,11 @@ test.beforeAll(async () => { `, "app/routes/_index.tsx": js` import { useState, useEffect } from "react"; - import { json } from "react-router"; import { serverOnly1, serverOnly2 } from "../utils.server"; export const loader = () => { - return json({ serverOnly1 }) + return { serverOnly1 } } export const action = () => { @@ -95,12 +94,10 @@ test.beforeAll(async () => { export const serverOnly2 = "SERVER_ONLY_2" `, "app/routes/resource.ts": js` - import { json } from "react-router"; - import { serverOnly1, serverOnly2 } from "../utils.server"; export const loader = () => { - return json({ serverOnly1 }) + return { serverOnly1 } } export const action = () => { @@ -110,16 +107,15 @@ test.beforeAll(async () => { `, "app/routes/mdx.mdx": js` import { useEffect, useState } from "react"; - import { json } from "react-router"; import { useLoaderData } from "react-router"; import { serverOnly1, serverOnly2 } from "../utils.server"; export const loader = () => { - return json({ + return { serverOnly1, content: "MDX route content from loader", - }) + } } export const action = () => { @@ -170,13 +166,12 @@ test.beforeAll(async () => { } `, "app/routes/dotenv.tsx": js` - import { json } from "react-router"; import { useLoaderData } from "react-router"; export const loader = () => { - return json({ + return { loaderContent: process.env.ENV_VAR_FROM_DOTENV_FILE ?? '.env file was NOT loaded, which is a good thing', - }) + } } export default function DotenvRoute() { diff --git a/integration/vite-cloudflare-test.ts b/integration/vite-cloudflare-test.ts index fe27dc9424..8a017eb19d 100644 --- a/integration/vite-cloudflare-test.ts +++ b/integration/vite-cloudflare-test.ts @@ -69,7 +69,6 @@ const files: Files = async ({ port }) => ({ import { type LoaderFunctionArgs, type ActionFunctionArgs, - json, Form, useLoaderData, } from "react-router"; @@ -79,7 +78,7 @@ const files: Files = async ({ port }) => ({ export async function loader({ context }: LoaderFunctionArgs) { const { MY_KV } = context.cloudflare.env; const value = await MY_KV.get(key); - return json({ value, extra: context.extra }); + return { value, extra: context.extra }; } export async function action({ request, context }: ActionFunctionArgs) { diff --git a/integration/vite-dev-test.ts b/integration/vite-dev-test.ts index b250e6ec2e..7c84726eed 100644 --- a/integration/vite-dev-test.ts +++ b/integration/vite-dev-test.ts @@ -102,10 +102,12 @@ const files: Files = async ({ port }) => ({ }; `, "app/routes/get-cookies.tsx": js` - import { json, LoaderFunctionArgs } from "react-router"; + import type { LoaderFunctionArgs } from "react-router"; import { useLoaderData } from "react-router" - export const loader = ({ request }: LoaderFunctionArgs) => json({cookies: request.headers.get("Cookie")}); + export const loader = ({ request }: LoaderFunctionArgs) => ({ + cookies: request.headers.get("Cookie") + }); export default function IndexRoute() { const { cookies } = useLoaderData(); @@ -128,13 +130,12 @@ const files: Files = async ({ port }) => ({ } `, "app/routes/mdx.mdx": js` - import { json } from "react-router"; import { useLoaderData } from "react-router"; export const loader = () => { - return json({ + return { content: "MDX route content from loader", - }) + } } export function MdxComponent() { @@ -151,13 +152,12 @@ const files: Files = async ({ port }) => ({ `, "app/routes/dotenv.tsx": js` import { useState, useEffect } from "react"; - import { json } from "react-router"; import { useLoaderData } from "react-router"; export const loader = () => { - return json({ + return { loaderContent: process.env.ENV_VAR_FROM_DOTENV_FILE, - }) + } } export default function DotenvRoute() { diff --git a/integration/vite-dot-server-test.ts b/integration/vite-dot-server-test.ts index ed9e1bc8ae..366dafb6c4 100644 --- a/integration/vite-dot-server-test.ts +++ b/integration/vite-dot-server-test.ts @@ -45,7 +45,6 @@ test("Vite / dead-code elimination for server exports", async () => { "app/.server/utils.ts": serverOnlyModule, "app/routes/remove-server-exports-and-dce.tsx": ` import fs from "node:fs"; - import { json } from "react-router"; import { useLoaderData } from "react-router"; import { serverOnly as serverOnlyFile } from "../utils.server"; @@ -53,7 +52,7 @@ test("Vite / dead-code elimination for server exports", async () => { export const loader = () => { let contents = fs.readFileSync("server_only.txt"); - return json({ serverOnlyFile, serverOnlyDir, contents }) + return { serverOnlyFile, serverOnlyDir, contents } } export const action = () => { diff --git a/integration/vite-dotenv-test.ts b/integration/vite-dotenv-test.ts index 4d2058c604..dd27b43520 100644 --- a/integration/vite-dotenv-test.ts +++ b/integration/vite-dotenv-test.ts @@ -14,13 +14,12 @@ let files = { `, "app/routes/dotenv.tsx": String.raw` import { useState, useEffect } from "react"; - import { json } from "react-router"; import { useLoaderData } from "react-router"; export const loader = () => { - return json({ + return { loaderContent: process.env.ENV_VAR_FROM_DOTENV_FILE, - }) + } } export default function DotenvRoute() { diff --git a/integration/vite-hmr-hdr-test.ts b/integration/vite-hmr-hdr-test.ts index ff6be438e9..f86ad66b8d 100644 --- a/integration/vite-hmr-hdr-test.ts +++ b/integration/vite-hmr-hdr-test.ts @@ -162,11 +162,11 @@ async function workflow({ contents .replace( "// imports", - `// imports\nimport { json } from "react-router";\nimport { useLoaderData } from "react-router"` + `// imports\nimport { useLoaderData } from "react-router"` ) .replace( "// loader", - `// loader\nexport const loader = () => json({ message: "HDR updated: 0" });` + `// loader\nexport const loader = () => ({ message: "HDR updated: 0" });` ) .replace( "// hooks", @@ -279,8 +279,8 @@ async function workflow({ `// imports\nimport { direct } from "../direct-hdr-dep"` ) .replace( - `json({ message: "HDR updated: 2" })`, - `json({ message: "HDR updated: " + direct })` + `{ message: "HDR updated: 2" }`, + `{ message: "HDR updated: " + direct }` ) ); await page.waitForLoadState("networkidle"); diff --git a/integration/vite-loader-context-test.ts b/integration/vite-loader-context-test.ts index 0aa17080fe..dc69733b25 100644 --- a/integration/vite-loader-context-test.ts +++ b/integration/vite-loader-context-test.ts @@ -18,11 +18,10 @@ test.beforeAll(async () => { "vite.config.js": await viteConfig.basic({ port }), "server.mjs": EXPRESS_SERVER({ port, loadContext: { value: "value" } }), "app/routes/_index.tsx": String.raw` - import { json } from "react-router"; import { useLoaderData } from "react-router"; export const loader = ({ context }) => { - return json({ context }) + return { context } } export default function IndexRoute() { diff --git a/packages/react-router/__tests__/data-router-no-dom-test.tsx b/packages/react-router/__tests__/data-router-no-dom-test.tsx index 376891fb01..b8125c7cea 100644 --- a/packages/react-router/__tests__/data-router-no-dom-test.tsx +++ b/packages/react-router/__tests__/data-router-no-dom-test.tsx @@ -53,6 +53,8 @@ describe("RouterProvider works when no DOM APIs are available", () => { }); it("is defensive against a view transition navigation", async () => { + let warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {}); + let router = createMemoryRouter([ { path: "/", @@ -121,6 +123,14 @@ describe("RouterProvider works when no DOM APIs are available", () => { }, }); + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy).toHaveBeenCalledWith( + "You provided the `viewTransition` option to a router update, but you do " + + "not appear to be running in a DOM environment as `window.startViewTransition` " + + "is not available." + ); + warnSpy.mockRestore(); + unsubscribe(); }); diff --git a/packages/react-router/__tests__/dom/data-static-router-test.tsx b/packages/react-router/__tests__/dom/data-static-router-test.tsx index d1bdbde1c6..9a738d1a90 100644 --- a/packages/react-router/__tests__/dom/data-static-router-test.tsx +++ b/packages/react-router/__tests__/dom/data-static-router-test.tsx @@ -4,7 +4,6 @@ import * as React from "react"; import * as ReactDOMServer from "react-dom/server"; -import { json } from "react-router"; import type { StaticHandlerContext } from "../../index"; import { Form, @@ -638,7 +637,7 @@ describe("A ", () => { { path: "/", loader: () => { - throw json( + throw Response.json( { not: "found" }, { status: 404, statusText: "Not Found" } ); @@ -688,7 +687,7 @@ describe("A ", () => { path: "/", lazy: async () => ({ loader: () => { - throw json( + throw Response.json( { not: "found" }, { status: 404, statusText: "Not Found" } ); diff --git a/packages/react-router/__tests__/dom/stub-test.tsx b/packages/react-router/__tests__/dom/stub-test.tsx index f7862ef4d2..015d803cb8 100644 --- a/packages/react-router/__tests__/dom/stub-test.tsx +++ b/packages/react-router/__tests__/dom/stub-test.tsx @@ -8,7 +8,6 @@ import { useFetcher, useLoaderData, useMatches, - json, createRoutesStub, } from "../../index"; @@ -62,7 +61,7 @@ test("loaders work", async () => { return
Message: {data.message}
; }, loader() { - return json({ message: "hello" }); + return Response.json({ message: "hello" }); }, }, ]); @@ -87,7 +86,7 @@ test("actions work", async () => { ); }, action() { - return json({ message: "hello" }); + return Response.json({ message: "hello" }); }, }, ]); @@ -116,7 +115,7 @@ test("fetchers work", async () => { { path: "/api", loader() { - return json({ count: ++count }); + return Response.json({ count: ++count }); }, }, ]); @@ -133,7 +132,7 @@ test("fetchers work", async () => { // eslint-disable-next-line jest/expect-expect test("can pass a predefined loader", () => { async function loader(_args: LoaderFunctionArgs) { - return json({ hi: "there" }); + return Response.json({ hi: "there" }); } createRoutesStub([ @@ -160,7 +159,7 @@ test("can pass context values", async () => { ); }, loader({ context }) { - return json(context); + return Response.json(context); }, children: [ { @@ -170,7 +169,7 @@ test("can pass context values", async () => { return
Context: {data.context}
; }, loader({ context }) { - return json(context); + return Response.json(context); }, }, ], diff --git a/packages/react-router/__tests__/dom/use-blocker-test.tsx b/packages/react-router/__tests__/dom/use-blocker-test.tsx index 5db0e10aca..97fec448cf 100644 --- a/packages/react-router/__tests__/dom/use-blocker-test.tsx +++ b/packages/react-router/__tests__/dom/use-blocker-test.tsx @@ -4,7 +4,6 @@ import { act } from "react-dom/test-utils"; import type { Blocker, RouteObject } from "../../index"; import { createMemoryRouter, - json, Link, NavLink, Outlet, @@ -19,7 +18,7 @@ const LOADER_LATENCY_MS = 200; async function slowLoader() { await sleep(LOADER_LATENCY_MS / 2); - return json(null); + return Response.json(null); } describe("navigation blocking with useBlocker", () => { diff --git a/packages/react-router/__tests__/router/data-strategy-test.ts b/packages/react-router/__tests__/router/data-strategy-test.ts index 501981ac62..cc6aed6640 100644 --- a/packages/react-router/__tests__/router/data-strategy-test.ts +++ b/packages/react-router/__tests__/router/data-strategy-test.ts @@ -3,7 +3,6 @@ import type { DataStrategyMatch, DataStrategyResult, } from "../../lib/router/utils"; -import { json } from "../../lib/router/utils"; import { createDeferred, setup } from "./utils/data-router-setup"; import { createFormData, tick } from "./utils/utils"; @@ -63,7 +62,7 @@ describe("router dataStrategy", () => { expect(A.loaders.json.stub).toHaveBeenCalledTimes(1); expect(A.loaders.text.stub).toHaveBeenCalledTimes(1); - await A.loaders.json.resolve(json({ message: "hello json" })); + await A.loaders.json.resolve({ message: "hello json" }); await A.loaders.text.resolve(new Response("hello text")); expect(t.router.state.loaderData).toEqual({ @@ -597,7 +596,7 @@ describe("router dataStrategy", () => { formData: createFormData({}), }); - await A.actions.json.resolve(json({ message: "hello json" })); + await A.actions.json.resolve({ message: "hello json" }); expect(t.router.state.actionData).toEqual({ json: { message: "hello json" }, @@ -686,7 +685,7 @@ describe("router dataStrategy", () => { let key = "key"; let A = await t.fetch("/test", key); - await A.loaders.json.resolve(json({ message: "hello json" })); + await A.loaders.json.resolve({ message: "hello json" }); expect(t.fetchers[key].data.message).toBe("hello json"); @@ -772,7 +771,7 @@ describe("router dataStrategy", () => { formData: createFormData({}), }); - await A.actions.json.resolve(json({ message: "hello json" })); + await A.actions.json.resolve({ message: "hello json" }); expect(t.fetchers[key].data.message).toBe("hello json"); @@ -881,7 +880,7 @@ describe("router dataStrategy", () => { }); let A = await t.navigate("/test"); - await A.loaders.json.resolve(json({ message: "hello json" })); + await A.loaders.json.resolve({ message: "hello json" }); await A.loaders.reverse.resolve( new Response("hello text", { headers: { "Content-Type": "application/reverse" }, diff --git a/packages/react-router/__tests__/router/lazy-test.ts b/packages/react-router/__tests__/router/lazy-test.ts index 9f6923acce..c53671b2dc 100644 --- a/packages/react-router/__tests__/router/lazy-test.ts +++ b/packages/react-router/__tests__/router/lazy-test.ts @@ -1,6 +1,5 @@ import { createMemoryHistory } from "../../lib/router/history"; import { createRouter, createStaticHandler } from "../../lib/router/router"; -import { json } from "../../lib/router/utils"; import type { TestRouteObject } from "./utils/data-router-setup"; import { cleanup, createDeferred, setup } from "./utils/data-router-setup"; @@ -210,7 +209,7 @@ describe("lazily loaded route modules", () => { await tick(); return { async loader() { - return json({ value: "LAZY LOADER" }); + return Response.json({ value: "LAZY LOADER" }); }, }; }, @@ -234,7 +233,7 @@ describe("lazily loaded route modules", () => { await tick(); return { async loader() { - return json({ value: "LAZY LOADER" }); + return Response.json({ value: "LAZY LOADER" }); }, }; }, @@ -429,7 +428,7 @@ describe("lazily loaded route modules", () => { let consoleWarn = jest.spyOn(console, "warn"); let lazyLoaderStub = jest.fn(async () => { await tick(); - return json({ value: "LAZY LOADER" }); + return Response.json({ value: "LAZY LOADER" }); }); let { query } = createStaticHandler([ @@ -438,7 +437,7 @@ describe("lazily loaded route modules", () => { path: "/lazy", loader: async () => { await tick(); - return json({ value: "STATIC LOADER" }); + return Response.json({ value: "STATIC LOADER" }); }, lazy: async () => { await tick(); @@ -470,7 +469,7 @@ describe("lazily loaded route modules", () => { let consoleWarn = jest.spyOn(console, "warn"); let lazyLoaderStub = jest.fn(async () => { await tick(); - return json({ value: "LAZY LOADER" }); + return Response.json({ value: "LAZY LOADER" }); }); let { query } = createStaticHandler([ @@ -479,7 +478,7 @@ describe("lazily loaded route modules", () => { path: "/lazy", loader: async () => { await tick(); - return json({ value: "STATIC LOADER" }); + return Response.json({ value: "STATIC LOADER" }); }, lazy: async () => { await tick(); diff --git a/packages/react-router/__tests__/router/navigation-test.ts b/packages/react-router/__tests__/router/navigation-test.ts index 30bb17a75f..9210490c91 100644 --- a/packages/react-router/__tests__/router/navigation-test.ts +++ b/packages/react-router/__tests__/router/navigation-test.ts @@ -1,5 +1,4 @@ import type { HydrationState } from "../../lib/router/router"; -import { json } from "../../lib/router/utils"; import { cleanup, setup } from "./utils/data-router-setup"; import { createFormData } from "./utils/utils"; @@ -104,10 +103,12 @@ describe("navigations", () => { }); }); - it("unwraps non-redirect json Responses (json helper)", async () => { + it("unwraps non-redirect json Responses (Response.json() helper)", async () => { let t = initializeTest(); let A = await t.navigate("/foo"); - await A.loaders.foo.resolve(json({ key: "value" }, 200)); + await A.loaders.foo.resolve( + Response.json({ key: "value" }, { status: 200 }) + ); expect(t.router.state.loaderData).toMatchObject({ root: "ROOT", foo: { key: "value" }, diff --git a/packages/react-router/__tests__/router/ssr-test.ts b/packages/react-router/__tests__/router/ssr-test.ts index fb58a9a16d..a38afa1f6e 100644 --- a/packages/react-router/__tests__/router/ssr-test.ts +++ b/packages/react-router/__tests__/router/ssr-test.ts @@ -14,7 +14,6 @@ import { import { ErrorResponseImpl, isRouteErrorResponse, - json, redirect, } from "../../lib/router/utils"; import { createDeferred } from "./utils/data-router-setup"; @@ -60,8 +59,8 @@ describe("ssr", () => { { id: "json", path: "json", - loader: () => json({ type: "loader" }), - action: () => json({ type: "action" }), + loader: () => Response.json({ type: "loader" }), + action: () => Response.json({ type: "action" }), }, { id: "deferred", @@ -1069,7 +1068,7 @@ describe("ssr", () => { { id: "root", path: "/", - loader: () => json({ data: "ROOT" }, { status: 201 }), + loader: () => Response.json({ data: "ROOT" }, { status: 201 }), children: [ { id: "child", @@ -1090,12 +1089,12 @@ describe("ssr", () => { { id: "root", path: "/", - loader: () => json({ data: "ROOT" }, { status: 201 }), + loader: () => Response.json({ data: "ROOT" }, { status: 201 }), children: [ { id: "child", index: true, - loader: () => json({ data: "CHILD" }, { status: 202 }), + loader: () => Response.json({ data: "CHILD" }, { status: 202 }), action: () => { throw new Error("πŸ’₯"); }, @@ -1114,7 +1113,7 @@ describe("ssr", () => { { id: "root", path: "/", - loader: () => json({ data: "ROOT" }, { status: 201 }), + loader: () => Response.json({ data: "ROOT" }, { status: 201 }), children: [ { id: "child", @@ -1135,12 +1134,12 @@ describe("ssr", () => { { id: "root", path: "/", - loader: () => json({ data: "ROOT" }, { status: 201 }), + loader: () => Response.json({ data: "ROOT" }, { status: 201 }), children: [ { id: "child", index: true, - loader: () => json({ data: "CHILD" }, { status: 202 }), + loader: () => Response.json({ data: "CHILD" }, { status: 202 }), action: () => { throw new Response(null, { status: 400 }); }, @@ -1159,13 +1158,13 @@ describe("ssr", () => { { id: "root", path: "/", - loader: () => json({ data: "ROOT" }, { status: 201 }), + loader: () => Response.json({ data: "ROOT" }, { status: 201 }), children: [ { id: "child", index: true, - loader: () => json({ data: "ROOT" }, { status: 202 }), - action: () => json({ data: "ROOT" }, { status: 203 }), + loader: () => Response.json({ data: "ROOT" }, { status: 202 }), + action: () => Response.json({ data: "ROOT" }, { status: 203 }), }, ], }, @@ -1181,12 +1180,12 @@ describe("ssr", () => { { id: "root", path: "/", - loader: () => json({ data: "ROOT" }, { status: 201 }), + loader: () => Response.json({ data: "ROOT" }, { status: 201 }), children: [ { id: "child", index: true, - loader: () => json({ data: "ROOT" }, { status: 202 }), + loader: () => Response.json({ data: "ROOT" }, { status: 202 }), }, ], }, @@ -1944,7 +1943,7 @@ describe("ssr", () => { }); it("should not unwrap responses returned from loaders", async () => { - let response = json({ key: "value" }); + let response = Response.json({ key: "value" }); let { queryRoute } = createStaticHandler([ { id: "root", @@ -1959,7 +1958,7 @@ describe("ssr", () => { }); it("should not unwrap responses returned from actions", async () => { - let response = json({ key: "value" }); + let response = Response.json({ key: "value" }); let { queryRoute } = createStaticHandler([ { id: "root", @@ -1974,7 +1973,7 @@ describe("ssr", () => { }); it("should not unwrap responses thrown from loaders", async () => { - let response = json({ key: "value" }); + let response = Response.json({ key: "value" }); let { queryRoute } = createStaticHandler([ { id: "root", @@ -1994,7 +1993,7 @@ describe("ssr", () => { }); it("should not unwrap responses thrown from actions", async () => { - let response = json({ key: "value" }); + let response = Response.json({ key: "value" }); let { queryRoute } = createStaticHandler([ { id: "root", diff --git a/packages/react-router/__tests__/server-runtime/handle-error-test.ts b/packages/react-router/__tests__/server-runtime/handle-error-test.ts index 5715268ce6..b7d0f0f8cf 100644 --- a/packages/react-router/__tests__/server-runtime/handle-error-test.ts +++ b/packages/react-router/__tests__/server-runtime/handle-error-test.ts @@ -1,6 +1,5 @@ import type { ServerBuild } from "../../lib/server-runtime/build"; import { createRequestHandler } from "../../lib/server-runtime/server"; -import { json } from "../../lib/server-runtime/responses"; import { ErrorResponseImpl } from "../../lib/router/utils"; function getHandler(routeModule = {}, entryServerModule = {}) { @@ -90,7 +89,7 @@ describe("handleError", () => { it("does not provide user-thrown Responses to handleError", async () => { let { handler, handleErrorSpy } = getHandler({ loader() { - throw json( + throw Response.json( { message: "not found" }, { status: 404, statusText: "Not Found" } ); @@ -145,7 +144,7 @@ describe("handleError", () => { it("does not provide user-thrown Responses to handleError", async () => { let { handler, handleErrorSpy } = getHandler({ loader() { - throw json( + throw Response.json( { message: "not found" }, { status: 404, statusText: "Not Found" } ); @@ -201,7 +200,7 @@ describe("handleError", () => { it("does not provide user-thrown Responses to handleError", async () => { let { handler, handleErrorSpy } = getHandler({ loader() { - throw json( + throw Response.json( { message: "not found" }, { status: 404, statusText: "Not Found" } ); diff --git a/packages/react-router/__tests__/server-runtime/handler-test.ts b/packages/react-router/__tests__/server-runtime/handler-test.ts index 01fefa9577..17f56aa69a 100644 --- a/packages/react-router/__tests__/server-runtime/handler-test.ts +++ b/packages/react-router/__tests__/server-runtime/handler-test.ts @@ -1,4 +1,3 @@ -import { json } from "../../lib/server-runtime/responses"; import { createRequestHandler } from "../../lib/server-runtime/server"; describe("createRequestHandler", () => { @@ -9,7 +8,8 @@ describe("createRequestHandler", () => { id: "routes/test", path: "/test", module: { - loader: ({ request }) => json(request.headers.get("X-Foo")), + loader: ({ request }) => + Response.json(request.headers.get("X-Foo")), } as any, }, }, diff --git a/packages/react-router/__tests__/server-runtime/responses-test.ts b/packages/react-router/__tests__/server-runtime/responses-test.ts index f023d8c065..44bb4ab8f4 100644 --- a/packages/react-router/__tests__/server-runtime/responses-test.ts +++ b/packages/react-router/__tests__/server-runtime/responses-test.ts @@ -2,20 +2,16 @@ * @jest-environment node */ -import type { TypedResponse } from "../../lib/server-runtime/responses"; -import { json, redirect } from "../../lib/server-runtime/responses"; -import { isEqual } from "./utils"; +import { redirect } from "../../lib/router/utils"; describe("json", () => { it("sets the Content-Type header", () => { - let response = json({}); - expect(response.headers.get("Content-Type")).toEqual( - "application/json; charset=utf-8" - ); + let response = Response.json({}); + expect(response.headers.get("Content-Type")).toEqual("application/json"); }); it("preserves existing headers, including Content-Type", () => { - let response = json( + let response = Response.json( {}, { headers: { @@ -32,33 +28,26 @@ describe("json", () => { }); it("encodes the response body", async () => { - let response = json({ hello: "remix" }); + let response = Response.json({ hello: "remix" }); expect(await response.json()).toEqual({ hello: "remix" }); }); it("accepts status as a second parameter", () => { - let response = json({}, 201); + let response = Response.json({}, { status: 201 }); expect(response.status).toEqual(201); }); it("infers input type", async () => { - let response = json({ hello: "remix" }); - isEqual>(true); + let response = Response.json({ hello: "remix" }); let result = await response.json(); expect(result).toMatchObject({ hello: "remix" }); }); - // eslint-disable-next-line jest/expect-expect - it("disallows unmatched typed responses", async () => { - let response = json("hello"); - isEqual, typeof response>(false); - }); - it("disallows unserializables", () => { // @ts-expect-error - expect(() => json(124n)).toThrow(); + expect(() => Response.json(124n)).toThrow(); // @ts-expect-error - expect(() => json({ field: 124n })).toThrow(); + expect(() => Response.json({ field: 124n })).toThrow(); }); }); diff --git a/packages/react-router/__tests__/server-runtime/server-test.ts b/packages/react-router/__tests__/server-runtime/server-test.ts index d4e749285c..ca5bd2f875 100644 --- a/packages/react-router/__tests__/server-runtime/server-test.ts +++ b/packages/react-router/__tests__/server-runtime/server-test.ts @@ -3,7 +3,6 @@ */ import type { StaticHandlerContext } from "react-router"; -import { json } from "react-router"; import { createRequestHandler } from "../../lib/server-runtime/server"; import { ServerMode } from "../../lib/server-runtime/mode"; @@ -133,7 +132,7 @@ describe("shared server runtime", () => { return "root"; }); let resourceLoader = jest.fn(() => { - return json("resource"); + return Response.json("resource"); }); let build = mockServerBuild({ root: { @@ -163,10 +162,10 @@ describe("shared server runtime", () => { return "root"; }); let resourceLoader = jest.fn(() => { - return json("resource"); + return Response.json("resource"); }); let subResourceLoader = jest.fn(() => { - return json("sub"); + return Response.json("sub"); }); let build = mockServerBuild({ root: { @@ -276,7 +275,7 @@ describe("shared server runtime", () => { return "root"; }); let resourceAction = jest.fn(() => { - return json("resource"); + return Response.json("resource"); }); let build = mockServerBuild({ root: { @@ -306,10 +305,10 @@ describe("shared server runtime", () => { return "root"; }); let resourceAction = jest.fn(() => { - return json("resource"); + return Response.json("resource"); }); let subResourceAction = jest.fn(() => { - return json("sub"); + return Response.json("sub"); }); let build = mockServerBuild({ root: { diff --git a/packages/react-router/index.ts b/packages/react-router/index.ts index 244785d346..0e63a1d4b0 100644 --- a/packages/react-router/index.ts +++ b/packages/react-router/index.ts @@ -60,7 +60,6 @@ export { data, generatePath, isRouteErrorResponse, - json, matchPath, matchRoutes, redirect, @@ -231,11 +230,6 @@ export { createMemorySessionStorage } from "./lib/server-runtime/sessions/memory export { setDevServerHooks as unstable_setDevServerHooks } from "./lib/server-runtime/dev"; export type { IsCookieFunction } from "./lib/server-runtime/cookies"; -// TODO: (v7) Clean up code paths for these exports -// export type { -// JsonFunction, -// RedirectFunction, -// } from "./lib/server-runtime/responses"; export type { CreateRequestHandlerFunction } from "./lib/server-runtime/server"; export type { IsSessionFunction } from "./lib/server-runtime/sessions"; @@ -263,11 +257,6 @@ export type { LinkDescriptor, } from "./lib/router/links"; -export type { - TypedResponse, - JsonFunction, -} from "./lib/server-runtime/responses"; - export type { // TODO: (v7) Clean up code paths for these exports // ActionFunction, diff --git a/packages/react-router/lib/dom/ssr/data.ts b/packages/react-router/lib/dom/ssr/data.ts index 8f622e69cf..0705ce47a5 100644 --- a/packages/react-router/lib/dom/ssr/data.ts +++ b/packages/react-router/lib/dom/ssr/data.ts @@ -5,16 +5,6 @@ import "../global"; */ export type AppData = unknown; -export function isResponse(value: any): value is Response { - return ( - value != null && - typeof value.status === "number" && - typeof value.statusText === "string" && - typeof value.headers === "object" && - typeof value.body !== "undefined" - ); -} - export async function createRequestInit( request: Request ): Promise { diff --git a/packages/react-router/lib/dom/ssr/single-fetch.tsx b/packages/react-router/lib/dom/ssr/single-fetch.tsx index 8a08407046..0a04684019 100644 --- a/packages/react-router/lib/dom/ssr/single-fetch.tsx +++ b/packages/react-router/lib/dom/ssr/single-fetch.tsx @@ -1,6 +1,7 @@ import * as React from "react"; import { decode } from "turbo-stream"; import type { Router as DataRouter } from "../../router/router"; +import { isResponse } from "../../router/router"; import type { DataStrategyFunction, DataStrategyFunctionArgs, @@ -13,7 +14,7 @@ import { redirect, data, } from "../../router/utils"; -import { createRequestInit, isResponse } from "./data"; +import { createRequestInit } from "./data"; import type { AssetsManifest, EntryContext } from "./entry"; import { escapeHtml } from "./markup"; import type { RouteModules } from "./routeModules"; diff --git a/packages/react-router/lib/router/router.ts b/packages/react-router/lib/router/router.ts index 9d445c43c7..bd7f421a7c 100644 --- a/packages/react-router/lib/router/router.ts +++ b/packages/react-router/lib/router/router.ts @@ -730,7 +730,7 @@ const validRequestMethodsArr: FormMethod[] = [ ]; const validRequestMethods = new Set(validRequestMethodsArr); -const redirectStatusCodes = new Set([301, 302, 303, 307, 308]); +export const redirectStatusCodes = new Set([301, 302, 303, 307, 308]); const redirectPreserveMethodStatusCodes = new Set([307, 308]); export const IDLE_NAVIGATION: NavigationStates["Idle"] = { @@ -5388,7 +5388,8 @@ export function isDataWithResponseInit( value.type === "DataWithResponseInit" ); } -function isResponse(value: any): value is Response { + +export function isResponse(value: any): value is Response { return ( value != null && typeof value.status === "number" && @@ -5398,14 +5399,16 @@ function isResponse(value: any): value is Response { ); } -function isRedirectResponse(result: any): result is Response { - if (!isResponse(result)) { - return false; - } +export function isRedirectStatusCode(statusCode: number): boolean { + return redirectStatusCodes.has(statusCode); +} - let status = result.status; - let location = result.headers.get("Location"); - return status >= 300 && status <= 399 && location != null; +export function isRedirectResponse(result: any): result is Response { + return ( + isResponse(result) && + isRedirectStatusCode(result.status) && + result.headers.has("Location") + ); } function isValidMethod(method: string): method is FormMethod { diff --git a/packages/react-router/lib/router/utils.ts b/packages/react-router/lib/router/utils.ts index eeb91d30be..a667a4d791 100644 --- a/packages/react-router/lib/router/utils.ts +++ b/packages/react-router/lib/router/utils.ts @@ -1,4 +1,3 @@ -import type { JsonFunction } from "../server-runtime/responses"; import type { Location, Path, To } from "./history"; import { invariant, parsePath, warning } from "./history"; @@ -1306,26 +1305,6 @@ export const normalizeSearch = (search: string): string => export const normalizeHash = (hash: string): string => !hash || hash === "#" ? "" : hash.startsWith("#") ? hash : "#" + hash; -/** - * This is a shortcut for creating `application/json` responses. Converts `data` - * to JSON and sets the `Content-Type` header. - * - * @category Utils - */ -export const json: JsonFunction = (data, init = {}) => { - let responseInit = typeof init === "number" ? { status: init } : init; - - let headers = new Headers(responseInit.headers); - if (!headers.has("Content-Type")) { - headers.set("Content-Type", "application/json; charset=utf-8"); - } - - return new Response(JSON.stringify(data), { - ...responseInit, - headers, - }); -}; - export class DataWithResponseInit { type: string = "DataWithResponseInit"; data: D; @@ -1349,6 +1328,9 @@ export function data(data: D, init?: number | ResponseInit) { typeof init === "number" ? { status: init } : init ); } + +// This is now only used by the Await component and will eventually probably +// go away in favor of the format used by `React.use` export interface TrackedPromise extends Promise { _tracked?: boolean; _data?: any; diff --git a/packages/react-router/lib/server-runtime/data.ts b/packages/react-router/lib/server-runtime/data.ts index 8249c22946..6e80b95301 100644 --- a/packages/react-router/lib/server-runtime/data.ts +++ b/packages/react-router/lib/server-runtime/data.ts @@ -1,5 +1,4 @@ -import { isDataWithResponseInit } from "../router/router"; -import { isRedirectStatusCode } from "./responses"; +import { isDataWithResponseInit, isRedirectStatusCode } from "../router/router"; import type { ActionFunction, ActionFunctionArgs, diff --git a/packages/react-router/lib/server-runtime/responses.ts b/packages/react-router/lib/server-runtime/responses.ts deleted file mode 100644 index 635174296d..0000000000 --- a/packages/react-router/lib/server-runtime/responses.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { - json as routerJson, - redirect as routerRedirect, - redirectDocument as routerRedirectDocument, - replace as routerReplace, -} from "../router/utils"; - -export type JsonFunction = ( - data: Data, - init?: number | ResponseInit -) => TypedResponse; - -// must be a type since this is a subtype of response -// interfaces must conform to the types they extend -export type TypedResponse = Omit & { - json(): Promise; -}; - -/** - * This is a shortcut for creating `application/json` responses. Converts `data` - * to JSON and sets the `Content-Type` header. - * - * @see https://remix.run/utils/json - */ -export const json: JsonFunction = (data, init = {}) => { - return routerJson(data, init); -}; - -export type RedirectFunction = ( - url: string, - init?: number | ResponseInit -) => TypedResponse; - -/** - * A redirect response. Sets the status code and the `Location` header. - * Defaults to "302 Found". - * - * @see https://remix.run/utils/redirect - */ -export const redirect: RedirectFunction = (url, init = 302) => { - return routerRedirect(url, init) as TypedResponse; -}; - -/** - * A redirect response that will force a document reload to the new location. - * Sets the status code and the `Location` header. - * Defaults to "302 Found". - * - * @see https://remix.run/utils/redirect - */ -export const redirectDocument: RedirectFunction = (url, init = 302) => { - return routerRedirectDocument(url, init) as TypedResponse; -}; - -/** - * A redirect response. Sets the status code and the `Location` header. - * Defaults to "302 Found". - * - * @see https://remix.run/utils/redirect - */ -export const replace: RedirectFunction = (url, init = 302) => { - return routerReplace(url, init) as TypedResponse; -}; - -export function isResponse(value: any): value is Response { - return ( - value != null && - typeof value.status === "number" && - typeof value.statusText === "string" && - typeof value.headers === "object" && - typeof value.body !== "undefined" - ); -} - -const redirectStatusCodes = new Set([301, 302, 303, 307, 308]); -export function isRedirectStatusCode(statusCode: number): boolean { - return redirectStatusCodes.has(statusCode); -} -export function isRedirectResponse(response: Response): boolean { - return isRedirectStatusCode(response.status); -} diff --git a/packages/react-router/lib/server-runtime/routeModules.ts b/packages/react-router/lib/server-runtime/routeModules.ts index f1b3b198e9..16c1633f2f 100644 --- a/packages/react-router/lib/server-runtime/routeModules.ts +++ b/packages/react-router/lib/server-runtime/routeModules.ts @@ -129,21 +129,15 @@ export interface LinksFunction { * * ```ts * // app/root.tsx - * const loader = () => { - * return json({ hello: "world" } as const) - * } + * const loader = () => ({ hello: "world" }) * export type Loader = typeof loader * * // app/routes/sales.tsx - * const loader = () => { - * return json({ salesCount: 1074 }) - * } + * const loader = () => ({ salesCount: 1074 }) * export type Loader = typeof loader * * // app/routes/sales/customers.tsx - * const loader = () => { - * return json({ customerCount: 74 }) - * } + * const loader = () => ({ customerCount: 74 }) * export type Loader = typeof loader * * // app/routes/sales/customers/$customersId.tsx @@ -151,9 +145,7 @@ export interface LinksFunction { * import type { Loader as SalesLoader } from "../../sales" * import type { Loader as CustomersLoader } from "../../sales/customers" * - * const loader = () => { - * return json({ name: "Customer name" }) - * } + * const loader = () => ({ name: "Customer name" }) * * const meta: MetaFunction = T; @@ -33,13 +32,11 @@ type DataFrom = // prettier-ignore type ClientData = - T extends TypedResponse ? Jsonify : T extends DataWithResponseInit ? U : T // prettier-ignore type ServerData = - T extends TypedResponse ? Jsonify : T extends DataWithResponseInit ? Serialize : Serialize @@ -177,11 +174,12 @@ type __tests = [ Pretty< ServerDataFrom< () => - | TypedResponse<{ json: string; b: Date; c: () => boolean }> + | { json: string; b: Date; c: () => boolean } | DataWithResponseInit<{ data: string; b: Date; c: () => boolean }> > >, - { json: string; b: string } | { data: string; b: Date; c: undefined } + | { json: string; b: Date; c: undefined } + | { data: string; b: Date; c: undefined } > >, @@ -198,11 +196,12 @@ type __tests = [ Pretty< ClientDataFrom< () => - | TypedResponse<{ json: string; b: Date; c: () => boolean }> + | { json: string; b: Date; c: () => boolean } | DataWithResponseInit<{ data: string; b: Date; c: () => boolean }> > >, - { json: string; b: string } | { data: string; b: Date; c: () => boolean } + | { json: string; b: Date; c: () => boolean } + | { data: string; b: Date; c: () => boolean } > >, From 02b363c76dd1c5338bd5d0b9be4f6ebfbf21da29 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Fri, 18 Oct 2024 10:56:09 -0400 Subject: [PATCH 21/33] Fix lint errors, remove unused jsonify implementation (#12154) --- .../__tests__/dom/partial-hydration-test.tsx | 2 + packages/react-router/lib/components.tsx | 6 +- packages/react-router/lib/router/router.ts | 19 +- .../lib/server-runtime/headers.ts | 2 +- .../lib/server-runtime/jsonify.ts | 257 ------------------ packages/react-router/lib/types.ts | 1 - templates/basic/postcss.config.js | 4 +- 7 files changed, 10 insertions(+), 281 deletions(-) delete mode 100644 packages/react-router/lib/server-runtime/jsonify.ts diff --git a/packages/react-router/__tests__/dom/partial-hydration-test.tsx b/packages/react-router/__tests__/dom/partial-hydration-test.tsx index 27fafc232e..7f6c9e259d 100644 --- a/packages/react-router/__tests__/dom/partial-hydration-test.tsx +++ b/packages/react-router/__tests__/dom/partial-hydration-test.tsx @@ -88,6 +88,7 @@ describe("Partial Hydration Behavior", () => { } ); let { container } = render( + // eslint-disable-next-line react/jsx-pascal-case ); @@ -176,6 +177,7 @@ describe("Partial Hydration Behavior", () => { } ); let { container } = render( + // eslint-disable-next-line react/jsx-pascal-case ); diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx index 8caf152e6b..9aa96dd231 100644 --- a/packages/react-router/lib/components.tsx +++ b/packages/react-router/lib/components.tsx @@ -215,11 +215,7 @@ export function RouterProvider({ let setState = React.useCallback( ( newState: RouterState, - { - deletedFetchers, - flushSync: flushSync, - viewTransitionOpts: viewTransitionOpts, - } + { deletedFetchers, flushSync, viewTransitionOpts } ) => { deletedFetchers.forEach((key) => fetcherData.current.delete(key)); newState.fetchers.forEach((fetcher, key) => { diff --git a/packages/react-router/lib/router/router.ts b/packages/react-router/lib/router/router.ts index bd7f421a7c..0ec3d5438c 100644 --- a/packages/react-router/lib/router/router.ts +++ b/packages/react-router/lib/router/router.ts @@ -998,13 +998,6 @@ export function createRouter(init: RouterInit): Router { // we don't need to update UI state if they change let blockerFunctions = new Map(); - // Map of pending patchRoutesOnNavigation() promises (keyed by path/matches) so - // that we only kick them off once for a given combo - let pendingPatchRoutes = new Map< - string, - ReturnType - >(); - // Flag to ignore the next history update, so we can revert the URL change on // a POP navigation that was blocked by the user without touching router state let unblockBlockerHistoryUpdate: (() => void) | undefined = undefined; @@ -2750,7 +2743,7 @@ export function createRouter(init: RouterInit): Router { } for (let [routeId, result] of Object.entries(results)) { - if (isRedirectDataStrategyResultResult(result)) { + if (isRedirectDataStrategyResult(result)) { let response = result.result as Response; dataResults[routeId] = { type: ResultType.redirect, @@ -2779,8 +2772,6 @@ export function createRouter(init: RouterInit): Router { fetchersToLoad: RevalidatingFetcher[], request: Request ) { - let currentMatches = state.matches; - // Kick off loaders and fetchers in parallel let loaderResultsPromise = callDataStrategy( "loader", @@ -3861,7 +3852,7 @@ export function createStaticHandler( return; } let result = results[match.route.id]; - if (isRedirectDataStrategyResultResult(result)) { + if (isRedirectDataStrategyResult(result)) { let response = result.result as Response; // Throw redirects and let the server handle them with an HTTP redirect throw normalizeRelativeRoutingRedirectResponse( @@ -5349,10 +5340,6 @@ function isHashChangeOnly(a: Location, b: Location): boolean { return false; } -function isPromise(val: unknown): val is Promise { - return typeof val === "object" && val != null && "then" in val; -} - function isDataStrategyResult(result: unknown): result is DataStrategyResult { return ( result != null && @@ -5363,7 +5350,7 @@ function isDataStrategyResult(result: unknown): result is DataStrategyResult { ); } -function isRedirectDataStrategyResultResult(result: DataStrategyResult) { +function isRedirectDataStrategyResult(result: DataStrategyResult) { return ( isResponse(result.result) && redirectStatusCodes.has(result.result.status) ); diff --git a/packages/react-router/lib/server-runtime/headers.ts b/packages/react-router/lib/server-runtime/headers.ts index 21ebc7f720..3c8cfe04b6 100644 --- a/packages/react-router/lib/server-runtime/headers.ts +++ b/packages/react-router/lib/server-runtime/headers.ts @@ -44,7 +44,7 @@ export function getDocumentHeaders( // Only expose errorHeaders to the leaf headers() function to // avoid duplication via parentHeaders let includeErrorHeaders = - errorHeaders != undefined && idx === matches.length - 1; + errorHeaders != null && idx === matches.length - 1; // Only prepend cookies from errorHeaders at the leaf renderable route // when it's not the same as loaderHeaders/actionHeaders to avoid // duplicate cookies diff --git a/packages/react-router/lib/server-runtime/jsonify.ts b/packages/react-router/lib/server-runtime/jsonify.ts deleted file mode 100644 index 43b1f97769..0000000000 --- a/packages/react-router/lib/server-runtime/jsonify.ts +++ /dev/null @@ -1,257 +0,0 @@ -import type { Equal, Expect, MutualExtends } from "./typecheck"; -import { expectType } from "./typecheck"; - -// prettier-ignore -// `Jsonify` emulates `let y = JSON.parse(JSON.stringify(x))`, but for types -// so that we can infer the shape of the data sent over the network. -export type Jsonify = - // any - IsAny extends true ? any : - - // toJSON - T extends { toJSON(): infer U } ? (U extends JsonValue ? U : unknown) : - - // primitives - T extends JsonPrimitive ? T : - T extends String ? string : - T extends Number ? number : - T extends Boolean ? boolean : - - // Promises JSON.stringify to an empty object - T extends Promise ? EmptyObject : - - // Map & Set - T extends Map ? EmptyObject : - T extends Set ? EmptyObject : - - // TypedArray - T extends TypedArray ? Record : - - // Not JSON serializable - T extends NotJson ? never : - - // tuple & array - T extends [] ? [] : - T extends readonly [infer F, ...infer R] ? [NeverToNull>, ...Jsonify] : - T extends readonly unknown[] ? Array>>: - - // object - T extends Record ? JsonifyObject : - - // unknown - unknown extends T ? unknown : - - never - -// value is always not JSON => true -// value is always JSON => false -// value is somtimes JSON, sometimes not JSON => boolean -// note: cannot be inlined as logic requires union distribution -type ValueIsNotJson = T extends NotJson ? true : false; - -// note: remove optionality so that produced values are never `undefined`, -// only `true`, `false`, or `boolean` -type IsNotJson = { [K in keyof T]-?: ValueIsNotJson }; - -type JsonifyValues = { [K in keyof T]: Jsonify }; - -// prettier-ignore -type JsonifyObject> = - // required - { [K in keyof T as - unknown extends T[K] ? never : - IsNotJson[K] extends false ? K : - never - ]: JsonifyValues[K] } & - // optional - { [K in keyof T as - unknown extends T[K] ? K : - // if the value is always JSON, then it's not optional - IsNotJson[K] extends false ? never : - // if the value is always not JSON, omit it entirely - IsNotJson[K] extends true ? never : - // if the value is mixed, then it's optional - K - ]? : JsonifyValues[K]} - -// types ------------------------------------------------------------ - -type JsonPrimitive = string | number | boolean | null; - -type JsonArray = JsonValue[] | readonly JsonValue[]; - -// prettier-ignore -type JsonObject = - { [K in string]: JsonValue } & - { [K in string]?: JsonValue } - -type JsonValue = JsonPrimitive | JsonObject | JsonArray; - -type NotJson = undefined | symbol | AnyFunction; - -type TypedArray = - | Int8Array - | Uint8Array - | Uint8ClampedArray - | Int16Array - | Uint16Array - | Int32Array - | Uint32Array - | Float32Array - | Float64Array - | BigInt64Array - | BigUint64Array; - -// tests ------------------------------------------------------------ - -// prettier-ignore -// eslint-disable-next-line @typescript-eslint/no-unused-vars -type _tests = [ - // any - Expect, any>>, - - // primitives - Expect, string>>, - Expect, number>>, - Expect, boolean>>, - Expect, null>>, - Expect, string>>, - Expect, number>>, - Expect, boolean>>, - Expect>, EmptyObject>>, - - // Map & Set - Expect>, EmptyObject>>, - Expect>, EmptyObject>>, - - // TypedArray - Expect, Record>>, - Expect, Record>>, - Expect, Record>>, - Expect, Record>>, - Expect, Record>>, - Expect, Record>>, - Expect, Record>>, - Expect, Record>>, - Expect, Record>>, - Expect, Record>>, - Expect, Record>>, - - // Not Json - Expect, never>>, - Expect, never>>, - Expect void>, never>>, - Expect, never>>, - - // toJson - Expect, "stuff">>, - Expect, string>>, - Expect, unknown>>, - Expect, unknown>>, - Expect, string>>, - - - // tuple & array - Expect, []>>, - Expect, [1, 'two', string, null, false]>>, - Expect, (string | number)[]>>, - Expect, null[]>>, - Expect, [1,2,3]>>, - - // object - Expect>, {}>>, - Expect>, {a: string}>>, - Expect>, {a?: string}>>, - Expect>, {a?: string}>>, - Expect>, {a: string, b?: string}>>, - Expect>, {}>>, - Expect>>, Record>>, - Expect>>, Record>>, - Expect}>, { payload: Record}>>, - Expect any); - optionalFunctionUnion?: string | (() => any); - optionalFunctionUnionUndefined: string | (() => any) | undefined; - - // Should be omitted - requiredFunction: () => any; - optionalFunction?: () => any; - optionalFunctionUndefined: (() => any) | undefined; - }>>, { - requiredString: string - requiredUnion: number | boolean - - optionalString?: string; - optionalUnion?: number | string; - optionalStringUndefined?: string | undefined; - optionalUnionUndefined?: number | string | undefined; - requiredFunctionUnion?: string - optionalFunctionUnion?: string; - optionalFunctionUnionUndefined?: string - }>>, - - // unknown - Expect, unknown>>, - Expect, unknown[]>>, - Expect, [unknown, 1]>>, - Expect>, {a?: unknown}>>, - Expect>, {a?: unknown, b: 'hello'}>>, - - // never - Expect, never>>, - Expect>, {a: never}>>, - Expect>, {a: never, b:string}>>, - Expect>, {a: never, b: string} | {a: string, b: never}>>, - - // class - Expect>, {a: string}>>, -]; - -class MyClass { - a: string; - b: () => string; - - constructor() { - this.a = "hello"; - this.b = () => "world"; - } -} - -// real-world example: `InvoiceLineItem` from `stripe` -type Recursive = { - a: Date; - recur?: Recursive; -}; -declare const recursive: Jsonify; -expectType<{ a: string; recur?: Jsonify }>( - recursive.recur!.recur!.recur! -); - -// real-world example: `Temporal` from `@js-temporal/polyfill` -interface BooleanWithToJson extends Boolean { - toJSON(): string; -} - -// utils ------------------------------------------------------------ - -type Pretty = { [K in keyof T]: T[K] }; - -type AnyFunction = (...args: any[]) => unknown; - -type NeverToNull = [T] extends [never] ? null : T; - -// adapted from https://github.com/sindresorhus/type-fest/blob/main/source/empty-object.d.ts -declare const emptyObjectSymbol: unique symbol; -export type EmptyObject = { [emptyObjectSymbol]?: never }; - -// adapted from https://github.com/type-challenges/type-challenges/blob/main/utils/index.d.ts -type IsAny = 0 extends 1 & T ? true : false; diff --git a/packages/react-router/lib/types.ts b/packages/react-router/lib/types.ts index 99d9265614..9198cf5244 100644 --- a/packages/react-router/lib/types.ts +++ b/packages/react-router/lib/types.ts @@ -1,6 +1,5 @@ import type { DataWithResponseInit } from "./router/utils"; import type { AppLoadContext } from "./server-runtime/data"; -import type { Jsonify } from "./server-runtime/jsonify"; import type { Serializable } from "./server-runtime/single-fetch"; export type Expect = T; diff --git a/templates/basic/postcss.config.js b/templates/basic/postcss.config.js index 2aa7205d4b..a982c6414e 100644 --- a/templates/basic/postcss.config.js +++ b/templates/basic/postcss.config.js @@ -1,6 +1,8 @@ -export default { +const config = { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; + +export default config; From a934cc9e75d5222b05c0832d6a43c2c45852a1a2 Mon Sep 17 00:00:00 2001 From: Bilal Kazmi Date: Mon, 21 Oct 2024 22:38:14 +0500 Subject: [PATCH 22/33] refactor(react-router): replace `substr` with `substring` (#12080) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Replaced deprecated subtr method with substring in history package * Update .changeset/silver-cats-shave.md --------- Co-authored-by: MichaΓ«l De Boey --- .changeset/silver-cats-shave.md | 5 +++++ packages/react-router/lib/router/history.ts | 12 ++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 .changeset/silver-cats-shave.md diff --git a/.changeset/silver-cats-shave.md b/.changeset/silver-cats-shave.md new file mode 100644 index 0000000000..698836f2c7 --- /dev/null +++ b/.changeset/silver-cats-shave.md @@ -0,0 +1,5 @@ +--- +"react-router": patch +--- + +Replace `substr` with `substring` diff --git a/packages/react-router/lib/router/history.ts b/packages/react-router/lib/router/history.ts index 1118193972..e7e07ac28b 100644 --- a/packages/react-router/lib/router/history.ts +++ b/packages/react-router/lib/router/history.ts @@ -424,7 +424,7 @@ export function createHashHistory( pathname = "/", search = "", hash = "", - } = parsePath(window.location.hash.substr(1)); + } = parsePath(window.location.hash.substring(1)); // Hash URL should always have a leading / just like window.location.pathname // does, so if an app ends up at a route like /#something then we add a @@ -510,7 +510,7 @@ export function warning(cond: any, message: string) { } function createKey() { - return Math.random().toString(36).substr(2, 8); + return Math.random().toString(36).substring(2, 10); } /** @@ -576,14 +576,14 @@ export function parsePath(path: string): Partial { if (path) { let hashIndex = path.indexOf("#"); if (hashIndex >= 0) { - parsedPath.hash = path.substr(hashIndex); - path = path.substr(0, hashIndex); + parsedPath.hash = path.substring(hashIndex); + path = path.substring(0, hashIndex); } let searchIndex = path.indexOf("?"); if (searchIndex >= 0) { - parsedPath.search = path.substr(searchIndex); - path = path.substr(0, searchIndex); + parsedPath.search = path.substring(searchIndex); + path = path.substring(0, searchIndex); } if (path) { From 3a1a31117eb9108f15e612aeac128a50bf97b04d Mon Sep 17 00:00:00 2001 From: Shodai Suzuki Date: Tue, 22 Oct 2024 02:47:03 +0900 Subject: [PATCH 23/33] docs(upgrading/remix): show entry files diff (#12158) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: MichaΓ«l De Boey Co-authored-by: Matt Brophy --- contributors.yml | 1 + docs/upgrading/remix.md | 27 ++++++++++++++++++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/contributors.yml b/contributors.yml index 182253523a..09cfbf1df5 100644 --- a/contributors.yml +++ b/contributors.yml @@ -240,6 +240,7 @@ - SimenB - SkayuX - smithki +- soartec-lab - sorrycc - souzasmatheus - srmagura diff --git a/docs/upgrading/remix.md b/docs/upgrading/remix.md index c868bb219c..0a6e26a125 100644 --- a/docs/upgrading/remix.md +++ b/docs/upgrading/remix.md @@ -89,12 +89,29 @@ export const routes: RouteConfig = flatRoutes(); ### Step 6 - Rename components in entry files -If you have an `entry.server.tsx` and/or an `entry.client.tsx` file in your application, you will need to rename the main components in this files: +If you have an `entry.server.tsx` and/or an `entry.client.tsx` file in your application, you will need to update the main components in these files: + +```diff filename=app/entry.server.tsx +- import { RemixServer } from "@remix-run/react"; ++ import { ServerRouter } from "react-router"; + +- , ++ , +``` + +```diff filename=app/entry.client.tsx +- import { RemixBrowser } from "@remix-run/react"; ++ import { HydratedRouter } from "react-router/dom"; + +hydrateRoot( + document, + +- ++ + , +); +``` -| Entry File | Remix v2 Component | | React Router v7 Component | -| ------------------ | ------------------ | --- | ------------------------- | -| `entry.server.tsx` | `` | ➑️ | `` | -| `entry.client.stx` | `` | ➑️ | `` | ### Step 7 - Update types for `AppLoadContext` From 8360bc6e057fbec6504f207779d9551c426b7275 Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Mon, 21 Oct 2024 17:47:42 +0000 Subject: [PATCH 24/33] chore: format --- docs/upgrading/remix.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/upgrading/remix.md b/docs/upgrading/remix.md index 0a6e26a125..a259f50c8d 100644 --- a/docs/upgrading/remix.md +++ b/docs/upgrading/remix.md @@ -112,7 +112,6 @@ hydrateRoot( ); ``` - ### Step 7 - Update types for `AppLoadContext` This is only applicable if you were using a custom server in Remix v2. If you were using `remix-serve` you can skip this step. From 7fd797a72a3408d2a1967ac6ea3a6c349060c2f1 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 21 Oct 2024 14:31:13 -0400 Subject: [PATCH 25/33] Docs: add note on data to loaders/action docs --- docs/start/actions.md | 41 ++++++++++++++++++++++++++++++++++++-- docs/start/data-loading.md | 30 ++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/docs/start/actions.md b/docs/start/actions.md index 3b42de83c9..77d3d4c13c 100644 --- a/docs/start/actions.md +++ b/docs/start/actions.md @@ -17,6 +17,7 @@ Client actions only run in the browser and take priority over a server action wh // route('/projects/:projectId', './project.tsx') import type * as Route from "./+types.project"; import { Form } from "react-router"; +import { someApi } from "./api"; export async function clientAction({ request, @@ -53,13 +54,14 @@ Server actions only run on the server and are removed from client bundles. // route('/projects/:projectId', './project.tsx') import type * as Route from "./+types.project"; import { Form } from "react-router"; +import { fakeDb } from "../db"; export async function action({ request, }: Route.ActionArgs) { let formData = await request.formData(); let title = await formData.get("title"); - let project = await someApi.updateProject({ title }); + let project = await fakeDb.updateProject({ title }); return project; } @@ -81,6 +83,38 @@ export default function Project({ } ``` +### Custom Status Codes and Headers + +If you need to return a custom HTTP status code or custom headers from your `action`, you can do so using the [`data`][data] utility: + +```tsx filename=app/project.tsx lines=[3,11-14,19] +// route('/projects/:projectId', './project.tsx') +import type * as Route from "./+types.project"; +import { data } from "react-router"; +import { fakeDb } from "../db"; + +export async function action({ + request, +}: Route.ActionArgs) { + let formData = await request.formData(); + let title = await formData.get("title"); + if (!title) { + throw data( + { message: "Invalid title" }, + { status: 400 } + ); + } + + if (!projectExists(title)) { + let project = await fakeDb.createProject({ title }); + return data(project, { status: 201 }); + } else { + let project = await fakeDb.updateProject({ title }); + return project; + } +} +``` + ## Calling Actions Actions are called declaratively through `
` and imperatively through `useSubmit` (or `` and `fetcher.submit`) by referencing the route's path and a "post" method. @@ -159,4 +193,7 @@ fetcher.submit( ); ``` -See the [Using Fetchers](../misc/fetchers) guide for more information. +See the [Using Fetchers][fetchers] guide for more information. + +[fetchers]: ../misc/fetchers +[data]: ../../api/react-router/data diff --git a/docs/start/data-loading.md b/docs/start/data-loading.md index efe194894c..9d2b3f194a 100644 --- a/docs/start/data-loading.md +++ b/docs/start/data-loading.md @@ -65,6 +65,35 @@ export default function Product({ Note that the `loader` function is removed from client bundles so you can use server only APIs without worrying about them being included in the browser. +### Custom Status Codes and Headers + +If you need to return a custom HTTP status code or custom headers from your `loader`, you can do so using the [`data`][data] utility: + +```tsx filename=app/product.tsx lines=[3,6-8,14,17-21] +// route("products/:pid", "./product.tsx"); +import type * as Route from "./+types.product"; +import { data } from "react-router"; +import { fakeDb } from "../db"; + +export function headers({ loaderHeaders }: HeadersArgs) { + return loaderHeaders; +} + +export async function loader({ params }: Route.LoaderArgs) { + const product = await fakeDb.getProduct(params.pid); + + if (!product) { + throw data(null, { status: 404 }); + } + + return data(product, { + headers: { + "Cache-Control": "public; max-age=300", + }, + }); +} +``` + ## Static Data Loading When pre-rendering, loaders are used to fetch data during the production build. @@ -195,3 +224,4 @@ export async function Product({ id }: { id: string }) { ``` [advanced_data_fetching]: ../tutorials/advanced-data-fetching +[data]: ../../api/react-router/data From a6e57f41b90dbacbed5ed234300701a7c031f1d8 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 22 Oct 2024 10:10:23 -0400 Subject: [PATCH 26/33] Update cookie dependency to 1.0 (#12172) --- .changeset/wet-actors-act.md | 5 +++ .../lib/server-runtime/cookies.ts | 34 +++++++++++-------- .../lib/server-runtime/sessions.ts | 8 ++--- packages/react-router/package.json | 2 +- pnpm-lock.yaml | 19 +++++++---- 5 files changed, 43 insertions(+), 25 deletions(-) create mode 100644 .changeset/wet-actors-act.md diff --git a/.changeset/wet-actors-act.md b/.changeset/wet-actors-act.md new file mode 100644 index 0000000000..0171c0f48e --- /dev/null +++ b/.changeset/wet-actors-act.md @@ -0,0 +1,5 @@ +--- +"react-router": major +--- + +Update `cookie` dependency to `^1.0.1` - please see the [release notes](https://github.com/jshttp/cookie/releases) for any breaking changes diff --git a/packages/react-router/lib/server-runtime/cookies.ts b/packages/react-router/lib/server-runtime/cookies.ts index 796437f438..64bf164d94 100644 --- a/packages/react-router/lib/server-runtime/cookies.ts +++ b/packages/react-router/lib/server-runtime/cookies.ts @@ -1,10 +1,13 @@ -import type { CookieParseOptions, CookieSerializeOptions } from "cookie"; +import type { ParseOptions, SerializeOptions } from "cookie"; import { parse, serialize } from "cookie"; import { sign, unsign } from "./crypto"; import { warnOnce } from "./warnings"; -export type { CookieParseOptions, CookieSerializeOptions }; +export type { + ParseOptions as CookieParseOptions, + SerializeOptions as CookieSerializeOptions, +}; export interface CookieSignatureOptions { /** @@ -18,8 +21,8 @@ export interface CookieSignatureOptions { secrets?: string[]; } -export type CookieOptions = CookieParseOptions & - CookieSerializeOptions & +export type CookieOptions = ParseOptions & + SerializeOptions & CookieSignatureOptions; /** @@ -55,16 +58,13 @@ export interface Cookie { * Parses a raw `Cookie` header and returns the value of this cookie or * `null` if it's not present. */ - parse( - cookieHeader: string | null, - options?: CookieParseOptions - ): Promise; + parse(cookieHeader: string | null, options?: ParseOptions): Promise; /** * Serializes the given value to a string and returns the `Set-Cookie` * header. */ - serialize(value: any, options?: CookieSerializeOptions): Promise; + serialize(value: any, options?: SerializeOptions): Promise; } /** @@ -98,11 +98,17 @@ export const createCookie = ( async parse(cookieHeader, parseOptions) { if (!cookieHeader) return null; let cookies = parse(cookieHeader, { ...options, ...parseOptions }); - return name in cookies - ? cookies[name] === "" - ? "" - : await decodeCookieValue(cookies[name], secrets) - : null; + if (name in cookies) { + let value = cookies[name]; + if (typeof value === "string" && value !== "") { + let decoded = await decodeCookieValue(value, secrets); + return decoded; + } else { + return ""; + } + } else { + return null; + } }, async serialize(value, serializeOptions) { return serialize( diff --git a/packages/react-router/lib/server-runtime/sessions.ts b/packages/react-router/lib/server-runtime/sessions.ts index 79bd5ba396..8c38c8393e 100644 --- a/packages/react-router/lib/server-runtime/sessions.ts +++ b/packages/react-router/lib/server-runtime/sessions.ts @@ -1,4 +1,4 @@ -import type { CookieParseOptions, CookieSerializeOptions } from "cookie"; +import type { ParseOptions, SerializeOptions } from "cookie"; import type { Cookie, CookieOptions } from "./cookies"; import { createCookie, isCookie } from "./cookies"; @@ -176,7 +176,7 @@ export interface SessionStorage { */ getSession: ( cookieHeader?: string | null, - options?: CookieParseOptions + options?: ParseOptions ) => Promise>; /** @@ -185,7 +185,7 @@ export interface SessionStorage { */ commitSession: ( session: Session, - options?: CookieSerializeOptions + options?: SerializeOptions ) => Promise; /** @@ -194,7 +194,7 @@ export interface SessionStorage { */ destroySession: ( session: Session, - options?: CookieSerializeOptions + options?: SerializeOptions ) => Promise; } diff --git a/packages/react-router/package.json b/packages/react-router/package.json index 215eb32c85..89f65b333b 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -41,7 +41,7 @@ "dependencies": { "@types/cookie": "^0.6.0", "@web3-storage/multipart-parser": "^1.0.0", - "cookie": "^0.6.0", + "cookie": "^1.0.1", "react-router": "workspace:*", "set-cookie-parser": "^2.6.0", "source-map": "^0.7.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 318d5e6b4c..c1d9354a59 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -171,7 +171,7 @@ importers: version: 7.34.1(eslint@8.57.0) eslint-plugin-react-hooks: specifier: next - version: 5.1.0-rc-5dcb0097-20240918(eslint@8.57.0) + version: 5.1.0-rc-69d4b800-20241021(eslint@8.57.0) fs-extra: specifier: ^10.1.0 version: 10.1.0 @@ -557,8 +557,8 @@ importers: specifier: ^1.0.0 version: 1.0.0 cookie: - specifier: ^0.6.0 - version: 0.6.0 + specifier: ^1.0.1 + version: 1.0.1 react-router: specifier: workspace:* version: 'link:' @@ -3651,6 +3651,10 @@ packages: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} + cookie@1.0.1: + resolution: {integrity: sha512-Xd8lFX4LM9QEEwxQpF9J9NTUh8pmdJO0cyRJhFiDoLTk2eH8FXlRv2IFGYVadZpqI3j8fhNrSdKCeYPxiAhLXw==} + engines: {node: '>=18'} + cookiejar@2.1.4: resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} @@ -4152,8 +4156,8 @@ packages: peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 - eslint-plugin-react-hooks@5.1.0-rc-5dcb0097-20240918: - resolution: {integrity: sha512-Kx+m5yAm0w1RSpzo0vQ4aN7kIlM9bcAvWyCRWqS5HjxxnzChMAYg4a3fQlwxFvwZ3WkoHA9HecJUBRTf49L5ew==} + eslint-plugin-react-hooks@5.1.0-rc-69d4b800-20241021: + resolution: {integrity: sha512-BlHwxPe59W888jvcwa2dRJXzJf2DWJm9ZMwlpaeobPzW1gaghptM+ISk/E2UK/PTVuDmdkuqZ4je/9v3QE99Gg==} engines: {node: '>=10'} peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 @@ -4189,6 +4193,7 @@ packages: eslint@8.57.0: resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true espree@9.6.1: @@ -11246,6 +11251,8 @@ snapshots: cookie@0.6.0: {} + cookie@1.0.1: {} + cookiejar@2.1.4: {} core-js-compat@3.32.0: @@ -11929,7 +11936,7 @@ snapshots: dependencies: eslint: 8.57.0 - eslint-plugin-react-hooks@5.1.0-rc-5dcb0097-20240918(eslint@8.57.0): + eslint-plugin-react-hooks@5.1.0-rc-69d4b800-20241021(eslint@8.57.0): dependencies: eslint: 8.57.0 From f294df8a72359a1f9b2876ca7e28d98b36b6511d Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 22 Oct 2024 10:32:01 -0400 Subject: [PATCH 27/33] Fix defaultShouldRevalidate when using single fetch (Remix PR 10139) (#12170) --- integration/single-fetch-test.ts | 92 ++++++++++++++++++++ packages/react-router/lib/dom/ssr/links.ts | 86 ++++++++---------- packages/react-router/lib/dom/ssr/routes.tsx | 52 +++++++---- 3 files changed, 166 insertions(+), 64 deletions(-) diff --git a/integration/single-fetch-test.ts b/integration/single-fetch-test.ts index dbdceaed8b..39a3a4898e 100644 --- a/integration/single-fetch-test.ts +++ b/integration/single-fetch-test.ts @@ -2108,6 +2108,98 @@ test.describe("single-fetch", () => { ).toBe(true); }); + test("provides the proper defaultShouldRevalidate value", async ({ + page, + }) => { + let fixture = await createFixture({ + files: { + ...files, + "app/routes/_index.tsx": js` + import { Link } from 'react-router'; + export default function Component() { + return Go to /parent/a; + } + `, + "app/routes/parent.tsx": js` + import { Link, Outlet, useLoaderData } from 'react-router'; + let count = 0; + export function loader({ request }) { + return { count: ++count }; + } + export function shouldRevalidate({ defaultShouldRevalidate }) { + return defaultShouldRevalidate; + } + 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 'react-router'; + 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 'react-router'; + let count = 0; + export function loader({ request }) { + return { count: ++count }; + } + export default function Component() { + return

B Count: {useLoaderData().count}

; + } + `, + }, + }); + let appFixture = await createAppFixture(fixture); + let app = new PlaywrightFixture(appFixture, page); + + let urls: string[] = []; + page.on("request", (req) => { + if (req.url().includes(".data")) { + urls.push(req.url()); + } + }); + + await app.goto("/"); + + await app.clickLink("/parent/a"); + await page.waitForSelector("#a"); + expect(await app.getHtml("#parent")).toContain("Parent Count: 1"); + expect(await app.getHtml("#a")).toContain("A Count: 1"); + expect(urls.length).toBe(1); + expect(urls[0].endsWith("/parent/a.data")).toBe(true); + urls = []; + + await app.clickLink("/parent/b"); + await page.waitForSelector("#b"); + expect(await app.getHtml("#parent")).toContain("Parent Count: 2"); + expect(await app.getHtml("#b")).toContain("B Count: 1"); + expect(urls.length).toBe(1); + // Reload the parent route + expect(urls[0].endsWith("/parent/b.data")).toBe(true); + urls = []; + + await app.clickLink("/parent/a"); + await page.waitForSelector("#a"); + expect(await app.getHtml("#parent")).toContain("Parent Count: 3"); + expect(await app.getHtml("#a")).toContain("A Count: 2"); + expect(urls.length).toBe(1); + // Reload the parent route + expect(urls[0].endsWith("/parent/a.data")).toBe(true); + }); + test("does not add a _routes param for routes without loaders", async ({ page, }) => { diff --git a/packages/react-router/lib/dom/ssr/links.ts b/packages/react-router/lib/dom/ssr/links.ts index 80473550fd..50bb073e92 100644 --- a/packages/react-router/lib/dom/ssr/links.ts +++ b/packages/react-router/lib/dom/ssr/links.ts @@ -168,8 +168,6 @@ export function getNewMatchesForLinks( location: Location, mode: "data" | "assets" ): AgnosticDataRouteMatch[] { - let path = parsePathPatch(page); - let isNew = (match: AgnosticDataRouteMatch, index: number) => { if (!currentMatches[index]) return true; return match.route.id !== currentMatches[index].route.id; @@ -186,48 +184,47 @@ export function getNewMatchesForLinks( ); }; - // NOTE: keep this mostly up-to-date w/ the transition data diff, but this + if (mode === "assets") { + return nextMatches.filter( + (match, index) => isNew(match, index) || matchPathChanged(match, index) + ); + } + + // NOTE: keep this mostly up-to-date w/ the router data diff, but this // version doesn't care about submissions - let newMatches = - mode === "data" && location.search !== path.search - ? // this is really similar to stuff in transition.ts, maybe somebody smarter - // than me (or in less of a hurry) can share some of it. You're the best. - nextMatches.filter((match, index) => { - let manifestRoute = manifest.routes[match.route.id]; - if (!manifestRoute.hasLoader) { - return false; - } - - if (isNew(match, index) || matchPathChanged(match, index)) { - return true; - } - - if (match.route.shouldRevalidate) { - let routeChoice = match.route.shouldRevalidate({ - currentUrl: new URL( - location.pathname + location.search + location.hash, - window.origin - ), - currentParams: currentMatches[0]?.params || {}, - nextUrl: new URL(page, window.origin), - nextParams: match.params, - defaultShouldRevalidate: true, - }); - if (typeof routeChoice === "boolean") { - return routeChoice; - } - } - return true; - }) - : nextMatches.filter((match, index) => { - let manifestRoute = manifest.routes[match.route.id]; - return ( - (mode === "assets" || manifestRoute.hasLoader) && - (isNew(match, index) || matchPathChanged(match, index)) - ); + // TODO: this is really similar to stuff in router.ts, maybe somebody smarter + // than me (or in less of a hurry) can share some of it. You're the best. + if (mode === "data") { + return nextMatches.filter((match, index) => { + let manifestRoute = manifest.routes[match.route.id]; + if (!manifestRoute.hasLoader) { + return false; + } + + if (isNew(match, index) || matchPathChanged(match, index)) { + return true; + } + + if (match.route.shouldRevalidate) { + let routeChoice = match.route.shouldRevalidate({ + currentUrl: new URL( + location.pathname + location.search + location.hash, + window.origin + ), + currentParams: currentMatches[0]?.params || {}, + nextUrl: new URL(page, window.origin), + nextParams: match.params, + defaultShouldRevalidate: true, }); + if (typeof routeChoice === "boolean") { + return routeChoice; + } + } + return true; + }); + } - return newMatches; + return []; } export function getModuleLinkHrefs( @@ -320,13 +317,6 @@ function dedupeLinkDescriptors( }, [] as KeyedLinkDescriptor[]); } -// https://github.com/remix-run/history/issues/897 -function parsePathPatch(href: string) { - let path = parsePath(href); - if (path.search === undefined) path.search = ""; - return path; -} - // Detect if this browser supports (or has it enabled). // Originally added to handle the firefox `network.preload` config: // https://bugzilla.mozilla.org/show_bug.cgi?id=1847811 diff --git a/packages/react-router/lib/dom/ssr/routes.tsx b/packages/react-router/lib/dom/ssr/routes.tsx index f4e1c43cf1..d83664a189 100644 --- a/packages/react-router/lib/dom/ssr/routes.tsx +++ b/packages/react-router/lib/dom/ssr/routes.tsx @@ -5,6 +5,7 @@ import type { ActionFunctionArgs, LoaderFunctionArgs, ShouldRevalidateFunction, + ShouldRevalidateFunctionArgs, } from "../../router/utils"; import { ErrorResponseImpl } from "../../router/utils"; import type { RouteModule, RouteModules } from "./routeModules"; @@ -288,13 +289,11 @@ export function createClientRoutes( ...dataRoute, ...getRouteComponents(route, routeModule, isSpaMode), handle: routeModule.handle, - shouldRevalidate: needsRevalidation - ? wrapShouldRevalidateForHdr( - route.id, - routeModule.shouldRevalidate, - needsRevalidation - ) - : routeModule.shouldRevalidate, + shouldRevalidate: getShouldRevalidateFunction( + routeModule, + route.id, + needsRevalidation + ), }); let hasInitialData = @@ -456,19 +455,15 @@ export function createClientRoutes( }); } - if (needsRevalidation) { - lazyRoute.shouldRevalidate = wrapShouldRevalidateForHdr( - route.id, - mod.shouldRevalidate, - needsRevalidation - ); - } - return { ...(lazyRoute.loader ? { loader: lazyRoute.loader } : {}), ...(lazyRoute.action ? { action: lazyRoute.action } : {}), hasErrorBoundary: lazyRoute.hasErrorBoundary, - shouldRevalidate: lazyRoute.shouldRevalidate, + shouldRevalidate: getShouldRevalidateFunction( + lazyRoute, + route.id, + needsRevalidation + ), handle: lazyRoute.handle, // No need to wrap these in layout since the root route is never // loaded via route.lazy() @@ -492,6 +487,31 @@ export function createClientRoutes( }); } +function getShouldRevalidateFunction( + route: Partial, + routeId: string, + needsRevalidation: Set | undefined +) { + // During HDR we force revalidation for updated routes + if (needsRevalidation) { + return wrapShouldRevalidateForHdr( + routeId, + route.shouldRevalidate, + needsRevalidation + ); + } + + // Single fetch revalidates by default, so override the RR default value which + // matches the multi-fetch behavior with `true` + if (route.shouldRevalidate) { + let fn = route.shouldRevalidate; + return (opts: ShouldRevalidateFunctionArgs) => + fn({ ...opts, defaultShouldRevalidate: true }); + } + + return route.shouldRevalidate; +} + // When an HMR / HDR update happens we opt out of all user-defined // revalidation logic and force a revalidation on the first call function wrapShouldRevalidateForHdr( From 813497a10eefbf0fdd3049baddeace6139268744 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 22 Oct 2024 11:24:21 -0400 Subject: [PATCH 28/33] Drop support for Node 18 and installGlobals (#12171) --- .changeset/tidy-pens-help.md | 10 ++++ .github/workflows/integration-full.yml | 6 +- .github/workflows/integration-pr-ubuntu.yml | 2 +- .../integration-pr-windows-macos.yml | 6 +- .github/workflows/shared-integration.yml | 2 +- .github/workflows/test.yml | 2 +- docs/deploying/custom-node.md | 38 +++++++++++++ integration/helpers/create-fixture.ts | 3 - .../helpers/node-template/package.json | 2 +- .../vite-cloudflare-template/package.json | 2 +- .../helpers/vite-template/package.json | 2 +- integration/helpers/vite.ts | 3 - integration/vite-basename-test.ts | 4 -- jest/jest.config.shared.js | 1 - package.json | 3 +- .../react-router-architect/__tests__/setup.ts | 2 - packages/react-router-architect/package.json | 2 +- packages/react-router-cloudflare/package.json | 2 +- .../__tests__/fixtures/node/package.json | 2 +- packages/react-router-dev/cli/run.ts | 10 +++- packages/react-router-dev/package.json | 2 +- packages/react-router-dom/package.json | 2 +- .../react-router-express/__tests__/setup.ts | 2 - packages/react-router-express/package.json | 2 +- packages/react-router-fs-routes/package.json | 2 +- packages/react-router-node/__tests__/setup.ts | 3 - packages/react-router-node/globals.ts | 55 ------------------- packages/react-router-node/index.ts | 2 - packages/react-router-node/install.ts | 3 - packages/react-router-node/package.json | 6 +- packages/react-router-node/rollup.config.js | 4 +- .../package.json | 2 +- packages/react-router-serve/cli.ts | 3 - packages/react-router-serve/package.json | 2 +- .../__tests__/router/navigation-test.ts | 18 +++--- .../__tests__/router/router-memory-test.ts | 32 ----------- .../react-router/__tests__/router/ssr-test.ts | 6 -- packages/react-router/__tests__/setup.ts | 29 ++-------- packages/react-router/jest.config.js | 1 + packages/react-router/package.json | 2 +- playground/compiler-express/package.json | 2 +- playground/compiler-express/server.js | 3 - playground/compiler-spa/package.json | 2 +- playground/compiler/package.json | 2 +- playground/compiler/vite.config.ts | 3 - pnpm-lock.yaml | 17 ------ rollup.utils.js | 2 +- 47 files changed, 100 insertions(+), 213 deletions(-) create mode 100644 .changeset/tidy-pens-help.md delete mode 100644 packages/react-router-architect/__tests__/setup.ts delete mode 100644 packages/react-router-express/__tests__/setup.ts delete mode 100644 packages/react-router-node/__tests__/setup.ts delete mode 100644 packages/react-router-node/globals.ts delete mode 100644 packages/react-router-node/install.ts diff --git a/.changeset/tidy-pens-help.md b/.changeset/tidy-pens-help.md new file mode 100644 index 0000000000..5db3b598d6 --- /dev/null +++ b/.changeset/tidy-pens-help.md @@ -0,0 +1,10 @@ +--- +"@react-router/express": major +"@react-router/node": major +"@react-router/dev": major +"react-router": major +--- + +Drop support for Node 18, update minimum Node vestion to 20 + +- Remove `installGlobals()` as this should no longer be necessary diff --git a/.github/workflows/integration-full.yml b/.github/workflows/integration-full.yml index bdebc1d319..3f79331816 100644 --- a/.github/workflows/integration-full.yml +++ b/.github/workflows/integration-full.yml @@ -34,7 +34,7 @@ jobs: uses: ./.github/workflows/shared-integration.yml with: os: "ubuntu-latest" - node_version: "[18, 20]" + node_version: "[20, 22]" browser: '["chromium", "firefox"]' integration-windows: @@ -43,7 +43,7 @@ jobs: uses: ./.github/workflows/shared-integration.yml with: os: "windows-latest" - node_version: "[18, 20]" + node_version: "[20, 22]" browser: '["msedge"]' integration-macos: @@ -52,5 +52,5 @@ jobs: uses: ./.github/workflows/shared-integration.yml with: os: "macos-latest" - node_version: "[18, 20]" + node_version: "[20, 22]" browser: '["webkit"]' diff --git a/.github/workflows/integration-pr-ubuntu.yml b/.github/workflows/integration-pr-ubuntu.yml index 6223906475..ac25713e08 100644 --- a/.github/workflows/integration-pr-ubuntu.yml +++ b/.github/workflows/integration-pr-ubuntu.yml @@ -31,5 +31,5 @@ jobs: uses: ./.github/workflows/shared-integration.yml with: os: "ubuntu-latest" - node_version: "[20]" + node_version: "[22]" browser: '["chromium"]' diff --git a/.github/workflows/integration-pr-windows-macos.yml b/.github/workflows/integration-pr-windows-macos.yml index c5e08598e6..780a81f289 100644 --- a/.github/workflows/integration-pr-windows-macos.yml +++ b/.github/workflows/integration-pr-windows-macos.yml @@ -21,7 +21,7 @@ jobs: uses: ./.github/workflows/shared-integration.yml with: os: "ubuntu-latest" - node_version: "[20]" + node_version: "[22]" browser: '["firefox"]' integration-msedge: @@ -30,7 +30,7 @@ jobs: uses: ./.github/workflows/shared-integration.yml with: os: "windows-latest" - node_version: "[20]" + node_version: "[22]" browser: '["msedge"]' integration-webkit: @@ -39,5 +39,5 @@ jobs: uses: ./.github/workflows/shared-integration.yml with: os: "macos-latest" - node_version: "[20]" + node_version: "[22]" browser: '["webkit"]' diff --git a/.github/workflows/shared-integration.yml b/.github/workflows/shared-integration.yml index 33536ca41f..cc2c3e0ccb 100644 --- a/.github/workflows/shared-integration.yml +++ b/.github/workflows/shared-integration.yml @@ -9,7 +9,7 @@ on: node_version: required: true # this is limited to string | boolean | number (https://github.uint.cloudmunity/t/can-action-inputs-be-arrays/16457) - # but we want to pass an array (node_version: "[18, 20]"), + # but we want to pass an array (node_version: "[20, 22]"), # so we'll need to manually stringify it for now type: string browser: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b85bab5c89..e19ec5116a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,8 +26,8 @@ jobs: fail-fast: false matrix: node: - - 18 - 20 + - 22 runs-on: ubuntu-latest diff --git a/docs/deploying/custom-node.md b/docs/deploying/custom-node.md index d3021a698b..af9ef966ed 100644 --- a/docs/deploying/custom-node.md +++ b/docs/deploying/custom-node.md @@ -7,3 +7,41 @@ title: Custom Node.js This document is a work in progress. There's not much to see here (yet). + +## Polyfilling `fetch` + +React Router officially supports Active and Maintenance[^1] [Node LTS veleases][node-releases] at any given point in time. Dropping support for End of Life Node versions may be done in a React Router Minor release. + +[^1]: Based on timing, React Router may drop support for a Node Maintenance LTS version shortly before it goes end-of-life if it better aligns with a React Router Major SemVer release. + +At the time React Router v7 was released, all versions had a usable `fetch` implementation so there is generally no need to polyfill any `fetch` APIs so long as you're on Node 22 or one of the later Node 20 releases. + +- Node 22 (Active LTS) has a stable [`fetch`][node-22-fetch] implementation +- Node 20 (Maintenance LTS) has an experimental (but suitable from our testing) [`fetch`][node-20-fetch] implementation + +If you do find that you need to polyfill anything, you can do so directly from the [undici] package which node uses internally. + +```ts +import { + fetch as nodeFetch, + File as NodeFile, + FormData as NodeFormData, + Headers as NodeHeaders, + Request as NodeRequest, + Response as NodeResponse, +} from "undici"; + +export function polyfillFetch() { + global.File = NodeFile; + global.Headers = NodeHeaders; + global.Request = NodeRequest; + global.Response = NodeResponse; + global.fetch = nodeFetch; + global.FormData = NodeFormData; +} +``` + +[node-releases]: https://nodejs.org/en/about/previous-releases +[node-20-fetch]: https://nodejs.org/docs/latest-v20.x/api/globals.html#fetch +[node-22-fetch]: https://nodejs.org/docs/latest-v22.x/api/globals.html#fetch +[undici]: https://github.com/nodejs/undici diff --git a/integration/helpers/create-fixture.ts b/integration/helpers/create-fixture.ts index f8358c51f6..ee4d9fe648 100644 --- a/integration/helpers/create-fixture.ts +++ b/integration/helpers/create-fixture.ts @@ -15,7 +15,6 @@ import { UNSAFE_decodeViaTurboStream as decodeViaTurboStream, } from "react-router"; import { createRequestHandler as createExpressHandler } from "@react-router/express"; -import { installGlobals } from "@react-router/node"; import { viteConfig } from "./vite.js"; @@ -43,8 +42,6 @@ export function json(value: JsonObject) { } export async function createFixture(init: FixtureInit, mode?: ServerMode) { - installGlobals(); - let projectDir = await createFixtureProject(init, mode); let buildPath = url.pathToFileURL( path.join(projectDir, "build/server/index.js") diff --git a/integration/helpers/node-template/package.json b/integration/helpers/node-template/package.json index cfe718a8b2..e43fc371a6 100644 --- a/integration/helpers/node-template/package.json +++ b/integration/helpers/node-template/package.json @@ -34,6 +34,6 @@ "typescript": "^5.1.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/integration/helpers/vite-cloudflare-template/package.json b/integration/helpers/vite-cloudflare-template/package.json index cb05e97ba9..62caf49967 100644 --- a/integration/helpers/vite-cloudflare-template/package.json +++ b/integration/helpers/vite-cloudflare-template/package.json @@ -30,6 +30,6 @@ "wrangler": "^3.28.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/integration/helpers/vite-template/package.json b/integration/helpers/vite-template/package.json index 2555f0659a..286a7c9ca7 100644 --- a/integration/helpers/vite-template/package.json +++ b/integration/helpers/vite-template/package.json @@ -36,6 +36,6 @@ "vite-tsconfig-paths": "^4.2.1" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/integration/helpers/vite.ts b/integration/helpers/vite.ts index cb09f54f9a..3e33597f6b 100644 --- a/integration/helpers/vite.ts +++ b/integration/helpers/vite.ts @@ -69,11 +69,8 @@ export const EXPRESS_SERVER = (args: { }) => String.raw` import { createRequestHandler } from "@react-router/express"; - import { installGlobals } from "@react-router/node"; import express from "express"; - installGlobals(); - let viteDevServer = process.env.NODE_ENV === "production" ? undefined diff --git a/integration/vite-basename-test.ts b/integration/vite-basename-test.ts index 42f5c25e6c..f723b625b5 100644 --- a/integration/vite-basename-test.ts +++ b/integration/vite-basename-test.ts @@ -95,9 +95,7 @@ const customServerFile = ({ return js` import { createRequestHandler } from "@react-router/express"; - import { installGlobals } from "@react-router/node"; import express from "express"; - installGlobals(); const viteDevServer = process.env.NODE_ENV === "production" @@ -488,9 +486,7 @@ test.describe("Vite base / React Router basename / express build", async () => { // Slim server that only serves basename (route) requests from the React Router handler "server.mjs": String.raw` import { createRequestHandler } from "@react-router/express"; - import { installGlobals } from "@react-router/node"; import express from "express"; - installGlobals(); const app = express(); app.all( diff --git a/jest/jest.config.shared.js b/jest/jest.config.shared.js index 186df726b9..05322e3a9b 100644 --- a/jest/jest.config.shared.js +++ b/jest/jest.config.shared.js @@ -19,7 +19,6 @@ module.exports = { ), }, modulePathIgnorePatterns: ignorePatterns, - setupFiles: ["/__tests__/setup.ts"], testMatch: ["/**/*-test.[jt]s?(x)"], transform: { "\\.[jt]sx?$": require.resolve("./transform"), diff --git a/package.json b/package.json index 73cf006e6f..e3fb9b90d6 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,6 @@ "@types/wait-on": "^5.3.2", "@typescript-eslint/eslint-plugin": "^7.5.0", "@typescript-eslint/parser": "^7.5.0", - "abort-controller": "^3.0.0", "babel-jest": "^29.7.0", "babel-plugin-dev-expression": "^0.2.3", "babel-plugin-transform-remove-console": "^6.9.4", @@ -122,7 +121,7 @@ "vite-tsconfig-paths": "^4.2.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "pnpm": { "patchedDependencies": { diff --git a/packages/react-router-architect/__tests__/setup.ts b/packages/react-router-architect/__tests__/setup.ts deleted file mode 100644 index 996e99893d..0000000000 --- a/packages/react-router-architect/__tests__/setup.ts +++ /dev/null @@ -1,2 +0,0 @@ -import { installGlobals } from "@react-router/node"; -installGlobals(); diff --git a/packages/react-router-architect/package.json b/packages/react-router-architect/package.json index a9664a798f..0029467b24 100644 --- a/packages/react-router-architect/package.json +++ b/packages/react-router-architect/package.json @@ -48,7 +48,7 @@ } }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "files": [ "dist/", diff --git a/packages/react-router-cloudflare/package.json b/packages/react-router-cloudflare/package.json index 472d627722..f261eb7812 100644 --- a/packages/react-router-cloudflare/package.json +++ b/packages/react-router-cloudflare/package.json @@ -40,7 +40,7 @@ } }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "files": [ "dist/", diff --git a/packages/react-router-dev/__tests__/fixtures/node/package.json b/packages/react-router-dev/__tests__/fixtures/node/package.json index ffff19de35..922e211c64 100644 --- a/packages/react-router-dev/__tests__/fixtures/node/package.json +++ b/packages/react-router-dev/__tests__/fixtures/node/package.json @@ -24,6 +24,6 @@ "typescript": "^5.1.6" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/packages/react-router-dev/cli/run.ts b/packages/react-router-dev/cli/run.ts index 5751d49a68..518a92f1e0 100644 --- a/packages/react-router-dev/cli/run.ts +++ b/packages/react-router-dev/cli/run.ts @@ -81,9 +81,15 @@ ${colors.logoBlue("react-router")} export async function run(argv: string[] = process.argv.slice(2)) { // Check the node version let versions = process.versions; - if (versions && versions.node && semver.major(versions.node) < 18) { + let MINIMUM_NODE_VERSION = 20; + if ( + versions && + versions.node && + semver.major(versions.node) < MINIMUM_NODE_VERSION + ) { throw new Error( - `️🚨 Oops, Node v${versions.node} detected. react-router requires a Node version greater than 18.` + `️🚨 Oops, Node v${versions.node} detected. react-router requires ` + + `a Node version greater than ${MINIMUM_NODE_VERSION}.` ); } diff --git a/packages/react-router-dev/package.json b/packages/react-router-dev/package.json index 7acf8e2124..c9abd90fd9 100644 --- a/packages/react-router-dev/package.json +++ b/packages/react-router-dev/package.json @@ -112,7 +112,7 @@ } }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "files": [ "dist/", diff --git a/packages/react-router-dom/package.json b/packages/react-router-dom/package.json index dcfa6fcefd..66576cf452 100644 --- a/packages/react-router-dom/package.json +++ b/packages/react-router-dom/package.json @@ -47,6 +47,6 @@ "README.md" ], "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/packages/react-router-express/__tests__/setup.ts b/packages/react-router-express/__tests__/setup.ts deleted file mode 100644 index 996e99893d..0000000000 --- a/packages/react-router-express/__tests__/setup.ts +++ /dev/null @@ -1,2 +0,0 @@ -import { installGlobals } from "@react-router/node"; -installGlobals(); diff --git a/packages/react-router-express/package.json b/packages/react-router-express/package.json index d714b261bb..127acb5c50 100644 --- a/packages/react-router-express/package.json +++ b/packages/react-router-express/package.json @@ -46,7 +46,7 @@ } }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "files": [ "dist/", diff --git a/packages/react-router-fs-routes/package.json b/packages/react-router-fs-routes/package.json index 6f1792964c..1a8e28a7b2 100644 --- a/packages/react-router-fs-routes/package.json +++ b/packages/react-router-fs-routes/package.json @@ -40,7 +40,7 @@ } }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "files": [ "dist/", diff --git a/packages/react-router-node/__tests__/setup.ts b/packages/react-router-node/__tests__/setup.ts deleted file mode 100644 index 53d59d28c0..0000000000 --- a/packages/react-router-node/__tests__/setup.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { installGlobals } from "../globals"; - -installGlobals(); diff --git a/packages/react-router-node/globals.ts b/packages/react-router-node/globals.ts deleted file mode 100644 index cbd188c312..0000000000 --- a/packages/react-router-node/globals.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { - File as NodeFile, - fetch as nodeFetch, - FormData as NodeFormData, - Headers as NodeHeaders, - Request as NodeRequest, - Response as NodeResponse, -} from "undici"; -import { webcrypto as nodeWebCrypto } from "node:crypto"; - -declare global { - namespace NodeJS { - interface ProcessEnv { - NODE_ENV: "development" | "production" | "test"; - } - - interface Global { - File: typeof File; - - Headers: typeof Headers; - Request: typeof Request; - Response: typeof Response; - fetch: typeof fetch; - FormData: typeof FormData; - - ReadableStream: typeof ReadableStream; - WritableStream: typeof WritableStream; - - crypto: typeof nodeWebCrypto; - } - } - - interface RequestInit { - duplex?: "half"; - } -} - -export function installGlobals() { - global.File = NodeFile as unknown as typeof File; - // @ts-ignore - this shows as an error in VSCode but is not an error via TSC so we can't use `ts-expect-error` - global.Headers = NodeHeaders; - // @ts-expect-error - overriding globals - global.Request = NodeRequest; - // @ts-expect-error - overriding globals - global.Response = NodeResponse; - // @ts-expect-error - overriding globals - global.fetch = nodeFetch; - // @ts-expect-error - overriding globals - global.FormData = NodeFormData; - - if (!global.crypto) { - // @ts-expect-error - overriding globals - global.crypto = nodeWebCrypto; - } -} diff --git a/packages/react-router-node/index.ts b/packages/react-router-node/index.ts index 616231434b..7d7c108d44 100644 --- a/packages/react-router-node/index.ts +++ b/packages/react-router-node/index.ts @@ -1,5 +1,3 @@ -export { installGlobals } from "./globals"; - export { createFileSessionStorage } from "./sessions/fileStorage"; export { diff --git a/packages/react-router-node/install.ts b/packages/react-router-node/install.ts deleted file mode 100644 index e5bc566ae4..0000000000 --- a/packages/react-router-node/install.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { installGlobals } from "./globals"; - -installGlobals(); diff --git a/packages/react-router-node/package.json b/packages/react-router-node/package.json index e4640d532a..a4ffb33922 100644 --- a/packages/react-router-node/package.json +++ b/packages/react-router-node/package.json @@ -18,10 +18,6 @@ "types": "./dist/index.d.ts", "default": "./dist/index.js" }, - "./install": { - "types": "./dist/install.d.ts", - "default": "./dist/install.js" - }, "./package.json": "./package.json" }, "sideEffects": [ @@ -52,7 +48,7 @@ } }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "files": [ "dist/", diff --git a/packages/react-router-node/rollup.config.js b/packages/react-router-node/rollup.config.js index 49266fca02..b100ca6c4c 100644 --- a/packages/react-router-node/rollup.config.js +++ b/packages/react-router-node/rollup.config.js @@ -21,11 +21,9 @@ module.exports = function rollup() { "react-router-node" ); - const input = [`${SOURCE_DIR}/index.ts`, `${SOURCE_DIR}/install.ts`]; - return [ { - input, + input: `${SOURCE_DIR}/index.ts`, external: (id) => isBareModuleId(id), output: { banner: createBanner(name, version), diff --git a/packages/react-router-remix-config-routes-adapter/package.json b/packages/react-router-remix-config-routes-adapter/package.json index ec6a103d95..ed5795e7e3 100644 --- a/packages/react-router-remix-config-routes-adapter/package.json +++ b/packages/react-router-remix-config-routes-adapter/package.json @@ -37,7 +37,7 @@ } }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "files": [ "dist/", diff --git a/packages/react-router-serve/cli.ts b/packages/react-router-serve/cli.ts index 8929f8624e..acd33b61b9 100644 --- a/packages/react-router-serve/cli.ts +++ b/packages/react-router-serve/cli.ts @@ -1,10 +1,8 @@ -import "@react-router/node/install"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import url from "node:url"; import type { ServerBuild } from "react-router"; -import { installGlobals } from "@react-router/node"; import { createRequestHandler } from "@react-router/express"; import compression from "compression"; import express from "express"; @@ -30,7 +28,6 @@ sourceMapSupport.install({ return null; }, }); -installGlobals(); run(); diff --git a/packages/react-router-serve/package.json b/packages/react-router-serve/package.json index f2198dbf7f..977299e83d 100644 --- a/packages/react-router-serve/package.json +++ b/packages/react-router-serve/package.json @@ -39,7 +39,7 @@ "@types/source-map-support": "^0.5.6" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "files": [ "dist/", diff --git a/packages/react-router/__tests__/router/navigation-test.ts b/packages/react-router/__tests__/router/navigation-test.ts index 9210490c91..356f6ec38d 100644 --- a/packages/react-router/__tests__/router/navigation-test.ts +++ b/packages/react-router/__tests__/router/navigation-test.ts @@ -171,11 +171,10 @@ describe("navigations", () => { ); expect(t.router.state.loaderData).toEqual({}); - // Node 16/18 versus 20 output different errors here :/ - let expected = process.version.startsWith("v18") - ? "Unexpected token } in JSON at position 15" - : "Unexpected non-whitespace character after JSON at position 15"; - expect(t.router.state.errors?.foo).toEqual(new SyntaxError(expected)); + expect(t.router.state.errors?.foo).toBeInstanceOf(SyntaxError); + expect(t.router.state.errors?.foo.message).toContain( + "Unexpected non-whitespace character after JSON at position 15" + ); }); it("bubbles errors when unwrapping Responses", async () => { @@ -207,11 +206,10 @@ describe("navigations", () => { ); expect(t.router.state.loaderData).toEqual({}); - // Node 16/18 versus 20 output different errors here :/ - let expected = process.version.startsWith("v18") - ? "Unexpected token } in JSON at position 15" - : "Unexpected non-whitespace character after JSON at position 15"; - expect(t.router.state.errors?.root).toEqual(new SyntaxError(expected)); + expect(t.router.state.errors?.root).toBeInstanceOf(SyntaxError); + expect(t.router.state.errors?.root.message).toContain( + "Unexpected non-whitespace character after JSON at position 15" + ); }); it("does not fetch unchanging layout data", async () => { diff --git a/packages/react-router/__tests__/router/router-memory-test.ts b/packages/react-router/__tests__/router/router-memory-test.ts index 49ae9617c7..5a06d3510f 100644 --- a/packages/react-router/__tests__/router/router-memory-test.ts +++ b/packages/react-router/__tests__/router/router-memory-test.ts @@ -211,36 +211,4 @@ describe("a memory router", () => { router.dispose(); }); - - it("throws on submitting FormData when it's not available", async () => { - if (global.FormData) { - // This is globally available in Node 18, this test is primarily for Node 16 - // eslint-disable-next-line jest/no-conditional-expect - expect(true).toBe(true); - return; - } - - let actionSpy = jest.fn(); - - let router = createRouter({ - routes: [ - { - path: "/", - action: actionSpy, - }, - ], - history: createMemoryHistory(), - }); - - await expect(() => - router.navigate("/", { - formMethod: "post", - body: { key: "value" }, - }) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"FormData is not available in this environment"` - ); - - router.dispose(); - }); }); diff --git a/packages/react-router/__tests__/router/ssr-test.ts b/packages/react-router/__tests__/router/ssr-test.ts index a38afa1f6e..4ef923a368 100644 --- a/packages/react-router/__tests__/router/ssr-test.ts +++ b/packages/react-router/__tests__/router/ssr-test.ts @@ -753,9 +753,6 @@ describe("ssr", () => { let e; try { let contextPromise = query(request); - // Note this works in Node 18+ - but it does not work if using the - // `abort-controller` polyfill which doesn't yet support a custom `reason` - // See: https://github.com/mysticatea/abort-controller/issues/33 controller.abort(new Error("Oh no!")); // This should resolve even though we never resolved the loader await contextPromise; @@ -2082,9 +2079,6 @@ describe("ssr", () => { let e; try { let statePromise = queryRoute(request, { routeId: "root" }); - // Note this works in Node 18+ - but it does not work if using the - // `abort-controller` polyfill which doesn't yet support a custom `reason` - // See: https://github.com/mysticatea/abort-controller/issues/33 controller.abort(new Error("Oh no!")); // This should resolve even though we never resolved the loader await statePromise; diff --git a/packages/react-router/__tests__/setup.ts b/packages/react-router/__tests__/setup.ts index 357b5924bc..8c208adb43 100644 --- a/packages/react-router/__tests__/setup.ts +++ b/packages/react-router/__tests__/setup.ts @@ -1,15 +1,19 @@ // https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#configuring-your-testing-environment globalThis.IS_REACT_ACT_ENVIRONMENT = true; -if (!globalThis.fetch) { +if (!globalThis.TextEncoder || !globalThis.TextDecoder) { const { TextDecoder, TextEncoder } = require("node:util"); - globalThis.TextDecoder = TextDecoder; globalThis.TextEncoder = TextEncoder; + globalThis.TextDecoder = TextDecoder; +} +if (!globalThis.ReadableStream || !globalThis.WritableStream) { const { ReadableStream, WritableStream } = require("node:stream/web"); globalThis.ReadableStream = ReadableStream; globalThis.WritableStream = WritableStream; +} +if (!globalThis.fetch) { const { fetch, FormData, Request, Response, Headers } = require("undici"); globalThis.fetch = fetch; @@ -20,17 +24,6 @@ if (!globalThis.fetch) { globalThis.FormData = globalThis.FormData || FormData; } -if (!globalThis.AbortController) { - const { AbortController } = require("abort-controller"); - globalThis.AbortController = AbortController; -} - -if (!globalThis.TextEncoder || !globalThis.TextDecoder) { - const { TextDecoder, TextEncoder } = require("node:util"); - globalThis.TextEncoder = TextEncoder; - globalThis.TextDecoder = TextDecoder; -} - if (!globalThis.TextEncoderStream) { const { TextEncoderStream } = require("node:stream/web"); globalThis.TextEncoderStream = TextEncoderStream; @@ -40,13 +33,3 @@ if (!globalThis.TransformStream) { const { TransformStream } = require("node:stream/web"); globalThis.TransformStream = TransformStream; } - -if (!globalThis.File) { - const { File } = require("undici"); - globalThis.File = File; -} - -if (!globalThis.crypto) { - const { webcrypto } = require("node:crypto"); - globalThis.crypto = webcrypto; -} diff --git a/packages/react-router/jest.config.js b/packages/react-router/jest.config.js index 8408526b8d..dc45566ce5 100644 --- a/packages/react-router/jest.config.js +++ b/packages/react-router/jest.config.js @@ -1,6 +1,7 @@ /** @type {import('jest').Config} */ module.exports = { ...require("../../jest/jest.config.shared"), + setupFiles: ["/__tests__/setup.ts"], setupFilesAfterEnv: ["@testing-library/jest-dom"], testEnvironment: "jsdom", }; diff --git a/packages/react-router/package.json b/packages/react-router/package.json index 89f65b333b..6b1ad4a02f 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -68,6 +68,6 @@ "README.md" ], "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/playground/compiler-express/package.json b/playground/compiler-express/package.json index 33e0243b91..3902c0aafc 100644 --- a/playground/compiler-express/package.json +++ b/playground/compiler-express/package.json @@ -34,6 +34,6 @@ "vite-tsconfig-paths": "^4.2.1" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/playground/compiler-express/server.js b/playground/compiler-express/server.js index 9d28534e9f..fa5048f32c 100644 --- a/playground/compiler-express/server.js +++ b/playground/compiler-express/server.js @@ -1,11 +1,8 @@ import { createRequestHandler } from "@react-router/express"; -import { installGlobals } from "@react-router/node"; import compression from "compression"; import express from "express"; import morgan from "morgan"; -installGlobals(); - const viteDevServer = process.env.NODE_ENV === "production" ? undefined diff --git a/playground/compiler-spa/package.json b/playground/compiler-spa/package.json index 503fac9554..b42a057d06 100644 --- a/playground/compiler-spa/package.json +++ b/playground/compiler-spa/package.json @@ -25,6 +25,6 @@ "vite-tsconfig-paths": "^4.2.1" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/playground/compiler/package.json b/playground/compiler/package.json index 1c0efa9e8a..d6d8a3d145 100644 --- a/playground/compiler/package.json +++ b/playground/compiler/package.json @@ -27,6 +27,6 @@ "vite-tsconfig-paths": "^4.2.1" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/playground/compiler/vite.config.ts b/playground/compiler/vite.config.ts index 6735e6449d..f910ad4c18 100644 --- a/playground/compiler/vite.config.ts +++ b/playground/compiler/vite.config.ts @@ -1,10 +1,7 @@ import { reactRouter } from "@react-router/dev/vite"; -import { installGlobals } from "@react-router/node"; import { defineConfig } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; -installGlobals(); - export default defineConfig({ plugins: [reactRouter(), tsconfigPaths()], }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c1d9354a59..727fdd41cd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -133,9 +133,6 @@ importers: '@typescript-eslint/parser': specifier: ^7.5.0 version: 7.5.0(eslint@8.57.0)(typescript@5.4.5) - abort-controller: - specifier: ^3.0.0 - version: 3.0.0 babel-jest: specifier: ^29.7.0 version: 29.7.0(@babel/core@7.22.9) @@ -3108,10 +3105,6 @@ packages: resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} deprecated: Use your platform's native atob() and btoa() methods instead - abort-controller@3.0.0: - resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} - engines: {node: '>=6.5'} - accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -4260,10 +4253,6 @@ packages: resolution: {integrity: sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==} engines: {node: '>= 0.8'} - event-target-shim@5.0.1: - resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} - engines: {node: '>=6'} - execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -10537,10 +10526,6 @@ snapshots: abab@2.0.6: {} - abort-controller@3.0.0: - dependencies: - event-target-shim: 5.0.1 - accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -12090,8 +12075,6 @@ snapshots: '@types/node': 18.19.26 require-like: 0.1.2 - event-target-shim@5.0.1: {} - execa@5.1.1: dependencies: cross-spawn: 7.0.3 diff --git a/rollup.utils.js b/rollup.utils.js index e131cc72df..4de511b754 100644 --- a/rollup.utils.js +++ b/rollup.utils.js @@ -143,7 +143,7 @@ function isBareModuleId(id) { const remixBabelConfig = { presets: [ - ["@babel/preset-env", { targets: { node: "18" } }], + ["@babel/preset-env", { targets: { node: "20" } }], "@babel/preset-react", "@babel/preset-typescript", ], From e00b62528cdb45bdc7958c074b5613805ae435ac Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 22 Oct 2024 11:25:46 -0400 Subject: [PATCH 29/33] Adjust footnote --- docs/deploying/custom-node.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deploying/custom-node.md b/docs/deploying/custom-node.md index af9ef966ed..bcbfdc39d3 100644 --- a/docs/deploying/custom-node.md +++ b/docs/deploying/custom-node.md @@ -10,7 +10,7 @@ title: Custom Node.js ## Polyfilling `fetch` -React Router officially supports Active and Maintenance[^1] [Node LTS veleases][node-releases] at any given point in time. Dropping support for End of Life Node versions may be done in a React Router Minor release. +React Router officially supports Active and Maintenance[Node LTS veleases][node-releases] ([^1]) at any given point in time. Dropping support for End of Life Node versions may be done in a React Router Minor release. [^1]: Based on timing, React Router may drop support for a Node Maintenance LTS version shortly before it goes end-of-life if it better aligns with a React Router Major SemVer release. From 31a9ad8477544f2d41cc41e49422c08c286ab88e Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 22 Oct 2024 11:26:40 -0400 Subject: [PATCH 30/33] Separate out version support --- docs/deploying/custom-node.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/deploying/custom-node.md b/docs/deploying/custom-node.md index bcbfdc39d3..3aa7727672 100644 --- a/docs/deploying/custom-node.md +++ b/docs/deploying/custom-node.md @@ -8,12 +8,14 @@ title: Custom Node.js This document is a work in progress. There's not much to see here (yet). -## Polyfilling `fetch` +## Version Support React Router officially supports Active and Maintenance[Node LTS veleases][node-releases] ([^1]) at any given point in time. Dropping support for End of Life Node versions may be done in a React Router Minor release. [^1]: Based on timing, React Router may drop support for a Node Maintenance LTS version shortly before it goes end-of-life if it better aligns with a React Router Major SemVer release. +## Polyfilling `fetch` + At the time React Router v7 was released, all versions had a usable `fetch` implementation so there is generally no need to polyfill any `fetch` APIs so long as you're on Node 22 or one of the later Node 20 releases. - Node 22 (Active LTS) has a stable [`fetch`][node-22-fetch] implementation From 5a363e58cd92cdf0881bf6009f208b2dd7ea8bc7 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 23 Oct 2024 15:13:13 -0400 Subject: [PATCH 31/33] Clean up types within consolidated package (#12177) --- .changeset/sour-cycles-lie.md | 11 + packages/react-router-dev/vite/plugin.ts | 10 +- packages/react-router-dev/vite/styles.ts | 19 +- packages/react-router/index.ts | 16 -- .../lib/dom-export/hydrated-router.tsx | 1 + .../react-router/lib/dom/ssr/components.tsx | 9 +- packages/react-router/lib/dom/ssr/data.ts | 5 - packages/react-router/lib/dom/ssr/entry.ts | 7 +- .../react-router/lib/dom/ssr/fog-of-war.ts | 17 +- packages/react-router/lib/dom/ssr/links.ts | 21 +- .../react-router/lib/dom/ssr/routeModules.ts | 87 ++++--- packages/react-router/lib/dom/ssr/routes.tsx | 19 +- packages/react-router/lib/dom/ssr/server.tsx | 1 + .../react-router/lib/dom/ssr/single-fetch.tsx | 11 +- packages/react-router/lib/router/utils.ts | 5 +- .../react-router/lib/server-runtime/build.ts | 2 +- .../react-router/lib/server-runtime/data.ts | 13 +- .../react-router/lib/server-runtime/entry.ts | 5 +- .../lib/server-runtime/headers.ts | 5 +- .../lib/server-runtime/routeModules.ts | 217 +----------------- .../react-router/lib/server-runtime/routes.ts | 51 +--- .../react-router/lib/server-runtime/server.ts | 8 +- 22 files changed, 175 insertions(+), 365 deletions(-) create mode 100644 .changeset/sour-cycles-lie.md diff --git a/.changeset/sour-cycles-lie.md b/.changeset/sour-cycles-lie.md new file mode 100644 index 0000000000..6cc890d54e --- /dev/null +++ b/.changeset/sour-cycles-lie.md @@ -0,0 +1,11 @@ +--- +"@react-router/dev": major +"react-router": major +--- + +- Consolidate types previously duplicated across `@remix-run/router`, `@remix-run/server-runtime`, and `@remix-run/react` now that they all live in `react-router` + - Examples: `LoaderFunction`, `LoaderFunctionArgs`, `ActionFunction`, `ActionFunctionArgs`, `DataFunctionArgs`, `RouteManifest`, `LinksFunction`, `Route`, `EntryRoute` + - The `RouteManifest` type used by the "remix" code is now slightly stricter because it is using the former `@remix-run/router` `RouteManifest` + - `Record -> Record` + - Removed `AppData` type in favor of inlining `unknown` in the few locations it was used + - Removed `ServerRuntimeMeta*` types in favor of the `Meta*` types they were duplicated from diff --git a/packages/react-router-dev/vite/plugin.ts b/packages/react-router-dev/vite/plugin.ts index 2d87c13bfc..3f0bc19a3d 100644 --- a/packages/react-router-dev/vite/plugin.ts +++ b/packages/react-router-dev/vite/plugin.ts @@ -2023,11 +2023,13 @@ function groupRoutesByParentId(manifest: ServerBuild["routes"]) { let routes: Record[]> = {}; Object.values(manifest).forEach((route) => { - let parentId = route.parentId || ""; - if (!routes[parentId]) { - routes[parentId] = []; + if (route) { + let parentId = route.parentId || ""; + if (!routes[parentId]) { + routes[parentId] = []; + } + routes[parentId].push(route); } - routes[parentId].push(route); }); return routes; diff --git a/packages/react-router-dev/vite/styles.ts b/packages/react-router-dev/vite/styles.ts index 2db7e89287..1e16372135 100644 --- a/packages/react-router-dev/vite/styles.ts +++ b/packages/react-router-dev/vite/styles.ts @@ -171,14 +171,16 @@ const findDeps = async ( }; const groupRoutesByParentId = (manifest: ServerRouteManifest) => { - let routes: Record[]> = {}; + let routes: Record[]> = {}; Object.values(manifest).forEach((route) => { - let parentId = route.parentId || ""; - if (!routes[parentId]) { - routes[parentId] = []; + if (route) { + let parentId = route.parentId || ""; + if (!routes[parentId]) { + routes[parentId] = []; + } + routes[parentId].push(route); } - routes[parentId].push(route); }); return routes; @@ -189,11 +191,8 @@ const groupRoutesByParentId = (manifest: ServerRouteManifest) => { const createRoutes = ( manifest: ServerRouteManifest, parentId: string = "", - routesByParentId: Record< - string, - Omit[] - > = groupRoutesByParentId(manifest) -): ServerRoute[] => { + routesByParentId = groupRoutesByParentId(manifest) +): NonNullable[] => { return (routesByParentId[parentId] || []).map((route) => ({ ...route, children: createRoutes(manifest, route.id, routesByParentId), diff --git a/packages/react-router/index.ts b/packages/react-router/index.ts index 0e63a1d4b0..8edab6eacb 100644 --- a/packages/react-router/index.ts +++ b/packages/react-router/index.ts @@ -213,12 +213,6 @@ export { createRoutesStub } from "./lib/dom/ssr/routes-test-stub"; // Expose old @remix-run/server-runtime API, minus duplicate APIs export { createCookie, isCookie } from "./lib/server-runtime/cookies"; -// TODO: (v7) Clean up code paths for these exports -// export { -// json, -// redirect, -// redirectDocument, -// } from "./lib/server-runtime/responses"; export { createRequestHandler } from "./lib/server-runtime/server"; export { createSession, @@ -258,16 +252,6 @@ export type { } from "./lib/router/links"; export type { - // TODO: (v7) Clean up code paths for these exports - // ActionFunction, - // ActionFunctionArgs, - // LinksFunction, - // LoaderFunction, - // LoaderFunctionArgs, - // ServerRuntimeMetaArgs, - // ServerRuntimeMetaDescriptor, - // ServerRuntimeMetaFunction, - DataFunctionArgs, HeadersArgs, HeadersFunction, } from "./lib/server-runtime/routeModules"; diff --git a/packages/react-router/lib/dom-export/hydrated-router.tsx b/packages/react-router/lib/dom-export/hydrated-router.tsx index 295470be06..af8ae89b04 100644 --- a/packages/react-router/lib/dom-export/hydrated-router.tsx +++ b/packages/react-router/lib/dom-export/hydrated-router.tsx @@ -133,6 +133,7 @@ function createHydratedRouter(): DataRouter { // * or doesn't have a server loader and we have no data to render if ( route && + manifestRoute && shouldHydrateRouteLoader( manifestRoute, route, diff --git a/packages/react-router/lib/dom/ssr/components.tsx b/packages/react-router/lib/dom/ssr/components.tsx index c0a5c5914e..e91636138f 100644 --- a/packages/react-router/lib/dom/ssr/components.tsx +++ b/packages/react-router/lib/dom/ssr/components.tsx @@ -366,7 +366,8 @@ function PrefetchPageLinksImpl({ let routesParams = new Set(); let foundOptOutRoute = false; nextMatches.forEach((m) => { - if (!manifest.routes[m.route.id].hasLoader) { + let manifestRoute = manifest.routes[m.route.id]; + if (!manifestRoute || !manifestRoute.hasLoader) { return; } @@ -376,7 +377,7 @@ function PrefetchPageLinksImpl({ routeModules[m.route.id]?.shouldRevalidate ) { foundOptOutRoute = true; - } else if (manifest.routes[m.route.id].hasClientLoader) { + } else if (manifestRoute.hasClientLoader) { foundOptOutRoute = true; } else { routesParams.add(m.route.id); @@ -682,7 +683,7 @@ ${matches .map( (match, index) => `import * as route${index} from ${JSON.stringify( - manifest.routes[match.route.id].module + manifest.routes[match.route.id]!.module )};` ) .join("\n")} @@ -728,7 +729,7 @@ import(${JSON.stringify(manifest.entry.module)});`; let routePreloads = matches .map((match) => { let route = manifest.routes[match.route.id]; - return (route.imports || []).concat([route.module]); + return route ? (route.imports || []).concat([route.module]) : []; }) .flat(1); diff --git a/packages/react-router/lib/dom/ssr/data.ts b/packages/react-router/lib/dom/ssr/data.ts index 0705ce47a5..bc0aeab8e9 100644 --- a/packages/react-router/lib/dom/ssr/data.ts +++ b/packages/react-router/lib/dom/ssr/data.ts @@ -1,10 +1,5 @@ import "../global"; -/** - * Data for a route that was returned from a `loader()`. - */ -export type AppData = unknown; - export async function createRequestInit( request: Request ): Promise { diff --git a/packages/react-router/lib/dom/ssr/entry.ts b/packages/react-router/lib/dom/ssr/entry.ts index 76e0f29c2d..bded38733d 100644 --- a/packages/react-router/lib/dom/ssr/entry.ts +++ b/packages/react-router/lib/dom/ssr/entry.ts @@ -1,14 +1,15 @@ import type { StaticHandlerContext } from "../../router/router"; -import type { RouteManifest, EntryRoute } from "./routes"; +import type { EntryRoute } from "./routes"; import type { RouteModules } from "./routeModules"; - -// Object passed to RemixContext.Provider +import type { RouteManifest } from "../../router/utils"; type SerializedError = { message: string; stack?: string; }; + +// Object passed to RemixContext.Provider export interface FrameworkContextObject { manifest: AssetsManifest; routeModules: RouteModules; diff --git a/packages/react-router/lib/dom/ssr/fog-of-war.ts b/packages/react-router/lib/dom/ssr/fog-of-war.ts index 366a4eb49f..a9bfa8de6d 100644 --- a/packages/react-router/lib/dom/ssr/fog-of-war.ts +++ b/packages/react-router/lib/dom/ssr/fog-of-war.ts @@ -1,9 +1,11 @@ import * as React from "react"; import type { PatchRoutesOnNavigationFunction } from "../../context"; import type { Router as DataRouter } from "../../router/router"; +import type { RouteManifest } from "../../router/utils"; import { matchRoutes } from "../../router/utils"; import type { AssetsManifest } from "./entry"; import type { RouteModules } from "./routeModules"; +import type { EntryRoute } from "./routes"; import { createClientRoutes } from "./routes"; declare global { @@ -233,13 +235,12 @@ export async function fetchAndApplyManifestPatches( // Patch routes we don't know about yet into the manifest let knownRoutes = new Set(Object.keys(manifest.routes)); - let patches: AssetsManifest["routes"] = Object.values(serverPatches).reduce( - (acc, route) => - !knownRoutes.has(route.id) - ? Object.assign(acc, { [route.id]: route }) - : acc, - {} - ); + let patches = Object.values(serverPatches).reduce((acc, route) => { + if (route && !knownRoutes.has(route.id)) { + acc[route.id] = route; + } + return acc; + }, {} as RouteManifest); Object.assign(manifest.routes, patches); // Track discovered paths so we don't have to fetch them again @@ -249,7 +250,7 @@ export async function fetchAndApplyManifestPatches( // in their new children let parentIds = new Set(); Object.values(patches).forEach((patch) => { - if (!patch.parentId || !patches[patch.parentId]) { + if (patch && (!patch.parentId || !patches[patch.parentId])) { parentIds.add(patch.parentId); } }); diff --git a/packages/react-router/lib/dom/ssr/links.ts b/packages/react-router/lib/dom/ssr/links.ts index 50bb073e92..860aaa8f89 100644 --- a/packages/react-router/lib/dom/ssr/links.ts +++ b/packages/react-router/lib/dom/ssr/links.ts @@ -26,7 +26,9 @@ export function getKeyedLinksForMatches( let module = routeModules[match.route.id]; let route = manifest.routes[match.route.id]; return [ - route.css ? route.css.map((href) => ({ rel: "stylesheet", href })) : [], + route && route.css + ? route.css.map((href) => ({ rel: "stylesheet", href })) + : [], module?.links?.() || [], ]; }) @@ -138,11 +140,12 @@ export async function getKeyedPrefetchLinks( ): Promise { let links = await Promise.all( matches.map(async (match) => { - let mod = await loadRouteModule( - manifest.routes[match.route.id], - routeModules - ); - return mod.links ? mod.links() : []; + let route = manifest.routes[match.route.id]; + if (route) { + let mod = await loadRouteModule(route, routeModules); + return mod.links ? mod.links() : []; + } + return []; }) ); @@ -197,7 +200,7 @@ export function getNewMatchesForLinks( if (mode === "data") { return nextMatches.filter((match, index) => { let manifestRoute = manifest.routes[match.route.id]; - if (!manifestRoute.hasLoader) { + if (!manifestRoute || !manifestRoute.hasLoader) { return false; } @@ -235,6 +238,7 @@ export function getModuleLinkHrefs( matches .map((match) => { let route = manifestPatch.routes[match.route.id]; + if (!route) return []; let hrefs = [route.module]; if (route.imports) { hrefs = hrefs.concat(route.imports); @@ -256,12 +260,11 @@ function getCurrentPageModulePreloadHrefs( matches .map((match) => { let route = manifest.routes[match.route.id]; + if (!route) return []; let hrefs = [route.module]; - if (route.imports) { hrefs = hrefs.concat(route.imports); } - return hrefs; }) .flat(1) diff --git a/packages/react-router/lib/dom/ssr/routeModules.ts b/packages/react-router/lib/dom/ssr/routeModules.ts index 45d2f0294d..5c3500faa4 100644 --- a/packages/react-router/lib/dom/ssr/routeModules.ts +++ b/packages/react-router/lib/dom/ssr/routeModules.ts @@ -1,17 +1,15 @@ import type { ComponentType, ReactElement } from "react"; import type { Location } from "../../router/history"; import type { - ActionFunction as RRActionFunction, - ActionFunctionArgs as RRActionFunctionArgs, - LoaderFunction as RRLoaderFunction, - LoaderFunctionArgs as RRLoaderFunctionArgs, + ActionFunction, + ActionFunctionArgs, + LoaderFunction, + LoaderFunctionArgs, Params, ShouldRevalidateFunction, - LoaderFunctionArgs, } from "../../router/utils"; import type { SerializeFrom } from "./components"; -import type { AppData } from "./data"; import type { EntryRoute } from "./routes"; import type { DataRouteMatch } from "../../context"; import type { LinkDescriptor } from "../../router/links"; @@ -38,13 +36,13 @@ export interface RouteModule { */ export type ClientActionFunction = ( args: ClientActionFunctionArgs -) => ReturnType; +) => ReturnType; /** * Arguments passed to a route `clientAction` function */ -export type ClientActionFunctionArgs = RRActionFunctionArgs & { - serverAction: () => Promise>; +export type ClientActionFunctionArgs = ActionFunctionArgs & { + serverAction: () => Promise>; }; /** @@ -52,15 +50,15 @@ export type ClientActionFunctionArgs = RRActionFunctionArgs & { */ export type ClientLoaderFunction = (( args: ClientLoaderFunctionArgs -) => ReturnType) & { +) => ReturnType) & { hydrate?: boolean; }; /** * Arguments passed to a route `clientLoader` function */ -export type ClientLoaderFunctionArgs = RRLoaderFunctionArgs & { - serverLoader: () => Promise>; +export type ClientLoaderFunctionArgs = LoaderFunctionArgs & { + serverLoader: () => Promise>; }; /** @@ -96,19 +94,6 @@ export interface LinksFunction { (): LinkDescriptor[]; } -// Loose copy from @react-router/server-runtime to avoid circular imports -type LoaderFunction = ( - args: LoaderFunctionArgs & { - // Context is always provided in Remix, and typed for module augmentation support. - context: unknown; - // TODO: (v7) Make this non-optional once single-fetch is the default - response?: { - status: number | undefined; - headers: Headers; - }; - } -) => ReturnType; - export interface MetaMatch< RouteId extends string = string, Loader extends LoaderFunction | unknown = unknown @@ -144,7 +129,7 @@ export interface MetaArgs< > > { data: - | (Loader extends LoaderFunction ? SerializeFrom : AppData) + | (Loader extends LoaderFunction ? SerializeFrom : unknown) | undefined; params: Params; location: Location; @@ -152,6 +137,56 @@ export interface MetaArgs< error?: unknown; } +/** + * A function that returns an array of data objects to use for rendering + * metadata HTML tags in a route. These tags are not rendered on descendant + * routes in the route hierarchy. In other words, they will only be rendered on + * the route in which they are exported. + * + * @param Loader - The type of the current route's loader function + * @param MatchLoaders - Mapping from a parent route's filepath to its loader + * function type + * + * Note that parent route filepaths are relative to the `app/` directory. + * + * For example, if this meta function is for `/sales/customers/$customerId`: + * + * ```ts + * // app/root.tsx + * const loader = () => ({ hello: "world" }) + * export type Loader = typeof loader + * + * // app/routes/sales.tsx + * const loader = () => ({ salesCount: 1074 }) + * export type Loader = typeof loader + * + * // app/routes/sales/customers.tsx + * const loader = () => ({ customerCount: 74 }) + * export type Loader = typeof loader + * + * // app/routes/sales/customers/$customersId.tsx + * import type { Loader as RootLoader } from "../../../root" + * import type { Loader as SalesLoader } from "../../sales" + * import type { Loader as CustomersLoader } from "../../sales/customers" + * + * const loader = () => ({ name: "Customer name" }) + * + * const meta: MetaFunction = ({ data, matches }) => { + * const { name } = data + * // ^? string + * const { customerCount } = matches.find((match) => match.id === "routes/sales/customers").data + * // ^? number + * const { salesCount } = matches.find((match) => match.id === "routes/sales").data + * // ^? number + * const { hello } = matches.find((match) => match.id === "root").data + * // ^? "world" + * } + * ``` + */ export interface MetaFunction< Loader extends LoaderFunction | unknown = unknown, MatchLoaders extends Record = Record< diff --git a/packages/react-router/lib/dom/ssr/routes.tsx b/packages/react-router/lib/dom/ssr/routes.tsx index d83664a189..375e4da6e3 100644 --- a/packages/react-router/lib/dom/ssr/routes.tsx +++ b/packages/react-router/lib/dom/ssr/routes.tsx @@ -4,6 +4,7 @@ import type { HydrationState } from "../../router/router"; import type { ActionFunctionArgs, LoaderFunctionArgs, + RouteManifest, ShouldRevalidateFunction, ShouldRevalidateFunctionArgs, } from "../../router/utils"; @@ -18,12 +19,7 @@ import invariant from "./invariant"; import { useRouteError } from "../../hooks"; import type { DataRouteObject } from "../../context"; -export interface RouteManifest { - [routeId: string]: Route; -} - -// NOTE: make sure to change the Route in server-runtime if you change this -interface Route { +export interface Route { index?: boolean; caseSensitive?: boolean; id: string; @@ -31,7 +27,6 @@ interface Route { path?: string; } -// NOTE: make sure to change the EntryRoute in server-runtime if you change this export interface EntryRoute extends Route { hasAction: boolean; hasLoader: boolean; @@ -50,11 +45,13 @@ function groupRoutesByParentId(manifest: RouteManifest) { let routes: Record[]> = {}; Object.values(manifest).forEach((route) => { - let parentId = route.parentId || ""; - if (!routes[parentId]) { - routes[parentId] = []; + if (route) { + let parentId = route.parentId || ""; + if (!routes[parentId]) { + routes[parentId] = []; + } + routes[parentId].push(route); } - routes[parentId].push(route); }); return routes; diff --git a/packages/react-router/lib/dom/ssr/server.tsx b/packages/react-router/lib/dom/ssr/server.tsx index f8ad22f6f7..1d91e31801 100644 --- a/packages/react-router/lib/dom/ssr/server.tsx +++ b/packages/react-router/lib/dom/ssr/server.tsx @@ -58,6 +58,7 @@ export function ServerRouter({ // * or doesn't have a server loader and we have no data to render if ( route && + manifestRoute && shouldHydrateRouteLoader(manifestRoute, route, context.isSpaMode) && (route.HydrateFallback || !manifestRoute.hasLoader) ) { diff --git a/packages/react-router/lib/dom/ssr/single-fetch.tsx b/packages/react-router/lib/dom/ssr/single-fetch.tsx index 0a04684019..7829b400ca 100644 --- a/packages/react-router/lib/dom/ssr/single-fetch.tsx +++ b/packages/react-router/lib/dom/ssr/single-fetch.tsx @@ -234,6 +234,8 @@ async function singleFetchLoaderNavigationStrategy( m.resolve(async (handler) => { routeDfds[i].resolve(); + let manifestRoute = manifest.routes[m.route.id]; + if (!m.shouldLoad) { // If we're not yet initialized and this is the initial load, respect // `shouldLoad` because we're only dealing with `clientLoader.hydrate` @@ -247,7 +249,8 @@ async function singleFetchLoaderNavigationStrategy( // via `shouldRevalidate` if ( m.route.id in router.state.loaderData && - manifest.routes[m.route.id].hasLoader && + manifestRoute && + manifestRoute.hasLoader && routeModules[m.route.id]?.shouldRevalidate ) { foundOptOutRoute = true; @@ -257,8 +260,8 @@ async function singleFetchLoaderNavigationStrategy( // When a route has a client loader, it opts out of the singular call and // calls it's server loader via `serverLoader()` using a `?_routes` param - if (manifest.routes[m.route.id].hasClientLoader) { - if (manifest.routes[m.route.id].hasLoader) { + if (manifestRoute && manifestRoute.hasClientLoader) { + if (manifestRoute.hasLoader) { foundOptOutRoute = true; } try { @@ -276,7 +279,7 @@ async function singleFetchLoaderNavigationStrategy( } // Load this route on the server if it has a loader - if (manifest.routes[m.route.id].hasLoader) { + if (manifestRoute && manifestRoute.hasLoader) { routesParams.add(m.route.id); } diff --git a/packages/react-router/lib/router/utils.ts b/packages/react-router/lib/router/utils.ts index a667a4d791..8a913a80db 100644 --- a/packages/react-router/lib/router/utils.ts +++ b/packages/react-router/lib/router/utils.ts @@ -337,7 +337,10 @@ export type AgnosticDataRouteObject = | AgnosticDataIndexRouteObject | AgnosticDataNonIndexRouteObject; -export type RouteManifest = Record; +export type RouteManifest = Record< + string, + R | undefined +>; // Recursive helper for finding path parameters in the absence of wildcards type _PathParam = diff --git a/packages/react-router/lib/server-runtime/build.ts b/packages/react-router/lib/server-runtime/build.ts index 64f351cd26..b81a326440 100644 --- a/packages/react-router/lib/server-runtime/build.ts +++ b/packages/react-router/lib/server-runtime/build.ts @@ -1,4 +1,4 @@ -import type { ActionFunctionArgs, LoaderFunctionArgs } from "./routeModules"; +import type { ActionFunctionArgs, LoaderFunctionArgs } from "../router/utils"; import type { AssetsManifest, EntryContext, diff --git a/packages/react-router/lib/server-runtime/data.ts b/packages/react-router/lib/server-runtime/data.ts index 6e80b95301..8fbc1eb442 100644 --- a/packages/react-router/lib/server-runtime/data.ts +++ b/packages/react-router/lib/server-runtime/data.ts @@ -1,10 +1,10 @@ -import { isDataWithResponseInit, isRedirectStatusCode } from "../router/router"; import type { - ActionFunction, - ActionFunctionArgs, LoaderFunction, + ActionFunction, LoaderFunctionArgs, -} from "./routeModules"; + ActionFunctionArgs, +} from "../router/utils"; +import { isDataWithResponseInit, isRedirectStatusCode } from "../router/router"; /** * An object of unknown type for route loaders and actions provided by the @@ -16,11 +16,6 @@ export interface AppLoadContext { [key: string]: unknown; } -/** - * Data for a route that was returned from a `loader()`. - */ -export type AppData = unknown; - // Need to use RR's version here to permit the optional context even // though we know it'll always be provided in remix export async function callRouteHandler( diff --git a/packages/react-router/lib/server-runtime/entry.ts b/packages/react-router/lib/server-runtime/entry.ts index f25c6fdedd..b4895260e6 100644 --- a/packages/react-router/lib/server-runtime/entry.ts +++ b/packages/react-router/lib/server-runtime/entry.ts @@ -5,7 +5,10 @@ export function createEntryRouteModules( manifest: ServerRouteManifest ): RouteModules { return Object.keys(manifest).reduce((memo, routeId) => { - memo[routeId] = manifest[routeId].module; + let route = manifest[routeId]; + if (route) { + memo[routeId] = route.module; + } return memo; }, {} as RouteModules); } diff --git a/packages/react-router/lib/server-runtime/headers.ts b/packages/react-router/lib/server-runtime/headers.ts index 3c8cfe04b6..142891d5cb 100644 --- a/packages/react-router/lib/server-runtime/headers.ts +++ b/packages/react-router/lib/server-runtime/headers.ts @@ -2,6 +2,7 @@ import { splitCookiesString } from "set-cookie-parser"; import type { ServerBuild } from "./build"; import type { StaticHandlerContext } from "../router/router"; +import invariant from "./invariant"; export function getDocumentHeaders( build: ServerBuild, @@ -37,7 +38,9 @@ export function getDocumentHeaders( return matches.reduce((parentHeaders, match, idx) => { let { id } = match.route; - let routeModule = build.routes[id].module; + let route = build.routes[id]; + invariant(route, `Route with id "${id}" not found in build`); + let routeModule = route.module; let loaderHeaders = context.loaderHeaders[id] || new Headers(); let actionHeaders = context.actionHeaders[id] || new Headers(); diff --git a/packages/react-router/lib/server-runtime/routeModules.ts b/packages/react-router/lib/server-runtime/routeModules.ts index 16c1633f2f..18e03a1b85 100644 --- a/packages/react-router/lib/server-runtime/routeModules.ts +++ b/packages/react-router/lib/server-runtime/routeModules.ts @@ -1,95 +1,15 @@ -import type { Location } from "../router/history"; +import type { ActionFunction, LoaderFunction } from "../router/utils"; import type { - ActionFunction as RRActionFunction, - ActionFunctionArgs as RRActionFunctionArgs, - AgnosticRouteMatch, - LoaderFunction as RRLoaderFunction, - LoaderFunctionArgs as RRLoaderFunctionArgs, - Params, -} from "../router/utils"; -import type { AppData, AppLoadContext } from "./data"; -import type { SerializeFrom } from "../dom/ssr/components"; -import type { LinkDescriptor } from "../router/links"; + ClientActionFunction, + ClientLoaderFunction, + LinksFunction, + MetaFunction, +} from "../dom/ssr/routeModules"; export interface RouteModules { [routeId: string]: RouteModule | undefined; } -/** - * @deprecated Use `LoaderFunctionArgs`/`ActionFunctionArgs` instead - */ -export type DataFunctionArgs = RRActionFunctionArgs & - RRLoaderFunctionArgs & { - // Context is always provided in Remix, and typed for module augmentation support. - // RR also doesn't export DataFunctionArgs, so we extend the two interfaces here - // even tough they're identical under the hood - context: AppLoadContext; - }; - -/** - * A function that handles data mutations for a route on the server - */ -export type ActionFunction = ( - args: ActionFunctionArgs -) => ReturnType; - -/** - * Arguments passed to a route `action` function - */ -export type ActionFunctionArgs = RRActionFunctionArgs & { - // Context is always provided in Remix, and typed for module augmentation support. - context: AppLoadContext; -}; - -/** - * A function that handles data mutations for a route on the client - * @private Public API is exported from @react-router/react - */ -type ClientActionFunction = ( - args: ClientActionFunctionArgs -) => ReturnType; - -/** - * Arguments passed to a route `clientAction` function - * @private Public API is exported from @react-router/react - */ -export type ClientActionFunctionArgs = RRActionFunctionArgs & { - serverAction: () => Promise>; -}; - -/** - * A function that loads data for a route on the server - */ -export type LoaderFunction = ( - args: LoaderFunctionArgs -) => ReturnType; - -/** - * Arguments passed to a route `loader` function - */ -export type LoaderFunctionArgs = RRLoaderFunctionArgs & { - // Context is always provided in Remix, and typed for module augmentation support. - context: AppLoadContext; -}; - -/** - * A function that loads data for a route on the client - * @private Public API is exported from @react-router/react - */ -type ClientLoaderFunction = (( - args: ClientLoaderFunctionArgs -) => ReturnType) & { - hydrate?: boolean; -}; - -/** - * Arguments passed to a route `clientLoader` function - * @private Public API is exported from @react-router/react - */ -export type ClientLoaderFunctionArgs = RRLoaderFunctionArgs & { - serverLoader: () => Promise>; -}; - export type HeadersArgs = { loaderHeaders: Headers; parentHeaders: Headers; @@ -105,129 +25,6 @@ export interface HeadersFunction { (args: HeadersArgs): Headers | HeadersInit; } -/** - * A function that defines `` tags to be inserted into the `` of - * the document on route transitions. - */ -export interface LinksFunction { - (): LinkDescriptor[]; -} - -/** - * A function that returns an array of data objects to use for rendering - * metadata HTML tags in a route. These tags are not rendered on descendant - * routes in the route hierarchy. In other words, they will only be rendered on - * the route in which they are exported. - * - * @param Loader - The type of the current route's loader function - * @param MatchLoaders - Mapping from a parent route's filepath to its loader - * function type - * - * Note that parent route filepaths are relative to the `app/` directory. - * - * For example, if this meta function is for `/sales/customers/$customerId`: - * - * ```ts - * // app/root.tsx - * const loader = () => ({ hello: "world" }) - * export type Loader = typeof loader - * - * // app/routes/sales.tsx - * const loader = () => ({ salesCount: 1074 }) - * export type Loader = typeof loader - * - * // app/routes/sales/customers.tsx - * const loader = () => ({ customerCount: 74 }) - * export type Loader = typeof loader - * - * // app/routes/sales/customers/$customersId.tsx - * import type { Loader as RootLoader } from "../../../root" - * import type { Loader as SalesLoader } from "../../sales" - * import type { Loader as CustomersLoader } from "../../sales/customers" - * - * const loader = () => ({ name: "Customer name" }) - * - * const meta: MetaFunction = ({ data, matches }) => { - * const { name } = data - * // ^? string - * const { customerCount } = matches.find((match) => match.id === "routes/sales/customers").data - * // ^? number - * const { salesCount } = matches.find((match) => match.id === "routes/sales").data - * // ^? number - * const { hello } = matches.find((match) => match.id === "root").data - * // ^? "world" - * } - * ``` - */ -export interface ServerRuntimeMetaFunction< - Loader extends LoaderFunction | unknown = unknown, - ParentsLoaders extends Record = Record< - string, - unknown - > -> { - ( - args: ServerRuntimeMetaArgs - ): ServerRuntimeMetaDescriptor[]; -} - -interface ServerRuntimeMetaMatch< - RouteId extends string = string, - Loader extends LoaderFunction | unknown = unknown -> { - id: RouteId; - pathname: AgnosticRouteMatch["pathname"]; - data: Loader extends LoaderFunction ? SerializeFrom : unknown; - handle?: RouteHandle; - params: AgnosticRouteMatch["params"]; - meta: ServerRuntimeMetaDescriptor[]; - error?: unknown; -} - -type ServerRuntimeMetaMatches< - MatchLoaders extends Record = Record< - string, - unknown - > -> = Array< - { - [K in keyof MatchLoaders]: ServerRuntimeMetaMatch< - Exclude, - MatchLoaders[K] - >; - }[keyof MatchLoaders] ->; - -export interface ServerRuntimeMetaArgs< - Loader extends LoaderFunction | unknown = unknown, - MatchLoaders extends Record = Record< - string, - unknown - > -> { - data: - | (Loader extends LoaderFunction ? SerializeFrom : AppData) - | undefined; - params: Params; - location: Location; - matches: ServerRuntimeMetaMatches; - error?: unknown; -} - -export type ServerRuntimeMetaDescriptor = - | { charSet: "utf-8" } - | { title: string } - | { name: string; content: string } - | { property: string; content: string } - | { httpEquiv: string; content: string } - | { "script:ld+json": LdJsonObject } - | { tagName: "meta" | "link"; [name: string]: string } - | { [name: string]: unknown }; - type LdJsonObject = { [Key in string]: LdJsonValue } & { [Key in string]?: LdJsonValue | undefined; }; @@ -249,7 +46,7 @@ export interface EntryRouteModule { default: any; // Weakly typed because server-runtime is not React-aware handle?: RouteHandle; links?: LinksFunction; - meta?: ServerRuntimeMetaFunction; + meta?: MetaFunction; } export interface ServerRouteModule extends EntryRouteModule { diff --git a/packages/react-router/lib/server-runtime/routes.ts b/packages/react-router/lib/server-runtime/routes.ts index 26fdd34a64..6023927eec 100644 --- a/packages/react-router/lib/server-runtime/routes.ts +++ b/packages/react-router/lib/server-runtime/routes.ts @@ -2,14 +2,12 @@ import type { AgnosticDataRouteObject, LoaderFunctionArgs as RRLoaderFunctionArgs, ActionFunctionArgs as RRActionFunctionArgs, + RouteManifest, } from "../router/utils"; import { callRouteHandler } from "./data"; import type { FutureConfig } from "../dom/ssr/entry"; -import type { - ActionFunctionArgs, - LoaderFunctionArgs, - ServerRouteModule, -} from "./routeModules"; +import type { Route } from "../dom/ssr/routes"; +import type { ServerRouteModule } from "./routeModules"; import type { SingleFetchResult, SingleFetchResults, @@ -17,34 +15,8 @@ import type { import { decodeViaTurboStream } from "../dom/ssr/single-fetch"; import invariant from "./invariant"; -export interface RouteManifest { - [routeId: string]: Route; -} - export type ServerRouteManifest = RouteManifest>; -// NOTE: make sure to change the Route in remix-react/react-router-dev if you change this -export interface Route { - index?: boolean; - caseSensitive?: boolean; - id: string; - parentId?: string; - path?: string; -} - -// NOTE: make sure to change the EntryRoute in react-router/react-router-dev if you change this -export interface EntryRoute extends Route { - hasAction: boolean; - hasLoader: boolean; - hasClientAction: boolean; - hasClientLoader: boolean; - hasErrorBoundary: boolean; - imports?: string[]; - css?: string[]; - module: string; - parentId?: string; -} - export interface ServerRoute extends Route { children: ServerRoute[]; module: ServerRouteModule; @@ -54,11 +26,13 @@ function groupRoutesByParentId(manifest: ServerRouteManifest) { let routes: Record[]> = {}; Object.values(manifest).forEach((route) => { - let parentId = route.parentId || ""; - if (!routes[parentId]) { - routes[parentId] = []; + if (route) { + let parentId = route.parentId || ""; + if (!routes[parentId]) { + routes[parentId] = []; + } + routes[parentId].push(route); } - routes[parentId].push(route); }); return routes; @@ -126,16 +100,13 @@ export function createStaticHandlerDataRoutes( invariant("data" in result, "Unable to process prerendered data"); return result.data; } - let val = await callRouteHandler( - route.module.loader!, - args as LoaderFunctionArgs - ); + let val = await callRouteHandler(route.module.loader!, args); return val; } : undefined, action: route.module.action ? (args: RRActionFunctionArgs) => - callRouteHandler(route.module.action!, args as ActionFunctionArgs) + callRouteHandler(route.module.action!, args) : undefined, handle: route.module.handle, }; diff --git a/packages/react-router/lib/server-runtime/server.ts b/packages/react-router/lib/server-runtime/server.ts index c94b9cab72..de0231ca88 100644 --- a/packages/react-router/lib/server-runtime/server.ts +++ b/packages/react-router/lib/server-runtime/server.ts @@ -15,7 +15,7 @@ import { sanitizeErrors, serializeError, serializeErrors } from "./errors"; import { ServerMode, isServerMode } from "./mode"; import type { RouteMatch } from "./routeMatching"; import { matchServerRoutes } from "./routeMatching"; -import type { EntryRoute, ServerRoute } from "./routes"; +import type { ServerRoute } from "./routes"; import { createStaticHandlerDataRoutes, createRoutes } from "./routes"; import { createServerHandoffString } from "./serverHandoff"; import { getDevServerHooks } from "./dev"; @@ -30,6 +30,7 @@ import { } from "./single-fetch"; import { getDocumentHeaders } from "./headers"; import invariant from "./invariant"; +import type { EntryRoute } from "../dom/ssr/routes"; export type RequestHandler = ( request: Request, @@ -244,7 +245,10 @@ async function handleManifestRequest( if (matches) { for (let match of matches) { let routeId = match.route.id; - patches[routeId] = build.assets.routes[routeId]; + let route = build.assets.routes[routeId]; + if (route) { + patches[routeId] = route; + } } } } From 49ffd5315f28ea7db6cc61e8908473db92d93fae Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 23 Oct 2024 15:23:36 -0400 Subject: [PATCH 32/33] Update DEVELOPMENT.md --- DEVELOPMENT.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 0022a023ce..6c1c4ab8ba 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -68,6 +68,7 @@ You may need to make changes to a pre-release prior to publishing a final stable - `git checkout main` - `git merge --no-ff release-next` - `git push origin main` + - _Note:_ For the `v7.0.0` stable release, there will probably be a bunch of conflicts on `docs/**/*.md` files here because we have made changes to v6 docs but in `dev` we removed a lot of those files in favor of auto-generated API docs. To resolve those conflicts, we should accept the deletion from the `release-next` branch. - Merge the `release-next` branch into `dev` **using a non-fast-forward merge** and push it up to GitHub - `git checkout dev` - `git merge --no-ff release-next` @@ -90,11 +91,16 @@ After the `6.25.0` release, we branched off a `v6` branch for continued `6.x` wo - Starting the release process for 6.x is the same as outlined above, with a few changes: - Branch from `v6` instead of `dev` - Use the name `release-v6` to avoid collisions with the ongoing v7 (pre)releases using `release-next` - - Do not merge `main` into the `release-v6` branch + - **Do not** merge `main` into the `release-v6` branch - The process of the PRs and iterating on prereleases remains the same - Once the stable release is out: - Merge `release-v6` back to `v6` with a **Normal Merge** + - **Do not** merge `release-v6` to `main` - Copy the updated changelog entry for the `6.X.Y` version to `main` + - Copy the docs changes to `main` so they show up on the live docs site for v6 + - `git checkout main` + - `git diff react-router@6.X.Y...react-router@6.X.Y docs/ > ./docs.patch` + - `git apply ./docs.patch` - The _code_ changes should already be in the `dev` branch but confirm that the commits in this release are all included in `dev` already: - I.e., https://github.com/remix-run/react-router/compare/react-router@6.26.1...react-router@6.26.2 - If one or more are not, then you can manually bring them over by cherry-picking the commit (or re-doing the work) From 5c7cc020bec95687801c9ccba232de6c71c4ff3f Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 23 Oct 2024 15:25:56 -0400 Subject: [PATCH 33/33] Add generics for loader data, action data, and fetchers (#12180) --- .changeset/long-peas-doubt.md | 15 ++++++++ packages/react-router/lib/components.tsx | 14 +++++--- packages/react-router/lib/dom/lib.tsx | 5 +-- .../react-router/lib/dom/ssr/components.tsx | 3 -- .../react-router/lib/dom/ssr/routeModules.ts | 34 +++++++++++-------- packages/react-router/lib/hooks.tsx | 17 ++++++---- packages/react-router/lib/types.ts | 15 ++++++++ 7 files changed, 72 insertions(+), 31 deletions(-) create mode 100644 .changeset/long-peas-doubt.md diff --git a/.changeset/long-peas-doubt.md b/.changeset/long-peas-doubt.md new file mode 100644 index 0000000000..67d9fa1952 --- /dev/null +++ b/.changeset/long-peas-doubt.md @@ -0,0 +1,15 @@ +--- +"react-router": major +--- + +Migrate Remix type generics to React Router + +- These generics are provided for Remix v2 migration purposes +- These generics and the APIs they exist on should be considered informally deprecated in favor of the new `Route.*` types +- Anyone migrating from React Router v6 should probably not leverage these new generics and should migrate straight to the `Route.*` types +- For React Router v6 users, these generics are new and should not impact your app, with one exception + - `useFetcher` previously had an optional generic (used primarily by Remix v2) that expected the data type + - This has been updated in v7 to expect the type of the function that generates the data (i.e., `typeof loader`/`typeof action`) + - Therefore, you should update your usages: + - ❌ `useFetcher()` + - βœ… `useFetcher()` diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx index 9aa96dd231..0e69f97fef 100644 --- a/packages/react-router/lib/components.tsx +++ b/packages/react-router/lib/components.tsx @@ -817,14 +817,14 @@ export function Routes({ return useRoutes(createRoutesFromChildren(children), location); } -export interface AwaitResolveRenderFunction { - (data: Awaited): React.ReactNode; +export interface AwaitResolveRenderFunction { + (data: Awaited): React.ReactNode; } /** * @category Types */ -export interface AwaitProps { +export interface AwaitProps { /** When using a function, the resolved value is provided as the parameter. @@ -923,7 +923,7 @@ export interface AwaitProps { } ``` */ - resolve: TrackedPromise | any; + resolve: Resolve; } /** @@ -967,7 +967,11 @@ function Book() { @category Components */ -export function Await({ children, errorElement, resolve }: AwaitProps) { +export function Await({ + children, + errorElement, + resolve, +}: AwaitProps) { return ( {children} diff --git a/packages/react-router/lib/dom/lib.tsx b/packages/react-router/lib/dom/lib.tsx index f2ecb3ee97..cc971d2361 100644 --- a/packages/react-router/lib/dom/lib.tsx +++ b/packages/react-router/lib/dom/lib.tsx @@ -88,6 +88,7 @@ import { useResolvedPath, useRouteId, } from "../hooks"; +import type { SerializeFrom } from "../types"; //////////////////////////////////////////////////////////////////////////////// //#region Global Stuff @@ -1792,7 +1793,7 @@ export type FetcherWithComponents = Fetcher & { @category Hooks */ -export function useFetcher({ +export function useFetcher({ key, }: { /** @@ -1813,7 +1814,7 @@ export function useFetcher({ ``` */ key?: string; -} = {}): FetcherWithComponents { +} = {}): FetcherWithComponents> { let { router } = useDataRouterContext(DataRouterHook.UseFetcher); let state = useDataRouterState(DataRouterStateHook.UseFetcher); let fetcherData = React.useContext(FetchersContext); diff --git a/packages/react-router/lib/dom/ssr/components.tsx b/packages/react-router/lib/dom/ssr/components.tsx index e91636138f..f56ea11cb3 100644 --- a/packages/react-router/lib/dom/ssr/components.tsx +++ b/packages/react-router/lib/dom/ssr/components.tsx @@ -32,9 +32,6 @@ import { useLocation } from "../../hooks"; import { getPartialManifest, isFogOfWarEnabled } from "./fog-of-war"; import type { PageLinkDescriptor } from "../../router/links"; -// TODO: Temporary shim until we figure out the way to handle typings in v7 -export type SerializeFrom = D extends () => {} ? Awaited> : D; - function useDataRouterContext() { let context = React.useContext(DataRouterContext); invariant( diff --git a/packages/react-router/lib/dom/ssr/routeModules.ts b/packages/react-router/lib/dom/ssr/routeModules.ts index 5c3500faa4..5e3f607bf0 100644 --- a/packages/react-router/lib/dom/ssr/routeModules.ts +++ b/packages/react-router/lib/dom/ssr/routeModules.ts @@ -9,10 +9,10 @@ import type { ShouldRevalidateFunction, } from "../../router/utils"; -import type { SerializeFrom } from "./components"; import type { EntryRoute } from "./routes"; import type { DataRouteMatch } from "../../context"; import type { LinkDescriptor } from "../../router/links"; +import type { SerializeFrom } from "../../types"; export interface RouteModules { [routeId: string]: RouteModule | undefined; @@ -96,11 +96,13 @@ export interface LinksFunction { export interface MetaMatch< RouteId extends string = string, - Loader extends LoaderFunction | unknown = unknown + Loader extends LoaderFunction | ClientLoaderFunction | unknown = unknown > { id: RouteId; pathname: DataRouteMatch["pathname"]; - data: Loader extends LoaderFunction ? SerializeFrom : unknown; + data: Loader extends LoaderFunction | ClientLoaderFunction + ? SerializeFrom + : unknown; handle?: RouteHandle; params: DataRouteMatch["params"]; meta: MetaDescriptor[]; @@ -108,10 +110,10 @@ export interface MetaMatch< } export type MetaMatches< - MatchLoaders extends Record = Record< + MatchLoaders extends Record< string, - unknown - > + LoaderFunction | ClientLoaderFunction | unknown + > = Record > = Array< { [K in keyof MatchLoaders]: MetaMatch< @@ -122,14 +124,16 @@ export type MetaMatches< >; export interface MetaArgs< - Loader extends LoaderFunction | unknown = unknown, - MatchLoaders extends Record = Record< + Loader extends LoaderFunction | ClientLoaderFunction | unknown = unknown, + MatchLoaders extends Record< string, - unknown - > + LoaderFunction | ClientLoaderFunction | unknown + > = Record > { data: - | (Loader extends LoaderFunction ? SerializeFrom : unknown) + | (Loader extends LoaderFunction | ClientLoaderFunction + ? SerializeFrom + : unknown) | undefined; params: Params; location: Location; @@ -188,11 +192,11 @@ export interface MetaArgs< * ``` */ export interface MetaFunction< - Loader extends LoaderFunction | unknown = unknown, - MatchLoaders extends Record = Record< + Loader extends LoaderFunction | ClientLoaderFunction | unknown = unknown, + MatchLoaders extends Record< string, - unknown - > + LoaderFunction | ClientLoaderFunction | unknown + > = Record > { (args: MetaArgs): MetaDescriptor[] | undefined; } diff --git a/packages/react-router/lib/hooks.tsx b/packages/react-router/lib/hooks.tsx index 430e14674e..68e7b9a8a5 100644 --- a/packages/react-router/lib/hooks.tsx +++ b/packages/react-router/lib/hooks.tsx @@ -48,6 +48,7 @@ import { resolveTo, stripBasename, } from "./router/utils"; +import type { SerializeFrom } from "./types"; // TODO: Let's get this back to using an import map and development/production // condition once we get the rollup build replaced @@ -1082,10 +1083,10 @@ export function useMatches(): UIMatch[] { @category Hooks */ -export function useLoaderData(): unknown { +export function useLoaderData(): SerializeFrom { let state = useDataRouterState(DataRouterStateHook.UseLoaderData); let routeId = useCurrentRouteId(DataRouterStateHook.UseLoaderData); - return state.loaderData[routeId]; + return state.loaderData[routeId] as SerializeFrom; } /** @@ -1115,9 +1116,11 @@ export function useLoaderData(): unknown { @category Hooks */ -export function useRouteLoaderData(routeId: string): unknown { +export function useRouteLoaderData( + routeId: string +): SerializeFrom | undefined { let state = useDataRouterState(DataRouterStateHook.UseRouteLoaderData); - return state.loaderData[routeId]; + return state.loaderData[routeId] as SerializeFrom | undefined; } /** @@ -1145,10 +1148,12 @@ export function useRouteLoaderData(routeId: string): unknown { @category Hooks */ -export function useActionData(): unknown { +export function useActionData(): SerializeFrom | undefined { let state = useDataRouterState(DataRouterStateHook.UseActionData); let routeId = useCurrentRouteId(DataRouterStateHook.UseLoaderData); - return state.actionData ? state.actionData[routeId] : undefined; + return (state.actionData ? state.actionData[routeId] : undefined) as + | SerializeFrom + | undefined; } /** diff --git a/packages/react-router/lib/types.ts b/packages/react-router/lib/types.ts index 9198cf5244..0e1897df8a 100644 --- a/packages/react-router/lib/types.ts +++ b/packages/react-router/lib/types.ts @@ -1,3 +1,7 @@ +import type { + ClientLoaderFunctionArgs, + ClientActionFunctionArgs, +} from "./dom/ssr/routeModules"; import type { DataWithResponseInit } from "./router/utils"; import type { AppLoadContext } from "./server-runtime/data"; import type { Serializable } from "./server-runtime/single-fetch"; @@ -121,6 +125,17 @@ type Serialize = undefined +/** + * @deprecated Generics on data APIs such as `useLoaderData`, `useActionData`, + * `meta`, etc. are deprecated in favor of the `Route.*` types generated via + * `react-router typegen` + */ +export type SerializeFrom = T extends (...args: infer Args) => unknown + ? Args extends [ClientLoaderFunctionArgs | ClientActionFunctionArgs] + ? ClientData> + : ServerData> + : T; + export type CreateServerLoaderArgs = ServerDataFunctionArgs; export type CreateClientLoaderArgs<