From 0ef10abcde832d882e98fc0cf345622ffb979422 Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Tue, 6 Jan 2026 16:55:32 +0000 Subject: [PATCH 01/21] chore: generate markdown docs from jsdocs --- docs/api/rsc/matchRSCServerRequest.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/api/rsc/matchRSCServerRequest.md b/docs/api/rsc/matchRSCServerRequest.md index 69ff6e999c..2fbed41c7d 100644 --- a/docs/api/rsc/matchRSCServerRequest.md +++ b/docs/api/rsc/matchRSCServerRequest.md @@ -70,6 +70,7 @@ matchRSCServerRequest({ ```tsx async function matchRSCServerRequest({ + allowedActionOrigins, createTemporaryReferenceSet, basename, decodeReply, @@ -82,6 +83,7 @@ async function matchRSCServerRequest({ routes, generateResponse, }: { + allowedActionOrigins?: string[]; createTemporaryReferenceSet: () => unknown; basename?: string; decodeReply?: DecodeReplyFunction; @@ -107,6 +109,10 @@ async function matchRSCServerRequest({ ## Params +### opts.allowedActionOrigins + +Origin patterns that are allowed to execute actions. + ### opts.basename The basename to use when matching the request. From 02d430c16c5335b87ce63d8263fc7520bb722aff Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 7 Jan 2026 10:04:13 -0500 Subject: [PATCH 02/21] Bump @remix-run/node-fetch-server dep in dev package (#14704) --- .changeset/honest-planes-glow.md | 5 +++++ packages/react-router-dev/package.json | 2 +- .../vite/cloudflare-dev-proxy.ts | 6 ++++-- packages/react-router-dev/vite/node-adapter.ts | 9 +++++---- packages/react-router-dev/vite/plugin.ts | 16 +++++++++++++--- pnpm-lock.yaml | 10 +++++----- 6 files changed, 33 insertions(+), 15 deletions(-) create mode 100644 .changeset/honest-planes-glow.md diff --git a/.changeset/honest-planes-glow.md b/.changeset/honest-planes-glow.md new file mode 100644 index 0000000000..23e8b1fed6 --- /dev/null +++ b/.changeset/honest-planes-glow.md @@ -0,0 +1,5 @@ +--- +"@react-router/dev": patch +--- + +Bump @remix-run/node-fetch-server dep diff --git a/packages/react-router-dev/package.json b/packages/react-router-dev/package.json index de524fe450..ccf3d4e05a 100644 --- a/packages/react-router-dev/package.json +++ b/packages/react-router-dev/package.json @@ -77,7 +77,7 @@ "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@react-router/node": "workspace:*", - "@remix-run/node-fetch-server": "^0.9.0", + "@remix-run/node-fetch-server": "^0.13.0", "arg": "^5.0.1", "babel-dead-code-elimination": "^1.0.6", "chokidar": "^4.0.0", diff --git a/packages/react-router-dev/vite/cloudflare-dev-proxy.ts b/packages/react-router-dev/vite/cloudflare-dev-proxy.ts index 2492f7c743..2db12f6e8a 100644 --- a/packages/react-router-dev/vite/cloudflare-dev-proxy.ts +++ b/packages/react-router-dev/vite/cloudflare-dev-proxy.ts @@ -1,4 +1,3 @@ -import { sendResponse } from "@remix-run/node-fetch-server"; import { createRequestHandler } from "react-router"; import { type AppLoadContext, @@ -121,6 +120,9 @@ export const cloudflareDevProxyVitePlugin = ( } }, configureServer: async (viteDevServer) => { + // Async import here to allow ESM only module on Node 20.18. + // TODO(v8): Can move to a normal import when Node 20 support + const { sendResponse } = await import("@remix-run/node-fetch-server"); let context: Awaited>; let getContext = async () => { let { getPlatformProxy } = await importWrangler(); @@ -139,7 +141,7 @@ export const cloudflareDevProxyVitePlugin = ( )) as ServerBuild; let handler = createRequestHandler(build, "development"); - let req = fromNodeRequest(nodeReq, nodeRes); + let req = await fromNodeRequest(nodeReq, nodeRes); context ??= await getContext(); let loadContext = getLoadContext ? await getLoadContext({ request: req, context }) diff --git a/packages/react-router-dev/vite/node-adapter.ts b/packages/react-router-dev/vite/node-adapter.ts index 5a71c9c212..403add36ef 100644 --- a/packages/react-router-dev/vite/node-adapter.ts +++ b/packages/react-router-dev/vite/node-adapter.ts @@ -1,8 +1,6 @@ import type { ServerResponse } from "node:http"; -import { createRequest } from "@remix-run/node-fetch-server"; import type * as Vite from "vite"; - import invariant from "../invariant"; export type NodeRequestHandler = ( @@ -10,10 +8,10 @@ export type NodeRequestHandler = ( res: ServerResponse, ) => Promise; -export function fromNodeRequest( +export async function fromNodeRequest( nodeReq: Vite.Connect.IncomingMessage, nodeRes: ServerResponse, -): Request { +): Promise { // Use `req.originalUrl` so React Router is aware of the full path invariant( nodeReq.originalUrl, @@ -21,5 +19,8 @@ export function fromNodeRequest( ); nodeReq.url = nodeReq.originalUrl; + // Async import here to allow ESM only module on Node 20.18. + // TODO(v8): Can move to a normal import when Node 20 support + const { createRequest } = await import("@remix-run/node-fetch-server"); return createRequest(nodeReq, nodeRes); } diff --git a/packages/react-router-dev/vite/plugin.ts b/packages/react-router-dev/vite/plugin.ts index 62935c15bc..5ea6f1b1f6 100644 --- a/packages/react-router-dev/vite/plugin.ts +++ b/packages/react-router-dev/vite/plugin.ts @@ -15,7 +15,6 @@ import { import * as path from "node:path"; import * as url from "node:url"; import * as babel from "@babel/core"; -import { sendResponse } from "@remix-run/node-fetch-server"; import { unstable_setDevServerHooks as setDevServerHooks, createRequestHandler, @@ -1673,11 +1672,16 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => { nodeReq, nodeRes, ) => { - let req = fromNodeRequest(nodeReq, nodeRes); + let req = await fromNodeRequest(nodeReq, nodeRes); let res = await handler( req, await reactRouterDevLoadContext(req), ); + // Async import here to allow ESM only module on Node 20.18. + // TODO(v8): Can move to a normal import when Node 20 support + const { sendResponse } = await import( + "@remix-run/node-fetch-server" + ); await sendResponse(nodeRes, res); }; await nodeHandler(req, res); @@ -1717,11 +1721,17 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => { nodeReq, nodeRes, ) => { - let req = fromNodeRequest(nodeReq, nodeRes); + let req = await fromNodeRequest(nodeReq, nodeRes); let res = await handler( req, await reactRouterDevLoadContext(req), ); + + // Async import here to allow ESM only module on Node 20.18. + // TODO(v8): Can move to a normal import when Node 20 support + const { sendResponse } = await import( + "@remix-run/node-fetch-server" + ); await sendResponse(nodeRes, res); }; await nodeHandler(req, res); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9a9419e03b..774e438150 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1027,8 +1027,8 @@ importers: specifier: workspace:* version: link:../react-router-node '@remix-run/node-fetch-server': - specifier: ^0.9.0 - version: 0.9.0 + specifier: ^0.13.0 + version: 0.13.0 arg: specifier: ^5.0.1 version: 5.0.2 @@ -3567,8 +3567,8 @@ packages: '@remix-run/node-fetch-server@0.12.0': resolution: {integrity: sha512-oeg8w8aJJSuq1fCx85jCkcgTfI6On7sKwWVSO4/OW5AvTBuosAIwnuBd/LYeU/I7lYPOTW2NXhUfyfpyeexs4w==} - '@remix-run/node-fetch-server@0.9.0': - resolution: {integrity: sha512-SoLMv7dbH+njWzXnOY6fI08dFMI5+/dQ+vY3n8RnnbdG7MdJEgiP28Xj/xWlnRnED/aB6SFw56Zop+LbmaaKqA==} + '@remix-run/node-fetch-server@0.13.0': + resolution: {integrity: sha512-1EsNo0ZpgXu/90AWoRZf/oE3RVTUS80tiTUpt+hv5pjtAkw7icN4WskDwz/KdAw5ARbJLMhZBrO1NqThmy/McA==} '@remix-run/web-blob@3.1.0': resolution: {integrity: sha512-owGzFLbqPH9PlKb8KvpNJ0NO74HWE2euAn61eEiyCXX/oteoVzTVSN8mpLgDjaxBf2btj5/nUllSUgpyd6IH6g==} @@ -11121,7 +11121,7 @@ snapshots: '@remix-run/node-fetch-server@0.12.0': {} - '@remix-run/node-fetch-server@0.9.0': {} + '@remix-run/node-fetch-server@0.13.0': {} '@remix-run/web-blob@3.1.0': dependencies: From 690939f7feaafd54d9658152b93a6f8a4d273560 Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Wed, 7 Jan 2026 15:04:57 +0000 Subject: [PATCH 03/21] chore: deduplicate `pnpm-lock.yaml` --- pnpm-lock.yaml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 774e438150..c48d337aef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -147,7 +147,7 @@ importers: version: 7.34.1(eslint@8.57.0) eslint-plugin-react-hooks: specifier: next - version: 7.1.0-canary-b061b597-20251212(eslint@8.57.0) + version: 7.1.0-canary-65eec428-20251218(eslint@8.57.0) fast-glob: specifier: 3.2.11 version: 3.2.11 @@ -5587,8 +5587,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@7.1.0-canary-b061b597-20251212: - resolution: {integrity: sha512-tSQTLvkBVbN4rZZZjGIDeUVFgNrY8/hqU6mteRetyCuLYCP92r0x8hetyshU1jInpMQxeJitpOox8A2uMAIrrg==} + eslint-plugin-react-hooks@7.1.0-canary-65eec428-20251218: + resolution: {integrity: sha512-7HtYkBLCNKakC5OOJucfhzxT/Kp+C63yEnh614v9ZiC8PuydYiYs3gQzokbYhy1Uc29XtEXv5GZ/dui1H4Z9Zw==} 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 @@ -9064,10 +9064,12 @@ packages: whatwg-encoding@2.0.0: resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} engines: {node: '>=12'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-mimetype@3.0.0: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} @@ -13605,7 +13607,7 @@ snapshots: dependencies: eslint: 8.57.0 - eslint-plugin-react-hooks@7.1.0-canary-b061b597-20251212(eslint@8.57.0): + eslint-plugin-react-hooks@7.1.0-canary-65eec428-20251218(eslint@8.57.0): dependencies: '@babel/core': 7.27.7 '@babel/parser': 7.27.7 From c38d76ce4e9c7d3b689d7b375032859ea7b29d7f Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Wed, 7 Jan 2026 19:17:14 +0000 Subject: [PATCH 04/21] 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 | 22 +++++---------- 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 | 28 ++++++------------- 8 files changed, 15 insertions(+), 50 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 e9cbd01c5e..e7c07e020c 100644 --- a/packages/react-router-architect/CHANGELOG.md +++ b/packages/react-router-architect/CHANGELOG.md @@ -87,7 +87,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 @@ -311,7 +310,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` @@ -320,7 +318,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 5ae4fd0220..6c3ff8718e 100644 --- a/packages/react-router-cloudflare/CHANGELOG.md +++ b/packages/react-router-cloudflare/CHANGELOG.md @@ -77,7 +77,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 @@ -276,7 +275,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` @@ -285,7 +283,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 386e68cd2c..8e06400721 100644 --- a/packages/react-router-dev/CHANGELOG.md +++ b/packages/react-router-dev/CHANGELOG.md @@ -18,25 +18,25 @@ | URL `/a/b/c` | **HTTP pathname** | **`request` pathname\`** | | ------------ | ----------------- | ------------------------ | - | **Document** | `/a/b/c` | `/a/b/c` ✅ | - | **Data** | `/a/b/c.data` | `/a/b/c` ✅ | + | **Document** | `/a/b/c` | `/a/b/c` ✅ | + | **Data** | `/a/b/c.data` | `/a/b/c` ✅ | | URL `/a/b/c/` | **HTTP pathname** | **`request` pathname\`** | | ------------- | ----------------- | ------------------------ | - | **Document** | `/a/b/c/` | `/a/b/c/` ✅ | + | **Document** | `/a/b/c/` | `/a/b/c/` ✅ | | **Data** | `/a/b/c.data` | `/a/b/c` ⚠️ | With this flag enabled, these pathnames will be made consistent though a new `_.data` format for client-side `.data` requests: | URL `/a/b/c` | **HTTP pathname** | **`request` pathname\`** | | ------------ | ----------------- | ------------------------ | - | **Document** | `/a/b/c` | `/a/b/c` ✅ | - | **Data** | `/a/b/c.data` | `/a/b/c` ✅ | + | **Document** | `/a/b/c` | `/a/b/c` ✅ | + | **Data** | `/a/b/c.data` | `/a/b/c` ✅ | | URL `/a/b/c/` | **HTTP pathname** | **`request` pathname\`** | | ------------- | ------------------ | ------------------------ | - | **Document** | `/a/b/c/` | `/a/b/c/` ✅ | - | **Data** | `/a/b/c/_.data` ⬅️ | `/a/b/c/` ✅ | + | **Document** | `/a/b/c/` | `/a/b/c/` ✅ | + | **Data** | `/a/b/c/_.data` ⬅️ | `/a/b/c/` ✅ | This a bug fix but we are putting it behind an opt-in flag because it has the potential to be a "breaking bug fix" if you are relying on the URL format for any other application or caching logic. @@ -294,7 +294,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 @@ -1037,7 +1036,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 @@ -1052,7 +1050,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) @@ -1252,7 +1249,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` @@ -1589,7 +1585,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 @@ -1993,7 +1988,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) @@ -2091,7 +2085,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 @@ -2362,7 +2355,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 416b894aef..d7250df76a 100644 --- a/packages/react-router-express/CHANGELOG.md +++ b/packages/react-router-express/CHANGELOG.md @@ -87,7 +87,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 c3bbfe486d..1018e18450 100644 --- a/packages/react-router-node/CHANGELOG.md +++ b/packages/react-router-node/CHANGELOG.md @@ -78,7 +78,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 @@ -278,7 +277,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` @@ -287,7 +285,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` @@ -695,12 +692,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 c5815264fb..bddf4f8296 100644 --- a/packages/react-router-serve/CHANGELOG.md +++ b/packages/react-router-serve/CHANGELOG.md @@ -710,12 +710,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 b992d3b4a6..3940384f29 100644 --- a/packages/react-router/CHANGELOG.md +++ b/packages/react-router/CHANGELOG.md @@ -24,25 +24,25 @@ | URL `/a/b/c` | **HTTP pathname** | **`request` pathname\`** | | ------------ | ----------------- | ------------------------ | - | **Document** | `/a/b/c` | `/a/b/c` ✅ | - | **Data** | `/a/b/c.data` | `/a/b/c` ✅ | + | **Document** | `/a/b/c` | `/a/b/c` ✅ | + | **Data** | `/a/b/c.data` | `/a/b/c` ✅ | | URL `/a/b/c/` | **HTTP pathname** | **`request` pathname\`** | | ------------- | ----------------- | ------------------------ | - | **Document** | `/a/b/c/` | `/a/b/c/` ✅ | + | **Document** | `/a/b/c/` | `/a/b/c/` ✅ | | **Data** | `/a/b/c.data` | `/a/b/c` ⚠️ | With this flag enabled, these pathnames will be made consistent though a new `_.data` format for client-side `.data` requests: | URL `/a/b/c` | **HTTP pathname** | **`request` pathname\`** | | ------------ | ----------------- | ------------------------ | - | **Document** | `/a/b/c` | `/a/b/c` ✅ | - | **Data** | `/a/b/c.data` | `/a/b/c` ✅ | + | **Document** | `/a/b/c` | `/a/b/c` ✅ | + | **Data** | `/a/b/c.data` | `/a/b/c` ✅ | | URL `/a/b/c/` | **HTTP pathname** | **`request` pathname\`** | | ------------- | ------------------ | ------------------------ | - | **Document** | `/a/b/c/` | `/a/b/c/` ✅ | - | **Data** | `/a/b/c/_.data` ⬅️ | `/a/b/c/` ✅ | + | **Document** | `/a/b/c/` | `/a/b/c/` ✅ | + | **Data** | `/a/b/c/_.data` ⬅️ | `/a/b/c/` ✅ | This a bug fix but we are putting it behind an opt-in flag because it has the potential to be a "breaking bug fix" if you are relying on the URL format for any other application or caching logic. @@ -69,14 +69,12 @@ - \[UNSTABLE] Add a new `unstable_defaultShouldRevalidate` flag to various APIs to allow opt-ing out of standard revalidation behaviors. ([#14542](https://github.com/remix-run/react-router/pull/14542)) If active routes include a `shouldRevalidate` function, then your value will be passed as `defaultShouldRevalidate` in those function so that the route always has the final revalidation determination. - - `
` - `submit(data, { method: "post", unstable_defaultShouldRevalidate: false })` - `` - `fetcher.submit(data, { method: "post", unstable_defaultShouldRevalidate: false })` This is also available on non-submission APIs that may trigger revalidations due to changing search params: - - `` - `navigate("/?foo=bar", { unstable_defaultShouldRevalidate: false })` - `setSearchParams(params, { unstable_defaultShouldRevalidate: false })` @@ -99,7 +97,6 @@ - ⚠️ This is a breaking change if you have begun using `fetcher.unstable_reset()` - Stabilize the `dataStrategy` `match.shouldRevalidateArgs`/`match.shouldCallHandler()` APIs. ([#14592](https://github.com/remix-run/react-router/pull/14592)) - - The `match.shouldLoad` API is now marked deprecated in favor of these more powerful alternatives - If you're using this API in a custom `dataStrategy` today, you can swap to the new API at your convenience: @@ -228,7 +225,6 @@ - Ensure action handlers run for routes with middleware even if no loader is present ([#14443](https://github.com/remix-run/react-router/pull/14443)) - Add `unstable_instrumentations` API to allow users to add observablity to their apps by instrumenting route loaders, actions, middlewares, lazy, as well as server-side request handlers and client side navigations/fetches ([#14412](https://github.com/remix-run/react-router/pull/14412)) - - Framework Mode: - `entry.server.tsx`: `export const unstable_instrumentations = [...]` - `entry.client.tsx`: `` @@ -390,7 +386,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 @@ -417,7 +412,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)) @@ -466,7 +461,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. @@ -500,7 +494,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` @@ -1146,7 +1139,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 - `` @@ -1334,7 +1326,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` @@ -1343,7 +1334,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` @@ -1499,7 +1489,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 @@ -1514,7 +1503,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 dd08f8d3b152ac3b6e7d126680fb74d88d18de9a Mon Sep 17 00:00:00 2001 From: joseph0926 Date: Thu, 8 Jan 2026 16:15:12 +0900 Subject: [PATCH 05/21] fix(react-router): add crossOrigin prop to Links component (#14687) --- .changeset/thick-meals-worry.md | 5 + .../__tests__/dom/ssr/links-test.tsx | 173 ++++++++++++++++++ .../react-router/lib/dom/ssr/components.tsx | 31 +++- 3 files changed, 205 insertions(+), 4 deletions(-) create mode 100644 .changeset/thick-meals-worry.md create mode 100644 packages/react-router/__tests__/dom/ssr/links-test.tsx diff --git a/.changeset/thick-meals-worry.md b/.changeset/thick-meals-worry.md new file mode 100644 index 0000000000..0f128e0ba2 --- /dev/null +++ b/.changeset/thick-meals-worry.md @@ -0,0 +1,5 @@ +--- +"react-router": minor +--- + +Add `crossOrigin` prop to `Links` component diff --git a/packages/react-router/__tests__/dom/ssr/links-test.tsx b/packages/react-router/__tests__/dom/ssr/links-test.tsx new file mode 100644 index 0000000000..9c44f731b2 --- /dev/null +++ b/packages/react-router/__tests__/dom/ssr/links-test.tsx @@ -0,0 +1,173 @@ +import { render } from "@testing-library/react"; +import * as React from "react"; + +import { Links, Outlet, createRoutesStub } from "../../../index"; + +describe("", () => { + describe("crossOrigin", () => { + it("renders stylesheet links with crossOrigin attribute when provided", () => { + let RoutesStub = createRoutesStub([ + { + id: "root", + path: "/", + links: () => [{ rel: "stylesheet", href: "/assets/styles.css" }], + Component() { + return ( + <> + + + + ); + }, + children: [ + { id: "index", index: true, Component: () =>
Index
}, + ], + }, + ]); + + let { container } = render(); + + let stylesheetLink = container.ownerDocument.querySelector( + 'link[rel="stylesheet"][href="/assets/styles.css"]', + ); + expect(stylesheetLink).toBeTruthy(); + expect(stylesheetLink?.getAttribute("crossorigin")).toBe("anonymous"); + }); + + it("renders stylesheet links without crossOrigin when not provided", () => { + let RoutesStub = createRoutesStub([ + { + id: "root", + path: "/", + links: () => [{ rel: "stylesheet", href: "/assets/styles.css" }], + Component() { + return ( + <> + + + + ); + }, + children: [ + { id: "index", index: true, Component: () =>
Index
}, + ], + }, + ]); + + let { container } = render(); + + let stylesheetLink = container.ownerDocument.querySelector( + 'link[rel="stylesheet"][href="/assets/styles.css"]', + ); + expect(stylesheetLink).toBeTruthy(); + expect(stylesheetLink?.hasAttribute("crossorigin")).toBe(false); + }); + + it("link descriptor crossOrigin overrides the component prop", () => { + let RoutesStub = createRoutesStub([ + { + id: "root", + path: "/", + links: () => [ + { + rel: "stylesheet", + href: "/assets/styles.css", + crossOrigin: "use-credentials", + }, + ], + Component() { + return ( + <> + + + + ); + }, + children: [ + { id: "index", index: true, Component: () =>
Index
}, + ], + }, + ]); + + let { container } = render(); + + let stylesheetLink = container.ownerDocument.querySelector( + 'link[rel="stylesheet"][href="/assets/styles.css"]', + ); + expect(stylesheetLink).toBeTruthy(); + expect(stylesheetLink?.getAttribute("crossorigin")).toBe( + "use-credentials", + ); + }); + + it("link descriptor crossOrigin works without the component prop", () => { + let RoutesStub = createRoutesStub([ + { + id: "root", + path: "/", + links: () => [ + { + rel: "stylesheet", + href: "/assets/styles.css", + crossOrigin: "anonymous", + }, + ], + Component() { + return ( + <> + + + + ); + }, + children: [ + { id: "index", index: true, Component: () =>
Index
}, + ], + }, + ]); + + let { container } = render(); + + let stylesheetLink = container.ownerDocument.querySelector( + 'link[rel="stylesheet"][href="/assets/styles.css"]', + ); + expect(stylesheetLink).toBeTruthy(); + expect(stylesheetLink?.getAttribute("crossorigin")).toBe("anonymous"); + }); + + it("link descriptor crossOrigin undefined does not override the component prop", () => { + let RoutesStub = createRoutesStub([ + { + id: "root", + path: "/", + links: () => [ + { + rel: "stylesheet", + href: "/assets/styles.css", + crossOrigin: undefined, + }, + ], + Component() { + return ( + <> + + + + ); + }, + children: [ + { id: "index", index: true, Component: () =>
Index
}, + ], + }, + ]); + + let { container } = render(); + + let stylesheetLink = container.ownerDocument.querySelector( + 'link[rel="stylesheet"][href="/assets/styles.css"]', + ); + expect(stylesheetLink).toBeTruthy(); + expect(stylesheetLink?.getAttribute("crossorigin")).toBe("anonymous"); + }); + }); +}); diff --git a/packages/react-router/lib/dom/ssr/components.tsx b/packages/react-router/lib/dom/ssr/components.tsx index 48fb544fb3..c29f73c73d 100644 --- a/packages/react-router/lib/dom/ssr/components.tsx +++ b/packages/react-router/lib/dom/ssr/components.tsx @@ -227,6 +227,12 @@ export interface LinksProps { * element */ nonce?: string | undefined; + /** + * A [`crossOrigin`](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin) + * attribute to render on the [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link) + * element + */ + crossOrigin?: "anonymous" | "use-credentials"; } /** @@ -254,10 +260,11 @@ export interface LinksProps { * @mode framework * @param props Props * @param {LinksProps.nonce} props.nonce n/a + * @param {LinksProps.crossOrigin} props.crossOrigin n/a * @returns A collection of React elements for [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link) * tags */ -export function Links({ nonce }: LinksProps): React.JSX.Element { +export function Links({ nonce, crossOrigin }: LinksProps): React.JSX.Element { let { isSpaMode, manifest, routeModules, criticalCss } = useFrameworkContext(); let { errors, matches: routerMatches } = useDataRouterStateContext(); @@ -283,13 +290,24 @@ export function Links({ nonce }: LinksProps): React.JSX.Element { rel="stylesheet" href={criticalCss.href} nonce={nonce} + crossOrigin={crossOrigin} /> ) : null} {keyedLinks.map(({ key, link }) => isPageLinkDescriptor(link) ? ( - + ) : ( - + ), )} @@ -487,7 +505,12 @@ function PrefetchPageLinksImpl({ {keyedPrefetchLinks.map(({ key, link }) => ( // these don't spread `linkProps` because they are full link descriptors // already with their own props - + ))} ); From b248e39d0f5682ce156593a918ec369bff918087 Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Thu, 8 Jan 2026 07:17:11 +0000 Subject: [PATCH 06/21] chore: generate markdown docs from jsdocs --- docs/api/components/Links.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/api/components/Links.md b/docs/api/components/Links.md index b1125a41a3..fa48661c7e 100644 --- a/docs/api/components/Links.md +++ b/docs/api/components/Links.md @@ -45,7 +45,7 @@ export default function Root() { ## Signature ```tsx -function Links({ nonce }: LinksProps): React.JSX.Element +function Links({ nonce, crossOrigin }: LinksProps): React.JSX.Element ``` ## Props @@ -56,3 +56,9 @@ A [`nonce`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_a attribute to render on the [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link) element +### crossOrigin + +A [`crossOrigin`](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin) +attribute to render on the [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link) +element + From 9949c1dc5294d8aeb9e873b60b9c0b8f3623fa63 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 8 Jan 2026 16:00:02 -0500 Subject: [PATCH 07/21] Add data mode playground --- playground/data/index.html | 12 ++++++++++++ playground/data/package.json | 23 +++++++++++++++++++++++ playground/data/src/main.tsx | 24 ++++++++++++++++++++++++ playground/data/tsconfig.json | 17 +++++++++++++++++ playground/data/vite.config.ts | 7 +++++++ pnpm-lock.yaml | 28 ++++++++++++++++++++++++++++ 6 files changed, 111 insertions(+) create mode 100644 playground/data/index.html create mode 100644 playground/data/package.json create mode 100644 playground/data/src/main.tsx create mode 100644 playground/data/tsconfig.json create mode 100644 playground/data/vite.config.ts diff --git a/playground/data/index.html b/playground/data/index.html new file mode 100644 index 0000000000..7e2cc201cf --- /dev/null +++ b/playground/data/index.html @@ -0,0 +1,12 @@ + + + + + + React Router (data mode) + + +
+ + + diff --git a/playground/data/package.json b/playground/data/package.json new file mode 100644 index 0000000000..3a86597ed1 --- /dev/null +++ b/playground/data/package.json @@ -0,0 +1,23 @@ +{ + "name": "@playground/data", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "catalog:", + "react-dom": "catalog:", + "react-router": "workspace:" + }, + "devDependencies": { + "@types/react": "catalog:", + "@types/react-dom": "catalog:", + "@vitejs/plugin-react": "^4", + "typescript": "catalog:", + "vite": "6.4.1" + } +} diff --git a/playground/data/src/main.tsx b/playground/data/src/main.tsx new file mode 100644 index 0000000000..7dd9a9387c --- /dev/null +++ b/playground/data/src/main.tsx @@ -0,0 +1,24 @@ +import * as React from "react"; +import * as ReactClient from "react-dom/client"; +import { createBrowserRouter, useLoaderData } from "react-router"; +import { RouterProvider } from "react-router/dom"; + +const router = createBrowserRouter([ + { + id: "index", + path: "/", + loader() { + return { message: "Hello React Router!" }; + }, + Component() { + let data = useLoaderData(); + return

{data.message}

; + }, + }, +]); + +ReactClient.createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/playground/data/tsconfig.json b/playground/data/tsconfig.json new file mode 100644 index 0000000000..ee47d3b35f --- /dev/null +++ b/playground/data/tsconfig.json @@ -0,0 +1,17 @@ +{ + "include": ["src"], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "jsx": "react-jsx", + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "verbatimModuleSyntax": true, + "esModuleInterop": true, + "strict": true, + "allowJs": true, + "skipLibCheck": true, + "noEmit": true + } +} diff --git a/playground/data/vite.config.ts b/playground/data/vite.config.ts new file mode 100644 index 0000000000..0e43ae8def --- /dev/null +++ b/playground/data/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c48d337aef..08e1c71ef8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1324,6 +1324,34 @@ importers: specifier: 'catalog:' version: 0.14.9 + playground/data: + dependencies: + react: + specifier: 'catalog:' + version: 19.2.3 + react-dom: + specifier: 'catalog:' + version: 19.2.3(react@19.2.3) + react-router: + specifier: 'workspace:' + version: link:../../packages/react-router + devDependencies: + '@types/react': + specifier: ^18.0.27 + version: 18.2.18 + '@types/react-dom': + specifier: ^18.0.10 + version: 18.2.7 + '@vitejs/plugin-react': + specifier: ^4 + version: 4.5.2(vite@6.4.1(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.0)) + typescript: + specifier: 'catalog:' + version: 5.4.5 + vite: + specifier: 6.4.1 + version: 6.4.1(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.0) + playground/framework: dependencies: '@react-router/node': From e630aeae097c2ffd7a98c46afeeb60e710556111 Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Thu, 8 Jan 2026 21:01:16 +0000 Subject: [PATCH 08/21] 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 08e1c71ef8..de6f00b290 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -147,7 +147,7 @@ importers: version: 7.34.1(eslint@8.57.0) eslint-plugin-react-hooks: specifier: next - version: 7.1.0-canary-65eec428-20251218(eslint@8.57.0) + version: 7.1.0-canary-d6cae440-20260106(eslint@8.57.0) fast-glob: specifier: 3.2.11 version: 3.2.11 @@ -5615,8 +5615,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@7.1.0-canary-65eec428-20251218: - resolution: {integrity: sha512-7HtYkBLCNKakC5OOJucfhzxT/Kp+C63yEnh614v9ZiC8PuydYiYs3gQzokbYhy1Uc29XtEXv5GZ/dui1H4Z9Zw==} + eslint-plugin-react-hooks@7.1.0-canary-d6cae440-20260106: + resolution: {integrity: sha512-uIHYvMstCMzczNT658m/AvxiHZA77RIyU8vZpyixi8YfZXiPLIaKZEhqt8ce3OLwnaqISYoyPtkpxPtk6Z5Ydw==} 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 @@ -13635,7 +13635,7 @@ snapshots: dependencies: eslint: 8.57.0 - eslint-plugin-react-hooks@7.1.0-canary-65eec428-20251218(eslint@8.57.0): + eslint-plugin-react-hooks@7.1.0-canary-d6cae440-20260106(eslint@8.57.0): dependencies: '@babel/core': 7.27.7 '@babel/parser': 7.27.7 From 9987ba0a22f71df9b5d0a94252d43532c7d1067d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=B3=C3=B0i=20Karlsson?= <53127288+frodi-karlsson@users.noreply.github.com> Date: Fri, 9 Jan 2026 05:38:20 +0100 Subject: [PATCH 09/21] fix(fs-routes): resolve route file outside of app dir relative to app dir (#13937) --- .changeset/red-pugs-itch.md | 5 +++++ .../__tests__/flatRoutes-test.ts | 20 +++++++++++++++++++ packages/react-router-fs-routes/flatRoutes.ts | 7 ++++--- 3 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 .changeset/red-pugs-itch.md diff --git a/.changeset/red-pugs-itch.md b/.changeset/red-pugs-itch.md new file mode 100644 index 0000000000..3429182931 --- /dev/null +++ b/.changeset/red-pugs-itch.md @@ -0,0 +1,5 @@ +--- +"@react-router/fs-routes": patch +--- + +Fix route file paths when routes directory is outside of the app directory diff --git a/packages/react-router-fs-routes/__tests__/flatRoutes-test.ts b/packages/react-router-fs-routes/__tests__/flatRoutes-test.ts index 94a77f237b..964d20e27e 100644 --- a/packages/react-router-fs-routes/__tests__/flatRoutes-test.ts +++ b/packages/react-router-fs-routes/__tests__/flatRoutes-test.ts @@ -912,4 +912,24 @@ describe("flatRoutes", () => { ); }); }); + + describe("generates route manifest entry files relative to the app directory", () => { + test("routes directory inside the app directory", () => { + let routeFile = path.posix.join(APP_DIR, "routes", "route.tsx"); + let routeInfo = flatRoutesUniversal(APP_DIR, [routeFile]); + let routes = Object.values(routeInfo); + + expect(routes).toHaveLength(1); + expect(routes[0].file).toBe("routes/route.tsx"); + }); + + test("routes directory outside the app directory", () => { + let routeFile = path.posix.join(APP_DIR, "..", "routes", "route.tsx"); + let routeInfo = flatRoutesUniversal(APP_DIR, [routeFile]); + let routes = Object.values(routeInfo); + + expect(routes).toHaveLength(1); + expect(routes[0].file).toBe("../routes/route.tsx"); + }); + }); }); diff --git a/packages/react-router-fs-routes/flatRoutes.ts b/packages/react-router-fs-routes/flatRoutes.ts index 8ab6c05f8f..ae13cfdd8a 100644 --- a/packages/react-router-fs-routes/flatRoutes.ts +++ b/packages/react-router-fs-routes/flatRoutes.ts @@ -134,6 +134,8 @@ export function flatRoutesUniversal( let prefixLookup = new PrefixLookupTrie(); let uniqueRoutes = new Map(); let routeIdConflicts = new Map(); + let normalizedApp = normalizeSlashes(appDirectory); + let appWithPrefix = path.posix.join(normalizedApp, prefix); // id -> file let routeIds = new Map(); @@ -142,9 +144,8 @@ export function flatRoutesUniversal( let normalizedFile = normalizeSlashes(file); let routeExt = path.extname(normalizedFile); let routeDir = path.dirname(normalizedFile); - let normalizedApp = normalizeSlashes(appDirectory); let routeId = - routeDir === path.posix.join(normalizedApp, prefix) + routeDir === appWithPrefix ? path.posix .relative(normalizedApp, normalizedFile) .slice(0, -routeExt.length) @@ -174,7 +175,7 @@ export function flatRoutesUniversal( let pathname = createRoutePath(segments, raw, index); routeManifest[routeId] = { - file: file.slice(appDirectory.length + 1), + file: path.posix.relative(normalizedApp, file), id: routeId, path: pathname, }; From 0bb972b74740cad416f5e551c10b54f2c20078ef Mon Sep 17 00:00:00 2001 From: AnandShiva Date: Tue, 13 Jan 2026 01:39:38 +0530 Subject: [PATCH 10/21] fix(react-router/dom/ssr): add `nonce` to inline critical css (#14691) --- .changeset/odd-crabs-sing.md | 5 + contributors.yml | 1 + .../__tests__/dom/ssr/components-test.tsx | 176 +++++++++++++++++- .../react-router/lib/dom/ssr/components.tsx | 1 + 4 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 .changeset/odd-crabs-sing.md diff --git a/.changeset/odd-crabs-sing.md b/.changeset/odd-crabs-sing.md new file mode 100644 index 0000000000..a233edc766 --- /dev/null +++ b/.changeset/odd-crabs-sing.md @@ -0,0 +1,5 @@ +--- +"react-router": patch +--- + +Bugfix #14666: Inline criticalCss is missing nonce diff --git a/contributors.yml b/contributors.yml index 38278eb0c9..3f3ea48c6f 100644 --- a/contributors.yml +++ b/contributors.yml @@ -469,3 +469,4 @@ - zeromask1337 - zheng-chuang - zxTomw +- AnandShiva diff --git a/packages/react-router/__tests__/dom/ssr/components-test.tsx b/packages/react-router/__tests__/dom/ssr/components-test.tsx index 194f159762..f13a6b561a 100644 --- a/packages/react-router/__tests__/dom/ssr/components-test.tsx +++ b/packages/react-router/__tests__/dom/ssr/components-test.tsx @@ -5,12 +5,21 @@ import * as React from "react"; import { createMemoryRouter, Link, + Links, NavLink, Outlet, RouterProvider, + Scripts, } from "../../../index"; import { HydratedRouter } from "../../../lib/dom-export/hydrated-router"; -import { FrameworkContext } from "../../../lib/dom/ssr/components"; +import { + FrameworkContext, + usePrefetchBehavior, +} from "../../../lib/dom/ssr/components"; +import { + DataRouterContext, + DataRouterStateContext, +} from "../../../lib/context"; import invariant from "../../../lib/dom/ssr/invariant"; import { ServerRouter } from "../../../lib/dom/ssr/server"; import "@testing-library/jest-dom"; @@ -283,3 +292,168 @@ describe("", () => { expect(container.innerHTML).toMatch("

Root

"); }); }); + +describe("", () => { + it("renders critical css with nonce", () => { + let context = mockFrameworkContext({ + criticalCss: ".critical { color: red; }", + }); + + let { container } = render( + + + + + , + ); + + let style = container.querySelector("style"); + expect(style).toHaveAttribute("data-react-router-critical-css"); + expect(style).toHaveAttribute("nonce", "test-nonce"); + expect(style).toHaveTextContent(".critical { color: red; }"); + }); + + it("renders critical css object with nonce", () => { + let context = mockFrameworkContext({ + criticalCss: { rel: "stylesheet", href: "/critical.css" }, + }); + + let { container } = render( + + + + + , + ); + + let link = container.querySelector("link[rel='stylesheet']"); + expect(link).toHaveAttribute("data-react-router-critical-css"); + expect(link).toHaveAttribute("href", "/critical.css"); + expect(link).toHaveAttribute("nonce", "test-nonce"); + }); + + it("propagates nonce to route links", () => { + let context = mockFrameworkContext({ + routeModules: { + root: { + default: () => null, + links: () => [{ rel: "stylesheet", href: "/style.css" }], + }, + }, + manifest: { + routes: { + root: { + id: "root", + module: "root.js", + hasLoader: false, + hasAction: false, + hasErrorBoundary: false, + hasClientAction: false, + hasClientLoader: false, + hasClientMiddleware: false, + clientActionModule: undefined, + clientLoaderModule: undefined, + clientMiddlewareModule: undefined, + hydrateFallbackModule: undefined, + }, + }, + entry: { imports: [], module: "" }, + url: "", + version: "", + }, + }); + + let { container } = render( + + + + + , + ); + + let link = container.querySelector("link[href='/style.css']"); + expect(link).toHaveAttribute("nonce", "test-nonce"); + }); +}); + +describe("usePrefetchBehavior", () => { + function TestComponent({ + prefetch, + }: { + prefetch: "intent" | "render" | "none" | "viewport"; + }) { + let [shouldPrefetch, ref] = usePrefetchBehavior(prefetch, {}); + return ( + + Link + + ); + } + + it("handles prefetch='render'", () => { + let context = mockFrameworkContext({}); + + // Wrap in FrameworkContext because usePrefetchBehavior checks for it + let { container } = render( + + + , + ); + + expect(container.firstChild).toHaveAttribute("data-prefetch", "true"); + }); + + it("handles prefetch='viewport'", () => { + let context = mockFrameworkContext({}); + let observeCallback: IntersectionObserverCallback; + let observeMock = jest.fn(); + let disconnectMock = jest.fn(); + + window.IntersectionObserver = class { + constructor(cb: IntersectionObserverCallback) { + observeCallback = cb; + } + observe = observeMock; + unobserve = jest.fn(); + disconnect = disconnectMock; + takeRecords = () => []; + root = null; + rootMargin = ""; + thresholds = []; + }; + + let { container } = render( + + + , + ); + + // Initial state + expect(container.firstChild).toHaveAttribute("data-prefetch", "false"); + expect(observeMock).toHaveBeenCalled(); + + // Trigger intersection + act(() => { + observeCallback( + [{ isIntersecting: true } as IntersectionObserverEntry], + new IntersectionObserver(() => {}), + ); + }); + + expect(container.firstChild).toHaveAttribute("data-prefetch", "true"); + }); +}); diff --git a/packages/react-router/lib/dom/ssr/components.tsx b/packages/react-router/lib/dom/ssr/components.tsx index c29f73c73d..2e0f101511 100644 --- a/packages/react-router/lib/dom/ssr/components.tsx +++ b/packages/react-router/lib/dom/ssr/components.tsx @@ -281,6 +281,7 @@ export function Links({ nonce, crossOrigin }: LinksProps): React.JSX.Element { {typeof criticalCss === "string" ? (