From 003f09a3a5784606dcedc91db3afd0b70c849e69 Mon Sep 17 00:00:00 2001 From: Brooks Lybrand Date: Thu, 3 Oct 2024 14:22:03 -0500 Subject: [PATCH 01/26] Update template to use pre-release tags (#12061) --- templates/basic/.gitignore | 1 + templates/basic/.vscode/settings.json | 4 ++++ templates/basic/package.json | 16 ++++++++-------- templates/basic/tsconfig.json | 7 +++++-- templates/basic/vite.config.ts | 3 --- 5 files changed, 18 insertions(+), 13 deletions(-) create mode 100644 templates/basic/.vscode/settings.json diff --git a/templates/basic/.gitignore b/templates/basic/.gitignore index 80ec311f4f..c08251ce0e 100644 --- a/templates/basic/.gitignore +++ b/templates/basic/.gitignore @@ -3,3 +3,4 @@ node_modules /.cache /build .env +.react-router diff --git a/templates/basic/.vscode/settings.json b/templates/basic/.vscode/settings.json new file mode 100644 index 0000000000..fae8e3d8a9 --- /dev/null +++ b/templates/basic/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true +} diff --git a/templates/basic/package.json b/templates/basic/package.json index 21d43a35dc..5fb395e304 100644 --- a/templates/basic/package.json +++ b/templates/basic/package.json @@ -6,24 +6,24 @@ "dev": "react-router dev", "build": "react-router build", "start": "react-router-serve ./build/server/index.js", - "typecheck": "tsc" + "typecheck": "react-router typegen && tsc" }, "dependencies": { - "@react-router/node": "0.0.0-nightly-8f12ed19a-20240924", - "@react-router/serve": "0.0.0-nightly-8f12ed19a-20240924", - "@tailwindcss/vite": "^4.0.0-alpha.24", + "@react-router/node": "7.0.0-pre.0", + "@react-router/serve": "7.0.0-pre.0", + "@tailwindcss/vite": "^4.0.0-alpha.25", "isbot": "^5.1.17", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router": "0.0.0-nightly-8f12ed19a-20240924" + "react-router": "7.0.0-pre.0" }, "devDependencies": { - "@react-router/dev": "0.0.0-nightly-8f12ed19a-20240924", + "@react-router/dev": "7.0.0-pre.0", "@types/react": "^18.3.9", "@types/react-dom": "^18.3.0", - "tailwindcss": "^4.0.0-alpha.24", + "tailwindcss": "^4.0.0-alpha.25", "typescript": "^5.6.2", - "vite": "^5.4.7", + "vite": "^5.4.8", "vite-tsconfig-paths": "^5.0.1" }, "engines": { diff --git a/templates/basic/tsconfig.json b/templates/basic/tsconfig.json index a2d574ae2c..29b2316386 100644 --- a/templates/basic/tsconfig.json +++ b/templates/basic/tsconfig.json @@ -5,7 +5,8 @@ "**/.server/**/*.ts", "**/.server/**/*.tsx", "**/.client/**/*.ts", - "**/.client/**/*.tsx" + "**/.client/**/*.tsx", + ".react-router/types/**/*" ], "compilerOptions": { "lib": ["DOM", "DOM.Iterable", "ES2022"], @@ -25,6 +26,8 @@ "paths": { "~/*": ["./app/*"] }, - "noEmit": true + "noEmit": true, + "rootDirs": [".", "./.react-router/types"], + "plugins": [{ "name": "@react-router/dev" }] } } diff --git a/templates/basic/vite.config.ts b/templates/basic/vite.config.ts index 761cd98178..f2bd0c92cd 100644 --- a/templates/basic/vite.config.ts +++ b/templates/basic/vite.config.ts @@ -4,8 +4,5 @@ import tsconfigPaths from "vite-tsconfig-paths"; import { defineConfig } from "vite"; export default defineConfig({ - optimizeDeps: { - include: ["react", "react-dom"], - }, plugins: [tailwindcss(), reactRouter(), tsconfigPaths()], }); From bef5976730ced8212a962596ca4b3fa541979fc5 Mon Sep 17 00:00:00 2001 From: Brooks Lybrand Date: Thu, 3 Oct 2024 14:37:34 -0500 Subject: [PATCH 02/26] Swap out template logos --- templates/basic/app/routes/home.tsx | 6 +++--- templates/basic/public/logo-dark.png | Bin 4829 -> 0 bytes templates/basic/public/logo-dark.svg | 25 +++++++++++++++++++++++++ templates/basic/public/logo-light.png | Bin 4917 -> 0 bytes templates/basic/public/logo-light.svg | 25 +++++++++++++++++++++++++ 5 files changed, 53 insertions(+), 3 deletions(-) delete mode 100644 templates/basic/public/logo-dark.png create mode 100644 templates/basic/public/logo-dark.svg delete mode 100644 templates/basic/public/logo-light.png create mode 100644 templates/basic/public/logo-light.svg diff --git a/templates/basic/app/routes/home.tsx b/templates/basic/app/routes/home.tsx index acd216304c..d737a957f0 100644 --- a/templates/basic/app/routes/home.tsx +++ b/templates/basic/app/routes/home.tsx @@ -15,14 +15,14 @@ export default function Index() {

Welcome to React Router

-
+
Remix Remix diff --git a/templates/basic/public/logo-dark.png b/templates/basic/public/logo-dark.png deleted file mode 100644 index d83f2967dd8cde985929966ee1e8cc701c88362d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4829 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 diff --git a/templates/basic/public/logo-dark.svg b/templates/basic/public/logo-dark.svg new file mode 100644 index 0000000000..41c7e4f85c --- /dev/null +++ b/templates/basic/public/logo-dark.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/basic/public/logo-light.png b/templates/basic/public/logo-light.png deleted file mode 100644 index 112570f10a64159122c1803cfbfd2959bda735bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4917 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 + + + + + + + + + + + + + + + + + + + + + + + + From 4263ec297bf971893c1f8a952321c6f8047c024c Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Fri, 4 Oct 2024 15:54:23 +1000 Subject: [PATCH 03/26] Rename private data router global (#12066) --- packages/react-router-dev/vite/static/refresh-utils.cjs | 6 +++--- packages/react-router/lib/dom-export/hydrated-router.tsx | 2 +- packages/react-router/lib/dom/global.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/react-router-dev/vite/static/refresh-utils.cjs b/packages/react-router-dev/vite/static/refresh-utils.cjs index 0e5f3a7808..02b0f8358f 100644 --- a/packages/react-router-dev/vite/static/refresh-utils.cjs +++ b/packages/react-router-dev/vite/static/refresh-utils.cjs @@ -49,21 +49,21 @@ const enqueueUpdate = debounce(async () => { .map((route) => route.id) ); - let routes = __reactRouterInstance.createRoutesForHMR( + let routes = __reactRouterDataRouter.createRoutesForHMR( needsRevalidation, manifest.routes, window.__reactRouterRouteModules, window.__reactRouterContext.future, window.__reactRouterContext.isSpaMode ); - __reactRouterInstance._internalSetRoutes(routes); + __reactRouterDataRouter._internalSetRoutes(routes); routeUpdates.clear(); window.__reactRouterRouteModuleUpdates.clear(); } try { window.__reactRouterHdrActive = true; - await __reactRouterInstance.revalidate(); + await __reactRouterDataRouter.revalidate(); } finally { window.__reactRouterHdrActive = false; } diff --git a/packages/react-router/lib/dom-export/hydrated-router.tsx b/packages/react-router/lib/dom-export/hydrated-router.tsx index 17735fd233..295470be06 100644 --- a/packages/react-router/lib/dom-export/hydrated-router.tsx +++ b/packages/react-router/lib/dom-export/hydrated-router.tsx @@ -192,7 +192,7 @@ function createHydratedRouter(): DataRouter { router.createRoutesForHMR = /* spacer so ts-ignore does not affect the right hand of the assignment */ createClientRoutesWithHMRRevalidationOptOut; - window.__reactRouterInstance = router; + window.__reactRouterDataRouter = router; return router; } diff --git a/packages/react-router/lib/dom/global.ts b/packages/react-router/lib/dom/global.ts index 175e079619..30c54dd0b2 100644 --- a/packages/react-router/lib/dom/global.ts +++ b/packages/react-router/lib/dom/global.ts @@ -36,7 +36,7 @@ declare global { var __reactRouterContext: WindowReactRouterContext | undefined; var __reactRouterManifest: AssetsManifest | undefined; var __reactRouterRouteModules: RouteModules | undefined; - var __reactRouterInstance: DataRouter | undefined; + var __reactRouterDataRouter: DataRouter | undefined; var __reactRouterHdrActive: boolean; var __reactRouterClearCriticalCss: (() => void) | undefined; var $RefreshRuntime$: From de54f085d16368516fd36a9d9b274b90c4e937ee Mon Sep 17 00:00:00 2001 From: Brooks Lybrand Date: Fri, 4 Oct 2024 11:05:27 -0500 Subject: [PATCH 04/26] revert to tailwind 3 (#12069) --- templates/basic/app/app.css | 9 +++------ templates/basic/app/routes/home.tsx | 2 +- templates/basic/package.json | 5 +++-- templates/basic/postcss.config.js | 6 ++++++ templates/basic/tailwind.config.ts | 22 ++++++++++++++++++++++ templates/basic/vite.config.ts | 3 +-- 6 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 templates/basic/postcss.config.js create mode 100644 templates/basic/tailwind.config.ts diff --git a/templates/basic/app/app.css b/templates/basic/app/app.css index 87a5e2fb51..303fe158fc 100644 --- a/templates/basic/app/app.css +++ b/templates/basic/app/app.css @@ -1,9 +1,6 @@ -@import "tailwindcss"; - -@theme { - --font-family-sans: "Inter", ui-sans-serif, system-ui, sans-serif, - "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; -} +@tailwind base; +@tailwind components; +@tailwind utilities; html, body { diff --git a/templates/basic/app/routes/home.tsx b/templates/basic/app/routes/home.tsx index d737a957f0..15d6f05ddd 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

-
+
Remix Date: Fri, 4 Oct 2024 19:06:10 -0400 Subject: [PATCH 05/26] fix typegen for async route config --- packages/react-router-dev/typescript/typegen.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/react-router-dev/typescript/typegen.ts b/packages/react-router-dev/typescript/typegen.ts index bde4fd87de..72f941ea18 100644 --- a/packages/react-router-dev/typescript/typegen.ts +++ b/packages/react-router-dev/typescript/typegen.ts @@ -5,6 +5,7 @@ import dedent from "dedent"; import * as Path from "pathe"; import * as Pathe from "pathe/utils"; +import type { RouteConfig } from "../config/routes"; import { configRoutesToRouteManifest, type RouteManifest, @@ -56,10 +57,13 @@ export async function watch(rootDirectory: string) { routesViteNodeContext.devServer.moduleGraph.invalidateAll(); routesViteNodeContext.runner.moduleCache.clear(); - const result = await routesViteNodeContext.runner.executeFile(routesTsPath); + const routeConfig: RouteConfig = ( + await routesViteNodeContext.runner.executeFile(routesTsPath) + ).routes; + return { ...routes, - ...configRoutesToRouteManifest(result.routes), + ...configRoutesToRouteManifest(await routeConfig), }; } From 5358eebe97e5f2ca13fc139dec59de98c77fb92a Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Fri, 4 Oct 2024 19:57:26 -0400 Subject: [PATCH 06/26] fix typegen for `data` and `json` responses --- packages/react-router/index.ts | 8 ++-- packages/react-router/lib/router/utils.ts | 6 +-- packages/react-router/lib/types.ts | 45 ++++++++++++++++++++++- 3 files changed, 49 insertions(+), 10 deletions(-) diff --git a/packages/react-router/index.ts b/packages/react-router/index.ts index 8de1e5a02e..244785d346 100644 --- a/packages/react-router/index.ts +++ b/packages/react-router/index.ts @@ -32,7 +32,6 @@ export type { FormEncType, FormMethod, HTMLFormMethod, - JsonFunction, LazyRouteFunction, LoaderFunction, LoaderFunctionArgs, @@ -169,7 +168,7 @@ export { useFetchers, useBeforeUnload, usePrompt as unstable_usePrompt, - useViewTransitionState as useViewTransitionState, + useViewTransitionState, } from "./lib/dom/lib"; export type { FetcherSubmitOptions, @@ -264,7 +263,10 @@ export type { LinkDescriptor, } from "./lib/router/links"; -export type { TypedResponse } from "./lib/server-runtime/responses"; +export type { + TypedResponse, + JsonFunction, +} from "./lib/server-runtime/responses"; export type { // TODO: (v7) Clean up code paths for these exports diff --git a/packages/react-router/lib/router/utils.ts b/packages/react-router/lib/router/utils.ts index 8715cec2b6..eeb91d30be 100644 --- a/packages/react-router/lib/router/utils.ts +++ b/packages/react-router/lib/router/utils.ts @@ -1,3 +1,4 @@ +import type { JsonFunction } from "../server-runtime/responses"; import type { Location, Path, To } from "./history"; import { invariant, parsePath, warning } from "./history"; @@ -1305,11 +1306,6 @@ export const normalizeSearch = (search: string): string => export const normalizeHash = (hash: string): string => !hash || hash === "#" ? "" : hash.startsWith("#") ? hash : "#" + hash; -export type JsonFunction = ( - data: Data, - init?: number | ResponseInit -) => Response; - /** * This is a shortcut for creating `application/json` responses. Converts `data` * to JSON and sets the `Content-Type` header. diff --git a/packages/react-router/lib/types.ts b/packages/react-router/lib/types.ts index 01fd21b1d4..0890c10462 100644 --- a/packages/react-router/lib/types.ts +++ b/packages/react-router/lib/types.ts @@ -1,4 +1,7 @@ +import type { DataWithResponseInit } from "./router/utils"; import type { AppLoadContext } from "./server-runtime/data"; +import type { Jsonify } from "./server-runtime/jsonify"; +import type { TypedResponse } from "./server-runtime/responses"; import type { Serializable } from "./server-runtime/single-fetch"; export type Expect = T; @@ -28,8 +31,20 @@ type DataFrom = T extends Fn ? VoidToUndefined>> : undefined -type ServerDataFrom = Serialize>; -type ClientDataFrom = 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 + +type ServerDataFrom = ServerData>; +type ClientDataFrom = ClientData>; // prettier-ignore type IsHydrate = @@ -145,6 +160,8 @@ export type CreateErrorBoundaryProps = { actionData?: ActionData; }; +type Pretty = { [K in keyof T]: T[K] } & {}; + // eslint-disable-next-line @typescript-eslint/no-unused-vars type __tests = [ // ServerDataFrom @@ -155,6 +172,18 @@ type __tests = [ { a: string; b: Date; c: undefined } > >, + Expect< + Equal< + Pretty< + ServerDataFrom< + () => + | TypedResponse<{ json: string; b: Date; c: () => boolean }> + | DataWithResponseInit<{ data: string; b: Date; c: () => boolean }> + > + >, + { json: string; b: string } | { data: string; b: Date; c: undefined } + > + >, // ClientDataFrom Expect, undefined>>, @@ -164,6 +193,18 @@ type __tests = [ { a: string; b: Date; c: () => boolean } > >, + Expect< + Equal< + Pretty< + ClientDataFrom< + () => + | TypedResponse<{ json: string; b: Date; c: () => boolean }> + | DataWithResponseInit<{ data: string; b: Date; c: () => boolean }> + > + >, + { json: string; b: string } | { data: string; b: Date; c: () => boolean } + > + >, // LoaderData Expect, undefined>>, From 82f32bfefe8430953102baa8698b67b48635a1b9 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Mon, 7 Oct 2024 11:22:36 +1100 Subject: [PATCH 07/26] Add cloudflare and architect to typedoc (#12086) --- packages/react-router-architect/typedoc.json | 3 +++ packages/react-router-cloudflare/typedoc.json | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 packages/react-router-architect/typedoc.json create mode 100644 packages/react-router-cloudflare/typedoc.json diff --git a/packages/react-router-architect/typedoc.json b/packages/react-router-architect/typedoc.json new file mode 100644 index 0000000000..eed6d3852f --- /dev/null +++ b/packages/react-router-architect/typedoc.json @@ -0,0 +1,3 @@ +{ + "entryPoints": ["./index.ts"] +} diff --git a/packages/react-router-cloudflare/typedoc.json b/packages/react-router-cloudflare/typedoc.json new file mode 100644 index 0000000000..eed6d3852f --- /dev/null +++ b/packages/react-router-cloudflare/typedoc.json @@ -0,0 +1,3 @@ +{ + "entryPoints": ["./index.ts"] +} From 24cc854fef6771774e6f0505a684bae6a9491e27 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Mon, 7 Oct 2024 11:22:56 +1100 Subject: [PATCH 08/26] Remove Remix references from test fixtures (#12087) --- .../node-template/app/routes/_index.tsx | 31 ++----------------- .../vite-template/app/routes/_index.tsx | 31 ++----------------- .../fixtures/node/app/routes/_index.tsx | 31 ++----------------- 3 files changed, 9 insertions(+), 84 deletions(-) diff --git a/integration/helpers/node-template/app/routes/_index.tsx b/integration/helpers/node-template/app/routes/_index.tsx index d35260cb00..ecfc25c614 100644 --- a/integration/helpers/node-template/app/routes/_index.tsx +++ b/integration/helpers/node-template/app/routes/_index.tsx @@ -2,40 +2,15 @@ import type { MetaFunction } from "react-router"; export const meta: MetaFunction = () => { return [ - { title: "New Remix App" }, - { name: "description", content: "Welcome to Remix!" }, + { title: "New React Router App" }, + { name: "description", content: "Welcome to React Router!" }, ]; }; export default function Index() { return (
-

Welcome to Remix

- +

Welcome to React Router

); } diff --git a/integration/helpers/vite-template/app/routes/_index.tsx b/integration/helpers/vite-template/app/routes/_index.tsx index d35260cb00..ecfc25c614 100644 --- a/integration/helpers/vite-template/app/routes/_index.tsx +++ b/integration/helpers/vite-template/app/routes/_index.tsx @@ -2,40 +2,15 @@ import type { MetaFunction } from "react-router"; export const meta: MetaFunction = () => { return [ - { title: "New Remix App" }, - { name: "description", content: "Welcome to Remix!" }, + { title: "New React Router App" }, + { name: "description", content: "Welcome to React Router!" }, ]; }; export default function Index() { return (
-

Welcome to Remix

- +

Welcome to React Router

); } diff --git a/packages/react-router-dev/__tests__/fixtures/node/app/routes/_index.tsx b/packages/react-router-dev/__tests__/fixtures/node/app/routes/_index.tsx index d35260cb00..ecfc25c614 100644 --- a/packages/react-router-dev/__tests__/fixtures/node/app/routes/_index.tsx +++ b/packages/react-router-dev/__tests__/fixtures/node/app/routes/_index.tsx @@ -2,40 +2,15 @@ import type { MetaFunction } from "react-router"; export const meta: MetaFunction = () => { return [ - { title: "New Remix App" }, - { name: "description", content: "Welcome to Remix!" }, + { title: "New React Router App" }, + { name: "description", content: "Welcome to React Router!" }, ]; }; export default function Index() { return (
-

Welcome to Remix

- +

Welcome to React Router

); } From 3d3357fb342f8a043cfbdcb8198c3c4128f96e51 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Mon, 7 Oct 2024 11:27:33 +1100 Subject: [PATCH 09/26] Tidy up readmes (#12088) --- packages/react-router-architect/README.md | 2 +- packages/react-router-cloudflare/README.md | 2 +- packages/react-router-dev/README.md | 12 +++--------- packages/react-router-express/README.md | 12 +++--------- packages/react-router-fs-routes/README.md | 2 +- packages/react-router-node/README.md | 12 +++--------- .../README.md | 2 +- packages/react-router-serve/README.md | 12 +++--------- 8 files changed, 16 insertions(+), 40 deletions(-) diff --git a/packages/react-router-architect/README.md b/packages/react-router-architect/README.md index 7180258125..897dbdc8f1 100644 --- a/packages/react-router-architect/README.md +++ b/packages/react-router-architect/README.md @@ -1,4 +1,4 @@ -# React Router Architect +# @react-router/architect Architect server request handler for React Router. diff --git a/packages/react-router-cloudflare/README.md b/packages/react-router-cloudflare/README.md index 5bc72d9acc..9c8b6b051b 100644 --- a/packages/react-router-cloudflare/README.md +++ b/packages/react-router-cloudflare/README.md @@ -1,4 +1,4 @@ -# React Router Cloudflare +# @react-router/cloudflare Cloudflare platform abstractions for [React Router.](https://reactrouter.com) diff --git a/packages/react-router-dev/README.md b/packages/react-router-dev/README.md index 40685a7476..9cb7bfc251 100644 --- a/packages/react-router-dev/README.md +++ b/packages/react-router-dev/README.md @@ -1,13 +1,7 @@ -# Welcome to Remix! +# @react-router/dev -[Remix](https://remix.run) is a web framework that helps you build better websites with React. - -To get started, open a new shell and run: +Dev tools and CLI for [React Router.](https://github.com/remix-run/react-router) ```sh -npx create-remix@latest +npm install @react-router/dev ``` - -Then follow the prompts you see in your terminal. - -For more information about Remix, [visit remix.run](https://remix.run)! diff --git a/packages/react-router-express/README.md b/packages/react-router-express/README.md index 40685a7476..b949e57465 100644 --- a/packages/react-router-express/README.md +++ b/packages/react-router-express/README.md @@ -1,13 +1,7 @@ -# Welcome to Remix! +# @react-router/express -[Remix](https://remix.run) is a web framework that helps you build better websites with React. - -To get started, open a new shell and run: +[Express](https://expressjs.com) server request handler for [React Router.](https://github.com/remix-run/react-router) ```sh -npx create-remix@latest +npm install @react-router/express ``` - -Then follow the prompts you see in your terminal. - -For more information about Remix, [visit remix.run](https://remix.run)! diff --git a/packages/react-router-fs-routes/README.md b/packages/react-router-fs-routes/README.md index 705be3f154..16784ba0ed 100644 --- a/packages/react-router-fs-routes/README.md +++ b/packages/react-router-fs-routes/README.md @@ -3,5 +3,5 @@ File system routing conventions for [React Router.](https://github.com/remix-run/react-router) ```sh -npm i @react-router/fs-routes +npm install @react-router/fs-routes ``` diff --git a/packages/react-router-node/README.md b/packages/react-router-node/README.md index 40685a7476..6478831278 100644 --- a/packages/react-router-node/README.md +++ b/packages/react-router-node/README.md @@ -1,13 +1,7 @@ -# Welcome to Remix! +# @react-router/node -[Remix](https://remix.run) is a web framework that helps you build better websites with React. - -To get started, open a new shell and run: +Node.js platform abstractions for [React Router.](https://github.com/remix-run/react-router) ```sh -npx create-remix@latest +npm install @react-router/node ``` - -Then follow the prompts you see in your terminal. - -For more information about Remix, [visit remix.run](https://remix.run)! diff --git a/packages/react-router-remix-config-routes-adapter/README.md b/packages/react-router-remix-config-routes-adapter/README.md index 20aa361a89..df1e7e2ff9 100644 --- a/packages/react-router-remix-config-routes-adapter/README.md +++ b/packages/react-router-remix-config-routes-adapter/README.md @@ -3,5 +3,5 @@ [Remix](https://remix.run) config route support for [React Router.](https://github.com/remix-run/react-router) ```sh -npm i @react-router/remix-config-routes-adapter +npm install @react-router/remix-config-routes-adapter ``` diff --git a/packages/react-router-serve/README.md b/packages/react-router-serve/README.md index 40685a7476..7215d98987 100644 --- a/packages/react-router-serve/README.md +++ b/packages/react-router-serve/README.md @@ -1,13 +1,7 @@ -# Welcome to Remix! +# @react-router/serve -[Remix](https://remix.run) is a web framework that helps you build better websites with React. - -To get started, open a new shell and run: +Production application server for [React Router.](https://github.com/remix-run/react-router) ```sh -npx create-remix@latest +npm install @react-router/serve ``` - -Then follow the prompts you see in your terminal. - -For more information about Remix, [visit remix.run](https://remix.run)! From 2c99c5dfd97cc5157425f1dc1ebda9ff3c05f80b Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Mon, 7 Oct 2024 11:43:50 +1100 Subject: [PATCH 10/26] Remove Remix references in basic template (#12089) --- templates/basic/app/routes/home.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/basic/app/routes/home.tsx b/templates/basic/app/routes/home.tsx index 15d6f05ddd..2fdc30dd86 100644 --- a/templates/basic/app/routes/home.tsx +++ b/templates/basic/app/routes/home.tsx @@ -2,8 +2,8 @@ import type { MetaFunction } from "react-router"; export const meta: MetaFunction = () => { return [ - { title: "New Remix App" }, - { name: "description", content: "Welcome to Remix!" }, + { title: "New React Router App" }, + { name: "description", content: "Welcome to React Router!" }, ]; }; @@ -18,12 +18,12 @@ export default function Index() {
Remix Remix
From 44c4b66b26cb0f363f0b29003d495cae1de54cf3 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Mon, 7 Oct 2024 11:53:18 +1100 Subject: [PATCH 11/26] Fix Remix reference in pre-rendering docs (#12090) --- docs/misc/pre-rendering.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/misc/pre-rendering.md b/docs/misc/pre-rendering.md index a115a6b94a..d924e88c2c 100644 --- a/docs/misc/pre-rendering.md +++ b/docs/misc/pre-rendering.md @@ -92,7 +92,7 @@ If you pre-render all of the paths in your application, you can deploy your `bui ### Serving via react-router-serve -By default, `react-router-serve` will serve these files via [`express.static`][express-static] and any paths that do not match a static file will fall through to the Remix handler. +By default, `react-router-serve` will serve these files via [`express.static`][express-static] and any paths that do not match a static file will fall through to the React Router handler. This even allows you to run a hybrid setup where _some_ of your routes are pre-rendered and others are dynamically rendered at runtime. For example, you could prerender anything inside `/blog/*` and server-render anything inside `/auth/*`. From 0a65ca59a5fb1b1da7dc6532da5d2bd45fd6a1de Mon Sep 17 00:00:00 2001 From: Onur Guvenc Date: Mon, 7 Oct 2024 06:26:28 +0300 Subject: [PATCH 12/26] docs: fix minor issues in docs/start/routing.md (#12084) --- contributors.yml | 1 + docs/start/routing.md | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/contributors.yml b/contributors.yml index cff1a1df41..0a7c400fb8 100644 --- a/contributors.yml +++ b/contributors.yml @@ -206,6 +206,7 @@ - OlegDev1 - omahs - omar-moquete +- OnurGvnc - p13i - parched - parveen232 diff --git a/docs/start/routing.md b/docs/start/routing.md index 4ddcba1da5..3f3eae8150 100644 --- a/docs/start/routing.md +++ b/docs/start/routing.md @@ -24,7 +24,7 @@ Routes are configured in `app/routes.ts`. Routes have a url pattern to match the import { route } from "@react-router/dev/routes"; export const routes = [ - route("some/path", "./some/file.tsx"); + route("some/path", "./some/file.tsx"), // pattern ^ ^ module file ] ``` @@ -63,7 +63,7 @@ If you prefer to define your routes via file naming conventions rather than conf The files referenced in `routes.ts` define each route's behavior: ```tsx filename=app/routes.ts -route("teams/:teamId", "./team.tsx"); +route("teams/:teamId", "./team.tsx"), // route module ^^^^^^^^ ``` @@ -83,7 +83,7 @@ export async function loader({ params }: Route.LoaderArgs) { export default function Component({ loaderData, }: Route.ComponentProps) { - return

{data.name}

; + return

{loaderData.name}

; } ``` @@ -162,7 +162,7 @@ export const routes: RouteConfig = [ ## Index Routes ```ts -index(componentFile); +index(componentFile), ``` Index routes render into their parent's [Outlet][outlet] at their parent's URL (like a default child route). @@ -192,14 +192,14 @@ Note that index routes can't have children. If a path segment starts with `:` then it becomes a "dynamic segment". When the route matches the URL, the dynamic segment will be parsed from the URL and provided as `params` to other router APIs. ```ts filename=app/routes.ts -route("teams/:teamId", "./team.tsx"); +route("teams/:teamId", "./team.tsx"), ``` ```tsx filename=app/team.tsx import type * as Route from "./+types.team"; -async function loader({ params }: Route.LoaderArgs) { - // ^? { teamId: string } +export async function loader({ params }: Route.LoaderArgs) { + // ^? { teamId: string } } export default function Component({ @@ -213,7 +213,7 @@ export default function Component({ You can have multiple dynamic segments in one route path: ```ts filename=app/routes.ts -route("c/:categoryId/p/:productId", "./product.tsx"); +route("c/:categoryId/p/:productId", "./product.tsx"), ``` ```tsx filename=app/product.tsx @@ -229,7 +229,7 @@ async function loader({ params }: LoaderArgs) { You can make a route segment optional by adding a `?` to the end of the segment. ```ts filename=app/routes.ts -route(":lang?/categories", "./categories.tsx"); +route(":lang?/categories", "./categories.tsx"), ``` You can have optional static segments, too: @@ -243,7 +243,7 @@ route("users/:userId/edit?", "./user.tsx"); Also known as "catchall" and "star" segments. If a route path pattern ends with `/*` then it will match any characters following the `/`, including other `/` characters. ```ts filename=app/routes.ts -route("files/*", "./files.tsx"); +route("files/*", "./files.tsx"), ``` ```tsx filename=app/files.tsx From d294ab2749d6ad4af386bece803e7b486774e983 Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Mon, 7 Oct 2024 03:27:05 +0000 Subject: [PATCH 13/26] chore: format --- docs/start/routing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/start/routing.md b/docs/start/routing.md index 3f3eae8150..3a73e96129 100644 --- a/docs/start/routing.md +++ b/docs/start/routing.md @@ -26,7 +26,7 @@ import { route } from "@react-router/dev/routes"; export const routes = [ route("some/path", "./some/file.tsx"), // pattern ^ ^ module file -] +]; ``` Here is a larger sample route config: From 2e59ea1a81ba3029116b175d581a40c490307dcf Mon Sep 17 00:00:00 2001 From: Brooks Lybrand Date: Mon, 7 Oct 2024 08:31:48 -0500 Subject: [PATCH 14/26] docs: fix bad link to deploying --- docs/upgrading/vite-router-provider.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/upgrading/vite-router-provider.md b/docs/upgrading/vite-router-provider.md index 6f11322527..45f50a3c59 100644 --- a/docs/upgrading/vite-router-provider.md +++ b/docs/upgrading/vite-router-provider.md @@ -230,7 +230,7 @@ The first few routes you migrate are the hardest because you often have to acces ## Enable SSR and/or Pre-rendering -If you want to enable server rendering and static pre-rendering, you can do so with the `ssr` and `prerender` options in the bundler plugin. For SSR you'll need to also deploy the server build to a server. See [Deploying](./deploying) for more information. +If you want to enable server rendering and static pre-rendering, you can do so with the `ssr` and `prerender` options in the bundler plugin. For SSR you'll need to also deploy the server build to a server. See [Deploying](../start/deploying) for more information. ```ts filename=vite.config.ts import { reactRouter } from "@react-router/dev/vite"; From e62d585cc4c664d9e58c7342a9e1140a0ad6f8c9 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 7 Oct 2024 12:37:42 -0400 Subject: [PATCH 15/26] Fix react-router-serve for prerendered files and avoid dup loader invocation (#12071) --- .changeset/rare-plums-chew.md | 8 +++ docs/misc/pre-rendering.md | 14 +---- integration/vite-prerender-test.ts | 57 +++++++++++++++++++ packages/react-router-dev/vite/plugin.ts | 20 ++++--- packages/react-router-serve/cli.ts | 13 +---- .../react-router/lib/server-runtime/routes.ts | 39 ++++++++++++- 6 files changed, 115 insertions(+), 36 deletions(-) create mode 100644 .changeset/rare-plums-chew.md diff --git a/.changeset/rare-plums-chew.md b/.changeset/rare-plums-chew.md new file mode 100644 index 0000000000..6624883cd6 --- /dev/null +++ b/.changeset/rare-plums-chew.md @@ -0,0 +1,8 @@ +--- +"@react-router/serve": patch +"@react-router/dev": patch +"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 diff --git a/docs/misc/pre-rendering.md b/docs/misc/pre-rendering.md index d924e88c2c..419d402e49 100644 --- a/docs/misc/pre-rendering.md +++ b/docs/misc/pre-rendering.md @@ -111,19 +111,7 @@ app.use( ); // Serve static HTML and .data requests without Cache-Control -app.use( - "/", - express.static("build/client", { - // Don't redirect directory index.html requests to include a trailing slash - redirect: false, - setHeaders: function (res, path) { - // Add the proper Content-Type for turbo-stream data responses - if (path.endsWith(".data")) { - res.set("Content-Type", "text/x-turbo"); - } - }, - }) -); +app.use("/", express.static("build/client")); // Serve remaining unhandled requests via your React Router handler app.all( diff --git a/integration/vite-prerender-test.ts b/integration/vite-prerender-test.ts index ca08a359f7..4b50eb57df 100644 --- a/integration/vite-prerender-test.ts +++ b/integration/vite-prerender-test.ts @@ -414,6 +414,63 @@ test.describe("Prerendering", () => { expect(await app.getHtml()).toContain("NOT-PRERENDERED-false"); }); + test("Does not encounter header limits on large prerendered data", async ({ + page, + }) => { + fixture = await createFixture({ + // Even thogh we are prerendering, we want a running server so we can + // hit the pre-rendered HTML file and a non-prerendered route + prerender: false, + files: { + ...files, + "vite.config.ts": js` + import { defineConfig } from "vite"; + import { reactRouter } from "@react-router/dev/vite"; + + export default defineConfig({ + build: { manifest: true }, + plugins: [ + reactRouter({ + prerender: ["/", "/about"], + }) + ], + }); + `, + "app/routes/about.tsx": js` + import { useLoaderData } from 'react-router'; + export function loader({ request }) { + return { + prerendered: request.headers.has('X-React-Router-Prerender') ? 'yes' : 'no', + // 24999 characters + data: new Array(5000).fill('test').join('-'), + }; + } + + export default function Comp() { + let data = useLoaderData(); + return ( + <> +

Large loader

+

{data.prerendered}

+

{data.data.length}

+ + ); + } + `, + }, + }); + appFixture = await createAppFixture(fixture); + + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/about"); + await page.waitForSelector("[data-mounted]"); + expect(await app.getHtml("[data-title]")).toContain("Large loader"); + expect(await app.getHtml("[data-prerendered]")).toContain("yes"); + expect(await app.getHtml("[data-length]")).toBe( + '

24999

' + ); + }); + test("Renders down to the proper HydrateFallback", async ({ page }) => { fixture = await createFixture({ prerender: true, diff --git a/packages/react-router-dev/vite/plugin.ts b/packages/react-router-dev/vite/plugin.ts index ceda0905a4..07593480a3 100644 --- a/packages/react-router-dev/vite/plugin.ts +++ b/packages/react-router-dev/vite/plugin.ts @@ -1831,23 +1831,22 @@ async function handlePrerender( } else { routesToPrerender = reactRouterConfig.prerender || ["/"]; } - let requestInit = { - headers: { - // Header that can be used in the loader to know if you're running at - // build time or runtime - "X-React-Router-Prerender": "yes", - }, + let headers = { + // Header that can be used in the loader to know if you're running at + // build time or runtime + "X-React-Router-Prerender": "yes", }; for (let path of routesToPrerender) { let hasLoaders = matchRoutes(routes, path)?.some((m) => m.route.loader); + let data: string | undefined; if (hasLoaders) { - await prerenderData( + data = await prerenderData( handler, path, clientBuildDirectory, reactRouterConfig, viteConfig, - requestInit + { headers } ); } await prerenderRoute( @@ -1856,7 +1855,9 @@ async function handlePrerender( clientBuildDirectory, reactRouterConfig, viteConfig, - requestInit + data + ? { headers: { ...headers, "X-React-Router-Prerender-Data": data } } + : { headers } ); } @@ -1934,6 +1935,7 @@ async function prerenderData( await fse.ensureDir(path.dirname(outfile)); await fse.outputFile(outfile, data); viteConfig.logger.info(`Prerender: Generated ${colors.bold(outfile)}`); + return data; } async function prerenderRoute( diff --git a/packages/react-router-serve/cli.ts b/packages/react-router-serve/cli.ts index ab7a0feff7..8929f8624e 100644 --- a/packages/react-router-serve/cli.ts +++ b/packages/react-router-serve/cli.ts @@ -83,18 +83,7 @@ async function run() { maxAge: "1y", }) ); - app.use( - build.publicPath, - express.static(build.assetsBuildDirectory, { - // Don't redirect directory index.html request to include a trailing slash - redirect: false, - setHeaders: function (res, path) { - if (path.endsWith(".data")) { - res.set("Content-Type", "text/x-turbo"); - } - }, - }) - ); + app.use(build.publicPath, express.static(build.assetsBuildDirectory)); app.use(express.static("public", { maxAge: "1h" })); app.use(morgan("tiny")); diff --git a/packages/react-router/lib/server-runtime/routes.ts b/packages/react-router/lib/server-runtime/routes.ts index 136121a5ee..26fdd34a64 100644 --- a/packages/react-router/lib/server-runtime/routes.ts +++ b/packages/react-router/lib/server-runtime/routes.ts @@ -10,6 +10,12 @@ import type { LoaderFunctionArgs, ServerRouteModule, } from "./routeModules"; +import type { + SingleFetchResult, + SingleFetchResults, +} from "../dom/ssr/single-fetch"; +import { decodeViaTurboStream } from "../dom/ssr/single-fetch"; +import invariant from "./invariant"; export interface RouteManifest { [routeId: string]: Route; @@ -95,8 +101,37 @@ export function createStaticHandlerDataRoutes( // Need to use RR's version in the param typed here to permit the optional // context even though we know it'll always be provided in remix loader: route.module.loader - ? (args: RRLoaderFunctionArgs) => - callRouteHandler(route.module.loader!, args as LoaderFunctionArgs) + ? async (args: RRLoaderFunctionArgs) => { + // If we're prerendering, use the data passed in from prerendering + // the .data route so we dom't call loaders twice + if (args.request.headers.has("X-React-Router-Prerender-Data")) { + let encoded = args.request.headers.get( + "X-React-Router-Prerender-Data" + ); + invariant(encoded, "Missing prerendered data for route"); + let uint8array = new TextEncoder().encode(encoded); + let stream = new ReadableStream({ + start(controller) { + controller.enqueue(uint8array); + controller.close(); + }, + }); + let decoded = await decodeViaTurboStream(stream, global); + let data = decoded.value as SingleFetchResults; + invariant( + data && route.id in data, + "Unable to decode prerendered data" + ); + let result = data[route.id] as SingleFetchResult; + invariant("data" in result, "Unable to process prerendered data"); + return result.data; + } + let val = await callRouteHandler( + route.module.loader!, + args as LoaderFunctionArgs + ); + return val; + } : undefined, action: route.module.action ? (args: RRActionFunctionArgs) => From 6c7c5147c6e434d6d2350d1762ab8f4fb6287069 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Tue, 8 Oct 2024 14:57:08 +1100 Subject: [PATCH 16/26] Add `prefix` route config helper (#12094) --- .changeset/three-seals-play.md | 5 + docs/start/routing.md | 36 +++- .../__tests__/route-config-test.ts | 161 +++++++++++++++++- packages/react-router-dev/config/routes.ts | 67 +++++--- packages/react-router-dev/routes.ts | 1 + 5 files changed, 244 insertions(+), 26 deletions(-) create mode 100644 .changeset/three-seals-play.md diff --git a/.changeset/three-seals-play.md b/.changeset/three-seals-play.md new file mode 100644 index 0000000000..59ab4af832 --- /dev/null +++ b/.changeset/three-seals-play.md @@ -0,0 +1,5 @@ +--- +"@react-router/dev": minor +--- + +Add `prefix` route config helper to `@react-router/dev/routes` diff --git a/docs/start/routing.md b/docs/start/routing.md index 3a73e96129..0791835156 100644 --- a/docs/start/routing.md +++ b/docs/start/routing.md @@ -37,6 +37,7 @@ import { route, index, layout, + prefix, } from "@react-router/dev/routes"; export const routes: RouteConfig = [ @@ -48,7 +49,7 @@ export const routes: RouteConfig = [ route("register", "./auth/register.tsx"), ]), - route("concerts", [ + ...prefix("concerts", [ index("./concerts/home.tsx"), route(":city", "./concerts/city.tsx"), route("trending", "./concerts/trending.tsx"), @@ -136,12 +137,13 @@ Every route in `routes.ts` is nested inside the special `app/root.tsx` module. Using `layout`, layout routes create new nesting for their children, but they don't add any segments to the URL. It's like the root route but they can be added at any level. -```tsx filename=app/routes.ts lines=[9,15] +```tsx filename=app/routes.ts lines=[10,16] import { type RouteConfig, route, layout, index, + prefix, } from "@react-router/dev/routes"; export const routes: RouteConfig = [ @@ -149,7 +151,7 @@ export const routes: RouteConfig = [ index("./marketing/home.tsx"), route("contact", "./marketing/contact.tsx"), ]), - route("projects", [ + ...prefix("projects", [ index("./projects/home.tsx"), layout("./projects/project-layout.tsx", [ route(":pid", "./projects/project.tsx"), @@ -187,6 +189,34 @@ export const routes: RouteConfig = [ Note that index routes can't have children. +## Route Prefixes + +Using `prefix`, you can add a path prefix to a set of routes without needing to introduce a parent route file. + +```tsx filename=app/routes.ts lines=[14] +import { + type RouteConfig, + route, + layout, + index, + prefix, +} from "@react-router/dev/routes"; + +export const routes: RouteConfig = [ + layout("./marketing/layout.tsx", [ + index("./marketing/home.tsx"), + route("contact", "./marketing/contact.tsx"), + ]), + ...prefix("projects", [ + index("./projects/home.tsx"), + layout("./projects/project-layout.tsx", [ + route(":pid", "./projects/project.tsx"), + route(":pid/edit", "./projects/edit-project.tsx"), + ]), + ]), +]; +``` + ## Dynamic Segments If a path segment starts with `:` then it becomes a "dynamic segment". When the route matches the URL, the dynamic segment will be parsed from the URL and provided as `params` to other router APIs. diff --git a/packages/react-router-dev/__tests__/route-config-test.ts b/packages/react-router-dev/__tests__/route-config-test.ts index e8ed077b73..4fcdddbb4a 100644 --- a/packages/react-router-dev/__tests__/route-config-test.ts +++ b/packages/react-router-dev/__tests__/route-config-test.ts @@ -3,6 +3,7 @@ import { route, layout, index, + prefix, relative, } from "../config/routes"; @@ -12,9 +13,9 @@ describe("route config", () => { expect( validateRouteConfig({ routeConfigFile: "routes.ts", - routeConfig: [ + routeConfig: prefix("prefix", [ route("parent", "parent.tsx", [route("child", "child.tsx")]), - ], + ]), }).valid ).toBe(true); }); @@ -306,6 +307,157 @@ describe("route config", () => { }); }); + describe("prefix", () => { + it("adds a prefix to routes", () => { + expect(prefix("prefix", [route("route", "routes/route.tsx")])) + .toMatchInlineSnapshot(` + [ + { + "children": undefined, + "file": "routes/route.tsx", + "path": "prefix/route", + }, + ] + `); + }); + + it("adds a prefix to routes with a blank path", () => { + expect(prefix("prefix", [route("", "routes/route.tsx")])) + .toMatchInlineSnapshot(` + [ + { + "children": undefined, + "file": "routes/route.tsx", + "path": "prefix", + }, + ] + `); + }); + + it("adds a prefix with a trailing slash to routes", () => { + expect(prefix("prefix/", [route("route", "routes/route.tsx")])) + .toMatchInlineSnapshot(` + [ + { + "children": undefined, + "file": "routes/route.tsx", + "path": "prefix/route", + }, + ] + `); + }); + + it("adds a prefix to routes with leading slash", () => { + expect(prefix("prefix", [route("/route", "routes/route.tsx")])) + .toMatchInlineSnapshot(` + [ + { + "children": undefined, + "file": "routes/route.tsx", + "path": "prefix/route", + }, + ] + `); + }); + + it("adds a prefix with a trailing slash to routes with leading slash", () => { + expect(prefix("prefix/", [route("/route", "routes/route.tsx")])) + .toMatchInlineSnapshot(` + [ + { + "children": undefined, + "file": "routes/route.tsx", + "path": "prefix/route", + }, + ] + `); + }); + + it("adds a prefix to index routes", () => { + expect(prefix("prefix", [index("routes/index.tsx")])) + .toMatchInlineSnapshot(` + [ + { + "children": undefined, + "file": "routes/index.tsx", + "index": true, + "path": "prefix", + }, + ] + `); + }); + + it("adds a prefix to children of layout routes", () => { + expect( + prefix("prefix", [ + layout("routes/layout.tsx", [route("route", "routes/route.tsx")]), + ]) + ).toMatchInlineSnapshot(` + [ + { + "children": [ + { + "children": undefined, + "file": "routes/route.tsx", + "path": "prefix/route", + }, + ], + "file": "routes/layout.tsx", + }, + ] + `); + }); + + it("adds a prefix to children of nested layout routes", () => { + expect( + prefix("prefix", [ + layout("routes/layout-1.tsx", [ + route("layout-1-child", "routes/layout-1-child.tsx"), + layout("routes/layout-2.tsx", [ + route("layout-2-child", "routes/layout-2-child.tsx"), + layout("routes/layout-3.tsx", [ + route("layout-3-child", "routes/layout-3-child.tsx"), + ]), + ]), + ]), + ]) + ).toMatchInlineSnapshot(` + [ + { + "children": [ + { + "children": undefined, + "file": "routes/layout-1-child.tsx", + "path": "prefix/layout-1-child", + }, + { + "children": [ + { + "children": undefined, + "file": "routes/layout-2-child.tsx", + "path": "prefix/layout-2-child", + }, + { + "children": [ + { + "children": undefined, + "file": "routes/layout-3-child.tsx", + "path": "prefix/layout-3-child", + }, + ], + "file": "routes/layout-3.tsx", + }, + ], + "file": "routes/layout-2.tsx", + }, + ], + "file": "routes/layout-1.tsx", + }, + ] + `); + }); + }); + describe("relative", () => { it("supports relative routes", () => { let { route } = relative("/path/to/dirname"); @@ -368,6 +520,11 @@ describe("route config", () => { } `); }); + + it("provides passthrough for non-relative APIs", () => { + let { prefix: relativePrefix } = relative("/path/to/dirname"); + expect(relativePrefix).toBe(prefix); + }); }); }); }); diff --git a/packages/react-router-dev/config/routes.ts b/packages/react-router-dev/config/routes.ts index 428f62081a..2595d4da7d 100644 --- a/packages/react-router-dev/config/routes.ts +++ b/packages/react-router-dev/config/routes.ts @@ -183,18 +183,18 @@ type CreateRouteOptions = Pick< * Helper function for creating a route config entry, for use within * `routes.ts`. */ -function createRoute( +function route( path: string | null | undefined, file: string, children?: RouteConfigEntry[] ): RouteConfigEntry; -function createRoute( +function route( path: string | null | undefined, file: string, options: CreateRouteOptions, children?: RouteConfigEntry[] ): RouteConfigEntry; -function createRoute( +function route( path: string | null | undefined, file: string, optionsOrChildren: CreateRouteOptions | RouteConfigEntry[] | undefined, @@ -227,10 +227,7 @@ type CreateIndexOptions = Pick< * Helper function for creating a route config entry for an index route, for use * within `routes.ts`. */ -function createIndex( - file: string, - options?: CreateIndexOptions -): RouteConfigEntry { +function index(file: string, options?: CreateIndexOptions): RouteConfigEntry { return { file, index: true, @@ -249,16 +246,13 @@ type CreateLayoutOptions = Pick< * Helper function for creating a route config entry for a layout route, for use * within `routes.ts`. */ -function createLayout( - file: string, - children?: RouteConfigEntry[] -): RouteConfigEntry; -function createLayout( +function layout(file: string, children?: RouteConfigEntry[]): RouteConfigEntry; +function layout( file: string, options: CreateLayoutOptions, children?: RouteConfigEntry[] ): RouteConfigEntry; -function createLayout( +function layout( file: string, optionsOrChildren: CreateLayoutOptions | RouteConfigEntry[] | undefined, children?: RouteConfigEntry[] @@ -278,19 +272,39 @@ function createLayout( }; } -export const route = createRoute; -export const index = createIndex; -export const layout = createLayout; +/** + * Helper function for adding a path prefix to a set of routes without needing + * to introduce a parent route file, for use within `routes.ts`. + */ +function prefix( + prefixPath: string, + routes: RouteConfigEntry[] +): RouteConfigEntry[] { + return routes.map((route) => { + if (route.index || typeof route.path === "string") { + return { + ...route, + path: route.path ? joinRoutePaths(prefixPath, route.path) : prefixPath, + children: route.children, + }; + } else if (route.children) { + return { + ...route, + children: prefix(prefixPath, route.children), + }; + } + return route; + }); +} + +const helpers = { route, index, layout, prefix }; +export { route, index, layout, prefix }; /** * Creates a set of route config helpers that resolve file paths relative to the * given directory, for use within `routes.ts`. This is designed to support * splitting route config into multiple files within different directories. */ -export function relative(directory: string): { - route: typeof route; - index: typeof index; - layout: typeof layout; -} { +export function relative(directory: string): typeof helpers { return { /** * Helper function for creating a route config entry, for use within @@ -319,6 +333,10 @@ export function relative(directory: string): { layout: (file, ...rest) => { return layout(resolve(directory, file), ...(rest as any)); }, + + // Passthrough of helper functions that don't need relative scoping so that + // a complete API is still provided. + prefix, }; } @@ -371,3 +389,10 @@ function normalizeSlashes(file: string) { function stripFileExtension(file: string) { return file.replace(/\.[a-z0-9]+$/i, ""); } + +function joinRoutePaths(path1: string, path2: string): string { + return [ + path1.replace(/\/+$/, ""), // Remove trailing slashes + path2.replace(/^\/+/, ""), // Remove leading slashes + ].join("/"); +} diff --git a/packages/react-router-dev/routes.ts b/packages/react-router-dev/routes.ts index 96140d6f72..c4ea5420ec 100644 --- a/packages/react-router-dev/routes.ts +++ b/packages/react-router-dev/routes.ts @@ -4,6 +4,7 @@ export { route, index, layout, + prefix, relative, getAppDirectory, } from "./config/routes"; From a5212bd82ad9e3dd6c2a052fb7227e1e6fecc121 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 10 Oct 2024 09:50:29 -0400 Subject: [PATCH 17/26] docs(upgrading/remix): update upgrading process (#12104) --- docs/upgrading/remix.md | 123 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 113 insertions(+), 10 deletions(-) diff --git a/docs/upgrading/remix.md b/docs/upgrading/remix.md index 8ece6f86ca..3e48e13e0c 100644 --- a/docs/upgrading/remix.md +++ b/docs/upgrading/remix.md @@ -5,22 +5,125 @@ hidden: true # Upgrading from Remix -This guide is still in development +This guide is still in development and is subject to change as React Router stabilizes prior to the `7.0.0` stable release -After the final React Router v7 release, we will go back to Remix to add future flags to any changed APIs. +Our intention is for the **Remix v2 -> React Router v7** upgrade path to be as non-breaking as possible via the use of [Future Flags][future-flags] and codemods for minor and straightforward code adjustments. To best prepare for this eventual upgrade, you can start by adopting all of the existing [Remix v2 Future Flags][v2-future-flags]. -If you want to attempt the rocky migration now, the following table will be helpful: +## Upgrading to the v7 Prerelease -| Remix v2 Package | | React Router v7 Package | -| ----------------------- | --- | -------------------------- | -| `@remix-run/react` | ➡️ | `react-router` | -| `@remix-run/dev` | ➡️ | `@react-router/dev` | -| `@remix-run/node` | ➡️ | `@react-router/node` | -| `@remix-run/cloudflare` | ➡️ | `@react-router/cloudflare` | +If you want to attempt the (potentially rocky) migration now, the following steps should get you most of the way there. If you run into issues please let us know in [Discord][remix-discord] or [Github][github-new-issue]. -Also note that nearly all modules your app needs come from `react-router` now instead of `@remix-run/node` and `@remix-run/cloudflare`, so try to import from there first. +### Step 1 - Adopt future flags + +Adopt all existing [future flags][v2-future-flags] in your Remix v2 application. + +### Step 2 - Update dependencies + +You'll need to update your dependencies from the `@remix-run/*` packages to `react-router` and `@react-router/*` packages in `package.json` and in your code where you import from packages: + +| Remix v2 Package | | React Router v7 Package | +| --------------------------- | --- | -------------------------- | +| `@remix-run/architect` | ➡️ | `@react-router/architect` | +| `@remix-run/cloudflare` | ➡️ | `@react-router/cloudflare` | +| `@remix-run/dev` | ➡️ | `@react-router/dev` | +| `@remix-run/express` | ➡️ | `@react-router/express` | +| `@remix-run/node` | ➡️ | `@react-router/node` | +| `@remix-run/react` | ➡️ | `react-router` | +| `@remix-run/serve` | ➡️ | `@react-router/serve` | +| `@remix-run/server-runtime` | ➡️ | `react-router` | +| `@remix-run/testing` | ➡️ | `react-router` | + +Most of the "shared" APIs that used to be re-exported through the runtime-specific packages (`@remix-run/node`, `@remix-run/cloudflare`, etc.) have all been collapsed into `react-router` in v7. So instead of importing from `@react-router/node` or `@react-router/cloudflare`, you'll import those directly from `react-router`. ```diff -import { redirect } from "@react-router/node"; +import { redirect } from "react-router"; ``` + +The only APIs should be importing from the runtime-specific packages in v7 are APIs that are specific to that runtime, such as `createFileSessionStorage` for Node and `createWorkersKVSessionStorage` for Cloudflare. + +### Step 3 - Change `scripts` in `package.json` + +Update the scripts in your `package.json`: + +| Script | Remix v2 | | React Router v7 | +| ----------- | ----------------------------------- | --- | ------------------------------------------ | +| `dev` | `remix vite:dev` | ➡️ | `react-router dev` | +| `build` | `remix vite:build` | ➡️ | `react-router build` | +| `start` | `remix-serve build/server/index.js` | ➡️ | `react-router-serve build/server/index.js` | +| `typecheck` | `tsc` | ➡️ | `react-router typegen && tsc` | + +### Step 4 - Rename plugin in `vite.config` + +Update the import and rename the plugin in your `vite.config.ts`: + +```diff +-import { vitePlugin as remix } from "@remix-run/dev"; ++import { reactRouter } from "@react-router/dev/vite"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig({ + plugins: [ +- remix({ +- future: { +- // all future flags adopted +- }, +- }), ++ reactRouter(), + tsconfigPaths(), + ], +}); +``` + +### Step 5 - Add a `routes.ts` file + +In React Router v7 you define your routes using the [`app/routes.ts`][routing] file. For backwards-compatibility and for folks who prefer [file-based conventions][fs-routing], you can opt-into the same "flat routes" convention you are using in Remix v2 via the new `@react-router/fs-routes` package: + +```ts filename=app/routes.ts +import { type RouteConfig } from "@react-router/dev/routes"; +import { flatRoutes } from "@react-router/fs-routes"; + +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: + +| Entry File | Remix v2 Component | | React Router v7 Component | +| ------------------ | ------------------ | --- | ------------------------- | +| `entry.server.tsx` | `` | ➡️ | `` | +| `entry.client.stx` | `` | ➡️ | `` | + +## Known Prerelease Issues + +### Typesafety + +We have introduced some major changes to improve the type story in v7, but we're still working on making sure the path to adopt them is as smooth as possible prior to a stable v7 release. You can read more about the new type story in the [v7 draft release notes][v7-changelog-types] and if it's not a huge lift, your best bet for types in v7 is to migrate to that approach across the board. + +For the time being we don't have a great story to _incrementally_ migrate data types to the v7 prerelease. We never brought the generics on data APIs (`useLoaderData`, `useFetcher`, `Await`, etc.) over from Remix to React Router because we knew that we could do better than what Remix v2 had, and we wanted to ensure that we didn't ship APIs in React Router just to yank them out. Now that we have a better idea of the type story in React Router v7, we're better able to see what the migration path looks like and we plan on shipping improvements in this area in an upcoming v7 prerelease. + +Currently, when you upgrade to React Router v7 you're going to get typescript yelling at you a bunch for these missing generics that existed in your Remix v2 app code. For now, you have 2 options to continue testing out the prerelease: + +**Option 1 - Ignore the type errors with `@ts-expect-error` or `@ts-ignore`** + +```diff ++// @ts-expect-error +let data = useLoaderData(); +``` + +**Option 2 - Remove the generics and cast the types manually** + +```diff +-let data = useLoaderData(); ++let data = useLoaderData() as ReturnType>; +``` + +[future-flags]: ../community/api-development-strategy +[v2-future-flags]: https://remix.run/docs/start/future-flags +[remix-discord]: https://rmx.as/discord +[github-new-issue]: https://github.com/remix-run/react-router/issues/new/choose +[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 From 845c9c9b6ffe73d8efde5bf02d49e0c578a0d369 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 10 Oct 2024 11:45:49 -0400 Subject: [PATCH 18/26] Expose patchRoutesOnNavigation errors directly (#12111) --- .../__tests__/router/lazy-discovery-test.ts | 31 ++---------- packages/react-router/lib/router/router.ts | 47 ++++--------------- 2 files changed, 13 insertions(+), 65 deletions(-) diff --git a/packages/react-router/__tests__/router/lazy-discovery-test.ts b/packages/react-router/__tests__/router/lazy-discovery-test.ts index 91d56d40b7..6428870d6d 100644 --- a/packages/react-router/__tests__/router/lazy-discovery-test.ts +++ b/packages/react-router/__tests__/router/lazy-discovery-test.ts @@ -1944,15 +1944,7 @@ describe("Lazy Route Discovery (Fog of War)", () => { actionData: null, loaderData: {}, errors: { - a: new ErrorResponseImpl( - 400, - "Bad Request", - new Error( - 'Unable to match URL "/a/b" - the `patchRoutesOnNavigation()` ' + - "function threw the following error:\nError: broke!" - ), - true - ), + a: new Error("broke!"), }, }); expect(router.state.matches.map((m) => m.route.id)).toEqual(["a"]); @@ -2019,15 +2011,7 @@ describe("Lazy Route Discovery (Fog of War)", () => { actionData: null, loaderData: {}, errors: { - a: new ErrorResponseImpl( - 400, - "Bad Request", - new Error( - 'Unable to match URL "/a/b" - the `patchRoutesOnNavigation()` ' + - "function threw the following error:\nError: broke!" - ), - true - ), + a: new Error("broke!"), }, }); expect(router.state.matches.map((m) => m.route.id)).toEqual(["a"]); @@ -2096,16 +2080,7 @@ describe("Lazy Route Discovery (Fog of War)", () => { actionData: null, loaderData: {}, errors: { - parent: new ErrorResponseImpl( - 400, - "Bad Request", - new Error( - 'Unable to match URL "/parent/child/grandchild" - the ' + - "`patchRoutesOnNavigation()` function threw the following " + - "error:\nError: broke!" - ), - true - ), + parent: new Error("broke!"), }, }); expect(router.state.matches.map((m) => m.route.id)).toEqual([ diff --git a/packages/react-router/lib/router/router.ts b/packages/react-router/lib/router/router.ts index a7eedb871c..81b5f1eed8 100644 --- a/packages/react-router/lib/router/router.ts +++ b/packages/react-router/lib/router/router.ts @@ -1714,17 +1714,15 @@ export function createRouter(init: RouterInit): Router { if (discoverResult.type === "aborted") { return { shortCircuited: true }; } else if (discoverResult.type === "error") { - let { boundaryId, error } = handleDiscoverRouteError( - location.pathname, - discoverResult - ); + let boundaryId = findNearestBoundary(discoverResult.partialMatches) + .route.id; return { matches: discoverResult.partialMatches, pendingActionResult: [ boundaryId, { type: ResultType.error, - error, + error: discoverResult.error, }, ], }; @@ -1887,15 +1885,13 @@ export function createRouter(init: RouterInit): Router { if (discoverResult.type === "aborted") { return { shortCircuited: true }; } else if (discoverResult.type === "error") { - let { boundaryId, error } = handleDiscoverRouteError( - location.pathname, - discoverResult - ); + let boundaryId = findNearestBoundary(discoverResult.partialMatches) + .route.id; return { matches: discoverResult.partialMatches, loaderData: {}, errors: { - [boundaryId]: error, + [boundaryId]: discoverResult.error, }, }; } else if (!discoverResult.matches) { @@ -2242,8 +2238,7 @@ export function createRouter(init: RouterInit): Router { if (discoverResult.type === "aborted") { return; } else if (discoverResult.type === "error") { - let { error } = handleDiscoverRouteError(path, discoverResult); - setFetcherError(key, routeId, error, { flushSync }); + setFetcherError(key, routeId, discoverResult.error, { flushSync }); return; } else if (!discoverResult.matches) { setFetcherError( @@ -2527,8 +2522,7 @@ export function createRouter(init: RouterInit): Router { if (discoverResult.type === "aborted") { return; } else if (discoverResult.type === "error") { - let { error } = handleDiscoverRouteError(path, discoverResult); - setFetcherError(key, routeId, error, { flushSync }); + setFetcherError(key, routeId, discoverResult.error, { flushSync }); return; } else if (!discoverResult.matches) { setFetcherError( @@ -3067,23 +3061,6 @@ export function createRouter(init: RouterInit): Router { return { notFoundMatches: matches, route, error }; } - function handleDiscoverRouteError( - pathname: string, - discoverResult: DiscoverRoutesErrorResult - ) { - return { - boundaryId: findNearestBoundary(discoverResult.partialMatches).route.id, - error: getInternalRouterError(400, { - type: "route-discovery", - pathname, - message: - discoverResult.error != null && "message" in discoverResult.error - ? discoverResult.error - : String(discoverResult.error), - }), - }; - } - // Opt in to capturing and reporting scroll positions during navigations, // used by the component function enableScrollRestoration( @@ -5310,7 +5287,7 @@ function getInternalRouterError( pathname?: string; routeId?: string; method?: string; - type?: "invalid-body" | "route-discovery"; + type?: "invalid-body"; message?: string; } = {} ) { @@ -5319,11 +5296,7 @@ function getInternalRouterError( if (status === 400) { statusText = "Bad Request"; - if (type === "route-discovery") { - errorMessage = - `Unable to match URL "${pathname}" - the \`patchRoutesOnNavigation()\` ` + - `function threw the following error:\n${message}`; - } else if (method && pathname && routeId) { + if (method && pathname && routeId) { errorMessage = `You made a ${method} request to "${pathname}" but ` + `did not provide a \`loader\` for route "${routeId}", ` + From 0e77c528735c25d421bab683c0b52072022e04f1 Mon Sep 17 00:00:00 2001 From: Ryuya Yanagi <57742720+apple-yagi@users.noreply.github.com> Date: Fri, 11 Oct 2024 02:23:36 +0900 Subject: [PATCH 19/26] Fix picking-a-router link (#12110) --- contributors.yml | 1 + packages/react-router/lib/dom/lib.tsx | 2 +- packages/react-router/lib/hooks.tsx | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/contributors.yml b/contributors.yml index 0a7c400fb8..c6e12a5890 100644 --- a/contributors.yml +++ b/contributors.yml @@ -283,3 +283,4 @@ - yuleicul - zeromask1337 - zheng-chuang +- apple-yagi diff --git a/packages/react-router/lib/dom/lib.tsx b/packages/react-router/lib/dom/lib.tsx index b6f6cbf4e8..f2ecb3ee97 100644 --- a/packages/react-router/lib/dom/lib.tsx +++ b/packages/react-router/lib/dom/lib.tsx @@ -1247,7 +1247,7 @@ enum DataRouterStateHook { function getDataRouterConsoleError( hookName: DataRouterHook | DataRouterStateHook ) { - return `${hookName} must be used within a data router. See https://reactrouter.com/routers/picking-a-router.`; + return `${hookName} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`; } function useDataRouterContext(hookName: DataRouterHook) { diff --git a/packages/react-router/lib/hooks.tsx b/packages/react-router/lib/hooks.tsx index bf78516858..430e14674e 100644 --- a/packages/react-router/lib/hooks.tsx +++ b/packages/react-router/lib/hooks.tsx @@ -949,7 +949,7 @@ enum DataRouterStateHook { function getDataRouterConsoleError( hookName: DataRouterHook | DataRouterStateHook ) { - return `${hookName} must be used within a data router. See https://reactrouter.com/routers/picking-a-router.`; + return `${hookName} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`; } function useDataRouterContext(hookName: DataRouterHook) { From 38dae6fc3ae243a213f8f9cd3bfdc2e2c2bd1384 Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Thu, 10 Oct 2024 17:24:16 +0000 Subject: [PATCH 20/26] chore: format --- contributors.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributors.yml b/contributors.yml index c6e12a5890..5ffecc6f66 100644 --- a/contributors.yml +++ b/contributors.yml @@ -22,6 +22,7 @@ - andreiduca - antonmontrezor - appden +- apple-yagi - arjunyel - arka1002 - Armanio @@ -283,4 +284,3 @@ - yuleicul - zeromask1337 - zheng-chuang -- apple-yagi From 4d5675132f289fd9be9472e13dd3daa6ed23e704 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 2 Oct 2024 16:06:55 -0400 Subject: [PATCH 21/26] Remove internal cache from patchRoutesOnNavigation (#12055) --- .../__tests__/router/lazy-discovery-test.ts | 75 ++++++++++++++++-- packages/react-router/lib/router/router.ts | 78 +++++-------------- 2 files changed, 89 insertions(+), 64 deletions(-) diff --git a/packages/react-router/__tests__/router/lazy-discovery-test.ts b/packages/react-router/__tests__/router/lazy-discovery-test.ts index 6428870d6d..13b84c3793 100644 --- a/packages/react-router/__tests__/router/lazy-discovery-test.ts +++ b/packages/react-router/__tests__/router/lazy-discovery-test.ts @@ -1,7 +1,7 @@ import type { Router } from "../../lib/router/router"; import type { AgnosticDataRouteObject } from "../../lib/router/utils"; import { createMemoryHistory } from "../../lib/router/history"; -import { createRouter } from "../../lib/router/router"; +import { IDLE_NAVIGATION, createRouter } from "../../lib/router/router"; import { ErrorResponseImpl } from "../../lib/router/utils"; import { getFetcherData } from "./utils/data-router-setup"; import { createDeferred, createFormData, tick } from "./utils/utils"; @@ -272,7 +272,7 @@ describe("Lazy Route Discovery (Fog of War)", () => { ]); }); - it("reuses promises", async () => { + it("does not reuse former calls to patchRoutes on interruptions", async () => { let aDfd = createDeferred(); let calls: string[][] = []; router = createRouter({ @@ -308,8 +308,10 @@ describe("Lazy Route Discovery (Fog of War)", () => { expect(router.state).toMatchObject({ navigation: { state: "submitting", location: { pathname: "/a/b" } }, }); - // Didn't call again for the same path - expect(calls).toEqual([["/a/b", "a"]]); + expect(calls).toEqual([ + ["/a/b", "a"], + ["/a/b", "a"], + ]); aDfd.resolve([ { @@ -324,10 +326,71 @@ describe("Lazy Route Discovery (Fog of War)", () => { navigation: { state: "idle" }, location: { pathname: "/a/b" }, }); - expect(calls).toEqual([["/a/b", "a"]]); + expect(calls).toEqual([ + ["/a/b", "a"], + ["/a/b", "a"], + ]); + }); + + it("handles interruptions when navigating to the same route", async () => { + let dfd1 = createDeferred(); + let dfd2 = createDeferred(); + let called = false; + router = createRouter({ + history: createMemoryHistory(), + routes: [ + { + path: "/", + }, + ], + async patchRoutesOnNavigation({ patch }) { + if (!called) { + called = true; + patch(null, await dfd1.promise); + } else { + patch(null, await dfd2.promise); + } + }, + }); + + router.navigate("/a"); + await tick(); + expect(router.state).toMatchObject({ + navigation: { state: "loading", location: { pathname: "/a" } }, + }); + + router.navigate("/a"); + await tick(); + expect(router.state).toMatchObject({ + navigation: { state: "loading", location: { pathname: "/a" } }, + }); + + dfd1.resolve([ + { + id: "a1", + path: "/a", + }, + ]); + await tick(); + expect(router.state).toMatchObject({ + navigation: { state: "loading", location: { pathname: "/a" } }, + }); + + dfd2.resolve([ + { + id: "a2", + path: "/a", + }, + ]); + await tick(); + expect(router.state).toMatchObject({ + location: { pathname: "/a" }, + navigation: IDLE_NAVIGATION, + matches: [{ route: { id: "a2" } }], + }); }); - it("handles interruptions", async () => { + it("handles interruptions when navigating to a new route", async () => { let aDfd = createDeferred(); let bDfd = createDeferred(); router = createRouter({ diff --git a/packages/react-router/lib/router/router.ts b/packages/react-router/lib/router/router.ts index 81b5f1eed8..f58f7b509b 100644 --- a/packages/react-router/lib/router/router.ts +++ b/packages/react-router/lib/router/router.ts @@ -3179,21 +3179,30 @@ export function createRouter(init: RouterInit): Router { pathname: string, signal: AbortSignal ): Promise { + if (!patchRoutesOnNavigationImpl) { + return { type: "success", matches }; + } + let partialMatches: AgnosticDataRouteMatch[] | null = matches; while (true) { let isNonHMR = inFlightDataRoutes == null; let routesToUse = inFlightDataRoutes || dataRoutes; + let localManifest = manifest; try { - await loadLazyRouteChildren( - patchRoutesOnNavigationImpl!, - pathname, - partialMatches, - routesToUse, - manifest, - mapRouteProperties, - pendingPatchRoutes, - signal - ); + await patchRoutesOnNavigationImpl({ + path: pathname, + matches: partialMatches, + patch: (routeId, children) => { + if (signal.aborted) return; + patchRoutesImpl( + routeId, + children, + routesToUse, + localManifest, + mapRouteProperties + ); + }, + }); } catch (e) { return { type: "error", error: e, partialMatches }; } finally { @@ -3203,7 +3212,7 @@ export function createRouter(init: RouterInit): Router { // trigger a re-run of memoized `router.routes` dependencies. // HMR will already update the identity and reflow when it lands // `inFlightDataRoutes` in `completeNavigation` - if (isNonHMR) { + if (isNonHMR && !signal.aborted) { dataRoutes = [...dataRoutes]; } } @@ -4420,53 +4429,6 @@ function shouldRevalidateLoader( return arg.defaultShouldRevalidate; } -/** - * Idempotent utility to execute patchRoutesOnNavigation() to lazily load route - * definitions and update the routes/routeManifest - */ -async function loadLazyRouteChildren( - patchRoutesOnNavigationImpl: AgnosticPatchRoutesOnNavigationFunction, - path: string, - matches: AgnosticDataRouteMatch[], - routes: AgnosticDataRouteObject[], - manifest: RouteManifest, - mapRouteProperties: MapRoutePropertiesFunction, - pendingRouteChildren: Map< - string, - ReturnType - >, - signal: AbortSignal -) { - let key = [path, ...matches.map((m) => m.route.id)].join("-"); - try { - let pending = pendingRouteChildren.get(key); - if (!pending) { - pending = patchRoutesOnNavigationImpl({ - path, - matches, - patch: (routeId, children) => { - if (!signal.aborted) { - patchRoutesImpl( - routeId, - children, - routes, - manifest, - mapRouteProperties - ); - } - }, - }); - pendingRouteChildren.set(key, pending); - } - - if (pending && isPromise(pending)) { - await pending; - } - } finally { - pendingRouteChildren.delete(key); - } -} - function patchRoutesImpl( routeId: string | null, children: AgnosticRouteObject[], From ebacd9881ea252dad63839937ec9077055a64dfb Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 7 Oct 2024 12:34:48 -0400 Subject: [PATCH 22/26] Fix partial hydration bugs when hydrating with errors (#12070) --- .../__tests__/router/route-fallback-test.ts | 120 +++++++++++++++- packages/react-router/lib/router/router.ts | 134 ++++++++++-------- 2 files changed, 190 insertions(+), 64 deletions(-) diff --git a/packages/react-router/__tests__/router/route-fallback-test.ts b/packages/react-router/__tests__/router/route-fallback-test.ts index 068a899c4c..a38d1e8200 100644 --- a/packages/react-router/__tests__/router/route-fallback-test.ts +++ b/packages/react-router/__tests__/router/route-fallback-test.ts @@ -5,6 +5,7 @@ import { IDLE_NAVIGATION, createRouter } from "../../lib/router/router"; import { urlMatch } from "./utils/custom-matchers"; import { createDeferred } from "./utils/data-router-setup"; +import { tick } from "./utils/utils"; interface CustomMatchers { urlMatch(url: string); @@ -188,7 +189,7 @@ describe("route HydrateFallback", () => { let parentSpy = jest.fn(() => parentDfd.promise); let childDfd = createDeferred(); let childSpy = jest.fn(() => childDfd.promise); - let router = createRouter({ + router = createRouter({ history: createMemoryHistory({ initialEntries: ["/child"] }), routes: [ { @@ -230,7 +231,6 @@ describe("route HydrateFallback", () => { }, }); - router.dispose(); consoleWarnSpy.mockReset(); }); @@ -242,7 +242,7 @@ describe("route HydrateFallback", () => { let parentSpy = jest.fn(() => parentDfd.promise); let childDfd = createDeferred(); let childSpy = jest.fn(() => childDfd.promise); - let router = createRouter({ + router = createRouter({ history: createMemoryHistory({ initialEntries: ["/child"] }), routes: [ { @@ -284,7 +284,6 @@ describe("route HydrateFallback", () => { }, }); - router.dispose(); consoleWarnSpy.mockReset(); }); @@ -294,7 +293,7 @@ describe("route HydrateFallback", () => { .mockImplementation(() => {}); let parentDfd = createDeferred(); let parentSpy = jest.fn(() => parentDfd.promise); - let router = createRouter({ + router = createRouter({ history: createMemoryHistory({ initialEntries: ["/child"] }), routes: [ { @@ -322,7 +321,116 @@ describe("route HydrateFallback", () => { }, }); - router.dispose(); consoleWarnSpy.mockReset(); }); + + it("does not kick off initial data loads below SSR error boundaries (child throw)", async () => { + let parentCount = 0; + let childCount = 0; + let routes = [ + { + id: "parent", + path: "/", + loader: () => `PARENT ${++parentCount}`, + hasErrorBoundary: true, + children: [ + { + path: "child", + loader: () => `CHILD ${++childCount}`, + }, + ], + }, + ]; + + // @ts-expect-error + routes[0].loader.hydrate = true; + // @ts-expect-error + routes[0].children[0].loader.hydrate = true; + + router = createRouter({ + history: createMemoryHistory({ initialEntries: ["/child"] }), + routes, + hydrationData: { + loaderData: { + parent: "PARENT 0", + }, + errors: { + // Child threw and bubbled to parent + parent: "CHILD SSR ERROR", + }, + }, + }).initialize(); + expect(router.state).toMatchObject({ + initialized: false, + navigation: IDLE_NAVIGATION, + loaderData: { + parent: "PARENT 0", + }, + errors: { + parent: "CHILD SSR ERROR", + }, + }); + await tick(); + expect(router.state).toMatchObject({ + initialized: true, + navigation: IDLE_NAVIGATION, + loaderData: { + parent: "PARENT 1", + }, + errors: { + parent: "CHILD SSR ERROR", + }, + }); + + expect(parentCount).toBe(1); + expect(childCount).toBe(0); + }); + + it("does not kick off initial data loads at SSR error boundaries (boundary throw)", async () => { + let parentCount = 0; + let childCount = 0; + let routes = [ + { + id: "parent", + path: "/", + loader: () => `PARENT ${++parentCount}`, + hasErrorBoundary: true, + children: [ + { + path: "child", + loader: () => `CHILD ${++childCount}`, + }, + ], + }, + ]; + + // @ts-expect-error + routes[0].loader.hydrate = true; + // @ts-expect-error + routes[0].children[0].loader.hydrate = true; + + router = createRouter({ + history: createMemoryHistory({ initialEntries: ["/child"] }), + routes, + hydrationData: { + loaderData: {}, + errors: { + // Parent threw + parent: "PARENT SSR ERROR", + }, + }, + }).initialize(); + + expect(router.state).toMatchObject({ + initialized: true, + navigation: IDLE_NAVIGATION, + loaderData: {}, + errors: { + parent: "PARENT SSR ERROR", + }, + }); + + expect(parentCount).toBe(0); + expect(childCount).toBe(0); + }); }); diff --git a/packages/react-router/lib/router/router.ts b/packages/react-router/lib/router/router.ts index f58f7b509b..9d445c43c7 100644 --- a/packages/react-router/lib/router/router.ts +++ b/packages/react-router/lib/router/router.ts @@ -897,33 +897,18 @@ export function createRouter(init: RouterInit): Router { // were marked for explicit hydration let loaderData = init.hydrationData ? init.hydrationData.loaderData : null; let errors = init.hydrationData ? init.hydrationData.errors : null; - let isRouteInitialized = (m: AgnosticDataRouteMatch) => { - // No loader, nothing to initialize - if (!m.route.loader) { - return true; - } - // Explicitly opting-in to running on hydration - if ( - typeof m.route.loader === "function" && - m.route.loader.hydrate === true - ) { - return false; - } - // Otherwise, initialized if hydrated with data or an error - return ( - (loaderData && loaderData[m.route.id] !== undefined) || - (errors && errors[m.route.id] !== undefined) - ); - }; - // If errors exist, don't consider routes below the boundary if (errors) { let idx = initialMatches.findIndex( (m) => errors![m.route.id] !== undefined ); - initialized = initialMatches.slice(0, idx + 1).every(isRouteInitialized); + initialized = initialMatches + .slice(0, idx + 1) + .every((m) => !shouldLoadRouteOnHydration(m.route, loaderData, errors)); } else { - initialized = initialMatches.every(isRouteInitialized); + initialized = initialMatches.every( + (m) => !shouldLoadRouteOnHydration(m.route, loaderData, errors) + ); } } @@ -1564,7 +1549,7 @@ export function createRouter(init: RouterInit): Router { // Short circuit if it's only a hash change and not a revalidation or // mutation submission. // - // Ignore on initial page loads because since the initial load will always + // Ignore on initial page loads because since the initial hydration will always // be "same hash". For example, on /page#hash and submit a
// which will default to a navigation to /page if ( @@ -2043,13 +2028,9 @@ export function createRouter(init: RouterInit): Router { fetcherResults ); - // With "partial hydration", preserve SSR errors for routes that don't re-run + // Preserve SSR errors during partial hydration if (initialHydration && state.errors) { - Object.entries(state.errors) - .filter(([id]) => !matchesToLoad.some((m) => m.route.id === id)) - .forEach(([routeId, error]) => { - errors = Object.assign(errors || {}, { [routeId]: error }); - }); + errors = { ...state.errors, ...errors }; } let updatedFetchers = markFetchRedirectsDone(); @@ -4181,20 +4162,18 @@ function normalizeNavigateOptions( return { path: createPath(parsedPath), submission }; } -// Filter out all routes below any caught error as they aren't going to +// Filter out all routes at/below any caught error as they aren't going to // render so we don't need to load them function getLoaderMatchesUntilBoundary( matches: AgnosticDataRouteMatch[], - boundaryId: string + boundaryId: string, + includeBoundary = false ) { - let boundaryMatches = matches; - if (boundaryId) { - let index = matches.findIndex((m) => m.route.id === boundaryId); - if (index >= 0) { - boundaryMatches = matches.slice(0, index); - } + let index = matches.findIndex((m) => m.route.id === boundaryId); + if (index >= 0) { + return matches.slice(0, includeBoundary ? index + 1 : index); } - return boundaryMatches; + return matches; } function getMatchesToLoad( @@ -4203,7 +4182,7 @@ function getMatchesToLoad( matches: AgnosticDataRouteMatch[], submission: Submission | undefined, location: Location, - isInitialLoad: boolean, + initialHydration: boolean, isRevalidationRequired: boolean, cancelledFetcherLoads: Set, fetchersQueuedForDeletion: Set, @@ -4222,13 +4201,26 @@ function getMatchesToLoad( let nextUrl = history.createURL(location); // Pick navigation matches that are net-new or qualify for revalidation - let boundaryId = - pendingActionResult && isErrorResult(pendingActionResult[1]) - ? pendingActionResult[0] - : undefined; - let boundaryMatches = boundaryId - ? getLoaderMatchesUntilBoundary(matches, boundaryId) - : matches; + let boundaryMatches = matches; + if (initialHydration && state.errors) { + // On initial hydration, only consider matches up to _and including_ the boundary. + // This is inclusive to handle cases where a server loader ran successfully, + // a child server loader bubbled up to this route, but this route has + // `clientLoader.hydrate` so we want to still run the `clientLoader` so that + // we have a complete version of `loaderData` + boundaryMatches = getLoaderMatchesUntilBoundary( + matches, + Object.keys(state.errors)[0], + true + ); + } else if (pendingActionResult && isErrorResult(pendingActionResult[1])) { + // If an action threw an error, we call loaders up to, but not including the + // boundary + boundaryMatches = getLoaderMatchesUntilBoundary( + matches, + pendingActionResult[0] + ); + } // Don't revalidate loaders by default after action 4xx/5xx responses // when the flag is enabled. They can still opt-into revalidation via @@ -4249,15 +4241,8 @@ function getMatchesToLoad( return false; } - if (isInitialLoad) { - if (typeof route.loader !== "function" || route.loader.hydrate) { - return true; - } - return ( - !state.loaderData.hasOwnProperty(route.id) && - // Don't re-run if the loader ran and threw an error - (!state.errors || state.errors[route.id] === undefined) - ); + if (initialHydration) { + return shouldLoadRouteOnHydration(route, state.loaderData, state.errors); } // Always call the loader on new route instances @@ -4296,11 +4281,12 @@ function getMatchesToLoad( let revalidatingFetchers: RevalidatingFetcher[] = []; fetchLoadMatches.forEach((f, key) => { // Don't revalidate: - // - on initial load (shouldn't be any fetchers then anyway) - // - if fetcher no longer matches the URL - // - if fetcher was unmounted + // - on initial hydration (shouldn't be any fetchers then anyway) + // - if fetcher won't be present in the subsequent render + // - no longer matches the URL (v7_fetcherPersist=false) + // - was unmounted but persisted due to v7_fetcherPersist=true if ( - isInitialLoad || + initialHydration || !matches.some((m) => m.route.id === f.routeId) || fetchersQueuedForDeletion.has(key) ) { @@ -4380,6 +4366,38 @@ function getMatchesToLoad( return [navigationMatches, revalidatingFetchers]; } +function shouldLoadRouteOnHydration( + route: AgnosticDataRouteObject, + loaderData: RouteData | null | undefined, + errors: RouteData | null | undefined +) { + // We dunno if we have a loader - gotta find out! + if (route.lazy) { + return true; + } + + // No loader, nothing to initialize + if (!route.loader) { + return false; + } + + let hasData = loaderData != null && loaderData[route.id] !== undefined; + let hasError = errors != null && errors[route.id] !== undefined; + + // Don't run if we error'd during SSR + if (!hasData && hasError) { + return false; + } + + // Explicitly opting-in to running on hydration + if (typeof route.loader === "function" && route.loader.hydrate === true) { + return true; + } + + // Otherwise, run if we're not yet initialized with anything + return !hasData && !hasError; +} + function isNewLoader( currentLoaderData: RouteData, currentMatch: AgnosticDataRouteMatch, From d386c0d7ee055a1cbea12448899121b72facfde3 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 10 Oct 2024 14:22:42 -0400 Subject: [PATCH 23/26] Fix adapter logic for aborting requests (Remix PR #10046) --- packages/react-router-dev/vite/node-adapter.ts | 11 ++++++++--- packages/react-router-express/server.ts | 11 ++++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/react-router-dev/vite/node-adapter.ts b/packages/react-router-dev/vite/node-adapter.ts index 1c53cf42dd..45fbcae0ce 100644 --- a/packages/react-router-dev/vite/node-adapter.ts +++ b/packages/react-router-dev/vite/node-adapter.ts @@ -47,15 +47,20 @@ export function fromNodeRequest( let url = new URL(nodeReq.originalUrl, origin); // Abort action/loaders once we can no longer write a response - let controller = new AbortController(); - nodeRes.on("close", () => controller.abort()); - + let controller: AbortController | null = new AbortController(); let init: RequestInit = { method: nodeReq.method, headers: fromNodeHeaders(nodeReq.headers), signal: controller.signal, }; + // Abort action/loaders once we can no longer write a response iff we have + // not yet sent a response (i.e., `close` without `finish`) + // `finish` -> done rendering the response + // `close` -> response can no longer be written to + nodeRes.on("finish", () => (controller = null)); + nodeRes.on("close", () => controller?.abort()); + if (nodeReq.method !== "GET" && nodeReq.method !== "HEAD") { init.body = createReadableStreamFromReadable(nodeReq); (init as { duplex: "half" }).duplex = "half"; diff --git a/packages/react-router-express/server.ts b/packages/react-router-express/server.ts index 2b339e3799..6ff7e5387e 100644 --- a/packages/react-router-express/server.ts +++ b/packages/react-router-express/server.ts @@ -98,15 +98,20 @@ export function createRemixRequest( let url = new URL(`${req.protocol}://${resolvedHost}${req.originalUrl}`); // Abort action/loaders once we can no longer write a response - let controller = new AbortController(); - res.on("close", () => controller.abort()); - + let controller: AbortController | null = new AbortController(); let init: RequestInit = { method: req.method, headers: createRemixHeaders(req.headers), signal: controller.signal, }; + // Abort action/loaders once we can no longer write a response iff we have + // not yet sent a response (i.e., `close` without `finish`) + // `finish` -> done rendering the response + // `close` -> response can no longer be written to + res.on("finish", () => (controller = null)); + res.on("close", () => controller?.abort()); + if (req.method !== "GET" && req.method !== "HEAD") { init.body = createReadableStreamFromReadable(req); (init as { duplex: "half" }).duplex = "half"; From 38134bd7ddbb8441580683eed3bc8ca63a5aaff3 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 10 Oct 2024 15:51:00 -0400 Subject: [PATCH 24/26] Fix bug with clientLoader.hydrate when hydrating with bubbled errors (Remix PR 10063) (#12114) --- integration/client-data-test.ts | 109 +++++++++++++++++++ packages/react-router/lib/dom/ssr/routes.tsx | 20 +++- 2 files changed, 125 insertions(+), 4 deletions(-) diff --git a/integration/client-data-test.ts b/integration/client-data-test.ts index 41b6556849..093135f3e3 100644 --- a/integration/client-data-test.ts +++ b/integration/client-data-test.ts @@ -780,6 +780,115 @@ test.describe("Client Data", () => { expect(html).not.toMatch("Should not see me"); console.error = _consoleError; }); + + test("bubbled server loader errors are persisted for hydrating routes", async ({ + page, + }) => { + let _consoleError = console.error; + console.error = () => {}; + appFixture = await createAppFixture( + await createFixture( + { + files: { + ...getFiles({ + parentClientLoader: false, + parentClientLoaderHydrate: false, + childClientLoader: false, + childClientLoaderHydrate: false, + }), + "app/routes/parent.tsx": js` + import { Outlet, useLoaderData, useRouteLoaderData, useRouteError } from 'react-router' + export function loader() { + return { message: 'Parent Server Loader'}; + } + export async function clientLoader({ serverLoader }) { + console.log('running parent client loader') + // Need a small delay to ensure we capture the server-rendered + // fallbacks for assertions + await new Promise(r => setTimeout(r, 100)); + let data = await serverLoader(); + return { message: data.message + " (mutated by client)" }; + } + clientLoader.hydrate = true; + export default function Component() { + let data = useLoaderData(); + return ( + <> +

{data.message}

+ + + ); + } + export function ErrorBoundary() { + let data = useRouteLoaderData("routes/parent") + let error = useRouteError(); + return ( + <> +

Parent Error

+

{data?.message}

+

{error?.message}

+ + ); + } + `, + "app/routes/parent.child.tsx": js` + import { useRouteError, useLoaderData } from 'react-router' + export function loader() { + throw new Error('Child Server Error'); + } + export function clientLoader() { + console.log('running child client loader') + return "Should not see me"; + } + clientLoader.hydrate = true; + export default function Component() { + let data = useLoaderData() + return ( + <> +

Should not see me

+

{data}

; + + ); + } + `, + }, + }, + ServerMode.Development // Avoid error sanitization + ), + ServerMode.Development // Avoid error sanitization + ); + let app = new PlaywrightFixture(appFixture, page); + let logs: string[] = []; + page.on("console", (msg) => { + let text = msg.text(); + if ( + // Chrome logs the 500 as a console error, so skip that since it's not + // what we are asserting against here + /500 \(Internal Server Error\)/.test(text) || + // Ignore any dev tools messages. This may only happen locally when dev + // tools is installed and not in CI but either way we don't care + /Download the React DevTools/.test(text) + ) { + return; + } + logs.push(text); + }); + await app.goto("/parent/child", false); + let html = await app.getHtml("main"); + expect(html).toMatch("Parent Server Loader

"); + expect(html).toMatch("Child Server Error"); + expect(html).not.toMatch("Should not see me"); + // Ensure we hydrate and remain on the boundary + await page.waitForSelector( + ":has-text('Parent Server Loader (mutated by client)')" + ); + html = await app.getHtml("main"); + expect(html).toMatch("Parent Server Loader (mutated by client)

"); + expect(html).toMatch("Child Server Error"); + expect(html).not.toMatch("Should not see me"); + expect(logs).toEqual(["running parent client loader"]); + console.error = _consoleError; + }); }); test.describe("clientLoader - lazy route module", () => { diff --git a/packages/react-router/lib/dom/ssr/routes.tsx b/packages/react-router/lib/dom/ssr/routes.tsx index 7874281735..f4e1c43cf1 100644 --- a/packages/react-router/lib/dom/ssr/routes.tsx +++ b/packages/react-router/lib/dom/ssr/routes.tsx @@ -297,8 +297,18 @@ export function createClientRoutes( : routeModule.shouldRevalidate, }); - let initialData = initialState?.loaderData?.[route.id]; - let initialError = initialState?.errors?.[route.id]; + let hasInitialData = + initialState && + initialState.loaderData && + route.id in initialState.loaderData; + let initialData = hasInitialData + ? initialState?.loaderData?.[route.id] + : undefined; + let hasInitialError = + initialState && initialState.errors && route.id in initialState.errors; + let initialError = hasInitialError + ? initialState?.errors?.[route.id] + : undefined; let isHydrationRequest = needsRevalidation == null && (routeModule.clientLoader?.hydrate === true || !route.hasLoader); @@ -327,10 +337,12 @@ export function createClientRoutes( // On the first call, resolve with the server result if (isHydrationRequest) { - if (initialError !== undefined) { + if (hasInitialData) { + return initialData; + } + if (hasInitialError) { throw initialError; } - return initialData; } // Call the server loader for client-side navigations From a41323a0b94197c62322c1b4812538b4500d8040 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Fri, 11 Oct 2024 16:59:06 +1100 Subject: [PATCH 25/26] Remove unused FS routes from playground (#12115) --- playground/compiler/app/remix-routes/hello.tsx | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 playground/compiler/app/remix-routes/hello.tsx diff --git a/playground/compiler/app/remix-routes/hello.tsx b/playground/compiler/app/remix-routes/hello.tsx deleted file mode 100644 index 1e22f0ff5f..0000000000 --- a/playground/compiler/app/remix-routes/hello.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Hello() { - return

Hello, world!

; -} From 8f44d011415b73e3e704b3094edb637eedb976b4 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Fri, 11 Oct 2024 10:23:26 -0400 Subject: [PATCH 26/26] Fix typegen for routes with a client loader but no server loader (#12117) --- .changeset/little-cooks-pull.md | 5 +++++ integration/vite-prerender-test.ts | 23 +++++++++++++++++++++++ packages/react-router-dev/vite/plugin.ts | 11 ++++++----- packages/react-router/lib/types.ts | 9 +++------ 4 files changed, 37 insertions(+), 11 deletions(-) create mode 100644 .changeset/little-cooks-pull.md diff --git a/.changeset/little-cooks-pull.md b/.changeset/little-cooks-pull.md new file mode 100644 index 0000000000..9159ca5938 --- /dev/null +++ b/.changeset/little-cooks-pull.md @@ -0,0 +1,5 @@ +--- +"react-router": patch +--- + +Fix typegen for routes with a client loader but no server loader diff --git a/integration/vite-prerender-test.ts b/integration/vite-prerender-test.ts index 4b50eb57df..7e4e7381b6 100644 --- a/integration/vite-prerender-test.ts +++ b/integration/vite-prerender-test.ts @@ -1,5 +1,6 @@ import fs from "node:fs"; import path from "node:path"; +import { PassThrough } from "node:stream"; import { test, expect } from "@playwright/test"; import { @@ -153,7 +154,9 @@ test.describe("Prerendering", () => { }); test("Prerenders known static routes when true is specified", async () => { + let buildStdio = new PassThrough(); fixture = await createFixture({ + buildStdio, prerender: true, files: { ...files, @@ -192,6 +195,26 @@ test.describe("Prerendering", () => { `, }, }); + + let buildOutput: string; + let chunks: Buffer[] = []; + buildOutput = await new Promise((resolve, reject) => { + buildStdio.on("data", (chunk) => chunks.push(Buffer.from(chunk))); + buildStdio.on("error", (err) => reject(err)); + buildStdio.on("end", () => + resolve(Buffer.concat(chunks).toString("utf8")) + ); + }); + + expect(buildOutput).toContain( + [ + "⚠️ Paths with dynamic/splat params cannot be prerendered when using `prerender: true`.", + "You may want to use the `prerender()` API to prerender the following paths:", + " - :slug", + " - *", + ].join("\n") + ); + appFixture = await createAppFixture(fixture); let clientDir = path.join(fixture.projectDir, "build", "client"); diff --git a/packages/react-router-dev/vite/plugin.ts b/packages/react-router-dev/vite/plugin.ts index 07593480a3..2d87c13bfc 100644 --- a/packages/react-router-dev/vite/plugin.ts +++ b/packages/react-router-dev/vite/plugin.ts @@ -1897,12 +1897,13 @@ function determineStaticPrerenderRoutes( } recurse(routes); - if (isBooleanUsage && paramRoutes) { + if (isBooleanUsage && paramRoutes.length > 0) { viteConfig.logger.warn( - "The following paths were not prerendered because Dynamic Param and Splat " + - "routes cannot be prerendered when using `prerender:true`. You may want to " + - "consider using the `prerender()` API if you wish to prerender slug and " + - "splat routes." + [ + "⚠️ Paths with dynamic/splat params cannot be prerendered when using `prerender: true`.", + "You may want to use the `prerender()` API to prerender the following paths:", + ...paramRoutes.map((p) => " - " + p), + ].join("\n") ); } diff --git a/packages/react-router/lib/types.ts b/packages/react-router/lib/types.ts index 0890c10462..9e980d2957 100644 --- a/packages/react-router/lib/types.ts +++ b/packages/react-router/lib/types.ts @@ -66,15 +66,12 @@ type _CreateLoaderData< ClientLoaderHydrate extends boolean, HasHydrateFallback > = - [HasHydrateFallback, ClientLoaderHydrate] extends [true, true] ? + [HasHydrateFallback, ClientLoaderHydrate] extends [true, true] ? IsDefined extends true ? ClientLoaderData : undefined : [IsDefined, IsDefined] extends [true, true] ? ServerLoaderData | ClientLoaderData : - IsDefined extends true ? - ClientLoaderHydrate extends true ? ClientLoaderData : - ClientLoaderData | undefined - : + IsDefined extends true ? ClientLoaderData : IsDefined extends true ? ServerLoaderData : undefined @@ -221,7 +218,7 @@ type __tests = [ CreateLoaderData<{ clientLoader: () => { a: string; b: Date; c: () => boolean }; }>, - undefined | { a: string; b: Date; c: () => boolean } + { a: string; b: Date; c: () => boolean } > >, Expect<