From 4ec4999206e18244e44d7f9fab65a0a2f56c969f Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Fri, 26 Sep 2025 19:43:14 +0000 Subject: [PATCH 01/16] chore: format --- integration/CHANGELOG.md | 1 - packages/react-router-architect/CHANGELOG.md | 3 --- packages/react-router-cloudflare/CHANGELOG.md | 3 --- packages/react-router-dev/CHANGELOG.md | 8 -------- packages/react-router-express/CHANGELOG.md | 1 - packages/react-router-node/CHANGELOG.md | 5 ----- packages/react-router-serve/CHANGELOG.md | 2 -- packages/react-router/CHANGELOG.md | 10 +--------- 8 files changed, 1 insertion(+), 32 deletions(-) diff --git a/integration/CHANGELOG.md b/integration/CHANGELOG.md index 6fccf850d7..2cf67d87b7 100644 --- a/integration/CHANGELOG.md +++ b/integration/CHANGELOG.md @@ -5,7 +5,6 @@ ### Minor Changes - Unstable Vite support for Node-based Remix apps ([#7590](https://github.com/remix-run/remix/pull/7590)) - - `remix build` 👉 `vite build && vite build --ssr` - `remix dev` 👉 `vite dev` diff --git a/packages/react-router-architect/CHANGELOG.md b/packages/react-router-architect/CHANGELOG.md index 4f04a6878b..5ec74a1162 100644 --- a/packages/react-router-architect/CHANGELOG.md +++ b/packages/react-router-architect/CHANGELOG.md @@ -31,7 +31,6 @@ - Stabilize middleware and context APIs. ([#14215](https://github.com/remix-run/react-router/pull/14215)) We have removed the `unstable_` prefix from the following APIs and they are now considered stable and ready for production use: - - [`RouterContextProvider`](https://reactrouter.com/api/utils/RouterContextProvider) - [`createContext`](https://reactrouter.com/api/utils/createContext) - `createBrowserRouter` [`getContext`](https://reactrouter.com/api/data-routers/createBrowserRouter#optsgetcontext) option @@ -255,7 +254,6 @@ ### Major Changes - For Remix consumers migrating to React Router, the `crypto` global from the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) is now required when using cookie and session APIs. This means that the following APIs are provided from `react-router` rather than platform-specific packages: ([#11837](https://github.com/remix-run/react-router/pull/11837)) - - `createCookie` - `createCookieSessionStorage` - `createMemorySessionStorage` @@ -264,7 +262,6 @@ For consumers running older versions of Node, the `installGlobals` function from `@remix-run/node` has been updated to define `globalThis.crypto`, using [Node's `require('node:crypto').webcrypto` implementation.](https://nodejs.org/api/webcrypto.html) Since platform-specific packages no longer need to implement this API, the following low-level APIs have been removed: - - `createCookieFactory` - `createSessionStorageFactory` - `createCookieSessionStorageFactory` diff --git a/packages/react-router-cloudflare/CHANGELOG.md b/packages/react-router-cloudflare/CHANGELOG.md index f2dac5adcc..d74b9aa8d9 100644 --- a/packages/react-router-cloudflare/CHANGELOG.md +++ b/packages/react-router-cloudflare/CHANGELOG.md @@ -28,7 +28,6 @@ - Stabilize middleware and context APIs. ([#14215](https://github.com/remix-run/react-router/pull/14215)) We have removed the `unstable_` prefix from the following APIs and they are now considered stable and ready for production use: - - [`RouterContextProvider`](https://reactrouter.com/api/utils/RouterContextProvider) - [`createContext`](https://reactrouter.com/api/utils/createContext) - `createBrowserRouter` [`getContext`](https://reactrouter.com/api/data-routers/createBrowserRouter#optsgetcontext) option @@ -227,7 +226,6 @@ - For Remix consumers migrating to React Router, all exports from `@remix-run/cloudflare-pages` are now provided for React Router consumers in the `@react-router/cloudflare` package. There is no longer a separate package for Cloudflare Pages. ([#11801](https://github.com/remix-run/react-router/pull/11801)) - For Remix consumers migrating to React Router, the `crypto` global from the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) is now required when using cookie and session APIs. This means that the following APIs are provided from `react-router` rather than platform-specific packages: ([#11837](https://github.com/remix-run/react-router/pull/11837)) - - `createCookie` - `createCookieSessionStorage` - `createMemorySessionStorage` @@ -236,7 +234,6 @@ For consumers running older versions of Node, the `installGlobals` function from `@remix-run/node` has been updated to define `globalThis.crypto`, using [Node's `require('node:crypto').webcrypto` implementation.](https://nodejs.org/api/webcrypto.html) Since platform-specific packages no longer need to implement this API, the following low-level APIs have been removed: - - `createCookieFactory` - `createSessionStorageFactory` - `createCookieSessionStorageFactory` diff --git a/packages/react-router-dev/CHANGELOG.md b/packages/react-router-dev/CHANGELOG.md index a3adc9c1b5..2d0aa8e6df 100644 --- a/packages/react-router-dev/CHANGELOG.md +++ b/packages/react-router-dev/CHANGELOG.md @@ -45,7 +45,6 @@ - Stabilize middleware and context APIs. ([#14215](https://github.com/remix-run/react-router/pull/14215)) We have removed the `unstable_` prefix from the following APIs and they are now considered stable and ready for production use: - - [`RouterContextProvider`](https://reactrouter.com/api/utils/RouterContextProvider) - [`createContext`](https://reactrouter.com/api/utils/createContext) - `createBrowserRouter` [`getContext`](https://reactrouter.com/api/data-routers/createBrowserRouter#optsgetcontext) option @@ -788,7 +787,6 @@ ``` This initial implementation targets type inference for: - - `Params` : Path parameters from your routing config in `routes.ts` including file-based routing - `LoaderData` : Loader data from `loader` and/or `clientLoader` within your route module - `ActionData` : Action data from `action` and/or `clientAction` within your route module @@ -803,7 +801,6 @@ ``` Check out our docs for more: - - [_Explanations > Type Safety_](https://reactrouter.com/dev/guides/explanation/type-safety) - [_How-To > Setting up type safety_](https://reactrouter.com/dev/guides/how-to/setting-up-type-safety) @@ -1003,7 +1000,6 @@ - Vite: Provide `Unstable_ServerBundlesFunction` and `Unstable_VitePluginConfig` types ([#8654](https://github.com/remix-run/remix/pull/8654)) - Vite: add `--sourcemapClient` and `--sourcemapServer` flags to `remix vite:build` ([#8613](https://github.com/remix-run/remix/pull/8613)) - - `--sourcemapClient` - `--sourcemapClient=inline` @@ -1340,7 +1336,6 @@ - Add support for `clientLoader`/`clientAction`/`HydrateFallback` route exports ([RFC](https://github.com/remix-run/remix/discussions/7634)) ([#8173](https://github.com/remix-run/remix/pull/8173)) Remix now supports loaders/actions that run on the client (in addition to, or instead of the loader/action that runs on the server). While we still recommend server loaders/actions for the majority of your data needs in a Remix app - these provide some levers you can pull for more advanced use-cases such as: - - Leveraging a data source local to the browser (i.e., `localStorage`) - Managing a client-side cache of server data (like `IndexedDB`) - Bypassing the Remix server in a BFF setup and hitting your API directly from the browser @@ -1744,7 +1739,6 @@ - Output esbuild metafiles for bundle analysis ([#6772](https://github.com/remix-run/remix/pull/6772)) Written to server build directory (`build/` by default): - - `metafile.css.json` - `metafile.js.json` (browser JS) - `metafile.server.json` (server JS) @@ -1842,7 +1836,6 @@ - built-in tls support ([#6483](https://github.com/remix-run/remix/pull/6483)) New options: - - `--tls-key` / `tlsKey`: TLS key - `--tls-cert` / `tlsCert`: TLS Certificate @@ -2113,7 +2106,6 @@ ``` The dev server will: - - force `NODE_ENV=development` and warn you if it was previously set to something else - rebuild your app whenever your Remix app code changes - restart your app server whenever rebuilds succeed diff --git a/packages/react-router-express/CHANGELOG.md b/packages/react-router-express/CHANGELOG.md index 7504642bc9..d3e49f9170 100644 --- a/packages/react-router-express/CHANGELOG.md +++ b/packages/react-router-express/CHANGELOG.md @@ -31,7 +31,6 @@ - Stabilize middleware and context APIs. ([#14215](https://github.com/remix-run/react-router/pull/14215)) We have removed the `unstable_` prefix from the following APIs and they are now considered stable and ready for production use: - - [`RouterContextProvider`](https://reactrouter.com/api/utils/RouterContextProvider) - [`createContext`](https://reactrouter.com/api/utils/createContext) - `createBrowserRouter` [`getContext`](https://reactrouter.com/api/data-routers/createBrowserRouter#optsgetcontext) option diff --git a/packages/react-router-node/CHANGELOG.md b/packages/react-router-node/CHANGELOG.md index 226fdbe953..8955ab93d2 100644 --- a/packages/react-router-node/CHANGELOG.md +++ b/packages/react-router-node/CHANGELOG.md @@ -28,7 +28,6 @@ - Stabilize middleware and context APIs. ([#14215](https://github.com/remix-run/react-router/pull/14215)) We have removed the `unstable_` prefix from the following APIs and they are now considered stable and ready for production use: - - [`RouterContextProvider`](https://reactrouter.com/api/utils/RouterContextProvider) - [`createContext`](https://reactrouter.com/api/utils/createContext) - `createBrowserRouter` [`getContext`](https://reactrouter.com/api/data-routers/createBrowserRouter#optsgetcontext) option @@ -228,7 +227,6 @@ - Remove single fetch future flag. ([#11522](https://github.com/remix-run/react-router/pull/11522)) - For Remix consumers migrating to React Router, the `crypto` global from the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) is now required when using cookie and session APIs. This means that the following APIs are provided from `react-router` rather than platform-specific packages: ([#11837](https://github.com/remix-run/react-router/pull/11837)) - - `createCookie` - `createCookieSessionStorage` - `createMemorySessionStorage` @@ -237,7 +235,6 @@ For consumers running older versions of Node, the `installGlobals` function from `@remix-run/node` has been updated to define `globalThis.crypto`, using [Node's `require('node:crypto').webcrypto` implementation.](https://nodejs.org/api/webcrypto.html) Since platform-specific packages no longer need to implement this API, the following low-level APIs have been removed: - - `createCookieFactory` - `createSessionStorageFactory` - `createCookieSessionStorageFactory` @@ -645,12 +642,10 @@ - Introduces the `defer()` API from `@remix-run/router` with support for server-rendering and HTTP streaming. This utility allows you to defer values returned from `loader` functions by returning promises instead of resolved values. This has been refered to as _"sending a promise over the wire"_. ([#4920](https://github.com/remix-run/remix/pull/4920)) Informational Resources: - - - Documentation Resources (better docs specific to Remix are in the works): - - - - diff --git a/packages/react-router-serve/CHANGELOG.md b/packages/react-router-serve/CHANGELOG.md index ad9e156f40..cb2c8edc65 100644 --- a/packages/react-router-serve/CHANGELOG.md +++ b/packages/react-router-serve/CHANGELOG.md @@ -645,12 +645,10 @@ - Introduces the `defer()` API from `@remix-run/router` with support for server-rendering and HTTP streaming. This utility allows you to defer values returned from `loader` functions by returning promises instead of resolved values. This has been refered to as _"sending a promise over the wire"_. ([#4920](https://github.com/remix-run/remix/pull/4920)) Informational Resources: - - - Documentation Resources (better docs specific to Remix are in the works): - - - - diff --git a/packages/react-router/CHANGELOG.md b/packages/react-router/CHANGELOG.md index f7d25f8b71..2f0d19ed90 100644 --- a/packages/react-router/CHANGELOG.md +++ b/packages/react-router/CHANGELOG.md @@ -48,7 +48,6 @@ - Stabilize middleware and context APIs. ([#14215](https://github.com/remix-run/react-router/pull/14215)) We have removed the `unstable_` prefix from the following APIs and they are now considered stable and ready for production use: - - [`RouterContextProvider`](https://reactrouter.com/api/utils/RouterContextProvider) - [`createContext`](https://reactrouter.com/api/utils/createContext) - `createBrowserRouter` [`getContext`](https://reactrouter.com/api/data-routers/createBrowserRouter#optsgetcontext) option @@ -75,7 +74,7 @@ - \[UNSTABLE] Add ``/`` prop for client side error reporting ([#14162](https://github.com/remix-run/react-router/pull/14162)) -- server action revalidation opt out via $SKIP\_REVALIDATION field ([#14154](https://github.com/remix-run/react-router/pull/14154)) +- server action revalidation opt out via $SKIP_REVALIDATION field ([#14154](https://github.com/remix-run/react-router/pull/14154)) - Properly escape interpolated param values in `generatePath()` ([#13530](https://github.com/remix-run/react-router/pull/13530)) @@ -124,7 +123,6 @@ - Remove dependency on `@types/node` in TypeScript declaration files ([#14059](https://github.com/remix-run/react-router/pull/14059)) - Fix types for `UIMatch` to reflect that the `loaderData`/`data` properties may be `undefined` ([#12206](https://github.com/remix-run/react-router/pull/12206)) - - When an `ErrorBoundary` is being rendered, not all active matches will have loader data available, since it may have been their `loader` that threw to trigger the boundary - The `UIMatch.data` type was not correctly handing this and would always reflect the presence of data, leading to the unexpected runtime errors when an `ErrorBoundary` was rendered - ⚠️ This may cause some type errors to show up in your code for unguarded `match.data` accesses - you should properly guard for `undefined` values in those scenarios. @@ -158,7 +156,6 @@ - \[UNSTABLE] When middleware is enabled, make the `context` parameter read-only (via `Readonly`) so that TypeScript will not allow you to write arbitrary fields to it in loaders, actions, or middleware. ([#14097](https://github.com/remix-run/react-router/pull/14097)) - \[UNSTABLE] Rename and alter the signature/functionality of the `unstable_respond` API in `staticHandler.query`/`staticHandler.queryRoute` ([#14103](https://github.com/remix-run/react-router/pull/14103)) - - The API has been renamed to `unstable_generateMiddlewareResponse` for clarity - The main functional change is that instead of running the loaders/actions before calling `unstable_respond` and handing you the result, we now pass a `query`/`queryRoute` function as a parameter and you execute the loaders/actions inside your callback, giving you full access to pre-processing and error handling - The `query` version of the API now has a signature of `(query: (r: Request) => Promise) => Promise` @@ -804,7 +801,6 @@ ``` Similar to server-side requests, a fresh `context` will be created per navigation (or `fetcher` call). If you have initial data you'd like to populate in the context for every request, you can provide an `unstable_getContext` function at the root of your app: - - Library mode - `createBrowserRouter(routes, { unstable_getContext })` - Framework mode - `` @@ -992,7 +988,6 @@ _No changes_ - Remove `future.v7_normalizeFormMethod` future flag ([#11697](https://github.com/remix-run/react-router/pull/11697)) - For Remix consumers migrating to React Router, the `crypto` global from the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) is now required when using cookie and session APIs. This means that the following APIs are provided from `react-router` rather than platform-specific packages: ([#11837](https://github.com/remix-run/react-router/pull/11837)) - - `createCookie` - `createCookieSessionStorage` - `createMemorySessionStorage` @@ -1001,7 +996,6 @@ _No changes_ For consumers running older versions of Node, the `installGlobals` function from `@remix-run/node` has been updated to define `globalThis.crypto`, using [Node's `require('node:crypto').webcrypto` implementation.](https://nodejs.org/api/webcrypto.html) Since platform-specific packages no longer need to implement this API, the following low-level APIs have been removed: - - `createCookieFactory` - `createSessionStorageFactory` - `createCookieSessionStorageFactory` @@ -1157,7 +1151,6 @@ _No changes_ ``` This initial implementation targets type inference for: - - `Params` : Path parameters from your routing config in `routes.ts` including file-based routing - `LoaderData` : Loader data from `loader` and/or `clientLoader` within your route module - `ActionData` : Action data from `action` and/or `clientAction` within your route module @@ -1172,7 +1165,6 @@ _No changes_ ``` Check out our docs for more: - - [_Explanations > Type Safety_](https://reactrouter.com/dev/guides/explanation/type-safety) - [_How-To > Setting up type safety_](https://reactrouter.com/dev/guides/how-to/setting-up-type-safety) From 81dffec5acd67b79e4f4195919f5e4620ad65daf Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Fri, 26 Sep 2025 15:43:16 -0400 Subject: [PATCH 02/16] Update finish release script --- scripts/delete-pre-tags.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/delete-pre-tags.sh b/scripts/delete-pre-tags.sh index 2959d37279..f521897d90 100755 --- a/scripts/delete-pre-tags.sh +++ b/scripts/delete-pre-tags.sh @@ -24,5 +24,7 @@ echo "Found ${NUM_TAGS} tags to delete. To delete, run the following commands:" echo "" echo "git push origin --delete ${TAGS_LINE}" echo "git fetch --prune --prune-tags" +echo "" +echo "" set +e From 742267c1c0a52f0e01d64f10f931c3202abc2b37 Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Mon, 29 Sep 2025 17:28:34 -0400 Subject: [PATCH 03/16] better playwright fixtures: cwd, edit, $ (#14402) --- integration/helpers/fixtures.ts | 138 ++++ integration/package.json | 2 +- integration/typegen-test.ts | 1320 ++++++++++++++----------------- pnpm-lock.yaml | 131 ++- 4 files changed, 877 insertions(+), 714 deletions(-) create mode 100644 integration/helpers/fixtures.ts diff --git a/integration/helpers/fixtures.ts b/integration/helpers/fixtures.ts new file mode 100644 index 0000000000..ab644568ae --- /dev/null +++ b/integration/helpers/fixtures.ts @@ -0,0 +1,138 @@ +import { ChildProcess } from "node:child_process"; +import * as fs from "node:fs/promises"; +import { fileURLToPath } from "node:url"; + +import { test as base } from "@playwright/test"; +import { + execa, + ExecaError, + type Options, + parseCommandString, + type ResultPromise, +} from "execa"; +import * as Path from "pathe"; + +import type { TemplateName } from "./vite.js"; + +declare module "@playwright/test" { + interface Page { + errors: Error[]; + } +} + +const __filename = fileURLToPath(import.meta.url); +const ROOT = Path.join(__filename, "../../.."); +const TMP = Path.join(ROOT, ".tmp/integration"); +const templatePath = (templateName: string) => + Path.resolve(ROOT, "integration/helpers", templateName); + +type Edits = Record string)>; + +async function applyEdits(cwd: string, edits: Edits) { + const promises = Object.entries(edits).map(async ([file, transform]) => { + const filepath = Path.join(cwd, file); + await fs.writeFile( + filepath, + typeof transform === "function" + ? transform(await fs.readFile(filepath, "utf8")) + : transform, + "utf8", + ); + return; + }); + await Promise.all(promises); +} + +export const test = base.extend<{ + template: TemplateName; + files: Edits; + cwd: string; + edit: (edits: Edits) => Promise; + $: ( + command: string, + options?: Pick, + ) => ResultPromise<{ reject: false }> & { + buffer: { stdout: string; stderr: string }; + }; +}>({ + template: ["vite-6-template", { option: true }], + files: [{}, { option: true }], + page: async ({ page }, use) => { + page.errors = []; + page.on("pageerror", (error: Error) => page.errors.push(error)); + await use(page); + }, + + cwd: async ({ template, files }, use, testInfo) => { + await fs.mkdir(TMP, { recursive: true }); + const cwd = await fs.mkdtemp(Path.join(TMP, template + "-")); + testInfo.attach("cwd", { body: cwd }); + + await fs.cp(templatePath(template), cwd, { + errorOnExist: true, + recursive: true, + }); + + await applyEdits(cwd, files); + + await use(cwd); + }, + + edit: async ({ cwd }, use) => { + await use(async (edits) => applyEdits(cwd, edits)); + }, + + $: async ({ cwd }, use) => { + const spawn = execa({ + cwd, + env: { + NO_COLOR: "1", + FORCE_COLOR: "0", + }, + reject: false, + }); + + let testHasEnded = false; + const processes: Array = []; + const unexpectedErrors: Array = []; + + await use((command, options = {}) => { + const [file, ...args] = parseCommandString(command); + + const p = spawn(file, args, options); + if (p instanceof ChildProcess) { + processes.push(p); + } + + p.then((result) => { + if (!(result instanceof Error)) return result; + + // Once the test has ended, this process will be killed as part of its teardown resulting in an ExecaError. + // We only care about surfacing errors that occurred during test execution, not during teardown. + const expectedError = testHasEnded && result instanceof ExecaError; + if (expectedError) return result; + unexpectedErrors.push(result); + }); + + const buffer = { stdout: "", stderr: "" }; + p.stdout?.on("data", (data) => (buffer.stdout += data.toString())); + p.stderr?.on("data", (data) => (buffer.stderr += data.toString())); + return Object.assign(p, { buffer }); + }); + + testHasEnded = true; + processes.forEach((p) => p.kill()); + + // Throw any unexpected errors that occurred during test execution + if (unexpectedErrors.length > 0) { + const errorMessage = + unexpectedErrors.length === 1 + ? `Unexpected process error: ${unexpectedErrors[0].message}` + : `${unexpectedErrors.length} unexpected process errors:\n${unexpectedErrors.map((e, i) => `${i + 1}. ${e.message}`).join("\n")}`; + + const error = new Error(errorMessage); + error.stack = unexpectedErrors[0].stack; + throw error; + } + }, +}); diff --git a/integration/package.json b/integration/package.json index 824fed2598..fb81ff14d5 100644 --- a/integration/package.json +++ b/integration/package.json @@ -25,7 +25,7 @@ "cheerio": "^1.0.0-rc.12", "cross-spawn": "^7.0.3", "dedent": "^0.7.0", - "execa": "^5.1.1", + "execa": "^9.6.0", "express": "^4.19.2", "get-port": "^5.1.1", "glob": "8.0.3", diff --git a/integration/typegen-test.ts b/integration/typegen-test.ts index 9bcd7ac928..9ffcc5e183 100644 --- a/integration/typegen-test.ts +++ b/integration/typegen-test.ts @@ -1,31 +1,16 @@ -import { spawnSync } from "node:child_process"; -import { mkdirSync, renameSync } from "node:fs"; -import { readFile, writeFile } from "node:fs/promises"; -import * as path from "node:path"; +import fs from "node:fs/promises"; -import { expect, test } from "@playwright/test"; -import dedent from "dedent"; +import tsx from "dedent"; +import * as Path from "pathe"; -import { createProject } from "./helpers/vite"; +import { test } from "./helpers/fixtures"; -const tsx = dedent; - -const nodeBin = process.argv[0]; -const reactRouterBin = "node_modules/@react-router/dev/dist/cli/index.js"; -const tscBin = "node_modules/typescript/bin/tsc"; - -function typecheck(cwd: string) { - const typegen = spawnSync(nodeBin, [reactRouterBin, "typegen"], { cwd }); - expect(typegen.stdout.toString()).toBe(""); - expect(typegen.stderr.toString()).toBe(""); - expect(typegen.status).toBe(0); - - return spawnSync(nodeBin, [tscBin], { cwd }); -} - -const viteConfig = ({ rsc }: { rsc: boolean } = { rsc: false }) => { +const viteConfig = ({ rsc }: { rsc: boolean }) => { + const reactRouterImportSpecifier = rsc + ? "unstable_reactRouterRSC as reactRouter" + : "reactRouter"; return tsx` - import { ${rsc ? "unstable_reactRouterRSC as reactRouter" : "reactRouter"} } from "@react-router/dev/vite"; + import { ${reactRouterImportSpecifier} } from "@react-router/dev/vite"; export default { plugins: [reactRouter()], @@ -33,19 +18,22 @@ const viteConfig = ({ rsc }: { rsc: boolean } = { rsc: false }) => { `; }; -const expectType = tsx` - export type Expect = T - - export type Equal = - (() => T extends X ? 1 : 2) extends - (() => T extends Y ? 1 : 2) ? true : false -`; +test.use({ + files: { + "vite.config.ts": viteConfig({ rsc: false }), + "app/expect-type.ts": tsx` + export type Expect = T + + export type Equal = + (() => T extends X ? 1 : 2) extends + (() => T extends Y ? 1 : 2) ? true : false + `, + }, +}); test.describe("typegen", () => { - test("basic", async () => { - const cwd = await createProject({ - "vite.config.ts": viteConfig(), - "app/expect-type.ts": expectType, + test("basic", async ({ edit, $ }) => { + await edit({ "app/routes.ts": tsx` import { type RouteConfig, route } from "@react-router/dev/routes"; @@ -68,237 +56,208 @@ test.describe("typegen", () => { } `, }); - - const proc = typecheck(cwd); - expect(proc.stdout.toString()).toBe(""); - expect(proc.stderr.toString()).toBe(""); - expect(proc.status).toBe(0); + await $("pnpm typecheck"); }); - test.describe("params", () => { - test("repeated", async () => { - const cwd = await createProject({ - "vite.config.ts": viteConfig(), - "app/expect-type.ts": expectType, - "app/routes.ts": tsx` - import { type RouteConfig, route } from "@react-router/dev/routes"; - - export default [ - route("only-required/:id/:id", "routes/only-required.tsx"), - route("only-optional/:id?/:id?", "routes/only-optional.tsx"), - route("optional-then-required/:id?/:id", "routes/optional-then-required.tsx"), - route("required-then-optional/:id/:id?", "routes/required-then-optional.tsx"), - ] satisfies RouteConfig; - `, - "app/routes/only-required.tsx": tsx` - import type { Expect, Equal } from "../expect-type" - import type { Route } from "./+types/only-required" - export function loader({ params }: Route.LoaderArgs) { - type Test = Expect> - return null - } - `, - "app/routes/only-optional.tsx": tsx` - import type { Expect, Equal } from "../expect-type" - import type { Route } from "./+types/only-optional" - export function loader({ params }: Route.LoaderArgs) { - type Test = Expect> - return null - } - `, - "app/routes/optional-then-required.tsx": tsx` - import type { Expect, Equal } from "../expect-type" - import type { Route } from "./+types/optional-then-required" - export function loader({ params }: Route.LoaderArgs) { - type Test = Expect> - return null - } - `, - "app/routes/required-then-optional.tsx": tsx` - import type { Expect, Equal } from "../expect-type" - import type { Route } from "./+types/required-then-optional" - - export function loader({ params }: Route.LoaderArgs) { - type Test = Expect> - return null - } - `, - }); - const proc = typecheck(cwd); - expect(proc.stdout.toString()).toBe(""); - expect(proc.stderr.toString()).toBe(""); - expect(proc.status).toBe(0); - }); - - test("splat", async () => { - const cwd = await createProject({ - "vite.config.ts": viteConfig(), - "app/expect-type.ts": expectType, - "app/routes.ts": tsx` - import { type RouteConfig, route } from "@react-router/dev/routes"; + test("repeated params", async ({ edit, $ }) => { + await edit({ + "app/routes.ts": tsx` + import { type RouteConfig, route } from "@react-router/dev/routes"; - export default [ - route("splat/*", "routes/splat.tsx") - ] satisfies RouteConfig; - `, - "app/routes/splat.tsx": tsx` - import type { Expect, Equal } from "../expect-type" - import type { Route } from "./+types/splat" + export default [ + route("only-required/:id/:id", "routes/only-required.tsx"), + route("only-optional/:id?/:id?", "routes/only-optional.tsx"), + route("optional-then-required/:id?/:id", "routes/optional-then-required.tsx"), + route("required-then-optional/:id/:id?", "routes/required-then-optional.tsx"), + ] satisfies RouteConfig; + `, + "app/routes/only-required.tsx": tsx` + import type { Expect, Equal } from "../expect-type" + import type { Route } from "./+types/only-required" + export function loader({ params }: Route.LoaderArgs) { + type Test = Expect> + return null + } + `, + "app/routes/only-optional.tsx": tsx` + import type { Expect, Equal } from "../expect-type" + import type { Route } from "./+types/only-optional" + export function loader({ params }: Route.LoaderArgs) { + type Test = Expect> + return null + } + `, + "app/routes/optional-then-required.tsx": tsx` + import type { Expect, Equal } from "../expect-type" + import type { Route } from "./+types/optional-then-required" + export function loader({ params }: Route.LoaderArgs) { + type Test = Expect> + return null + } + `, + "app/routes/required-then-optional.tsx": tsx` + import type { Expect, Equal } from "../expect-type" + import type { Route } from "./+types/required-then-optional" - export function loader({ params }: Route.LoaderArgs) { - type Test = Expect> - return null - } - `, - }); - const proc = typecheck(cwd); - expect(proc.stdout.toString()).toBe(""); - expect(proc.stderr.toString()).toBe(""); - expect(proc.status).toBe(0); + export function loader({ params }: Route.LoaderArgs) { + type Test = Expect> + return null + } + `, }); + await $("pnpm typecheck"); + }); - test("with extension", async () => { - const cwd = await createProject({ - "vite.config.ts": viteConfig(), - "app/expect-type.ts": expectType, - "app/routes.ts": tsx` - import { type RouteConfig, route } from "@react-router/dev/routes"; + test("params with extension", async ({ edit, $ }) => { + await edit({ + "app/routes.ts": tsx` + import { type RouteConfig, route } from "@react-router/dev/routes"; - export default [ - route(":lang.xml", "routes/param-with-ext.tsx"), - route(":user?.pdf", "routes/optional-param-with-ext.tsx"), - ] satisfies RouteConfig; - `, - "app/routes/param-with-ext.tsx": tsx` - import type { Expect, Equal } from "../expect-type" - import type { Route } from "./+types/param-with-ext" + export default [ + route(":lang.xml", "routes/param-with-ext.tsx"), + route(":user?.pdf", "routes/optional-param-with-ext.tsx"), + ] satisfies RouteConfig; + `, + "app/routes/param-with-ext.tsx": tsx` + import type { Expect, Equal } from "../expect-type" + import type { Route } from "./+types/param-with-ext" - export function loader({ params }: Route.LoaderArgs) { - type Test = Expect> - return null - } - `, - "app/routes/optional-param-with-ext.tsx": tsx` - import type { Expect, Equal } from "../expect-type" - import type { Route } from "./+types/optional-param-with-ext" + export function loader({ params }: Route.LoaderArgs) { + type Test = Expect> + return null + } + `, + "app/routes/optional-param-with-ext.tsx": tsx` + import type { Expect, Equal } from "../expect-type" + import type { Route } from "./+types/optional-param-with-ext" - export function loader({ params }: Route.LoaderArgs) { - type Test = Expect> - return null - } - `, - }); - const proc = typecheck(cwd); - expect(proc.stdout.toString()).toBe(""); - expect(proc.stderr.toString()).toBe(""); - expect(proc.status).toBe(0); + export function loader({ params }: Route.LoaderArgs) { + type Test = Expect> + return null + } + `, }); + await $("pnpm typecheck"); + }); - test("normalized params", async () => { - const cwd = await createProject({ - "vite.config.ts": viteConfig(), - "app/expect-type.ts": expectType, - "app/routes.ts": tsx` - import { type RouteConfig, route, layout } from "@react-router/dev/routes"; + test("normalized param", async ({ edit, $ }) => { + await edit({ + "app/routes.ts": tsx` + import { type RouteConfig, route, layout } from "@react-router/dev/routes"; - export default [ - route("parent/:p", "routes/parent.tsx", [ - route("route/:r", "routes/route.tsx", [ - route("child1/:c1a/:c1b", "routes/child1.tsx"), - route("child2/:c2a/:c2b", "routes/child2.tsx") - ]), + export default [ + route("parent/:p", "routes/parent.tsx", [ + route("route/:r", "routes/route.tsx", [ + route("child1/:c1a/:c1b", "routes/child1.tsx"), + route("child2/:c2a/:c2b", "routes/child2.tsx") ]), - layout("routes/layout.tsx", [ - route("in-layout1/:id", "routes/in-layout1.tsx"), - route("in-layout2/:id/:other", "routes/in-layout2.tsx") - ]) - ] satisfies RouteConfig; - `, - "app/routes/parent.tsx": tsx` - import type { Expect, Equal } from "../expect-type" - import type { Route } from "./+types/parent" + ]), + layout("routes/layout.tsx", [ + route("in-layout1/:id", "routes/in-layout1.tsx"), + route("in-layout2/:id/:other", "routes/in-layout2.tsx") + ]) + ] satisfies RouteConfig; + `, + "app/routes/parent.tsx": tsx` + import type { Expect, Equal } from "../expect-type" + import type { Route } from "./+types/parent" - export function loader({ params }: Route.LoaderArgs) { - type Test = Expect> - return null - } - `, - "app/routes/route.tsx": tsx` - import type { Expect, Equal } from "../expect-type" - import type { Route } from "./+types/route" + export function loader({ params }: Route.LoaderArgs) { + type Test = Expect> + return null + } + `, + "app/routes/route.tsx": tsx` + import type { Expect, Equal } from "../expect-type" + import type { Route } from "./+types/route" - export function loader({ params }: Route.LoaderArgs) { - type Test = Expect> - return null - } - `, - "app/routes/child1.tsx": tsx` - import type { Expect, Equal } from "../expect-type" - import type { Route } from "./+types/child1" + export function loader({ params }: Route.LoaderArgs) { + type Test = Expect> + return null + } + `, + "app/routes/child1.tsx": tsx` + import type { Expect, Equal } from "../expect-type" + import type { Route } from "./+types/child1" - export function loader({ params }: Route.LoaderArgs) { - type Test = Expect> - return null - } - `, - "app/routes/child2.tsx": tsx` - import type { Expect, Equal } from "../expect-type" - import type { Route } from "./+types/child2" + export function loader({ params }: Route.LoaderArgs) { + type Test = Expect> + return null + } + `, + "app/routes/child2.tsx": tsx` + import type { Expect, Equal } from "../expect-type" + import type { Route } from "./+types/child2" - export function loader({ params }: Route.LoaderArgs) { - type Test = Expect> - return null - } - `, - "app/routes/layout.tsx": tsx` - import type { Expect, Equal } from "../expect-type" - import type { Route } from "./+types/layout" + export function loader({ params }: Route.LoaderArgs) { + type Test = Expect> + return null + } + `, + "app/routes/layout.tsx": tsx` + import type { Expect, Equal } from "../expect-type" + import type { Route } from "./+types/layout" - export function loader({ params }: Route.LoaderArgs) { - type Test = Expect> - return null - } - `, - "app/routes/in-layout1.tsx": tsx` - import type { Expect, Equal } from "../expect-type" - import type { Route } from "./+types/in-layout1" + export function loader({ params }: Route.LoaderArgs) { + type Test = Expect> + return null + } + `, + "app/routes/in-layout1.tsx": tsx` + import type { Expect, Equal } from "../expect-type" + import type { Route } from "./+types/in-layout1" - export function loader({ params }: Route.LoaderArgs) { - type Test = Expect> - return null - } - `, - "app/routes/in-layout2.tsx": tsx` - import type { Expect, Equal } from "../expect-type" - import type { Route } from "./+types/in-layout2" + export function loader({ params }: Route.LoaderArgs) { + type Test = Expect> + return null + } + `, + "app/routes/in-layout2.tsx": tsx` + import type { Expect, Equal } from "../expect-type" + import type { Route } from "./+types/in-layout2" - export function loader({ params }: Route.LoaderArgs) { - type Test = Expect> - return null - } - `, - }); + export function loader({ params }: Route.LoaderArgs) { + type Test = Expect> + return null + } + `, + }); + await $("pnpm typecheck"); + }); + + test("splat", async ({ edit, $ }) => { + await edit({ + "app/routes.ts": tsx` + import { type RouteConfig, route } from "@react-router/dev/routes"; + + export default [ + route("splat/*", "routes/splat.tsx") + ] satisfies RouteConfig; + `, + "app/routes/splat.tsx": tsx` + import type { Expect, Equal } from "../expect-type" + import type { Route } from "./+types/splat" - const proc = typecheck(cwd); - expect(proc.stdout.toString()).toBe(""); - expect(proc.stderr.toString()).toBe(""); - expect(proc.status).toBe(0); + export function loader({ params }: Route.LoaderArgs) { + type Test = Expect> + return null + } + `, }); + await $("pnpm typecheck"); }); - test("clientLoader.hydrate = true", async () => { - const cwd = await createProject({ - "vite.config.ts": viteConfig(), - "app/expect-type.ts": expectType, + test("clientLoader.hydrate = true", async ({ edit, $ }) => { + await edit({ "app/routes/_index.tsx": tsx` import type { Expect, Equal } from "../expect-type" import type { Route } from "./+types/_index" @@ -322,16 +281,11 @@ test.describe("typegen", () => { } `, }); - const proc = typecheck(cwd); - expect(proc.stdout.toString()).toBe(""); - expect(proc.stderr.toString()).toBe(""); - expect(proc.status).toBe(0); + await $("pnpm typecheck"); }); - test("clientLoader data should not be serialized", async () => { - const cwd = await createProject({ - "vite.config.ts": viteConfig(), - "app/expect-type.ts": expectType, + test("clientLoader data should not be serialized", async ({ edit, $ }) => { + await edit({ "app/routes/_index.tsx": tsx` import { useRouteLoaderData } from "react-router" @@ -352,360 +306,67 @@ test.describe("typegen", () => { } `, }); - const proc = typecheck(cwd); - expect(proc.stdout.toString()).toBe(""); - expect(proc.stderr.toString()).toBe(""); - expect(proc.status).toBe(0); + await $("pnpm typecheck"); }); - test.describe("server-first route component detection", async () => { - test.describe("ServerComponent export", async () => { - test("when RSC Framework Mode plugin is present", async () => { - const cwd = await await createProject({ - "vite.config.ts": viteConfig({ rsc: true }), - "app/expect-type.ts": expectType, - "app/routes.ts": tsx` - import { type RouteConfig, route } from "@react-router/dev/routes"; + test("custom app dir", async ({ cwd, edit, $ }) => { + await edit({ + "react-router.config.ts": tsx` + export default { + appDirectory: "src/myapp", + } + `, + "app/routes/products.$id.tsx": tsx` + import type { Expect, Equal } from "../expect-type" + import type { Route } from "./+types/products.$id" - export default [ - route("server-component/:id", "routes/server-component.tsx") - ] satisfies RouteConfig; - `, - "app/routes/server-component.tsx": tsx` - import type { Expect, Equal } from "../expect-type" - import type { Route } from "./+types/server-component" + export function loader({ params }: Route.LoaderArgs) { + type Test = Expect> + return { planet: "world" } + } - export function loader({ params }: Route.LoaderArgs) { - type Test = Expect> - return { server: "server" } - } + export default function Component({ loaderData }: Route.ComponentProps) { + type Test = Expect> + return

Hello, {loaderData.planet}!

+ } + `, + }); + await fs.mkdir(Path.join(cwd, "src")); + await fs.rename(Path.join(cwd, "app"), Path.join(cwd, "src/myapp")); + await $("pnpm typecheck"); + }); - export function clientLoader() { - return { client: "client" } - } + test("matches", async ({ edit, $ }) => { + await edit({ + "app/routes.ts": tsx` + import { type RouteConfig, route } from "@react-router/dev/routes"; - export function action() { - return { server: "server" } - } + export default [ + route("parent1/:parent1", "routes/parent1.tsx", [ + route("parent2/:parent2", "routes/parent2.tsx", [ + route("current", "routes/current.tsx") + ]) + ]) + ] satisfies RouteConfig; + `, + "app/routes/parent1.tsx": tsx` + import { Outlet } from "react-router" - export function clientAction() { - return { client: "client" } - } + export function loader() { + return { parent1: 1 } + } - export function ServerComponent({ - loaderData, - actionData - }: Route.ComponentProps) { - type TestLoaderData = Expect> - type TestActionData = Expect> - - return ( - <> -

ServerComponent

-

Loader data: {loaderData.server}

-

Action data: {actionData?.server}

- - ) - } - - export function ErrorBoundary({ - loaderData, - actionData - }: Route.ErrorBoundaryProps) { - type TestLoaderData = Expect> - type TestActionData = Expect> - - return ( - <> -

ErrorBoundary

-

Loader data: {loaderData?.server}

-

Action data: {actionData?.server}

- - ) - } - - export function HydrateFallback({ - loaderData, - actionData - }: Route.HydrateFallbackProps) { - type TestLoaderData = Expect> - type TestActionData = Expect> - - return ( - <> -

HydrateFallback

-

Loader data: {loaderData?.server}

-

Action data: {actionData?.server}

- - ) - } - `, - }); - const proc = typecheck(cwd); - expect(proc.stdout.toString()).toBe(""); - expect(proc.stderr.toString()).toBe(""); - expect(proc.status).toBe(0); - }); - - test("when RSC Framework Mode plugin is not present", async () => { - const cwd = await await createProject({ - "vite.config.ts": viteConfig({ rsc: false }), - "app/expect-type.ts": expectType, - "app/routes.ts": tsx` - import { type RouteConfig, route } from "@react-router/dev/routes"; - - export default [ - route("server-component/:id", "routes/server-component.tsx") - ] satisfies RouteConfig; - `, - "app/routes/server-component.tsx": tsx` - import type { Expect, Equal } from "../expect-type" - import type { Route } from "./+types/server-component" - - export function loader({ params }: Route.LoaderArgs) { - type Test = Expect> - return { server: "server" } - } - - export function clientLoader() { - return { client: "client" } - } - - export function action() { - return { server: "server" } - } - - export function clientAction() { - return { client: "client" } - } - - // This export is not used in standard Framework Mode. This is just - // to test that the typegen is unaffected by this export outside of - // RSC Framework Mode. - export function ServerComponent({ - loaderData, - actionData - }: Route.ComponentProps) { - type TestLoaderData = Expect> - type TestActionData = Expect> - - return ( - <> -

ServerComponent (unused)

-

Loader data: {"server" in loaderData ? loaderData.server : loaderData.client}

- {actionData &&

Action data: {"server" in actionData ? actionData.server : actionData.client}

} - - ) - } - - export function ErrorBoundary({ - loaderData, - actionData - }: Route.ErrorBoundaryProps) { - type TestLoaderData = Expect> - type TestActionData = Expect> - - return ( - <> -

ErrorBoundary

- {loaderData &&

Loader data: {"server" in loaderData ? loaderData.server : loaderData.client}

} - {actionData &&

Action data: {"server" in actionData ? actionData.server : actionData.client}

} - - ) - } - - export function HydrateFallback({ - loaderData, - actionData - }: Route.HydrateFallbackProps) { - type TestLoaderData = Expect> - type TestActionData = Expect> - - return ( - <> -

HydrateFallback

- {loaderData &&

Loader data: {"server" in loaderData ? loaderData.server : loaderData.client}

} - {actionData &&

Action data: {"server" in actionData ? actionData.server : actionData.client}

} - - ) - } - `, - }); - const proc = typecheck(cwd); - expect(proc.stdout.toString()).toBe(""); - expect(proc.stderr.toString()).toBe(""); - expect(proc.status).toBe(0); - }); - }); - - test.describe("default export", async () => { - async function createClientFirstRouteProject({ rsc }: { rsc: boolean }) { - return await await createProject({ - "vite.config.ts": viteConfig({ rsc }), - "app/expect-type.ts": expectType, - "app/routes.ts": tsx` - import { type RouteConfig, route } from "@react-router/dev/routes"; - - export default [ - route("client-component/:id", "routes/client-component.tsx") - ] satisfies RouteConfig; - `, - "app/routes/client-component.tsx": tsx` - import type { Expect, Equal } from "../expect-type" - import type { Route } from "./+types/client-component" - - export function loader({ params }: Route.LoaderArgs) { - type Test = Expect> - return { server: "server" } - } - - export function clientLoader() { - return { client: "client" } - } - - export function action() { - return { server: "server" } - } - - export function clientAction() { - return { client: "client" } - } - - export default function ClientComponent({ - loaderData, - actionData - }: Route.ComponentProps) { - type TestLoaderData = Expect> - type TestActionData = Expect> - - return ( - <> -

default (Component)

-

Loader data: {"server" in loaderData ? loaderData.server : loaderData.client}

- {actionData &&

Action data: {"server" in actionData ? actionData.server : actionData.client}

} - - ) - } - - export function ErrorBoundary({ - loaderData, - actionData - }: Route.ErrorBoundaryProps) { - type TestLoaderData = Expect> - type TestActionData = Expect> - - return ( - <> -

ErrorBoundary

- {loaderData &&

Loader data: {"server" in loaderData ? loaderData.server : loaderData.client}

} - {actionData &&

Action data: {"server" in actionData ? actionData.server : actionData.client}

} - - ) - } - - export function HydrateFallback({ - loaderData, - actionData - }: Route.HydrateFallbackProps) { - type TestLoaderData = Expect> - type TestActionData = Expect> - - return ( - <> -

HydrateFallback

- {loaderData &&

Loader data: {"server" in loaderData ? loaderData.server : loaderData.client}

} - {actionData &&

Action data: {"server" in actionData ? actionData.server : actionData.client}

} - - ) - } - `, - }); - } - - test("when RSC Framework Mode plugin is present", async () => { - const cwd = await createClientFirstRouteProject({ rsc: true }); - const proc = typecheck(cwd); - expect(proc.stdout.toString()).toBe(""); - expect(proc.stderr.toString()).toBe(""); - expect(proc.status).toBe(0); - }); - - test("when RSC Framework Mode plugin is not present", async () => { - const cwd = await createClientFirstRouteProject({ rsc: false }); - const proc = typecheck(cwd); - expect(proc.stdout.toString()).toBe(""); - expect(proc.stderr.toString()).toBe(""); - expect(proc.status).toBe(0); - }); - }); - }); - - test("custom app dir", async () => { - const cwd = await createProject({ - "vite.config.ts": viteConfig(), - "react-router.config.ts": tsx` - export default { - appDirectory: "src/myapp", - } - `, - "app/expect-type.ts": expectType, - "app/routes/products.$id.tsx": tsx` - import type { Expect, Equal } from "../expect-type" - import type { Route } from "./+types/products.$id" - - export function loader({ params }: Route.LoaderArgs) { - type Test = Expect> - return { planet: "world" } - } - - export default function Component({ loaderData }: Route.ComponentProps) { - type Test = Expect> - return

Hello, {loaderData.planet}!

- } - `, - }); - mkdirSync(path.join(cwd, "src")); - renameSync(path.join(cwd, "app"), path.join(cwd, "src/myapp")); - - const proc = typecheck(cwd); - expect(proc.stdout.toString()).toBe(""); - expect(proc.stderr.toString()).toBe(""); - expect(proc.status).toBe(0); - }); - - test("matches", async () => { - const cwd = await createProject({ - "vite.config.ts": viteConfig(), - "app/expect-type.ts": expectType, - "app/routes.ts": tsx` - import { type RouteConfig, route } from "@react-router/dev/routes"; - - export default [ - route("parent1/:parent1", "routes/parent1.tsx", [ - route("parent2/:parent2", "routes/parent2.tsx", [ - route("current", "routes/current.tsx") - ]) - ]) - ] satisfies RouteConfig; - `, - "app/routes/parent1.tsx": tsx` - import { Outlet } from "react-router" - - export function loader() { - return { parent1: 1 } - } - - export default function Component() { - return ( -
-

Parent1

- -
- ) - } - `, - "app/routes/parent2.tsx": tsx` - import { Outlet } from "react-router" + export default function Component() { + return ( +
+

Parent1

+ +
+ ) + } + `, + "app/routes/parent2.tsx": tsx` + import { Outlet } from "react-router" export function loader() { return { parent2: 2 } @@ -764,16 +425,11 @@ test.describe("typegen", () => { } `, }); - const proc = typecheck(cwd); - expect(proc.stdout.toString()).toBe(""); - expect(proc.stderr.toString()).toBe(""); - expect(proc.status).toBe(0); + await $("pnpm typecheck"); }); - test("route files with absolute paths", async () => { - const cwd = await createProject({ - "vite.config.ts": viteConfig(), - "app/expect-type.ts": expectType, + test("route files with absolute paths", async ({ edit, $ }) => { + await edit({ "app/routes.ts": tsx` import path from "node:path"; import { type RouteConfig, route } from "@react-router/dev/routes"; @@ -797,17 +453,11 @@ test.describe("typegen", () => { } `, }); - - const proc = typecheck(cwd); - expect(proc.stdout.toString()).toBe(""); - expect(proc.stderr.toString()).toBe(""); - expect(proc.status).toBe(0); + await $("pnpm typecheck"); }); - test("href", async () => { - const cwd = await createProject({ - "vite.config.ts": viteConfig(), - "app/expect-type.ts": expectType, + test("href", async ({ edit, $ }) => { + await edit({ "app/routes.ts": tsx` import path from "node:path"; import { type RouteConfig, route } from "@react-router/dev/routes"; @@ -855,101 +505,23 @@ test.describe("typegen", () => { // @ts-expect-error href("/optional-param") - // @ts-expect-error - href("/optional-param/:opt", { opt: "hello" }) - href("/optional-param/:opt?") - href("/optional-param/:opt?", { opt: "hello" }) - - href("/leading-and-trailing-slash") - // @ts-expect-error - href("/leading-and-trailing-slash/") - - export default function Component() {} - `, - }); - const proc = typecheck(cwd); - expect(proc.stdout.toString()).toBe(""); - expect(proc.stderr.toString()).toBe(""); - expect(proc.status).toBe(0); - }); - - test.describe("virtual:react-router/server-build", async () => { - test("static import matches 'createRequestHandler' argument type", async () => { - const cwd = await createProject({ - "vite.config.ts": viteConfig(), - "app/routes.ts": tsx` - import { type RouteConfig } from "@react-router/dev/routes"; - export default [] satisfies RouteConfig; - `, - "app/handler.ts": tsx` - import { createRequestHandler } from "react-router"; - import * as serverBuild from "virtual:react-router/server-build"; - export default createRequestHandler(serverBuild); - `, - }); - - const proc = typecheck(cwd); - expect(proc.stdout.toString()).toBe(""); - expect(proc.stderr.toString()).toBe(""); - expect(proc.status).toBe(0); - }); - - test("works with tsconfig 'moduleDetection' set to 'force'", async () => { - const cwd = await createProject({ - "vite.config.ts": viteConfig(), - "app/routes.ts": tsx` - import { type RouteConfig } from "@react-router/dev/routes"; - export default [] satisfies RouteConfig; - `, - "app/handler.ts": tsx` - import { createRequestHandler } from "react-router"; - import * as serverBuild from "virtual:react-router/server-build"; - export default createRequestHandler(serverBuild); - `, - }); - - const tsconfig = JSON.parse( - await readFile(path.join(cwd, "tsconfig.json"), "utf-8"), - ); - tsconfig.compilerOptions.moduleDetection = "force"; - await writeFile( - path.join(cwd, "tsconfig.json"), - JSON.stringify(tsconfig), - "utf-8", - ); - - const proc = typecheck(cwd); - expect(proc.stdout.toString()).toBe(""); - expect(proc.stderr.toString()).toBe(""); - expect(proc.status).toBe(0); - }); + // @ts-expect-error + href("/optional-param/:opt", { opt: "hello" }) + href("/optional-param/:opt?") + href("/optional-param/:opt?", { opt: "hello" }) - test("dynamic import matches 'createRequestHandler' function argument type", async () => { - const cwd = await createProject({ - "vite.config.ts": viteConfig(), - "app/routes.ts": tsx` - import { type RouteConfig } from "@react-router/dev/routes"; - export default [] satisfies RouteConfig; - `, - "app/handler.ts": tsx` - import { createRequestHandler } from "react-router"; - export default createRequestHandler( - () => import("virtual:react-router/server-build") - ); - `, - }); + href("/leading-and-trailing-slash") + // @ts-expect-error + href("/leading-and-trailing-slash/") - const proc = typecheck(cwd); - expect(proc.stdout.toString()).toBe(""); - expect(proc.stderr.toString()).toBe(""); - expect(proc.status).toBe(0); + export default function Component() {} + `, }); + await $("pnpm typecheck"); }); - test("reuse route file at multiple paths", async () => { - const cwd = await createProject({ - "vite.config.ts": viteConfig(), - "app/expect-type.ts": expectType, + test("reuse route file at multiple paths", async ({ edit, $ }) => { + await edit({ "app/routes.ts": tsx` import { type RouteConfig, route } from "@react-router/dev/routes"; export default [ @@ -1021,10 +593,344 @@ test.describe("typegen", () => { } `, }); + await $("pnpm typecheck"); + }); + + test.describe("virtual:react-router/server-build", async () => { + test("static import matches 'createRequestHandler' argument type", async ({ + edit, + $, + }) => { + await edit({ + "app/routes.ts": tsx` + import { type RouteConfig } from "@react-router/dev/routes"; + export default [] satisfies RouteConfig; + `, + "app/handler.ts": tsx` + import { createRequestHandler } from "react-router"; + import * as serverBuild from "virtual:react-router/server-build"; + export default createRequestHandler(serverBuild); + `, + }); + await $("pnpm typecheck"); + }); + + test("works with tsconfig 'moduleDetection' set to 'force'", async ({ + edit, + $, + }) => { + await edit({ + "app/routes.ts": tsx` + import { type RouteConfig } from "@react-router/dev/routes"; + export default [] satisfies RouteConfig; + `, + "app/handler.ts": tsx` + import { createRequestHandler } from "react-router"; + import * as serverBuild from "virtual:react-router/server-build"; + export default createRequestHandler(serverBuild); + `, + "tsconfig.json": (contents) => { + const tsconfig = JSON.parse(contents); + tsconfig.compilerOptions.moduleDetection = "force"; + return JSON.stringify(tsconfig, null, 2); + }, + }); + await $("pnpm typecheck"); + }); + + test("dynamic import matches 'createRequestHandler' function argument type", async ({ + edit, + $, + }) => { + await edit({ + "app/routes.ts": tsx` + import { type RouteConfig } from "@react-router/dev/routes"; + export default [] satisfies RouteConfig; + `, + "app/handler.ts": tsx` + import { createRequestHandler } from "react-router"; + export default createRequestHandler( + () => import("virtual:react-router/server-build") + ); + `, + }); + await $("pnpm typecheck"); + }); + }); + + test.describe("server-first route component detection", () => { + test.describe("ServerComponent export", () => { + test("when RSC Framework Mode plugin is present", async ({ edit, $ }) => { + await edit({ + "vite.config.ts": viteConfig({ rsc: true }), + "app/routes.ts": tsx` + import { type RouteConfig, route } from "@react-router/dev/routes"; + + export default [ + route("server-component/:id", "routes/server-component.tsx") + ] satisfies RouteConfig; + `, + "app/routes/server-component.tsx": tsx` + import type { Expect, Equal } from "../expect-type" + import type { Route } from "./+types/server-component" + + export function loader({ params }: Route.LoaderArgs) { + type Test = Expect> + return { server: "server" } + } + + export function clientLoader() { + return { client: "client" } + } + + export function action() { + return { server: "server" } + } + + export function clientAction() { + return { client: "client" } + } + + export function ServerComponent({ + loaderData, + actionData + }: Route.ComponentProps) { + type TestLoaderData = Expect> + type TestActionData = Expect> + + return ( + <> +

ServerComponent

+

Loader data: {loaderData.server}

+

Action data: {actionData?.server}

+ + ) + } + + export function ErrorBoundary({ + loaderData, + actionData + }: Route.ErrorBoundaryProps) { + type TestLoaderData = Expect> + type TestActionData = Expect> + + return ( + <> +

ErrorBoundary

+

Loader data: {loaderData?.server}

+

Action data: {actionData?.server}

+ + ) + } + + export function HydrateFallback({ + loaderData, + actionData + }: Route.HydrateFallbackProps) { + type TestLoaderData = Expect> + type TestActionData = Expect> + + return ( + <> +

HydrateFallback

+

Loader data: {loaderData?.server}

+

Action data: {actionData?.server}

+ + ) + } + `, + }); + await $("pnpm typecheck"); + }); + + test("when RSC Framework Mode plugin is not present", async ({ + edit, + $, + }) => { + await edit({ + "vite.config.ts": viteConfig({ rsc: false }), + "app/routes.ts": tsx` + import { type RouteConfig, route } from "@react-router/dev/routes"; + + export default [ + route("server-component/:id", "routes/server-component.tsx") + ] satisfies RouteConfig; + `, + "app/routes/server-component.tsx": tsx` + import type { Expect, Equal } from "../expect-type" + import type { Route } from "./+types/server-component" + + export function loader({ params }: Route.LoaderArgs) { + type Test = Expect> + return { server: "server" } + } + + export function clientLoader() { + return { client: "client" } + } + + export function action() { + return { server: "server" } + } + + export function clientAction() { + return { client: "client" } + } + + // This export is not used in standard Framework Mode. This is just + // to test that the typegen is unaffected by this export outside of + // RSC Framework Mode. + export function ServerComponent({ + loaderData, + actionData + }: Route.ComponentProps) { + type TestLoaderData = Expect> + type TestActionData = Expect> + + return ( + <> +

ServerComponent (unused)

+

Loader data: {"server" in loaderData ? loaderData.server : loaderData.client}

+ {actionData &&

Action data: {"server" in actionData ? actionData.server : actionData.client}

} + + ) + } + + export function ErrorBoundary({ + loaderData, + actionData + }: Route.ErrorBoundaryProps) { + type TestLoaderData = Expect> + type TestActionData = Expect> + + return ( + <> +

ErrorBoundary

+ {loaderData &&

Loader data: {"server" in loaderData ? loaderData.server : loaderData.client}

} + {actionData &&

Action data: {"server" in actionData ? actionData.server : actionData.client}

} + + ) + } + + export function HydrateFallback({ + loaderData, + actionData + }: Route.HydrateFallbackProps) { + type TestLoaderData = Expect> + type TestActionData = Expect> + + return ( + <> +

HydrateFallback

+ {loaderData &&

Loader data: {"server" in loaderData ? loaderData.server : loaderData.client}

} + {actionData &&

Action data: {"server" in actionData ? actionData.server : actionData.client}

} + + ) + } + `, + }); + await $("pnpm typecheck"); + }); + }); - const proc = typecheck(cwd); - expect(proc.stdout.toString()).toBe(""); - expect(proc.stderr.toString()).toBe(""); - expect(proc.status).toBe(0); + test.describe("default export", () => { + const clientFirstRouteFiles = { + "app/routes.ts": tsx` + import { type RouteConfig, route } from "@react-router/dev/routes"; + + export default [ + route("client-component/:id", "routes/client-component.tsx") + ] satisfies RouteConfig; + `, + "app/routes/client-component.tsx": tsx` + import type { Expect, Equal } from "../expect-type" + import type { Route } from "./+types/client-component" + + export function loader({ params }: Route.LoaderArgs) { + type Test = Expect> + return { server: "server" } + } + + export function clientLoader() { + return { client: "client" } + } + + export function action() { + return { server: "server" } + } + + export function clientAction() { + return { client: "client" } + } + + export default function ClientComponent({ + loaderData, + actionData + }: Route.ComponentProps) { + type TestLoaderData = Expect> + type TestActionData = Expect> + + return ( + <> +

default (Component)

+

Loader data: {"server" in loaderData ? loaderData.server : loaderData.client}

+ {actionData &&

Action data: {"server" in actionData ? actionData.server : actionData.client}

} + + ) + } + + export function ErrorBoundary({ + loaderData, + actionData + }: Route.ErrorBoundaryProps) { + type TestLoaderData = Expect> + type TestActionData = Expect> + + return ( + <> +

ErrorBoundary

+ {loaderData &&

Loader data: {"server" in loaderData ? loaderData.server : loaderData.client}

} + {actionData &&

Action data: {"server" in actionData ? actionData.server : actionData.client}

} + + ) + } + + export function HydrateFallback({ + loaderData, + actionData + }: Route.HydrateFallbackProps) { + type TestLoaderData = Expect> + type TestActionData = Expect> + + return ( + <> +

HydrateFallback

+ {loaderData &&

Loader data: {"server" in loaderData ? loaderData.server : loaderData.client}

} + {actionData &&

Action data: {"server" in actionData ? actionData.server : actionData.client}

} + + ) + } + `, + }; + + test("when RSC Framework Mode plugin is present", async ({ edit, $ }) => { + await edit({ + "vite.config.ts": viteConfig({ rsc: true }), + ...clientFirstRouteFiles, + }); + await $("pnpm typecheck"); + }); + + test("when RSC Framework Mode plugin is not present", async ({ + edit, + $, + }) => { + await edit({ + "vite.config.ts": viteConfig({ rsc: false }), + ...clientFirstRouteFiles, + }); + await $("pnpm typecheck"); + }); + }); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 182ceea34b..b1ed9597b7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -217,8 +217,8 @@ importers: specifier: ^0.7.0 version: 0.7.0 execa: - specifier: ^5.1.1 - version: 5.1.1 + specifier: ^9.6.0 + version: 9.6.0 express: specifier: ^4.19.2 version: 4.21.2 @@ -598,7 +598,7 @@ importers: version: 3.0.1(vite@5.1.3(@types/node@22.14.0)(lightningcss@1.30.1)) vite-tsconfig-paths: specifier: ^4.2.1 - version: 4.3.2(typescript@5.4.5)(vite@5.1.3(@types/node@22.14.0)(lightningcss@1.30.1)) + version: 4.3.2(typescript@5.4.5)(vite@5.1.3(@types/node@22.14.0)(lightningcss@1.30.1)(terser@5.15.0)) integration/helpers/vite-6-template: dependencies: @@ -1161,7 +1161,7 @@ importers: version: 0.4.30(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@6.2.5(@types/node@20.11.30)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.3)(yaml@2.8.0)) esbuild-register: specifier: ^3.6.0 - version: 3.6.0(esbuild@0.25.0) + version: 3.6.0(esbuild@0.25.4) execa: specifier: 5.1.1 version: 5.1.1 @@ -1573,7 +1573,7 @@ importers: version: 5.1.3(@types/node@22.14.0)(lightningcss@1.30.1)(terser@5.15.0) vite-tsconfig-paths: specifier: ^4.2.1 - version: 4.3.2(typescript@5.4.5)(vite@5.1.3(@types/node@22.14.0)(lightningcss@1.30.1)) + version: 4.3.2(typescript@5.4.5)(vite@5.1.3(@types/node@22.14.0)(lightningcss@1.30.1)(terser@5.15.0)) playground/framework-vite-7-beta: dependencies: @@ -4561,6 +4561,9 @@ packages: '@rushstack/eslint-patch@1.10.1': resolution: {integrity: sha512-S3Kq8e7LqxkA9s7HKLqXGTGck1uwis5vAXan3FnU5yw1Ec5hsSGnq4s/UCaSqABPOnOTg7zASLyst7+ohgWexg==} + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + '@shikijs/engine-oniguruma@3.8.1': resolution: {integrity: sha512-KGQJZHlNY7c656qPFEQpIoqOuC4LrxjyNndRdzk5WKB/Ie87+NJCF1xo9KkOUxwxylk7rT6nhlZyTGTC4fCe1g==} @@ -4588,6 +4591,10 @@ packages: '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + '@sinonjs/commons@2.0.0': resolution: {integrity: sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==} @@ -6376,6 +6383,10 @@ packages: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} + execa@9.6.0: + resolution: {integrity: sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==} + engines: {node: ^18.19.0 || >=20.5.0} + exit-hook@2.2.1: resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} engines: {node: '>=6'} @@ -6438,6 +6449,10 @@ packages: picomatch: optional: true + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -6584,6 +6599,10 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + get-symbol-description@1.0.2: resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} engines: {node: '>= 0.4'} @@ -6767,6 +6786,10 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -6988,6 +7011,10 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + is-string@1.0.7: resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} engines: {node: '>= 0.4'} @@ -7004,6 +7031,10 @@ packages: resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} engines: {node: '>= 0.4'} + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} @@ -8004,6 +8035,10 @@ packages: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -8149,6 +8184,10 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + parse-statements@1.0.11: resolution: {integrity: sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==} @@ -8180,6 +8219,10 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -8318,6 +8361,10 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + pretty-ms@9.3.0: + resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + engines: {node: '>=18'} + printable-characters@1.0.42: resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} @@ -9064,6 +9111,10 @@ packages: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} @@ -9388,6 +9439,10 @@ packages: resolution: {integrity: sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ==} engines: {node: '>=4'} + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + unified@10.1.2: resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==} @@ -9878,6 +9933,10 @@ packages: resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} engines: {node: '>=18'} + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + youch@3.3.4: resolution: {integrity: sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==} @@ -12878,6 +12937,8 @@ snapshots: '@rushstack/eslint-patch@1.10.1': {} + '@sec-ant/readable-stream@0.4.1': {} + '@shikijs/engine-oniguruma@3.8.1': dependencies: '@shikijs/types': 3.8.1 @@ -12908,6 +12969,8 @@ snapshots: '@sinclair/typebox@0.27.8': {} + '@sindresorhus/merge-streams@4.0.0': {} + '@sinonjs/commons@2.0.0': dependencies: type-detect: 4.0.8 @@ -14784,6 +14847,13 @@ snapshots: transitivePeerDependencies: - supports-color + esbuild-register@3.6.0(esbuild@0.25.4): + dependencies: + debug: 4.4.1 + esbuild: 0.25.4 + transitivePeerDependencies: + - supports-color + esbuild@0.19.12: optionalDependencies: '@esbuild/aix-ppc64': 0.19.12 @@ -15256,6 +15326,21 @@ snapshots: signal-exit: 3.0.7 strip-final-newline: 2.0.0 + execa@9.6.0: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.3.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.2 + exit-hook@2.2.1: {} exit@0.1.2: {} @@ -15348,6 +15433,10 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + file-entry-cache@6.0.1: dependencies: flat-cache: 3.0.4 @@ -15502,6 +15591,11 @@ snapshots: get-stream@6.0.1: {} + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + get-symbol-description@1.0.2: dependencies: call-bind: 1.0.7 @@ -15748,6 +15842,8 @@ snapshots: human-signals@2.1.0: {} + human-signals@8.0.1: {} + iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 @@ -15929,6 +16025,8 @@ snapshots: is-stream@2.0.1: {} + is-stream@4.0.1: {} + is-string@1.0.7: dependencies: has-tostringtag: 1.0.2 @@ -15945,6 +16043,8 @@ snapshots: dependencies: which-typed-array: 1.1.15 + is-unicode-supported@2.1.0: {} + is-weakmap@2.0.2: {} is-weakref@1.0.2: @@ -17543,6 +17643,11 @@ snapshots: dependencies: path-key: 3.1.1 + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + nth-check@2.1.1: dependencies: boolbase: 1.0.0 @@ -17725,6 +17830,8 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse-ms@4.0.0: {} + parse-statements@1.0.11: {} parse5-htmlparser2-tree-adapter@7.0.0: @@ -17750,6 +17857,8 @@ snapshots: path-key@3.1.1: {} + path-key@4.0.0: {} + path-parse@1.0.7: {} path-scurry@1.10.2: @@ -17868,6 +17977,10 @@ snapshots: ansi-styles: 5.2.0 react-is: 19.1.0 + pretty-ms@9.3.0: + dependencies: + parse-ms: 4.0.0 + printable-characters@1.0.42: {} proc-log@3.0.0: {} @@ -18710,6 +18823,8 @@ snapshots: strip-final-newline@2.0.0: {} + strip-final-newline@4.0.0: {} + strip-indent@3.0.0: dependencies: min-indent: 1.0.1 @@ -19056,6 +19171,8 @@ snapshots: unicode-property-aliases-ecmascript@2.0.0: {} + unicorn-magic@0.3.0: {} + unified@10.1.2: dependencies: '@types/unist': 2.0.10 @@ -19362,7 +19479,7 @@ snapshots: - supports-color - typescript - vite-tsconfig-paths@4.3.2(typescript@5.4.5)(vite@5.1.3(@types/node@22.14.0)(lightningcss@1.30.1)): + vite-tsconfig-paths@4.3.2(typescript@5.4.5)(vite@5.1.3(@types/node@22.14.0)(lightningcss@1.30.1)(terser@5.15.0)): dependencies: debug: 4.4.1 globrex: 0.1.2 @@ -19703,6 +19820,8 @@ snapshots: yoctocolors-cjs@2.1.2: {} + yoctocolors@2.1.2: {} + youch@3.3.4: dependencies: cookie: 0.7.2 From 40e396654180a434e84e9b2e00d47be382b35f0a Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Mon, 29 Sep 2025 21:29:20 +0000 Subject: [PATCH 04/16] chore: deduplicate `pnpm-lock.yaml` --- pnpm-lock.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b1ed9597b7..0f062aef95 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -113,7 +113,7 @@ importers: version: 7.34.1(eslint@8.57.0) eslint-plugin-react-hooks: specifier: next - version: 6.1.0-canary-128abcfa-20250917(eslint@8.57.0) + version: 6.1.0-canary-d15d7fd7-20250929(eslint@8.57.0) fast-glob: specifier: 3.2.11 version: 3.2.11 @@ -6259,8 +6259,8 @@ packages: peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 - eslint-plugin-react-hooks@6.1.0-canary-128abcfa-20250917: - resolution: {integrity: sha512-cR/EftrsVDqCbmfq6IEsLaPqMhkLFgoiJvnSF6nArECbchE8ZQJyGQv7sXGwsf1sKYXr7N9vaB45iDmZAx4Ecw==} + eslint-plugin-react-hooks@6.1.0-canary-d15d7fd7-20250929: + resolution: {integrity: sha512-BeJu8hPQW+FjteWcCVdVezI2ogQs2mrHSOznrk00dbXztd8NqnyHlB7Z1wx3ZwkUVVAVHmmxrBCrRMn6UP15FA==} engines: {node: '>=18'} peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 @@ -15135,7 +15135,7 @@ snapshots: dependencies: eslint: 8.57.0 - eslint-plugin-react-hooks@6.1.0-canary-128abcfa-20250917(eslint@8.57.0): + eslint-plugin-react-hooks@6.1.0-canary-d15d7fd7-20250929(eslint@8.57.0): dependencies: '@babel/core': 7.27.7 '@babel/parser': 7.27.7 From 850bf9a230d0c079abfbcf184fbe9b17d55a4e11 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Tue, 30 Sep 2025 13:04:49 -0700 Subject: [PATCH 05/16] fix: handle rsc external redirects (#14400) --- .changeset/breezy-planes-roll.md | 5 + integration/rsc/rsc-test.ts | 165 ++++++++++++++++++++++ packages/react-router/lib/rsc/browser.tsx | 9 +- 3 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 .changeset/breezy-planes-roll.md diff --git a/.changeset/breezy-planes-roll.md b/.changeset/breezy-planes-roll.md new file mode 100644 index 0000000000..6d1bb70e6a --- /dev/null +++ b/.changeset/breezy-planes-roll.md @@ -0,0 +1,5 @@ +--- +"react-router": patch +--- + +handle external redirects in from server actions diff --git a/integration/rsc/rsc-test.ts b/integration/rsc/rsc-test.ts index 7ddafe7421..9ddff47d05 100644 --- a/integration/rsc/rsc-test.ts +++ b/integration/rsc/rsc-test.ts @@ -435,6 +435,17 @@ implementations.forEach((implementation) => { } ] }, + { + id: "throw-external-redirect-server-action", + path: "throw-external-redirect-server-action", + children: [ + { + id: "throw-external-redirect-server-action.home", + index: true, + lazy: () => import("./routes/throw-external-redirect-server-action/home"), + } + ] + }, { id: "side-effect-redirect-server-action", path: "side-effect-redirect-server-action", @@ -446,6 +457,17 @@ implementations.forEach((implementation) => { } ] }, + { + id: "side-effect-external-redirect-server-action", + path: "side-effect-external-redirect-server-action", + children: [ + { + id: "side-effect-external-redirect-server-action.home", + index: true, + lazy: () => import("./routes/side-effect-external-redirect-server-action/home"), + } + ] + }, { id: "server-function-reference", path: "server-function-reference", @@ -986,6 +1008,82 @@ implementations.forEach((implementation) => { ); } `, + "src/routes/throw-external-redirect-server-action/home.actions.ts": js` + "use server"; + import { redirect } from "react-router"; + + export async function redirectAction(formData: FormData) { + // Throw a redirect to an external URL + throw redirect("https://example.com/"); + } + `, + "src/routes/throw-external-redirect-server-action/home.client.tsx": js` + "use client"; + + import { useState } from "react"; + + export function Counter() { + const [count, setCount] = useState(0); + return ; + } + `, + "src/routes/throw-external-redirect-server-action/home.tsx": js` + import { redirectAction } from "./home.actions"; + import { Counter } from "./home.client"; + + export default function HomeRoute(props) { + return ( +
+
+ +
+ +
+ ); + } + `, + "src/routes/side-effect-external-redirect-server-action/home.actions.ts": js` + "use server"; + import { redirect } from "react-router"; + + export async function redirectAction() { + // Perform a side-effect redirect to an external URL + redirect("https://example.com/", { headers: { "x-test": "test" } }); + return "redirected"; + } + `, + "src/routes/side-effect-external-redirect-server-action/home.client.tsx": js` + "use client"; + import { useState } from "react"; + + export function Counter() { + const [count, setCount] = useState(0); + return ; + } + `, + "src/routes/side-effect-external-redirect-server-action/home.tsx": js` + "use client"; + import {useActionState} from "react"; + import { redirectAction } from "./home.actions"; + import { Counter } from "./home.client"; + + export default function HomeRoute(props) { + const [state, action] = useActionState(redirectAction, null); + return ( +
+
+ +
+ {state &&
{state}
} + +
+ ); + } + `, "src/routes/server-function-reference/home.actions.ts": js` "use server"; @@ -1736,6 +1834,33 @@ implementations.forEach((implementation) => { validateRSCHtml(await page.content()); }); + test("Supports React Server Functions thrown external redirects", async ({ + page, + }) => { + // Test is expected to fail currently — skip running it + // test.skip(true, "Known failing test for external redirect behavior"); + + await page.goto( + `http://localhost:${port}/throw-external-redirect-server-action/`, + ); + + // Verify initial server render + await page.waitForSelector("[data-count]"); + expect(await page.locator("[data-count]").textContent()).toBe( + "Count: 0", + ); + await page.click("[data-count]"); + expect(await page.locator("[data-count]").textContent()).toBe( + "Count: 1", + ); + + // Submit the form to trigger server function redirect to external URL + await page.click("[data-submit]"); + + // We expect the browser to navigate to the external site (example.com) + await expect(page).toHaveURL(`https://example.com/`); + }); + test("Supports React Server Functions side-effect redirects", async ({ page, }) => { @@ -1789,6 +1914,46 @@ implementations.forEach((implementation) => { validateRSCHtml(await page.content()); }); + test("Supports React Server Functions side-effect external redirects", async ({ + page, + }) => { + // Test is expected to fail currently — skip running it + test.skip(implementation.name === "parcel", "Not working in parcel?"); + + await page.goto( + `http://localhost:${port}/side-effect-external-redirect-server-action`, + ); + + // Verify initial server render + await page.waitForSelector("[data-count]"); + expect(await page.locator("[data-count]").textContent()).toBe( + "Count: 0", + ); + await page.click("[data-count]"); + expect(await page.locator("[data-count]").textContent()).toBe( + "Count: 1", + ); + + const responseHeadersPromise = new Promise>( + (resolve) => { + page.addListener("response", (response) => { + if (response.request().method() === "POST") { + resolve(response.headers()); + } + }); + }, + ); + + // Submit the form to trigger server function redirect to external URL + await page.click("[data-submit]"); + + // We expect the browser to navigate to the external site (example.com) + await expect(page).toHaveURL(`https://example.com/`); + + // Optionally assert that the server sent the header + expect((await responseHeadersPromise)["x-test"]).toBe("test"); + }); + test("Supports React Server Function References", async ({ page }) => { await page.goto(`http://localhost:${port}/server-function-reference`); diff --git a/packages/react-router/lib/rsc/browser.tsx b/packages/react-router/lib/rsc/browser.tsx index 9b9974bc70..5046e28179 100644 --- a/packages/react-router/lib/rsc/browser.tsx +++ b/packages/react-router/lib/rsc/browser.tsx @@ -140,7 +140,7 @@ export function createCallServer({ Promise.resolve(payloadPromise) .then(async (payload) => { if (payload.type === "redirect") { - if (payload.reload) { + if (payload.reload || isExternalLocation(payload.location)) { window.location.href = payload.location; return () => {}; } @@ -163,7 +163,7 @@ export function createCallServer({ globalVar.__routerActionID <= actionId ) { if (rerender.type === "redirect") { - if (rerender.reload) { + if (rerender.reload || isExternalLocation(rerender.location)) { window.location.href = rerender.location; return; } @@ -1047,3 +1047,8 @@ function debounce(callback: (...args: unknown[]) => unknown, wait: number) { timeoutId = window.setTimeout(() => callback(...args), wait); }; } + +function isExternalLocation(location: string) { + const newLocation = new URL(location, window.location.href); + return newLocation.origin !== window.location.origin; +} From f7bacc1365de613b7984a8a6c5888b0ffb666280 Mon Sep 17 00:00:00 2001 From: Jonas Adler Date: Wed, 1 Oct 2025 20:26:23 +0200 Subject: [PATCH 06/16] Update valibot dependency to ^1.1.0 (#14379) --- .changeset/green-pens-push.md | 5 +++++ contributors.yml | 1 + .../__tests__/route-config-test.ts | 4 ++-- packages/react-router-dev/package.json | 2 +- pnpm-lock.yaml | 16 ++++++++-------- 5 files changed, 17 insertions(+), 11 deletions(-) create mode 100644 .changeset/green-pens-push.md diff --git a/.changeset/green-pens-push.md b/.changeset/green-pens-push.md new file mode 100644 index 0000000000..c0a3998ba5 --- /dev/null +++ b/.changeset/green-pens-push.md @@ -0,0 +1,5 @@ +--- +"@react-router/dev": patch +--- + +Update `valibot` dependency to `^1.1.0` diff --git a/contributors.yml b/contributors.yml index e38c4527a0..8dc3055ede 100644 --- a/contributors.yml +++ b/contributors.yml @@ -447,3 +447,4 @@ - zeromask1337 - zheng-chuang - zxTomw +- jadlr diff --git a/packages/react-router-dev/__tests__/route-config-test.ts b/packages/react-router-dev/__tests__/route-config-test.ts index 0692335b2b..8ecc5f03a2 100644 --- a/packages/react-router-dev/__tests__/route-config-test.ts +++ b/packages/react-router-dev/__tests__/route-config-test.ts @@ -76,7 +76,7 @@ describe("route config", () => { "Route config in "routes.ts" is invalid. Path: routes.0.children.0.file - Invalid type: Expected string but received undefined" + Invalid key: Expected "file" but received undefined" `); }); @@ -129,7 +129,7 @@ describe("route config", () => { "Route config in "routes.ts" is invalid. Path: routes.0.children.0.file - Invalid type: Expected string but received undefined + Invalid key: Expected "file" but received undefined Path: routes.0.children.1.file Invalid type: Expected string but received 123 diff --git a/packages/react-router-dev/package.json b/packages/react-router-dev/package.json index 5d8c1a0e17..fc0d72d1e2 100644 --- a/packages/react-router-dev/package.json +++ b/packages/react-router-dev/package.json @@ -89,7 +89,7 @@ "react-refresh": "^0.14.0", "semver": "^7.3.7", "tinyglobby": "^0.2.14", - "valibot": "^0.41.0", + "valibot": "^1.1.0", "vite-node": "^3.2.2" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0f062aef95..c3e43eaea6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1117,8 +1117,8 @@ importers: specifier: ^0.2.14 version: 0.2.14 valibot: - specifier: ^0.41.0 - version: 0.41.0(typescript@5.4.5) + specifier: ^1.1.0 + version: 1.1.0(typescript@5.4.5) vite-node: specifier: ^3.2.2 version: 3.2.4(@types/node@20.11.30)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.3)(yaml@2.8.0) @@ -9548,16 +9548,16 @@ packages: resolution: {integrity: sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==} engines: {node: '>=10.12.0'} - valibot@0.41.0: - resolution: {integrity: sha512-igDBb8CTYr8YTQlOKgaN9nSS0Be7z+WRuaeYqGf3Cjz3aKmSnqEmYnkfVjzIuumGqfHpa3fLIvMEAfhrpqN8ng==} + valibot@1.0.0: + resolution: {integrity: sha512-1Hc0ihzWxBar6NGeZv7fPLY0QuxFMyxwYR2sF1Blu7Wq7EnremwY2W02tit2ij2VJT8HcSkHAQqmFfl77f73Yw==} peerDependencies: typescript: '>=5' peerDependenciesMeta: typescript: optional: true - valibot@1.0.0: - resolution: {integrity: sha512-1Hc0ihzWxBar6NGeZv7fPLY0QuxFMyxwYR2sF1Blu7Wq7EnremwY2W02tit2ij2VJT8HcSkHAQqmFfl77f73Yw==} + valibot@1.1.0: + resolution: {integrity: sha512-Nk8lX30Qhu+9txPYTwM0cFlWLdPFsFr6LblzqIySfbZph9+BFsAHsNvHOymEviUepeIW6KFHzpX8TKhbptBXXw==} peerDependencies: typescript: '>=5' peerDependenciesMeta: @@ -19317,11 +19317,11 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.4 convert-source-map: 1.9.0 - valibot@0.41.0(typescript@5.4.5): + valibot@1.0.0(typescript@5.4.5): optionalDependencies: typescript: 5.4.5 - valibot@1.0.0(typescript@5.4.5): + valibot@1.1.0(typescript@5.4.5): optionalDependencies: typescript: 5.4.5 From 1e707c4c12a7d9a2a55185b10cd9bd505acff4b9 Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Wed, 1 Oct 2025 18:27:05 +0000 Subject: [PATCH 07/16] chore: format --- contributors.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributors.yml b/contributors.yml index 8dc3055ede..c428444f01 100644 --- a/contributors.yml +++ b/contributors.yml @@ -169,6 +169,7 @@ - JackPriceBurns - jacob-briscoe - jacob-ebey +- jadlr - JaffParker - jakkku - JakubDrozd @@ -447,4 +448,3 @@ - zeromask1337 - zheng-chuang - zxTomw -- jadlr From 368e0cb9a10f3c767b904d14b91d6834a81a12bd Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Wed, 1 Oct 2025 14:51:25 -0400 Subject: [PATCH 08/16] Fixtures for HMR & HDR test (#14403) * wip * wip * wip * wip * wip * wip --- integration/helpers/express.ts | 75 +++++ integration/helpers/stream.ts | 29 ++ integration/helpers/templates.ts | 30 ++ integration/vite-hmr-hdr-test.ts | 462 ++++++++++++++++--------------- 4 files changed, 371 insertions(+), 225 deletions(-) create mode 100644 integration/helpers/express.ts create mode 100644 integration/helpers/stream.ts create mode 100644 integration/helpers/templates.ts diff --git a/integration/helpers/express.ts b/integration/helpers/express.ts new file mode 100644 index 0000000000..ff6268c450 --- /dev/null +++ b/integration/helpers/express.ts @@ -0,0 +1,75 @@ +import tsx from "dedent"; + +export function server() { + return tsx` + import { createRequestHandler } from "@react-router/express"; + import express from "express"; + + const port = process.env.PORT ?? 3000 + const hmrPort = process.env.HMR_PORT ?? 3001 + + const app = express(); + + const getLoadContext = () => ({}); + + if (process.env.NODE_ENV === "production") { + app.use( + "/assets", + express.static("build/client/assets", { immutable: true, maxAge: "1y" }) + ); + app.use(express.static("build/client", { maxAge: "1h" })); + app.all("*", createRequestHandler({ + build: await import("./build/index.js"), + getLoadContext, + })); + } else { + const viteDevServer = await import("vite").then( + (vite) => vite.createServer({ + server: { + middlewareMode: true, + hmr: { port: hmrPort }, + }, + }) + ); + app.use(viteDevServer.middlewares); + app.all("*", createRequestHandler({ + build:() => viteDevServer.ssrLoadModule("virtual:react-router/server-build"), + getLoadContext, + })); + } + + app.listen(port, () => console.log('http://localhost:' + port)); + `; +} + +export function rsc() { + return tsx` + import { createRequestListener } from "@mjackson/node-fetch-server"; + import express from "express"; + + const port = process.env.PORT ?? 3000 + const hmrPort = process.env.HMR_PORT ?? 3001 + + const app = express(); + + if (process.env.NODE_ENV === "production") { + app.use( + "/assets", + express.static("build/client/assets", { immutable: true, maxAge: "1y" }) + ); + app.all("*", createRequestListener((await import("./build/server/index.js")).default)); + } else { + const viteDevServer = await import("vite").then( + (vite) => vite.createServer({ + server: { + middlewareMode: true, + hmr: { port: hmrPort }, + }, + }) + ); + app.use(viteDevServer.middlewares); + } + + app.listen(port, () => console.log('http://localhost:' + port)); + `; +} diff --git a/integration/helpers/stream.ts b/integration/helpers/stream.ts new file mode 100644 index 0000000000..2b9fe49c49 --- /dev/null +++ b/integration/helpers/stream.ts @@ -0,0 +1,29 @@ +import type { Readable } from "node:stream"; + +export async function match( + stream: Readable, + pattern: string | RegExp, + options: { + /** Measured in ms */ + timeout?: number; + } = {}, +): Promise { + // Prepare error outside of promise so that stacktrace points to caller of `matchLine` + const timeoutError = new Error( + `Timed out - Could not find pattern: ${pattern}`, + ); + return new Promise(async (resolve, reject) => { + const timeout = setTimeout( + () => reject(timeoutError), + options.timeout ?? 10_000, + ); + stream.on("data", (data) => { + const line: string = data.toString(); + const matches = line.match(pattern); + if (matches) { + resolve(matches); + clearTimeout(timeout); + } + }); + }); +} diff --git a/integration/helpers/templates.ts b/integration/helpers/templates.ts new file mode 100644 index 0000000000..6e580c5ba9 --- /dev/null +++ b/integration/helpers/templates.ts @@ -0,0 +1,30 @@ +const templates = [ + // Vite Major templates + { name: "vite-5-template", displayName: "Vite 5" }, + { name: "vite-6-template", displayName: "Vite 6" }, + { name: "vite-7-beta-template", displayName: "Vite 7 Beta" }, + { name: "vite-rolldown-template", displayName: "Vite Rolldown" }, + + // RSC templates + { name: "rsc-vite", displayName: "RSC (Vite)" }, + { name: "rsc-parcel", displayName: "RSC (Parcel)" }, + { name: "rsc-vite-framework", displayName: "RSC Framework" }, + + // Cloudflare + // { name: "cloudflare-dev-proxy-template", displayName: "Cloudflare Dev Proxy" }, + { name: "vite-plugin-cloudflare-template", displayName: "Cloudflare" }, +] as const; + +export type Template = (typeof templates)[number]; + +export function getTemplates(names?: Array) { + if (names === undefined) return templates; + return templates.filter(({ name }) => names.includes(name)); +} + +export const viteMajorTemplates = getTemplates([ + "vite-5-template", + "vite-6-template", + "vite-7-beta-template", + "vite-rolldown-template", +]); diff --git a/integration/vite-hmr-hdr-test.ts b/integration/vite-hmr-hdr-test.ts index 59bab4ff2a..549587229e 100644 --- a/integration/vite-hmr-hdr-test.ts +++ b/integration/vite-hmr-hdr-test.ts @@ -1,155 +1,164 @@ -import fs from "node:fs/promises"; -import path from "node:path"; -import type { Page, PlaywrightWorkerOptions } from "@playwright/test"; +import type { Page } from "@playwright/test"; import { expect } from "@playwright/test"; +import getPort from "get-port"; +import dedent from "dedent"; -import type { Files, TemplateName } from "./helpers/vite.js"; -import { - test, - createEditor, - EXPRESS_SERVER, - viteConfig, - viteMajorTemplates, -} from "./helpers/vite.js"; +import * as Express from "./helpers/express"; +import { test } from "./helpers/fixtures"; +import * as Stream from "./helpers/stream"; +import { viteMajorTemplates, getTemplates } from "./helpers/templates"; + +const tsx = dedent; +const mdx = dedent; const templates = [ ...viteMajorTemplates, - { - templateName: "rsc-vite-framework", - templateDisplayName: "RSC Framework Mode", - }, -] as const satisfies ReadonlyArray<{ - templateName: TemplateName; - templateDisplayName: string; -}>; - -const indexRoute = ` - // imports - import { useState, useEffect } from "react"; - - export const meta = () => [{ title: "HMR updated title: 0" }] - - // loader - - export default function IndexRoute() { - // hooks - const [mounted, setMounted] = useState(false); - useEffect(() => { - setMounted(true); - }, []); - - return ( -
-

Index

- -

Mounted: {mounted ? "yes" : "no"}

-

HMR updated: 0

- {/* elements */} -
- ); - } -`; - -test.describe("Vite HMR & HDR", () => { - templates.forEach(({ templateName, templateDisplayName }) => { - test.describe(templateDisplayName, () => { - test("vite dev", async ({ page, browserName, dev }) => { - let files: Files = async ({ port }) => ({ - "vite.config.js": await viteConfig.basic({ port, templateName }), - "app/routes/_index.tsx": indexRoute, - }); - let { cwd, port } = await dev(files, templateName); - await workflow({ templateName, page, browserName, cwd, port }); + ...getTemplates(["rsc-vite-framework"]), +]; + +templates.forEach((template) => { + const isRsc = template.name.startsWith("rsc-"); + + test.describe(`${template.displayName} - HMR & HDR`, () => { + test.use({ + template: template.name, + files: { + "app/routes/_index.tsx": tsx` + // imports + import { useState, useEffect } from "react"; + + export const meta = () => [{ title: "HMR updated title: 0" }] + + // loader + + export default function IndexRoute() { + // hooks + const [mounted, setMounted] = useState(false); + useEffect(() => { + setMounted(true); + }, []); + + return ( +
+

Index

+ +

Mounted: {mounted ? "yes" : "no"}

+

HMR updated: 0

+ {/* elements */} +
+ ); + } + `, + }, + }); + + test("vite dev", async ({ page, edit, $ }) => { + const port = await getPort(); + const url = `http://localhost:${port}`; + + const dev = $(`pnpm dev --port ${port}`); + await Stream.match(dev.stdout, url); + + await workflow({ isRsc, page, edit, url }); + }); + + test("express", async ({ page, edit, $ }) => { + await edit({ + "server.mjs": isRsc ? Express.rsc() : Express.server(), }); - test("express", async ({ page, browserName, customDev }) => { - let files: Files = async ({ port }) => ({ - "vite.config.js": await viteConfig.basic({ port, templateName }), - "server.mjs": EXPRESS_SERVER({ port, templateName }), - "app/routes/_index.tsx": indexRoute, - }); - let { cwd, port } = await customDev(files, templateName); - await workflow({ templateName, page, browserName, cwd, port }); + await $("pnpm build"); + + const port = await getPort(); + const url = `http://localhost:${port}`; + + const server = $("node server.mjs", { + env: { + PORT: String(port), + HMR_PORT: String(await getPort()), + }, }); + await Stream.match(server.stdout, url); - test("mdx", async ({ page, dev }) => { - test.skip(templateName.includes("rsc"), "RSC is not supported"); - let files: Files = async ({ port }) => ({ - "vite.config.ts": ` - import { defineConfig } from "vite"; - import { reactRouter } from "@react-router/dev/vite"; - import mdx from "@mdx-js/rollup"; - - export default defineConfig({ - ${await viteConfig.server({ port })} - plugins: [ - mdx(), - reactRouter(), - ], - }); - `, - "app/component.tsx": ` - import {useState} from "react"; - - export const Counter = () => { - const [count, setCount] = useState(0); - return - } - `, - "app/routes/mdx.mdx": ` - import { Counter } from "../component"; - - # MDX Title (HMR: 0) - - - `, - }); - - let { port, cwd } = await dev(files, templateName); - let edit = createEditor(cwd); - await page.goto(`http://localhost:${port}/mdx`, { - waitUntil: "networkidle", - }); - - await expect(page.locator("h1")).toHaveText("MDX Title (HMR: 0)"); - let button = page.locator("button"); - await expect(button).toHaveText("Count: 0"); - await button.click(); - await expect(button).toHaveText("Count: 1"); - - await edit("app/routes/mdx.mdx", (contents) => - contents.replace("(HMR: 0)", "(HMR: 1)"), - ); - await page.waitForLoadState("networkidle"); + await workflow({ isRsc, page, edit, url }); + }); + + test("mdx", async ({ page, edit, $ }) => { + test.skip(template.name.includes("rsc"), "RSC is not supported"); + + await edit({ + "vite.config.ts": tsx` + import { defineConfig } from "vite"; + import { reactRouter } from "@react-router/dev/vite"; + import mdx from "@mdx-js/rollup"; + + export default defineConfig({ + plugins: [ + mdx(), + reactRouter(), + ], + }); + `, + "app/component.tsx": tsx` + import {useState} from "react"; + + export const Counter = () => { + const [count, setCount] = useState(0); + return + } + `, + "app/routes/mdx.mdx": mdx` + import { Counter } from "../component"; + + # MDX Title (HMR: 0) + + + `, + }); + + const port = await getPort(); + const url = `http://localhost:${port}`; - await expect(page.locator("h1")).toHaveText("MDX Title (HMR: 1)"); - await expect(page.locator("button")).toHaveText("Count: 1"); + const dev = $(`pnpm dev --port ${port}`); + await Stream.match(dev.stdout, url); - expect(page.errors).toEqual([]); + await page.goto(url + "/mdx", { waitUntil: "networkidle" }); + + await expect(page.locator("h1")).toHaveText("MDX Title (HMR: 0)"); + let button = page.locator("button"); + await expect(button).toHaveText("Count: 0"); + await button.click(); + await expect(button).toHaveText("Count: 1"); + + await edit({ + "app/routes/mdx.mdx": (contents) => + contents.replace("(HMR: 0)", "(HMR: 1)"), }); + await page.waitForLoadState("networkidle"); + + await expect(page.locator("h1")).toHaveText("MDX Title (HMR: 1)"); + await expect(page.locator("button")).toHaveText("Count: 1"); + + expect(page.errors).toEqual([]); }); }); }); async function workflow({ - templateName, + isRsc, page, - browserName, - cwd, - port, + edit, + url, }: { - templateName: TemplateName; + isRsc: boolean; page: Page; - browserName: PlaywrightWorkerOptions["browserName"]; - cwd: string; - port: number; + edit: ( + edits: Record string)>, + ) => Promise; + url: string; }) { - let edit = createEditor(cwd); - // setup: initial render - await page.goto(`http://localhost:${port}/`, { - waitUntil: "networkidle", - }); + await page.goto(url, { waitUntil: "networkidle" }); await expect(page.locator("#index [data-title]")).toHaveText("Index"); // setup: hydration @@ -164,15 +173,16 @@ async function workflow({ await expect(hmrStatus).toHaveText("HMR updated: 0"); let input = page.locator("#index input"); await expect(input).toBeVisible(); - await input.type("stateful"); + await input.fill("stateful"); expect(page.errors).toEqual([]); // route: HMR - await edit("app/routes/_index.tsx", (contents) => - contents - .replace("HMR updated title: 0", "HMR updated title: 1") - .replace("HMR updated: 0", "HMR updated: 1"), - ); + await edit({ + "app/routes/_index.tsx": (contents) => + contents + .replace("HMR updated title: 0", "HMR updated title: 1") + .replace("HMR updated: 0", "HMR updated: 1"), + }); await page.waitForLoadState("networkidle"); await expect(page).toHaveTitle("HMR updated title: 1"); @@ -181,31 +191,33 @@ async function workflow({ expect(page.errors).toEqual([]); // route: add loader - await edit("app/routes/_index.tsx", (contents) => - contents - .replace( - "// imports", - `// imports\nimport { useLoaderData } from "react-router"`, - ) - .replace( - "// loader", - `// loader\nexport const loader = () => ({ message: "HDR updated: 0" });`, - ) - .replace( - "// hooks", - "// hooks\nconst { message } = useLoaderData();", - ) - .replace( - "{/* elements */}", - `{/* elements */}\n

{message}

`, - ), - ); + await edit({ + "app/routes/_index.tsx": (contents) => + contents + .replace( + "// imports", + `// imports\nimport { useLoaderData } from "react-router"`, + ) + .replace( + "// loader", + `// loader\nexport const loader = () => ({ message: "HDR updated: 0" });`, + ) + .replace( + "// hooks", + "// hooks\nconst { message } = useLoaderData();", + ) + .replace( + "{/* elements */}", + `{/* elements */}\n

{message}

`, + ), + }); await page.waitForLoadState("networkidle"); let hdrStatus = page.locator("#index [data-hdr]"); await expect(hdrStatus).toHaveText("HDR updated: 0"); + // React Fast Refresh cannot preserve state for a component when hooks are added or removed await expect(input).toHaveValue(""); - await input.type("stateful"); + await input.fill("stateful"); expect(page.errors.length).toBeGreaterThan(0); expect( // When adding a loader, a harmless error is logged to the browser console. @@ -220,19 +232,21 @@ async function workflow({ page.errors = []; // route: HDR - await edit("app/routes/_index.tsx", (contents) => - contents.replace("HDR updated: 0", "HDR updated: 1"), - ); + await edit({ + "app/routes/_index.tsx": (contents) => + contents.replace("HDR updated: 0", "HDR updated: 1"), + }); await page.waitForLoadState("networkidle"); await expect(hdrStatus).toHaveText("HDR updated: 1"); await expect(input).toHaveValue("stateful"); // route: HMR + HDR - await edit("app/routes/_index.tsx", (contents) => - contents - .replace("HMR updated: 1", "HMR updated: 2") - .replace("HDR updated: 1", "HDR updated: 2"), - ); + await edit({ + "app/routes/_index.tsx": (contents) => + contents + .replace("HMR updated: 1", "HMR updated: 2") + .replace("HDR updated: 1", "HDR updated: 2"), + }); await page.waitForLoadState("networkidle"); await expect(hmrStatus).toHaveText("HMR updated: 2"); await expect(hdrStatus).toHaveText("HDR updated: 2"); @@ -240,23 +254,20 @@ async function workflow({ expect(page.errors).toEqual([]); // create new non-route component module - await fs.writeFile( - path.join(cwd, "app/component.tsx"), - String.raw` - export function MyComponent() { - return

Component HMR: 0

; - } + await edit({ + "app/component.tsx": tsx` + export function MyComponent() { + return

Component HMR: 0

; + } `, - "utf8", - ); - await edit("app/routes/_index.tsx", (contents) => - contents - .replace( - "// imports", - `// imports\nimport { MyComponent } from "../component";`, - ) - .replace("{/* elements */}", "{/* elements */}\n"), - ); + "app/routes/_index.tsx": (contents) => + contents + .replace( + "// imports", + `// imports\nimport { MyComponent } from "../component";`, + ) + .replace("{/* elements */}", "{/* elements */}\n"), + }); await page.waitForLoadState("networkidle"); let component = page.locator("#index [data-component]"); await expect(component).toBeVisible(); @@ -265,57 +276,53 @@ async function workflow({ expect(page.errors).toEqual([]); // non-route: HMR - await edit("app/component.tsx", (contents) => - contents.replace("Component HMR: 0", "Component HMR: 1"), - ); + await edit({ + "app/component.tsx": (contents) => + contents.replace("Component HMR: 0", "Component HMR: 1"), + }); await page.waitForLoadState("networkidle"); await expect(component).toHaveText("Component HMR: 1"); await expect(input).toHaveValue("stateful"); expect(page.errors).toEqual([]); // create new non-route server module - await fs.writeFile( - path.join(cwd, "app/indirect-hdr-dep.ts"), - String.raw`export const indirect = "indirect 0"`, - "utf8", - ); - await fs.writeFile( - path.join(cwd, "app/direct-hdr-dep.ts"), - String.raw` + await edit({ + "app/indirect-hdr-dep.ts": tsx`export const indirect = "indirect 0"`, + "app/direct-hdr-dep.ts": tsx` import { indirect } from "./indirect-hdr-dep" export const direct = "direct 0 & " + indirect `, - "utf8", - ); - await edit("app/routes/_index.tsx", (contents) => - contents - .replace( - "// imports", - `// imports\nimport { direct } from "../direct-hdr-dep"`, - ) - .replace( - `{ message: "HDR updated: 2" }`, - `{ message: "HDR updated: " + direct }`, - ), - ); + "app/routes/_index.tsx": (contents) => + contents + .replace( + "// imports", + `// imports\nimport { direct } from "../direct-hdr-dep"`, + ) + .replace( + `{ message: "HDR updated: 2" }`, + `{ message: "HDR updated: " + direct }`, + ), + }); await page.waitForLoadState("networkidle"); await expect(hdrStatus).toHaveText("HDR updated: direct 0 & indirect 0"); await expect(input).toHaveValue("stateful"); expect(page.errors).toEqual([]); // non-route: HDR for direct dependency - await edit("app/direct-hdr-dep.ts", (contents) => - contents.replace("direct 0 &", "direct 1 &"), - ); + await edit({ + "app/direct-hdr-dep.ts": (contents) => + contents.replace("direct 0 &", "direct 1 &"), + }); await page.waitForLoadState("networkidle"); await expect(hdrStatus).toHaveText("HDR updated: direct 1 & indirect 0"); await expect(input).toHaveValue("stateful"); expect(page.errors).toEqual([]); // non-route: HDR for indirect dependency - await edit("app/indirect-hdr-dep.ts", (contents) => - contents.replace("indirect 0", "indirect 1"), - ); + await edit({ + "app/indirect-hdr-dep.ts": (contents) => + contents.replace("indirect 0", "indirect 1"), + }); await page.waitForLoadState("networkidle"); await expect(hdrStatus).toHaveText("HDR updated: direct 1 & indirect 1"); await expect(input).toHaveValue("stateful"); @@ -323,20 +330,24 @@ async function workflow({ // everything everywhere all at once await Promise.all([ - edit("app/routes/_index.tsx", (contents) => - contents - .replace("HMR updated: 2", "HMR updated: 3") - .replace("HDR updated: ", "HDR updated: route & "), - ), - edit("app/component.tsx", (contents) => - contents.replace("Component HMR: 1", "Component HMR: 2"), - ), - edit("app/direct-hdr-dep.ts", (contents) => - contents.replace("direct 1 &", "direct 2 &"), - ), - edit("app/indirect-hdr-dep.ts", (contents) => - contents.replace("indirect 1", "indirect 2"), - ), + edit({ + "app/routes/_index.tsx": (contents) => + contents + .replace("HMR updated: 2", "HMR updated: 3") + .replace("HDR updated: ", "HDR updated: route & "), + }), + edit({ + "app/component.tsx": (contents) => + contents.replace("Component HMR: 1", "Component HMR: 2"), + }), + edit({ + "app/direct-hdr-dep.ts": (contents) => + contents.replace("direct 1 &", "direct 2 &"), + }), + edit({ + "app/indirect-hdr-dep.ts": (contents) => + contents.replace("indirect 1", "indirect 2"), + }), ]); await page.waitForLoadState("networkidle"); await expect(hmrStatus).toHaveText("HMR updated: 3"); @@ -345,8 +356,9 @@ async function workflow({ "HDR updated: route & direct 2 & indirect 2", ); // TODO: Investigate why this is flaky in CI for RSC Framework Mode - if (!templateName.includes("rsc")) { + if (isRsc) { await expect(input).toHaveValue("stateful"); } + expect(page.errors).toEqual([]); } From 45bad2be88635074508c5de0a9742ce5d0edd13f Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Fri, 3 Oct 2025 15:36:04 -0400 Subject: [PATCH 09/16] unstable_useRoute (#14407) * typegen: register lookup from route ID to route module * useRoute: type-safe data access for other routes on the page * useRoute: use current route ID when route ID is not provided * wip * useRoute: better types - no route ID -> { loaderData: unknown, actionData: unknown } - actionData gets `| undefined` added to it - `root` route is guaranteed to exist * useRoute: mark as unstable * useRoute: change `loaderData` to be optional since it could be accessed within an error boundary when the loader itself failed * changeset * updated changeset * useRoute: testing --- .changeset/six-lobsters-think.md | 104 +++++++++++++++ integration/use-route-test.ts | 118 ++++++++++++++++++ packages/react-router-dev/typegen/generate.ts | 28 ++++- packages/react-router/index.ts | 1 + packages/react-router/lib/hooks.tsx | 44 ++++++- packages/react-router/lib/types/register.ts | 10 ++ 6 files changed, 303 insertions(+), 2 deletions(-) create mode 100644 .changeset/six-lobsters-think.md create mode 100644 integration/use-route-test.ts diff --git a/.changeset/six-lobsters-think.md b/.changeset/six-lobsters-think.md new file mode 100644 index 0000000000..3db89c1017 --- /dev/null +++ b/.changeset/six-lobsters-think.md @@ -0,0 +1,104 @@ +--- +"@react-router/dev": patch +"react-router": patch +--- + +New (unstable) `useRoute` hook for accessing data from specific routes + +For example, let's say you have an `admin` route somewhere in your app and you want any child routes of `admin` to all have access to the `loaderData` and `actionData` from `admin.` + +```tsx +// app/routes/admin.tsx +import { Outlet } from "react-router"; + +export const loader = () => ({ message: "Hello, loader!" }); + +export const action = () => ({ count: 1 }); + +export default function Component() { + return ( +
+ {/* ... */} + + {/* ... */} +
+ ); +} +``` + +You might even want to create a reusable widget that all of the routes nested under `admin` could use: + +```tsx +import { unstable_useRoute as useRoute } from "react-router"; + +export function AdminWidget() { + // How to get `message` and `count` from `admin` route? +} +``` + +In framework mode, `useRoute` knows all your app's routes and gives you TS errors when invalid route IDs are passed in: + +```tsx +export function AdminWidget() { + const admin = useRoute("routes/dmin"); + // ^^^^^^^^^^^ +} +``` + +`useRoute` returns `undefined` if the route is not part of the current page: + +```tsx +export function AdminWidget() { + const admin = useRoute("routes/admin"); + if (!admin) { + throw new Error(`AdminWidget used outside of "routes/admin"`); + } +} +``` + +Note: the `root` route is the exception since it is guaranteed to be part of the current page. +As a result, `useRoute` never returns `undefined` for `root`. + +`loaderData` and `actionData` are marked as optional since they could be accessed before the `action` is triggered or after the `loader` threw an error: + +```tsx +export function AdminWidget() { + const admin = useRoute("routes/admin"); + if (!admin) { + throw new Error(`AdminWidget used outside of "routes/admin"`); + } + const { loaderData, actionData } = admin; + console.log(loaderData); + // ^? { message: string } | undefined + console.log(actionData); + // ^? { count: number } | undefined +} +``` + +If instead of a specific route, you wanted access to the _current_ route's `loaderData` and `actionData`, you can call `useRoute` without arguments: + +```tsx +export function AdminWidget() { + const currentRoute = useRoute(); + currentRoute.loaderData; + currentRoute.actionData; +} +``` + +This usage is equivalent to calling `useLoaderData` and `useActionData`, but consolidates all route data access into one hook: `useRoute`. + +Note: when calling `useRoute()` (without a route ID), TS has no way to know which route is the current route. +As a result, `loaderData` and `actionData` are typed as `unknown`. +If you want more type-safety, you can either narrow the type yourself with something like `zod` or you can refactor your app to pass down typed props to your `AdminWidget`: + +```tsx +export function AdminWidget({ + message, + count, +}: { + message: string; + count: number; +}) { + /* ... */ +} +``` diff --git a/integration/use-route-test.ts b/integration/use-route-test.ts new file mode 100644 index 0000000000..44b3bca2cb --- /dev/null +++ b/integration/use-route-test.ts @@ -0,0 +1,118 @@ +import tsx from "dedent"; +import { expect } from "@playwright/test"; + +import { test } from "./helpers/fixtures"; +import * as Stream from "./helpers/stream"; +import getPort from "get-port"; + +test.use({ + files: { + "app/expect-type.ts": tsx` + export type Expect = T + + export type Equal = + (() => T extends X ? 1 : 2) extends + (() => T extends Y ? 1 : 2) ? true : false + `, + "app/routes.ts": tsx` + import { type RouteConfig, route } from "@react-router/dev/routes" + + export default [ + route("parent", "routes/parent.tsx", [ + route("current", "routes/current.tsx") + ]), + route("other", "routes/other.tsx"), + ] satisfies RouteConfig + `, + "app/root.tsx": tsx` + import { Outlet } from "react-router" + + export const loader = () => ({ rootLoader: "root/loader" }) + export const action = () => ({ rootAction: "root/action" }) + + export default function Component() { + return ( + <> +

Root

+ + + ) + } + `, + "app/routes/parent.tsx": tsx` + import { Outlet } from "react-router" + + export const loader = () => ({ parentLoader: "parent/loader" }) + export const action = () => ({ parentAction: "parent/action" }) + + export default function Component() { + return ( + <> +

Parent

+ + + ) + } + `, + "app/routes/current.tsx": tsx` + import { unstable_useRoute as useRoute } from "react-router" + + import type { Expect, Equal } from "../expect-type" + + export const loader = () => ({ currentLoader: "current/loader" }) + export const action = () => ({ currentAction: "current/action" }) + + export default function Component() { + const current = useRoute() + type Test1 = Expect> + + const root = useRoute("root") + type Test2 = Expect> + + const parent = useRoute("routes/parent") + type Test3 = Expect> + + const other = useRoute("routes/other") + type Test4 = Expect> + + return ( + <> +
{root.loaderData?.rootLoader}
+
{parent?.loaderData?.parentLoader}
+ {/* @ts-expect-error */} +
{current?.loaderData?.currentLoader}
+
{other === undefined ? "undefined" : "something else"}
+ + ) + } + `, + "app/routes/other.tsx": tsx` + export const loader = () => ({ otherLoader: "other/loader" }) + export const action = () => ({ otherAction: "other/action" }) + + export default function Component() { + return

Other

+ } + `, + }, +}); + +test("useRoute", async ({ $, page }) => { + await $("pnpm typecheck"); + + const port = await getPort(); + const url = `http://localhost:${port}`; + + const dev = $(`pnpm dev --port ${port}`); + await Stream.match(dev.stdout, url); + + await page.goto(url + "/parent/current", { waitUntil: "networkidle" }); + + await expect(page.locator("[data-root]")).toHaveText("root/loader"); + + await expect(page.locator("[data-parent]")).toHaveText("parent/loader"); + + await expect(page.locator("[data-current]")).toHaveText("current/loader"); + + await expect(page.locator("[data-other]")).toHaveText("undefined"); +}); diff --git a/packages/react-router-dev/typegen/generate.ts b/packages/react-router-dev/typegen/generate.ts index 74df004ee6..dcab03fd27 100644 --- a/packages/react-router-dev/typegen/generate.ts +++ b/packages/react-router-dev/typegen/generate.ts @@ -105,13 +105,16 @@ export function generateRoutes(ctx: Context): Array { interface Register { pages: Pages routeFiles: RouteFiles + routeModules: RouteModules } } ` + "\n\n" + Babel.generate(pagesType(allPages)).code + "\n\n" + - Babel.generate(routeFilesType({ fileToRoutes, routeToPages })).code, + Babel.generate(routeFilesType({ fileToRoutes, routeToPages })).code + + "\n\n" + + Babel.generate(routeModulesType(ctx)).code, }; // **/+types/*.ts @@ -193,6 +196,29 @@ function routeFilesType({ ); } +function routeModulesType(ctx: Context) { + return t.tsTypeAliasDeclaration( + t.identifier("RouteModules"), + null, + t.tsTypeLiteral( + Object.values(ctx.config.routes).map((route) => + t.tsPropertySignature( + t.stringLiteral(route.id), + t.tsTypeAnnotation( + t.tsTypeQuery( + t.tsImportType( + t.stringLiteral( + `./${Path.relative(ctx.rootDirectory, ctx.config.appDirectory)}/${route.file}`, + ), + ), + ), + ), + ), + ), + ), + ); +} + function isInAppDirectory(ctx: Context, routeFile: string): boolean { const path = Path.resolve(ctx.config.appDirectory, routeFile); return path.startsWith(ctx.config.appDirectory); diff --git a/packages/react-router/index.ts b/packages/react-router/index.ts index ed1418c11e..fe3c25cea6 100644 --- a/packages/react-router/index.ts +++ b/packages/react-router/index.ts @@ -144,6 +144,7 @@ export { useRouteError, useRouteLoaderData, useRoutes, + useRoute as unstable_useRoute, } from "./lib/hooks"; // Expose old RR DOM API diff --git a/packages/react-router/lib/hooks.tsx b/packages/react-router/lib/hooks.tsx index d3f969ef02..259f75ace4 100644 --- a/packages/react-router/lib/hooks.tsx +++ b/packages/react-router/lib/hooks.tsx @@ -50,8 +50,13 @@ import { resolveTo, stripBasename, } from "./router/utils"; -import type { SerializeFrom } from "./types/route-data"; +import type { + GetActionData, + GetLoaderData, + SerializeFrom, +} from "./types/route-data"; import type { unstable_ClientOnErrorFunction } from "./components"; +import type { RouteModules } from "./types/register"; /** * Resolves a URL against the current {@link Location}. @@ -1282,6 +1287,7 @@ enum DataRouterStateHook { UseRevalidator = "useRevalidator", UseNavigateStable = "useNavigate", UseRouteId = "useRouteId", + UseRoute = "useRoute", } function getDataRouterConsoleError( @@ -1838,3 +1844,39 @@ function warningOnce(key: string, cond: boolean, message: string) { warning(false, message); } } + +type UseRouteArgs = [] | [routeId: keyof RouteModules]; + +// prettier-ignore +type UseRouteResult = + Args extends [] ? UseRoute : + Args extends ["root"] ? UseRoute<"root"> : + Args extends [infer RouteId extends keyof RouteModules] ? UseRoute | undefined : + never; + +type UseRoute = { + loaderData: RouteId extends keyof RouteModules + ? GetLoaderData | undefined + : unknown; + actionData: RouteId extends keyof RouteModules + ? GetActionData | undefined + : unknown; +}; + +export function useRoute( + ...args: Args +): UseRouteResult { + const currentRouteId: keyof RouteModules = useCurrentRouteId( + DataRouterStateHook.UseRoute, + ); + const id: keyof RouteModules = args[0] ?? currentRouteId; + + const state = useDataRouterState(DataRouterStateHook.UseRouteLoaderData); + const route = state.matches.find(({ route }) => route.id === id); + + if (route === undefined) return undefined as UseRouteResult; + return { + loaderData: state.loaderData[id], + actionData: state.actionData?.[id], + } as UseRouteResult; +} diff --git a/packages/react-router/lib/types/register.ts b/packages/react-router/lib/types/register.ts index a5bbd7f92b..51dbacf4a4 100644 --- a/packages/react-router/lib/types/register.ts +++ b/packages/react-router/lib/types/register.ts @@ -1,3 +1,5 @@ +import type { RouteModule } from "./route-module"; + /** * Apps can use this interface to "register" app-wide types for React Router via interface declaration merging and module augmentation. * React Router should handle this for you via type generation. @@ -7,6 +9,7 @@ export interface Register { // pages // routeFiles + // routeModules } // pages @@ -25,3 +28,10 @@ export type RouteFiles = Register extends { } ? Registered : AnyRouteFiles; + +type AnyRouteModules = Record; +export type RouteModules = Register extends { + routeModules: infer Registered extends AnyRouteModules; +} + ? Registered + : AnyRouteModules; From 6ff0bb35db54535b3436375784fd40225a3664c2 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 6 Oct 2025 11:32:17 -0400 Subject: [PATCH 10/16] Add some validation for session IDs (#14426) --- .changeset/quick-eels-join.md | 5 ++++ .../__tests__/sessions-test.ts | 26 +++++++++++++++++++ .../react-router-node/sessions/fileStorage.ts | 21 +++++++++++++-- 3 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 .changeset/quick-eels-join.md diff --git a/.changeset/quick-eels-join.md b/.changeset/quick-eels-join.md new file mode 100644 index 0000000000..c9116d9304 --- /dev/null +++ b/.changeset/quick-eels-join.md @@ -0,0 +1,5 @@ +--- +"@react-router/node": patch +--- + +Validate format of incoming session ids diff --git a/packages/react-router-node/__tests__/sessions-test.ts b/packages/react-router-node/__tests__/sessions-test.ts index 9a72c5b5ec..95446861a5 100644 --- a/packages/react-router-node/__tests__/sessions-test.ts +++ b/packages/react-router-node/__tests__/sessions-test.ts @@ -55,6 +55,32 @@ describe("File session storage", () => { expect(session.get("user")).toBeUndefined(); }); + it("returns an empty session for invalid session ids", async () => { + let spy = jest.spyOn(console, "warn").mockImplementation(() => {}); + let { getSession, commitSession } = createFileSessionStorage({ + dir, + }); + + let cookie = `__session=${btoa(JSON.stringify("0123456789abcdef"))}`; + let session = await getSession(cookie); + session.set("user", "mjackson"); + expect(session.get("user")).toBe("mjackson"); + let setCookie = await commitSession(session); + session = await getSession(getCookieFromSetCookie(setCookie)); + expect(session.get("user")).toBe("mjackson"); + + cookie = `__session=${btoa(JSON.stringify("0123456789abcdeg"))}`; + session = await getSession(cookie); + session.set("user", "mjackson"); + expect(session.get("user")).toBe("mjackson"); + debugger; + setCookie = await commitSession(session); + session = await getSession(getCookieFromSetCookie(setCookie)); + expect(session.get("user")).toBeUndefined(); + + spy.mockRestore(); + }); + it("doesn't destroy the entire session directory when destroying an empty file session", async () => { let { getSession, destroySession } = createFileSessionStorage({ dir, diff --git a/packages/react-router-node/sessions/fileStorage.ts b/packages/react-router-node/sessions/fileStorage.ts index 0df4bacb4d..3d2c30ae41 100644 --- a/packages/react-router-node/sessions/fileStorage.ts +++ b/packages/react-router-node/sessions/fileStorage.ts @@ -47,6 +47,9 @@ export function createFileSessionStorage({ try { let file = getFile(dir, id); + if (!file) { + throw new Error("Error generating session"); + } await fsp.mkdir(path.dirname(file), { recursive: true }); await fsp.writeFile(file, content, { encoding: "utf-8", flag: "wx" }); return id; @@ -58,6 +61,9 @@ export function createFileSessionStorage({ async readData(id) { try { let file = getFile(dir, id); + if (!file) { + return null; + } let content = JSON.parse(await fsp.readFile(file, "utf-8")); let data = content.data; let expires = @@ -81,6 +87,9 @@ export function createFileSessionStorage({ async updateData(id, data, expires) { let content = JSON.stringify({ data, expires }); let file = getFile(dir, id); + if (!file) { + return; + } await fsp.mkdir(path.dirname(file), { recursive: true }); await fsp.writeFile(file, content, "utf-8"); }, @@ -90,8 +99,12 @@ export function createFileSessionStorage({ if (!id) { return; } + let file = getFile(dir, id); + if (!file) { + return; + } try { - await fsp.unlink(getFile(dir, id)); + await fsp.unlink(file); } catch (error: any) { if (error.code !== "ENOENT") throw error; } @@ -99,7 +112,11 @@ export function createFileSessionStorage({ }); } -export function getFile(dir: string, id: string): string { +export function getFile(dir: string, id: string): string | null { + if (!/^[0-9a-f]{16}$/i.test(id)) { + return null; + } + // Divide the session id up into a directory (first 2 bytes) and filename // (remaining 6 bytes) to reduce the chance of having very large directories, // which should speed up file access. This is a maximum of 2^16 directories, From 8404868d46d7f64f925dc5466d7a9c662def7cf8 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 6 Oct 2025 11:54:14 -0400 Subject: [PATCH 11/16] Enter prerelease mode --- .changeset/pre.json | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .changeset/pre.json diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 0000000000..5292247897 --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,40 @@ +{ + "mode": "pre", + "tag": "pre", + "initialVersions": { + "integration": "0.0.0", + "integration-cloudflare-dev-proxy-template": "0.0.0", + "integration-rsc-parcel": "0.0.0", + "integration-rsc-vite": "0.0.0", + "integration-rsc-vite-framework": "0.0.0", + "integration-vite-5-template": "0.0.0", + "integration-vite-6-template": "0.0.0", + "integration-vite-7-beta-template": "0.0.0", + "integration-vite-plugin-cloudflare-template": "0.0.0", + "integration-vite-rolldown-template": "0.0.0", + "create-react-router": "7.9.3", + "react-router": "7.9.3", + "@react-router/architect": "7.9.3", + "@react-router/cloudflare": "7.9.3", + "@react-router/dev": "7.9.3", + "react-router-dom": "7.9.3", + "@react-router/express": "7.9.3", + "@react-router/fs-routes": "7.9.3", + "@react-router/node": "7.9.3", + "@react-router/remix-routes-option-adapter": "7.9.3", + "@react-router/serve": "7.9.3", + "@playground/framework": "0.0.0", + "@playground/framework-express": "0.0.0", + "@playground/framework-rolldown-vite": "0.0.0", + "@playground/framework-spa": "0.0.0", + "@playground/framework-vite-5": "0.0.0", + "@playground/framework-vite-7-beta": "0.0.0", + "@playground/rsc-parcel": "0.0.0", + "@playground/rsc-vite": "0.0.0", + "@playground/rsc-vite-framework": "0.0.0", + "@playground/split-route-modules": "0.0.0", + "@playground/split-route-modules-spa": "0.0.0", + "@playground/vite-plugin-cloudflare": "0.0.0" + }, + "changesets": [] +} From 7a2271e916e50427d726ac976b188c9e1aebc48c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 11:55:17 -0400 Subject: [PATCH 12/16] chore: Update version for release (pre) (#14427) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/pre.json | 7 +- packages/create-react-router/CHANGELOG.md | 2 + packages/create-react-router/package.json | 2 +- packages/react-router-architect/CHANGELOG.md | 8 ++ packages/react-router-architect/package.json | 2 +- packages/react-router-cloudflare/CHANGELOG.md | 7 ++ packages/react-router-cloudflare/package.json | 2 +- packages/react-router-dev/CHANGELOG.md | 110 ++++++++++++++++++ packages/react-router-dev/package.json | 2 +- packages/react-router-dom/CHANGELOG.md | 7 ++ packages/react-router-dom/package.json | 2 +- packages/react-router-express/CHANGELOG.md | 8 ++ packages/react-router-express/package.json | 2 +- packages/react-router-fs-routes/CHANGELOG.md | 7 ++ packages/react-router-fs-routes/package.json | 2 +- packages/react-router-node/CHANGELOG.md | 8 ++ packages/react-router-node/package.json | 2 +- .../CHANGELOG.md | 7 ++ .../package.json | 2 +- packages/react-router-serve/CHANGELOG.md | 9 ++ packages/react-router-serve/package.json | 2 +- packages/react-router/CHANGELOG.md | 105 +++++++++++++++++ packages/react-router/package.json | 2 +- 23 files changed, 295 insertions(+), 12 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 5292247897..370fee1430 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -36,5 +36,10 @@ "@playground/split-route-modules-spa": "0.0.0", "@playground/vite-plugin-cloudflare": "0.0.0" }, - "changesets": [] + "changesets": [ + "breezy-planes-roll", + "green-pens-push", + "quick-eels-join", + "six-lobsters-think" + ] } diff --git a/packages/create-react-router/CHANGELOG.md b/packages/create-react-router/CHANGELOG.md index da45cd74b2..b568ce6a01 100644 --- a/packages/create-react-router/CHANGELOG.md +++ b/packages/create-react-router/CHANGELOG.md @@ -1,5 +1,7 @@ # `create-react-router` +## 7.9.4-pre.0 + ## 7.9.3 ## 7.9.2 diff --git a/packages/create-react-router/package.json b/packages/create-react-router/package.json index f0eab96b1a..12b2b43891 100644 --- a/packages/create-react-router/package.json +++ b/packages/create-react-router/package.json @@ -1,6 +1,6 @@ { "name": "create-react-router", - "version": "7.9.3", + "version": "7.9.4-pre.0", "description": "Create a new React Router app", "homepage": "https://reactrouter.com", "bugs": { diff --git a/packages/react-router-architect/CHANGELOG.md b/packages/react-router-architect/CHANGELOG.md index 5ec74a1162..6b490f824c 100644 --- a/packages/react-router-architect/CHANGELOG.md +++ b/packages/react-router-architect/CHANGELOG.md @@ -1,5 +1,13 @@ # `@react-router/architect` +## 7.9.4-pre.0 + +### Patch Changes + +- Updated dependencies: + - `react-router@7.9.4-pre.0` + - `@react-router/node@7.9.4-pre.0` + ## 7.9.3 ### Patch Changes diff --git a/packages/react-router-architect/package.json b/packages/react-router-architect/package.json index 9b63583c6f..9338237309 100644 --- a/packages/react-router-architect/package.json +++ b/packages/react-router-architect/package.json @@ -1,6 +1,6 @@ { "name": "@react-router/architect", - "version": "7.9.3", + "version": "7.9.4-pre.0", "description": "Architect server request handler for React Router", "bugs": { "url": "https://github.com/remix-run/react-router/issues" diff --git a/packages/react-router-cloudflare/CHANGELOG.md b/packages/react-router-cloudflare/CHANGELOG.md index d74b9aa8d9..514ce4ed59 100644 --- a/packages/react-router-cloudflare/CHANGELOG.md +++ b/packages/react-router-cloudflare/CHANGELOG.md @@ -1,5 +1,12 @@ # `@react-router/cloudflare` +## 7.9.4-pre.0 + +### Patch Changes + +- Updated dependencies: + - `react-router@7.9.4-pre.0` + ## 7.9.3 ### Patch Changes diff --git a/packages/react-router-cloudflare/package.json b/packages/react-router-cloudflare/package.json index 249f43f0f8..af600ec3fe 100644 --- a/packages/react-router-cloudflare/package.json +++ b/packages/react-router-cloudflare/package.json @@ -1,6 +1,6 @@ { "name": "@react-router/cloudflare", - "version": "7.9.3", + "version": "7.9.4-pre.0", "description": "Cloudflare platform abstractions for React Router", "bugs": { "url": "https://github.com/remix-run/react-router/issues" diff --git a/packages/react-router-dev/CHANGELOG.md b/packages/react-router-dev/CHANGELOG.md index 2d0aa8e6df..9639e668e0 100644 --- a/packages/react-router-dev/CHANGELOG.md +++ b/packages/react-router-dev/CHANGELOG.md @@ -1,5 +1,115 @@ # `@react-router/dev` +## 7.9.4-pre.0 + +### Patch Changes + +- Update `valibot` dependency to `^1.1.0` ([#14379](https://github.com/remix-run/react-router/pull/14379)) +- New (unstable) `useRoute` hook for accessing data from specific routes ([#14407](https://github.com/remix-run/react-router/pull/14407)) + + For example, let's say you have an `admin` route somewhere in your app and you want any child routes of `admin` to all have access to the `loaderData` and `actionData` from `admin.` + + ```tsx + // app/routes/admin.tsx + import { Outlet } from "react-router"; + + export const loader = () => ({ message: "Hello, loader!" }); + + export const action = () => ({ count: 1 }); + + export default function Component() { + return ( +
+ {/* ... */} + + {/* ... */} +
+ ); + } + ``` + + You might even want to create a reusable widget that all of the routes nested under `admin` could use: + + ```tsx + import { unstable_useRoute as useRoute } from "react-router"; + + export function AdminWidget() { + // How to get `message` and `count` from `admin` route? + } + ``` + + In framework mode, `useRoute` knows all your app's routes and gives you TS errors when invalid route IDs are passed in: + + ```tsx + export function AdminWidget() { + const admin = useRoute("routes/dmin"); + // ^^^^^^^^^^^ + } + ``` + + `useRoute` returns `undefined` if the route is not part of the current page: + + ```tsx + export function AdminWidget() { + const admin = useRoute("routes/admin"); + if (!admin) { + throw new Error(`AdminWidget used outside of "routes/admin"`); + } + } + ``` + + Note: the `root` route is the exception since it is guaranteed to be part of the current page. + As a result, `useRoute` never returns `undefined` for `root`. + + `loaderData` and `actionData` are marked as optional since they could be accessed before the `action` is triggered or after the `loader` threw an error: + + ```tsx + export function AdminWidget() { + const admin = useRoute("routes/admin"); + if (!admin) { + throw new Error(`AdminWidget used outside of "routes/admin"`); + } + const { loaderData, actionData } = admin; + console.log(loaderData); + // ^? { message: string } | undefined + console.log(actionData); + // ^? { count: number } | undefined + } + ``` + + If instead of a specific route, you wanted access to the _current_ route's `loaderData` and `actionData`, you can call `useRoute` without arguments: + + ```tsx + export function AdminWidget() { + const currentRoute = useRoute(); + currentRoute.loaderData; + currentRoute.actionData; + } + ``` + + This usage is equivalent to calling `useLoaderData` and `useActionData`, but consolidates all route data access into one hook: `useRoute`. + + Note: when calling `useRoute()` (without a route ID), TS has no way to know which route is the current route. + As a result, `loaderData` and `actionData` are typed as `unknown`. + If you want more type-safety, you can either narrow the type yourself with something like `zod` or you can refactor your app to pass down typed props to your `AdminWidget`: + + ```tsx + export function AdminWidget({ + message, + count, + }: { + message: string; + count: number; + }) { + /* ... */ + } + ``` + +- Updated dependencies: + - `react-router@7.9.4-pre.0` + - `@react-router/node@7.9.4-pre.0` + - `@react-router/serve@7.9.4-pre.0` + ## 7.9.3 ### Patch Changes diff --git a/packages/react-router-dev/package.json b/packages/react-router-dev/package.json index fc0d72d1e2..9d558e4d0c 100644 --- a/packages/react-router-dev/package.json +++ b/packages/react-router-dev/package.json @@ -1,6 +1,6 @@ { "name": "@react-router/dev", - "version": "7.9.3", + "version": "7.9.4-pre.0", "description": "Dev tools and CLI for React Router", "homepage": "https://reactrouter.com", "bugs": { diff --git a/packages/react-router-dom/CHANGELOG.md b/packages/react-router-dom/CHANGELOG.md index 75c2ecb0ef..fb278e5db9 100644 --- a/packages/react-router-dom/CHANGELOG.md +++ b/packages/react-router-dom/CHANGELOG.md @@ -1,5 +1,12 @@ # react-router-dom +## 7.9.4-pre.0 + +### Patch Changes + +- Updated dependencies: + - `react-router@7.9.4-pre.0` + ## 7.9.3 ### Patch Changes diff --git a/packages/react-router-dom/package.json b/packages/react-router-dom/package.json index 715770624e..07882a79b7 100644 --- a/packages/react-router-dom/package.json +++ b/packages/react-router-dom/package.json @@ -1,6 +1,6 @@ { "name": "react-router-dom", - "version": "7.9.3", + "version": "7.9.4-pre.0", "description": "Declarative routing for React web applications", "keywords": [ "react", diff --git a/packages/react-router-express/CHANGELOG.md b/packages/react-router-express/CHANGELOG.md index d3e49f9170..2abc575995 100644 --- a/packages/react-router-express/CHANGELOG.md +++ b/packages/react-router-express/CHANGELOG.md @@ -1,5 +1,13 @@ # `@react-router/express` +## 7.9.4-pre.0 + +### Patch Changes + +- Updated dependencies: + - `react-router@7.9.4-pre.0` + - `@react-router/node@7.9.4-pre.0` + ## 7.9.3 ### Patch Changes diff --git a/packages/react-router-express/package.json b/packages/react-router-express/package.json index 18f5bf6a13..69ccc57335 100644 --- a/packages/react-router-express/package.json +++ b/packages/react-router-express/package.json @@ -1,6 +1,6 @@ { "name": "@react-router/express", - "version": "7.9.3", + "version": "7.9.4-pre.0", "description": "Express server request handler for React Router", "bugs": { "url": "https://github.com/remix-run/react-router/issues" diff --git a/packages/react-router-fs-routes/CHANGELOG.md b/packages/react-router-fs-routes/CHANGELOG.md index 0153883a9c..3bb3c8ae43 100644 --- a/packages/react-router-fs-routes/CHANGELOG.md +++ b/packages/react-router-fs-routes/CHANGELOG.md @@ -1,5 +1,12 @@ # `@react-router/fs-routes` +## 7.9.4-pre.0 + +### Patch Changes + +- Updated dependencies: + - `@react-router/dev@7.9.4-pre.0` + ## 7.9.3 ### Patch Changes diff --git a/packages/react-router-fs-routes/package.json b/packages/react-router-fs-routes/package.json index d7728fd930..1b8d3d223e 100644 --- a/packages/react-router-fs-routes/package.json +++ b/packages/react-router-fs-routes/package.json @@ -1,6 +1,6 @@ { "name": "@react-router/fs-routes", - "version": "7.9.3", + "version": "7.9.4-pre.0", "description": "File system routing conventions for React Router, for use within routes.ts", "bugs": { "url": "https://github.com/remix-run/react-router/issues" diff --git a/packages/react-router-node/CHANGELOG.md b/packages/react-router-node/CHANGELOG.md index 8955ab93d2..5fe4f76bfa 100644 --- a/packages/react-router-node/CHANGELOG.md +++ b/packages/react-router-node/CHANGELOG.md @@ -1,5 +1,13 @@ # `@react-router/node` +## 7.9.4-pre.0 + +### Patch Changes + +- Validate format of incoming session ids ([#14426](https://github.com/remix-run/react-router/pull/14426)) +- Updated dependencies: + - `react-router@7.9.4-pre.0` + ## 7.9.3 ### Patch Changes diff --git a/packages/react-router-node/package.json b/packages/react-router-node/package.json index be542771c3..91ea0eeaef 100644 --- a/packages/react-router-node/package.json +++ b/packages/react-router-node/package.json @@ -1,6 +1,6 @@ { "name": "@react-router/node", - "version": "7.9.3", + "version": "7.9.4-pre.0", "description": "Node.js platform abstractions for React Router", "bugs": { "url": "https://github.com/remix-run/react-router/issues" diff --git a/packages/react-router-remix-routes-option-adapter/CHANGELOG.md b/packages/react-router-remix-routes-option-adapter/CHANGELOG.md index 7d95d12953..63d3bfa64a 100644 --- a/packages/react-router-remix-routes-option-adapter/CHANGELOG.md +++ b/packages/react-router-remix-routes-option-adapter/CHANGELOG.md @@ -1,5 +1,12 @@ # `@react-router/remix-config-routes-adapter` +## 7.9.4-pre.0 + +### Patch Changes + +- Updated dependencies: + - `@react-router/dev@7.9.4-pre.0` + ## 7.9.3 ### Patch Changes diff --git a/packages/react-router-remix-routes-option-adapter/package.json b/packages/react-router-remix-routes-option-adapter/package.json index 691b5353ab..17994e4ec1 100644 --- a/packages/react-router-remix-routes-option-adapter/package.json +++ b/packages/react-router-remix-routes-option-adapter/package.json @@ -1,6 +1,6 @@ { "name": "@react-router/remix-routes-option-adapter", - "version": "7.9.3", + "version": "7.9.4-pre.0", "description": "Adapter for Remix's \"routes\" config option, for use within routes.ts", "bugs": { "url": "https://github.com/remix-run/react-router/issues" diff --git a/packages/react-router-serve/CHANGELOG.md b/packages/react-router-serve/CHANGELOG.md index cb2c8edc65..0ef42b1322 100644 --- a/packages/react-router-serve/CHANGELOG.md +++ b/packages/react-router-serve/CHANGELOG.md @@ -1,5 +1,14 @@ # `@react-router/serve` +## 7.9.4-pre.0 + +### Patch Changes + +- Updated dependencies: + - `react-router@7.9.4-pre.0` + - `@react-router/node@7.9.4-pre.0` + - `@react-router/express@7.9.4-pre.0` + ## 7.9.3 ### Patch Changes diff --git a/packages/react-router-serve/package.json b/packages/react-router-serve/package.json index 4d42b286a5..121fea9074 100644 --- a/packages/react-router-serve/package.json +++ b/packages/react-router-serve/package.json @@ -1,6 +1,6 @@ { "name": "@react-router/serve", - "version": "7.9.3", + "version": "7.9.4-pre.0", "description": "Production application server for React Router", "bugs": { "url": "https://github.com/remix-run/react-router/issues" diff --git a/packages/react-router/CHANGELOG.md b/packages/react-router/CHANGELOG.md index 2f0d19ed90..5d713cc6fe 100644 --- a/packages/react-router/CHANGELOG.md +++ b/packages/react-router/CHANGELOG.md @@ -1,5 +1,110 @@ # `react-router` +## 7.9.4-pre.0 + +### Patch Changes + +- handle external redirects in from server actions ([#14400](https://github.com/remix-run/react-router/pull/14400)) +- New (unstable) `useRoute` hook for accessing data from specific routes ([#14407](https://github.com/remix-run/react-router/pull/14407)) + + For example, let's say you have an `admin` route somewhere in your app and you want any child routes of `admin` to all have access to the `loaderData` and `actionData` from `admin.` + + ```tsx + // app/routes/admin.tsx + import { Outlet } from "react-router"; + + export const loader = () => ({ message: "Hello, loader!" }); + + export const action = () => ({ count: 1 }); + + export default function Component() { + return ( +
+ {/* ... */} + + {/* ... */} +
+ ); + } + ``` + + You might even want to create a reusable widget that all of the routes nested under `admin` could use: + + ```tsx + import { unstable_useRoute as useRoute } from "react-router"; + + export function AdminWidget() { + // How to get `message` and `count` from `admin` route? + } + ``` + + In framework mode, `useRoute` knows all your app's routes and gives you TS errors when invalid route IDs are passed in: + + ```tsx + export function AdminWidget() { + const admin = useRoute("routes/dmin"); + // ^^^^^^^^^^^ + } + ``` + + `useRoute` returns `undefined` if the route is not part of the current page: + + ```tsx + export function AdminWidget() { + const admin = useRoute("routes/admin"); + if (!admin) { + throw new Error(`AdminWidget used outside of "routes/admin"`); + } + } + ``` + + Note: the `root` route is the exception since it is guaranteed to be part of the current page. + As a result, `useRoute` never returns `undefined` for `root`. + + `loaderData` and `actionData` are marked as optional since they could be accessed before the `action` is triggered or after the `loader` threw an error: + + ```tsx + export function AdminWidget() { + const admin = useRoute("routes/admin"); + if (!admin) { + throw new Error(`AdminWidget used outside of "routes/admin"`); + } + const { loaderData, actionData } = admin; + console.log(loaderData); + // ^? { message: string } | undefined + console.log(actionData); + // ^? { count: number } | undefined + } + ``` + + If instead of a specific route, you wanted access to the _current_ route's `loaderData` and `actionData`, you can call `useRoute` without arguments: + + ```tsx + export function AdminWidget() { + const currentRoute = useRoute(); + currentRoute.loaderData; + currentRoute.actionData; + } + ``` + + This usage is equivalent to calling `useLoaderData` and `useActionData`, but consolidates all route data access into one hook: `useRoute`. + + Note: when calling `useRoute()` (without a route ID), TS has no way to know which route is the current route. + As a result, `loaderData` and `actionData` are typed as `unknown`. + If you want more type-safety, you can either narrow the type yourself with something like `zod` or you can refactor your app to pass down typed props to your `AdminWidget`: + + ```tsx + export function AdminWidget({ + message, + count, + }: { + message: string; + count: number; + }) { + /* ... */ + } + ``` + ## 7.9.3 ### Patch Changes diff --git a/packages/react-router/package.json b/packages/react-router/package.json index 563bcad27c..1f1ef9ac12 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -1,6 +1,6 @@ { "name": "react-router", - "version": "7.9.3", + "version": "7.9.4-pre.0", "description": "Declarative routing for React", "keywords": [ "react", From 64b87fa912f52968d84740ecf81d44149f6ce64c Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 6 Oct 2025 12:09:16 -0400 Subject: [PATCH 13/16] Draft release notes --- CHANGELOG.md | 387 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 257 insertions(+), 130 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2a1a00b55..80cfee7fa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,122 +13,127 @@ We manage release notes in this file instead of the paginated Github Releases Pa Table of Contents - [React Router Releases](#react-router-releases) - - [v7.9.3](#v793) + - [v7.9.4](#v794) + - [What's Changed](#whats-changed) + - [`useRoute()` (unstable)](#useroute-unstable) - [Patch Changes](#patch-changes) + - [Unstable Changes](#unstable-changes) + - [v7.9.3](#v793) + - [Patch Changes](#patch-changes-1) - [v7.9.2](#v792) - - [What's Changed](#whats-changed) + - [What's Changed](#whats-changed-1) - [RSC Framework Mode (unstable)](#rsc-framework-mode-unstable) - [Fetcher Reset (unstable)](#fetcher-reset-unstable) - - [Patch Changes](#patch-changes-1) - - [Unstable Changes](#unstable-changes) - - [v7.9.1](#v791) - [Patch Changes](#patch-changes-2) + - [Unstable Changes](#unstable-changes-1) + - [v7.9.1](#v791) + - [Patch Changes](#patch-changes-3) - [v7.9.0](#v790) - - [What's Changed](#whats-changed-1) + - [What's Changed](#whats-changed-2) - [Stable Middleware and Context APIs](#stable-middleware-and-context-apis) - [Minor Changes](#minor-changes) - - [Patch Changes](#patch-changes-3) - - [Unstable Changes](#unstable-changes-1) - - [v7.8.2](#v782) - [Patch Changes](#patch-changes-4) - [Unstable Changes](#unstable-changes-2) - - [v7.8.1](#v781) + - [v7.8.2](#v782) - [Patch Changes](#patch-changes-5) - [Unstable Changes](#unstable-changes-3) + - [v7.8.1](#v781) + - [Patch Changes](#patch-changes-6) + - [Unstable Changes](#unstable-changes-4) - [v7.8.0](#v780) - - [What's Changed](#whats-changed-2) + - [What's Changed](#whats-changed-3) - [Consistently named `loaderData` values](#consistently-named-loaderdata-values) - [Improvements/fixes to the middleware APIs (unstable)](#improvementsfixes-to-the-middleware-apis-unstable) - [Minor Changes](#minor-changes-1) - - [Patch Changes](#patch-changes-6) - - [Unstable Changes](#unstable-changes-4) - - [Changes by Package](#changes-by-package) - - [v7.7.1](#v771) - [Patch Changes](#patch-changes-7) - [Unstable Changes](#unstable-changes-5) + - [Changes by Package](#changes-by-package) + - [v7.7.1](#v771) + - [Patch Changes](#patch-changes-8) + - [Unstable Changes](#unstable-changes-6) - [v7.7.0](#v770) - - [What's Changed](#whats-changed-3) + - [What's Changed](#whats-changed-4) - [Unstable RSC APIs](#unstable-rsc-apis) - [Minor Changes](#minor-changes-2) - - [Patch Changes](#patch-changes-8) - - [Unstable Changes](#unstable-changes-6) + - [Patch Changes](#patch-changes-9) + - [Unstable Changes](#unstable-changes-7) - [Changes by Package](#changes-by-package-1) - [v7.6.3](#v763) - - [Patch Changes](#patch-changes-9) - - [v7.6.2](#v762) - [Patch Changes](#patch-changes-10) - - [v7.6.1](#v761) + - [v7.6.2](#v762) - [Patch Changes](#patch-changes-11) - - [Unstable Changes](#unstable-changes-7) + - [v7.6.1](#v761) + - [Patch Changes](#patch-changes-12) + - [Unstable Changes](#unstable-changes-8) - [v7.6.0](#v760) - - [What's Changed](#whats-changed-4) + - [What's Changed](#whats-changed-5) - [`routeDiscovery` Config Option](#routediscovery-config-option) - [Automatic Types for Future Flags](#automatic-types-for-future-flags) - [Minor Changes](#minor-changes-3) - - [Patch Changes](#patch-changes-12) - - [Unstable Changes](#unstable-changes-8) + - [Patch Changes](#patch-changes-13) + - [Unstable Changes](#unstable-changes-9) - [Changes by Package](#changes-by-package-2) - [v7.5.3](#v753) - - [Patch Changes](#patch-changes-13) + - [Patch Changes](#patch-changes-14) - [v7.5.2](#v752) - [Security Notice](#security-notice) - - [Patch Changes](#patch-changes-14) - - [v7.5.1](#v751) - [Patch Changes](#patch-changes-15) - - [Unstable Changes](#unstable-changes-9) + - [v7.5.1](#v751) + - [Patch Changes](#patch-changes-16) + - [Unstable Changes](#unstable-changes-10) - [v7.5.0](#v750) - - [What's Changed](#whats-changed-5) + - [What's Changed](#whats-changed-6) - [`route.lazy` Object API](#routelazy-object-api) - [Minor Changes](#minor-changes-4) - - [Patch Changes](#patch-changes-16) - - [Unstable Changes](#unstable-changes-10) + - [Patch Changes](#patch-changes-17) + - [Unstable Changes](#unstable-changes-11) - [Changes by Package](#changes-by-package-3) - [v7.4.1](#v741) - [Security Notice](#security-notice-1) - - [Patch Changes](#patch-changes-17) - - [Unstable Changes](#unstable-changes-11) - - [v7.4.0](#v740) - - [Minor Changes](#minor-changes-5) - [Patch Changes](#patch-changes-18) - [Unstable Changes](#unstable-changes-12) + - [v7.4.0](#v740) + - [Minor Changes](#minor-changes-5) + - [Patch Changes](#patch-changes-19) + - [Unstable Changes](#unstable-changes-13) - [Changes by Package](#changes-by-package-4) - [v7.3.0](#v730) - [Minor Changes](#minor-changes-6) - - [Patch Changes](#patch-changes-19) - - [Unstable Changes](#unstable-changes-13) + - [Patch Changes](#patch-changes-20) + - [Unstable Changes](#unstable-changes-14) - [Client-side `context` (unstable)](#client-side-context-unstable) - [Middleware (unstable)](#middleware-unstable) - [Middleware `context` parameter](#middleware-context-parameter) - [`unstable_SerializesTo`](#unstable_serializesto) - [Changes by Package](#changes-by-package-5) - [v7.2.0](#v720) - - [What's Changed](#whats-changed-6) + - [What's Changed](#whats-changed-7) - [Type-safe `href` utility](#type-safe-href-utility) - [Prerendering with a SPA Fallback](#prerendering-with-a-spa-fallback) - [Allow a root `loader` in SPA Mode](#allow-a-root-loader-in-spa-mode) - [Minor Changes](#minor-changes-7) - - [Patch Changes](#patch-changes-20) - - [Unstable Changes](#unstable-changes-14) + - [Patch Changes](#patch-changes-21) + - [Unstable Changes](#unstable-changes-15) - [Split Route Modules (unstable)](#split-route-modules-unstable) - [Changes by Package](#changes-by-package-6) - [v7.1.5](#v715) - - [Patch Changes](#patch-changes-21) - - [v7.1.4](#v714) - [Patch Changes](#patch-changes-22) - - [v7.1.3](#v713) + - [v7.1.4](#v714) - [Patch Changes](#patch-changes-23) - - [v7.1.2](#v712) + - [v7.1.3](#v713) - [Patch Changes](#patch-changes-24) - - [v7.1.1](#v711) + - [v7.1.2](#v712) - [Patch Changes](#patch-changes-25) + - [v7.1.1](#v711) + - [Patch Changes](#patch-changes-26) - [v7.1.0](#v710) - [Minor Changes](#minor-changes-8) - - [Patch Changes](#patch-changes-26) + - [Patch Changes](#patch-changes-27) - [Changes by Package](#changes-by-package-7) - [v7.0.2](#v702) - - [Patch Changes](#patch-changes-27) - - [v7.0.1](#v701) - [Patch Changes](#patch-changes-28) + - [v7.0.1](#v701) + - [Patch Changes](#patch-changes-29) - [v7.0.0](#v700) - [Breaking Changes](#breaking-changes) - [Package Restructuring](#package-restructuring) @@ -145,201 +150,201 @@ We manage release notes in this file instead of the paginated Github Releases Pa - [Major Changes (`react-router`)](#major-changes-react-router) - [Major Changes (`@react-router/*`)](#major-changes-react-router-1) - [Minor Changes](#minor-changes-9) - - [Patch Changes](#patch-changes-29) + - [Patch Changes](#patch-changes-30) - [Changes by Package](#changes-by-package-8) - [React Router v6 Releases](#react-router-v6-releases) - [v6.30.1](#v6301) - - [Patch Changes](#patch-changes-30) + - [Patch Changes](#patch-changes-31) - [v6.30.0](#v6300) - [Minor Changes](#minor-changes-10) - - [Patch Changes](#patch-changes-31) + - [Patch Changes](#patch-changes-32) - [v6.29.0](#v6290) - [Minor Changes](#minor-changes-11) - - [Patch Changes](#patch-changes-32) - - [v6.28.2](#v6282) - [Patch Changes](#patch-changes-33) - - [v6.28.1](#v6281) + - [v6.28.2](#v6282) - [Patch Changes](#patch-changes-34) + - [v6.28.1](#v6281) + - [Patch Changes](#patch-changes-35) - [v6.28.0](#v6280) - - [What's Changed](#whats-changed-7) + - [What's Changed](#whats-changed-8) - [Minor Changes](#minor-changes-12) - - [Patch Changes](#patch-changes-35) + - [Patch Changes](#patch-changes-36) - [v6.27.0](#v6270) - - [What's Changed](#whats-changed-8) + - [What's Changed](#whats-changed-9) - [Stabilized APIs](#stabilized-apis) - [Minor Changes](#minor-changes-13) - - [Patch Changes](#patch-changes-36) - - [v6.26.2](#v6262) - [Patch Changes](#patch-changes-37) - - [v6.26.1](#v6261) + - [v6.26.2](#v6262) - [Patch Changes](#patch-changes-38) + - [v6.26.1](#v6261) + - [Patch Changes](#patch-changes-39) - [v6.26.0](#v6260) - [Minor Changes](#minor-changes-14) - - [Patch Changes](#patch-changes-39) - - [v6.25.1](#v6251) - [Patch Changes](#patch-changes-40) + - [v6.25.1](#v6251) + - [Patch Changes](#patch-changes-41) - [v6.25.0](#v6250) - - [What's Changed](#whats-changed-9) + - [What's Changed](#whats-changed-10) - [Stabilized `v7_skipActionErrorRevalidation`](#stabilized-v7_skipactionerrorrevalidation) - [Minor Changes](#minor-changes-15) - - [Patch Changes](#patch-changes-41) - - [v6.24.1](#v6241) - [Patch Changes](#patch-changes-42) + - [v6.24.1](#v6241) + - [Patch Changes](#patch-changes-43) - [v6.24.0](#v6240) - - [What's Changed](#whats-changed-10) + - [What's Changed](#whats-changed-11) - [Lazy Route Discovery (a.k.a. "Fog of War")](#lazy-route-discovery-aka-fog-of-war) - [Minor Changes](#minor-changes-16) - - [Patch Changes](#patch-changes-43) - - [v6.23.1](#v6231) - [Patch Changes](#patch-changes-44) + - [v6.23.1](#v6231) + - [Patch Changes](#patch-changes-45) - [v6.23.0](#v6230) - - [What's Changed](#whats-changed-11) + - [What's Changed](#whats-changed-12) - [Data Strategy (unstable)](#data-strategy-unstable) - [Skip Action Error Revalidation (unstable)](#skip-action-error-revalidation-unstable) - [Minor Changes](#minor-changes-17) - [v6.22.3](#v6223) - - [Patch Changes](#patch-changes-45) - - [v6.22.2](#v6222) - [Patch Changes](#patch-changes-46) - - [v6.22.1](#v6221) + - [v6.22.2](#v6222) - [Patch Changes](#patch-changes-47) + - [v6.22.1](#v6221) + - [Patch Changes](#patch-changes-48) - [v6.22.0](#v6220) - - [What's Changed](#whats-changed-12) + - [What's Changed](#whats-changed-13) - [Core Web Vitals Technology Report Flag](#core-web-vitals-technology-report-flag) - [Minor Changes](#minor-changes-18) - - [Patch Changes](#patch-changes-48) - - [v6.21.3](#v6213) - [Patch Changes](#patch-changes-49) - - [v6.21.2](#v6212) + - [v6.21.3](#v6213) - [Patch Changes](#patch-changes-50) - - [v6.21.1](#v6211) + - [v6.21.2](#v6212) - [Patch Changes](#patch-changes-51) + - [v6.21.1](#v6211) + - [Patch Changes](#patch-changes-52) - [v6.21.0](#v6210) - - [What's Changed](#whats-changed-13) + - [What's Changed](#whats-changed-14) - [`future.v7_relativeSplatPath`](#futurev7_relativesplatpath) - [Partial Hydration](#partial-hydration) - [Minor Changes](#minor-changes-19) - - [Patch Changes](#patch-changes-52) - - [v6.20.1](#v6201) - [Patch Changes](#patch-changes-53) + - [v6.20.1](#v6201) + - [Patch Changes](#patch-changes-54) - [v6.20.0](#v6200) - [Minor Changes](#minor-changes-20) - - [Patch Changes](#patch-changes-54) + - [Patch Changes](#patch-changes-55) - [v6.19.0](#v6190) - - [What's Changed](#whats-changed-14) + - [What's Changed](#whats-changed-15) - [`unstable_flushSync` API](#unstable_flushsync-api) - [Minor Changes](#minor-changes-21) - - [Patch Changes](#patch-changes-55) + - [Patch Changes](#patch-changes-56) - [v6.18.0](#v6180) - - [What's Changed](#whats-changed-15) + - [What's Changed](#whats-changed-16) - [New Fetcher APIs](#new-fetcher-apis) - [Persistence Future Flag (`future.v7_fetcherPersist`)](#persistence-future-flag-futurev7_fetcherpersist) - [Minor Changes](#minor-changes-22) - - [Patch Changes](#patch-changes-56) + - [Patch Changes](#patch-changes-57) - [v6.17.0](#v6170) - - [What's Changed](#whats-changed-16) + - [What's Changed](#whats-changed-17) - [View Transitions 🚀](#view-transitions-) - [Minor Changes](#minor-changes-23) - - [Patch Changes](#patch-changes-57) + - [Patch Changes](#patch-changes-58) - [v6.16.0](#v6160) - [Minor Changes](#minor-changes-24) - - [Patch Changes](#patch-changes-58) + - [Patch Changes](#patch-changes-59) - [v6.15.0](#v6150) - [Minor Changes](#minor-changes-25) - - [Patch Changes](#patch-changes-59) - - [v6.14.2](#v6142) - [Patch Changes](#patch-changes-60) - - [v6.14.1](#v6141) + - [v6.14.2](#v6142) - [Patch Changes](#patch-changes-61) + - [v6.14.1](#v6141) + - [Patch Changes](#patch-changes-62) - [v6.14.0](#v6140) - - [What's Changed](#whats-changed-17) + - [What's Changed](#whats-changed-18) - [JSON/Text Submissions](#jsontext-submissions) - [Minor Changes](#minor-changes-26) - - [Patch Changes](#patch-changes-62) + - [Patch Changes](#patch-changes-63) - [v6.13.0](#v6130) - - [What's Changed](#whats-changed-18) + - [What's Changed](#whats-changed-19) - [`future.v7_startTransition`](#futurev7_starttransition) - [Minor Changes](#minor-changes-27) - - [Patch Changes](#patch-changes-63) - - [v6.12.1](#v6121) - [Patch Changes](#patch-changes-64) + - [v6.12.1](#v6121) + - [Patch Changes](#patch-changes-65) - [v6.12.0](#v6120) - - [What's Changed](#whats-changed-19) + - [What's Changed](#whats-changed-20) - [`React.startTransition` support](#reactstarttransition-support) - [Minor Changes](#minor-changes-28) - - [Patch Changes](#patch-changes-65) - - [v6.11.2](#v6112) - [Patch Changes](#patch-changes-66) - - [v6.11.1](#v6111) + - [v6.11.2](#v6112) - [Patch Changes](#patch-changes-67) + - [v6.11.1](#v6111) + - [Patch Changes](#patch-changes-68) - [v6.11.0](#v6110) - [Minor Changes](#minor-changes-29) - - [Patch Changes](#patch-changes-68) + - [Patch Changes](#patch-changes-69) - [v6.10.0](#v6100) - - [What's Changed](#whats-changed-20) + - [What's Changed](#whats-changed-21) - [Minor Changes](#minor-changes-30) - [`future.v7_normalizeFormMethod`](#futurev7_normalizeformmethod) - - [Patch Changes](#patch-changes-69) + - [Patch Changes](#patch-changes-70) - [v6.9.0](#v690) - - [What's Changed](#whats-changed-21) + - [What's Changed](#whats-changed-22) - [`Component`/`ErrorBoundary` route properties](#componenterrorboundary-route-properties) - [Introducing Lazy Route Modules](#introducing-lazy-route-modules) - [Minor Changes](#minor-changes-31) - - [Patch Changes](#patch-changes-70) - - [v6.8.2](#v682) - [Patch Changes](#patch-changes-71) - - [v6.8.1](#v681) + - [v6.8.2](#v682) - [Patch Changes](#patch-changes-72) + - [v6.8.1](#v681) + - [Patch Changes](#patch-changes-73) - [v6.8.0](#v680) - [Minor Changes](#minor-changes-32) - - [Patch Changes](#patch-changes-73) + - [Patch Changes](#patch-changes-74) - [v6.7.0](#v670) - [Minor Changes](#minor-changes-33) - - [Patch Changes](#patch-changes-74) - - [v6.6.2](#v662) - [Patch Changes](#patch-changes-75) - - [v6.6.1](#v661) + - [v6.6.2](#v662) - [Patch Changes](#patch-changes-76) + - [v6.6.1](#v661) + - [Patch Changes](#patch-changes-77) - [v6.6.0](#v660) - - [What's Changed](#whats-changed-22) + - [What's Changed](#whats-changed-23) - [Minor Changes](#minor-changes-34) - - [Patch Changes](#patch-changes-77) + - [Patch Changes](#patch-changes-78) - [v6.5.0](#v650) - - [What's Changed](#whats-changed-23) + - [What's Changed](#whats-changed-24) - [Minor Changes](#minor-changes-35) - - [Patch Changes](#patch-changes-78) - - [v6.4.5](#v645) - [Patch Changes](#patch-changes-79) - - [v6.4.4](#v644) + - [v6.4.5](#v645) - [Patch Changes](#patch-changes-80) - - [v6.4.3](#v643) + - [v6.4.4](#v644) - [Patch Changes](#patch-changes-81) - - [v6.4.2](#v642) + - [v6.4.3](#v643) - [Patch Changes](#patch-changes-82) - - [v6.4.1](#v641) + - [v6.4.2](#v642) - [Patch Changes](#patch-changes-83) + - [v6.4.1](#v641) + - [Patch Changes](#patch-changes-84) - [v6.4.0](#v640) - - [What's Changed](#whats-changed-24) + - [What's Changed](#whats-changed-25) - [Remix Data APIs](#remix-data-apis) - - [Patch Changes](#patch-changes-84) + - [Patch Changes](#patch-changes-85) - [v6.3.0](#v630) - [Minor Changes](#minor-changes-36) - [v6.2.2](#v622) - - [Patch Changes](#patch-changes-85) - - [v6.2.1](#v621) - [Patch Changes](#patch-changes-86) + - [v6.2.1](#v621) + - [Patch Changes](#patch-changes-87) - [v6.2.0](#v620) - [Minor Changes](#minor-changes-37) - - [Patch Changes](#patch-changes-87) - - [v6.1.1](#v611) - [Patch Changes](#patch-changes-88) + - [v6.1.1](#v611) + - [Patch Changes](#patch-changes-89) - [v6.1.0](#v610) - [Minor Changes](#minor-changes-38) - - [Patch Changes](#patch-changes-89) - - [v6.0.2](#v602) - [Patch Changes](#patch-changes-90) - - [v6.0.1](#v601) + - [v6.0.2](#v602) - [Patch Changes](#patch-changes-91) + - [v6.0.1](#v601) + - [Patch Changes](#patch-changes-92) - [v6.0.0](#v600) @@ -367,6 +372,128 @@ Date: YYYY-MM-DD **Full Changelog**: [`v7.X.Y...v7.X.Y`](https://github.com/remix-run/react-router/compare/react-router@7.X.Y...react-router@7.X.Y) --> +## v7.9.4 + +Date: 2025-10-08 + +### What's Changed + +#### `useRoute()` (unstable) + +This release includes a new `unstable_useRoute()` hook that provides a type-safe way to access route `loaderData`/`actionData` from a specific route in Framework Mode. Think if it like a better version of `useRouteLoaderData` that works with the typegen system and also supports `actionData`. Check out the changelog entry below for more information. + +### Patch Changes + +- `@react-router/dev` - Update `valibot` dependency to `^1.1.0` ([#14379](https://github.com/remix-run/react-router/pull/14379)) +- `@react-router/node` - Validate format of incoming session ids ([#14426](https://github.com/remix-run/react-router/pull/14426)) + +### Unstable Changes + +⚠️ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_ + +- `react-router` - handle external redirects in from server actions ([#14400](https://github.com/remix-run/react-router/pull/14400)) +- `react-router` - New (unstable) `useRoute` hook for accessing data from specific routes ([#14407](https://github.com/remix-run/react-router/pull/14407)) + + For example, let's say you have an `admin` route somewhere in your app and you want any child routes of `admin` to all have access to the `loaderData` and `actionData` from `admin.` + + ```tsx + // app/routes/admin.tsx + import { Outlet } from "react-router"; + + export const loader = () => ({ message: "Hello, loader!" }); + + export const action = () => ({ count: 1 }); + + export default function Component() { + return ( +
+ {/* ... */} + + {/* ... */} +
+ ); + } + ``` + + You might even want to create a reusable widget that all of the routes nested under `admin` could use: + + ```tsx + import { unstable_useRoute as useRoute } from "react-router"; + + export function AdminWidget() { + // How to get `message` and `count` from `admin` route? + } + ``` + + In framework mode, `useRoute` knows all your app's routes and gives you TS errors when invalid route IDs are passed in: + + ```tsx + export function AdminWidget() { + const admin = useRoute("routes/dmin"); + // ^^^^^^^^^^^ + } + ``` + + `useRoute` returns `undefined` if the route is not part of the current page: + + ```tsx + export function AdminWidget() { + const admin = useRoute("routes/admin"); + if (!admin) { + throw new Error(`AdminWidget used outside of "routes/admin"`); + } + } + ``` + + Note: the `root` route is the exception since it is guaranteed to be part of the current page. + As a result, `useRoute` never returns `undefined` for `root`. + + `loaderData` and `actionData` are marked as optional since they could be accessed before the `action` is triggered or after the `loader` threw an error: + + ```tsx + export function AdminWidget() { + const admin = useRoute("routes/admin"); + if (!admin) { + throw new Error(`AdminWidget used outside of "routes/admin"`); + } + const { loaderData, actionData } = admin; + console.log(loaderData); + // ^? { message: string } | undefined + console.log(actionData); + // ^? { count: number } | undefined + } + ``` + + If instead of a specific route, you wanted access to the _current_ route's `loaderData` and `actionData`, you can call `useRoute` without arguments: + + ```tsx + export function AdminWidget() { + const currentRoute = useRoute(); + currentRoute.loaderData; + currentRoute.actionData; + } + ``` + + This usage is equivalent to calling `useLoaderData` and `useActionData`, but consolidates all route data access into one hook: `useRoute`. + + Note: when calling `useRoute()` (without a route ID), TS has no way to know which route is the current route. + As a result, `loaderData` and `actionData` are typed as `unknown`. + If you want more type-safety, you can either narrow the type yourself with something like `zod` or you can refactor your app to pass down typed props to your `AdminWidget`: + + ```tsx + export function AdminWidget({ + message, + count, + }: { + message: string; + count: number; + }) { + /* ... */ + } + ``` + +**Full Changelog**: [`v7.9.3...v7.9.4`](https://github.com/remix-run/react-router/compare/react-router@7.9.3...react-router@7.9.4) + ## v7.9.3 Date: 2025-09-26 From bbc8ba259e84d94c5b2aa540e456a2be2c9fb979 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 8 Oct 2025 13:26:22 -0400 Subject: [PATCH 14/16] Exit prerelease mode --- .changeset/pre.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 370fee1430..f913c41df4 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -1,5 +1,5 @@ { - "mode": "pre", + "mode": "exit", "tag": "pre", "initialVersions": { "integration": "0.0.0", From e650acfa72280373471b329931f024d9445f2925 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 8 Oct 2025 13:35:03 -0400 Subject: [PATCH 15/16] chore: Update version for release (#14431) --- .changeset/breezy-planes-roll.md | 5 - .changeset/green-pens-push.md | 5 - .changeset/pre.json | 45 -------- .changeset/quick-eels-join.md | 5 - .changeset/six-lobsters-think.md | 104 ------------------ integration/CHANGELOG.md | 1 + packages/create-react-router/CHANGELOG.md | 6 +- packages/create-react-router/package.json | 2 +- packages/react-router-architect/CHANGELOG.md | 9 +- packages/react-router-architect/package.json | 2 +- packages/react-router-cloudflare/CHANGELOG.md | 7 +- packages/react-router-cloudflare/package.json | 2 +- packages/react-router-dev/CHANGELOG.md | 17 ++- packages/react-router-dev/package.json | 2 +- packages/react-router-dom/CHANGELOG.md | 4 +- packages/react-router-dom/package.json | 2 +- packages/react-router-express/CHANGELOG.md | 7 +- packages/react-router-express/package.json | 2 +- packages/react-router-fs-routes/CHANGELOG.md | 4 +- packages/react-router-fs-routes/package.json | 2 +- packages/react-router-node/CHANGELOG.md | 9 +- packages/react-router-node/package.json | 2 +- .../CHANGELOG.md | 4 +- .../package.json | 2 +- packages/react-router-serve/CHANGELOG.md | 10 +- packages/react-router-serve/package.json | 2 +- packages/react-router/CHANGELOG.md | 12 +- packages/react-router/package.json | 2 +- 28 files changed, 74 insertions(+), 202 deletions(-) delete mode 100644 .changeset/breezy-planes-roll.md delete mode 100644 .changeset/green-pens-push.md delete mode 100644 .changeset/pre.json delete mode 100644 .changeset/quick-eels-join.md delete mode 100644 .changeset/six-lobsters-think.md diff --git a/.changeset/breezy-planes-roll.md b/.changeset/breezy-planes-roll.md deleted file mode 100644 index 6d1bb70e6a..0000000000 --- a/.changeset/breezy-planes-roll.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"react-router": patch ---- - -handle external redirects in from server actions diff --git a/.changeset/green-pens-push.md b/.changeset/green-pens-push.md deleted file mode 100644 index c0a3998ba5..0000000000 --- a/.changeset/green-pens-push.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@react-router/dev": patch ---- - -Update `valibot` dependency to `^1.1.0` diff --git a/.changeset/pre.json b/.changeset/pre.json deleted file mode 100644 index f913c41df4..0000000000 --- a/.changeset/pre.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "mode": "exit", - "tag": "pre", - "initialVersions": { - "integration": "0.0.0", - "integration-cloudflare-dev-proxy-template": "0.0.0", - "integration-rsc-parcel": "0.0.0", - "integration-rsc-vite": "0.0.0", - "integration-rsc-vite-framework": "0.0.0", - "integration-vite-5-template": "0.0.0", - "integration-vite-6-template": "0.0.0", - "integration-vite-7-beta-template": "0.0.0", - "integration-vite-plugin-cloudflare-template": "0.0.0", - "integration-vite-rolldown-template": "0.0.0", - "create-react-router": "7.9.3", - "react-router": "7.9.3", - "@react-router/architect": "7.9.3", - "@react-router/cloudflare": "7.9.3", - "@react-router/dev": "7.9.3", - "react-router-dom": "7.9.3", - "@react-router/express": "7.9.3", - "@react-router/fs-routes": "7.9.3", - "@react-router/node": "7.9.3", - "@react-router/remix-routes-option-adapter": "7.9.3", - "@react-router/serve": "7.9.3", - "@playground/framework": "0.0.0", - "@playground/framework-express": "0.0.0", - "@playground/framework-rolldown-vite": "0.0.0", - "@playground/framework-spa": "0.0.0", - "@playground/framework-vite-5": "0.0.0", - "@playground/framework-vite-7-beta": "0.0.0", - "@playground/rsc-parcel": "0.0.0", - "@playground/rsc-vite": "0.0.0", - "@playground/rsc-vite-framework": "0.0.0", - "@playground/split-route-modules": "0.0.0", - "@playground/split-route-modules-spa": "0.0.0", - "@playground/vite-plugin-cloudflare": "0.0.0" - }, - "changesets": [ - "breezy-planes-roll", - "green-pens-push", - "quick-eels-join", - "six-lobsters-think" - ] -} diff --git a/.changeset/quick-eels-join.md b/.changeset/quick-eels-join.md deleted file mode 100644 index c9116d9304..0000000000 --- a/.changeset/quick-eels-join.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@react-router/node": patch ---- - -Validate format of incoming session ids diff --git a/.changeset/six-lobsters-think.md b/.changeset/six-lobsters-think.md deleted file mode 100644 index 3db89c1017..0000000000 --- a/.changeset/six-lobsters-think.md +++ /dev/null @@ -1,104 +0,0 @@ ---- -"@react-router/dev": patch -"react-router": patch ---- - -New (unstable) `useRoute` hook for accessing data from specific routes - -For example, let's say you have an `admin` route somewhere in your app and you want any child routes of `admin` to all have access to the `loaderData` and `actionData` from `admin.` - -```tsx -// app/routes/admin.tsx -import { Outlet } from "react-router"; - -export const loader = () => ({ message: "Hello, loader!" }); - -export const action = () => ({ count: 1 }); - -export default function Component() { - return ( -
- {/* ... */} - - {/* ... */} -
- ); -} -``` - -You might even want to create a reusable widget that all of the routes nested under `admin` could use: - -```tsx -import { unstable_useRoute as useRoute } from "react-router"; - -export function AdminWidget() { - // How to get `message` and `count` from `admin` route? -} -``` - -In framework mode, `useRoute` knows all your app's routes and gives you TS errors when invalid route IDs are passed in: - -```tsx -export function AdminWidget() { - const admin = useRoute("routes/dmin"); - // ^^^^^^^^^^^ -} -``` - -`useRoute` returns `undefined` if the route is not part of the current page: - -```tsx -export function AdminWidget() { - const admin = useRoute("routes/admin"); - if (!admin) { - throw new Error(`AdminWidget used outside of "routes/admin"`); - } -} -``` - -Note: the `root` route is the exception since it is guaranteed to be part of the current page. -As a result, `useRoute` never returns `undefined` for `root`. - -`loaderData` and `actionData` are marked as optional since they could be accessed before the `action` is triggered or after the `loader` threw an error: - -```tsx -export function AdminWidget() { - const admin = useRoute("routes/admin"); - if (!admin) { - throw new Error(`AdminWidget used outside of "routes/admin"`); - } - const { loaderData, actionData } = admin; - console.log(loaderData); - // ^? { message: string } | undefined - console.log(actionData); - // ^? { count: number } | undefined -} -``` - -If instead of a specific route, you wanted access to the _current_ route's `loaderData` and `actionData`, you can call `useRoute` without arguments: - -```tsx -export function AdminWidget() { - const currentRoute = useRoute(); - currentRoute.loaderData; - currentRoute.actionData; -} -``` - -This usage is equivalent to calling `useLoaderData` and `useActionData`, but consolidates all route data access into one hook: `useRoute`. - -Note: when calling `useRoute()` (without a route ID), TS has no way to know which route is the current route. -As a result, `loaderData` and `actionData` are typed as `unknown`. -If you want more type-safety, you can either narrow the type yourself with something like `zod` or you can refactor your app to pass down typed props to your `AdminWidget`: - -```tsx -export function AdminWidget({ - message, - count, -}: { - message: string; - count: number; -}) { - /* ... */ -} -``` diff --git a/integration/CHANGELOG.md b/integration/CHANGELOG.md index 2cf67d87b7..6fccf850d7 100644 --- a/integration/CHANGELOG.md +++ b/integration/CHANGELOG.md @@ -5,6 +5,7 @@ ### Minor Changes - Unstable Vite support for Node-based Remix apps ([#7590](https://github.com/remix-run/remix/pull/7590)) + - `remix build` 👉 `vite build && vite build --ssr` - `remix dev` 👉 `vite dev` diff --git a/packages/create-react-router/CHANGELOG.md b/packages/create-react-router/CHANGELOG.md index b568ce6a01..49f7ad6ac1 100644 --- a/packages/create-react-router/CHANGELOG.md +++ b/packages/create-react-router/CHANGELOG.md @@ -1,9 +1,13 @@ # `create-react-router` -## 7.9.4-pre.0 +## 7.9.4 + +_No changes_ ## 7.9.3 +_No changes_ + ## 7.9.2 _No changes_ diff --git a/packages/create-react-router/package.json b/packages/create-react-router/package.json index 12b2b43891..c51639a244 100644 --- a/packages/create-react-router/package.json +++ b/packages/create-react-router/package.json @@ -1,6 +1,6 @@ { "name": "create-react-router", - "version": "7.9.4-pre.0", + "version": "7.9.4", "description": "Create a new React Router app", "homepage": "https://reactrouter.com", "bugs": { diff --git a/packages/react-router-architect/CHANGELOG.md b/packages/react-router-architect/CHANGELOG.md index 6b490f824c..8dd3f96677 100644 --- a/packages/react-router-architect/CHANGELOG.md +++ b/packages/react-router-architect/CHANGELOG.md @@ -1,12 +1,12 @@ # `@react-router/architect` -## 7.9.4-pre.0 +## 7.9.4 ### Patch Changes - Updated dependencies: - - `react-router@7.9.4-pre.0` - - `@react-router/node@7.9.4-pre.0` + - `react-router@7.9.4` + - `@react-router/node@7.9.4` ## 7.9.3 @@ -39,6 +39,7 @@ - Stabilize middleware and context APIs. ([#14215](https://github.com/remix-run/react-router/pull/14215)) We have removed the `unstable_` prefix from the following APIs and they are now considered stable and ready for production use: + - [`RouterContextProvider`](https://reactrouter.com/api/utils/RouterContextProvider) - [`createContext`](https://reactrouter.com/api/utils/createContext) - `createBrowserRouter` [`getContext`](https://reactrouter.com/api/data-routers/createBrowserRouter#optsgetcontext) option @@ -262,6 +263,7 @@ ### Major Changes - For Remix consumers migrating to React Router, the `crypto` global from the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) is now required when using cookie and session APIs. This means that the following APIs are provided from `react-router` rather than platform-specific packages: ([#11837](https://github.com/remix-run/react-router/pull/11837)) + - `createCookie` - `createCookieSessionStorage` - `createMemorySessionStorage` @@ -270,6 +272,7 @@ For consumers running older versions of Node, the `installGlobals` function from `@remix-run/node` has been updated to define `globalThis.crypto`, using [Node's `require('node:crypto').webcrypto` implementation.](https://nodejs.org/api/webcrypto.html) Since platform-specific packages no longer need to implement this API, the following low-level APIs have been removed: + - `createCookieFactory` - `createSessionStorageFactory` - `createCookieSessionStorageFactory` diff --git a/packages/react-router-architect/package.json b/packages/react-router-architect/package.json index 9338237309..2e4c1ac8bf 100644 --- a/packages/react-router-architect/package.json +++ b/packages/react-router-architect/package.json @@ -1,6 +1,6 @@ { "name": "@react-router/architect", - "version": "7.9.4-pre.0", + "version": "7.9.4", "description": "Architect server request handler for React Router", "bugs": { "url": "https://github.com/remix-run/react-router/issues" diff --git a/packages/react-router-cloudflare/CHANGELOG.md b/packages/react-router-cloudflare/CHANGELOG.md index 514ce4ed59..46daa9383e 100644 --- a/packages/react-router-cloudflare/CHANGELOG.md +++ b/packages/react-router-cloudflare/CHANGELOG.md @@ -1,11 +1,11 @@ # `@react-router/cloudflare` -## 7.9.4-pre.0 +## 7.9.4 ### Patch Changes - Updated dependencies: - - `react-router@7.9.4-pre.0` + - `react-router@7.9.4` ## 7.9.3 @@ -35,6 +35,7 @@ - Stabilize middleware and context APIs. ([#14215](https://github.com/remix-run/react-router/pull/14215)) We have removed the `unstable_` prefix from the following APIs and they are now considered stable and ready for production use: + - [`RouterContextProvider`](https://reactrouter.com/api/utils/RouterContextProvider) - [`createContext`](https://reactrouter.com/api/utils/createContext) - `createBrowserRouter` [`getContext`](https://reactrouter.com/api/data-routers/createBrowserRouter#optsgetcontext) option @@ -233,6 +234,7 @@ - For Remix consumers migrating to React Router, all exports from `@remix-run/cloudflare-pages` are now provided for React Router consumers in the `@react-router/cloudflare` package. There is no longer a separate package for Cloudflare Pages. ([#11801](https://github.com/remix-run/react-router/pull/11801)) - For Remix consumers migrating to React Router, the `crypto` global from the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) is now required when using cookie and session APIs. This means that the following APIs are provided from `react-router` rather than platform-specific packages: ([#11837](https://github.com/remix-run/react-router/pull/11837)) + - `createCookie` - `createCookieSessionStorage` - `createMemorySessionStorage` @@ -241,6 +243,7 @@ For consumers running older versions of Node, the `installGlobals` function from `@remix-run/node` has been updated to define `globalThis.crypto`, using [Node's `require('node:crypto').webcrypto` implementation.](https://nodejs.org/api/webcrypto.html) Since platform-specific packages no longer need to implement this API, the following low-level APIs have been removed: + - `createCookieFactory` - `createSessionStorageFactory` - `createCookieSessionStorageFactory` diff --git a/packages/react-router-cloudflare/package.json b/packages/react-router-cloudflare/package.json index af600ec3fe..44275aa75e 100644 --- a/packages/react-router-cloudflare/package.json +++ b/packages/react-router-cloudflare/package.json @@ -1,6 +1,6 @@ { "name": "@react-router/cloudflare", - "version": "7.9.4-pre.0", + "version": "7.9.4", "description": "Cloudflare platform abstractions for React Router", "bugs": { "url": "https://github.com/remix-run/react-router/issues" diff --git a/packages/react-router-dev/CHANGELOG.md b/packages/react-router-dev/CHANGELOG.md index 9639e668e0..980bd3bab0 100644 --- a/packages/react-router-dev/CHANGELOG.md +++ b/packages/react-router-dev/CHANGELOG.md @@ -1,10 +1,11 @@ # `@react-router/dev` -## 7.9.4-pre.0 +## 7.9.4 ### Patch Changes - Update `valibot` dependency to `^1.1.0` ([#14379](https://github.com/remix-run/react-router/pull/14379)) + - New (unstable) `useRoute` hook for accessing data from specific routes ([#14407](https://github.com/remix-run/react-router/pull/14407)) For example, let's say you have an `admin` route somewhere in your app and you want any child routes of `admin` to all have access to the `loaderData` and `actionData` from `admin.` @@ -106,9 +107,9 @@ ``` - Updated dependencies: - - `react-router@7.9.4-pre.0` - - `@react-router/node@7.9.4-pre.0` - - `@react-router/serve@7.9.4-pre.0` + - `react-router@7.9.4` + - `@react-router/node@7.9.4` + - `@react-router/serve@7.9.4` ## 7.9.3 @@ -155,6 +156,7 @@ - Stabilize middleware and context APIs. ([#14215](https://github.com/remix-run/react-router/pull/14215)) We have removed the `unstable_` prefix from the following APIs and they are now considered stable and ready for production use: + - [`RouterContextProvider`](https://reactrouter.com/api/utils/RouterContextProvider) - [`createContext`](https://reactrouter.com/api/utils/createContext) - `createBrowserRouter` [`getContext`](https://reactrouter.com/api/data-routers/createBrowserRouter#optsgetcontext) option @@ -897,6 +899,7 @@ ``` This initial implementation targets type inference for: + - `Params` : Path parameters from your routing config in `routes.ts` including file-based routing - `LoaderData` : Loader data from `loader` and/or `clientLoader` within your route module - `ActionData` : Action data from `action` and/or `clientAction` within your route module @@ -911,6 +914,7 @@ ``` Check out our docs for more: + - [_Explanations > Type Safety_](https://reactrouter.com/dev/guides/explanation/type-safety) - [_How-To > Setting up type safety_](https://reactrouter.com/dev/guides/how-to/setting-up-type-safety) @@ -1110,6 +1114,7 @@ - Vite: Provide `Unstable_ServerBundlesFunction` and `Unstable_VitePluginConfig` types ([#8654](https://github.com/remix-run/remix/pull/8654)) - Vite: add `--sourcemapClient` and `--sourcemapServer` flags to `remix vite:build` ([#8613](https://github.com/remix-run/remix/pull/8613)) + - `--sourcemapClient` - `--sourcemapClient=inline` @@ -1446,6 +1451,7 @@ - Add support for `clientLoader`/`clientAction`/`HydrateFallback` route exports ([RFC](https://github.com/remix-run/remix/discussions/7634)) ([#8173](https://github.com/remix-run/remix/pull/8173)) Remix now supports loaders/actions that run on the client (in addition to, or instead of the loader/action that runs on the server). While we still recommend server loaders/actions for the majority of your data needs in a Remix app - these provide some levers you can pull for more advanced use-cases such as: + - Leveraging a data source local to the browser (i.e., `localStorage`) - Managing a client-side cache of server data (like `IndexedDB`) - Bypassing the Remix server in a BFF setup and hitting your API directly from the browser @@ -1849,6 +1855,7 @@ - Output esbuild metafiles for bundle analysis ([#6772](https://github.com/remix-run/remix/pull/6772)) Written to server build directory (`build/` by default): + - `metafile.css.json` - `metafile.js.json` (browser JS) - `metafile.server.json` (server JS) @@ -1946,6 +1953,7 @@ - built-in tls support ([#6483](https://github.com/remix-run/remix/pull/6483)) New options: + - `--tls-key` / `tlsKey`: TLS key - `--tls-cert` / `tlsCert`: TLS Certificate @@ -2216,6 +2224,7 @@ ``` The dev server will: + - force `NODE_ENV=development` and warn you if it was previously set to something else - rebuild your app whenever your Remix app code changes - restart your app server whenever rebuilds succeed diff --git a/packages/react-router-dev/package.json b/packages/react-router-dev/package.json index 9d558e4d0c..2722b58c1a 100644 --- a/packages/react-router-dev/package.json +++ b/packages/react-router-dev/package.json @@ -1,6 +1,6 @@ { "name": "@react-router/dev", - "version": "7.9.4-pre.0", + "version": "7.9.4", "description": "Dev tools and CLI for React Router", "homepage": "https://reactrouter.com", "bugs": { diff --git a/packages/react-router-dom/CHANGELOG.md b/packages/react-router-dom/CHANGELOG.md index fb278e5db9..3eef443c6c 100644 --- a/packages/react-router-dom/CHANGELOG.md +++ b/packages/react-router-dom/CHANGELOG.md @@ -1,11 +1,11 @@ # react-router-dom -## 7.9.4-pre.0 +## 7.9.4 ### Patch Changes - Updated dependencies: - - `react-router@7.9.4-pre.0` + - `react-router@7.9.4` ## 7.9.3 diff --git a/packages/react-router-dom/package.json b/packages/react-router-dom/package.json index 07882a79b7..348dc4fb66 100644 --- a/packages/react-router-dom/package.json +++ b/packages/react-router-dom/package.json @@ -1,6 +1,6 @@ { "name": "react-router-dom", - "version": "7.9.4-pre.0", + "version": "7.9.4", "description": "Declarative routing for React web applications", "keywords": [ "react", diff --git a/packages/react-router-express/CHANGELOG.md b/packages/react-router-express/CHANGELOG.md index 2abc575995..069d309d11 100644 --- a/packages/react-router-express/CHANGELOG.md +++ b/packages/react-router-express/CHANGELOG.md @@ -1,12 +1,12 @@ # `@react-router/express` -## 7.9.4-pre.0 +## 7.9.4 ### Patch Changes - Updated dependencies: - - `react-router@7.9.4-pre.0` - - `@react-router/node@7.9.4-pre.0` + - `react-router@7.9.4` + - `@react-router/node@7.9.4` ## 7.9.3 @@ -39,6 +39,7 @@ - Stabilize middleware and context APIs. ([#14215](https://github.com/remix-run/react-router/pull/14215)) We have removed the `unstable_` prefix from the following APIs and they are now considered stable and ready for production use: + - [`RouterContextProvider`](https://reactrouter.com/api/utils/RouterContextProvider) - [`createContext`](https://reactrouter.com/api/utils/createContext) - `createBrowserRouter` [`getContext`](https://reactrouter.com/api/data-routers/createBrowserRouter#optsgetcontext) option diff --git a/packages/react-router-express/package.json b/packages/react-router-express/package.json index 69ccc57335..fd349d39ea 100644 --- a/packages/react-router-express/package.json +++ b/packages/react-router-express/package.json @@ -1,6 +1,6 @@ { "name": "@react-router/express", - "version": "7.9.4-pre.0", + "version": "7.9.4", "description": "Express server request handler for React Router", "bugs": { "url": "https://github.com/remix-run/react-router/issues" diff --git a/packages/react-router-fs-routes/CHANGELOG.md b/packages/react-router-fs-routes/CHANGELOG.md index 3bb3c8ae43..0b8d38e5bb 100644 --- a/packages/react-router-fs-routes/CHANGELOG.md +++ b/packages/react-router-fs-routes/CHANGELOG.md @@ -1,11 +1,11 @@ # `@react-router/fs-routes` -## 7.9.4-pre.0 +## 7.9.4 ### Patch Changes - Updated dependencies: - - `@react-router/dev@7.9.4-pre.0` + - `@react-router/dev@7.9.4` ## 7.9.3 diff --git a/packages/react-router-fs-routes/package.json b/packages/react-router-fs-routes/package.json index 1b8d3d223e..606c050dfe 100644 --- a/packages/react-router-fs-routes/package.json +++ b/packages/react-router-fs-routes/package.json @@ -1,6 +1,6 @@ { "name": "@react-router/fs-routes", - "version": "7.9.4-pre.0", + "version": "7.9.4", "description": "File system routing conventions for React Router, for use within routes.ts", "bugs": { "url": "https://github.com/remix-run/react-router/issues" diff --git a/packages/react-router-node/CHANGELOG.md b/packages/react-router-node/CHANGELOG.md index 5fe4f76bfa..60f8387a04 100644 --- a/packages/react-router-node/CHANGELOG.md +++ b/packages/react-router-node/CHANGELOG.md @@ -1,12 +1,12 @@ # `@react-router/node` -## 7.9.4-pre.0 +## 7.9.4 ### Patch Changes - Validate format of incoming session ids ([#14426](https://github.com/remix-run/react-router/pull/14426)) - Updated dependencies: - - `react-router@7.9.4-pre.0` + - `react-router@7.9.4` ## 7.9.3 @@ -36,6 +36,7 @@ - Stabilize middleware and context APIs. ([#14215](https://github.com/remix-run/react-router/pull/14215)) We have removed the `unstable_` prefix from the following APIs and they are now considered stable and ready for production use: + - [`RouterContextProvider`](https://reactrouter.com/api/utils/RouterContextProvider) - [`createContext`](https://reactrouter.com/api/utils/createContext) - `createBrowserRouter` [`getContext`](https://reactrouter.com/api/data-routers/createBrowserRouter#optsgetcontext) option @@ -235,6 +236,7 @@ - Remove single fetch future flag. ([#11522](https://github.com/remix-run/react-router/pull/11522)) - For Remix consumers migrating to React Router, the `crypto` global from the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) is now required when using cookie and session APIs. This means that the following APIs are provided from `react-router` rather than platform-specific packages: ([#11837](https://github.com/remix-run/react-router/pull/11837)) + - `createCookie` - `createCookieSessionStorage` - `createMemorySessionStorage` @@ -243,6 +245,7 @@ For consumers running older versions of Node, the `installGlobals` function from `@remix-run/node` has been updated to define `globalThis.crypto`, using [Node's `require('node:crypto').webcrypto` implementation.](https://nodejs.org/api/webcrypto.html) Since platform-specific packages no longer need to implement this API, the following low-level APIs have been removed: + - `createCookieFactory` - `createSessionStorageFactory` - `createCookieSessionStorageFactory` @@ -650,10 +653,12 @@ - Introduces the `defer()` API from `@remix-run/router` with support for server-rendering and HTTP streaming. This utility allows you to defer values returned from `loader` functions by returning promises instead of resolved values. This has been refered to as _"sending a promise over the wire"_. ([#4920](https://github.com/remix-run/remix/pull/4920)) Informational Resources: + - - Documentation Resources (better docs specific to Remix are in the works): + - - - diff --git a/packages/react-router-node/package.json b/packages/react-router-node/package.json index 91ea0eeaef..d995c0702d 100644 --- a/packages/react-router-node/package.json +++ b/packages/react-router-node/package.json @@ -1,6 +1,6 @@ { "name": "@react-router/node", - "version": "7.9.4-pre.0", + "version": "7.9.4", "description": "Node.js platform abstractions for React Router", "bugs": { "url": "https://github.com/remix-run/react-router/issues" diff --git a/packages/react-router-remix-routes-option-adapter/CHANGELOG.md b/packages/react-router-remix-routes-option-adapter/CHANGELOG.md index 63d3bfa64a..73ffdbf88e 100644 --- a/packages/react-router-remix-routes-option-adapter/CHANGELOG.md +++ b/packages/react-router-remix-routes-option-adapter/CHANGELOG.md @@ -1,11 +1,11 @@ # `@react-router/remix-config-routes-adapter` -## 7.9.4-pre.0 +## 7.9.4 ### Patch Changes - Updated dependencies: - - `@react-router/dev@7.9.4-pre.0` + - `@react-router/dev@7.9.4` ## 7.9.3 diff --git a/packages/react-router-remix-routes-option-adapter/package.json b/packages/react-router-remix-routes-option-adapter/package.json index 17994e4ec1..8ed145ee96 100644 --- a/packages/react-router-remix-routes-option-adapter/package.json +++ b/packages/react-router-remix-routes-option-adapter/package.json @@ -1,6 +1,6 @@ { "name": "@react-router/remix-routes-option-adapter", - "version": "7.9.4-pre.0", + "version": "7.9.4", "description": "Adapter for Remix's \"routes\" config option, for use within routes.ts", "bugs": { "url": "https://github.com/remix-run/react-router/issues" diff --git a/packages/react-router-serve/CHANGELOG.md b/packages/react-router-serve/CHANGELOG.md index 0ef42b1322..48015455bd 100644 --- a/packages/react-router-serve/CHANGELOG.md +++ b/packages/react-router-serve/CHANGELOG.md @@ -1,13 +1,13 @@ # `@react-router/serve` -## 7.9.4-pre.0 +## 7.9.4 ### Patch Changes - Updated dependencies: - - `react-router@7.9.4-pre.0` - - `@react-router/node@7.9.4-pre.0` - - `@react-router/express@7.9.4-pre.0` + - `react-router@7.9.4` + - `@react-router/node@7.9.4` + - `@react-router/express@7.9.4` ## 7.9.3 @@ -654,10 +654,12 @@ - Introduces the `defer()` API from `@remix-run/router` with support for server-rendering and HTTP streaming. This utility allows you to defer values returned from `loader` functions by returning promises instead of resolved values. This has been refered to as _"sending a promise over the wire"_. ([#4920](https://github.com/remix-run/remix/pull/4920)) Informational Resources: + - - Documentation Resources (better docs specific to Remix are in the works): + - - - diff --git a/packages/react-router-serve/package.json b/packages/react-router-serve/package.json index 121fea9074..4b0ba29102 100644 --- a/packages/react-router-serve/package.json +++ b/packages/react-router-serve/package.json @@ -1,6 +1,6 @@ { "name": "@react-router/serve", - "version": "7.9.4-pre.0", + "version": "7.9.4", "description": "Production application server for React Router", "bugs": { "url": "https://github.com/remix-run/react-router/issues" diff --git a/packages/react-router/CHANGELOG.md b/packages/react-router/CHANGELOG.md index 5d713cc6fe..6ef3ba52ad 100644 --- a/packages/react-router/CHANGELOG.md +++ b/packages/react-router/CHANGELOG.md @@ -1,6 +1,6 @@ # `react-router` -## 7.9.4-pre.0 +## 7.9.4 ### Patch Changes @@ -153,6 +153,7 @@ - Stabilize middleware and context APIs. ([#14215](https://github.com/remix-run/react-router/pull/14215)) We have removed the `unstable_` prefix from the following APIs and they are now considered stable and ready for production use: + - [`RouterContextProvider`](https://reactrouter.com/api/utils/RouterContextProvider) - [`createContext`](https://reactrouter.com/api/utils/createContext) - `createBrowserRouter` [`getContext`](https://reactrouter.com/api/data-routers/createBrowserRouter#optsgetcontext) option @@ -179,7 +180,7 @@ - \[UNSTABLE] Add ``/`` prop for client side error reporting ([#14162](https://github.com/remix-run/react-router/pull/14162)) -- server action revalidation opt out via $SKIP_REVALIDATION field ([#14154](https://github.com/remix-run/react-router/pull/14154)) +- server action revalidation opt out via $SKIP\_REVALIDATION field ([#14154](https://github.com/remix-run/react-router/pull/14154)) - Properly escape interpolated param values in `generatePath()` ([#13530](https://github.com/remix-run/react-router/pull/13530)) @@ -228,6 +229,7 @@ - Remove dependency on `@types/node` in TypeScript declaration files ([#14059](https://github.com/remix-run/react-router/pull/14059)) - Fix types for `UIMatch` to reflect that the `loaderData`/`data` properties may be `undefined` ([#12206](https://github.com/remix-run/react-router/pull/12206)) + - When an `ErrorBoundary` is being rendered, not all active matches will have loader data available, since it may have been their `loader` that threw to trigger the boundary - The `UIMatch.data` type was not correctly handing this and would always reflect the presence of data, leading to the unexpected runtime errors when an `ErrorBoundary` was rendered - ⚠️ This may cause some type errors to show up in your code for unguarded `match.data` accesses - you should properly guard for `undefined` values in those scenarios. @@ -261,6 +263,7 @@ - \[UNSTABLE] When middleware is enabled, make the `context` parameter read-only (via `Readonly`) so that TypeScript will not allow you to write arbitrary fields to it in loaders, actions, or middleware. ([#14097](https://github.com/remix-run/react-router/pull/14097)) - \[UNSTABLE] Rename and alter the signature/functionality of the `unstable_respond` API in `staticHandler.query`/`staticHandler.queryRoute` ([#14103](https://github.com/remix-run/react-router/pull/14103)) + - The API has been renamed to `unstable_generateMiddlewareResponse` for clarity - The main functional change is that instead of running the loaders/actions before calling `unstable_respond` and handing you the result, we now pass a `query`/`queryRoute` function as a parameter and you execute the loaders/actions inside your callback, giving you full access to pre-processing and error handling - The `query` version of the API now has a signature of `(query: (r: Request) => Promise) => Promise` @@ -906,6 +909,7 @@ ``` Similar to server-side requests, a fresh `context` will be created per navigation (or `fetcher` call). If you have initial data you'd like to populate in the context for every request, you can provide an `unstable_getContext` function at the root of your app: + - Library mode - `createBrowserRouter(routes, { unstable_getContext })` - Framework mode - `` @@ -1093,6 +1097,7 @@ _No changes_ - Remove `future.v7_normalizeFormMethod` future flag ([#11697](https://github.com/remix-run/react-router/pull/11697)) - For Remix consumers migrating to React Router, the `crypto` global from the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) is now required when using cookie and session APIs. This means that the following APIs are provided from `react-router` rather than platform-specific packages: ([#11837](https://github.com/remix-run/react-router/pull/11837)) + - `createCookie` - `createCookieSessionStorage` - `createMemorySessionStorage` @@ -1101,6 +1106,7 @@ _No changes_ For consumers running older versions of Node, the `installGlobals` function from `@remix-run/node` has been updated to define `globalThis.crypto`, using [Node's `require('node:crypto').webcrypto` implementation.](https://nodejs.org/api/webcrypto.html) Since platform-specific packages no longer need to implement this API, the following low-level APIs have been removed: + - `createCookieFactory` - `createSessionStorageFactory` - `createCookieSessionStorageFactory` @@ -1256,6 +1262,7 @@ _No changes_ ``` This initial implementation targets type inference for: + - `Params` : Path parameters from your routing config in `routes.ts` including file-based routing - `LoaderData` : Loader data from `loader` and/or `clientLoader` within your route module - `ActionData` : Action data from `action` and/or `clientAction` within your route module @@ -1270,6 +1277,7 @@ _No changes_ ``` Check out our docs for more: + - [_Explanations > Type Safety_](https://reactrouter.com/dev/guides/explanation/type-safety) - [_How-To > Setting up type safety_](https://reactrouter.com/dev/guides/how-to/setting-up-type-safety) diff --git a/packages/react-router/package.json b/packages/react-router/package.json index 1f1ef9ac12..f89b50e9ba 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -1,6 +1,6 @@ { "name": "react-router", - "version": "7.9.4-pre.0", + "version": "7.9.4", "description": "Declarative routing for React", "keywords": [ "react", From b2be7f648b7d41ccbe0984e621b5795534a4d261 Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Wed, 8 Oct 2025 17:36:02 +0000 Subject: [PATCH 16/16] chore: format --- integration/CHANGELOG.md | 1 - packages/react-router-architect/CHANGELOG.md | 3 --- packages/react-router-cloudflare/CHANGELOG.md | 3 --- packages/react-router-dev/CHANGELOG.md | 8 -------- packages/react-router-express/CHANGELOG.md | 1 - packages/react-router-node/CHANGELOG.md | 5 ----- packages/react-router-serve/CHANGELOG.md | 2 -- packages/react-router/CHANGELOG.md | 10 +--------- 8 files changed, 1 insertion(+), 32 deletions(-) diff --git a/integration/CHANGELOG.md b/integration/CHANGELOG.md index 6fccf850d7..2cf67d87b7 100644 --- a/integration/CHANGELOG.md +++ b/integration/CHANGELOG.md @@ -5,7 +5,6 @@ ### Minor Changes - Unstable Vite support for Node-based Remix apps ([#7590](https://github.com/remix-run/remix/pull/7590)) - - `remix build` 👉 `vite build && vite build --ssr` - `remix dev` 👉 `vite dev` diff --git a/packages/react-router-architect/CHANGELOG.md b/packages/react-router-architect/CHANGELOG.md index 8dd3f96677..16cf02621b 100644 --- a/packages/react-router-architect/CHANGELOG.md +++ b/packages/react-router-architect/CHANGELOG.md @@ -39,7 +39,6 @@ - Stabilize middleware and context APIs. ([#14215](https://github.com/remix-run/react-router/pull/14215)) We have removed the `unstable_` prefix from the following APIs and they are now considered stable and ready for production use: - - [`RouterContextProvider`](https://reactrouter.com/api/utils/RouterContextProvider) - [`createContext`](https://reactrouter.com/api/utils/createContext) - `createBrowserRouter` [`getContext`](https://reactrouter.com/api/data-routers/createBrowserRouter#optsgetcontext) option @@ -263,7 +262,6 @@ ### Major Changes - For Remix consumers migrating to React Router, the `crypto` global from the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) is now required when using cookie and session APIs. This means that the following APIs are provided from `react-router` rather than platform-specific packages: ([#11837](https://github.com/remix-run/react-router/pull/11837)) - - `createCookie` - `createCookieSessionStorage` - `createMemorySessionStorage` @@ -272,7 +270,6 @@ For consumers running older versions of Node, the `installGlobals` function from `@remix-run/node` has been updated to define `globalThis.crypto`, using [Node's `require('node:crypto').webcrypto` implementation.](https://nodejs.org/api/webcrypto.html) Since platform-specific packages no longer need to implement this API, the following low-level APIs have been removed: - - `createCookieFactory` - `createSessionStorageFactory` - `createCookieSessionStorageFactory` diff --git a/packages/react-router-cloudflare/CHANGELOG.md b/packages/react-router-cloudflare/CHANGELOG.md index 46daa9383e..9a471e4a52 100644 --- a/packages/react-router-cloudflare/CHANGELOG.md +++ b/packages/react-router-cloudflare/CHANGELOG.md @@ -35,7 +35,6 @@ - Stabilize middleware and context APIs. ([#14215](https://github.com/remix-run/react-router/pull/14215)) We have removed the `unstable_` prefix from the following APIs and they are now considered stable and ready for production use: - - [`RouterContextProvider`](https://reactrouter.com/api/utils/RouterContextProvider) - [`createContext`](https://reactrouter.com/api/utils/createContext) - `createBrowserRouter` [`getContext`](https://reactrouter.com/api/data-routers/createBrowserRouter#optsgetcontext) option @@ -234,7 +233,6 @@ - For Remix consumers migrating to React Router, all exports from `@remix-run/cloudflare-pages` are now provided for React Router consumers in the `@react-router/cloudflare` package. There is no longer a separate package for Cloudflare Pages. ([#11801](https://github.com/remix-run/react-router/pull/11801)) - For Remix consumers migrating to React Router, the `crypto` global from the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) is now required when using cookie and session APIs. This means that the following APIs are provided from `react-router` rather than platform-specific packages: ([#11837](https://github.com/remix-run/react-router/pull/11837)) - - `createCookie` - `createCookieSessionStorage` - `createMemorySessionStorage` @@ -243,7 +241,6 @@ For consumers running older versions of Node, the `installGlobals` function from `@remix-run/node` has been updated to define `globalThis.crypto`, using [Node's `require('node:crypto').webcrypto` implementation.](https://nodejs.org/api/webcrypto.html) Since platform-specific packages no longer need to implement this API, the following low-level APIs have been removed: - - `createCookieFactory` - `createSessionStorageFactory` - `createCookieSessionStorageFactory` diff --git a/packages/react-router-dev/CHANGELOG.md b/packages/react-router-dev/CHANGELOG.md index 980bd3bab0..e9029d2b52 100644 --- a/packages/react-router-dev/CHANGELOG.md +++ b/packages/react-router-dev/CHANGELOG.md @@ -156,7 +156,6 @@ - Stabilize middleware and context APIs. ([#14215](https://github.com/remix-run/react-router/pull/14215)) We have removed the `unstable_` prefix from the following APIs and they are now considered stable and ready for production use: - - [`RouterContextProvider`](https://reactrouter.com/api/utils/RouterContextProvider) - [`createContext`](https://reactrouter.com/api/utils/createContext) - `createBrowserRouter` [`getContext`](https://reactrouter.com/api/data-routers/createBrowserRouter#optsgetcontext) option @@ -899,7 +898,6 @@ ``` This initial implementation targets type inference for: - - `Params` : Path parameters from your routing config in `routes.ts` including file-based routing - `LoaderData` : Loader data from `loader` and/or `clientLoader` within your route module - `ActionData` : Action data from `action` and/or `clientAction` within your route module @@ -914,7 +912,6 @@ ``` Check out our docs for more: - - [_Explanations > Type Safety_](https://reactrouter.com/dev/guides/explanation/type-safety) - [_How-To > Setting up type safety_](https://reactrouter.com/dev/guides/how-to/setting-up-type-safety) @@ -1114,7 +1111,6 @@ - Vite: Provide `Unstable_ServerBundlesFunction` and `Unstable_VitePluginConfig` types ([#8654](https://github.com/remix-run/remix/pull/8654)) - Vite: add `--sourcemapClient` and `--sourcemapServer` flags to `remix vite:build` ([#8613](https://github.com/remix-run/remix/pull/8613)) - - `--sourcemapClient` - `--sourcemapClient=inline` @@ -1451,7 +1447,6 @@ - Add support for `clientLoader`/`clientAction`/`HydrateFallback` route exports ([RFC](https://github.com/remix-run/remix/discussions/7634)) ([#8173](https://github.com/remix-run/remix/pull/8173)) Remix now supports loaders/actions that run on the client (in addition to, or instead of the loader/action that runs on the server). While we still recommend server loaders/actions for the majority of your data needs in a Remix app - these provide some levers you can pull for more advanced use-cases such as: - - Leveraging a data source local to the browser (i.e., `localStorage`) - Managing a client-side cache of server data (like `IndexedDB`) - Bypassing the Remix server in a BFF setup and hitting your API directly from the browser @@ -1855,7 +1850,6 @@ - Output esbuild metafiles for bundle analysis ([#6772](https://github.com/remix-run/remix/pull/6772)) Written to server build directory (`build/` by default): - - `metafile.css.json` - `metafile.js.json` (browser JS) - `metafile.server.json` (server JS) @@ -1953,7 +1947,6 @@ - built-in tls support ([#6483](https://github.com/remix-run/remix/pull/6483)) New options: - - `--tls-key` / `tlsKey`: TLS key - `--tls-cert` / `tlsCert`: TLS Certificate @@ -2224,7 +2217,6 @@ ``` The dev server will: - - force `NODE_ENV=development` and warn you if it was previously set to something else - rebuild your app whenever your Remix app code changes - restart your app server whenever rebuilds succeed diff --git a/packages/react-router-express/CHANGELOG.md b/packages/react-router-express/CHANGELOG.md index 069d309d11..3315423171 100644 --- a/packages/react-router-express/CHANGELOG.md +++ b/packages/react-router-express/CHANGELOG.md @@ -39,7 +39,6 @@ - Stabilize middleware and context APIs. ([#14215](https://github.com/remix-run/react-router/pull/14215)) We have removed the `unstable_` prefix from the following APIs and they are now considered stable and ready for production use: - - [`RouterContextProvider`](https://reactrouter.com/api/utils/RouterContextProvider) - [`createContext`](https://reactrouter.com/api/utils/createContext) - `createBrowserRouter` [`getContext`](https://reactrouter.com/api/data-routers/createBrowserRouter#optsgetcontext) option diff --git a/packages/react-router-node/CHANGELOG.md b/packages/react-router-node/CHANGELOG.md index 60f8387a04..4ec8cddc49 100644 --- a/packages/react-router-node/CHANGELOG.md +++ b/packages/react-router-node/CHANGELOG.md @@ -36,7 +36,6 @@ - Stabilize middleware and context APIs. ([#14215](https://github.com/remix-run/react-router/pull/14215)) We have removed the `unstable_` prefix from the following APIs and they are now considered stable and ready for production use: - - [`RouterContextProvider`](https://reactrouter.com/api/utils/RouterContextProvider) - [`createContext`](https://reactrouter.com/api/utils/createContext) - `createBrowserRouter` [`getContext`](https://reactrouter.com/api/data-routers/createBrowserRouter#optsgetcontext) option @@ -236,7 +235,6 @@ - Remove single fetch future flag. ([#11522](https://github.com/remix-run/react-router/pull/11522)) - For Remix consumers migrating to React Router, the `crypto` global from the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) is now required when using cookie and session APIs. This means that the following APIs are provided from `react-router` rather than platform-specific packages: ([#11837](https://github.com/remix-run/react-router/pull/11837)) - - `createCookie` - `createCookieSessionStorage` - `createMemorySessionStorage` @@ -245,7 +243,6 @@ For consumers running older versions of Node, the `installGlobals` function from `@remix-run/node` has been updated to define `globalThis.crypto`, using [Node's `require('node:crypto').webcrypto` implementation.](https://nodejs.org/api/webcrypto.html) Since platform-specific packages no longer need to implement this API, the following low-level APIs have been removed: - - `createCookieFactory` - `createSessionStorageFactory` - `createCookieSessionStorageFactory` @@ -653,12 +650,10 @@ - Introduces the `defer()` API from `@remix-run/router` with support for server-rendering and HTTP streaming. This utility allows you to defer values returned from `loader` functions by returning promises instead of resolved values. This has been refered to as _"sending a promise over the wire"_. ([#4920](https://github.com/remix-run/remix/pull/4920)) Informational Resources: - - - Documentation Resources (better docs specific to Remix are in the works): - - - - diff --git a/packages/react-router-serve/CHANGELOG.md b/packages/react-router-serve/CHANGELOG.md index 48015455bd..21f876abd6 100644 --- a/packages/react-router-serve/CHANGELOG.md +++ b/packages/react-router-serve/CHANGELOG.md @@ -654,12 +654,10 @@ - Introduces the `defer()` API from `@remix-run/router` with support for server-rendering and HTTP streaming. This utility allows you to defer values returned from `loader` functions by returning promises instead of resolved values. This has been refered to as _"sending a promise over the wire"_. ([#4920](https://github.com/remix-run/remix/pull/4920)) Informational Resources: - - - Documentation Resources (better docs specific to Remix are in the works): - - - - diff --git a/packages/react-router/CHANGELOG.md b/packages/react-router/CHANGELOG.md index 6ef3ba52ad..479bccdc23 100644 --- a/packages/react-router/CHANGELOG.md +++ b/packages/react-router/CHANGELOG.md @@ -153,7 +153,6 @@ - Stabilize middleware and context APIs. ([#14215](https://github.com/remix-run/react-router/pull/14215)) We have removed the `unstable_` prefix from the following APIs and they are now considered stable and ready for production use: - - [`RouterContextProvider`](https://reactrouter.com/api/utils/RouterContextProvider) - [`createContext`](https://reactrouter.com/api/utils/createContext) - `createBrowserRouter` [`getContext`](https://reactrouter.com/api/data-routers/createBrowserRouter#optsgetcontext) option @@ -180,7 +179,7 @@ - \[UNSTABLE] Add ``/`` prop for client side error reporting ([#14162](https://github.com/remix-run/react-router/pull/14162)) -- server action revalidation opt out via $SKIP\_REVALIDATION field ([#14154](https://github.com/remix-run/react-router/pull/14154)) +- server action revalidation opt out via $SKIP_REVALIDATION field ([#14154](https://github.com/remix-run/react-router/pull/14154)) - Properly escape interpolated param values in `generatePath()` ([#13530](https://github.com/remix-run/react-router/pull/13530)) @@ -229,7 +228,6 @@ - Remove dependency on `@types/node` in TypeScript declaration files ([#14059](https://github.com/remix-run/react-router/pull/14059)) - Fix types for `UIMatch` to reflect that the `loaderData`/`data` properties may be `undefined` ([#12206](https://github.com/remix-run/react-router/pull/12206)) - - When an `ErrorBoundary` is being rendered, not all active matches will have loader data available, since it may have been their `loader` that threw to trigger the boundary - The `UIMatch.data` type was not correctly handing this and would always reflect the presence of data, leading to the unexpected runtime errors when an `ErrorBoundary` was rendered - ⚠️ This may cause some type errors to show up in your code for unguarded `match.data` accesses - you should properly guard for `undefined` values in those scenarios. @@ -263,7 +261,6 @@ - \[UNSTABLE] When middleware is enabled, make the `context` parameter read-only (via `Readonly`) so that TypeScript will not allow you to write arbitrary fields to it in loaders, actions, or middleware. ([#14097](https://github.com/remix-run/react-router/pull/14097)) - \[UNSTABLE] Rename and alter the signature/functionality of the `unstable_respond` API in `staticHandler.query`/`staticHandler.queryRoute` ([#14103](https://github.com/remix-run/react-router/pull/14103)) - - The API has been renamed to `unstable_generateMiddlewareResponse` for clarity - The main functional change is that instead of running the loaders/actions before calling `unstable_respond` and handing you the result, we now pass a `query`/`queryRoute` function as a parameter and you execute the loaders/actions inside your callback, giving you full access to pre-processing and error handling - The `query` version of the API now has a signature of `(query: (r: Request) => Promise) => Promise` @@ -909,7 +906,6 @@ ``` Similar to server-side requests, a fresh `context` will be created per navigation (or `fetcher` call). If you have initial data you'd like to populate in the context for every request, you can provide an `unstable_getContext` function at the root of your app: - - Library mode - `createBrowserRouter(routes, { unstable_getContext })` - Framework mode - `` @@ -1097,7 +1093,6 @@ _No changes_ - Remove `future.v7_normalizeFormMethod` future flag ([#11697](https://github.com/remix-run/react-router/pull/11697)) - For Remix consumers migrating to React Router, the `crypto` global from the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) is now required when using cookie and session APIs. This means that the following APIs are provided from `react-router` rather than platform-specific packages: ([#11837](https://github.com/remix-run/react-router/pull/11837)) - - `createCookie` - `createCookieSessionStorage` - `createMemorySessionStorage` @@ -1106,7 +1101,6 @@ _No changes_ For consumers running older versions of Node, the `installGlobals` function from `@remix-run/node` has been updated to define `globalThis.crypto`, using [Node's `require('node:crypto').webcrypto` implementation.](https://nodejs.org/api/webcrypto.html) Since platform-specific packages no longer need to implement this API, the following low-level APIs have been removed: - - `createCookieFactory` - `createSessionStorageFactory` - `createCookieSessionStorageFactory` @@ -1262,7 +1256,6 @@ _No changes_ ``` This initial implementation targets type inference for: - - `Params` : Path parameters from your routing config in `routes.ts` including file-based routing - `LoaderData` : Loader data from `loader` and/or `clientLoader` within your route module - `ActionData` : Action data from `action` and/or `clientAction` within your route module @@ -1277,7 +1270,6 @@ _No changes_ ``` Check out our docs for more: - - [_Explanations > Type Safety_](https://reactrouter.com/dev/guides/explanation/type-safety) - [_How-To > Setting up type safety_](https://reactrouter.com/dev/guides/how-to/setting-up-type-safety)