From 1de9b0a2b4d7c2cd00dd5c53c1b4ebf8f25666cc Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 19 Feb 2026 10:42:50 -0500 Subject: [PATCH 01/33] Remove internal "agnostic" types (#14765) --- .changeset/remove-agnostic-types.md | 5 + decisions/0016-plan-remove-agnostic-types.md | 368 ++++++++++++++++++ packages/react-router/index.ts | 22 +- packages/react-router/lib/components.tsx | 20 +- packages/react-router/lib/context.ts | 74 +--- packages/react-router/lib/dom/lib.tsx | 8 +- packages/react-router/lib/dom/server.tsx | 9 +- .../react-router/lib/dom/ssr/components.tsx | 6 +- .../react-router/lib/dom/ssr/fog-of-war.ts | 6 +- .../react-router/lib/dom/ssr/hydration.tsx | 2 +- packages/react-router/lib/dom/ssr/links.ts | 18 +- .../react-router/lib/dom/ssr/routeModules.ts | 2 +- .../lib/dom/ssr/routes-test-stub.tsx | 11 +- packages/react-router/lib/dom/ssr/routes.tsx | 2 +- .../react-router/lib/dom/ssr/single-fetch.tsx | 2 +- packages/react-router/lib/hooks.tsx | 11 +- .../lib/router/instrumentation.ts | 16 +- packages/react-router/lib/router/router.ts | 191 +++++---- packages/react-router/lib/router/utils.ts | 213 ++++++---- packages/react-router/lib/rsc/browser.tsx | 18 +- packages/react-router/lib/rsc/server.rsc.ts | 7 +- packages/react-router/lib/rsc/server.ssr.tsx | 4 +- .../lib/server-runtime/headers.ts | 2 +- .../lib/server-runtime/routeMatching.ts | 4 +- .../react-router/lib/server-runtime/routes.ts | 4 +- 25 files changed, 678 insertions(+), 347 deletions(-) create mode 100644 .changeset/remove-agnostic-types.md create mode 100644 decisions/0016-plan-remove-agnostic-types.md diff --git a/.changeset/remove-agnostic-types.md b/.changeset/remove-agnostic-types.md new file mode 100644 index 0000000000..268c8f3fb5 --- /dev/null +++ b/.changeset/remove-agnostic-types.md @@ -0,0 +1,5 @@ +--- +"react-router": patch +--- + +Internal refactor to consolidate framework-agnostic/React-specific route type layers - no public API changes diff --git a/decisions/0016-plan-remove-agnostic-types.md b/decisions/0016-plan-remove-agnostic-types.md new file mode 100644 index 0000000000..9640c42c86 --- /dev/null +++ b/decisions/0016-plan-remove-agnostic-types.md @@ -0,0 +1,368 @@ +# Plan: Remove "Agnostic" Layer from RouteObject/RouteMatch Types + +**Goal**: Consolidate the framework-agnostic and React-specific type layers into a single React-aware layer without breaking the public API. + +## Background + +React Router historically maintained a framework-agnostic layer to support multiple UI frameworks. Since we no longer support other frameworks, we can simplify our type hierarchy by removing this abstraction. + +### Current Structure + +**Agnostic Layer** (`lib/router/utils.ts`): + +- `AgnosticBaseRouteObject` - Base route properties (loader, action, etc.) +- `AgnosticIndexRouteObject` - Index route without children +- `AgnosticNonIndexRouteObject` - Route with optional children +- `AgnosticRouteObject` - Union of index/non-index routes +- `AgnosticDataIndexRouteObject` - Index route with required `id` +- `AgnosticDataNonIndexRouteObject` - Non-index route with required `id` +- `AgnosticDataRouteObject` - Data route with required `id` +- `AgnosticRouteMatch` - Match result +- `AgnosticDataRouteMatch` - Data route match result + +**React Layer** (`lib/context.ts`): + +- `IndexRouteObject` - Extends agnostic type + React fields (`element`, `Component`, `errorElement`, `ErrorBoundary`, `hydrateFallbackElement`, `HydrateFallback`) +- `NonIndexRouteObject` - Extends agnostic type + React fields +- `RouteObject` - Union of index/non-index +- `DataRouteObject` - Route with required `id` +- `RouteMatch` - Extends `AgnosticRouteMatch` +- `DataRouteMatch` - Extends `AgnosticRouteMatch` + +### Problem + +The two-layer structure adds complexity: + +1. Duplicate type definitions with subtle differences +2. Requires understanding inheritance hierarchy +3. The "agnostic" layer no longer serves a purpose +4. React types extend agnostic types just to add React-specific fields + +## Proposed Solution + +**Strategy**: Consolidate types by renaming the "agnostic" types to be the primary React-aware types, eliminating the prefix entirely. + +### Approach: Rename and Merge (Alternative 1) + +Since `Agnostic*` types are not exported from the public API, we can safely rename them to become the primary `RouteObject`, `RouteMatch`, etc. types. The React layer in `context.ts` will re-export these types, maintaining the public API contract. + +#### Step 1: Update `lib/router/utils.ts` + +Rename existing `Agnostic*` types and add React-specific fields: + +```typescript +/** + * Base RouteObject with common props shared by all types of routes + */ +type BaseRouteObject = { + caseSensitive?: boolean; + path?: string; + id?: string; + middleware?: MiddlewareFunction[]; + loader?: LoaderFunction | boolean; + action?: ActionFunction | boolean; + hasErrorBoundary?: boolean; + shouldRevalidate?: ShouldRevalidateFunction; + handle?: any; + lazy?: LazyRouteDefinition; + // React-specific fields (merged from context.ts) + element?: React.ReactNode | null; + hydrateFallbackElement?: React.ReactNode | null; + errorElement?: React.ReactNode | null; + Component?: React.ComponentType | null; + HydrateFallback?: React.ComponentType | null; + ErrorBoundary?: React.ComponentType | null; +}; + +/** + * Index routes must not have children + */ +export type IndexRouteObject = BaseRouteObject & { + children?: undefined; + index: true; +}; + +/** + * Non-index routes may have children, but cannot have index + */ +export type NonIndexRouteObject = BaseRouteObject & { + children?: RouteObject[]; + index?: false; +}; + +/** + * A route object represents a logical route, with (optionally) its child + * routes organized in a tree-like structure. + */ +export type RouteObject = IndexRouteObject | NonIndexRouteObject; + +export type DataIndexRouteObject = IndexRouteObject & { + id: string; +}; + +export type DataNonIndexRouteObject = NonIndexRouteObject & { + children?: DataRouteObject[]; + id: string; +}; + +/** + * A data route object, which is just a RouteObject with a required unique ID + */ +export type DataRouteObject = DataIndexRouteObject | DataNonIndexRouteObject; + +/** + * A RouteMatch contains info about how a route matched a URL. + */ +export interface RouteMatch< + ParamKey extends string = string, + RouteObjectType extends RouteObject = RouteObject, +> { + /** + * The names and values of dynamic parameters in the URL. + */ + params: Params; + /** + * The portion of the URL pathname that was matched. + */ + pathname: string; + /** + * The portion of the URL pathname that was matched before child routes. + */ + pathnameBase: string; + /** + * The route object that was used to match. + */ + route: RouteObjectType; +} + +export interface DataRouteMatch extends RouteMatch {} +``` + +Key changes: + +- `AgnosticBaseRouteObject` → `BaseRouteObject` (add React fields) +- `AgnosticIndexRouteObject` → `IndexRouteObject` +- `AgnosticNonIndexRouteObject` → `NonIndexRouteObject` +- `AgnosticRouteObject` → `RouteObject` +- `AgnosticDataIndexRouteObject` → `DataIndexRouteObject` +- `AgnosticDataNonIndexRouteObject` → `DataNonIndexRouteObject` +- `AgnosticDataRouteObject` → `DataRouteObject` +- `AgnosticRouteMatch` → `RouteMatch` +- `AgnosticDataRouteMatch` → `DataRouteMatch` + +#### Step 2: Update `lib/context.ts` + +Convert to simple re-exports since the types now exist in `utils.ts`: + +```typescript +// Re-export route types from utils (they're now React-aware) +export type { + IndexRouteObject, + NonIndexRouteObject, + RouteObject, + DataRouteObject, + RouteMatch, + DataRouteMatch, +} from "./router/utils"; + +// PatchRoutesOnNavigationFunction types can now be simplified +export type PatchRoutesOnNavigationFunctionArgs = + AgnosticPatchRoutesOnNavigationFunctionArgs; + +export type PatchRoutesOnNavigationFunction = + AgnosticPatchRoutesOnNavigationFunction; +``` + +Remove duplicate type definitions and re-export from `utils.ts`: + +**Before:** +```typescriptimport or reference `Agnostic\*` types: + +**Files to update:** + +1. `lib/router/router.ts` - Update all `AgnosticDataRouteObject` → `DataRouteObject`, `AgnosticDataRouteMatch` → `DataRouteMatch`, `AgnosticRouteObject` → `RouteObject` +2. `lib/router/instrumentation.ts` - Update route type references +3. `lib/router/utils.ts` - Update internal function signatures, type parameters, and helper functions +4. `lib/dom/ssr/links.ts` - Update `AgnosticDataRouteMatch` → `DataRouteMatch` +5. `lib/rsc/server.rsc.ts` - Update `AgnosticDataRouteMatch` → `DataRouteMatch` +6. `lib/context.ts` - Update `AgnosticPatchRoutesOnNavigationFunctionArgs` references (or rename those too) +7. `__tests__/**/*.ts` - Update test type references (~40 files) + +**Note**: Some files may still reference `Agnostic*` types that are part of function/generic names (like `AgnosticPatchRoutesOnNavigationFunction`). These can be renamed in a follow-up or kept as-is if the name makes sense in context. + +```` + +**After:** +```typescript +// Re-export route types from utils (they're now React-aware) +export type { + IndexRouteObject, + NonIndexRouteObject, + RouteObject, + DataRouteObject, + RouteMatch, + DataRouteMatch, +} from "./router/utils"; + +// PatchRoutesOnNavigationFunction types remain the same +export type PatchRoutesOnNavigationFunctionArgs = + AgnosticPatchRoutesOnNavigationFunctionArgs; + +export type PatchRoutesOnNavigationFunction = + AgnosticPatchRoutesOnNavigationFunction; +```` + +This eliminates ~70 lines of duplicate type definitions.ction matchRoutes< +RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject + +> (...) + +// After +function matchRoutes< +RouteObjectType extends RouteObject = RouteObject + +> (...) + +```` + +#### Step 5: Update Helper Functions + +Update type guards and helper functions: + +```typescript +// Before +function isIndexRoute( + route: AgnosticRouteObject, +): route is AgnosticIndexRouteObject + +// After +function isIndexRoute( + route: RouteObject, +): route is IndexRouteObject +```` + +**No migration needed!** + +Since `Agnostic*` types are not part of the public API (not exported from `index.ts`), this is purely an internal refactoring. External consumers only use `RouteObject`, `DataRouteObject`, `RouteMatch`, and `DataRouteMatch` which remain unchanged in the public API.`RouteMatch`, `DataRouteMatch`) still exported + +**Internal API**: ⚠️ Requires updates + +- Functions using `Agnostic*` types need updating +- Tests using `Agnostic*` types need updating +- Backward compatibility aliases prevent hard breaks + +**Type Safety**: ✅ Maintained + +- All types remain strongly typed +- React-specific fields properly typed throughout +- No loss of type information + +### Migration Path for Consumers + +For any external code using the `Agnostic*` types (unlikely since they're not exported): + +1. **Short term**: Backward compatibility aliases work transparently +2. **Medium term**: Types marked `@internal` and `@deprecated` +3. **Long term**: Remove aliases in next major version + +### Testing Strategy + +1. **Type checks**: Ensure `pnpm typecheck` passes +2. **Unit tests**: Ensure `pnpm test` passes +3. **Integration tests**: Ensure `pnpm test:integration --project chromium` passes +4. **Build**: Ensure `pnpm build` succeeds +5. **Docs generation**: Ensure `pnpm docs` works correctly + +### Implementation Order + +### Implementation Order + +1. ✅ Create this plan document +2. ✅ Update `lib/router/utils.ts` - Rename `Agnostic*` types to remove prefix, add React fields to base types +3. ✅ Update `lib/context.ts` - Remove duplicate definitions, convert to re-exports +4. ✅ Update `lib/router/router.ts` - Replace all `Agnostic*` type references +5. ✅ Update `lib/router/instrumentation.ts` - Replace `Agnostic*` type references +6. ✅ Update `lib/dom/ssr/links.ts` - Replace `AgnosticDataRouteMatch` references +7. ✅ Update `lib/rsc/server.rsc.ts` - Replace `AgnosticDataRouteMatch` references +8. ✅ Update test files - Replace `Agnostic*` type references (~40 files) - No test files needed updating +9. ✅ Run full test suite (`pnpm typecheck && pnpm test && pnpm test:integration --project chromium`) - All passing +10. ✅ Update JSDoc comments if any reference "agnostic" or "framework-agnostic" +11. ✅ Create changeset + +### Risks & Mitigations + +Accidentally changing public API behavior + +- **Mitigation**: Public exports in `index.ts` remain unchanged, only importing from same location (`./lib/context`) + +**Risk**: Type inference changes in complex generic scenarios + +- **Mitigation**: All type fields remain identical, just location changes. Run full typecheck suite. + +**Risk**: Complex merge conflicts if done across multiple PRs + +- **Mitigation**: Complete in single atomic PR + +**Risk**: Missing some `Agnostic*` type references in large codebase + +- **Mitigation**: TypeScript compiler will catch all references; can also use global find/replace to identify all usages first +- **Mitigation**: Thorough testing at each step, update tests incrementally + +## Alternative Approaches Considered + +### Alternative 1: Rename to Remove "Agnostic" Prefix + +Move all types f2: In-Place Consolidation with Aliases + +Keep types in their current files but merge the layers by making the "agnostic" types React-aware and converting the React layer to aliases: + +- `AgnosticBaseRouteObject` stays in `utils.ts` but gains React fields +- React types in `context.ts` become aliases: `export type RouteObject = AgnosticRouteObject` +- Add `@deprecated` backward compatibility aliases + +**Pros**: Gradual migration path with deprecated aliases +**Cons**: Keeps confusing "Agnostic" naming, unnecessary since types aren't exported + +### Alternative 3but make React types standalone (not extending): + +- Both define all fields independently +- No inheritance relationship + +**Pros**: Minimal code changes +**Cons**: Doesn't reduce duplication, doesn't achieve goal + +### Alternative 3: Gradual Deprecation Path + +4: Gradual Deprecation Path + +Add new types, deprecate old ones, remove later: + +- Create `RouteObjectV2` with merged types +- Deprecate `AgnosticRouteObject` and React `RouteObject` +- Remove in next major + +**Pros**: Maximum safety, clear migration path +**Cons**: Temporary duplication, longer timeline, unnecessary complexity + +## Decision + +**Selected Approach**: Rename and Merge (Alternative 1, now main proposal) + +**Rationale**: + +1. ✅ Achieves goal of removing agnostic layer completely +2. ✅ **Non-breaking** - `Agnostic*` types are not in public API +3. ✅ Cleaner - removes confusing "Agnostic" prefix entirely +4. ✅ Simpler - no deprecated aliases needed since types aren't exported +5. ✅ Single atomic change is easier to review and test +6. ✅ Types live in one logical place (`utils.ts`) +7. ✅ Reduces ~70 lines of duplicate type definitions in `context.ts` + +## Follow-up Work + +After this refactoring: + +1. Consider if `RouteManifest` generic parameter is still needed +2. Evaluate if other "agnostic" patterns exist elsewhere +3. Update internal documentation about type architecture +4. Consider removing `@internal` aliases in v8 diff --git a/packages/react-router/index.ts b/packages/react-router/index.ts index 696362f091..a398f1d0ed 100644 --- a/packages/react-router/index.ts +++ b/packages/react-router/index.ts @@ -30,6 +30,9 @@ export type { export type { ActionFunction, ActionFunctionArgs, + BaseRouteObject, + DataRouteMatch, + DataRouteObject, DataStrategyFunction, DataStrategyFunctionArgs, DataStrategyMatch, @@ -39,16 +42,22 @@ export type { FormEncType, FormMethod, HTMLFormMethod, + IndexRouteObject, LazyRouteFunction, LoaderFunction, LoaderFunctionArgs, MiddlewareFunction, + NonIndexRouteObject, ParamParseKey, Params, + PatchRoutesOnNavigationFunction, + PatchRoutesOnNavigationFunctionArgs, PathMatch, PathParam, PathPattern, RedirectFunction, + RouteMatch, + RouteObject, RouterContext, ShouldRevalidateFunction, ShouldRevalidateFunctionArgs, @@ -87,18 +96,7 @@ export { } from "./lib/router/utils"; // Expose react-router public API -export type { - DataRouteMatch, - DataRouteObject, - IndexRouteObject, - NavigateOptions, - Navigator, - NonIndexRouteObject, - PatchRoutesOnNavigationFunction, - PatchRoutesOnNavigationFunctionArgs, - RouteMatch, - RouteObject, -} from "./lib/context"; +export type { NavigateOptions, Navigator } from "./lib/context"; export { AwaitContextProvider as UNSAFE_AwaitContextProvider } from "./lib/context"; export type { AwaitProps, diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx index e6a7a3d7af..78252451be 100644 --- a/packages/react-router/lib/components.tsx +++ b/packages/react-router/lib/components.tsx @@ -24,9 +24,15 @@ import type { } from "./router/router"; import { createRouter } from "./router/router"; import type { + DataRouteObject, DataStrategyFunction, + IndexRouteObject, LazyRouteFunction, + NonIndexRouteObject, Params, + PatchRoutesOnNavigationFunction, + RouteMatch, + RouteObject, TrackedPromise, } from "./router/utils"; import { @@ -36,16 +42,7 @@ import { stripBasename, } from "./router/utils"; -import type { - DataRouteObject, - IndexRouteObject, - Navigator, - NonIndexRouteObject, - PatchRoutesOnNavigationFunction, - RouteMatch, - RouteObject, - ViewTransitionContextObject, -} from "./context"; +import type { Navigator, ViewTransitionContextObject } from "./context"; import { AwaitContext, DataRouterContext, @@ -1215,6 +1212,9 @@ export interface IndexRouteProps { ErrorBoundary?: React.ComponentType | null; } +/** + * @category Types + */ export type RouteProps = PathRouteProps | LayoutRouteProps | IndexRouteProps; /** diff --git a/packages/react-router/lib/context.ts b/packages/react-router/lib/context.ts index 91757b6722..07e8ec6aca 100644 --- a/packages/react-router/lib/context.ts +++ b/packages/react-router/lib/context.ts @@ -11,79 +11,7 @@ import type { Router, StaticHandlerContext, } from "./router/router"; -import type { - AgnosticIndexRouteObject, - AgnosticNonIndexRouteObject, - AgnosticPatchRoutesOnNavigationFunction, - AgnosticPatchRoutesOnNavigationFunctionArgs, - AgnosticRouteMatch, - LazyRouteDefinition, - TrackedPromise, -} from "./router/utils"; - -// Create react-specific types from the agnostic types in @remix-run/router to -// export from react-router -export interface IndexRouteObject { - caseSensitive?: AgnosticIndexRouteObject["caseSensitive"]; - path?: AgnosticIndexRouteObject["path"]; - id?: AgnosticIndexRouteObject["id"]; - middleware?: AgnosticIndexRouteObject["middleware"]; - loader?: AgnosticIndexRouteObject["loader"]; - action?: AgnosticIndexRouteObject["action"]; - hasErrorBoundary?: AgnosticIndexRouteObject["hasErrorBoundary"]; - shouldRevalidate?: AgnosticIndexRouteObject["shouldRevalidate"]; - handle?: AgnosticIndexRouteObject["handle"]; - index: true; - children?: undefined; - element?: React.ReactNode | null; - hydrateFallbackElement?: React.ReactNode | null; - errorElement?: React.ReactNode | null; - Component?: React.ComponentType | null; - HydrateFallback?: React.ComponentType | null; - ErrorBoundary?: React.ComponentType | null; - lazy?: LazyRouteDefinition; -} - -export interface NonIndexRouteObject { - caseSensitive?: AgnosticNonIndexRouteObject["caseSensitive"]; - path?: AgnosticNonIndexRouteObject["path"]; - id?: AgnosticNonIndexRouteObject["id"]; - middleware?: AgnosticNonIndexRouteObject["middleware"]; - loader?: AgnosticNonIndexRouteObject["loader"]; - action?: AgnosticNonIndexRouteObject["action"]; - hasErrorBoundary?: AgnosticNonIndexRouteObject["hasErrorBoundary"]; - shouldRevalidate?: AgnosticNonIndexRouteObject["shouldRevalidate"]; - handle?: AgnosticNonIndexRouteObject["handle"]; - index?: false; - children?: RouteObject[]; - element?: React.ReactNode | null; - hydrateFallbackElement?: React.ReactNode | null; - errorElement?: React.ReactNode | null; - Component?: React.ComponentType | null; - HydrateFallback?: React.ComponentType | null; - ErrorBoundary?: React.ComponentType | null; - lazy?: LazyRouteDefinition; -} - -export type RouteObject = IndexRouteObject | NonIndexRouteObject; - -export type DataRouteObject = RouteObject & { - children?: DataRouteObject[]; - id: string; -}; - -export interface RouteMatch< - ParamKey extends string = string, - RouteObjectType extends RouteObject = RouteObject, -> extends AgnosticRouteMatch {} - -export interface DataRouteMatch extends RouteMatch {} - -export type PatchRoutesOnNavigationFunctionArgs = - AgnosticPatchRoutesOnNavigationFunctionArgs; - -export type PatchRoutesOnNavigationFunction = - AgnosticPatchRoutesOnNavigationFunction; +import type { TrackedPromise, RouteMatch } from "./router/utils"; export interface DataRouterContextObject // Omit `future` since those can be pulled from the `router` diff --git a/packages/react-router/lib/dom/lib.tsx b/packages/react-router/lib/dom/lib.tsx index d3924ca980..74dd0a01e6 100644 --- a/packages/react-router/lib/dom/lib.tsx +++ b/packages/react-router/lib/dom/lib.tsx @@ -30,6 +30,8 @@ import type { DataStrategyFunction, FormEncType, HTMLFormMethod, + PatchRoutesOnNavigationFunction, + RouteObject, UIMatch, } from "../router/utils"; import { @@ -73,11 +75,7 @@ import { mapRouteProperties, hydrationRouteProperties, } from "../components"; -import type { - RouteObject, - NavigateOptions, - PatchRoutesOnNavigationFunction, -} from "../context"; +import type { NavigateOptions } from "../context"; import { DataRouterContext, DataRouterStateContext, diff --git a/packages/react-router/lib/dom/server.tsx b/packages/react-router/lib/dom/server.tsx index ca9c5a3e8d..41dd35c02d 100644 --- a/packages/react-router/lib/dom/server.tsx +++ b/packages/react-router/lib/dom/server.tsx @@ -20,13 +20,12 @@ import { IDLE_NAVIGATION, createStaticHandler as routerCreateStaticHandler, } from "../router/router"; -import type { RouteManifest } from "../router/utils"; +import type { RouteManifest, RouteObject } from "../router/utils"; import { convertRoutesToDataRoutes, isRouteErrorResponse, } from "../router/utils"; import { DataRoutes, Router, mapRouteProperties } from "../components"; -import type { RouteObject } from "../context"; import { DataRouterContext, DataRouterStateContext, @@ -390,9 +389,9 @@ export function createStaticRouter( manifest, ); - // Because our context matches may be from a framework-agnostic set of - // routes passed to createStaticHandler(), we update them here with our - // newly created/enhanced data routes + // Because our context matches may be from a set of routes passed to + // createStaticHandler(), we update them here with our newly created/enhanced + // data routes let matches = context.matches.map((match) => { let route = manifest[match.route.id] || match.route; return { diff --git a/packages/react-router/lib/dom/ssr/components.tsx b/packages/react-router/lib/dom/ssr/components.tsx index 2e0f101511..19e6844751 100644 --- a/packages/react-router/lib/dom/ssr/components.tsx +++ b/packages/react-router/lib/dom/ssr/components.tsx @@ -6,7 +6,7 @@ import type { import * as React from "react"; import type { RouterState } from "../../router/router"; -import type { AgnosticDataRouteMatch } from "../../router/utils"; +import type { DataRouteMatch } from "../../router/utils"; import { matchRoutes } from "../../router/utils"; import type { FrameworkContextObject } from "./entry"; @@ -356,7 +356,7 @@ export function PrefetchPageLinks({ page, ...linkProps }: PageLinkDescriptor) { return ; } -function useKeyedPrefetchLinks(matches: AgnosticDataRouteMatch[]) { +function useKeyedPrefetchLinks(matches: DataRouteMatch[]) { let { manifest, routeModules } = useFrameworkContext(); let [keyedPrefetchLinks, setKeyedPrefetchLinks] = React.useState< @@ -387,7 +387,7 @@ function PrefetchPageLinksImpl({ matches: nextMatches, ...linkProps }: PageLinkDescriptor & { - matches: AgnosticDataRouteMatch[]; + matches: DataRouteMatch[]; }) { let location = useLocation(); let { future, manifest, routeModules } = useFrameworkContext(); diff --git a/packages/react-router/lib/dom/ssr/fog-of-war.ts b/packages/react-router/lib/dom/ssr/fog-of-war.ts index a9aaa8e861..72c24c7f8f 100644 --- a/packages/react-router/lib/dom/ssr/fog-of-war.ts +++ b/packages/react-router/lib/dom/ssr/fog-of-war.ts @@ -1,7 +1,9 @@ import * as React from "react"; -import type { PatchRoutesOnNavigationFunction } from "../../context"; import type { Router as DataRouter } from "../../router/router"; -import type { RouteManifest } from "../../router/utils"; +import type { + PatchRoutesOnNavigationFunction, + RouteManifest, +} from "../../router/utils"; import { matchRoutes } from "../../router/utils"; import type { AssetsManifest } from "./entry"; import type { RouteModules } from "./routeModules"; diff --git a/packages/react-router/lib/dom/ssr/hydration.tsx b/packages/react-router/lib/dom/ssr/hydration.tsx index 4185633bc7..bbcc904221 100644 --- a/packages/react-router/lib/dom/ssr/hydration.tsx +++ b/packages/react-router/lib/dom/ssr/hydration.tsx @@ -1,6 +1,6 @@ -import type { DataRouteObject } from "../../context"; import type { Path } from "../../router/history"; import type { Router as DataRouter, HydrationState } from "../../router/router"; +import type { DataRouteObject } from "../../router/utils"; import { matchRoutes } from "../../router/utils"; import type { ClientLoaderFunction } from "./routeModules"; import { shouldHydrateRouteLoader } from "./routes"; diff --git a/packages/react-router/lib/dom/ssr/links.ts b/packages/react-router/lib/dom/ssr/links.ts index 55f6dbc87b..ec5fc8dee2 100644 --- a/packages/react-router/lib/dom/ssr/links.ts +++ b/packages/react-router/lib/dom/ssr/links.ts @@ -1,5 +1,5 @@ import type { Location } from "../../router/history"; -import type { AgnosticDataRouteMatch } from "../../router/utils"; +import type { DataRouteMatch } from "../../router/utils"; import type { AssetsManifest } from "./entry"; import type { RouteModules, RouteModule } from "./routeModules"; @@ -16,7 +16,7 @@ import type { * loaded already. */ export function getKeyedLinksForMatches( - matches: AgnosticDataRouteMatch[], + matches: DataRouteMatch[], routeModules: RouteModules, manifest: AssetsManifest, ): KeyedLinkDescriptor[] { @@ -147,7 +147,7 @@ function isHtmlLinkDescriptor(object: any): object is HtmlLinkDescriptor { export type KeyedHtmlLinkDescriptor = { key: string; link: HtmlLinkDescriptor }; export async function getKeyedPrefetchLinks( - matches: AgnosticDataRouteMatch[], + matches: DataRouteMatch[], manifest: AssetsManifest, routeModules: RouteModules, ): Promise { @@ -178,18 +178,18 @@ export async function getKeyedPrefetchLinks( // This is ridiculously identical to transition.ts `filterMatchesToLoad` export function getNewMatchesForLinks( page: string, - nextMatches: AgnosticDataRouteMatch[], - currentMatches: AgnosticDataRouteMatch[], + nextMatches: DataRouteMatch[], + currentMatches: DataRouteMatch[], manifest: AssetsManifest, location: Location, mode: "data" | "assets", -): AgnosticDataRouteMatch[] { - let isNew = (match: AgnosticDataRouteMatch, index: number) => { +): DataRouteMatch[] { + let isNew = (match: DataRouteMatch, index: number) => { if (!currentMatches[index]) return true; return match.route.id !== currentMatches[index].route.id; }; - let matchPathChanged = (match: AgnosticDataRouteMatch, index: number) => { + let matchPathChanged = (match: DataRouteMatch, index: number) => { return ( // param change, /users/123 -> /users/456 currentMatches[index].pathname !== match.pathname || @@ -244,7 +244,7 @@ export function getNewMatchesForLinks( } export function getModuleLinkHrefs( - matches: AgnosticDataRouteMatch[], + matches: DataRouteMatch[], manifest: AssetsManifest, { includeHydrateFallback }: { includeHydrateFallback?: boolean } = {}, ): string[] { diff --git a/packages/react-router/lib/dom/ssr/routeModules.ts b/packages/react-router/lib/dom/ssr/routeModules.ts index e8926d46b4..43a79a8ece 100644 --- a/packages/react-router/lib/dom/ssr/routeModules.ts +++ b/packages/react-router/lib/dom/ssr/routeModules.ts @@ -8,11 +8,11 @@ import type { MiddlewareFunction, Params, ShouldRevalidateFunction, + DataRouteMatch, DataStrategyResult, } from "../../router/utils"; import type { EntryRoute } from "./routes"; -import type { DataRouteMatch } from "../../context"; import type { LinkDescriptor } from "../../router/links"; import type { SerializeFrom } from "../../types/route-data"; diff --git a/packages/react-router/lib/dom/ssr/routes-test-stub.tsx b/packages/react-router/lib/dom/ssr/routes-test-stub.tsx index b9038287cc..fd85d84ed8 100644 --- a/packages/react-router/lib/dom/ssr/routes-test-stub.tsx +++ b/packages/react-router/lib/dom/ssr/routes-test-stub.tsx @@ -2,15 +2,13 @@ import * as React from "react"; import type { ActionFunction, ActionFunctionArgs, + DataRouteObject, + IndexRouteObject, LoaderFunction, LoaderFunctionArgs, MiddlewareFunction, -} from "../../router/utils"; -import type { - DataRouteObject, - IndexRouteObject, NonIndexRouteObject, -} from "../../context"; +} from "../../router/utils"; import type { LinksFunction, MetaFunction, RouteModules } from "./routeModules"; import type { InitialEntry } from "../../router/history"; import type { HydrationState } from "../../router/router"; @@ -18,7 +16,6 @@ import { convertRoutesToDataRoutes, RouterContextProvider, } from "../../router/utils"; -import type { MiddlewareEnabled } from "../../types/future"; import type { AppLoadContext } from "../../server-runtime/data"; import type { AssetsManifest, @@ -154,7 +151,7 @@ export function createRoutesStub( // the manifest and routeModules during the walk let patched = processRoutes( // @ts-expect-error `StubRouteObject` is stricter about `loader`/`action` - // types compared to `AgnosticRouteObject` + // types compared to `RouteObject` convertRoutesToDataRoutes(routes, (r) => r), _context !== undefined ? _context diff --git a/packages/react-router/lib/dom/ssr/routes.tsx b/packages/react-router/lib/dom/ssr/routes.tsx index 0c1682c250..1965bce137 100644 --- a/packages/react-router/lib/dom/ssr/routes.tsx +++ b/packages/react-router/lib/dom/ssr/routes.tsx @@ -3,6 +3,7 @@ import * as React from "react"; import type { HydrationState } from "../../router/router"; import type { ActionFunctionArgs, + DataRouteObject, LoaderFunctionArgs, RouteManifest, ShouldRevalidateFunction, @@ -21,7 +22,6 @@ import { RemixRootDefaultErrorBoundary } from "./errorBoundaries"; import { RemixRootDefaultHydrateFallback } from "./fallback"; import invariant from "./invariant"; import { useRouteError } from "../../hooks"; -import type { DataRouteObject } from "../../context"; export interface Route { index?: boolean; diff --git a/packages/react-router/lib/dom/ssr/single-fetch.tsx b/packages/react-router/lib/dom/ssr/single-fetch.tsx index fdf78ff273..b952903e0c 100644 --- a/packages/react-router/lib/dom/ssr/single-fetch.tsx +++ b/packages/react-router/lib/dom/ssr/single-fetch.tsx @@ -4,6 +4,7 @@ import { decode } from "../../../vendor/turbo-stream-v2/turbo-stream"; import type { Router as DataRouter } from "../../router/router"; import { isDataWithResponseInit, isResponse } from "../../router/router"; import type { + DataRouteMatch, DataStrategyFunction, DataStrategyFunctionArgs, DataStrategyResult, @@ -20,7 +21,6 @@ import type { AssetsManifest, EntryContext } from "./entry"; import { escapeHtml } from "./markup"; import invariant from "./invariant"; import type { RouteModules } from "./routeModules"; -import type { DataRouteMatch } from "../../context"; export const SingleFetchRedirectSymbol = Symbol("SingleFetchRedirect"); diff --git a/packages/react-router/lib/hooks.tsx b/packages/react-router/lib/hooks.tsx index 29887101ef..597d1417f0 100644 --- a/packages/react-router/lib/hooks.tsx +++ b/packages/react-router/lib/hooks.tsx @@ -1,11 +1,5 @@ import * as React from "react"; -import type { - DataRouteMatch, - NavigateOptions, - RouteContextObject, - RouteMatch, - RouteObject, -} from "./context"; +import type { NavigateOptions, RouteContextObject } from "./context"; import { AwaitContext, DataRouterContext, @@ -34,10 +28,13 @@ import type { } from "./router/router"; import { IDLE_BLOCKER } from "./router/router"; import type { + DataRouteMatch, ParamParseKey, Params, PathMatch, PathPattern, + RouteMatch, + RouteObject, UIMatch, } from "./router/utils"; import { diff --git a/packages/react-router/lib/router/instrumentation.ts b/packages/react-router/lib/router/instrumentation.ts index 4ba8a16063..43fef3185e 100644 --- a/packages/react-router/lib/router/instrumentation.ts +++ b/packages/react-router/lib/router/instrumentation.ts @@ -5,7 +5,7 @@ import { createPath, invariant } from "./history"; import type { Router } from "./router"; import type { ActionFunctionArgs, - AgnosticDataRouteObject, + DataRouteObject, FormEncType, HTMLFormMethod, LazyRouteObject, @@ -141,7 +141,7 @@ const UninstrumentedSymbol = Symbol("Uninstrumented"); export function getRouteInstrumentationUpdates( fns: unstable_InstrumentRouteFunction[], - route: Readonly, + route: Readonly, ) { let aggregated: { lazy: InstrumentFunction[]; @@ -178,23 +178,23 @@ export function getRouteInstrumentationUpdates( ); let updates: { - middleware?: AgnosticDataRouteObject["middleware"]; - loader?: AgnosticDataRouteObject["loader"]; - action?: AgnosticDataRouteObject["action"]; - lazy?: AgnosticDataRouteObject["lazy"]; + middleware?: DataRouteObject["middleware"]; + loader?: DataRouteObject["loader"]; + action?: DataRouteObject["action"]; + lazy?: DataRouteObject["lazy"]; } = {}; // Instrument lazy functions if (typeof route.lazy === "function" && aggregated.lazy.length > 0) { let instrumented = wrapImpl(aggregated.lazy, route.lazy, () => undefined); if (instrumented) { - updates.lazy = instrumented as AgnosticDataRouteObject["lazy"]; + updates.lazy = instrumented as DataRouteObject["lazy"]; } } // Instrument the lazy object format if (typeof route.lazy === "object") { - let lazyObject: LazyRouteObject = route.lazy; + let lazyObject: LazyRouteObject = route.lazy; (["middleware", "loader", "action"] as const).forEach((key) => { let lazyFn = lazyObject[key]; let instrumentations = aggregated[`lazy.${key}`]; diff --git a/packages/react-router/lib/router/router.ts b/packages/react-router/lib/router/router.ts index e0c8ab445a..b2348c0c6b 100644 --- a/packages/react-router/lib/router/router.ts +++ b/packages/react-router/lib/router/router.ts @@ -1,4 +1,3 @@ -import type { DataRouteMatch, RouteObject } from "../context"; import type { History, Location, Path, To } from "./history"; import { Action as NavigationType, @@ -20,10 +19,10 @@ import { instrumentClientSideRouter, } from "./instrumentation"; import type { - AgnosticDataRouteMatch, - AgnosticDataRouteObject, + DataRouteMatch, + DataRouteObject, DataStrategyMatch, - AgnosticRouteObject, + RouteObject, DataResult, DataStrategyFunction, DataStrategyFunctionArgs, @@ -42,7 +41,6 @@ import type { Submission, SuccessResult, UIMatch, - AgnosticPatchRoutesOnNavigationFunction, DataWithResponseInit, LoaderFunctionArgs, ActionFunctionArgs, @@ -50,6 +48,7 @@ import type { ActionFunction, MiddlewareFunction, MiddlewareNextFunction, + PatchRoutesOnNavigationFunction, } from "./utils"; import { ErrorResponseImpl, @@ -109,7 +108,7 @@ export interface Router { * * Return the routes for this router instance */ - get routes(): AgnosticDataRouteObject[]; + get routes(): DataRouteObject[]; /** * @private @@ -285,7 +284,7 @@ export interface Router { */ patchRoutes( routeId: string | null, - children: AgnosticRouteObject[], + children: RouteObject[], unstable_allowElementMutations?: boolean, ): void; @@ -296,7 +295,7 @@ export interface Router { * HMR needs to pass in-flight route updates to React Router * TODO: Replace this with granular route update APIs (addRoute, updateRoute, deleteRoute) */ - _internalSetRoutes(routes: AgnosticRouteObject[]): void; + _internalSetRoutes(routes: RouteObject[]): void; /** * @private @@ -337,7 +336,7 @@ export interface RouterState { /** * The current set of route matches */ - matches: AgnosticDataRouteMatch[]; + matches: DataRouteMatch[]; /** * Tracks whether we've completed our initial data load @@ -415,7 +414,7 @@ export interface FutureConfig {} * Initialization options for createRouter */ export interface RouterInit { - routes: AgnosticRouteObject[]; + routes: RouteObject[]; history: History; basename?: string; getContext?: () => MaybePromise; @@ -426,7 +425,7 @@ export interface RouterInit { hydrationData?: HydrationState; window?: Window; dataStrategy?: DataStrategyFunction; - patchRoutesOnNavigation?: AgnosticPatchRoutesOnNavigationFunction; + patchRoutesOnNavigation?: PatchRoutesOnNavigationFunction; } /** @@ -449,12 +448,12 @@ export interface StaticHandlerContext { * A StaticHandler instance manages a singular SSR navigation/fetch event */ export interface StaticHandler { - dataRoutes: AgnosticDataRouteObject[]; + dataRoutes: DataRouteObject[]; query( request: Request, opts?: { requestContext?: unknown; - filterMatchesToLoad?: (match: AgnosticDataRouteMatch) => boolean; + filterMatchesToLoad?: (match: DataRouteMatch) => boolean; skipLoaderErrorBubbling?: boolean; skipRevalidation?: boolean; dataStrategy?: DataStrategyFunction; @@ -462,7 +461,7 @@ export interface StaticHandler { query: ( r: Request, args?: { - filterMatchesToLoad?: (match: AgnosticDataRouteMatch) => boolean; + filterMatchesToLoad?: (match: DataRouteMatch) => boolean; }, ) => Promise, ) => MaybePromise; @@ -793,7 +792,7 @@ interface FetchLoadMatch { */ interface RevalidatingFetcher extends FetchLoadMatch { key: string; - match: AgnosticDataRouteMatch | null; + match: DataRouteMatch | null; matches: DataStrategyMatch[] | null; request: Request | null; controller: AbortController | null; @@ -892,7 +891,7 @@ export function createRouter(init: RouterInit): Router { if (init.unstable_instrumentations) { let instrumentations = init.unstable_instrumentations; - mapRouteProperties = (route: AgnosticDataRouteObject) => { + mapRouteProperties = (route: DataRouteObject) => { return { ..._mapRouteProperties(route), ...getRouteInstrumentationUpdates( @@ -914,7 +913,7 @@ export function createRouter(init: RouterInit): Router { undefined, manifest, ); - let inFlightDataRoutes: AgnosticDataRouteObject[] | undefined; + let inFlightDataRoutes: DataRouteObject[] | undefined; let basename = init.basename || "/"; if (!basename.startsWith("/")) { basename = `/${basename}`; @@ -1273,7 +1272,7 @@ export function createRouter(init: RouterInit): Router { ) { return { ...m, - route: route as AgnosticDataRouteObject, + route: route as DataRouteObject, }; } return m; @@ -1907,7 +1906,7 @@ export function createRouter(init: RouterInit): Router { request: Request, location: Location, submission: Submission, - matches: AgnosticDataRouteMatch[], + matches: DataRouteMatch[], scopedContext: RouterContextProvider, isFogOfWar: boolean, initialHydration: boolean, @@ -2078,7 +2077,7 @@ export function createRouter(init: RouterInit): Router { async function handleLoaders( request: Request, location: Location, - matches: AgnosticDataRouteMatch[], + matches: DataRouteMatch[], scopedContext: RouterContextProvider, isFogOfWar: boolean, overrideNavigation?: Navigation, @@ -2465,7 +2464,7 @@ export function createRouter(init: RouterInit): Router { key: string, routeId: string, path: string, - requestMatches: AgnosticDataRouteMatch[], + requestMatches: DataRouteMatch[], scopedContext: RouterContextProvider, isFogOfWar: boolean, flushSync: boolean, @@ -2782,7 +2781,7 @@ export function createRouter(init: RouterInit): Router { key: string, routeId: string, path: string, - matches: AgnosticDataRouteMatch[], + matches: DataRouteMatch[], scopedContext: RouterContextProvider, isFogOfWar: boolean, flushSync: boolean, @@ -3428,7 +3427,7 @@ export function createRouter(init: RouterInit): Router { }; } - function getScrollKey(location: Location, matches: AgnosticDataRouteMatch[]) { + function getScrollKey(location: Location, matches: DataRouteMatch[]) { if (getScrollRestorationKey) { let key = getScrollRestorationKey( location, @@ -3441,7 +3440,7 @@ export function createRouter(init: RouterInit): Router { function saveScrollPosition( location: Location, - matches: AgnosticDataRouteMatch[], + matches: DataRouteMatch[], ): void { if (savedScrollPositions && getScrollPosition) { let key = getScrollKey(location, matches); @@ -3451,7 +3450,7 @@ export function createRouter(init: RouterInit): Router { function getSavedScrollPosition( location: Location, - matches: AgnosticDataRouteMatch[], + matches: DataRouteMatch[], ): number | null { if (savedScrollPositions) { let key = getScrollKey(location, matches); @@ -3464,13 +3463,13 @@ export function createRouter(init: RouterInit): Router { } function checkFogOfWar( - matches: AgnosticDataRouteMatch[] | null, - routesToUse: AgnosticDataRouteObject[], + matches: DataRouteMatch[] | null, + routesToUse: DataRouteObject[], pathname: string, - ): { active: boolean; matches: AgnosticDataRouteMatch[] | null } { + ): { active: boolean; matches: DataRouteMatch[] | null } { if (init.patchRoutesOnNavigation) { if (!matches) { - let fogMatches = matchRoutesImpl( + let fogMatches = matchRoutesImpl( routesToUse, pathname, basename, @@ -3483,7 +3482,7 @@ export function createRouter(init: RouterInit): Router { // If we matched a dynamic param or a splat, it might only be because // we haven't yet discovered other routes that would match with a // higher score. Call patchRoutesOnNavigation just to be sure - let partialMatches = matchRoutesImpl( + let partialMatches = matchRoutesImpl( routesToUse, pathname, basename, @@ -3499,12 +3498,12 @@ export function createRouter(init: RouterInit): Router { type DiscoverRoutesSuccessResult = { type: "success"; - matches: AgnosticDataRouteMatch[] | null; + matches: DataRouteMatch[] | null; }; type DiscoverRoutesErrorResult = { type: "error"; error: any; - partialMatches: AgnosticDataRouteMatch[]; + partialMatches: DataRouteMatch[]; }; type DiscoverRoutesAbortedResult = { type: "aborted" }; type DiscoverRoutesResult = @@ -3513,7 +3512,7 @@ export function createRouter(init: RouterInit): Router { | DiscoverRoutesAbortedResult; async function discoverRoutes( - matches: AgnosticDataRouteMatch[], + matches: DataRouteMatch[], pathname: string, signal: AbortSignal, fetcherKey?: string, @@ -3522,7 +3521,7 @@ export function createRouter(init: RouterInit): Router { return { type: "success", matches }; } - let partialMatches: AgnosticDataRouteMatch[] | null = matches; + let partialMatches: DataRouteMatch[] | null = matches; while (true) { let isNonHMR = inFlightDataRoutes == null; let routesToUse = inFlightDataRoutes || dataRoutes; @@ -3564,7 +3563,7 @@ export function createRouter(init: RouterInit): Router { } let newMatches = matchRoutes(routesToUse, pathname, basename); - let newPartialMatches: AgnosticDataRouteMatch[] | null = null; + let newPartialMatches: DataRouteMatch[] | null = null; if (newMatches) { if (Object.keys(newMatches[0].params).length === 0) { @@ -3598,7 +3597,7 @@ export function createRouter(init: RouterInit): Router { // Perform partial matching if we didn't already do it above if (!newPartialMatches) { - newPartialMatches = matchRoutesImpl( + newPartialMatches = matchRoutesImpl( routesToUse, pathname, basename, @@ -3618,16 +3617,13 @@ export function createRouter(init: RouterInit): Router { } } - function compareMatches( - a: AgnosticDataRouteMatch[], - b: AgnosticDataRouteMatch[], - ) { + function compareMatches(a: DataRouteMatch[], b: DataRouteMatch[]) { return ( a.length === b.length && a.every((m, i) => m.route.id === b[i].route.id) ); } - function _internalSetRoutes(newRoutes: AgnosticDataRouteObject[]) { + function _internalSetRoutes(newRoutes: DataRouteObject[]) { manifest = {}; inFlightDataRoutes = convertRoutesToDataRoutes( newRoutes, @@ -3639,7 +3635,7 @@ export function createRouter(init: RouterInit): Router { function patchRoutes( routeId: string | null, - children: AgnosticRouteObject[], + children: RouteObject[], unstable_allowElementMutations = false, ): void { let isNonHMR = inFlightDataRoutes == null; @@ -3731,7 +3727,7 @@ export interface CreateStaticHandlerOptions { } export function createStaticHandler( - routes: AgnosticRouteObject[], + routes: RouteObject[], opts?: CreateStaticHandlerOptions, ): StaticHandler { invariant( @@ -3750,7 +3746,7 @@ export function createStaticHandler( if (opts?.unstable_instrumentations) { let instrumentations = opts.unstable_instrumentations; - mapRouteProperties = (route: AgnosticDataRouteObject) => { + mapRouteProperties = (route: DataRouteObject) => { return { ..._mapRouteProperties(route), ...getRouteInstrumentationUpdates( @@ -3901,7 +3897,7 @@ export function createStaticHandler( revalidationRequest: Request, opts: { filterMatchesToLoad?: - | ((match: AgnosticDataRouteMatch) => boolean) + | ((match: DataRouteMatch) => boolean) | undefined; } = {}, ) => { @@ -4211,12 +4207,12 @@ export function createStaticHandler( async function queryImpl( request: Request, location: Location, - matches: AgnosticDataRouteMatch[], + matches: DataRouteMatch[], requestContext: unknown, dataStrategy: DataStrategyFunction | null, skipLoaderErrorBubbling: boolean, - routeMatch: AgnosticDataRouteMatch | null, - filterMatchesToLoad: ((m: AgnosticDataRouteMatch) => boolean) | null, + routeMatch: DataRouteMatch | null, + filterMatchesToLoad: ((m: DataRouteMatch) => boolean) | null, skipRevalidation: boolean, ): Promise | Response> { invariant( @@ -4277,13 +4273,13 @@ export function createStaticHandler( async function submit( request: Request, - matches: AgnosticDataRouteMatch[], - actionMatch: AgnosticDataRouteMatch, + matches: DataRouteMatch[], + actionMatch: DataRouteMatch, requestContext: unknown, dataStrategy: DataStrategyFunction | null, skipLoaderErrorBubbling: boolean, isRouteRequest: boolean, - filterMatchesToLoad: ((m: AgnosticDataRouteMatch) => boolean) | null, + filterMatchesToLoad: ((m: DataRouteMatch) => boolean) | null, skipRevalidation: boolean, ): Promise | Response> { let result: DataResult; @@ -4466,12 +4462,12 @@ export function createStaticHandler( async function loadRouteData( request: Request, - matches: AgnosticDataRouteMatch[], + matches: DataRouteMatch[], requestContext: unknown, dataStrategy: DataStrategyFunction | null, skipLoaderErrorBubbling: boolean, - routeMatch: AgnosticDataRouteMatch | null, - filterMatchesToLoad: ((match: AgnosticDataRouteMatch) => boolean) | null, + routeMatch: DataRouteMatch | null, + filterMatchesToLoad: ((match: DataRouteMatch) => boolean) | null, pendingActionResult?: PendingActionResult, ): Promise< | Omit< @@ -4664,7 +4660,7 @@ export function createStaticHandler( * @category Utils */ export function getStaticContextFromError( - routes: AgnosticDataRouteObject[], + routes: DataRouteObject[], handlerContext: StaticHandlerContext, error: any, boundaryId?: string, @@ -4706,14 +4702,14 @@ function isSubmissionNavigation( function normalizeTo( location: Path, - matches: AgnosticDataRouteMatch[], + matches: DataRouteMatch[], basename: string, to: To | null, fromRouteId?: string, relative?: RelativeRoutingType, ) { - let contextualMatches: AgnosticDataRouteMatch[]; - let activeRouteMatch: AgnosticDataRouteMatch | undefined; + let contextualMatches: DataRouteMatch[]; + let activeRouteMatch: DataRouteMatch | undefined; if (fromRouteId) { // Grab matches up to the calling route so our route-relative logic is // relative to the correct source route @@ -4926,7 +4922,7 @@ function getMatchesToLoad( manifest: RouteManifest, history: History, state: RouterState, - matches: AgnosticDataRouteMatch[], + matches: DataRouteMatch[], submission: Submission | undefined, location: Location, lazyRoutePropertiesToSkip: string[], @@ -4936,7 +4932,7 @@ function getMatchesToLoad( fetchersQueuedForDeletion: Set, fetchLoadMatches: Map, fetchRedirectIds: Set, - routesToUse: AgnosticDataRouteObject[], + routesToUse: DataRouteObject[], basename: string | undefined, hasPatchRoutesOnNavigation: boolean, pendingActionResult?: PendingActionResult, @@ -5227,7 +5223,7 @@ function routeHasLoaderOrMiddleware(route: RouteObject) { // except for when we are loading a route due to `loader.hydrate=true`, in which // case we don't want to render a fallback function getRouteHydrationStatus( - route: AgnosticDataRouteObject, + route: DataRouteObject, loaderData: RouteData | null | undefined, errors: RouteData | null | undefined, ): { shouldLoad: boolean; renderFallback: boolean } { @@ -5262,8 +5258,8 @@ function getRouteHydrationStatus( function isNewLoader( currentLoaderData: RouteData, - currentMatch: AgnosticDataRouteMatch, - match: AgnosticDataRouteMatch, + currentMatch: DataRouteMatch, + match: DataRouteMatch, ) { let isNew = // [a] -> [a, b] @@ -5280,8 +5276,8 @@ function isNewLoader( } function isNewRouteInstance( - currentMatch: AgnosticDataRouteMatch, - match: AgnosticDataRouteMatch, + currentMatch: DataRouteMatch, + match: DataRouteMatch, ) { let currentPath = currentMatch.route.path; return ( @@ -5296,7 +5292,7 @@ function isNewRouteInstance( } function shouldRevalidateLoader( - loaderMatch: AgnosticDataRouteMatch, + loaderMatch: DataRouteMatch, arg: ShouldRevalidateFunctionArgs, ) { if (loaderMatch.route.shouldRevalidate) { @@ -5311,13 +5307,13 @@ function shouldRevalidateLoader( function patchRoutesImpl( routeId: string | null, - children: AgnosticRouteObject[], - routesToUse: AgnosticDataRouteObject[], + children: RouteObject[], + routesToUse: DataRouteObject[], manifest: RouteManifest, mapRouteProperties: MapRoutePropertiesFunction, allowElementMutations: boolean, ) { - let childrenToPatch: AgnosticDataRouteObject[]; + let childrenToPatch: DataRouteObject[]; if (routeId) { let route = manifest[routeId]; invariant( @@ -5335,10 +5331,10 @@ function patchRoutesImpl( // Don't patch in routes we already know about so that `patch` is idempotent // to simplify user-land code. This is useful because we re-call the // `patchRoutesOnNavigation` function for matched routes with params. - let uniqueChildren: AgnosticRouteObject[] = []; + let uniqueChildren: RouteObject[] = []; let existingChildren: { - existingRoute: AgnosticRouteObject; - newRoute: AgnosticRouteObject; + existingRoute: RouteObject; + newRoute: RouteObject; }[] = []; children.forEach((newRoute) => { let existingRoute = childrenToPatch.find((existingRoute) => @@ -5392,8 +5388,8 @@ function patchRoutesImpl( } function isSameRoute( - newRoute: AgnosticRouteObject, - existingRoute: AgnosticRouteObject, + newRoute: RouteObject, + existingRoute: RouteObject, ): boolean { // Most optimal check is by id if ( @@ -5434,8 +5430,8 @@ function isSameRoute( } const lazyRoutePropertyCache = new WeakMap< - AgnosticDataRouteObject, - Partial>> + DataRouteObject, + Partial>> >(); const loadLazyRouteProperty = ({ @@ -5444,8 +5440,8 @@ const loadLazyRouteProperty = ({ manifest, mapRouteProperties, }: { - key: keyof AgnosticDataRouteObject; - route: AgnosticDataRouteObject; + key: keyof DataRouteObject; + route: DataRouteObject; manifest: RouteManifest; mapRouteProperties: MapRoutePropertiesFunction; }): Promise | undefined => { @@ -5516,10 +5512,7 @@ const loadLazyRouteProperty = ({ return propertyPromise; }; -const lazyRouteFunctionCache = new WeakMap< - AgnosticDataRouteObject, - Promise ->(); +const lazyRouteFunctionCache = new WeakMap>(); /** * Execute route.lazy functions to lazily load route modules (loader, action, @@ -5527,7 +5520,7 @@ const lazyRouteFunctionCache = new WeakMap< * with dataRoutes so those get updated as well. */ function loadLazyRoute( - route: AgnosticDataRouteObject, + route: DataRouteObject, type: "loader" | "action", manifest: RouteManifest, mapRouteProperties: MapRoutePropertiesFunction, @@ -5684,7 +5677,7 @@ function isNonNullable(value: T): value is NonNullable { } function loadLazyMiddlewareForMatches( - matches: AgnosticDataRouteMatch[], + matches: DataRouteMatch[], manifest: RouteManifest, mapRouteProperties: MapRoutePropertiesFunction, ): Promise | void { @@ -5739,7 +5732,7 @@ function runServerMiddlewarePipeline( ) & { // Don't use `DataStrategyFunctionArgs` directly so we can we reduce these // back from `DataStrategyMatch` to regular matches for use in the staticHandler - matches: AgnosticDataRouteMatch[]; + matches: DataRouteMatch[]; }, handler: () => Promise, errorHandler: ( @@ -5833,7 +5826,7 @@ async function runMiddlewarePipeline( ) & { // Don't use `DataStrategyFunctionArgs` directly so we can we reduce these // back from `DataStrategyMatch` to regular matches for use in the staticHandler - matches: AgnosticDataRouteMatch[]; + matches: DataRouteMatch[]; }, // Handler to generate a Result in the leaf next() function handler: () => Promise, @@ -6072,8 +6065,8 @@ function getTargetedDataStrategyMatches( mapRouteProperties: MapRoutePropertiesFunction, manifest: RouteManifest, request: Request, - matches: AgnosticDataRouteMatch[], - targetMatch: AgnosticDataRouteMatch, + matches: DataRouteMatch[], + targetMatch: DataRouteMatch, lazyRoutePropertiesToSkip: string[], scopedContext: unknown, shouldRevalidateArgs: DataStrategyMatch["shouldRevalidateArgs"] = null, @@ -6197,7 +6190,7 @@ async function callLoaderOrAction({ }: { request: Request; unstable_pattern: string; - match: AgnosticDataRouteMatch; + match: DataRouteMatch; lazyHandlerPromise: Promise | undefined; lazyRoutePromise: Promise | undefined; handlerOverride: Parameters[0]; @@ -6416,7 +6409,7 @@ function normalizeRelativeRoutingRedirectResponse( response: Response, request: Request, routeId: string, - matches: AgnosticDataRouteMatch[], + matches: DataRouteMatch[], basename: string, ) { let location = response.headers.get("Location"); @@ -6551,7 +6544,7 @@ function convertSearchParamsToFormData( } function processRouteLoaderData( - matches: AgnosticDataRouteMatch[], + matches: DataRouteMatch[], results: Record, pendingActionResult: PendingActionResult | undefined, isStaticHandler = false, @@ -6657,7 +6650,7 @@ function processRouteLoaderData( function processLoaderData( state: RouterState, - matches: AgnosticDataRouteMatch[], + matches: DataRouteMatch[], results: Record, pendingActionResult: PendingActionResult | undefined, revalidatingFetchers: RevalidatingFetcher[], @@ -6713,7 +6706,7 @@ function processLoaderData( function mergeLoaderData( loaderData: RouteData, newLoaderData: RouteData, - matches: AgnosticDataRouteMatch[], + matches: DataRouteMatch[], errors: RouteData | null | undefined, ): RouteData { // Start with all new entries that are not being reset @@ -6766,9 +6759,9 @@ function getActionDataForCommit( // route specified by routeId) for the closest ancestor error boundary, // defaulting to the root match function findNearestBoundary( - matches: AgnosticDataRouteMatch[], + matches: DataRouteMatch[], routeId?: string, -): AgnosticDataRouteMatch { +): DataRouteMatch { let eligibleMatches = routeId ? matches.slice(0, matches.findIndex((m) => m.route.id === routeId) + 1) : [...matches]; @@ -6778,9 +6771,9 @@ function findNearestBoundary( ); } -function getShortCircuitMatches(routes: AgnosticDataRouteObject[]): { - matches: AgnosticDataRouteMatch[]; - route: AgnosticDataRouteObject; +function getShortCircuitMatches(routes: DataRouteObject[]): { + matches: DataRouteMatch[]; + route: DataRouteObject; } { // Prefer a root layout route if present, otherwise shim in a route object let route = @@ -6999,7 +6992,7 @@ function hasNakedIndexQuery(search: string): boolean { } function getTargetMatch( - matches: AgnosticDataRouteMatch[], + matches: DataRouteMatch[], location: Location | string, ) { let search = diff --git a/packages/react-router/lib/router/utils.ts b/packages/react-router/lib/router/utils.ts index 7a5c10b7c7..369093b9e0 100644 --- a/packages/react-router/lib/router/utils.ts +++ b/packages/react-router/lib/router/utils.ts @@ -1,3 +1,4 @@ +import type * as React from "react"; import type { MiddlewareEnabled } from "../types/future"; import type { Equal, Expect } from "../types/utils"; import type { Location, Path, To } from "./history"; @@ -362,11 +363,11 @@ export interface ShouldRevalidateFunctionArgs { /** This is the url the navigation started from. You can compare it with `nextUrl` to decide if you need to revalidate this route's data. */ currentUrl: URL; /** These are the {@link https://reactrouter.com/start/framework/routing#dynamic-segments dynamic route params} from the URL that can be compared to the `nextParams` to decide if you need to reload or not. Perhaps you're using only a partial piece of the param for data loading, you don't need to revalidate if a superfluous part of the param changed. */ - currentParams: AgnosticDataRouteMatch["params"]; + currentParams: DataRouteMatch["params"]; /** In the case of navigation, this the URL the user is requesting. Some revalidations are not navigation, so it will simply be the same as currentUrl. */ nextUrl: URL; /** In the case of navigation, these are the {@link https://reactrouter.com/start/framework/routing#dynamic-segments dynamic route params} from the next location the user is requesting. Some revalidations are not navigation, so it will simply be the same as currentParams. */ - nextParams: AgnosticDataRouteMatch["params"]; + nextParams: DataRouteMatch["params"]; /** The method (probably `"GET"` or `"POST"`) used in the form submission that triggered the revalidation. */ formMethod?: Submission["formMethod"]; /** The form action (`
`) that triggered the revalidation. */ @@ -423,8 +424,7 @@ export interface ShouldRevalidateFunction { (args: ShouldRevalidateFunctionArgs): boolean; } -export interface DataStrategyMatch - extends AgnosticRouteMatch { +export interface DataStrategyMatch extends RouteMatch { /** * @private */ @@ -527,30 +527,23 @@ export interface DataStrategyFunction { ): Promise>; } -export type AgnosticPatchRoutesOnNavigationFunctionArgs< - O extends AgnosticRouteObject = AgnosticRouteObject, - M extends AgnosticRouteMatch = AgnosticRouteMatch, -> = { +export type PatchRoutesOnNavigationFunctionArgs = { signal: AbortSignal; path: string; - matches: M[]; + matches: RouteMatch[]; fetcherKey: string | undefined; - patch: (routeId: string | null, children: O[]) => void; + patch: (routeId: string | null, children: RouteObject[]) => void; }; -export type AgnosticPatchRoutesOnNavigationFunction< - O extends AgnosticRouteObject = AgnosticRouteObject, - M extends AgnosticRouteMatch = AgnosticRouteMatch, -> = ( - opts: AgnosticPatchRoutesOnNavigationFunctionArgs, +export type PatchRoutesOnNavigationFunction = ( + opts: PatchRoutesOnNavigationFunctionArgs, ) => MaybePromise; /** - * Function provided by the framework-aware layers to set any framework-specific - * properties from framework-agnostic properties + * Function provided to set route-specific properties from route objects */ export interface MapRoutePropertiesFunction { - (route: AgnosticDataRouteObject): { + (route: DataRouteObject): { hasErrorBoundary: boolean; } & Record; } @@ -613,7 +606,7 @@ export function isUnsupportedLazyRouteFunctionKey( * lazy object to load route properties, which can add non-matching * related properties to a route */ -export type LazyRouteObject = { +export type LazyRouteObject = { [K in keyof R as K extends UnsupportedLazyRouteObjectKey ? never : K]?: () => Promise; @@ -623,46 +616,124 @@ export type LazyRouteObject = { * lazy() function to load a route definition, which can add non-matching * related properties to a route */ -export interface LazyRouteFunction { +export interface LazyRouteFunction { (): Promise< Omit & Partial> >; } -export type LazyRouteDefinition = +export type LazyRouteDefinition = | LazyRouteObject | LazyRouteFunction; /** * Base RouteObject with common props shared by all types of routes + * @internal */ -type AgnosticBaseRouteObject = { +export type BaseRouteObject = { + /** + * Whether the path should be case-sensitive. Defaults to `false`. + */ caseSensitive?: boolean; + /** + * The path pattern to match. If unspecified or empty, then this becomes a + * layout route. + */ path?: string; + /** + * The unique identifier for this route (for use with {@link DataRouter}s) + */ id?: string; + /** + * The route middleware. + * See [`middleware`](../../start/data/route-object#middleware). + */ middleware?: MiddlewareFunction[]; + /** + * The route loader. + * See [`loader`](../../start/data/route-object#loader). + */ loader?: LoaderFunction | boolean; + /** + * The route action. + * See [`action`](../../start/data/route-object#action). + */ action?: ActionFunction | boolean; + // TODO(v8): deprecate/remove hasErrorBoundary?: boolean; + /** + * The route shouldRevalidate function. + * See [`shouldRevalidate`](../../start/data/route-object#shouldRevalidate). + */ shouldRevalidate?: ShouldRevalidateFunction; + /** + * The route handle. + */ handle?: any; - lazy?: LazyRouteDefinition; + /** + * A function that returns a promise that resolves to the route object. + * Used for code-splitting routes. + * See [`lazy`](../../start/data/route-object#lazy). + */ + lazy?: LazyRouteDefinition; + /** + * The React Component to render when this route matches. + * Mutually exclusive with `element`. + */ + Component?: React.ComponentType | null; + /** + * The React element to render when this Route matches. + * Mutually exclusive with `Component`. + */ + element?: React.ReactNode | null; + /** + * The React Component to render at this route if an error occurs. + * Mutually exclusive with `errorElement`. + */ + ErrorBoundary?: React.ComponentType | null; + /** + * The React element to render at this route if an error occurs. + * Mutually exclusive with `ErrorBoundary`. + */ + errorElement?: React.ReactNode | null; + /** + * The React Component to render while this router is loading data. + * Mutually exclusive with `hydrateFallbackElement`. + */ + HydrateFallback?: React.ComponentType | null; + /** + * The React element to render while this router is loading data. + * Mutually exclusive with `HydrateFallback`. + */ + hydrateFallbackElement?: React.ReactNode | null; }; /** * Index routes must not have children */ -export type AgnosticIndexRouteObject = AgnosticBaseRouteObject & { +export type IndexRouteObject = BaseRouteObject & { + /** + * Child Route objects - not valid on index routes. + */ children?: undefined; + /** + * Whether this is an index route. + */ index: true; }; /** - * Non-index routes may have children, but cannot have index + * Non-index routes may have children, but cannot have `index` set to `true`. */ -export type AgnosticNonIndexRouteObject = AgnosticBaseRouteObject & { - children?: AgnosticRouteObject[]; +export type NonIndexRouteObject = BaseRouteObject & { + /** + * Child Route objects. + */ + children?: RouteObject[]; + /** + * Whether this is an index route - must be `false` or undefined on non-index routes. + */ index?: false; }; @@ -670,30 +741,23 @@ export type AgnosticNonIndexRouteObject = AgnosticBaseRouteObject & { * A route object represents a logical route, with (optionally) its child * routes organized in a tree-like structure. */ -export type AgnosticRouteObject = - | AgnosticIndexRouteObject - | AgnosticNonIndexRouteObject; +export type RouteObject = IndexRouteObject | NonIndexRouteObject; -export type AgnosticDataIndexRouteObject = AgnosticIndexRouteObject & { +export type DataIndexRouteObject = IndexRouteObject & { id: string; }; -export type AgnosticDataNonIndexRouteObject = AgnosticNonIndexRouteObject & { - children?: AgnosticDataRouteObject[]; +export type DataNonIndexRouteObject = NonIndexRouteObject & { + children?: DataRouteObject[]; id: string; }; /** * A data route object, which is just a RouteObject with a required unique ID */ -export type AgnosticDataRouteObject = - | AgnosticDataIndexRouteObject - | AgnosticDataNonIndexRouteObject; +export type DataRouteObject = DataIndexRouteObject | DataNonIndexRouteObject; -export type RouteManifest = Record< - string, - R | undefined ->; +export type RouteManifest = Record; // prettier-ignore type Regex_az = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" @@ -767,9 +831,9 @@ export type Params = { /** * A RouteMatch contains info about how a route matched a URL. */ -export interface AgnosticRouteMatch< +export interface RouteMatch< ParamKey extends string = string, - RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject, + RouteObjectType extends RouteObject = RouteObject, > { /** * The names and values of dynamic parameters in the URL. @@ -789,24 +853,21 @@ export interface AgnosticRouteMatch< route: RouteObjectType; } -export interface AgnosticDataRouteMatch - extends AgnosticRouteMatch {} +export interface DataRouteMatch extends RouteMatch {} -function isIndexRoute( - route: AgnosticRouteObject, -): route is AgnosticIndexRouteObject { +function isIndexRoute(route: RouteObject): route is IndexRouteObject { return route.index === true; } // Walk the route tree generating unique IDs where necessary, so we are working -// solely with AgnosticDataRouteObject's within the Router +// solely with DataRouteObject's within the Router export function convertRoutesToDataRoutes( - routes: AgnosticRouteObject[], + routes: RouteObject[], mapRouteProperties: MapRoutePropertiesFunction, parentPath: string[] = [], manifest: RouteManifest = {}, allowInPlaceMutations = false, -): AgnosticDataRouteObject[] { +): DataRouteObject[] { return routes.map((route, index) => { let treePath = [...parentPath, String(index)]; let id = typeof route.id === "string" ? route.id : treePath.join("-"); @@ -821,7 +882,7 @@ export function convertRoutesToDataRoutes( ); if (isIndexRoute(route)) { - let indexRoute: AgnosticDataIndexRouteObject = { + let indexRoute: DataIndexRouteObject = { ...route, id, }; @@ -831,7 +892,7 @@ export function convertRoutesToDataRoutes( ); return indexRoute; } else { - let pathOrLayoutRoute: AgnosticDataNonIndexRouteObject = { + let pathOrLayoutRoute: DataNonIndexRouteObject = { ...route, id, children: undefined, @@ -856,7 +917,7 @@ export function convertRoutesToDataRoutes( }); } -function mergeRouteUpdates( +function mergeRouteUpdates( route: T, updates: ReturnType, ): T { @@ -899,24 +960,22 @@ function mergeRouteUpdates( * Defaults to `/`. * @returns An array of matched routes, or `null` if no matches were found. */ -export function matchRoutes< - RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject, ->( +export function matchRoutes( routes: RouteObjectType[], locationArg: Partial | string, basename = "/", -): AgnosticRouteMatch[] | null { +): RouteMatch[] | null { return matchRoutesImpl(routes, locationArg, basename, false); } export function matchRoutesImpl< - RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject, + RouteObjectType extends RouteObject = RouteObject, >( routes: RouteObjectType[], locationArg: Partial | string, basename: string, allowPartial: boolean, -): AgnosticRouteMatch[] | null { +): RouteMatch[] | null { let location = typeof locationArg === "string" ? parsePath(locationArg) : locationArg; @@ -954,7 +1013,7 @@ export interface UIMatch { /** * {@link https://reactrouter.com/start/framework/routing#dynamic-segments Dynamic route params} for the matched route. */ - params: AgnosticRouteMatch["params"]; + params: RouteMatch["params"]; /** * The return value from the matched route's loader or clientLoader. This might * be `undefined` if this route's `loader` (or a deeper route's `loader`) threw @@ -977,7 +1036,7 @@ export interface UIMatch { } export function convertRouteMatchToUiMatch( - match: AgnosticDataRouteMatch, + match: DataRouteMatch, loaderData: RouteData, ): UIMatch { let { route, pathname, params } = match; @@ -991,26 +1050,20 @@ export function convertRouteMatchToUiMatch( }; } -interface RouteMeta< - RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject, -> { +interface RouteMeta { relativePath: string; caseSensitive: boolean; childrenIndex: number; route: RouteObjectType; } -interface RouteBranch< - RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject, -> { +interface RouteBranch { path: string; score: number; routesMeta: RouteMeta[]; } -function flattenRoutes< - RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject, ->( +function flattenRoutes( routes: RouteObjectType[], branches: RouteBranch[] = [], parentsMeta: RouteMeta[] = [], @@ -1222,17 +1275,17 @@ function compareIndexes(a: number[], b: number[]): number { function matchRouteBranch< ParamKey extends string = string, - RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject, + RouteObjectType extends RouteObject = RouteObject, >( branch: RouteBranch, pathname: string, allowPartial = false, -): AgnosticRouteMatch[] | null { +): RouteMatch[] | null { let { routesMeta } = branch; let matchedParams = {}; let matchedPathname = "/"; - let matches: AgnosticRouteMatch[] = []; + let matches: RouteMatch[] = []; for (let i = 0; i < routesMeta.length; ++i) { let meta = routesMeta[i]; let end = i === routesMeta.length - 1; @@ -1687,9 +1740,9 @@ function getInvalidPathError( // // -export function getPathContributingMatches< - T extends AgnosticRouteMatch = AgnosticRouteMatch, ->(matches: T[]) { +export function getPathContributingMatches( + matches: T[], +) { return matches.filter( (match, index) => index === 0 || (match.route.path && match.route.path.length > 0), @@ -1698,9 +1751,9 @@ export function getPathContributingMatches< // Return the array of pathnames for the current route matches - used to // generate the routePathnames input for resolveTo() -export function getResolveToMatches< - T extends AgnosticRouteMatch = AgnosticRouteMatch, ->(matches: T[]) { +export function getResolveToMatches( + matches: T[], +) { let pathMatches = getPathContributingMatches(matches); // Use the full pathname for the leaf match so we include splat values for "." links @@ -2063,7 +2116,7 @@ by the star-slash in the `getRoutePattern` regex and messes up the parsed commen for `isRouteErrorResponse` above. This comment seems to reset the parser. */ -export function getRoutePattern(matches: AgnosticRouteMatch[]) { +export function getRoutePattern(matches: RouteMatch[]) { return ( matches .map((m) => m.route.path) diff --git a/packages/react-router/lib/rsc/browser.tsx b/packages/react-router/lib/rsc/browser.tsx index 865ef8832f..fd38871cd6 100644 --- a/packages/react-router/lib/rsc/browser.tsx +++ b/packages/react-router/lib/rsc/browser.tsx @@ -2,11 +2,7 @@ import * as React from "react"; import * as ReactDOM from "react-dom"; import { RouterProvider } from "../components"; -import { - RSCRouterContext, - type DataRouteMatch, - type DataRouteObject, -} from "../context"; +import { RSCRouterContext } from "../context"; import { FrameworkContext, setIsHydrated } from "../dom/ssr/components"; import type { FrameworkContextObject } from "../dom/ssr/entry"; import { createBrowserHistory, invariant } from "../router/history"; @@ -18,7 +14,8 @@ import type { RSCRenderPayload, } from "./server.rsc"; import type { - AgnosticDataRouteObject, + DataRouteMatch, + DataRouteObject, DataStrategyFunction, DataStrategyFunctionArgs, RouterContextProvider, @@ -1084,9 +1081,7 @@ function isExternalLocation(location: string) { return newLocation.origin !== window.location.origin; } -function cloneRoutes( - routes: AgnosticDataRouteObject[] | undefined, -): AgnosticDataRouteObject[] { +function cloneRoutes(routes: DataRouteObject[] | undefined): DataRouteObject[] { if (!routes) return undefined as any; return routes.map((route) => ({ ...route, @@ -1094,10 +1089,7 @@ function cloneRoutes( })) as any; } -function diffRoutes( - a: AgnosticDataRouteObject[], - b: AgnosticDataRouteObject[], -): boolean { +function diffRoutes(a: DataRouteObject[], b: DataRouteObject[]): boolean { if (a.length !== b.length) return true; return a.some((route, index) => { if ((route as any).element !== (b[index] as any).element) return true; diff --git a/packages/react-router/lib/rsc/server.rsc.ts b/packages/react-router/lib/rsc/server.rsc.ts index e19c12326d..cc41cbaca6 100644 --- a/packages/react-router/lib/rsc/server.rsc.ts +++ b/packages/react-router/lib/rsc/server.rsc.ts @@ -20,10 +20,12 @@ import { } from "../router/router"; import { type ActionFunction, - type AgnosticDataRouteMatch, + type DataRouteMatch, type LoaderFunction, type Params, type ShouldRevalidateFunction, + type RouteMatch, + type RouteObject, type RouterContextProvider, type TrackedPromise, isAbsoluteUrl, @@ -39,7 +41,6 @@ import { import { getDocumentHeadersImpl } from "../server-runtime/headers"; import { SINGLE_FETCH_REDIRECT_STATUS } from "../dom/ssr/single-fetch"; import { throwIfPotentialCSRFAttack } from "../actions"; -import type { RouteMatch, RouteObject } from "../context"; import invariant from "../server-runtime/invariant"; import { @@ -1167,7 +1168,7 @@ async function getRSCRouteMatch({ parentId, }: { staticContext: StaticHandlerContext; - match: AgnosticDataRouteMatch; + match: DataRouteMatch; isBelowErrorBoundary: boolean; routeIdsToLoad: string[] | null; parentId: string | undefined; diff --git a/packages/react-router/lib/rsc/server.ssr.tsx b/packages/react-router/lib/rsc/server.ssr.tsx index 845d04544f..491bfbe8f7 100644 --- a/packages/react-router/lib/rsc/server.ssr.tsx +++ b/packages/react-router/lib/rsc/server.ssr.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import { RSCRouterContext, type DataRouteObject } from "../context"; +import { RSCRouterContext } from "../context"; import { FrameworkContext } from "../dom/ssr/components"; import type { FrameworkContextObject } from "../dom/ssr/entry"; import { SINGLE_FETCH_REDIRECT_STATUS } from "../dom/ssr/single-fetch"; @@ -9,7 +9,7 @@ import { RSCRouterGlobalErrorBoundary } from "./errorBoundaries"; import { shouldHydrateRouteLoader } from "../dom/ssr/routes"; import type { RSCPayload } from "./server.rsc"; import { createRSCRouteModules } from "./route-modules"; -import { isRouteErrorResponse } from "../router/utils"; +import { isRouteErrorResponse, type DataRouteObject } from "../router/utils"; import { decodeRedirectErrorDigest, decodeRouteErrorResponseDigest, diff --git a/packages/react-router/lib/server-runtime/headers.ts b/packages/react-router/lib/server-runtime/headers.ts index bef73e22c7..fa1988a172 100644 --- a/packages/react-router/lib/server-runtime/headers.ts +++ b/packages/react-router/lib/server-runtime/headers.ts @@ -1,6 +1,6 @@ import { splitCookiesString } from "set-cookie-parser"; -import type { DataRouteMatch } from "../context"; +import type { DataRouteMatch } from "../router/utils"; import type { StaticHandlerContext } from "../router/router"; import type { ServerRouteModule } from "../dom/ssr/routeModules"; import type { ServerBuild } from "./build"; diff --git a/packages/react-router/lib/server-runtime/routeMatching.ts b/packages/react-router/lib/server-runtime/routeMatching.ts index e6d38dab7d..c0f105c464 100644 --- a/packages/react-router/lib/server-runtime/routeMatching.ts +++ b/packages/react-router/lib/server-runtime/routeMatching.ts @@ -1,4 +1,4 @@ -import type { Params, AgnosticRouteObject } from "../router/utils"; +import type { Params, RouteObject } from "../router/utils"; import { matchRoutes } from "../router/utils"; import type { ServerRoute } from "./routes"; @@ -14,7 +14,7 @@ export function matchServerRoutes( basename?: string, ): RouteMatch[] | null { let matches = matchRoutes( - routes as unknown as AgnosticRouteObject[], + routes as unknown as RouteObject[], pathname, basename, ); diff --git a/packages/react-router/lib/server-runtime/routes.ts b/packages/react-router/lib/server-runtime/routes.ts index a445101e44..d221b1f752 100644 --- a/packages/react-router/lib/server-runtime/routes.ts +++ b/packages/react-router/lib/server-runtime/routes.ts @@ -1,5 +1,5 @@ import type { - AgnosticDataRouteObject, + DataRouteObject, LoaderFunctionArgs as RRLoaderFunctionArgs, ActionFunctionArgs as RRActionFunctionArgs, RouteManifest, @@ -70,7 +70,7 @@ export function createStaticHandlerDataRoutes( string, Omit[] > = groupRoutesByParentId(manifest), -): AgnosticDataRouteObject[] { +): DataRouteObject[] { return (routesByParentId[parentId] || []).map((route) => { let commonRoute = { // Always include root due to default boundaries From 792e435bdb7fdcd0694569e4a2f83efe6906ced4 Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Thu, 19 Feb 2026 15:44:53 +0000 Subject: [PATCH 02/33] chore: generate markdown docs from jsdocs --- docs/api/utils/matchRoutes.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/api/utils/matchRoutes.md b/docs/api/utils/matchRoutes.md index 8bdaad062e..bba7764655 100644 --- a/docs/api/utils/matchRoutes.md +++ b/docs/api/utils/matchRoutes.md @@ -42,13 +42,11 @@ matchRoutes(routes, "/dashboard"); // [rootMatch, dashboardMatch] ## Signature ```tsx -function matchRoutes< - RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject, ->( +function matchRoutes( routes: RouteObjectType[], locationArg: Partial | string, basename = "/", -): AgnosticRouteMatch[] | null +): RouteMatch[] | null ``` ## Params From 492d7c39ec19f7d9c15f5173adc2cd1792265f14 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 19 Feb 2026 11:05:01 -0500 Subject: [PATCH 03/33] Update modal example to react 18 --- examples/modal/package-lock.json | 342 ++++--------------------------- examples/modal/package.json | 9 +- examples/modal/src/App.tsx | 124 ++++++++++- 3 files changed, 164 insertions(+), 311 deletions(-) diff --git a/examples/modal/package-lock.json b/examples/modal/package-lock.json index 08cb323fe0..da288722cb 100644 --- a/examples/modal/package-lock.json +++ b/examples/modal/package-lock.json @@ -6,16 +6,15 @@ "": { "name": "modal", "dependencies": { - "@reach/dialog": "0.18.0", - "react": "^17.0.2", - "react-dom": "^17.0.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", "react-router-dom": "^6.15.0" }, "devDependencies": { "@rollup/plugin-replace": "^5.0.2", "@types/node": "18.x", - "@types/react": "^17.0.59", - "@types/react-dom": "^17.0.20", + "@types/react": "^18.3.28", + "@types/react-dom": "^18.3.7", "@vitejs/plugin-react": "^3.0.1", "typescript": "^4.9.5", "vite": "^4.0.4" @@ -314,17 +313,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/runtime": { - "version": "7.22.3", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.3.tgz", - "integrity": "sha512-XsDuspWKLUsxwCp6r7EhsExHtYfbe5oAGQ19kqngTdCPUoPQzOPdUbD/pB9PJiwb2ptYKQDjSJT3R6dC+EPqfQ==", - "dependencies": { - "regenerator-runtime": "^0.13.11" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/template": { "version": "7.21.9", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.21.9.tgz", @@ -780,51 +768,6 @@ "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", "dev": true }, - "node_modules/@reach/dialog": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@reach/dialog/-/dialog-0.18.0.tgz", - "integrity": "sha512-hWhzmBK8VJj+yf6OivFqHFZIV4q9TISZrkPaglKE5oFYtrr75lxWjrBTA+BshL0r/FfKodvNtdT8yq4vj/6Gcw==", - "dependencies": { - "@reach/polymorphic": "0.18.0", - "@reach/portal": "0.18.0", - "@reach/utils": "0.18.0", - "react-focus-lock": "2.5.2", - "react-remove-scroll": "2.4.3" - }, - "peerDependencies": { - "react": "^16.8.0 || 17.x", - "react-dom": "^16.8.0 || 17.x" - } - }, - "node_modules/@reach/polymorphic": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@reach/polymorphic/-/polymorphic-0.18.0.tgz", - "integrity": "sha512-N9iAjdMbE//6rryZZxAPLRorzDcGBnluf7YQij6XDLiMtfCj1noa7KyLpEc/5XCIB/EwhX3zCluFAwloBKdblA==", - "peerDependencies": { - "react": "^16.8.0 || 17.x" - } - }, - "node_modules/@reach/portal": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@reach/portal/-/portal-0.18.0.tgz", - "integrity": "sha512-TImozRapd576ofRk30Le2L3lRTFXF1p47B182wnp5eMTdZa74JX138BtNGEPJFOyrMaVmguVF8SSwZ6a0fon1Q==", - "dependencies": { - "@reach/utils": "0.18.0" - }, - "peerDependencies": { - "react": "^16.8.0 || 17.x", - "react-dom": "^16.8.0 || 17.x" - } - }, - "node_modules/@reach/utils": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@reach/utils/-/utils-0.18.0.tgz", - "integrity": "sha512-KdVMdpTgDyK8FzdKO9SCpiibuy/kbv3pwgfXshTI6tEcQT1OOwj7BAksnzGC0rPz0UholwC+AgkqEl3EJX3M1A==", - "peerDependencies": { - "react": "^16.8.0 || 17.x", - "react-dom": "^16.8.0 || 17.x" - } - }, "node_modules/@remix-run/router": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.8.0.tgz", @@ -892,34 +835,29 @@ "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "devOptional": true + "dev": true }, "node_modules/@types/react": { - "version": "17.0.60", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.60.tgz", - "integrity": "sha512-pCH7bqWIfzHs3D+PDs3O/COCQJka+Kcw3RnO9rFA2zalqoXg7cNjJDh6mZ7oRtY1wmY4LVwDdAbA1F7Z8tv3BQ==", - "devOptional": true, + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", "dependencies": { "@types/prop-types": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" + "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { - "version": "17.0.20", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.20.tgz", - "integrity": "sha512-4pzIjSxDueZZ90F52mU3aPoogkHIoSIDG+oQ+wQK7Cy2B9S+MvOqY0uEA/qawKz381qrEDkvpwyt8Bm31I8sbA==", + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "dev": true, - "dependencies": { - "@types/react": "^17" + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" } }, - "node_modules/@types/scheduler": { - "version": "0.16.3", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", - "devOptional": true - }, "node_modules/@vitejs/plugin-react": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-3.1.0.tgz", @@ -1039,10 +977,11 @@ "dev": true }, "node_modules/csstype": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", - "devOptional": true + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" }, "node_modules/debug": { "version": "4.3.4", @@ -1061,11 +1000,6 @@ } } }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" - }, "node_modules/electron-to-chromium": { "version": "1.4.419", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.419.tgz", @@ -1133,17 +1067,6 @@ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true }, - "node_modules/focus-lock": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/focus-lock/-/focus-lock-0.9.2.tgz", - "integrity": "sha512-YtHxjX7a0IC0ZACL5wsX8QdncXofWpGPNoVMuI/nZUrPGp6LmNI6+D5j0pPj+v8Kw5EpweA+T5yImK0rnWf7oQ==", - "dependencies": { - "tslib": "^2.0.3" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -1167,14 +1090,6 @@ "node": ">=6.9.0" } }, - "node_modules/get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "engines": { - "node": ">=6" - } - }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -1193,14 +1108,6 @@ "node": ">=4" } }, - "node_modules/invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -1292,14 +1199,6 @@ "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==", "dev": true }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -1346,73 +1245,31 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, "node_modules/react": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", - "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "loose-envify": "^1.1.0" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/react-clientside-effect": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz", - "integrity": "sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==", - "dependencies": { - "@babel/runtime": "^7.12.13" - }, - "peerDependencies": { - "react": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/react-dom": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", - "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "scheduler": "^0.20.2" - }, - "peerDependencies": { - "react": "17.0.2" - } - }, - "node_modules/react-focus-lock": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.5.2.tgz", - "integrity": "sha512-WzpdOnEqjf+/A3EH9opMZWauag7gV0BxFl+EY4ElA4qFqYsUsBLnmo2sELbN5OC30S16GAWMy16B9DLPpdJKAQ==", - "dependencies": { - "@babel/runtime": "^7.0.0", - "focus-lock": "^0.9.1", - "prop-types": "^15.6.2", - "react-clientside-effect": "^1.2.5", - "use-callback-ref": "^1.2.5", - "use-sidecar": "^1.0.5" + "scheduler": "^0.23.2" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0" + "react": "^18.3.1" } }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, "node_modules/react-refresh": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", @@ -1422,56 +1279,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-remove-scroll": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.4.3.tgz", - "integrity": "sha512-lGWYXfV6jykJwbFpsuPdexKKzp96f3RbvGapDSIdcyGvHb7/eqyn46C7/6h+rUzYar1j5mdU+XECITHXCKBk9Q==", - "dependencies": { - "react-remove-scroll-bar": "^2.1.0", - "react-style-singleton": "^2.1.0", - "tslib": "^1.0.0", - "use-callback-ref": "^1.2.3", - "use-sidecar": "^1.0.1" - }, - "engines": { - "node": ">=8.5.0" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0", - "react": "^16.8.0 || ^17.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz", - "integrity": "sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==", - "dependencies": { - "react-style-singleton": "^2.2.1", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-remove-scroll/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, "node_modules/react-router": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.15.0.tgz", @@ -1502,33 +1309,6 @@ "react-dom": ">=16.8" } }, - "node_modules/react-style-singleton": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", - "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", - "dependencies": { - "get-nonce": "^1.0.0", - "invariant": "^2.2.4", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" - }, "node_modules/rollup": { "version": "3.23.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.23.1.tgz", @@ -1546,12 +1326,12 @@ } }, "node_modules/scheduler": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", - "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "loose-envify": "^1.1.0" } }, "node_modules/semver": { @@ -1593,11 +1373,6 @@ "node": ">=4" } }, - "node_modules/tslib": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz", - "integrity": "sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==" - }, "node_modules/typescript": { "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", @@ -1641,47 +1416,6 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/use-callback-ref": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.0.tgz", - "integrity": "sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w==", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sidecar": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", - "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/vite": { "version": "4.3.9", "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz", diff --git a/examples/modal/package.json b/examples/modal/package.json index c665689c27..b6a0b9e621 100644 --- a/examples/modal/package.json +++ b/examples/modal/package.json @@ -7,16 +7,15 @@ "serve": "vite preview" }, "dependencies": { - "@reach/dialog": "0.18.0", - "react": "^17.0.2", - "react-dom": "^17.0.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", "react-router-dom": "^6.15.0" }, "devDependencies": { "@rollup/plugin-replace": "^5.0.2", "@types/node": "18.x", - "@types/react": "^17.0.59", - "@types/react-dom": "^17.0.20", + "@types/react": "^18.3.28", + "@types/react-dom": "^18.3.7", "@vitejs/plugin-react": "^3.0.1", "typescript": "^4.9.5", "vite": "^4.0.4" diff --git a/examples/modal/src/App.tsx b/examples/modal/src/App.tsx index 50aece0aa0..2e092a493b 100644 --- a/examples/modal/src/App.tsx +++ b/examples/modal/src/App.tsx @@ -1,4 +1,5 @@ import * as React from "react"; +import { createPortal } from "react-dom"; import { Routes, Route, @@ -8,8 +9,6 @@ import { useNavigate, useParams, } from "react-router-dom"; -import { Dialog } from "@reach/dialog"; -import "@reach/dialog/styles.css"; import { IMAGES, getImageById } from "./images"; @@ -228,3 +227,124 @@ function NoMatch() { ); } + +type DialogProps = { + children: React.ReactNode; + onDismiss: () => void; + "aria-label"?: string; + "aria-labelledby"?: string; + initialFocusRef?: React.RefObject; +}; + +function Dialog({ + children, + onDismiss, + "aria-label": ariaLabel, + "aria-labelledby": ariaLabelledby, + initialFocusRef, +}: DialogProps) { + let overlayRef = React.useRef(null); + let contentRef = React.useRef(null); + let previouslyFocusedRef = React.useRef(null); + + React.useEffect(() => { + previouslyFocusedRef.current = document.activeElement as HTMLElement | null; + + let focusTarget = initialFocusRef?.current ?? contentRef.current; + if (focusTarget) { + focusTarget.focus(); + } + + function onKeyDown(event: KeyboardEvent) { + if (event.key === "Escape") { + event.stopPropagation(); + onDismiss(); + return; + } + + if (event.key === "Tab") { + let container = contentRef.current; + if (!container) return; + + let focusable = getFocusableElements(container); + if (focusable.length === 0) { + event.preventDefault(); + container.focus(); + return; + } + + let activeElement = document.activeElement as HTMLElement | null; + let currentIndex = focusable.indexOf(activeElement ?? focusable[0]); + + let nextIndex = currentIndex; + if (event.shiftKey) { + nextIndex = + currentIndex <= 0 ? focusable.length - 1 : currentIndex - 1; + } else { + nextIndex = + currentIndex === focusable.length - 1 ? 0 : currentIndex + 1; + } + + event.preventDefault(); + focusable[nextIndex].focus(); + } + } + + document.addEventListener("keydown", onKeyDown); + + return () => { + document.removeEventListener("keydown", onKeyDown); + previouslyFocusedRef.current?.focus(); + }; + }, [initialFocusRef, onDismiss]); + + return createPortal( +
{ + if (event.target === event.currentTarget) { + onDismiss(); + } + }} + style={{ + position: "fixed", + inset: 0, + background: "rgba(0, 0, 0, 0.55)", + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "24px", + zIndex: 1000, + }} + > +
+ {children} +
+
, + document.body, + ); +} + +function getFocusableElements(container: HTMLElement) { + return Array.from( + container.querySelectorAll( + 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, [tabindex]:not([tabindex="-1"]), [contenteditable="true"]', + ), + ).filter((element) => !element.hasAttribute("disabled")); +} From 5745c4ba3f7ad956cbea0a2b027edc2bd8e84360 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 19 Feb 2026 11:10:00 -0500 Subject: [PATCH 04/33] Add CLAUDE.md file linking to AGENTS.md file --- .gitignore | 2 ++ CLAUDE.md | 1 + 2 files changed, 3 insertions(+) create mode 100644 CLAUDE.md diff --git a/.gitignore b/.gitignore index 9209f20a17..eed2c1bf39 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,5 @@ worker-configuration.d.ts # v7 reference docs /public + +.claude/settings.local.json \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..4bb4db343e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +See [./AGENTS.md] From d8ffb2dbda9ef975287b902c4c7784a1e8abd8be Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Mon, 23 Feb 2026 16:55:55 +0000 Subject: [PATCH 05/33] 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 | 33 ++++++------------- 8 files changed, 17 insertions(+), 53 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 ac04993c40..c8c119e4ca 100644 --- a/packages/react-router-architect/CHANGELOG.md +++ b/packages/react-router-architect/CHANGELOG.md @@ -103,7 +103,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 @@ -327,7 +326,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` @@ -336,7 +334,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 4190774f96..00809f3abf 100644 --- a/packages/react-router-cloudflare/CHANGELOG.md +++ b/packages/react-router-cloudflare/CHANGELOG.md @@ -91,7 +91,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 @@ -290,7 +289,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` @@ -299,7 +297,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 c0ee22e6cd..aad0e8732d 100644 --- a/packages/react-router-dev/CHANGELOG.md +++ b/packages/react-router-dev/CHANGELOG.md @@ -37,25 +37,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. @@ -313,7 +313,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 @@ -1056,7 +1055,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 @@ -1071,7 +1069,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) @@ -1271,7 +1268,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` @@ -1608,7 +1604,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 @@ -2012,7 +2007,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) @@ -2110,7 +2104,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 @@ -2381,7 +2374,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 f394ddf339..e3a04f2727 100644 --- a/packages/react-router-express/CHANGELOG.md +++ b/packages/react-router-express/CHANGELOG.md @@ -103,7 +103,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 d731580ca7..46f78a092c 100644 --- a/packages/react-router-node/CHANGELOG.md +++ b/packages/react-router-node/CHANGELOG.md @@ -92,7 +92,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 @@ -292,7 +291,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` @@ -301,7 +299,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` @@ -709,12 +706,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 f956dc5e12..ba58db9041 100644 --- a/packages/react-router-serve/CHANGELOG.md +++ b/packages/react-router-serve/CHANGELOG.md @@ -728,12 +728,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 98ed5b8338..fe148cf413 100644 --- a/packages/react-router/CHANGELOG.md +++ b/packages/react-router/CHANGELOG.md @@ -12,9 +12,9 @@ - Fix matchPath optional params matching without a "/" separator. ([#14689](https://github.com/remix-run/react-router/pull/14689)) - matchPath("/users/:id?", "/usersblah") now returns null. - - matchPath("/test\_route/:part?", "/test\_route\_more") now returns null. + - matchPath("/test_route/:part?", "/test_route_more") now returns null. -- add RSC unstable\_getRequest ([#14758](https://github.com/remix-run/react-router/pull/14758)) +- add RSC unstable_getRequest ([#14758](https://github.com/remix-run/react-router/pull/14758)) - Fix `HydrateFallback` rendering during initial lazy route discovery with matching splat route ([#14740](https://github.com/remix-run/react-router/pull/14740)) @@ -60,7 +60,6 @@ ``` Notes: - - The masked location, if present, will be available on `useLocation().unstable_mask` so you can detect whether you are currently masked or not. - Masked URLs only work for SPA use cases, and will be removed from `history.state` during SSR. - This provides a first-class API to mask URLs in Data Mode to achieve the same behavior you could do in Declarative Mode via [manual `backgroundLocation` management](https://github.com/remix-run/react-router/tree/main/examples/modal). @@ -106,25 +105,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. @@ -151,14 +150,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 })` @@ -181,7 +178,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: @@ -310,7 +306,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`: `` @@ -472,7 +467,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 @@ -499,7 +493,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)) @@ -548,7 +542,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. @@ -582,7 +575,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` @@ -1228,7 +1220,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 - `` @@ -1416,7 +1407,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` @@ -1425,7 +1415,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` @@ -1581,7 +1570,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 @@ -1596,7 +1584,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 e0c207db147755b18bf4de4c4d50f8d92b05d5eb Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Fri, 27 Feb 2026 09:44:24 -0500 Subject: [PATCH 06/33] Fix clientLoader hydrate behavior when ancestor route is hydrating a loader (#14835) --- .changeset/cold-schools-relate.md | 5 ++ .../__tests__/dom/partial-hydration-test.tsx | 81 ++++++++++++++++--- packages/react-router/lib/router/router.ts | 7 +- 3 files changed, 82 insertions(+), 11 deletions(-) create mode 100644 .changeset/cold-schools-relate.md diff --git a/.changeset/cold-schools-relate.md b/.changeset/cold-schools-relate.md new file mode 100644 index 0000000000..d45d6f8fb1 --- /dev/null +++ b/.changeset/cold-schools-relate.md @@ -0,0 +1,5 @@ +--- +"react-router": patch +--- + +Fix clientLoader.hydrate when an ancestor route is also hydrating a clientLoader diff --git a/packages/react-router/__tests__/dom/partial-hydration-test.tsx b/packages/react-router/__tests__/dom/partial-hydration-test.tsx index 9b61ac66ba..6ec3501380 100644 --- a/packages/react-router/__tests__/dom/partial-hydration-test.tsx +++ b/packages/react-router/__tests__/dom/partial-hydration-test.tsx @@ -66,9 +66,6 @@ describe("Partial Hydration Behavior", () => { }, ], { - future: { - v7_partialHydration: true, - }, patchRoutesOnNavigation({ path, patch }) { if (path === "/parent/child") { patch("parent", [ @@ -155,9 +152,6 @@ describe("Partial Hydration Behavior", () => { }, ], { - future: { - v7_partialHydration: true, - }, patchRoutesOnNavigation({ path, patch }) { if (path === "/parent/child") { patch("parent", [ @@ -248,9 +242,6 @@ describe("Partial Hydration Behavior", () => { }, ], { - future: { - v7_partialHydration: true, - }, async patchRoutesOnNavigation({ path, patch }) { await patchDfd.promise; if (path === "/parent/child") { @@ -853,4 +844,76 @@ function testPartialHydration( expect(rootSpy).toHaveBeenCalledTimes(1); expect(indexSpy).not.toHaveBeenCalled(); }); + + it("renders child fallback when ancestor route has hydration data and a hydrating loader", async () => { + let rootDfd = createDeferred(); + let rootLoader: LoaderFunction = () => rootDfd.promise; + rootLoader.hydrate = true; + let indexDfd = createDeferred(); + let indexLoader: LoaderFunction = () => indexDfd.promise; + indexLoader.hydrate = true; + let router = createTestRouter( + [ + { + id: "root", + path: "/", + loader: rootLoader, + Component() { + let data = useLoaderData() as string; + return ( + <> +

{`Home - ${data}`}

+ + + ); + }, + children: [ + { + id: "index", + index: true, + loader: indexLoader, + HydrateFallback: () =>

Index Loading...

, + Component() { + let data = useLoaderData() as string; + return

{`Index - ${data}`}

; + }, + }, + ], + }, + ], + { + hydrationData: { + loaderData: { + root: "HYDRATED ROOT", + }, + }, + }, + ); + let { container } = render(); + + expect(getHtml(container)).toMatchInlineSnapshot(` + "
+

+ Home - HYDRATED ROOT +

+

+ Index Loading... +

+
" + `); + + rootDfd.resolve("ROOT UPDATED"); + indexDfd.resolve("INDEX UPDATED"); + await waitFor(() => screen.getByText(/INDEX UPDATED/)); + expect(getHtml(container)).toMatchInlineSnapshot(` + "
+

+ Home - ROOT UPDATED +

+

+ Index - INDEX UPDATED +

+
" + `); + }); } diff --git a/packages/react-router/lib/router/router.ts b/packages/react-router/lib/router/router.ts index b2348c0c6b..42479c0940 100644 --- a/packages/react-router/lib/router/router.ts +++ b/packages/react-router/lib/router/router.ts @@ -1024,11 +1024,14 @@ export function createRouter(init: RouterInit): Router { } // Toggle renderFallback based on per-route values + // Using a `.forEach` is important instead of something like an `.every` + // here because we need to evaluate renderFallback for all matches renderFallback = false; - initialized = relevantMatches.every((m) => { + initialized = true; + relevantMatches.forEach((m) => { let status = getRouteHydrationStatus(m.route, loaderData, errors); renderFallback = renderFallback || status.renderFallback; - return !status.shouldLoad; + initialized = initialized && !status.shouldLoad; }); } } From fdcdcef106cc9b66ff2f4652aeade5bb22d147f9 Mon Sep 17 00:00:00 2001 From: Roli Bosch Date: Fri, 27 Feb 2026 08:58:35 -0800 Subject: [PATCH 07/33] chore: replace chalk with picocolors (#14837) --- .changeset/sweet-houses-kick.md | 5 ++ contributors.yml | 1 + package.json | 2 +- packages/create-react-router/package.json | 2 +- packages/create-react-router/utils.ts | 103 ++++++++++++---------- pnpm-lock.yaml | 12 +-- scripts/playground.js | 6 +- scripts/version.js | 10 +-- 8 files changed, 79 insertions(+), 62 deletions(-) create mode 100644 .changeset/sweet-houses-kick.md diff --git a/.changeset/sweet-houses-kick.md b/.changeset/sweet-houses-kick.md new file mode 100644 index 0000000000..daeca90ffd --- /dev/null +++ b/.changeset/sweet-houses-kick.md @@ -0,0 +1,5 @@ +--- +"create-react-router": patch +--- + +chore: replace chalk with picocolors diff --git a/contributors.yml b/contributors.yml index 31584cfcfc..e26d62fcde 100644 --- a/contributors.yml +++ b/contributors.yml @@ -356,6 +356,7 @@ - robbtraister - RobHannay - robinvdvleuten +- roli-lpci - rossipedia - rtmann - rtzll diff --git a/package.json b/package.json index 568a7299e6..581823b18f 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "@typescript-eslint/parser": "^7.5.0", "babel-jest": "^29.7.0", "babel-plugin-dev-expression": "^0.2.3", - "chalk": "^4.1.2", + "picocolors": "^1.1.1", "dox": "^1.0.0", "eslint": "^8.57.0", "eslint-config-react-app": "^7.0.1", diff --git a/packages/create-react-router/package.json b/packages/create-react-router/package.json index a2df126d02..dd0aaafaf5 100644 --- a/packages/create-react-router/package.json +++ b/packages/create-react-router/package.json @@ -39,7 +39,7 @@ "dependencies": { "@remix-run/web-fetch": "^4.4.2", "arg": "^5.0.1", - "chalk": "^4.1.2", + "picocolors": "^1.1.1", "execa": "5.1.1", "gunzip-maybe": "^1.4.2", "log-update": "^5.0.1", diff --git a/packages/create-react-router/utils.ts b/packages/create-react-router/utils.ts index 0effcdb084..6c30c56c85 100644 --- a/packages/create-react-router/utils.ts +++ b/packages/create-react-router/utils.ts @@ -5,56 +5,69 @@ import process from "node:process"; import os from "node:os"; import { type Key as ActionKey } from "node:readline"; import { erase, cursor } from "sisteransi"; -import chalk from "chalk"; +import pc from "picocolors"; // https://no-color.org/ -const SUPPORTS_COLOR = chalk.supportsColor && !process.env.NO_COLOR; +// picocolors natively respects NO_COLOR and FORCE_COLOR env vars +const SUPPORTS_COLOR = pc.isColorSupported; export const color = { supportsColor: SUPPORTS_COLOR, - heading: safeColor(chalk.bold), - arg: safeColor(chalk.yellowBright), - error: safeColor(chalk.red), - warning: safeColor(chalk.yellow), - hint: safeColor(chalk.blue), - bold: safeColor(chalk.bold), - black: safeColor(chalk.black), - white: safeColor(chalk.white), - blue: safeColor(chalk.blue), - cyan: safeColor(chalk.cyan), - red: safeColor(chalk.red), - yellow: safeColor(chalk.yellow), - green: safeColor(chalk.green), - blackBright: safeColor(chalk.blackBright), - whiteBright: safeColor(chalk.whiteBright), - blueBright: safeColor(chalk.blueBright), - cyanBright: safeColor(chalk.cyanBright), - redBright: safeColor(chalk.redBright), - yellowBright: safeColor(chalk.yellowBright), - greenBright: safeColor(chalk.greenBright), - bgBlack: safeColor(chalk.bgBlack), - bgWhite: safeColor(chalk.bgWhite), - bgBlue: safeColor(chalk.bgBlue), - bgCyan: safeColor(chalk.bgCyan), - bgRed: safeColor(chalk.bgRed), - bgYellow: safeColor(chalk.bgYellow), - bgGreen: safeColor(chalk.bgGreen), - bgBlackBright: safeColor(chalk.bgBlackBright), - bgWhiteBright: safeColor(chalk.bgWhiteBright), - bgBlueBright: safeColor(chalk.bgBlueBright), - bgCyanBright: safeColor(chalk.bgCyanBright), - bgRedBright: safeColor(chalk.bgRedBright), - bgYellowBright: safeColor(chalk.bgYellowBright), - bgGreenBright: safeColor(chalk.bgGreenBright), - gray: safeColor(chalk.gray), - dim: safeColor(chalk.dim), - reset: safeColor(chalk.reset), - inverse: safeColor(chalk.inverse), - hex: (color: string) => safeColor(chalk.hex(color)), - underline: chalk.underline, + heading: safeColor(pc.bold), + arg: safeColor(pc.yellowBright), + error: safeColor(pc.red), + warning: safeColor(pc.yellow), + hint: safeColor(pc.blue), + bold: safeColor(pc.bold), + black: safeColor(pc.black), + white: safeColor(pc.white), + blue: safeColor(pc.blue), + cyan: safeColor(pc.cyan), + red: safeColor(pc.red), + yellow: safeColor(pc.yellow), + green: safeColor(pc.green), + blackBright: safeColor(pc.blackBright), + whiteBright: safeColor(pc.whiteBright), + blueBright: safeColor(pc.blueBright), + cyanBright: safeColor(pc.cyanBright), + redBright: safeColor(pc.redBright), + yellowBright: safeColor(pc.yellowBright), + greenBright: safeColor(pc.greenBright), + bgBlack: safeColor(pc.bgBlack), + bgWhite: safeColor(pc.bgWhite), + bgBlue: safeColor(pc.bgBlue), + bgCyan: safeColor(pc.bgCyan), + bgRed: safeColor(pc.bgRed), + bgYellow: safeColor(pc.bgYellow), + bgGreen: safeColor(pc.bgGreen), + bgBlackBright: safeColor(pc.bgBlackBright), + bgWhiteBright: safeColor(pc.bgWhiteBright), + bgBlueBright: safeColor(pc.bgBlueBright), + bgCyanBright: safeColor(pc.bgCyanBright), + bgRedBright: safeColor(pc.bgRedBright), + bgYellowBright: safeColor(pc.bgYellowBright), + bgGreenBright: safeColor(pc.bgGreenBright), + gray: safeColor(pc.gray), + dim: safeColor(pc.dim), + reset: safeColor(pc.reset), + inverse: safeColor(pc.inverse), + hex: (hex: string) => safeColor(hexColor(hex)), + underline: pc.underline, }; -function safeColor(style: chalk.Chalk) { +/** + * Converts a hex color string to an ANSI true-color (24-bit) formatter. + * Used by the loading indicator gradient animation. + */ +function hexColor(hex: string): (input: string) => string { + let h = hex.replace("#", ""); + let r = parseInt(h.substring(0, 2), 16); + let g = parseInt(h.substring(2, 4), 16); + let b = parseInt(h.substring(4, 6), 16); + return (input: string) => `\x1b[38;2;${r};${g};${b}m${input}\x1b[39m`; +} + +function safeColor(style: (input: string) => string) { return SUPPORTS_COLOR ? style : identity; } @@ -93,8 +106,8 @@ export function logError(message: string) { function logBullet( logger: typeof log | typeof logError, - colorizePrefix: (v: V) => V, - colorizeText: (v: V) => V, + colorizePrefix: (v: string) => string, + colorizeText: (v: string) => string, symbol: string, prefix: string, text?: string | string[], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 098fc66e24..ec15823e78 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -115,9 +115,6 @@ importers: babel-plugin-dev-expression: specifier: ^0.2.3 version: 0.2.3(@babel/core@7.27.7) - chalk: - specifier: ^4.1.2 - version: 4.1.2 dox: specifier: ^1.0.0 version: 1.0.0 @@ -160,6 +157,9 @@ importers: jsonfile: specifier: ^6.1.0 version: 6.1.0 + picocolors: + specifier: ^1.1.1 + version: 1.1.1 prettier: specifier: ^3.6.2 version: 3.6.2 @@ -828,9 +828,6 @@ importers: arg: specifier: ^5.0.1 version: 5.0.2 - chalk: - specifier: ^4.1.2 - version: 4.1.2 execa: specifier: 5.1.1 version: 5.1.1 @@ -840,6 +837,9 @@ importers: log-update: specifier: ^5.0.1 version: 5.0.1 + picocolors: + specifier: ^1.1.1 + version: 1.1.1 proxy-agent: specifier: ^6.3.0 version: 6.4.0 diff --git a/scripts/playground.js b/scripts/playground.js index b987ecaef0..20ea5648b0 100644 --- a/scripts/playground.js +++ b/scripts/playground.js @@ -4,7 +4,7 @@ let { existsSync, readdirSync } = require("node:fs"); let { cp } = require("node:fs/promises"); let path = require("node:path"); let prompts = require("prompts"); -let chalk = require("chalk"); +let pc = require("picocolors"); copyPlayground(); @@ -40,8 +40,8 @@ async function copyPlayground() { console.log( [ "", - chalk.green`Created local copy of "${templateName}"`, - chalk.green`To start playground, run:`, + pc.green(`Created local copy of "${templateName}"`), + pc.green(`To start playground, run:`), "", `cd ${relativeDestDir}`, "pnpm dev", diff --git a/scripts/version.js b/scripts/version.js index 71d96c6c16..6fc21a9a94 100644 --- a/scripts/version.js +++ b/scripts/version.js @@ -1,6 +1,6 @@ const fs = require("node:fs"); const { execSync } = require("child_process"); -const chalk = require("chalk"); +const pc = require("picocolors"); const semver = require("semver"); const { @@ -40,20 +40,18 @@ async function run() { packageName = pkg.name; pkg.version = version; }); - console.log( - chalk.green(` Updated ${packageName} to version ${version}`), - ); + console.log(pc.green(` Updated ${packageName} to version ${version}`)); } // 3. Commit and tag if (!skipGit) { execSync(`git commit --all --message="Version ${version}"`); execSync(`git tag -a -m "Version ${version}" v${version}`); - console.log(chalk.green(` Committed and tagged version ${version}`)); + console.log(pc.green(` Committed and tagged version ${version}`)); } } catch (error) { console.log(); - console.error(chalk.red(` ${error.message}`)); + console.error(pc.red(` ${error.message}`)); console.log(); return 1; } From 5e12c46dc7e6a7e8305c4261e74caabc8ef66ddc Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Fri, 27 Feb 2026 16:59:17 +0000 Subject: [PATCH 08/33] chore: deduplicate `pnpm-lock.yaml` --- pnpm-lock.yaml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ec15823e78..bc26854688 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -144,7 +144,7 @@ importers: version: 7.34.1(eslint@8.57.0) eslint-plugin-react-hooks: specifier: next - version: 7.1.0-canary-24d8716e-20260123(eslint@8.57.0) + version: 7.1.0-canary-e0cc7202-20260227(eslint@8.57.0) fast-glob: specifier: 3.2.11 version: 3.2.11 @@ -4799,6 +4799,7 @@ packages: basic-ftp@5.0.5: resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==} engines: {node: '>=10.0.0'} + deprecated: Security vulnerability fixed in 5.2.0, please upgrade better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} @@ -5615,11 +5616,11 @@ 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-24d8716e-20260123: - resolution: {integrity: sha512-Ku4UmX2ZuQtAUHu+9IqLcyKc0uqG8nAMXgpD0ycZUZaZ4nOneQP/qD0riPM6M/PUkDNF5xNLNc5Sde1mDkR9ig==} + eslint-plugin-react-hooks@7.1.0-canary-e0cc7202-20260227: + resolution: {integrity: sha512-Kg4EiP6olCKf9zrf3TGaMfyQfUOADsQDFa6q3Cfv+Fr47dQhOtbq6FkkyNZJEb+yz8kGrJJmIPKb+0Q2f+FrZw==} 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 + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 eslint-plugin-react@7.34.1: resolution: {integrity: sha512-N97CxlouPT1AHt8Jn0mhhN2RrADlUAsk1/atcT2KyA/l9Q/E6ll7OIGwNumFmWfZ9skV3XXccYS19h80rHtgkw==} @@ -5986,16 +5987,17 @@ packages: glob@10.3.10: resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} engines: {node: '>=16 || 14 >=14.17'} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@8.0.3: resolution: {integrity: sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==} engines: {node: '>=12'} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} @@ -7787,6 +7789,7 @@ packages: react-server-dom-webpack@19.2.3: resolution: {integrity: sha512-ifo7aqqdNJyV6U2zuvvWX4rRQ51pbleuUFNG7ZYhIuSuWZzQPbfmYv11GNsyJm/3uGNbt8buJ9wmoISn/uOAfw==} engines: {node: '>=0.10.0'} + deprecated: High Security Vulnerability in React Server Components peerDependencies: react: ^19.2.3 react-dom: ^19.2.3 @@ -13635,7 +13638,7 @@ snapshots: dependencies: eslint: 8.57.0 - eslint-plugin-react-hooks@7.1.0-canary-24d8716e-20260123(eslint@8.57.0): + eslint-plugin-react-hooks@7.1.0-canary-e0cc7202-20260227(eslint@8.57.0): dependencies: '@babel/core': 7.27.7 '@babel/parser': 7.27.7 From d827bb9285612a5fb472ab1b88b255b975fa01ee Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Fri, 27 Feb 2026 12:00:42 -0500 Subject: [PATCH 09/33] Add support for `future.unstable_passThroughRequests` (#14775) --- .changeset/passthrough-reqeusts.md | 37 +++ .changeset/unstable-url.md | 10 + CLAUDE.md | 23 +- docs/upgrading/future.md | 72 +++++- integration/passthrough-requests-test.ts | 231 ++++++++++++++++++ integration/vite-presets-test.ts | 1 + packages/react-router-dev/config/config.ts | 3 + .../__tests__/router/fetchers-test.ts | 38 ++- .../__tests__/router/router-test.ts | 9 +- .../react-router/__tests__/router/ssr-test.ts | 50 +++- .../__tests__/router/submission-test.ts | 6 + .../lib/dom-export/hydrated-router.tsx | 3 +- packages/react-router/lib/dom/server.tsx | 1 + packages/react-router/lib/dom/ssr/entry.ts | 1 + .../lib/dom/ssr/routes-test-stub.tsx | 2 + packages/react-router/lib/dom/ssr/routes.tsx | 18 +- packages/react-router/lib/router/router.ts | 123 ++++++++-- packages/react-router/lib/router/utils.ts | 9 + packages/react-router/lib/rsc/browser.tsx | 1 + packages/react-router/lib/rsc/server.rsc.ts | 3 + packages/react-router/lib/rsc/server.ssr.tsx | 1 + .../react-router/lib/server-runtime/data.ts | 12 +- .../react-router/lib/server-runtime/routes.ts | 8 +- .../react-router/lib/server-runtime/server.ts | 70 +++--- .../lib/server-runtime/single-fetch.ts | 31 ++- .../react-router/lib/server-runtime/urls.ts | 52 ++++ packages/react-router/lib/types/route-data.ts | 19 ++ 27 files changed, 740 insertions(+), 94 deletions(-) create mode 100644 .changeset/passthrough-reqeusts.md create mode 100644 .changeset/unstable-url.md create mode 100644 integration/passthrough-requests-test.ts create mode 100644 packages/react-router/lib/server-runtime/urls.ts diff --git a/.changeset/passthrough-reqeusts.md b/.changeset/passthrough-reqeusts.md new file mode 100644 index 0000000000..a466f7ec0e --- /dev/null +++ b/.changeset/passthrough-reqeusts.md @@ -0,0 +1,37 @@ +--- +"@react-router/dev": patch +"react-router": patch +--- + +Add `future.unstable_passThroughRequests` flag + +By default, React Router normalizes the `request.url` passed to your `loader`, `action`, and `middleware` functions by removing React Router's internal implementation details (`.data` suffixes, `index` + `_routes` query params). + +Enabling this flag removes that normalization and passes the raw HTTP `request` instance to your handlers. This provides a few benefits: + +- Reduces server-side overhead by eliminating multiple `new Request()` calls on the critical path +- Allows you to distinguish document from data requests in your handlers base don the presence of a `.data` suffix (useful for observability purposes) + +If you were previously relying on the normalization of `request.url`, you can switch to use the new sibling `unstable_url` parameter which contains a `URL` instance representing the normalized location: + +```tsx +// ❌ Before: you could assume there was no `.data` suffix in `request.url` +export async function loader({ request }: Route.LoaderArgs) { + let url = new URL(request.url); + if (url.pathname === "/path") { + // This check will fail with the flag enabled because the `.data` suffix will + // exist on data requests + } +} + +// ✅ After: use `unstable_url` for normalized routing logic and `request.url` +// for raw routing logic +export async function loader({ request, unstable_url }: Route.LoaderArgs) { + if (unstable_url.pathname === "/path") { + // This will always have the `.data` suffix stripped + } + + // And now you can distinguish between document versus data requests + let isDataRequest = new URL(request.url).pathname.endsWith(".data"); +} +``` diff --git a/.changeset/unstable-url.md b/.changeset/unstable-url.md new file mode 100644 index 0000000000..77b7d1ad12 --- /dev/null +++ b/.changeset/unstable-url.md @@ -0,0 +1,10 @@ +--- +"@react-router/dev": patch +"react-router": patch +--- + +Add a new `unstable_url: URL` parameter to route handler methods (`loader`, `action`, `middleware`, etc.) representing the normalized URL the application is navigating to or fetching, with React Router implementation details removed (`.data`suffix, `index`/`_routes` query params) + +This is being added alongside the new `future.unstable_passthroughRequests` future flag so that users still have a way to access the normalized URL when that flag is enabled and non-normalized `request`'s are being passed to your handlers. When adopting this flag, you will only need to start leveraging this new parameter if you are relying on the normalization of `request.url` in your application code. + +If you don't have the flag enabled, then `unstable_url` will match `request.url`. diff --git a/CLAUDE.md b/CLAUDE.md index 4bb4db343e..50ee466624 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1 +1,22 @@ -See [./AGENTS.md] +# React Router Project Instructions + +## Session Start + +**REQUIRED**: Read [./AGENTS.md](./AGENTS.md) at the start of every session. It contains: + +- Project architecture and key files +- React Router modes (Declarative, Data, Framework, RSC) +- Build/test commands (Jest unit tests, Playwright integration tests with `--project chromium`) +- Testing patterns and conventions +- Documentation guidelines + +## During Work + +**Always consult AGENTS.md** when you need to: + +- Run tests or builds +- Understand which mode(s) a feature applies to +- Find key file locations +- Understand testing patterns + +Do not guess at commands - reference AGENTS.md for the correct syntax. diff --git a/docs/upgrading/future.md b/docs/upgrading/future.md index afdbbf9e61..71c265bf83 100644 --- a/docs/upgrading/future.md +++ b/docs/upgrading/future.md @@ -5,7 +5,7 @@ order: 1 # Future Flags and Deprecations -This guide walks you through the process of adopting future flags in your React Router app. By following this strategy, you will be able to upgrade to the next major version of React Router with minimal changes. To read more about future flags see [API Development Strategy](../community/api-development-strategy). +This guide walks you through the process of adopting future flags in your React Router app. By following this strategy, you will be able to upgrade to the next major version of React Router with minimal changes. To read more about future flags see [API Development Strategy][api-development-strategy]. We highly recommend you make a commit after each step and ship it instead of doing everything all at once. Most flags can be adopted in any order, with exceptions noted below. @@ -104,5 +104,75 @@ export default { No code changes are required unless you have custom Vite configuration that needs to be updated for the [Environment API][vite-environment]. Most users won't need to make any changes. +## Unstable Future Flags (Optional) + +We document some [unstable] flags here as a reference for folks contributing to the project via beta testing, but they are not generally recommended for production use and may having breaking changes patch/minor releases - adopt with caution! + +### future.unstable_passThroughRequests + +[MODES: framework] + +
+
+ +**Background** + +By default, React Router normalizes the `request.url` passed to your `loader`, `action`, and `middleware` functions by removing React Router's internal implementation details. Specifically, it removes `.data` suffixes and internal search parameters like `?index` and `?_routes`. + +This flag eliminates that normalization and passes the raw HTTP `request` instance to your handlers. This provides a few benefits: + +- Reduces server-side overhead by eliminating multiple `new Request()` calls on the critical path +- Allows you to distinguish document from data requests in your handlers base don the presence of a `.data` suffix (useful for [observability] purposes) + +If you were previously relying on the normalization of `request.url`, you can switch to use the new sibling `unstable_url` parameter which contains a `URL` instance representing the normalized location. + +👉 **Enable the Flag** + +```ts filename=react-router.config.ts +import type { Config } from "@react-router/dev/config"; + +export default { + future: { + unstable_passThroughRequests: true, + }, +} satisfies Config; +``` + +**Update your Code** + +If your code relies on inspecting the request URL, you should review it for any assumptions about the URL format: + +```tsx +// ❌ Before: assuming no `.data` suffix in `request.url` pathname +export async function loader({ + request, +}: Route.LoaderArgs) { + let url = new URL(request.url); + if (url.pathname === "/path") { + // This check might now behave differently because the request pathname will + // contain the `.data` suffix on data requests + } +} + +// ✅ After: use `unstable_url` for normalized routing logic and `request.url` +// for raw routing logic +export async function loader({ + request, + unstable_url, +}: Route.LoaderArgs) { + if (unstable_url.pathname === "/path") { + // This will always have the `.data` suffix stripped + } + + // And now you can distinguish between document versus data requests + let isDataRequest = new URL( + request.url, + ).pathname.endsWith(".data"); +} +``` + +[api-development-strategy]: ../community/api-development-strategy +[unstable]: ../community/api-development-strategy#unstable-flags +[observability]: ../how-to/instrumentation [Response]: https://developer.mozilla.org/en-US/docs/Web/API/Response [vite-environment]: https://vite.dev/guide/api-environment diff --git a/integration/passthrough-requests-test.ts b/integration/passthrough-requests-test.ts new file mode 100644 index 0000000000..e9b1e19ba2 --- /dev/null +++ b/integration/passthrough-requests-test.ts @@ -0,0 +1,231 @@ +import { test, expect } from "@playwright/test"; +import { + type AppFixture, + createAppFixture, + createFixture, + js, +} from "./helpers/create-fixture.js"; +import { PlaywrightFixture } from "./helpers/playwright-fixture.js"; +import { reactRouterConfig } from "./helpers/vite.js"; + +const files = { + "app/root.tsx": js` + import { Links, Meta, Outlet, Scripts } from "react-router"; + + export function loader() { + return "ROOT"; + } + + export default function Root() { + return ( + + + + + + + + + + + ); + } + + export function shouldRevalidate({ defaultShouldRevalidate }) { + return defaultShouldRevalidate; + } + `, + "app/routes/_index.tsx": js` + import { Form, Link } from "react-router"; + + export function loader({ request, unstable_url }) { + let url = new URL(request.url); + return { + url: url.pathname + url.search, + path: unstable_url.pathname + unstable_url.search + }; + } + + export function action({ request, unstable_url }) { + let url = new URL(request.url); + return { + url: url.pathname + url.search, + path: unstable_url.pathname + unstable_url.search + }; + } + + export default function Component({ loaderData, actionData }) { + return ( + <> + Add param + + + + + Go to new page + +

{loaderData.url}

+

{loaderData.path}

+ {actionData ? + <> +

{actionData.url}

+

{actionData.path}

+ : + null} + + ) + } + `, + "app/routes/page.tsx": js` + export function loader({ request, unstable_url }) { + let url = new URL(request.url); + return { + url: url.pathname + url.search, + path: unstable_url.pathname + unstable_url.search + }; + } + + export default function Component({ loaderData }) { + return ( + <> +

{loaderData.url}

+

{loaderData.path}

+ + ) + } + `, +}; + +test.describe("pass through requests", () => { + test("sends proper arguments to loaders when future.unstable_passThroughRequests is disabled", async ({ + page, + }) => { + let fixture = await createFixture({ + files: { + "react-router.config.ts": reactRouterConfig({ + future: { + unstable_passThroughRequests: false, + }, + }), + ...files, + }, + }); + let appFixture = await createAppFixture(fixture); + let app = new PlaywrightFixture(appFixture, page); + + let requests: string[] = []; + page.on("request", (req) => { + let url = new URL(req.url()); + if (url.pathname.endsWith(".data")) { + requests.push(url.pathname + url.search); + } + }); + + // Document load + await app.goto("/"); + expect(await page.locator("[data-loader-url]").textContent()).toBe("/"); + expect(await page.locator("[data-loader-path]").textContent()).toBe("/"); + + // Client-side navigation with query params + await app.clickLink("/?a=1"); + expect(await page.locator("[data-loader-url]").textContent()).toBe("/?a=1"); + expect(await page.locator("[data-loader-path]").textContent()).toBe( + "/?a=1", + ); + expect(requests).toEqual(["/_root.data?a=1"]); + requests = []; + + // Client-side form submission with query params + await app.clickElement('button[type="submit"]'); + expect(await page.locator("[data-action-url]").textContent()).toBe("/?a=1"); + expect(await page.locator("[data-action-path]").textContent()).toBe( + "/?a=1", + ); + expect(await page.locator("[data-loader-url]").textContent()).toBe("/?a=1"); + expect(await page.locator("[data-loader-path]").textContent()).toBe( + "/?a=1", + ); + expect(requests).toEqual(["/_root.data?index&a=1", "/_root.data?a=1"]); + requests = []; + + // Navigate to new page + await app.clickLink("/page?b=2"); + expect(await page.locator("[data-loader-url]").textContent()).toBe( + "/page?b=2", + ); + expect(await page.locator("[data-loader-path]").textContent()).toBe( + "/page?b=2", + ); + expect(requests).toEqual(["/page.data?b=2&_routes=routes%2Fpage"]); + requests = []; + }); + + test("sends proper arguments to loaders when future.unstable_passThroughRequests is enabled", async ({ + page, + }) => { + let fixture = await createFixture({ + files: { + "react-router.config.ts": reactRouterConfig({ + future: { + unstable_passThroughRequests: true, + }, + }), + ...files, + }, + }); + let appFixture = await createAppFixture(fixture); + let app = new PlaywrightFixture(appFixture, page); + + let requests: string[] = []; + page.on("request", (req) => { + let url = new URL(req.url()); + if (url.pathname.endsWith(".data")) { + requests.push(url.pathname + url.search); + } + }); + + // Document load + await app.goto("/"); + expect(await page.locator("[data-loader-url]").textContent()).toBe("/"); + expect(await page.locator("[data-loader-path]").textContent()).toBe("/"); + + // Client-side navigation with query params + await app.clickLink("/?a=1"); + expect(await page.locator("[data-loader-url]").textContent()).toBe( + "/_root.data?a=1", + ); + expect(await page.locator("[data-loader-path]").textContent()).toBe( + "/?a=1", + ); + expect(requests).toEqual(["/_root.data?a=1"]); + requests = []; + + // Client-side form submission with query params + await app.clickElement('button[type="submit"]'); + expect(await page.locator("[data-action-url]").textContent()).toBe( + "/_root.data?index&a=1", + ); + expect(await page.locator("[data-action-path]").textContent()).toBe( + "/?a=1", + ); + expect(await page.locator("[data-loader-url]").textContent()).toBe( + "/_root.data?a=1", + ); + expect(await page.locator("[data-loader-path]").textContent()).toBe( + "/?a=1", + ); + expect(requests).toEqual(["/_root.data?index&a=1", "/_root.data?a=1"]); + requests = []; + + // Navigate to new page + await app.clickLink("/page?b=2"); + expect(await page.locator("[data-loader-url]").textContent()).toBe( + "/page.data?b=2&_routes=routes%2Fpage", + ); + expect(await page.locator("[data-loader-path]").textContent()).toBe( + "/page?b=2", + ); + expect(requests).toEqual(["/page.data?b=2&_routes=routes%2Fpage"]); + requests = []; + }); +}); diff --git a/integration/vite-presets-test.ts b/integration/vite-presets-test.ts index 4c2eb5a979..abf835389a 100644 --- a/integration/vite-presets-test.ts +++ b/integration/vite-presets-test.ts @@ -245,6 +245,7 @@ test.describe("Vite / presets", async () => { // Ensure future flags from presets are properly merged expect(buildEndArgsMeta.futureFlags).toEqual({ unstable_optimizeDeps: true, + unstable_passThroughRequests: false, unstable_subResourceIntegrity: false, unstable_trailingSlashAwareDataRequests: false, unstable_previewServerPrerendering: false, diff --git a/packages/react-router-dev/config/config.ts b/packages/react-router-dev/config/config.ts index 0052aeb646..7caf7ed27d 100644 --- a/packages/react-router-dev/config/config.ts +++ b/packages/react-router-dev/config/config.ts @@ -86,6 +86,7 @@ type ValidateConfigFunction = (config: ReactRouterConfig) => string | void; interface FutureConfig { unstable_optimizeDeps: boolean; + unstable_passThroughRequests: boolean; unstable_subResourceIntegrity: boolean; unstable_trailingSlashAwareDataRequests: boolean; /** @@ -684,6 +685,8 @@ async function resolveConfig({ let future: FutureConfig = { unstable_optimizeDeps: userAndPresetConfigs.future?.unstable_optimizeDeps ?? false, + unstable_passThroughRequests: + userAndPresetConfigs.future?.unstable_passThroughRequests ?? false, unstable_subResourceIntegrity: userAndPresetConfigs.future?.unstable_subResourceIntegrity ?? false, unstable_trailingSlashAwareDataRequests: diff --git a/packages/react-router/__tests__/router/fetchers-test.ts b/packages/react-router/__tests__/router/fetchers-test.ts index 0fdf16c762..a8bcf82c96 100644 --- a/packages/react-router/__tests__/router/fetchers-test.ts +++ b/packages/react-router/__tests__/router/fetchers-test.ts @@ -172,6 +172,15 @@ describe("fetchers", () => { await A.loaders.foo.resolve("A DATA"); expect(A.fetcher.state).toBe("idle"); expect(A.fetcher.data).toBe("A DATA"); + expect(A.loaders.foo.stub).toHaveBeenCalledWith({ + params: {}, + request: new Request("http://localhost/foo", { + signal: A.loaders.foo.stub.mock.calls[0][0].request.signal, + }), + unstable_pattern: "/foo", + unstable_url: new URL("http://localhost/foo"), + context: {}, + }); }); it("loader re-fetch", async () => { @@ -212,11 +221,15 @@ describe("fetchers", () => { expect(A.fetcher.formAction).toBe("/foo"); expect(A.fetcher.formData).toEqual(createFormData({ key: "value" })); expect(A.fetcher.formEncType).toBe("application/x-www-form-urlencoded"); - expect( - new URL( - A.loaders.foo.stub.mock.calls[0][0].request.url, - ).searchParams.toString(), - ).toBe("key=value"); + expect(A.loaders.foo.stub).toHaveBeenCalledWith({ + params: {}, + request: new Request("http://localhost/foo?key=value", { + signal: A.loaders.foo.stub.mock.calls[0][0].request.signal, + }), + unstable_pattern: "/foo", + unstable_url: new URL("http://localhost/foo?key=value"), + context: {}, + }); await A.loaders.foo.resolve("A DATA"); expect(A.fetcher.state).toBe("idle"); @@ -264,6 +277,13 @@ describe("fetchers", () => { formData: createFormData({ key: "value" }), }); expect(A.fetcher.state).toBe("submitting"); + expect(A.actions.foo.stub).toHaveBeenCalledWith({ + params: {}, + request: expect.any(Request), + unstable_pattern: "/foo", + unstable_url: new URL("http://localhost/foo"), + context: {}, + }); await A.actions.foo.resolve("A ACTION"); expect(A.fetcher.state).toBe("loading"); @@ -374,6 +394,7 @@ describe("fetchers", () => { signal: A.loaders.root.stub.mock.calls[0][0].request.signal, }), unstable_pattern: expect.any(String), + unstable_url: expect.any(URL), context: {}, }); }); @@ -3375,6 +3396,7 @@ describe("fetchers", () => { params: {}, request: expect.any(Request), unstable_pattern: expect.any(String), + unstable_url: expect.any(URL), context: {}, }); @@ -3405,6 +3427,7 @@ describe("fetchers", () => { params: {}, request: expect.any(Request), unstable_pattern: expect.any(String), + unstable_url: expect.any(URL), context: {}, }); @@ -3433,6 +3456,7 @@ describe("fetchers", () => { params: {}, request: expect.any(Request), unstable_pattern: expect.any(String), + unstable_url: expect.any(URL), context: {}, }); @@ -3461,6 +3485,7 @@ describe("fetchers", () => { params: {}, request: expect.any(Request), unstable_pattern: expect.any(String), + unstable_url: expect.any(URL), context: {}, }); @@ -3490,6 +3515,7 @@ describe("fetchers", () => { params: {}, request: expect.any(Request), unstable_pattern: expect.any(String), + unstable_url: expect.any(URL), context: {}, }); @@ -3521,6 +3547,7 @@ describe("fetchers", () => { params: {}, request: expect.any(Request), unstable_pattern: expect.any(String), + unstable_url: expect.any(URL), context: {}, }); @@ -3551,6 +3578,7 @@ describe("fetchers", () => { params: {}, request: expect.any(Request), unstable_pattern: expect.any(String), + unstable_url: expect.any(URL), context: {}, }); diff --git a/packages/react-router/__tests__/router/router-test.ts b/packages/react-router/__tests__/router/router-test.ts index c5f9bbee35..7bd315f1b1 100644 --- a/packages/react-router/__tests__/router/router-test.ts +++ b/packages/react-router/__tests__/router/router-test.ts @@ -1752,6 +1752,7 @@ describe("a router", () => { signal: nav.loaders.tasks.stub.mock.calls[0][0].request.signal, }), unstable_pattern: "/tasks", + unstable_url: new URL("http://localhost/tasks"), context: {}, }); @@ -1762,6 +1763,7 @@ describe("a router", () => { signal: nav2.loaders.tasksId.stub.mock.calls[0][0].request.signal, }), unstable_pattern: "/tasks/:id", + unstable_url: new URL("http://localhost/tasks/1"), context: {}, }); @@ -1772,6 +1774,7 @@ describe("a router", () => { signal: nav3.loaders.tasks.stub.mock.calls[0][0].request.signal, }), unstable_pattern: "/tasks", + unstable_url: new URL("http://localhost/tasks?foo=bar#hash"), context: {}, }); @@ -1784,6 +1787,7 @@ describe("a router", () => { signal: nav4.loaders.tasks.stub.mock.calls[0][0].request.signal, }), unstable_pattern: "/tasks", + unstable_url: new URL("http://localhost/tasks?foo=bar#hash"), context: {}, }); @@ -2210,6 +2214,7 @@ describe("a router", () => { params: {}, request: expect.any(Request), unstable_pattern: "/tasks", + unstable_url: new URL("http://localhost/tasks"), context: {}, }); @@ -2254,7 +2259,8 @@ describe("a router", () => { expect(nav.actions.tasks.stub).toHaveBeenCalledWith({ params: {}, request: expect.any(Request), - unstable_pattern: expect.any(String), + unstable_pattern: "/tasks", + unstable_url: new URL("http://localhost/tasks?foo=bar"), context: {}, }); // Assert request internals, cannot do a deep comparison above since some @@ -2289,6 +2295,7 @@ describe("a router", () => { params: {}, request: expect.any(Request), unstable_pattern: expect.any(String), + unstable_url: expect.any(URL), context: {}, }); diff --git a/packages/react-router/__tests__/router/ssr-test.ts b/packages/react-router/__tests__/router/ssr-test.ts index 317bbe50d8..2cf9bd61c2 100644 --- a/packages/react-router/__tests__/router/ssr-test.ts +++ b/packages/react-router/__tests__/router/ssr-test.ts @@ -837,12 +837,29 @@ describe("ssr", () => { ]); await query(createRequest("/child")); + expect(rootLoaderStub).toHaveBeenCalledTimes(1); + expect(rootLoaderStub).toHaveBeenCalledWith({ + request: new Request("http://localhost/child"), + unstable_pattern: "/child", + unstable_url: new URL("http://localhost/child"), + params: {}, + context: {}, + }); // @ts-expect-error let rootLoaderRequest = rootLoaderStub.mock.calls[0][0]?.request; - // @ts-expect-error - let childLoaderRequest = childLoaderStub.mock.calls[0][0]?.request; expect(rootLoaderRequest.method).toBe("GET"); expect(rootLoaderRequest.url).toBe("http://localhost/child"); + + expect(childLoaderStub).toHaveBeenCalledTimes(1); + expect(childLoaderStub).toHaveBeenCalledWith({ + request: new Request("http://localhost/child"), + unstable_pattern: "/child", + unstable_url: new URL("http://localhost/child"), + params: {}, + context: {}, + }); + // @ts-expect-error + let childLoaderRequest = childLoaderStub.mock.calls[0][0]?.request; expect(childLoaderRequest.method).toBe("GET"); expect(childLoaderRequest.url).toBe("http://localhost/child"); }); @@ -874,6 +891,14 @@ describe("ssr", () => { }), ); + expect(actionStub).toHaveBeenCalledTimes(1); + expect(actionStub).toHaveBeenCalledWith({ + request: expect.any(Request), + unstable_pattern: "/child", + unstable_url: new URL("http://localhost/child"), + params: {}, + context: {}, + }); // @ts-expect-error let actionRequest = actionStub.mock.calls[0][0]?.request; expect(actionRequest.method).toBe("POST"); @@ -883,14 +908,31 @@ describe("ssr", () => { ); expect((await actionRequest.formData()).get("key")).toBe("value"); + expect(rootLoaderStub).toHaveBeenCalledTimes(1); + expect(rootLoaderStub).toHaveBeenCalledWith({ + request: expect.any(Request), + unstable_pattern: "/child", + unstable_url: new URL("http://localhost/child"), + params: {}, + context: {}, + }); // @ts-expect-error let rootLoaderRequest = rootLoaderStub.mock.calls[0][0]?.request; - // @ts-expect-error - let childLoaderRequest = childLoaderStub.mock.calls[0][0]?.request; expect(rootLoaderRequest.method).toBe("GET"); expect(rootLoaderRequest.url).toBe("http://localhost/child"); expect(rootLoaderRequest.headers.get("test")).toBe("value"); expect(await rootLoaderRequest.text()).toBe(""); + + expect(childLoaderStub).toHaveBeenCalledTimes(1); + expect(childLoaderStub).toHaveBeenCalledWith({ + request: expect.any(Request), + unstable_pattern: "/child", + unstable_url: new URL("http://localhost/child"), + params: {}, + context: {}, + }); + // @ts-expect-error + let childLoaderRequest = childLoaderStub.mock.calls[0][0]?.request; expect(childLoaderRequest.method).toBe("GET"); expect(childLoaderRequest.url).toBe("http://localhost/child"); expect(childLoaderRequest.headers.get("test")).toBe("value"); diff --git a/packages/react-router/__tests__/router/submission-test.ts b/packages/react-router/__tests__/router/submission-test.ts index 7cc38b1c31..fcfb3d369a 100644 --- a/packages/react-router/__tests__/router/submission-test.ts +++ b/packages/react-router/__tests__/router/submission-test.ts @@ -949,6 +949,7 @@ describe("submissions", () => { params: {}, request: expect.any(Request), unstable_pattern: expect.any(String), + unstable_url: expect.any(URL), context: {}, }); @@ -984,6 +985,7 @@ describe("submissions", () => { params: {}, request: expect.any(Request), unstable_pattern: expect.any(String), + unstable_url: expect.any(URL), context: {}, }); @@ -1017,6 +1019,7 @@ describe("submissions", () => { params: {}, request: expect.any(Request), unstable_pattern: expect.any(String), + unstable_url: expect.any(URL), context: {}, }); @@ -1122,6 +1125,7 @@ describe("submissions", () => { params: {}, request: expect.any(Request), unstable_pattern: expect.any(String), + unstable_url: expect.any(URL), context: {}, }); @@ -1161,6 +1165,7 @@ describe("submissions", () => { params: {}, request: expect.any(Request), unstable_pattern: expect.any(String), + unstable_url: expect.any(URL), context: {}, }); @@ -1197,6 +1202,7 @@ describe("submissions", () => { params: {}, request: expect.any(Request), unstable_pattern: expect.any(String), + unstable_url: expect.any(URL), context: {}, }); diff --git a/packages/react-router/lib/dom-export/hydrated-router.tsx b/packages/react-router/lib/dom-export/hydrated-router.tsx index d229f31320..ddb69df172 100644 --- a/packages/react-router/lib/dom-export/hydrated-router.tsx +++ b/packages/react-router/lib/dom-export/hydrated-router.tsx @@ -188,7 +188,8 @@ function createHydratedRouter({ unstable_instrumentations, mapRouteProperties, future: { - middleware: ssrInfo.context.future.v8_middleware, + unstable_passThroughRequests: + ssrInfo.context.future.unstable_passThroughRequests, }, dataStrategy: getTurboStreamSingleFetchDataStrategy( () => router, diff --git a/packages/react-router/lib/dom/server.tsx b/packages/react-router/lib/dom/server.tsx index 41dd35c02d..8714f1859b 100644 --- a/packages/react-router/lib/dom/server.tsx +++ b/packages/react-router/lib/dom/server.tsx @@ -410,6 +410,7 @@ export function createStaticRouter( get future() { return { v8_middleware: false, + unstable_passThroughRequests: false, ...opts?.future, }; }, diff --git a/packages/react-router/lib/dom/ssr/entry.ts b/packages/react-router/lib/dom/ssr/entry.ts index f965b43444..c8eaf06c74 100644 --- a/packages/react-router/lib/dom/ssr/entry.ts +++ b/packages/react-router/lib/dom/ssr/entry.ts @@ -44,6 +44,7 @@ export interface EntryContext extends FrameworkContextObject { } export interface FutureConfig { + unstable_passThroughRequests: boolean; unstable_subResourceIntegrity: boolean; unstable_trailingSlashAwareDataRequests: boolean; v8_middleware: boolean; diff --git a/packages/react-router/lib/dom/ssr/routes-test-stub.tsx b/packages/react-router/lib/dom/ssr/routes-test-stub.tsx index fd85d84ed8..4ecc5647ff 100644 --- a/packages/react-router/lib/dom/ssr/routes-test-stub.tsx +++ b/packages/react-router/lib/dom/ssr/routes-test-stub.tsx @@ -129,6 +129,8 @@ export function createRoutesStub( if (routerRef.current == null) { frameworkContextRef.current = { future: { + unstable_passThroughRequests: + future?.unstable_passThroughRequests === true, unstable_subResourceIntegrity: future?.unstable_subResourceIntegrity === true, v8_middleware: future?.v8_middleware === true, diff --git a/packages/react-router/lib/dom/ssr/routes.tsx b/packages/react-router/lib/dom/ssr/routes.tsx index 1965bce137..3f87f76666 100644 --- a/packages/react-router/lib/dom/ssr/routes.tsx +++ b/packages/react-router/lib/dom/ssr/routes.tsx @@ -340,7 +340,13 @@ export function createClientRoutes( (routeModule.clientLoader?.hydrate === true || !route.hasLoader); dataRoute.loader = async ( - { request, params, context, unstable_pattern }: LoaderFunctionArgs, + { + request, + params, + context, + unstable_pattern, + unstable_url, + }: LoaderFunctionArgs, singleFetch?: unknown, ) => { try { @@ -359,6 +365,7 @@ export function createClientRoutes( params, context, unstable_pattern, + unstable_url, async serverLoader() { preventInvalidServerHandlerCall("loader", route); @@ -394,7 +401,13 @@ export function createClientRoutes( ); dataRoute.action = ( - { request, params, context, unstable_pattern }: ActionFunctionArgs, + { + request, + params, + context, + unstable_pattern, + unstable_url, + }: ActionFunctionArgs, singleFetch?: unknown, ) => { return prefetchStylesAndCallHandler(async () => { @@ -414,6 +427,7 @@ export function createClientRoutes( params, context, unstable_pattern, + unstable_url, async serverAction() { preventInvalidServerHandlerCall("action", route); return fetchServerAction(singleFetch); diff --git a/packages/react-router/lib/router/router.ts b/packages/react-router/lib/router/router.ts index 42479c0940..dfcbb108bf 100644 --- a/packages/react-router/lib/router/router.ts +++ b/packages/react-router/lib/router/router.ts @@ -408,7 +408,9 @@ export type HydrationState = Partial< /** * Future flags to toggle new feature behavior */ -export interface FutureConfig {} +export interface FutureConfig { + unstable_passThroughRequests: boolean; +} /** * Initialization options for createRouter @@ -465,6 +467,7 @@ export interface StaticHandler { }, ) => Promise, ) => MaybePromise; + unstable_normalizePath?: (request: Request) => Path; }, ): Promise; queryRoute( @@ -476,6 +479,7 @@ export interface StaticHandler { generateMiddlewareResponse?: ( queryRoute: (r: Request) => Promise, ) => MaybePromise; + unstable_normalizePath?: (request: Request) => Path; }, ): Promise; } @@ -922,6 +926,7 @@ export function createRouter(init: RouterInit): Router { // Config driven behavior flags let future: FutureConfig = { + unstable_passThroughRequests: false, ...init.future, }; // Cleanup function for history @@ -1993,6 +1998,7 @@ export function createRouter(init: RouterInit): Router { mapRouteProperties, manifest, request, + location, matches, actionMatch, initialHydration ? [] : hydrationRouteProperties, @@ -2000,6 +2006,7 @@ export function createRouter(init: RouterInit): Router { ); let results = await callDataStrategy( request, + location, dsMatches, scopedContext, null, @@ -2274,6 +2281,7 @@ export function createRouter(init: RouterInit): Router { dsMatches, revalidatingFetchers, request, + location, scopedContext, ); @@ -2538,6 +2546,7 @@ export function createRouter(init: RouterInit): Router { mapRouteProperties, manifest, fetchRequest, + path, requestMatches, match, hydrationRouteProperties, @@ -2545,6 +2554,7 @@ export function createRouter(init: RouterInit): Router { ); let actionResults = await callDataStrategy( fetchRequest, + path, fetchMatches, scopedContext, key, @@ -2686,6 +2696,7 @@ export function createRouter(init: RouterInit): Router { dsMatches, revalidatingFetchers, revalidationRequest, + nextLocation, scopedContext, ); @@ -2844,6 +2855,7 @@ export function createRouter(init: RouterInit): Router { mapRouteProperties, manifest, fetchRequest, + path, matches, match, hydrationRouteProperties, @@ -2851,6 +2863,7 @@ export function createRouter(init: RouterInit): Router { ); let results = await callDataStrategy( fetchRequest, + path, dsMatches, scopedContext, key, @@ -3052,6 +3065,7 @@ export function createRouter(init: RouterInit): Router { // pass around the manifest, mapRouteProperties, etc. async function callDataStrategy( request: Request, + path: To, matches: DataStrategyMatch[], scopedContext: RouterContextProvider, fetcherKey: string | null, @@ -3062,6 +3076,7 @@ export function createRouter(init: RouterInit): Router { results = await callDataStrategyImpl( dataStrategyImpl as DataStrategyFunction, request, + path, matches, fetcherKey, scopedContext, @@ -3137,11 +3152,13 @@ export function createRouter(init: RouterInit): Router { matches: DataStrategyMatch[], fetchersToLoad: RevalidatingFetcher[], request: Request, + location: Location, scopedContext: RouterContextProvider, ) { // Kick off loaders and fetchers in parallel let loaderResultsPromise = callDataStrategy( request, + location, matches, scopedContext, null, @@ -3152,6 +3169,7 @@ export function createRouter(init: RouterInit): Router { if (f.matches && f.match && f.request && f.controller) { let results = await callDataStrategy( f.request, + f.path, f.matches, scopedContext, f.key, @@ -3726,7 +3744,7 @@ export interface CreateStaticHandlerOptions { basename?: string; mapRouteProperties?: MapRoutePropertiesFunction; unstable_instrumentations?: Pick[]; - future?: {}; + future?: Partial; } export function createStaticHandler( @@ -3743,6 +3761,12 @@ export function createStaticHandler( let _mapRouteProperties = opts?.mapRouteProperties || defaultMapRouteProperties; let mapRouteProperties = _mapRouteProperties; + // Currently unused in the static handler, but available for additional flags in the future + // eslint-disable-next-line @typescript-eslint/no-unused-vars + let future: FutureConfig = { + unstable_passThroughRequests: false, // unused in static handler + ...opts?.future, + }; // Leverage the existing mapRouteProperties logic to execute instrumentRoute // (if it exists) on all routes in the application @@ -3804,11 +3828,12 @@ export function createStaticHandler( skipRevalidation, dataStrategy, generateMiddlewareResponse, + unstable_normalizePath, }: Parameters[1] = {}, ): Promise { - let url = new URL(request.url); + let normalizePath = unstable_normalizePath || defaultNormalizePath; let method = request.method; - let location = createLocation("", createPath(url), null, "default"); + let location = createLocation("", normalizePath(request), null, "default"); let matches = matchRoutes(dataRoutes, location, basename); requestContext = requestContext != null ? requestContext : new RouterContextProvider(); @@ -3887,6 +3912,7 @@ export function createStaticHandler( let response = await runServerMiddlewarePipeline( { request, + unstable_url: createDataFunctionUrl(request, location), unstable_pattern: getRoutePattern(matches), matches, params: matches[0].params, @@ -4079,11 +4105,12 @@ export function createStaticHandler( requestContext, dataStrategy, generateMiddlewareResponse, + unstable_normalizePath, }: Parameters[1] = {}, ): Promise { - let url = new URL(request.url); + let normalizePath = unstable_normalizePath || defaultNormalizePath; let method = request.method; - let location = createLocation("", createPath(url), null, "default"); + let location = createLocation("", normalizePath(request), null, "default"); let matches = matchRoutes(dataRoutes, location, basename); requestContext = requestContext != null ? requestContext : new RouterContextProvider(); @@ -4119,6 +4146,7 @@ export function createStaticHandler( let response = await runServerMiddlewarePipeline( { request, + unstable_url: createDataFunctionUrl(request, location), unstable_pattern: getRoutePattern(matches), matches, params: matches[0].params, @@ -4227,6 +4255,7 @@ export function createStaticHandler( if (isMutationMethod(request.method)) { let result = await submit( request, + location, matches, routeMatch || getTargetMatch(matches, location), requestContext, @@ -4241,6 +4270,7 @@ export function createStaticHandler( let result = await loadRouteData( request, + location, matches, requestContext, dataStrategy, @@ -4276,6 +4306,7 @@ export function createStaticHandler( async function submit( request: Request, + location: Location, matches: DataRouteMatch[], actionMatch: DataRouteMatch, requestContext: unknown, @@ -4305,6 +4336,7 @@ export function createStaticHandler( mapRouteProperties, manifest, request, + location, matches, actionMatch, [], @@ -4313,6 +4345,7 @@ export function createStaticHandler( let results = await callDataStrategy( request, + location, dsMatches, isRouteRequest, requestContext, @@ -4416,6 +4449,7 @@ export function createStaticHandler( let handlerContext = await loadRouteData( loaderRequest, + location, matches, requestContext, dataStrategy, @@ -4442,6 +4476,7 @@ export function createStaticHandler( let handlerContext = await loadRouteData( loaderRequest, + location, matches, requestContext, dataStrategy, @@ -4465,6 +4500,7 @@ export function createStaticHandler( async function loadRouteData( request: Request, + location: Location, matches: DataRouteMatch[], requestContext: unknown, dataStrategy: DataStrategyFunction | null, @@ -4500,6 +4536,7 @@ export function createStaticHandler( mapRouteProperties, manifest, request, + location, matches, routeMatch, [], @@ -4519,6 +4556,7 @@ export function createStaticHandler( mapRouteProperties, manifest, request, + location, pattern, match, [], @@ -4531,6 +4569,7 @@ export function createStaticHandler( mapRouteProperties, manifest, request, + location, pattern, match, [], @@ -4560,6 +4599,7 @@ export function createStaticHandler( let results = await callDataStrategy( request, + location, dsMatches, isRouteRequest, requestContext, @@ -4589,6 +4629,7 @@ export function createStaticHandler( // pass around the manifest, mapRouteProperties, etc. async function callDataStrategy( request: Request, + location: Location, matches: DataStrategyMatch[], isRouteRequest: boolean, requestContext: unknown, @@ -4597,6 +4638,7 @@ export function createStaticHandler( let results = await callDataStrategyImpl( dataStrategy || defaultDataStrategy, request, + location, matches, null, requestContext, @@ -4703,6 +4745,15 @@ function isSubmissionNavigation( ); } +function defaultNormalizePath(request: Request): Path { + let url = new URL(request.url); + return { + pathname: url.pathname, + search: url.search, + hash: url.hash, + }; +} + function normalizeTo( location: Path, matches: DataRouteMatch[], @@ -5022,6 +5073,7 @@ function getMatchesToLoad( mapRouteProperties, manifest, request, + location, pattern, match, lazyRoutePropertiesToSkip, @@ -5066,6 +5118,7 @@ function getMatchesToLoad( mapRouteProperties, manifest, request, + location, pattern, match, lazyRoutePropertiesToSkip, @@ -5147,6 +5200,7 @@ function getMatchesToLoad( mapRouteProperties, manifest, fetchRequest, + f.path, fetcherMatches, fetcherMatch, lazyRoutePropertiesToSkip, @@ -5161,6 +5215,7 @@ function getMatchesToLoad( mapRouteProperties, manifest, fetchRequest, + f.path, fetcherMatches, fetcherMatch, lazyRoutePropertiesToSkip, @@ -5189,6 +5244,7 @@ function getMatchesToLoad( mapRouteProperties, manifest, fetchRequest, + f.path, fetcherMatches, fetcherMatch, lazyRoutePropertiesToSkip, @@ -5848,18 +5904,13 @@ async function runMiddlewarePipeline( nextResult: { value: Result } | undefined, ) => Promise, ): Promise { - let { matches, request, params, context, unstable_pattern } = args; + let { matches, ...dataFnArgs } = args; let tuples = matches.flatMap((m) => m.route.middleware ? m.route.middleware.map((fn) => [m.route.id, fn]) : [], ) as [string, MiddlewareFunction][]; let result = await callRouteMiddleware( - { - request, - params, - context, - unstable_pattern, - }, + dataFnArgs, tuples, handler, processResult, @@ -5981,6 +6032,7 @@ function getDataStrategyMatch( mapRouteProperties: MapRoutePropertiesFunction, manifest: RouteManifest, request: Request, + path: To, unstable_pattern: string, match: DataRouteMatch, lazyRoutePropertiesToSkip: string[], @@ -6051,6 +6103,7 @@ function getDataStrategyMatch( ) { return callLoaderOrAction({ request, + path, unstable_pattern, match, lazyHandlerPromise: _lazyPromises?.handler, @@ -6068,6 +6121,7 @@ function getTargetedDataStrategyMatches( mapRouteProperties: MapRoutePropertiesFunction, manifest: RouteManifest, request: Request, + path: To, matches: DataRouteMatch[], targetMatch: DataRouteMatch, lazyRoutePropertiesToSkip: string[], @@ -6098,6 +6152,7 @@ function getTargetedDataStrategyMatches( mapRouteProperties, manifest, request, + path, getRoutePattern(matches), match, lazyRoutePropertiesToSkip, @@ -6111,6 +6166,7 @@ function getTargetedDataStrategyMatches( async function callDataStrategyImpl( dataStrategyImpl: DataStrategyFunction, request: Request, + path: To, matches: DataStrategyMatch[], fetcherKey: string | null, scopedContext: unknown, @@ -6124,8 +6180,12 @@ async function callDataStrategyImpl( // Send all matches here to allow for a middleware-type implementation. // handler will be a no-op for unneeded routes and we filter those results // back out below. - let dataStrategyArgs = { + let dataStrategyArgs: Omit< + DataStrategyFunctionArgs, + "fetcherKey" | "runClientMiddleware" + > = { request, + unstable_url: createDataFunctionUrl(request, path), unstable_pattern: getRoutePattern(matches), params: matches[0].params, context: scopedContext, @@ -6184,6 +6244,7 @@ async function callDataStrategyImpl( // Default logic for calling a loader/action is the user has no specified a dataStrategy async function callLoaderOrAction({ request, + path, unstable_pattern, match, lazyHandlerPromise, @@ -6192,6 +6253,7 @@ async function callLoaderOrAction({ scopedContext, }: { request: Request; + path: To; unstable_pattern: string; match: DataRouteMatch; lazyHandlerPromise: Promise | undefined; @@ -6226,6 +6288,7 @@ async function callLoaderOrAction({ return handler( { request, + unstable_url: createDataFunctionUrl(request, path), unstable_pattern, params: match.params, context: scopedContext, @@ -6525,6 +6588,33 @@ function createClientSideRequest( return new Request(url, init); } +// Create the unstable_url object to pass to loaders/actions/middleware. +// We strip the `?index` param because that is a React Router implementation detail. +function createDataFunctionUrl(request: Request, path: To): URL { + let url = new URL(request.url); + + let parsed = typeof path === "string" ? parsePath(path) : path; + url.pathname = parsed.pathname || "/"; + + if (parsed.search) { + let searchParams = new URLSearchParams(parsed.search); + + // Strip naked index param, preserve any other index params with values + let indexValues = searchParams.getAll("index"); + searchParams.delete("index"); + for (let value of indexValues.filter(Boolean)) { + searchParams.append("index", value); + } + url.search = searchParams.size ? `?${searchParams.toString()}` : ""; + } else { + url.search = ""; + } + + url.hash = parsed.hash || ""; + + return url; +} + function convertFormDataToSearchParams(formData: FormData): URLSearchParams { let searchParams = new URLSearchParams(); @@ -6994,10 +7084,7 @@ function hasNakedIndexQuery(search: string): boolean { return new URLSearchParams(search).getAll("index").some((v) => v === ""); } -function getTargetMatch( - matches: DataRouteMatch[], - location: Location | string, -) { +function getTargetMatch(matches: DataRouteMatch[], location: Path | string) { let search = typeof location === "string" ? parsePath(location).search : location.search; if ( diff --git a/packages/react-router/lib/router/utils.ts b/packages/react-router/lib/router/utils.ts index 369093b9e0..09345aaabe 100644 --- a/packages/react-router/lib/router/utils.ts +++ b/packages/react-router/lib/router/utils.ts @@ -270,6 +270,15 @@ type DefaultContext = MiddlewareEnabled extends true interface DataFunctionArgs { /** A {@link https://developer.mozilla.org/en-US/docs/Web/API/Request Fetch Request instance} which you can use to read headers (like cookies, and {@link https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams URLSearchParams} from the request. */ request: Request; + /** + * A URL instance representing the application location being navigated to or fetched. + * Without `future.unstable_passThroughRequests` enabled, this matches `request.url`. + * With `future.unstable_passThroughRequests` enabled, this is a normalized + * URL with React-Router-specific implementation details removed (`.data` + * suffixes, `index`/`_routes` search params). + * The URL includes the origin from the request for convenience. + */ + unstable_url: URL; /** * Matched un-interpolated route pattern for the current path (i.e., /blog/:slug). * Mostly useful as a identifier to aggregate on for logging/tracing/etc. diff --git a/packages/react-router/lib/rsc/browser.tsx b/packages/react-router/lib/rsc/browser.tsx index fd38871cd6..b32c9f3f15 100644 --- a/packages/react-router/lib/rsc/browser.tsx +++ b/packages/react-router/lib/rsc/browser.tsx @@ -821,6 +821,7 @@ export function RSCHydratedRouter({ v8_middleware: false, unstable_subResourceIntegrity: false, unstable_trailingSlashAwareDataRequests: true, // always on for RSC + unstable_passThroughRequests: true, // always on for RSC }, isSpaMode: false, ssr: true, diff --git a/packages/react-router/lib/rsc/server.rsc.ts b/packages/react-router/lib/rsc/server.rsc.ts index cc41cbaca6..3cbb78df08 100644 --- a/packages/react-router/lib/rsc/server.rsc.ts +++ b/packages/react-router/lib/rsc/server.rsc.ts @@ -68,6 +68,7 @@ import { createRedirectErrorDigest, createRouteErrorResponseDigest, } from "../errors"; +import { getNormalizedPath } from "../server-runtime/urls"; const Outlet: typeof OutletType = UNTYPED_Outlet; const WithComponentProps: typeof WithComponentPropsType = @@ -715,6 +716,7 @@ async function generateResourceResponse( return generateErrorResponse(error); } }, + unstable_normalizePath: (r) => getNormalizedPath(r, basename, null), }); return response; } catch (error) { @@ -806,6 +808,7 @@ async function generateRenderResponse( ...(routeIdsToLoad ? { filterMatchesToLoad: (m) => routeIdsToLoad!.includes(m.route.id) } : {}), + unstable_normalizePath: (r) => getNormalizedPath(r, basename, null), async generateMiddlewareResponse(query) { // If this is an RSC server action, process that and then call query as a // revalidation. If this is a RR Form/Fetcher submission, diff --git a/packages/react-router/lib/rsc/server.ssr.tsx b/packages/react-router/lib/rsc/server.ssr.tsx index 491bfbe8f7..1e802e58cd 100644 --- a/packages/react-router/lib/rsc/server.ssr.tsx +++ b/packages/react-router/lib/rsc/server.ssr.tsx @@ -581,6 +581,7 @@ export function RSCStaticRouter({ getPayload }: RSCStaticRouterProps) { v8_middleware: false, unstable_subResourceIntegrity: false, unstable_trailingSlashAwareDataRequests: true, // always on for RSC + unstable_passThroughRequests: true, // always on for RSC }, isSpaMode: false, ssr: true, diff --git a/packages/react-router/lib/server-runtime/data.ts b/packages/react-router/lib/server-runtime/data.ts index db680dfd78..3d75870aaa 100644 --- a/packages/react-router/lib/server-runtime/data.ts +++ b/packages/react-router/lib/server-runtime/data.ts @@ -4,6 +4,7 @@ import type { LoaderFunctionArgs, ActionFunctionArgs, } from "../router/utils"; +import type { FutureConfig } from "../router/router"; import { isDataWithResponseInit, isRedirectStatusCode } from "../router/router"; /** @@ -21,9 +22,13 @@ export interface AppLoadContext { export async function callRouteHandler( handler: LoaderFunction | ActionFunction, args: LoaderFunctionArgs | ActionFunctionArgs, + future: FutureConfig, ) { let result = await handler({ - request: stripRoutesParam(stripIndexParam(args.request)), + request: future.unstable_passThroughRequests + ? args.request + : stripRoutesParam(stripIndexParam(args.request)), + unstable_url: args.unstable_url, params: args.params, context: args.context, unstable_pattern: args.unstable_pattern, @@ -42,11 +47,6 @@ export async function callRouteHandler( return result; } -// TODO: Document these search params better -// and stop stripping these in V2. These break -// support for running in a SW and also expose -// valuable info to data funcs that is being asked -// for such as "is this a data request?". function stripIndexParam(request: Request) { let url = new URL(request.url); let indexValues = url.searchParams.getAll("index"); diff --git a/packages/react-router/lib/server-runtime/routes.ts b/packages/react-router/lib/server-runtime/routes.ts index d221b1f752..9eebd42011 100644 --- a/packages/react-router/lib/server-runtime/routes.ts +++ b/packages/react-router/lib/server-runtime/routes.ts @@ -131,13 +131,17 @@ export function createStaticHandlerDataRoutes( return result.data; } } - let val = await callRouteHandler(route.module.loader!, args); + let val = await callRouteHandler( + route.module.loader!, + args, + future, + ); return val; } : undefined, action: route.module.action ? (args: RRActionFunctionArgs) => - callRouteHandler(route.module.action!, args) + callRouteHandler(route.module.action!, args, future) : undefined, handle: route.module.handle, }; diff --git a/packages/react-router/lib/server-runtime/server.ts b/packages/react-router/lib/server-runtime/server.ts index ea1df0ddad..12f249210e 100644 --- a/packages/react-router/lib/server-runtime/server.ts +++ b/packages/react-router/lib/server-runtime/server.ts @@ -39,6 +39,7 @@ import { getManifestPath } from "../dom/ssr/fog-of-war"; import type { unstable_InstrumentRequestHandlerFunction } from "../router/instrumentation"; import { instrumentHandler } from "../router/instrumentation"; import { throwIfPotentialCSRFAttack } from "../actions"; +import { getNormalizedPath } from "./urls"; export type RequestHandler = ( request: Request, @@ -106,31 +107,12 @@ function derive(build: ServerBuild, mode?: string) { loadContext = initialContext || {}; } - let url = new URL(request.url); - - let normalizedBasename = build.basename || "/"; - let normalizedPath = url.pathname; - if (build.future.unstable_trailingSlashAwareDataRequests) { - if (normalizedPath.endsWith("/_.data")) { - // Handle trailing slash URLs: /about/_.data -> /about/ - normalizedPath = normalizedPath.replace(/_.data$/, ""); - } else { - normalizedPath = normalizedPath.replace(/\.data$/, ""); - } - } else { - if (stripBasename(normalizedPath, normalizedBasename) === "/_root.data") { - normalizedPath = normalizedBasename; - } else if (normalizedPath.endsWith(".data")) { - normalizedPath = normalizedPath.replace(/\.data$/, ""); - } - - if ( - stripBasename(normalizedPath, normalizedBasename) !== "/" && - normalizedPath.endsWith("/") - ) { - normalizedPath = normalizedPath.slice(0, -1); - } - } + let requestUrl = new URL(request.url); + let normalizedPathname = getNormalizedPath( + request, + build.basename, + build.future, + ).pathname; let isSpaMode = getBuildTimeHeader(request, "X-React-Router-SPA-Mode") === "yes"; @@ -139,17 +121,17 @@ function derive(build: ServerBuild, mode?: string) { // pre-rendered site would if (!build.ssr) { // Decode the URL path before checking against the prerender config - let decodedPath = decodeURI(normalizedPath); + let decodedPath = decodeURI(normalizedPathname); - if (normalizedBasename !== "/") { - let strippedPath = stripBasename(decodedPath, normalizedBasename); + if (build.basename && build.basename !== "/") { + let strippedPath = stripBasename(decodedPath, build.basename); if (strippedPath == null) { errorHandler( new ErrorResponseImpl( 404, "Not Found", `Refusing to prerender the \`${decodedPath}\` path because it does ` + - `not start with the basename \`${normalizedBasename}\``, + `not start with the basename \`${build.basename}\``, ), { context: loadContext, @@ -174,7 +156,7 @@ function derive(build: ServerBuild, mode?: string) { !build.prerender.includes(decodedPath) && !build.prerender.includes(decodedPath + "/") ) { - if (url.pathname.endsWith(".data")) { + if (requestUrl.pathname.endsWith(".data")) { // 404 on non-pre-rendered `.data` requests errorHandler( new ErrorResponseImpl( @@ -202,11 +184,11 @@ function derive(build: ServerBuild, mode?: string) { // Manifest request for fog of war let manifestUrl = getManifestPath( build.routeDiscovery.manifestPath, - normalizedBasename, + build.basename, ); - if (url.pathname === manifestUrl) { + if (requestUrl.pathname === manifestUrl) { try { - let res = await handleManifestRequest(build, routes, url); + let res = await handleManifestRequest(build, routes, requestUrl); return res; } catch (e) { handleError(e); @@ -214,19 +196,16 @@ function derive(build: ServerBuild, mode?: string) { } } - let matches = matchServerRoutes(routes, normalizedPath, build.basename); + let matches = matchServerRoutes(routes, normalizedPathname, build.basename); if (matches && matches.length > 0) { Object.assign(params, matches[0].params); } let response: Response; - if (url.pathname.endsWith(".data")) { - let handlerUrl = new URL(request.url); - handlerUrl.pathname = normalizedPath; - + if (requestUrl.pathname.endsWith(".data")) { let singleFetchMatches = matchServerRoutes( routes, - handlerUrl.pathname, + normalizedPathname, build.basename, ); @@ -235,7 +214,7 @@ function derive(build: ServerBuild, mode?: string) { build, staticHandler, request, - handlerUrl, + normalizedPathname, loadContext, handleError, ); @@ -281,7 +260,7 @@ function derive(build: ServerBuild, mode?: string) { handleError, ); } else { - let { pathname } = url; + let { pathname } = requestUrl; let criticalCss: CriticalCss | undefined = undefined; if (build.unstable_getCriticalCss) { @@ -443,10 +422,13 @@ async function handleSingleFetchRequest( build: ServerBuild, staticHandler: StaticHandler, request: Request, - handlerUrl: URL, + normalizedPath: string, loadContext: AppLoadContext | RouterContextProvider, handleError: (err: unknown) => void, ): Promise { + let handlerUrl = new URL(request.url); + handlerUrl.pathname = normalizedPath; + let response = request.method !== "GET" ? await singleFetchAction( @@ -511,6 +493,8 @@ async function handleDocumentRequest( } } : undefined, + unstable_normalizePath: (r) => + getNormalizedPath(r, build.basename, build.future), }); if (!isResponse(result)) { @@ -688,6 +672,8 @@ async function handleResourceRequest( } } : undefined, + unstable_normalizePath: (r) => + getNormalizedPath(r, build.basename, build.future), }); return handleQueryRouteResult(result); diff --git a/packages/react-router/lib/server-runtime/single-fetch.ts b/packages/react-router/lib/server-runtime/single-fetch.ts index 71e469b5fd..aecd30970f 100644 --- a/packages/react-router/lib/server-runtime/single-fetch.ts +++ b/packages/react-router/lib/server-runtime/single-fetch.ts @@ -24,6 +24,7 @@ import { ServerMode } from "./mode"; import { getDocumentHeaders } from "./headers"; import type { ServerBuild } from "./build"; import { throwIfPotentialCSRFAttack } from "../actions"; +import { getNormalizedPath } from "./urls"; // Add 304 for server side - that is not included in the client side logic // because the browser should fill those responses with the cached data @@ -54,13 +55,15 @@ export async function singleFetchAction( return handleQueryError(new Error("Bad Request"), 400); } - let handlerRequest = new Request(handlerUrl, { - method: request.method, - body: request.body, - headers: request.headers, - signal: request.signal, - ...(request.body ? { duplex: "half" } : undefined), - }); + let handlerRequest = build.future.unstable_passThroughRequests + ? request + : new Request(handlerUrl, { + method: request.method, + body: request.body, + headers: request.headers, + signal: request.signal, + ...(request.body ? { duplex: "half" } : undefined), + }); let result = await staticHandler.query(handlerRequest, { requestContext: loadContext, @@ -76,6 +79,8 @@ export async function singleFetchAction( } } : undefined, + unstable_normalizePath: (r) => + getNormalizedPath(r, build.basename, build.future), }); return handleQueryResult(result); @@ -147,10 +152,12 @@ export async function singleFetchLoaders( let loadRouteIds = routesParam ? new Set(routesParam.split(",")) : null; try { - let handlerRequest = new Request(handlerUrl, { - headers: request.headers, - signal: request.signal, - }); + let handlerRequest = build.future.unstable_passThroughRequests + ? request + : new Request(handlerUrl, { + headers: request.headers, + signal: request.signal, + }); let result = await staticHandler.query(handlerRequest, { requestContext: loadContext, @@ -166,6 +173,8 @@ export async function singleFetchLoaders( } } : undefined, + unstable_normalizePath: (r) => + getNormalizedPath(r, build.basename, build.future), }); return handleQueryResult(result); diff --git a/packages/react-router/lib/server-runtime/urls.ts b/packages/react-router/lib/server-runtime/urls.ts new file mode 100644 index 0000000000..f71cc97719 --- /dev/null +++ b/packages/react-router/lib/server-runtime/urls.ts @@ -0,0 +1,52 @@ +import type { FutureConfig } from "../dom/ssr/entry"; +import type { Path } from "../router/history"; +import { stripBasename } from "../router/utils"; + +export function getNormalizedPath( + request: Request, + basename: string | undefined, + future: FutureConfig | null, +): Path { + basename = basename || "/"; + + let url = new URL(request.url); + let pathname = url.pathname; + + // Strip .data suffix + if (future?.unstable_trailingSlashAwareDataRequests) { + if (pathname.endsWith("/_.data")) { + // Handle trailing slash URLs: /about/_.data -> /about/ + pathname = pathname.replace(/_\.data$/, ""); + } else { + pathname = pathname.replace(/\.data$/, ""); + } + } else { + if (stripBasename(pathname, basename) === "/_root.data") { + pathname = basename; + } else if (pathname.endsWith(".data")) { + pathname = pathname.replace(/\.data$/, ""); + } + + if (stripBasename(pathname, basename) !== "/" && pathname.endsWith("/")) { + pathname = pathname.slice(0, -1); + } + } + + // Strip _routes param + let searchParams = new URLSearchParams(url.search); + searchParams.delete("_routes"); + let search = searchParams.toString(); + if (search) { + search = `?${search}`; + } + + // Don't touch index params here - they're needed for router matching and are + // stripped when creating the loader/action args + + return { + pathname, + search, + // No hashes on the server + hash: "", + }; +} diff --git a/packages/react-router/lib/types/route-data.ts b/packages/react-router/lib/types/route-data.ts index 52eefee088..b4bacc336d 100644 --- a/packages/react-router/lib/types/route-data.ts +++ b/packages/react-router/lib/types/route-data.ts @@ -2,6 +2,7 @@ import type { ClientLoaderFunctionArgs, ClientActionFunctionArgs, } from "../dom/ssr/routeModules"; +import type { Path } from "../router/history"; import type { DataWithResponseInit, RouterContextProvider, @@ -77,6 +78,15 @@ export type ClientDataFunctionArgs = { * @note Because client data functions are called before a network request is made, the Request object does not include the headers which the browser automatically adds. React Router infers the "content-type" header from the enc-type of the form that performed the submission. **/ request: Request; + /** + * A URL instance representing the application location being navigated to or fetched. + * Without `future.unstable_passThroughRequests` enabled, this matches `request.url`. + * With `future.unstable_passThroughRequests` enabled, this is a normalized + * URL with React-Router-specific implementation details removed (`.data` + * pathnames, `index`/`_routes` search params). + * The URL includes the origin from the request for convenience. + */ + unstable_url: URL; /** * {@link https://reactrouter.com/start/framework/routing#dynamic-segments Dynamic route params} for the current route. * @example @@ -111,6 +121,15 @@ export type ClientDataFunctionArgs = { export type ServerDataFunctionArgs = { /** A {@link https://developer.mozilla.org/en-US/docs/Web/API/Request Fetch Request instance} which you can use to read the url, method, headers (such as cookies), and request body from the request. */ request: Request; + /** + * A URL instance representing the application location being navigated to or fetched. + * Without `future.unstable_passThroughRequests` enabled, this matches `request.url`. + * With `future.unstable_passThroughRequests` enabled, this is a normalized + * URL with React-Router-specific implementation details removed (`.data` + * pathnames, `index`/`_routes` search params). + * The URL includes the origin from the request for convenience. + */ + unstable_url: URL; /** * {@link https://reactrouter.com/start/framework/routing#dynamic-segments Dynamic route params} for the current route. * @example From c4642c8f2ac6ad8df38aac38901bb9a81df75f29 Mon Sep 17 00:00:00 2001 From: Nandan Date: Sat, 28 Feb 2026 06:13:46 +0530 Subject: [PATCH 10/33] RSC: tighten route match typing in server render (#14830) --- contributors.yml | 1 + packages/react-router/lib/rsc/server.rsc.ts | 59 +++++++++++---------- 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/contributors.yml b/contributors.yml index e26d62fcde..e985a31b8b 100644 --- a/contributors.yml +++ b/contributors.yml @@ -301,6 +301,7 @@ - mtendekuyokwa19 - mtliendo - namoscato +- Nandann018-ux - nanianlisao - ned-park - nenene3 diff --git a/packages/react-router/lib/rsc/server.rsc.ts b/packages/react-router/lib/rsc/server.rsc.ts index 3cbb78df08..259a0c492e 100644 --- a/packages/react-router/lib/rsc/server.rsc.ts +++ b/packages/react-router/lib/rsc/server.rsc.ts @@ -20,7 +20,6 @@ import { } from "../router/router"; import { type ActionFunction, - type DataRouteMatch, type LoaderFunction, type Params, type ShouldRevalidateFunction, @@ -212,6 +211,8 @@ export type RSCRouteConfigEntry = RSCRouteConfigEntryBase & { export type RSCRouteConfig = Array; +type RSCRouteDataMatch = RouteMatch; + export type RSCRouteManifest = { clientAction?: ClientActionFunction; clientLoader?: ClientLoaderFunction; @@ -1134,7 +1135,7 @@ async function getRenderPayload( }); let matchesPromise = Promise.all( - staticContext.matches.map((match, i) => { + (staticContext.matches as RSCRouteDataMatch[]).map((match, i) => { let isBelowErrorBoundary = i > deepestRenderedRouteIdx; let parentId = parentIds[match.route.id]; return getRSCRouteMatch({ @@ -1171,24 +1172,24 @@ async function getRSCRouteMatch({ parentId, }: { staticContext: StaticHandlerContext; - match: DataRouteMatch; + match: RSCRouteDataMatch; isBelowErrorBoundary: boolean; routeIdsToLoad: string[] | null; parentId: string | undefined; }) { - // @ts-expect-error - FIXME: Fix the types here - await explodeLazyRoute(match.route); - const Layout = (match.route as any).Layout || React.Fragment; - const Component = (match.route as any).Component; - const ErrorBoundary = (match.route as any).ErrorBoundary; - const HydrateFallback = (match.route as any).HydrateFallback; - const loaderData = staticContext.loaderData[match.route.id]; - const actionData = staticContext.actionData?.[match.route.id]; + const route = match.route; + await explodeLazyRoute(route); + const Layout = route.Layout || React.Fragment; + const Component = route.Component; + const ErrorBoundary = route.ErrorBoundary; + const HydrateFallback = route.HydrateFallback; + const loaderData = staticContext.loaderData[route.id]; + const actionData = staticContext.actionData?.[route.id]; const params = match.params; // TODO: DRY this up once it's fully fleshed out let element: React.ReactElement | undefined = undefined; let shouldLoadRoute = - !routeIdsToLoad || routeIdsToLoad.includes(match.route.id); + !routeIdsToLoad || routeIdsToLoad.includes(route.id); // Only bother rendering Server Components for routes that we're surfacing, // so nothing at/below an error boundary and prune routes if included in // `routeIdsToLoad`. This is specifically important when a middleware @@ -1217,7 +1218,7 @@ async function getRSCRouteMatch({ let error: unknown = undefined; if (ErrorBoundary && staticContext.errors) { - error = staticContext.errors[match.route.id]; + error = staticContext.errors[route.id]; } const errorElement = ErrorBoundary ? React.createElement( @@ -1251,33 +1252,37 @@ async function getRSCRouteMatch({ ) : undefined; + const hmrRoute = route as RSCRouteConfigEntry & { + __ensureClientRouteModuleForHMR?: unknown; + }; + return { - clientAction: (match.route as any).clientAction, - clientLoader: (match.route as any).clientLoader, + clientAction: route.clientAction, + clientLoader: route.clientLoader, element, errorElement, - handle: (match.route as any).handle, - hasAction: !!match.route.action, + handle: route.handle, + hasAction: !!route.action, hasComponent: !!Component, hasErrorBoundary: !!ErrorBoundary, - hasLoader: !!match.route.loader, + hasLoader: !!route.loader, hydrateFallbackElement, - id: match.route.id, - index: match.route.index, - links: (match.route as any).links, - meta: (match.route as any).meta, + id: route.id, + index: "index" in route ? route.index : undefined, + links: route.links, + meta: route.meta, params, parentId, - path: match.route.path, + path: route.path, pathname: match.pathname, pathnameBase: match.pathnameBase, - shouldRevalidate: (match.route as any).shouldRevalidate, + shouldRevalidate: route.shouldRevalidate, // Add an unused client-only export (if present) so HMR can support // switching between server-first and client-only routes during development - ...((match.route as any).__ensureClientRouteModuleForHMR + ...(hmrRoute.__ensureClientRouteModuleForHMR ? { - __ensureClientRouteModuleForHMR: (match.route as any) - .__ensureClientRouteModuleForHMR, + __ensureClientRouteModuleForHMR: + hmrRoute.__ensureClientRouteModuleForHMR, } : {}), }; From bbe4a731723b9b1005b8d2c0bf82f83d8d763524 Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Sat, 28 Feb 2026 00:44:24 +0000 Subject: [PATCH 11/33] chore: format --- packages/react-router/lib/rsc/server.rsc.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/react-router/lib/rsc/server.rsc.ts b/packages/react-router/lib/rsc/server.rsc.ts index 259a0c492e..7653c2a192 100644 --- a/packages/react-router/lib/rsc/server.rsc.ts +++ b/packages/react-router/lib/rsc/server.rsc.ts @@ -1188,8 +1188,7 @@ async function getRSCRouteMatch({ const params = match.params; // TODO: DRY this up once it's fully fleshed out let element: React.ReactElement | undefined = undefined; - let shouldLoadRoute = - !routeIdsToLoad || routeIdsToLoad.includes(route.id); + let shouldLoadRoute = !routeIdsToLoad || routeIdsToLoad.includes(route.id); // Only bother rendering Server Components for routes that we're surfacing, // so nothing at/below an error boundary and prune routes if included in // `routeIdsToLoad`. This is specifically important when a middleware From bda5bb7e5769507e191caef78f056237353e5918 Mon Sep 17 00:00:00 2001 From: Tim Dorr Date: Mon, 2 Mar 2026 12:06:02 -0500 Subject: [PATCH 12/33] Fix typo in comment (#14844) --- packages/react-router/lib/dom/lib.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-router/lib/dom/lib.tsx b/packages/react-router/lib/dom/lib.tsx index 74dd0a01e6..7b804e76fa 100644 --- a/packages/react-router/lib/dom/lib.tsx +++ b/packages/react-router/lib/dom/lib.tsx @@ -1220,7 +1220,7 @@ export interface LinkProps unstable_defaultShouldRevalidate?: boolean; /** - * Masked path for for this navigation, when you want to navigate the router to + * Masked path for this navigation, when you want to navigate the router to * one location but display a separate location in the URL bar. * * This is useful for contextual navigations such as opening an image in a modal From 7990dd5d571686e12a840d01b261ddf0a9886f8f Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Mon, 2 Mar 2026 17:07:55 +0000 Subject: [PATCH 13/33] chore: generate markdown docs from jsdocs --- docs/api/components/Link.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/components/Link.md b/docs/api/components/Link.md index 3aae2112eb..cd92fa643c 100644 --- a/docs/api/components/Link.md +++ b/docs/api/components/Link.md @@ -236,7 +236,7 @@ standard revalidation behavior. [modes: framework, data] -Masked path for for this navigation, when you want to navigate the router to +Masked path for this navigation, when you want to navigate the router to one location but display a separate location in the URL bar. This is useful for contextual navigations such as opening an image in a modal From 8a10826f32c8b8332901f9e881c443adb4b9c338 Mon Sep 17 00:00:00 2001 From: Tim Dorr Date: Wed, 4 Mar 2026 11:56:56 -0500 Subject: [PATCH 14/33] docs: fix typo in useNavigate documentation (#14848) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kamil Kuźnicki --- contributors.yml | 1 + packages/react-router/lib/hooks.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/contributors.yml b/contributors.yml index e985a31b8b..0042fd098f 100644 --- a/contributors.yml +++ b/contributors.yml @@ -237,6 +237,7 @@ - KostiantynPopovych - KubasuIvanSakwa - KutnerUri +- kuzznicki - kylegirard - LadyTsukiko - landisdesign diff --git a/packages/react-router/lib/hooks.tsx b/packages/react-router/lib/hooks.tsx index 597d1417f0..fbc561545c 100644 --- a/packages/react-router/lib/hooks.tsx +++ b/packages/react-router/lib/hooks.tsx @@ -297,7 +297,7 @@ function useIsomorphicLayoutEffect( * * Be cautious with `navigate(number)`. If your application can load up to a * route that has a button that tries to navigate forward/back, there may not be - * a `[`History`](https://developer.mozilla.org/en-US/docs/Web/API/History) + * a [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History) * entry to go back or forward to, or it can go somewhere you don't expect * (like a different domain). * From c5012af14999d997b9b9a5aa4f1d4742e00fb3f4 Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Wed, 4 Mar 2026 16:58:48 +0000 Subject: [PATCH 15/33] chore: generate markdown docs from jsdocs --- docs/api/hooks/useNavigate.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/hooks/useNavigate.md b/docs/api/hooks/useNavigate.md index 83aee981a3..4a1afe21cb 100644 --- a/docs/api/hooks/useNavigate.md +++ b/docs/api/hooks/useNavigate.md @@ -103,7 +103,7 @@ navigate(1); Be cautious with `navigate(number)`. If your application can load up to a route that has a button that tries to navigate forward/back, there may not be -a `[`History`](https://developer.mozilla.org/en-US/docs/Web/API/History) +a [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History) entry to go back or forward to, or it can go somewhere you don't expect (like a different domain). From 9636a7d4a587005430e0f2ee159b748ae292ae9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20M=2E?= <34163393+amtins@users.noreply.github.com> Date: Tue, 10 Mar 2026 12:53:32 +0100 Subject: [PATCH 16/33] docs: remove typo in framework testing documentation (#14863) --- contributors.yml | 1 + docs/start/framework/testing.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/contributors.yml b/contributors.yml index 0042fd098f..2afd9dc877 100644 --- a/contributors.yml +++ b/contributors.yml @@ -25,6 +25,7 @@ - alexlbr - AlexWebLab - amitdahan +- amtins - AmRo045 - amsal - AnandShiva diff --git a/docs/start/framework/testing.md b/docs/start/framework/testing.md index dd2059f020..536f97edf8 100644 --- a/docs/start/framework/testing.md +++ b/docs/start/framework/testing.md @@ -80,7 +80,7 @@ test("LoginForm renders error messages", async () => { ## Using with Framework Mode Types -It's important to note that `createRoutesStub` is designed for _unit_ testing of reusable components in your application that rely on on contextual router information (i.e., `loaderData`, `actionData`, `matches`). These components usually obtain this information via the hooks (`useLoaderData`, `useActionData`, `useMatches`) or via props passed down from the ancestor route component. We **strongly** recommend limiting your usage of `createRoutesStub` to unit testing of these types of reusable components. +It's important to note that `createRoutesStub` is designed for _unit_ testing of reusable components in your application that rely on contextual router information (i.e., `loaderData`, `actionData`, `matches`). These components usually obtain this information via the hooks (`useLoaderData`, `useActionData`, `useMatches`) or via props passed down from the ancestor route component. We **strongly** recommend limiting your usage of `createRoutesStub` to unit testing of these types of reusable components. `createRoutesStub` is _not designed_ for (and is arguably incompatible with) direct testing of Route components using the [`Route.\*`](../../explanation/type-safety) types available in Framework Mode. This is because the `Route.*` types are derived from your actual application - including the real `loader`/`action` functions as well as the structure of your route tree structure (which defines the `matches` type). When you use `createRoutesStub`, you are providing stubbed values for `loaderData`, `actionData`, and even your `matches` based on the route tree you pass to `createRoutesStub`. Therefore, the types won't align with the `Route.*` types and you'll get type issues trying to use a route component in a route stub. From 11d280088d892e9a7bd7f8bf0b3a237b2448b2ab Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Tue, 10 Mar 2026 11:54:17 +0000 Subject: [PATCH 17/33] chore: format --- contributors.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributors.yml b/contributors.yml index 2afd9dc877..218e7d0422 100644 --- a/contributors.yml +++ b/contributors.yml @@ -25,9 +25,9 @@ - alexlbr - AlexWebLab - amitdahan -- amtins - AmRo045 - amsal +- amtins - AnandShiva - Andarist - andreasottosson-polestar From 7d21b1c15b190a590c90d5ac98d3f5eda59873b1 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 10 Mar 2026 12:03:28 -0400 Subject: [PATCH 18/33] Add additional unit test - hydrate fallback rendering for SPA middleware w/o loader (#14579) --- .../dom/data-browser-router-test.tsx | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/packages/react-router/__tests__/dom/data-browser-router-test.tsx b/packages/react-router/__tests__/dom/data-browser-router-test.tsx index ea8736a4f1..aa2d85d44d 100644 --- a/packages/react-router/__tests__/dom/data-browser-router-test.tsx +++ b/packages/react-router/__tests__/dom/data-browser-router-test.tsx @@ -336,6 +336,74 @@ function testDomRouter( `); }); + it("renders ancestor HydrateFallback during hydration middleware-only execution", async () => { + let middlewareDfd = createDeferred(); + let router = createTestRouter([ + { + path: "/", + Component: Outlet, + HydrateFallback: () => "Loading root...", + children: [ + { + index: true, + middleware: [() => middlewareDfd.promise], + Component: () => "Hello World!", + }, + ], + }, + ]); + + let { container } = render(); + + expect(getHtml(container)).toMatchInlineSnapshot(` + "
+ Loading root... +
" + `); + + middlewareDfd.resolve(); + await waitFor(() => screen.getByText("Hello World!")); + expect(getHtml(container)).toMatchInlineSnapshot(` + "
+ Hello World! +
" + `); + }); + + it("renders self HydrateFallback during hydration middleware-only execution", async () => { + let middlewareDfd = createDeferred(); + let router = createTestRouter([ + { + path: "/", + Component: Outlet, + children: [ + { + index: true, + HydrateFallback: () => "Loading index...", + middleware: [() => middlewareDfd.promise], + Component: () => "Hello World!", + }, + ], + }, + ]); + + let { container } = render(); + + expect(getHtml(container)).toMatchInlineSnapshot(` + "
+ Loading index... +
" + `); + + middlewareDfd.resolve(); + await waitFor(() => screen.getByText("Hello World!")); + expect(getHtml(container)).toMatchInlineSnapshot(` + "
+ Hello World! +
" + `); + }); + it("does not render hydrateFallback if no data fetch or lazy loading is required", async () => { let fooDefer = createDeferred(); let router = createTestRouter( From 7f1014972c789e732bfcb0f2e6e2b5e813931321 Mon Sep 17 00:00:00 2001 From: Roman Baiocco Date: Thu, 12 Mar 2026 18:56:06 -0400 Subject: [PATCH 19/33] fix: handle non-regular files in dev watcher to prevent crashes (#14854) * fix: handle non-regular files in dev watcher to prevent crashes The dev file watcher crashes when Unix socket files (e.g. from overmind or pm2) exist in the project root. chokidar v4 calls fs.watch() on these files, which fails on macOS with errno -102 (UNKNOWN). - Extract watcher ignore logic into an exported `isIgnoredByWatcher` function for testability - Run cheap path-based checks before the fs.statSync call to avoid unnecessary syscalls on already-ignored paths - Filter non-regular files (sockets, pipes, etc.) via fs.statSync - Add an error handler on the watcher so unexpected errors log a warning instead of crashing the process Fixes: https://github.com/RomanBaiocco/react-router-sock-repro Related: https://github.com/paulmillr/chokidar/issues/1391 * chore: sign CLA * add link to chokidar github issue * don't fail create-react-router tests on deprecation warnings --------- Co-authored-by: Pedro Cattori --- .changeset/fix-dev-socket-file-crash.md | 5 ++ contributors.yml | 1 + .../__tests__/create-react-router-test.ts | 3 + .../__tests__/watcher-ignored-test.ts | 70 +++++++++++++++++++ packages/react-router-dev/config/config.ts | 49 ++++++++++--- 5 files changed, 117 insertions(+), 11 deletions(-) create mode 100644 .changeset/fix-dev-socket-file-crash.md create mode 100644 packages/react-router-dev/__tests__/watcher-ignored-test.ts diff --git a/.changeset/fix-dev-socket-file-crash.md b/.changeset/fix-dev-socket-file-crash.md new file mode 100644 index 0000000000..f33ce09bcb --- /dev/null +++ b/.changeset/fix-dev-socket-file-crash.md @@ -0,0 +1,5 @@ +--- +"@react-router/dev": patch +--- + +Fix `react-router dev` crash when Unix socket files exist in the project root diff --git a/contributors.yml b/contributors.yml index 218e7d0422..dc0269449a 100644 --- a/contributors.yml +++ b/contributors.yml @@ -360,6 +360,7 @@ - RobHannay - robinvdvleuten - roli-lpci +- RomanBaiocco - rossipedia - rtmann - rtzll diff --git a/packages/create-react-router/__tests__/create-react-router-test.ts b/packages/create-react-router/__tests__/create-react-router-test.ts index c1ac99c31c..3bb7a31a8e 100644 --- a/packages/create-react-router/__tests__/create-react-router-test.ts +++ b/packages/create-react-router/__tests__/create-react-router-test.ts @@ -1205,6 +1205,9 @@ async function execCreateReactRouter({ env: { ...process.env, ...env, + NODE_OPTIONS: [process.env.NODE_OPTIONS, "--no-deprecation"] + .filter(Boolean) + .join(" "), ...(interactive ? { CREATE_REACT_ROUTER_FORCE_INTERACTIVE: "true" } : {}), diff --git a/packages/react-router-dev/__tests__/watcher-ignored-test.ts b/packages/react-router-dev/__tests__/watcher-ignored-test.ts new file mode 100644 index 0000000000..6d06533b7b --- /dev/null +++ b/packages/react-router-dev/__tests__/watcher-ignored-test.ts @@ -0,0 +1,70 @@ +import path from "node:path"; +import fs from "node:fs"; +import net from "node:net"; +import os from "node:os"; + +import { isIgnoredByWatcher } from "../config/config"; + +describe("isIgnoredByWatcher", () => { + let tmpDir: string; + let root: string; + let appDirectory: string; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "rr-watcher-test-")); + root = tmpDir; + appDirectory = path.join(root, "app"); + fs.mkdirSync(appDirectory, { recursive: true }); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it("does not ignore regular files at root level", () => { + let filePath = path.join(root, "react-router.config.ts"); + fs.writeFileSync(filePath, ""); + expect(isIgnoredByWatcher(filePath, { root, appDirectory })).toBe(false); + }); + + it("does not ignore the root directory itself", () => { + expect(isIgnoredByWatcher(root, { root, appDirectory })).toBe(false); + }); + + it("does not ignore files inside app directory", () => { + let filePath = path.join(appDirectory, "root.tsx"); + fs.writeFileSync(filePath, ""); + expect(isIgnoredByWatcher(filePath, { root, appDirectory })).toBe(false); + }); + + it("ignores files in subdirectories outside the app directory", () => { + let subDir = path.join(root, "node_modules", "some-package"); + fs.mkdirSync(subDir, { recursive: true }); + let filePath = path.join(subDir, "index.js"); + fs.writeFileSync(filePath, ""); + expect(isIgnoredByWatcher(filePath, { root, appDirectory })).toBe(true); + }); + + it("ignores Unix socket files at the root level", async () => { + let socketPath = path.join(root, "overmind.sock"); + let server = net.createServer(); + + await new Promise((resolve) => server.listen(socketPath, resolve)); + try { + expect(isIgnoredByWatcher(socketPath, { root, appDirectory })).toBe(true); + } finally { + await new Promise((resolve) => server.close(() => resolve())); + } + }); + + it("ignores paths that cannot be stat'd", () => { + let nonexistent = path.join(root, "ghost.sock"); + // File doesn't exist — statSync with throwIfNoEntry: false returns + // undefined, so this should fall through to `return false`. But if + // statSync throws for another reason, the catch block returns true. + // Here we just verify it doesn't throw. + expect(() => + isIgnoredByWatcher(nonexistent, { root, appDirectory }), + ).not.toThrow(); + }); +}); diff --git a/packages/react-router-dev/config/config.ts b/packages/react-router-dev/config/config.ts index 7caf7ed27d..f3269803b8 100644 --- a/packages/react-router-dev/config/config.ts +++ b/packages/react-router-dev/config/config.ts @@ -818,17 +818,12 @@ export async function createConfigLoader({ if (!fsWatcher) { fsWatcher = chokidar.watch([root, appDirectory], { ignoreInitial: true, - ignored: (path) => { - let dirname = Path.dirname(path); - - return ( - !dirname.startsWith(appDirectory) && - // Ensure we're only watching files outside of the app directory - // that are at the root level, not nested in subdirectories - path !== root && // Watch the root directory itself - dirname !== root // Watch files at the root level - ); - }, + ignored: (path) => isIgnoredByWatcher(path, { root, appDirectory }), + }); + + fsWatcher.on("error", (error: unknown) => { + let message = error instanceof Error ? error.message : String(error); + console.warn(colors.yellow(`File watcher error: ${message}`)); }); fsWatcher.on("all", async (...args) => { @@ -1166,3 +1161,35 @@ function isEntryFileDependency( return false; } + +export function isIgnoredByWatcher( + path: string, + { root, appDirectory }: { root: string; appDirectory: string }, +): boolean { + let dirname = Path.dirname(path); + + let ignoredByPath = + !dirname.startsWith(appDirectory) && + // Ensure we're only watching files outside of the app directory + // that are at the root level, not nested in subdirectories + path !== root && // Watch the root directory itself + dirname !== root; // Watch files at the root level + + if (ignoredByPath) { + return true; + } + + // Filter out non-regular files (sockets, pipes, etc.) that + // crash `fs.watch()` on macOS with errno -102 + // https://github.com/paulmillr/chokidar/issues/1391 + try { + let stat = fs.statSync(path, { throwIfNoEntry: false }); + if (stat && !stat.isFile() && !stat.isDirectory()) { + return true; + } + } catch { + return true; + } + + return false; +} From ea28e66926460656815cee9b7f2dfaa872513a8c Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 17 Mar 2026 11:02:33 -0400 Subject: [PATCH 20/33] Escape locations in prerendered redirect HTML (#14880) --- .changeset/kind-shirts-turn.md | 5 ++++ packages/react-router-dev/vite/plugin.ts | 35 ++++++++++++++++++------ 2 files changed, 32 insertions(+), 8 deletions(-) create mode 100644 .changeset/kind-shirts-turn.md diff --git a/.changeset/kind-shirts-turn.md b/.changeset/kind-shirts-turn.md new file mode 100644 index 0000000000..dc26b6af24 --- /dev/null +++ b/.changeset/kind-shirts-turn.md @@ -0,0 +1,5 @@ +--- +"@react-router/dev": patch +--- + +Escape redirect locations in prerendered redirect HTML diff --git a/packages/react-router-dev/vite/plugin.ts b/packages/react-router-dev/vite/plugin.ts index 6eaf1297a3..6dd897b04f 100644 --- a/packages/react-router-dev/vite/plugin.ts +++ b/packages/react-router-dev/vite/plugin.ts @@ -2752,15 +2752,17 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => { // A short delay causes Google to interpret the redirect as temporary. // https://developers.google.com/search/docs/crawling-indexing/301-redirects#metarefresh let delay = response.status === 302 ? 2 : 0; + let escapedLocation = escapeHtml(location ?? ""); + let escapedPathname = escapeHtml(pathname); html = ` -Redirecting to: ${location} - +Redirecting to: ${escapedLocation} + - - Redirecting from ${pathname} to ${location} + + Redirecting from ${escapedPathname} to ${escapedLocation} `; @@ -3345,15 +3347,17 @@ async function prerenderRoute( // A short delay causes Google to interpret the redirect as temporary. // https://developers.google.com/search/docs/crawling-indexing/301-redirects#metarefresh let delay = response.status === 302 ? 2 : 0; + let escapedLocation = escapeHtml(location ?? ""); + let escapedNormalizedPath = escapeHtml(normalizedPath); html = ` -Redirecting to: ${location} - +Redirecting to: ${escapedLocation} + - - Redirecting from ${normalizedPath} to ${location} + + Redirecting from ${escapedNormalizedPath} to ${escapedLocation} `; @@ -4328,3 +4332,18 @@ function createSpaModeRequest( metadata: { type: "spa", path: "/" }, }; } + +// Note: Duplicated from react-router/lib/dom/ssr/markup +// Must be kept in sync with the original implementation. +const ESCAPE_REGEX = /[&><\u2028\u2029]/g; +const ESCAPE_LOOKUP: { [match: string]: string } = { + "&": "\\u0026", + ">": "\\u003e", + "<": "\\u003c", + "\u2028": "\\u2028", + "\u2029": "\\u2029", +}; + +function escapeHtml(html: string) { + return html.replace(ESCAPE_REGEX, (match) => ESCAPE_LOOKUP[match]); +} From 8646d39bc7b10a43745dc255b4faa25673a9e908 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 17 Mar 2026 11:32:43 -0400 Subject: [PATCH 21/33] Align redirect protocol validation in RSC flows (#14882) --- .changeset/twelve-snails-wait.md | 5 ++++ packages/react-router/lib/router/router.ts | 32 +++++++++++----------- packages/react-router/lib/rsc/browser.tsx | 20 +++++++++++++- 3 files changed, 40 insertions(+), 17 deletions(-) create mode 100644 .changeset/twelve-snails-wait.md diff --git a/.changeset/twelve-snails-wait.md b/.changeset/twelve-snails-wait.md new file mode 100644 index 0000000000..02bf3f77f6 --- /dev/null +++ b/.changeset/twelve-snails-wait.md @@ -0,0 +1,5 @@ +--- +"react-router": patch +--- + +Sync protocol validation to rsc flows diff --git a/packages/react-router/lib/router/router.ts b/packages/react-router/lib/router/router.ts index dfcbb108bf..e383f06662 100644 --- a/packages/react-router/lib/router/router.ts +++ b/packages/react-router/lib/router/router.ts @@ -6501,28 +6501,28 @@ function normalizeRelativeRoutingRedirectResponse( return response; } +// Match Chrome's behavior: +// https://github.com/chromium/chromium/blob/216dbeb61db0c667e62082e5f5400a32d6983df3/content/public/common/url_utils.cc#L82 +export const invalidProtocols = [ + "about:", + "blob:", + "chrome:", + "chrome-untrusted:", + "content:", + "data:", + "devtools:", + "file:", + "filesystem:", + // eslint-disable-next-line no-script-url + "javascript:", +]; + function normalizeRedirectLocation( location: string, currentUrl: URL, basename: string, historyInstance: History, ): string { - // Match Chrome's behavior: - // https://github.com/chromium/chromium/blob/216dbeb61db0c667e62082e5f5400a32d6983df3/content/public/common/url_utils.cc#L82 - let invalidProtocols = [ - "about:", - "blob:", - "chrome:", - "chrome-untrusted:", - "content:", - "data:", - "devtools:", - "file:", - "filesystem:", - // eslint-disable-next-line no-script-url - "javascript:", - ]; - if (isAbsoluteUrl(location)) { // Strip off the protocol+origin for same-origin + same-basename absolute redirects let normalizedLocation = location; diff --git a/packages/react-router/lib/rsc/browser.tsx b/packages/react-router/lib/rsc/browser.tsx index b32c9f3f15..4e557b0b09 100644 --- a/packages/react-router/lib/rsc/browser.tsx +++ b/packages/react-router/lib/rsc/browser.tsx @@ -7,7 +7,11 @@ import { FrameworkContext, setIsHydrated } from "../dom/ssr/components"; import type { FrameworkContextObject } from "../dom/ssr/entry"; import { createBrowserHistory, invariant } from "../router/history"; import type { Router as DataRouter, RouterInit } from "../router/router"; -import { createRouter, isMutationMethod } from "../router/router"; +import { + createRouter, + invalidProtocols, + isMutationMethod, +} from "../router/router"; import type { RSCPayload, RSCRouteManifest, @@ -140,6 +144,9 @@ export function createCallServer({ .then(async (payload) => { if (payload.type === "redirect") { if (payload.reload || isExternalLocation(payload.location)) { + if (hasInvalidProtocol(payload.location)) { + throw new Error("Invalid redirect location"); + } window.location.href = payload.location; return; } @@ -164,6 +171,9 @@ export function createCallServer({ ) { if (rerender.type === "redirect") { if (rerender.reload || isExternalLocation(rerender.location)) { + if (hasInvalidProtocol(rerender.location)) { + throw new Error("Invalid redirect location"); + } window.location.href = rerender.location; return; } @@ -1082,6 +1092,14 @@ function isExternalLocation(location: string) { return newLocation.origin !== window.location.origin; } +function hasInvalidProtocol(location: string): boolean { + try { + return invalidProtocols.includes(new URL(location).protocol); + } catch { + return false; + } +} + function cloneRoutes(routes: DataRouteObject[] | undefined): DataRouteObject[] { if (!routes) return undefined as any; return routes.map((route) => ({ From 830d3bac11ac9c9aa975f6dfccaead24df9caae4 Mon Sep 17 00:00:00 2001 From: Varun Chawla <34209028+veeceey@users.noreply.github.com> Date: Tue, 17 Mar 2026 11:52:06 -0700 Subject: [PATCH 22/33] Fix percent encoding in relative path navigation (#14786) --- .changeset/gentle-doors-visit.md | 5 ++ contributors.yml | 1 + .../__tests__/dom/special-characters-test.tsx | 81 +++++++++++++++++++ packages/react-router/lib/hooks.tsx | 10 ++- 4 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 .changeset/gentle-doors-visit.md diff --git a/.changeset/gentle-doors-visit.md b/.changeset/gentle-doors-visit.md new file mode 100644 index 0000000000..c35f888597 --- /dev/null +++ b/.changeset/gentle-doors-visit.md @@ -0,0 +1,5 @@ +--- +"react-router": patch +--- + +Fix percent encoding in relative path navigation diff --git a/contributors.yml b/contributors.yml index dc0269449a..3feb787f0d 100644 --- a/contributors.yml +++ b/contributors.yml @@ -482,3 +482,4 @@ - zeromask1337 - zheng-chuang - zxTomw +- veeceey diff --git a/packages/react-router/__tests__/dom/special-characters-test.tsx b/packages/react-router/__tests__/dom/special-characters-test.tsx index e37a24f9d1..8a3c1c956c 100644 --- a/packages/react-router/__tests__/dom/special-characters-test.tsx +++ b/packages/react-router/__tests__/dom/special-characters-test.tsx @@ -761,6 +761,87 @@ describe("special character tests", () => { ); } }); + + it("handles encoded percent signs in ancestor splat route segments", async () => { + let ctx = render( + + + , + ); + + expect(getHtml(ctx.container)).toMatchInlineSnapshot(` + "" + `); + + await fireEvent.click(screen.getByText("Link to grandchild")); + await waitFor(() => screen.getByText("Grandchild")); + + expect(getHtml(ctx.container)).toMatchInlineSnapshot(` + "
+ + Link to grandchild + +

+ Grandchild +

+
+            {"*":"grandchild","param":"percent-%-sign"}
+          
+
" + `); + + function App() { + return ( + + } /> + + ); + } + + function Parent() { + return ( + + } /> + + ); + } + + function Child() { + let location = useLocation(); + let to = location.pathname.endsWith("grandchild") + ? "." + : "./grandchild"; + return ( + <> + Link to grandchild + + } /> + + + ); + } + + function Grandchild() { + return ( + <> +

Grandchild

+
{JSON.stringify(useParams())}
+ + ); + } + }); }); describe("when matching as part of the defined route path", () => { diff --git a/packages/react-router/lib/hooks.tsx b/packages/react-router/lib/hooks.tsx index fbc561545c..e7d917b046 100644 --- a/packages/react-router/lib/hooks.tsx +++ b/packages/react-router/lib/hooks.tsx @@ -888,12 +888,15 @@ export function useRoutesImpl( pathname: joinPaths([ parentPathnameBase, // Re-encode pathnames that were decoded inside matchRoutes. - // Pre-encode `?` and `#` ahead of `encodeLocation` because it uses + // Pre-encode `%`, `?` and `#` ahead of `encodeLocation` because it uses // `new URL()` internally and we need to prevent it from treating // them as separators navigator.encodeLocation ? navigator.encodeLocation( - match.pathname.replace(/\?/g, "%3F").replace(/#/g, "%23"), + match.pathname + .replace(/%/g, "%25") + .replace(/\?/g, "%3F") + .replace(/#/g, "%23"), ).pathname : match.pathname, ]), @@ -903,12 +906,13 @@ export function useRoutesImpl( : joinPaths([ parentPathnameBase, // Re-encode pathnames that were decoded inside matchRoutes - // Pre-encode `?` and `#` ahead of `encodeLocation` because it uses + // Pre-encode `%`, `?` and `#` ahead of `encodeLocation` because it uses // `new URL()` internally and we need to prevent it from treating // them as separators navigator.encodeLocation ? navigator.encodeLocation( match.pathnameBase + .replace(/%/g, "%25") .replace(/\?/g, "%3F") .replace(/#/g, "%23"), ).pathname From 1cd923e38fd4cf86195f15850e41106dd42d1808 Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Tue, 17 Mar 2026 18:52:57 +0000 Subject: [PATCH 23/33] chore: format --- contributors.yml | 2 +- .../react-router/__tests__/dom/special-characters-test.tsx | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/contributors.yml b/contributors.yml index 3feb787f0d..5065f1e816 100644 --- a/contributors.yml +++ b/contributors.yml @@ -448,6 +448,7 @@ - valerii15298 - ValiantCat - vdusart +- veeceey - vesan - vezaynk - VictorElHajj @@ -482,4 +483,3 @@ - zeromask1337 - zheng-chuang - zxTomw -- veeceey diff --git a/packages/react-router/__tests__/dom/special-characters-test.tsx b/packages/react-router/__tests__/dom/special-characters-test.tsx index 8a3c1c956c..f9c64b128e 100644 --- a/packages/react-router/__tests__/dom/special-characters-test.tsx +++ b/packages/react-router/__tests__/dom/special-characters-test.tsx @@ -764,9 +764,7 @@ describe("special character tests", () => { it("handles encoded percent signs in ancestor splat route segments", async () => { let ctx = render( - + , ); From 8c3c7ced1496522175c6839d30624955cc4534c1 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 18 Mar 2026 12:30:04 -0400 Subject: [PATCH 24/33] fix: allow Framework Mode route components to be passed to createRoutesStub (#14892) Route components typed with Route.ComponentProps have more specific params and matches types. Due to TypeScript contravariance on function parameters, they were not assignable to React.ComponentType. Widen Component, HydrateFallback, and ErrorBoundary in StubRouteExtensions to React.ComponentType so any route component can be passed without a type error. Fixes #14886 Co-authored-by: Claude Sonnet 4.6 --- .changeset/fix-createRoutesStub-component-type.md | 5 +++++ packages/react-router/lib/components.tsx | 6 +++--- packages/react-router/lib/dom/ssr/routes-test-stub.tsx | 9 +++------ 3 files changed, 11 insertions(+), 9 deletions(-) create mode 100644 .changeset/fix-createRoutesStub-component-type.md diff --git a/.changeset/fix-createRoutesStub-component-type.md b/.changeset/fix-createRoutesStub-component-type.md new file mode 100644 index 0000000000..2c604a85b7 --- /dev/null +++ b/.changeset/fix-createRoutesStub-component-type.md @@ -0,0 +1,5 @@ +--- +"react-router": patch +--- + +Fix type error when passing Framework Mode route components using `Route.ComponentProps` to `createRoutesStub` diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx index 78252451be..e1c9643dec 100644 --- a/packages/react-router/lib/components.tsx +++ b/packages/react-router/lib/components.tsx @@ -1936,7 +1936,7 @@ function useRouteComponentProps() { } export type RouteComponentProps = ReturnType; -export type RouteComponentType = React.ComponentType; +type RouteComponentType = React.ComponentType; export function WithComponentProps({ children, @@ -1963,7 +1963,7 @@ function useHydrateFallbackProps() { } export type HydrateFallbackProps = ReturnType; -export type HydrateFallbackType = React.ComponentType; +type HydrateFallbackType = React.ComponentType; export function WithHydrateFallbackProps({ children, @@ -1991,7 +1991,7 @@ function useErrorBoundaryProps() { } export type ErrorBoundaryProps = ReturnType; -export type ErrorBoundaryType = React.ComponentType; +type ErrorBoundaryType = React.ComponentType; export function WithErrorBoundaryProps({ children, diff --git a/packages/react-router/lib/dom/ssr/routes-test-stub.tsx b/packages/react-router/lib/dom/ssr/routes-test-stub.tsx index 4ecc5647ff..2d46872c62 100644 --- a/packages/react-router/lib/dom/ssr/routes-test-stub.tsx +++ b/packages/react-router/lib/dom/ssr/routes-test-stub.tsx @@ -23,9 +23,6 @@ import type { FrameworkContextObject, } from "./entry"; import { - type RouteComponentType, - type HydrateFallbackType, - type ErrorBoundaryType, Outlet, RouterProvider, createMemoryRouter, @@ -37,9 +34,9 @@ import type { EntryRoute } from "./routes"; import { FrameworkContext } from "./components"; interface StubRouteExtensions { - Component?: RouteComponentType; - HydrateFallback?: HydrateFallbackType; - ErrorBoundary?: ErrorBoundaryType; + Component?: React.ComponentType; + HydrateFallback?: React.ComponentType; + ErrorBoundary?: React.ComponentType; loader?: LoaderFunction; action?: ActionFunction; children?: StubRouteObject[]; From 2469dd6621fbcaec689571c3f003af5711bc54de Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 18 Mar 2026 10:22:41 -0400 Subject: [PATCH 25/33] Add bug fixing skill --- .agents/skills/fix-bug/SKILL.md | 151 ++++++++++++++++++++++++++++++++ .gitignore | 3 +- CLAUDE.md | 4 + 3 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 .agents/skills/fix-bug/SKILL.md diff --git a/.agents/skills/fix-bug/SKILL.md b/.agents/skills/fix-bug/SKILL.md new file mode 100644 index 0000000000..a1904c4e08 --- /dev/null +++ b/.agents/skills/fix-bug/SKILL.md @@ -0,0 +1,151 @@ +--- +name: fix-bug +description: Fix a reported bug in React Router from a GitHub issue. Use when the user provides a GitHub issue URL and asks to fix a bug, investigate an issue, or reproduce a problem. Handles the full workflow: fetching the issue, finding the reproduction, writing a failing test, and implementing the fix. +disable-model-invocation: true +--- + +# Fix React Router Bug + +Fix the bug reported in the following GitHub issue: $ARGUMENTS + +## Branching + +Bug fixes should start from a clean working tree. If there are changes, prompt me to resolve them before continuing. + +Bugs should be fixed from the `dev` branch in in a new branch using the format `{author}/{semantic-branch-name}` (i.e., `brophdawg11/fix-navigation`): + +```sh +git branch {author}/{semantic-branch-name} dev +git checkout {author}/{semantic-branch-name} +``` + +## Workflow + +### 1. Fetch and Understand the Issue + +Use `gh issue view --repo remix-run/react-router` or `WebFetch` to read the full issue. + +Extract: + +- Bug description and expected vs actual behavior +- React Router version and mode (Declarative / Data / Framework / RSC) +- Any code snippets in the issue +- Links to reproductions (StackBlitz, CodeSandbox, GitHub repo, etc.) + +### 2. Validate the Reproduction + +**If there's a StackBlitz/CodeSandbox/online sandbox link:** + +- Use `WebFetch` to read the sandbox URL and extract the relevant code +- Identify the exact sequence of events that triggers the bug + +**If there's a GitHub repository link:** + +- Use `WebFetch` to read key files (`package.json`, relevant source files) from the raw GitHub URL +- Identify the route configuration, loaders, actions, or components involved + +**If no reproduction link exists:** + +- Search the issue comments with `gh issue view --repo remix-run/react-router --comments` +- Look for code snippets in comments +- Ask the user: "No reproduction was provided. Can you share a minimal reproduction or paste the relevant code?" + +### 3. Identify the Affected Code + +Based on the bug, locate the relevant source files. Consult the key file map: + +| Area | Files | +| ---------------------- | ----------------------------------------------------------- | +| Core router logic | `packages/react-router/lib/router/router.ts` | +| React components/hooks | `packages/react-router/lib/components.tsx`, `lib/hooks.tsx` | +| DOM utilities | `packages/react-router/lib/dom/` | +| Vite/Framework plugin | `packages/react-router-dev/vite/plugin.ts` | +| RSC | `packages/react-router/lib/rsc/` | + +Use `Grep` and `Glob` to trace the relevant code paths. + +### 4. Write a Failing Test + +**Unit test** (for router logic, hooks, pure component behavior — no build needed): + +- Location: `packages/react-router/__tests__/` +- Use Jest; run with: `pnpm test packages/react-router/__tests__/` +- Match the style of nearby test files (describe/it blocks, `createStaticHandler`, `createMemoryRouter`, `render`, `screen`, etc.) + +**Integration test** (for Vite plugin, SSR, hydration, Framework Mode): + +- Location: `integration/` +- Use Playwright with `createFixture()` → `createAppFixture()` → `PlaywrightFixture` +- Run with: `pnpm test:integration:run --project chromium integration/` +- Build first if needed: `pnpm test:integration --project chromium` + +Write the test to **reproduce the bug exactly** — it must fail before the fix. + +Run it and confirm it fails: + +```bash +pnpm test packages/react-router/__tests__/ # unit +# or +pnpm test:integration:run --project chromium integration/ # integration +``` + +### 5. Implement the Fix + +- Make the minimal change needed to fix the bug +- Do not refactor unrelated code +- Confirm the fix addresses the root cause, not just the symptom +- Consider all five modes: does this fix break anything in Declarative / Data / Framework / RSC? + +Run the failing test again — it must now pass: + +```bash +pnpm test packages/react-router/__tests__/ +``` + +Run the broader test suite to check for regressions: + +```bash +pnpm test packages/react-router/ +``` + +If the fix touches Framework/Vite code, run integration tests too: + +```bash +pnpm test:integration:run --project chromium +``` + +Confirm linting and typechecking pass: + +```bash +pnpm lint +pnpm typecheck +``` + +### 6. Create a Changeset + +Create `.changeset/.md`: + +```markdown +--- +"react-router": patch +--- + +fix: +``` + +Use `patch` for bug fixes. Only include packages in the frontmatter that were actually changed. + +### 7. Report Results + +Summarize: + +- What the bug was and why it happened +- What code was changed and why +- That the test now passes +- Any edge cases or related issues noticed + +Ask me to review the changes and iterate based on any feedback. + +### 8. Open PR + +Once I approve the fix, commit the changes and open a PR to `dev`. Include a `Closes #NNNN` in the description to link the PR to the original issue. Also link the issue in the `Development` sidebar diff --git a/.gitignore b/.gitignore index eed2c1bf39..212db6e711 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,5 @@ worker-configuration.d.ts # v7 reference docs /public -.claude/settings.local.json \ No newline at end of file +.claude/settings.local.json +.claude/skills diff --git a/CLAUDE.md b/CLAUDE.md index 50ee466624..7ee0ae35c6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,6 +10,10 @@ - Testing patterns and conventions - Documentation guidelines +### Skills + +If there is an [`.agents/skills`](.agents/skills) directory in the repository, symlink those skills to `.claude/skills` to make sure they are made available to Claude. Refresh you list of available skills if needed. This is a git ignored directory because we want to keep the canonical skills in `.agents/skills`. + ## During Work **Always consult AGENTS.md** when you need to: From d904466cc6ac7eff6a735c875d1220ba6f16c5c9 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 18 Mar 2026 13:08:43 -0400 Subject: [PATCH 26/33] Enter prerelease mode --- .changeset/pre.json | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .changeset/pre.json diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 0000000000..baf47d3e0c --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,39 @@ +{ + "mode": "pre", + "tag": "pre", + "initialVersions": { + "integration": "0.0.0", + "integration-cloudflare-dev-proxy-template": "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.13.1", + "react-router": "7.13.1", + "@react-router/architect": "7.13.1", + "@react-router/cloudflare": "7.13.1", + "@react-router/dev": "7.13.1", + "react-router-dom": "7.13.1", + "@react-router/express": "7.13.1", + "@react-router/fs-routes": "7.13.1", + "@react-router/node": "7.13.1", + "@react-router/remix-routes-option-adapter": "7.13.1", + "@react-router/serve": "7.13.1", + "@playground/data": "0.0.0", + "@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-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 c68a9b35933fe5a3750ea8e52399d06f9aaecf77 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 13:11:00 -0400 Subject: [PATCH 27/33] chore: Update version for release (pre) (#14893) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/pre.json | 13 ++++- packages/create-react-router/CHANGELOG.md | 6 +++ 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 | 49 +++++++++++++++++++ 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 | 7 +++ 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 | 47 ++++++++++++++++++ packages/react-router/package.json | 2 +- 23 files changed, 185 insertions(+), 12 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index baf47d3e0c..fa93d8df80 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -35,5 +35,16 @@ "@playground/split-route-modules-spa": "0.0.0", "@playground/vite-plugin-cloudflare": "0.0.0" }, - "changesets": [] + "changesets": [ + "cold-schools-relate", + "fix-createRoutesStub-component-type", + "fix-dev-socket-file-crash", + "gentle-doors-visit", + "kind-shirts-turn", + "passthrough-reqeusts", + "remove-agnostic-types", + "sweet-houses-kick", + "twelve-snails-wait", + "unstable-url" + ] } diff --git a/packages/create-react-router/CHANGELOG.md b/packages/create-react-router/CHANGELOG.md index 0784fd5bb9..cae5c8d6b2 100644 --- a/packages/create-react-router/CHANGELOG.md +++ b/packages/create-react-router/CHANGELOG.md @@ -1,5 +1,11 @@ # `create-react-router` +## 7.13.2-pre.0 + +### Patch Changes + +- chore: replace chalk with picocolors ([#14837](https://github.com/remix-run/react-router/pull/14837)) + ## 7.13.1 ## 7.13.0 diff --git a/packages/create-react-router/package.json b/packages/create-react-router/package.json index dd0aaafaf5..806d81a411 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.13.1", + "version": "7.13.2-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 c8c119e4ca..33032786c6 100644 --- a/packages/react-router-architect/CHANGELOG.md +++ b/packages/react-router-architect/CHANGELOG.md @@ -1,5 +1,13 @@ # `@react-router/architect` +## 7.13.2-pre.0 + +### Patch Changes + +- Updated dependencies: + - `react-router@7.13.2-pre.0` + - `@react-router/node@7.13.2-pre.0` + ## 7.13.1 ### Patch Changes diff --git a/packages/react-router-architect/package.json b/packages/react-router-architect/package.json index dbe134378c..ea9202f8f9 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.13.1", + "version": "7.13.2-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 00809f3abf..3de75e1099 100644 --- a/packages/react-router-cloudflare/CHANGELOG.md +++ b/packages/react-router-cloudflare/CHANGELOG.md @@ -1,5 +1,12 @@ # `@react-router/cloudflare` +## 7.13.2-pre.0 + +### Patch Changes + +- Updated dependencies: + - `react-router@7.13.2-pre.0` + ## 7.13.1 ### Patch Changes diff --git a/packages/react-router-cloudflare/package.json b/packages/react-router-cloudflare/package.json index 6bb01d8ad4..6d3763e081 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.13.1", + "version": "7.13.2-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 aad0e8732d..171cc70d21 100644 --- a/packages/react-router-dev/CHANGELOG.md +++ b/packages/react-router-dev/CHANGELOG.md @@ -1,5 +1,54 @@ # `@react-router/dev` +## 7.13.2-pre.0 + +### Patch Changes + +- Fix `react-router dev` crash when Unix socket files exist in the project root ([#14854](https://github.com/remix-run/react-router/pull/14854)) +- Escape redirect locations in prerendered redirect HTML ([#14880](https://github.com/remix-run/react-router/pull/14880)) +- Add `future.unstable_passThroughRequests` flag ([#14775](https://github.com/remix-run/react-router/pull/14775)) + + By default, React Router normalizes the `request.url` passed to your `loader`, `action`, and `middleware` functions by removing React Router's internal implementation details (`.data` suffixes, `index` + `_routes` query params). + + Enabling this flag removes that normalization and passes the raw HTTP `request` instance to your handlers. This provides a few benefits: + - Reduces server-side overhead by eliminating multiple `new Request()` calls on the critical path + - Allows you to distinguish document from data requests in your handlers base don the presence of a `.data` suffix (useful for observability purposes) + + If you were previously relying on the normalization of `request.url`, you can switch to use the new sibling `unstable_url` parameter which contains a `URL` instance representing the normalized location: + + ```tsx + // ❌ Before: you could assume there was no `.data` suffix in `request.url` + export async function loader({ request }: Route.LoaderArgs) { + let url = new URL(request.url); + if (url.pathname === "/path") { + // This check will fail with the flag enabled because the `.data` suffix will + // exist on data requests + } + } + + // ✅ After: use `unstable_url` for normalized routing logic and `request.url` + // for raw routing logic + export async function loader({ request, unstable_url }: Route.LoaderArgs) { + if (unstable_url.pathname === "/path") { + // This will always have the `.data` suffix stripped + } + + // And now you can distinguish between document versus data requests + let isDataRequest = new URL(request.url).pathname.endsWith(".data"); + } + ``` + +- Add a new `unstable_url: URL` parameter to route handler methods (`loader`, `action`, `middleware`, etc.) representing the normalized URL the application is navigating to or fetching, with React Router implementation details removed (`.data`suffix, `index`/`_routes` query params) ([#14775](https://github.com/remix-run/react-router/pull/14775)) + + This is being added alongside the new `future.unstable_passthroughRequests` future flag so that users still have a way to access the normalized URL when that flag is enabled and non-normalized `request`'s are being passed to your handlers. When adopting this flag, you will only need to start leveraging this new parameter if you are relying on the normalization of `request.url` in your application code. + + If you don't have the flag enabled, then `unstable_url` will match `request.url`. + +- Updated dependencies: + - `react-router@7.13.2-pre.0` + - `@react-router/node@7.13.2-pre.0` + - `@react-router/serve@7.13.2-pre.0` + ## 7.13.1 ### Patch Changes diff --git a/packages/react-router-dev/package.json b/packages/react-router-dev/package.json index cbd36e9bd5..f14ce676fb 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.13.1", + "version": "7.13.2-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 08ae899ab4..9ea29e92ef 100644 --- a/packages/react-router-dom/CHANGELOG.md +++ b/packages/react-router-dom/CHANGELOG.md @@ -1,5 +1,12 @@ # react-router-dom +## 7.13.2-pre.0 + +### Patch Changes + +- Updated dependencies: + - `react-router@7.13.2-pre.0` + ## 7.13.1 ### Patch Changes diff --git a/packages/react-router-dom/package.json b/packages/react-router-dom/package.json index acf8f7e4fe..10bd5aae0c 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.13.1", + "version": "7.13.2-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 e3a04f2727..457466f26f 100644 --- a/packages/react-router-express/CHANGELOG.md +++ b/packages/react-router-express/CHANGELOG.md @@ -1,5 +1,13 @@ # `@react-router/express` +## 7.13.2-pre.0 + +### Patch Changes + +- Updated dependencies: + - `react-router@7.13.2-pre.0` + - `@react-router/node@7.13.2-pre.0` + ## 7.13.1 ### Patch Changes diff --git a/packages/react-router-express/package.json b/packages/react-router-express/package.json index 27cdb9fae2..9a43a2a13b 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.13.1", + "version": "7.13.2-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 57efb2e6c3..6000d21fff 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.13.2-pre.0 + +### Patch Changes + +- Updated dependencies: + - `@react-router/dev@7.13.2-pre.0` + ## 7.13.1 ### Patch Changes diff --git a/packages/react-router-fs-routes/package.json b/packages/react-router-fs-routes/package.json index d1782e99d3..8d56df5caf 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.13.1", + "version": "7.13.2-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 46f78a092c..178fe460b2 100644 --- a/packages/react-router-node/CHANGELOG.md +++ b/packages/react-router-node/CHANGELOG.md @@ -1,5 +1,12 @@ # `@react-router/node` +## 7.13.2-pre.0 + +### Patch Changes + +- Updated dependencies: + - `react-router@7.13.2-pre.0` + ## 7.13.1 ### Patch Changes diff --git a/packages/react-router-node/package.json b/packages/react-router-node/package.json index 5d44e20d50..0ee228f56b 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.13.1", + "version": "7.13.2-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 8f58373864..508bbc3ef8 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.13.2-pre.0 + +### Patch Changes + +- Updated dependencies: + - `@react-router/dev@7.13.2-pre.0` + ## 7.13.1 ### 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 874d21e0f9..41ee7ab6ec 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.13.1", + "version": "7.13.2-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 ba58db9041..a9820877d9 100644 --- a/packages/react-router-serve/CHANGELOG.md +++ b/packages/react-router-serve/CHANGELOG.md @@ -1,5 +1,14 @@ # `@react-router/serve` +## 7.13.2-pre.0 + +### Patch Changes + +- Updated dependencies: + - `react-router@7.13.2-pre.0` + - `@react-router/node@7.13.2-pre.0` + - `@react-router/express@7.13.2-pre.0` + ## 7.13.1 ### Patch Changes diff --git a/packages/react-router-serve/package.json b/packages/react-router-serve/package.json index 61fdba42e0..b9a5200b7d 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.13.1", + "version": "7.13.2-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 fe148cf413..66f55ffbf5 100644 --- a/packages/react-router/CHANGELOG.md +++ b/packages/react-router/CHANGELOG.md @@ -1,5 +1,52 @@ # `react-router` +## 7.13.2-pre.0 + +### Patch Changes + +- Fix clientLoader.hydrate when an ancestor route is also hydrating a clientLoader ([#14835](https://github.com/remix-run/react-router/pull/14835)) +- Fix type error when passing Framework Mode route components using `Route.ComponentProps` to `createRoutesStub` ([#14892](https://github.com/remix-run/react-router/pull/14892)) +- Fix percent encoding in relative path navigation ([#14786](https://github.com/remix-run/react-router/pull/14786)) +- Add `future.unstable_passThroughRequests` flag ([#14775](https://github.com/remix-run/react-router/pull/14775)) + + By default, React Router normalizes the `request.url` passed to your `loader`, `action`, and `middleware` functions by removing React Router's internal implementation details (`.data` suffixes, `index` + `_routes` query params). + + Enabling this flag removes that normalization and passes the raw HTTP `request` instance to your handlers. This provides a few benefits: + - Reduces server-side overhead by eliminating multiple `new Request()` calls on the critical path + - Allows you to distinguish document from data requests in your handlers base don the presence of a `.data` suffix (useful for observability purposes) + + If you were previously relying on the normalization of `request.url`, you can switch to use the new sibling `unstable_url` parameter which contains a `URL` instance representing the normalized location: + + ```tsx + // ❌ Before: you could assume there was no `.data` suffix in `request.url` + export async function loader({ request }: Route.LoaderArgs) { + let url = new URL(request.url); + if (url.pathname === "/path") { + // This check will fail with the flag enabled because the `.data` suffix will + // exist on data requests + } + } + + // ✅ After: use `unstable_url` for normalized routing logic and `request.url` + // for raw routing logic + export async function loader({ request, unstable_url }: Route.LoaderArgs) { + if (unstable_url.pathname === "/path") { + // This will always have the `.data` suffix stripped + } + + // And now you can distinguish between document versus data requests + let isDataRequest = new URL(request.url).pathname.endsWith(".data"); + } + ``` + +- Internal refactor to consolidate framework-agnostic/React-specific route type layers - no public API changes ([#14765](https://github.com/remix-run/react-router/pull/14765)) +- Sync protocol validation to rsc flows ([#14882](https://github.com/remix-run/react-router/pull/14882)) +- Add a new `unstable_url: URL` parameter to route handler methods (`loader`, `action`, `middleware`, etc.) representing the normalized URL the application is navigating to or fetching, with React Router implementation details removed (`.data`suffix, `index`/`_routes` query params) ([#14775](https://github.com/remix-run/react-router/pull/14775)) + + This is being added alongside the new `future.unstable_passthroughRequests` future flag so that users still have a way to access the normalized URL when that flag is enabled and non-normalized `request`'s are being passed to your handlers. When adopting this flag, you will only need to start leveraging this new parameter if you are relying on the normalization of `request.url` in your application code. + + If you don't have the flag enabled, then `unstable_url` will match `request.url`. + ## 7.13.1 ### Patch Changes diff --git a/packages/react-router/package.json b/packages/react-router/package.json index b82ce73ec9..7931ddba9a 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -1,6 +1,6 @@ { "name": "react-router", - "version": "7.13.1", + "version": "7.13.2-pre.0", "description": "Declarative routing for React", "keywords": [ "react", From c604398969b5e9bfd75087ff8baca8845841cf5c Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 18 Mar 2026 13:31:58 -0400 Subject: [PATCH 28/33] Draft release notes --- CHANGELOG.md | 374 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 221 insertions(+), 153 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5bc2b8116..fada63f41f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,170 +13,176 @@ We manage release notes in this file instead of the paginated Github Releases Pa Table of Contents - [React Router Releases](#react-router-releases) - - [v7.13.1](#v7131) - - [What's Changed](#whats-changed) - - [URL Masking (unstable)](#url-masking-unstable) + - [v7.13.2](#v7132) + - [What's Changed](#whats-changed) + - [Pass-through Requests (unstable)](#pass-through-requests-unstable) + - [Route handlers/middleware `unstable_url` parameter](#route-handlersmiddleware-unstable_url-parameter) - [Patch Changes](#patch-changes) - [Unstable Changes](#unstable-changes) + - [v7.13.1](#v7131) + - [What's Changed](#whats-changed-1) + - [URL Masking (unstable)](#url-masking-unstable) + - [Patch Changes](#patch-changes-1) + - [Unstable Changes](#unstable-changes-1) - [v7.13.0](#v7130) - [Minor Changes](#minor-changes) - - [Patch Changes](#patch-changes-1) + - [Patch Changes](#patch-changes-2) - [v7.12.0](#v7120) - [Security Notice](#security-notice) - [Minor Changes](#minor-changes-1) - - [Patch Changes](#patch-changes-2) - - [Unstable Changes](#unstable-changes-1) + - [Patch Changes](#patch-changes-3) + - [Unstable Changes](#unstable-changes-2) - [v7.11.0](#v7110) - - [What's Changed](#whats-changed-1) + - [What's Changed](#whats-changed-2) - [`vite preview` Support](#vite-preview-support) - [Stabilized Client-side `onError`](#stabilized-client-side-onerror) - [Call-site Revalidation Opt-out (unstable)](#call-site-revalidation-opt-out-unstable) - [Minor Changes](#minor-changes-2) - - [Patch Changes](#patch-changes-3) - - [Unstable Changes](#unstable-changes-2) - - [v7.10.1](#v7101) - [Patch Changes](#patch-changes-4) + - [Unstable Changes](#unstable-changes-3) + - [v7.10.1](#v7101) + - [Patch Changes](#patch-changes-5) - [v7.10.0](#v7100) - - [What's Changed](#whats-changed-2) + - [What's Changed](#whats-changed-3) - [Stabilized `future.v8_splitRouteModules`](#stabilized-futurev8_splitroutemodules) - [Stabilized `future.v8_viteEnvironmentApi`](#stabilized-futurev8_viteenvironmentapi) - [Stabilized `fetcher.reset()`](#stabilized-fetcherreset) - [Stabilized `DataStrategyMatch.shouldCallHandler()`](#stabilized-datastrategymatchshouldcallhandler) - [Minor Changes](#minor-changes-3) - - [Patch Changes](#patch-changes-5) - - [Unstable Changes](#unstable-changes-3) - - [v7.9.6](#v796) - - [Security Notice](#security-notice-1) - [Patch Changes](#patch-changes-6) - [Unstable Changes](#unstable-changes-4) - - [v7.9.5](#v795) - - [What's Changed](#whats-changed-3) - - [Instrumentation (unstable)](#instrumentation-unstable) + - [v7.9.6](#v796) + - [Security Notice](#security-notice-1) - [Patch Changes](#patch-changes-7) - [Unstable Changes](#unstable-changes-5) - - [v7.9.4](#v794) - - [Security Notice](#security-notice-2) + - [v7.9.5](#v795) - [What's Changed](#whats-changed-4) - - [`useRoute()` (unstable)](#useroute-unstable) + - [Instrumentation (unstable)](#instrumentation-unstable) - [Patch Changes](#patch-changes-8) - [Unstable Changes](#unstable-changes-6) - - [v7.9.3](#v793) + - [v7.9.4](#v794) + - [Security Notice](#security-notice-2) + - [What's Changed](#whats-changed-5) + - [`useRoute()` (unstable)](#useroute-unstable) - [Patch Changes](#patch-changes-9) + - [Unstable Changes](#unstable-changes-7) + - [v7.9.3](#v793) + - [Patch Changes](#patch-changes-10) - [v7.9.2](#v792) - - [What's Changed](#whats-changed-5) + - [What's Changed](#whats-changed-6) - [RSC Framework Mode (unstable)](#rsc-framework-mode-unstable) - [Fetcher Reset (unstable)](#fetcher-reset-unstable) - - [Patch Changes](#patch-changes-10) - - [Unstable Changes](#unstable-changes-7) - - [v7.9.1](#v791) - [Patch Changes](#patch-changes-11) + - [Unstable Changes](#unstable-changes-8) + - [v7.9.1](#v791) + - [Patch Changes](#patch-changes-12) - [v7.9.0](#v790) - [Security Notice](#security-notice-3) - - [What's Changed](#whats-changed-6) + - [What's Changed](#whats-changed-7) - [Stable Middleware and Context APIs](#stable-middleware-and-context-apis) - [Minor Changes](#minor-changes-4) - - [Patch Changes](#patch-changes-12) - - [Unstable Changes](#unstable-changes-8) - - [v7.8.2](#v782) - [Patch Changes](#patch-changes-13) - [Unstable Changes](#unstable-changes-9) - - [v7.8.1](#v781) + - [v7.8.2](#v782) - [Patch Changes](#patch-changes-14) - [Unstable Changes](#unstable-changes-10) + - [v7.8.1](#v781) + - [Patch Changes](#patch-changes-15) + - [Unstable Changes](#unstable-changes-11) - [v7.8.0](#v780) - - [What's Changed](#whats-changed-7) + - [What's Changed](#whats-changed-8) - [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-5) - - [Patch Changes](#patch-changes-15) - - [Unstable Changes](#unstable-changes-11) - - [Changes by Package](#changes-by-package) - - [v7.7.1](#v771) - [Patch Changes](#patch-changes-16) - [Unstable Changes](#unstable-changes-12) + - [Changes by Package](#changes-by-package) + - [v7.7.1](#v771) + - [Patch Changes](#patch-changes-17) + - [Unstable Changes](#unstable-changes-13) - [v7.7.0](#v770) - - [What's Changed](#whats-changed-8) + - [What's Changed](#whats-changed-9) - [Unstable RSC APIs](#unstable-rsc-apis) - [Minor Changes](#minor-changes-6) - - [Patch Changes](#patch-changes-17) - - [Unstable Changes](#unstable-changes-13) + - [Patch Changes](#patch-changes-18) + - [Unstable Changes](#unstable-changes-14) - [Changes by Package](#changes-by-package-1) - [v7.6.3](#v763) - - [Patch Changes](#patch-changes-18) - - [v7.6.2](#v762) - [Patch Changes](#patch-changes-19) - - [v7.6.1](#v761) + - [v7.6.2](#v762) - [Patch Changes](#patch-changes-20) - - [Unstable Changes](#unstable-changes-14) + - [v7.6.1](#v761) + - [Patch Changes](#patch-changes-21) + - [Unstable Changes](#unstable-changes-15) - [v7.6.0](#v760) - - [What's Changed](#whats-changed-9) + - [What's Changed](#whats-changed-10) - [`routeDiscovery` Config Option](#routediscovery-config-option) - [Automatic Types for Future Flags](#automatic-types-for-future-flags) - [Minor Changes](#minor-changes-7) - - [Patch Changes](#patch-changes-21) - - [Unstable Changes](#unstable-changes-15) + - [Patch Changes](#patch-changes-22) + - [Unstable Changes](#unstable-changes-16) - [Changes by Package](#changes-by-package-2) - [v7.5.3](#v753) - - [Patch Changes](#patch-changes-22) + - [Patch Changes](#patch-changes-23) - [v7.5.2](#v752) - [Security Notice](#security-notice-4) - - [Patch Changes](#patch-changes-23) - - [v7.5.1](#v751) - [Patch Changes](#patch-changes-24) - - [Unstable Changes](#unstable-changes-16) + - [v7.5.1](#v751) + - [Patch Changes](#patch-changes-25) + - [Unstable Changes](#unstable-changes-17) - [v7.5.0](#v750) - - [What's Changed](#whats-changed-10) + - [What's Changed](#whats-changed-11) - [`route.lazy` Object API](#routelazy-object-api) - [Minor Changes](#minor-changes-8) - - [Patch Changes](#patch-changes-25) - - [Unstable Changes](#unstable-changes-17) + - [Patch Changes](#patch-changes-26) + - [Unstable Changes](#unstable-changes-18) - [Changes by Package](#changes-by-package-3) - [v7.4.1](#v741) - [Security Notice](#security-notice-5) - - [Patch Changes](#patch-changes-26) - - [Unstable Changes](#unstable-changes-18) - - [v7.4.0](#v740) - - [Minor Changes](#minor-changes-9) - [Patch Changes](#patch-changes-27) - [Unstable Changes](#unstable-changes-19) + - [v7.4.0](#v740) + - [Minor Changes](#minor-changes-9) + - [Patch Changes](#patch-changes-28) + - [Unstable Changes](#unstable-changes-20) - [Changes by Package](#changes-by-package-4) - [v7.3.0](#v730) - [Minor Changes](#minor-changes-10) - - [Patch Changes](#patch-changes-28) - - [Unstable Changes](#unstable-changes-20) + - [Patch Changes](#patch-changes-29) + - [Unstable Changes](#unstable-changes-21) - [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-11) + - [What's Changed](#whats-changed-12) - [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-11) - - [Patch Changes](#patch-changes-29) - - [Unstable Changes](#unstable-changes-21) + - [Patch Changes](#patch-changes-30) + - [Unstable Changes](#unstable-changes-22) - [Split Route Modules (unstable)](#split-route-modules-unstable) - [Changes by Package](#changes-by-package-6) - [v7.1.5](#v715) - - [Patch Changes](#patch-changes-30) - - [v7.1.4](#v714) - [Patch Changes](#patch-changes-31) - - [v7.1.3](#v713) + - [v7.1.4](#v714) - [Patch Changes](#patch-changes-32) - - [v7.1.2](#v712) + - [v7.1.3](#v713) - [Patch Changes](#patch-changes-33) - - [v7.1.1](#v711) + - [v7.1.2](#v712) - [Patch Changes](#patch-changes-34) + - [v7.1.1](#v711) + - [Patch Changes](#patch-changes-35) - [v7.1.0](#v710) - [Minor Changes](#minor-changes-12) - - [Patch Changes](#patch-changes-35) + - [Patch Changes](#patch-changes-36) - [Changes by Package](#changes-by-package-7) - [v7.0.2](#v702) - - [Patch Changes](#patch-changes-36) - - [v7.0.1](#v701) - [Patch Changes](#patch-changes-37) + - [v7.0.1](#v701) + - [Patch Changes](#patch-changes-38) - [v7.0.0](#v700) - [Breaking Changes](#breaking-changes) - [Package Restructuring](#package-restructuring) @@ -193,207 +199,207 @@ 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-13) - - [Patch Changes](#patch-changes-38) + - [Patch Changes](#patch-changes-39) - [Changes by Package](#changes-by-package-8) - [React Router v6 Releases](#react-router-v6-releases) - [v6.30.3](#v6303) - [Security Notice](#security-notice-6) - - [Patch Changes](#patch-changes-39) + - [Patch Changes](#patch-changes-40) - [v6.30.2](#v6302) - [Security Notice](#security-notice-7) - - [Patch Changes](#patch-changes-40) - - [v6.30.1](#v6301) - [Patch Changes](#patch-changes-41) + - [v6.30.1](#v6301) + - [Patch Changes](#patch-changes-42) - [v6.30.0](#v6300) - [Minor Changes](#minor-changes-14) - - [Patch Changes](#patch-changes-42) + - [Patch Changes](#patch-changes-43) - [v6.29.0](#v6290) - [Minor Changes](#minor-changes-15) - - [Patch Changes](#patch-changes-43) - - [v6.28.2](#v6282) - [Patch Changes](#patch-changes-44) - - [v6.28.1](#v6281) + - [v6.28.2](#v6282) - [Patch Changes](#patch-changes-45) + - [v6.28.1](#v6281) + - [Patch Changes](#patch-changes-46) - [v6.28.0](#v6280) - - [What's Changed](#whats-changed-12) + - [What's Changed](#whats-changed-13) - [Minor Changes](#minor-changes-16) - - [Patch Changes](#patch-changes-46) + - [Patch Changes](#patch-changes-47) - [v6.27.0](#v6270) - - [What's Changed](#whats-changed-13) + - [What's Changed](#whats-changed-14) - [Stabilized APIs](#stabilized-apis) - [Minor Changes](#minor-changes-17) - - [Patch Changes](#patch-changes-47) - - [v6.26.2](#v6262) - [Patch Changes](#patch-changes-48) - - [v6.26.1](#v6261) + - [v6.26.2](#v6262) - [Patch Changes](#patch-changes-49) + - [v6.26.1](#v6261) + - [Patch Changes](#patch-changes-50) - [v6.26.0](#v6260) - [Minor Changes](#minor-changes-18) - - [Patch Changes](#patch-changes-50) - - [v6.25.1](#v6251) - [Patch Changes](#patch-changes-51) + - [v6.25.1](#v6251) + - [Patch Changes](#patch-changes-52) - [v6.25.0](#v6250) - - [What's Changed](#whats-changed-14) + - [What's Changed](#whats-changed-15) - [Stabilized `v7_skipActionErrorRevalidation`](#stabilized-v7_skipactionerrorrevalidation) - [Minor Changes](#minor-changes-19) - - [Patch Changes](#patch-changes-52) - - [v6.24.1](#v6241) - [Patch Changes](#patch-changes-53) + - [v6.24.1](#v6241) + - [Patch Changes](#patch-changes-54) - [v6.24.0](#v6240) - - [What's Changed](#whats-changed-15) + - [What's Changed](#whats-changed-16) - [Lazy Route Discovery (a.k.a. "Fog of War")](#lazy-route-discovery-aka-fog-of-war) - [Minor Changes](#minor-changes-20) - - [Patch Changes](#patch-changes-54) - - [v6.23.1](#v6231) - [Patch Changes](#patch-changes-55) + - [v6.23.1](#v6231) + - [Patch Changes](#patch-changes-56) - [v6.23.0](#v6230) - - [What's Changed](#whats-changed-16) + - [What's Changed](#whats-changed-17) - [Data Strategy (unstable)](#data-strategy-unstable) - [Skip Action Error Revalidation (unstable)](#skip-action-error-revalidation-unstable) - [Minor Changes](#minor-changes-21) - [v6.22.3](#v6223) - - [Patch Changes](#patch-changes-56) - - [v6.22.2](#v6222) - [Patch Changes](#patch-changes-57) - - [v6.22.1](#v6221) + - [v6.22.2](#v6222) - [Patch Changes](#patch-changes-58) + - [v6.22.1](#v6221) + - [Patch Changes](#patch-changes-59) - [v6.22.0](#v6220) - - [What's Changed](#whats-changed-17) + - [What's Changed](#whats-changed-18) - [Core Web Vitals Technology Report Flag](#core-web-vitals-technology-report-flag) - [Minor Changes](#minor-changes-22) - - [Patch Changes](#patch-changes-59) - - [v6.21.3](#v6213) - [Patch Changes](#patch-changes-60) - - [v6.21.2](#v6212) + - [v6.21.3](#v6213) - [Patch Changes](#patch-changes-61) - - [v6.21.1](#v6211) + - [v6.21.2](#v6212) - [Patch Changes](#patch-changes-62) + - [v6.21.1](#v6211) + - [Patch Changes](#patch-changes-63) - [v6.21.0](#v6210) - - [What's Changed](#whats-changed-18) + - [What's Changed](#whats-changed-19) - [`future.v7_relativeSplatPath`](#futurev7_relativesplatpath) - [Partial Hydration](#partial-hydration) - [Minor Changes](#minor-changes-23) - - [Patch Changes](#patch-changes-63) - - [v6.20.1](#v6201) - [Patch Changes](#patch-changes-64) + - [v6.20.1](#v6201) + - [Patch Changes](#patch-changes-65) - [v6.20.0](#v6200) - [Minor Changes](#minor-changes-24) - - [Patch Changes](#patch-changes-65) + - [Patch Changes](#patch-changes-66) - [v6.19.0](#v6190) - - [What's Changed](#whats-changed-19) + - [What's Changed](#whats-changed-20) - [`unstable_flushSync` API](#unstable_flushsync-api) - [Minor Changes](#minor-changes-25) - - [Patch Changes](#patch-changes-66) + - [Patch Changes](#patch-changes-67) - [v6.18.0](#v6180) - - [What's Changed](#whats-changed-20) + - [What's Changed](#whats-changed-21) - [New Fetcher APIs](#new-fetcher-apis) - [Persistence Future Flag (`future.v7_fetcherPersist`)](#persistence-future-flag-futurev7_fetcherpersist) - [Minor Changes](#minor-changes-26) - - [Patch Changes](#patch-changes-67) + - [Patch Changes](#patch-changes-68) - [v6.17.0](#v6170) - - [What's Changed](#whats-changed-21) + - [What's Changed](#whats-changed-22) - [View Transitions 🚀](#view-transitions-) - [Minor Changes](#minor-changes-27) - - [Patch Changes](#patch-changes-68) + - [Patch Changes](#patch-changes-69) - [v6.16.0](#v6160) - [Minor Changes](#minor-changes-28) - - [Patch Changes](#patch-changes-69) + - [Patch Changes](#patch-changes-70) - [v6.15.0](#v6150) - [Minor Changes](#minor-changes-29) - - [Patch Changes](#patch-changes-70) - - [v6.14.2](#v6142) - [Patch Changes](#patch-changes-71) - - [v6.14.1](#v6141) + - [v6.14.2](#v6142) - [Patch Changes](#patch-changes-72) + - [v6.14.1](#v6141) + - [Patch Changes](#patch-changes-73) - [v6.14.0](#v6140) - - [What's Changed](#whats-changed-22) + - [What's Changed](#whats-changed-23) - [JSON/Text Submissions](#jsontext-submissions) - [Minor Changes](#minor-changes-30) - - [Patch Changes](#patch-changes-73) + - [Patch Changes](#patch-changes-74) - [v6.13.0](#v6130) - - [What's Changed](#whats-changed-23) + - [What's Changed](#whats-changed-24) - [`future.v7_startTransition`](#futurev7_starttransition) - [Minor Changes](#minor-changes-31) - - [Patch Changes](#patch-changes-74) - - [v6.12.1](#v6121) - [Patch Changes](#patch-changes-75) + - [v6.12.1](#v6121) + - [Patch Changes](#patch-changes-76) - [v6.12.0](#v6120) - - [What's Changed](#whats-changed-24) + - [What's Changed](#whats-changed-25) - [`React.startTransition` support](#reactstarttransition-support) - [Minor Changes](#minor-changes-32) - - [Patch Changes](#patch-changes-76) - - [v6.11.2](#v6112) - [Patch Changes](#patch-changes-77) - - [v6.11.1](#v6111) + - [v6.11.2](#v6112) - [Patch Changes](#patch-changes-78) + - [v6.11.1](#v6111) + - [Patch Changes](#patch-changes-79) - [v6.11.0](#v6110) - [Minor Changes](#minor-changes-33) - - [Patch Changes](#patch-changes-79) + - [Patch Changes](#patch-changes-80) - [v6.10.0](#v6100) - - [What's Changed](#whats-changed-25) + - [What's Changed](#whats-changed-26) - [Minor Changes](#minor-changes-34) - [`future.v7_normalizeFormMethod`](#futurev7_normalizeformmethod) - - [Patch Changes](#patch-changes-80) + - [Patch Changes](#patch-changes-81) - [v6.9.0](#v690) - - [What's Changed](#whats-changed-26) + - [What's Changed](#whats-changed-27) - [`Component`/`ErrorBoundary` route properties](#componenterrorboundary-route-properties) - [Introducing Lazy Route Modules](#introducing-lazy-route-modules) - [Minor Changes](#minor-changes-35) - - [Patch Changes](#patch-changes-81) - - [v6.8.2](#v682) - [Patch Changes](#patch-changes-82) - - [v6.8.1](#v681) + - [v6.8.2](#v682) - [Patch Changes](#patch-changes-83) + - [v6.8.1](#v681) + - [Patch Changes](#patch-changes-84) - [v6.8.0](#v680) - [Minor Changes](#minor-changes-36) - - [Patch Changes](#patch-changes-84) + - [Patch Changes](#patch-changes-85) - [v6.7.0](#v670) - [Minor Changes](#minor-changes-37) - - [Patch Changes](#patch-changes-85) - - [v6.6.2](#v662) - [Patch Changes](#patch-changes-86) - - [v6.6.1](#v661) + - [v6.6.2](#v662) - [Patch Changes](#patch-changes-87) + - [v6.6.1](#v661) + - [Patch Changes](#patch-changes-88) - [v6.6.0](#v660) - - [What's Changed](#whats-changed-27) + - [What's Changed](#whats-changed-28) - [Minor Changes](#minor-changes-38) - - [Patch Changes](#patch-changes-88) + - [Patch Changes](#patch-changes-89) - [v6.5.0](#v650) - - [What's Changed](#whats-changed-28) + - [What's Changed](#whats-changed-29) - [Minor Changes](#minor-changes-39) - - [Patch Changes](#patch-changes-89) - - [v6.4.5](#v645) - [Patch Changes](#patch-changes-90) - - [v6.4.4](#v644) + - [v6.4.5](#v645) - [Patch Changes](#patch-changes-91) - - [v6.4.3](#v643) + - [v6.4.4](#v644) - [Patch Changes](#patch-changes-92) - - [v6.4.2](#v642) + - [v6.4.3](#v643) - [Patch Changes](#patch-changes-93) - - [v6.4.1](#v641) + - [v6.4.2](#v642) - [Patch Changes](#patch-changes-94) + - [v6.4.1](#v641) + - [Patch Changes](#patch-changes-95) - [v6.4.0](#v640) - - [What's Changed](#whats-changed-29) + - [What's Changed](#whats-changed-30) - [Remix Data APIs](#remix-data-apis) - - [Patch Changes](#patch-changes-95) + - [Patch Changes](#patch-changes-96) - [v6.3.0](#v630) - [Minor Changes](#minor-changes-40) - [v6.2.2](#v622) - - [Patch Changes](#patch-changes-96) - - [v6.2.1](#v621) - [Patch Changes](#patch-changes-97) + - [v6.2.1](#v621) + - [Patch Changes](#patch-changes-98) - [v6.2.0](#v620) - [Minor Changes](#minor-changes-41) - - [Patch Changes](#patch-changes-98) - - [v6.1.1](#v611) - [Patch Changes](#patch-changes-99) + - [v6.1.1](#v611) + - [Patch Changes](#patch-changes-100) - [v6.1.0](#v610) - [Minor Changes](#minor-changes-42) - - [Patch Changes](#patch-changes-100) - - [v6.0.2](#v602) - [Patch Changes](#patch-changes-101) - - [v6.0.1](#v601) + - [v6.0.2](#v602) - [Patch Changes](#patch-changes-102) + - [v6.0.1](#v601) + - [Patch Changes](#patch-changes-103) - [v6.0.0](#v600) @@ -421,6 +427,68 @@ 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.13.2 + +Date: 2026-03-19 + +### What's Changed + +#### Pass-through Requests (unstable) + +By default, React Router normalizes the `request.url` passed to your `loader`, `action`, and `middleware` functions by removing React Router's internal implementation details (`.data` suffixes, `index` + `_routes` query params). This release introduces a new `future.unstable_passThroughRequests` flag to disable this normalization and pass the raw HTTP `request` instance to your handlers. + +In addition to reducing server-side overhead by eliminating multiple `new Request()` calls on the critical path, this also provides additional visibility to your route handlers/instrumentations allowing you to differentiate document from data requests. + +If you were previously relying on the normalization of `request.url`, you can switch to use the new sibling `unstable_url` parameter which contains a `URL` instance representing the normalized location: + +```tsx +// ❌ Before: you could assume there was no `.data` suffix in `request.url` +export async function loader({ request }: Route.LoaderArgs) { + let url = new URL(request.url); + if (url.pathname === "/path") { + // This check will fail with the flag enabled because the `.data` suffix will + // exist on data requests + } +} + +// ✅ After: use `unstable_url` for normalized routing logic and `request.url` +// for raw routing logic +export async function loader({ request, unstable_url }: Route.LoaderArgs) { + if (unstable_url.pathname === "/path") { + // This will always have the `.data` suffix stripped + } + + // And now you can distinguish between document versus data requests + let isDataRequest = new URL(request.url).pathname.endsWith(".data"); +} +``` + +#### Route handlers/middleware `unstable_url` parameter + +We have added a new `unstable_url: URL` parameter to route handler methods (`loader`, `action`, `middleware`, etc.) that contains the normalized URL the application is navigating to or fetching with React Router implementation details removed (`.data`suffix, `index`/`_routes` query params). + +This parameter is primarily needed when adopting the new `future.unstable_passthroughRequests` future flag as a way to continue accessing the normalized URL. If you don't have the flag enabled, then `unstable_url` will match `request.url`. + +### Patch Changes + +- `react-router` - Fix `clientLoader.hydrate` when an ancestor route is also hydrating a `clientLoader` ([#14835](https://github.com/remix-run/react-router/pull/14835)) +- `react-router` - Fix type error when passing Framework Mode route components using `Route.ComponentProps` to `createRoutesStub` ([#14892](https://github.com/remix-run/react-router/pull/14892)) +- `react-router` - Fix percent encoding in relative path navigation ([#14786](https://github.com/remix-run/react-router/pull/14786)) +- `react-router` - Internal refactor to consolidate framework-agnostic/React-specific route type layers - no public API changes ([#14765](https://github.com/remix-run/react-router/pull/14765)) +- `@react-router/dev` - Fix `react-router dev` crash when Unix socket files exist in the project root ([#14854](https://github.com/remix-run/react-router/pull/14854)) +- `@react-router/dev` - Escape redirect locations in pre-rendered redirect HTML ([#14880](https://github.com/remix-run/react-router/pull/14880)) +- `create-react-router` - replace `chalk` with `picocolors` ([#14837](https://github.com/remix-run/react-router/pull/14837)) + +### Unstable Changes + +⚠️ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_ + +- `react-router` - Sync protocol validation to RSC flows ([#14882](https://github.com/remix-run/react-router/pull/14882)) +- `react-router` - Add `future.unstable_passThroughRequests` flag ([#14775](https://github.com/remix-run/react-router/pull/14775)) +- `react-router` - Add a new `unstable_url: URL` parameter to route handler methods (`loader`, `action`, `middleware`, etc.) representing the normalized URL the application is navigating to or fetching, with React Router implementation details removed (`.data`suffix, `index`/`_routes` query params) ([#14775](https://github.com/remix-run/react-router/pull/14775)) + +**Full Changelog**: [`v7.13.1...v7.13.2`](https://github.com/remix-run/react-router/compare/react-router@7.13.1...react-router@7.13.2) + ## v7.13.1 Date: 2026-02-23 From 5096d6974ed113e0e45de3f94f0a19de9a0a381a Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 23 Mar 2026 09:40:08 -0400 Subject: [PATCH 29/33] Update release notes --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fada63f41f..5ca0f73844 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -429,7 +429,7 @@ Date: YYYY-MM-DD ## v7.13.2 -Date: 2026-03-19 +Date: 2026-03-23 ### What's Changed From a71c922f6d5564f0d970d09e32752f4cbdd8aed6 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 23 Mar 2026 09:40:20 -0400 Subject: [PATCH 30/33] 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 fa93d8df80..0d584e1ae1 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 aadb56fa532e0eaf7e7b91c1d88e1f325851eb04 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 09:48:59 -0400 Subject: [PATCH 31/33] chore: Update version for release (#14908) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/cold-schools-relate.md | 5 -- .../fix-createRoutesStub-component-type.md | 5 -- .changeset/fix-dev-socket-file-crash.md | 5 -- .changeset/gentle-doors-visit.md | 5 -- .changeset/kind-shirts-turn.md | 5 -- .changeset/passthrough-reqeusts.md | 37 -------------- .changeset/pre.json | 50 ------------------- .changeset/remove-agnostic-types.md | 5 -- .changeset/sweet-houses-kick.md | 5 -- .changeset/twelve-snails-wait.md | 5 -- .changeset/unstable-url.md | 10 ---- integration/CHANGELOG.md | 1 + packages/create-react-router/CHANGELOG.md | 2 +- 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 | 33 ++++++++---- 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 | 41 +++++++++++---- packages/react-router/package.json | 2 +- 34 files changed, 99 insertions(+), 191 deletions(-) delete mode 100644 .changeset/cold-schools-relate.md delete mode 100644 .changeset/fix-createRoutesStub-component-type.md delete mode 100644 .changeset/fix-dev-socket-file-crash.md delete mode 100644 .changeset/gentle-doors-visit.md delete mode 100644 .changeset/kind-shirts-turn.md delete mode 100644 .changeset/passthrough-reqeusts.md delete mode 100644 .changeset/pre.json delete mode 100644 .changeset/remove-agnostic-types.md delete mode 100644 .changeset/sweet-houses-kick.md delete mode 100644 .changeset/twelve-snails-wait.md delete mode 100644 .changeset/unstable-url.md diff --git a/.changeset/cold-schools-relate.md b/.changeset/cold-schools-relate.md deleted file mode 100644 index d45d6f8fb1..0000000000 --- a/.changeset/cold-schools-relate.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"react-router": patch ---- - -Fix clientLoader.hydrate when an ancestor route is also hydrating a clientLoader diff --git a/.changeset/fix-createRoutesStub-component-type.md b/.changeset/fix-createRoutesStub-component-type.md deleted file mode 100644 index 2c604a85b7..0000000000 --- a/.changeset/fix-createRoutesStub-component-type.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"react-router": patch ---- - -Fix type error when passing Framework Mode route components using `Route.ComponentProps` to `createRoutesStub` diff --git a/.changeset/fix-dev-socket-file-crash.md b/.changeset/fix-dev-socket-file-crash.md deleted file mode 100644 index f33ce09bcb..0000000000 --- a/.changeset/fix-dev-socket-file-crash.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@react-router/dev": patch ---- - -Fix `react-router dev` crash when Unix socket files exist in the project root diff --git a/.changeset/gentle-doors-visit.md b/.changeset/gentle-doors-visit.md deleted file mode 100644 index c35f888597..0000000000 --- a/.changeset/gentle-doors-visit.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"react-router": patch ---- - -Fix percent encoding in relative path navigation diff --git a/.changeset/kind-shirts-turn.md b/.changeset/kind-shirts-turn.md deleted file mode 100644 index dc26b6af24..0000000000 --- a/.changeset/kind-shirts-turn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@react-router/dev": patch ---- - -Escape redirect locations in prerendered redirect HTML diff --git a/.changeset/passthrough-reqeusts.md b/.changeset/passthrough-reqeusts.md deleted file mode 100644 index a466f7ec0e..0000000000 --- a/.changeset/passthrough-reqeusts.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -"@react-router/dev": patch -"react-router": patch ---- - -Add `future.unstable_passThroughRequests` flag - -By default, React Router normalizes the `request.url` passed to your `loader`, `action`, and `middleware` functions by removing React Router's internal implementation details (`.data` suffixes, `index` + `_routes` query params). - -Enabling this flag removes that normalization and passes the raw HTTP `request` instance to your handlers. This provides a few benefits: - -- Reduces server-side overhead by eliminating multiple `new Request()` calls on the critical path -- Allows you to distinguish document from data requests in your handlers base don the presence of a `.data` suffix (useful for observability purposes) - -If you were previously relying on the normalization of `request.url`, you can switch to use the new sibling `unstable_url` parameter which contains a `URL` instance representing the normalized location: - -```tsx -// ❌ Before: you could assume there was no `.data` suffix in `request.url` -export async function loader({ request }: Route.LoaderArgs) { - let url = new URL(request.url); - if (url.pathname === "/path") { - // This check will fail with the flag enabled because the `.data` suffix will - // exist on data requests - } -} - -// ✅ After: use `unstable_url` for normalized routing logic and `request.url` -// for raw routing logic -export async function loader({ request, unstable_url }: Route.LoaderArgs) { - if (unstable_url.pathname === "/path") { - // This will always have the `.data` suffix stripped - } - - // And now you can distinguish between document versus data requests - let isDataRequest = new URL(request.url).pathname.endsWith(".data"); -} -``` diff --git a/.changeset/pre.json b/.changeset/pre.json deleted file mode 100644 index 0d584e1ae1..0000000000 --- a/.changeset/pre.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "mode": "exit", - "tag": "pre", - "initialVersions": { - "integration": "0.0.0", - "integration-cloudflare-dev-proxy-template": "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.13.1", - "react-router": "7.13.1", - "@react-router/architect": "7.13.1", - "@react-router/cloudflare": "7.13.1", - "@react-router/dev": "7.13.1", - "react-router-dom": "7.13.1", - "@react-router/express": "7.13.1", - "@react-router/fs-routes": "7.13.1", - "@react-router/node": "7.13.1", - "@react-router/remix-routes-option-adapter": "7.13.1", - "@react-router/serve": "7.13.1", - "@playground/data": "0.0.0", - "@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-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": [ - "cold-schools-relate", - "fix-createRoutesStub-component-type", - "fix-dev-socket-file-crash", - "gentle-doors-visit", - "kind-shirts-turn", - "passthrough-reqeusts", - "remove-agnostic-types", - "sweet-houses-kick", - "twelve-snails-wait", - "unstable-url" - ] -} diff --git a/.changeset/remove-agnostic-types.md b/.changeset/remove-agnostic-types.md deleted file mode 100644 index 268c8f3fb5..0000000000 --- a/.changeset/remove-agnostic-types.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"react-router": patch ---- - -Internal refactor to consolidate framework-agnostic/React-specific route type layers - no public API changes diff --git a/.changeset/sweet-houses-kick.md b/.changeset/sweet-houses-kick.md deleted file mode 100644 index daeca90ffd..0000000000 --- a/.changeset/sweet-houses-kick.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"create-react-router": patch ---- - -chore: replace chalk with picocolors diff --git a/.changeset/twelve-snails-wait.md b/.changeset/twelve-snails-wait.md deleted file mode 100644 index 02bf3f77f6..0000000000 --- a/.changeset/twelve-snails-wait.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"react-router": patch ---- - -Sync protocol validation to rsc flows diff --git a/.changeset/unstable-url.md b/.changeset/unstable-url.md deleted file mode 100644 index 77b7d1ad12..0000000000 --- a/.changeset/unstable-url.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -"@react-router/dev": patch -"react-router": patch ---- - -Add a new `unstable_url: URL` parameter to route handler methods (`loader`, `action`, `middleware`, etc.) representing the normalized URL the application is navigating to or fetching, with React Router implementation details removed (`.data`suffix, `index`/`_routes` query params) - -This is being added alongside the new `future.unstable_passthroughRequests` future flag so that users still have a way to access the normalized URL when that flag is enabled and non-normalized `request`'s are being passed to your handlers. When adopting this flag, you will only need to start leveraging this new parameter if you are relying on the normalization of `request.url` in your application code. - -If you don't have the flag enabled, then `unstable_url` will match `request.url`. 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 cae5c8d6b2..4f7c02a71c 100644 --- a/packages/create-react-router/CHANGELOG.md +++ b/packages/create-react-router/CHANGELOG.md @@ -1,6 +1,6 @@ # `create-react-router` -## 7.13.2-pre.0 +## 7.13.2 ### Patch Changes diff --git a/packages/create-react-router/package.json b/packages/create-react-router/package.json index 806d81a411..8aa481a8a5 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.13.2-pre.0", + "version": "7.13.2", "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 33032786c6..7067f24c9c 100644 --- a/packages/react-router-architect/CHANGELOG.md +++ b/packages/react-router-architect/CHANGELOG.md @@ -1,12 +1,12 @@ # `@react-router/architect` -## 7.13.2-pre.0 +## 7.13.2 ### Patch Changes - Updated dependencies: - - `react-router@7.13.2-pre.0` - - `@react-router/node@7.13.2-pre.0` + - `react-router@7.13.2` + - `@react-router/node@7.13.2` ## 7.13.1 @@ -111,6 +111,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 @@ -334,6 +335,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` @@ -342,6 +344,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 ea9202f8f9..eee863494e 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.13.2-pre.0", + "version": "7.13.2", "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 3de75e1099..16deaeb33a 100644 --- a/packages/react-router-cloudflare/CHANGELOG.md +++ b/packages/react-router-cloudflare/CHANGELOG.md @@ -1,11 +1,11 @@ # `@react-router/cloudflare` -## 7.13.2-pre.0 +## 7.13.2 ### Patch Changes - Updated dependencies: - - `react-router@7.13.2-pre.0` + - `react-router@7.13.2` ## 7.13.1 @@ -98,6 +98,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 @@ -296,6 +297,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` @@ -304,6 +306,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 6d3763e081..6630574b8a 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.13.2-pre.0", + "version": "7.13.2", "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 171cc70d21..5d2cf4b88b 100644 --- a/packages/react-router-dev/CHANGELOG.md +++ b/packages/react-router-dev/CHANGELOG.md @@ -1,16 +1,19 @@ # `@react-router/dev` -## 7.13.2-pre.0 +## 7.13.2 ### Patch Changes - Fix `react-router dev` crash when Unix socket files exist in the project root ([#14854](https://github.com/remix-run/react-router/pull/14854)) + - Escape redirect locations in prerendered redirect HTML ([#14880](https://github.com/remix-run/react-router/pull/14880)) + - Add `future.unstable_passThroughRequests` flag ([#14775](https://github.com/remix-run/react-router/pull/14775)) By default, React Router normalizes the `request.url` passed to your `loader`, `action`, and `middleware` functions by removing React Router's internal implementation details (`.data` suffixes, `index` + `_routes` query params). Enabling this flag removes that normalization and passes the raw HTTP `request` instance to your handlers. This provides a few benefits: + - Reduces server-side overhead by eliminating multiple `new Request()` calls on the critical path - Allows you to distinguish document from data requests in your handlers base don the presence of a `.data` suffix (useful for observability purposes) @@ -45,9 +48,9 @@ If you don't have the flag enabled, then `unstable_url` will match `request.url`. - Updated dependencies: - - `react-router@7.13.2-pre.0` - - `@react-router/node@7.13.2-pre.0` - - `@react-router/serve@7.13.2-pre.0` + - `react-router@7.13.2` + - `@react-router/node@7.13.2` + - `@react-router/serve@7.13.2` ## 7.13.1 @@ -86,25 +89,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. @@ -362,6 +365,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 @@ -1104,6 +1108,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 @@ -1118,6 +1123,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) @@ -1317,6 +1323,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` @@ -1653,6 +1660,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 @@ -2056,6 +2064,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) @@ -2153,6 +2162,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 @@ -2423,6 +2433,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 f14ce676fb..071d382389 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.13.2-pre.0", + "version": "7.13.2", "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 9ea29e92ef..cc18319f99 100644 --- a/packages/react-router-dom/CHANGELOG.md +++ b/packages/react-router-dom/CHANGELOG.md @@ -1,11 +1,11 @@ # react-router-dom -## 7.13.2-pre.0 +## 7.13.2 ### Patch Changes - Updated dependencies: - - `react-router@7.13.2-pre.0` + - `react-router@7.13.2` ## 7.13.1 diff --git a/packages/react-router-dom/package.json b/packages/react-router-dom/package.json index 10bd5aae0c..78ff090a7a 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.13.2-pre.0", + "version": "7.13.2", "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 457466f26f..20f4677eaa 100644 --- a/packages/react-router-express/CHANGELOG.md +++ b/packages/react-router-express/CHANGELOG.md @@ -1,12 +1,12 @@ # `@react-router/express` -## 7.13.2-pre.0 +## 7.13.2 ### Patch Changes - Updated dependencies: - - `react-router@7.13.2-pre.0` - - `@react-router/node@7.13.2-pre.0` + - `react-router@7.13.2` + - `@react-router/node@7.13.2` ## 7.13.1 @@ -111,6 +111,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 9a43a2a13b..74cf7546dd 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.13.2-pre.0", + "version": "7.13.2", "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 6000d21fff..430d52e93d 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.13.2-pre.0 +## 7.13.2 ### Patch Changes - Updated dependencies: - - `@react-router/dev@7.13.2-pre.0` + - `@react-router/dev@7.13.2` ## 7.13.1 diff --git a/packages/react-router-fs-routes/package.json b/packages/react-router-fs-routes/package.json index 8d56df5caf..a07087298d 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.13.2-pre.0", + "version": "7.13.2", "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 178fe460b2..f99a5dcb16 100644 --- a/packages/react-router-node/CHANGELOG.md +++ b/packages/react-router-node/CHANGELOG.md @@ -1,11 +1,11 @@ # `@react-router/node` -## 7.13.2-pre.0 +## 7.13.2 ### Patch Changes - Updated dependencies: - - `react-router@7.13.2-pre.0` + - `react-router@7.13.2` ## 7.13.1 @@ -99,6 +99,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 @@ -298,6 +299,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` @@ -306,6 +308,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` @@ -713,10 +716,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 0ee228f56b..503e94a189 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.13.2-pre.0", + "version": "7.13.2", "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 508bbc3ef8..b26491c899 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.13.2-pre.0 +## 7.13.2 ### Patch Changes - Updated dependencies: - - `@react-router/dev@7.13.2-pre.0` + - `@react-router/dev@7.13.2` ## 7.13.1 diff --git a/packages/react-router-remix-routes-option-adapter/package.json b/packages/react-router-remix-routes-option-adapter/package.json index 41ee7ab6ec..2228e95656 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.13.2-pre.0", + "version": "7.13.2", "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 a9820877d9..34dd0abf67 100644 --- a/packages/react-router-serve/CHANGELOG.md +++ b/packages/react-router-serve/CHANGELOG.md @@ -1,13 +1,13 @@ # `@react-router/serve` -## 7.13.2-pre.0 +## 7.13.2 ### Patch Changes - Updated dependencies: - - `react-router@7.13.2-pre.0` - - `@react-router/node@7.13.2-pre.0` - - `@react-router/express@7.13.2-pre.0` + - `react-router@7.13.2` + - `@react-router/node@7.13.2` + - `@react-router/express@7.13.2` ## 7.13.1 @@ -737,10 +737,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 b9a5200b7d..0b59a2e4a4 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.13.2-pre.0", + "version": "7.13.2", "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 66f55ffbf5..2f55fd94f7 100644 --- a/packages/react-router/CHANGELOG.md +++ b/packages/react-router/CHANGELOG.md @@ -1,17 +1,21 @@ # `react-router` -## 7.13.2-pre.0 +## 7.13.2 ### Patch Changes - Fix clientLoader.hydrate when an ancestor route is also hydrating a clientLoader ([#14835](https://github.com/remix-run/react-router/pull/14835)) + - Fix type error when passing Framework Mode route components using `Route.ComponentProps` to `createRoutesStub` ([#14892](https://github.com/remix-run/react-router/pull/14892)) + - Fix percent encoding in relative path navigation ([#14786](https://github.com/remix-run/react-router/pull/14786)) + - Add `future.unstable_passThroughRequests` flag ([#14775](https://github.com/remix-run/react-router/pull/14775)) By default, React Router normalizes the `request.url` passed to your `loader`, `action`, and `middleware` functions by removing React Router's internal implementation details (`.data` suffixes, `index` + `_routes` query params). Enabling this flag removes that normalization and passes the raw HTTP `request` instance to your handlers. This provides a few benefits: + - Reduces server-side overhead by eliminating multiple `new Request()` calls on the critical path - Allows you to distinguish document from data requests in your handlers base don the presence of a `.data` suffix (useful for observability purposes) @@ -40,7 +44,9 @@ ``` - Internal refactor to consolidate framework-agnostic/React-specific route type layers - no public API changes ([#14765](https://github.com/remix-run/react-router/pull/14765)) + - Sync protocol validation to rsc flows ([#14882](https://github.com/remix-run/react-router/pull/14882)) + - Add a new `unstable_url: URL` parameter to route handler methods (`loader`, `action`, `middleware`, etc.) representing the normalized URL the application is navigating to or fetching, with React Router implementation details removed (`.data`suffix, `index`/`_routes` query params) ([#14775](https://github.com/remix-run/react-router/pull/14775)) This is being added alongside the new `future.unstable_passthroughRequests` future flag so that users still have a way to access the normalized URL when that flag is enabled and non-normalized `request`'s are being passed to your handlers. When adopting this flag, you will only need to start leveraging this new parameter if you are relying on the normalization of `request.url` in your application code. @@ -59,9 +65,9 @@ - Fix matchPath optional params matching without a "/" separator. ([#14689](https://github.com/remix-run/react-router/pull/14689)) - matchPath("/users/:id?", "/usersblah") now returns null. - - matchPath("/test_route/:part?", "/test_route_more") now returns null. + - matchPath("/test\_route/:part?", "/test\_route\_more") now returns null. -- add RSC unstable_getRequest ([#14758](https://github.com/remix-run/react-router/pull/14758)) +- add RSC unstable\_getRequest ([#14758](https://github.com/remix-run/react-router/pull/14758)) - Fix `HydrateFallback` rendering during initial lazy route discovery with matching splat route ([#14740](https://github.com/remix-run/react-router/pull/14740)) @@ -107,6 +113,7 @@ ``` Notes: + - The masked location, if present, will be available on `useLocation().unstable_mask` so you can detect whether you are currently masked or not. - Masked URLs only work for SPA use cases, and will be removed from `history.state` during SSR. - This provides a first-class API to mask URLs in Data Mode to achieve the same behavior you could do in Declarative Mode via [manual `backgroundLocation` management](https://github.com/remix-run/react-router/tree/main/examples/modal). @@ -152,25 +159,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. @@ -197,12 +204,14 @@ - \[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 })` @@ -225,6 +234,7 @@ - ⚠️ 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: @@ -353,6 +363,7 @@ - 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`: `` @@ -514,6 +525,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 @@ -540,7 +552,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)) @@ -589,6 +601,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. @@ -622,6 +635,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` @@ -1267,6 +1281,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 - `` @@ -1454,6 +1469,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` @@ -1462,6 +1478,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` @@ -1617,6 +1634,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 @@ -1631,6 +1649,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 7931ddba9a..7cb1a1e999 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -1,6 +1,6 @@ { "name": "react-router", - "version": "7.13.2-pre.0", + "version": "7.13.2", "description": "Declarative routing for React", "keywords": [ "react", From 921db15de3b7026b992df08929488b68fe6b9885 Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Mon, 23 Mar 2026 13:50:16 +0000 Subject: [PATCH 32/33] 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 | 23 ++++--------- 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 | 34 ++++++------------- 8 files changed, 17 insertions(+), 55 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 7067f24c9c..db623847bd 100644 --- a/packages/react-router-architect/CHANGELOG.md +++ b/packages/react-router-architect/CHANGELOG.md @@ -111,7 +111,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 @@ -335,7 +334,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` @@ -344,7 +342,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 16deaeb33a..f94eba0164 100644 --- a/packages/react-router-cloudflare/CHANGELOG.md +++ b/packages/react-router-cloudflare/CHANGELOG.md @@ -98,7 +98,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 @@ -297,7 +296,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` @@ -306,7 +304,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 5d2cf4b88b..94ba71fd1c 100644 --- a/packages/react-router-dev/CHANGELOG.md +++ b/packages/react-router-dev/CHANGELOG.md @@ -13,7 +13,6 @@ By default, React Router normalizes the `request.url` passed to your `loader`, `action`, and `middleware` functions by removing React Router's internal implementation details (`.data` suffixes, `index` + `_routes` query params). Enabling this flag removes that normalization and passes the raw HTTP `request` instance to your handlers. This provides a few benefits: - - Reduces server-side overhead by eliminating multiple `new Request()` calls on the critical path - Allows you to distinguish document from data requests in your handlers base don the presence of a `.data` suffix (useful for observability purposes) @@ -89,25 +88,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. @@ -365,7 +364,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 @@ -1108,7 +1106,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 @@ -1123,7 +1120,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) @@ -1323,7 +1319,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` @@ -1660,7 +1655,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 @@ -2064,7 +2058,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) @@ -2162,7 +2155,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 @@ -2433,7 +2425,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 20f4677eaa..afcaecb26d 100644 --- a/packages/react-router-express/CHANGELOG.md +++ b/packages/react-router-express/CHANGELOG.md @@ -111,7 +111,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 f99a5dcb16..a95a36181d 100644 --- a/packages/react-router-node/CHANGELOG.md +++ b/packages/react-router-node/CHANGELOG.md @@ -99,7 +99,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 @@ -299,7 +298,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` @@ -308,7 +306,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` @@ -716,12 +713,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 34dd0abf67..91f64047b4 100644 --- a/packages/react-router-serve/CHANGELOG.md +++ b/packages/react-router-serve/CHANGELOG.md @@ -737,12 +737,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 2f55fd94f7..8a36ebe2be 100644 --- a/packages/react-router/CHANGELOG.md +++ b/packages/react-router/CHANGELOG.md @@ -15,7 +15,6 @@ By default, React Router normalizes the `request.url` passed to your `loader`, `action`, and `middleware` functions by removing React Router's internal implementation details (`.data` suffixes, `index` + `_routes` query params). Enabling this flag removes that normalization and passes the raw HTTP `request` instance to your handlers. This provides a few benefits: - - Reduces server-side overhead by eliminating multiple `new Request()` calls on the critical path - Allows you to distinguish document from data requests in your handlers base don the presence of a `.data` suffix (useful for observability purposes) @@ -65,9 +64,9 @@ - Fix matchPath optional params matching without a "/" separator. ([#14689](https://github.com/remix-run/react-router/pull/14689)) - matchPath("/users/:id?", "/usersblah") now returns null. - - matchPath("/test\_route/:part?", "/test\_route\_more") now returns null. + - matchPath("/test_route/:part?", "/test_route_more") now returns null. -- add RSC unstable\_getRequest ([#14758](https://github.com/remix-run/react-router/pull/14758)) +- add RSC unstable_getRequest ([#14758](https://github.com/remix-run/react-router/pull/14758)) - Fix `HydrateFallback` rendering during initial lazy route discovery with matching splat route ([#14740](https://github.com/remix-run/react-router/pull/14740)) @@ -113,7 +112,6 @@ ``` Notes: - - The masked location, if present, will be available on `useLocation().unstable_mask` so you can detect whether you are currently masked or not. - Masked URLs only work for SPA use cases, and will be removed from `history.state` during SSR. - This provides a first-class API to mask URLs in Data Mode to achieve the same behavior you could do in Declarative Mode via [manual `backgroundLocation` management](https://github.com/remix-run/react-router/tree/main/examples/modal). @@ -159,25 +157,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. @@ -204,14 +202,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 })` @@ -234,7 +230,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: @@ -363,7 +358,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`: `` @@ -525,7 +519,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 @@ -552,7 +545,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)) @@ -601,7 +594,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. @@ -635,7 +627,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` @@ -1281,7 +1272,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 - `` @@ -1469,7 +1459,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` @@ -1478,7 +1467,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` @@ -1634,7 +1622,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 @@ -1649,7 +1636,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 a11a013e580003cb175e6eb87205674396ccab38 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 23 Mar 2026 12:44:00 -0400 Subject: [PATCH 33/33] Copy preview workflow to main for manual workflow_dispatch usage --- .github/workflows/preview.yml | 78 +++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 .github/workflows/preview.yml diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml new file mode 100644 index 0000000000..40d45785d8 --- /dev/null +++ b/.github/workflows/preview.yml @@ -0,0 +1,78 @@ +# Create "installable" preview branches +# +# Commits to `dev` push builds to a `preview/dev` branch: +# pnpm install "remix-run/react-router#preview/dev&path:packages/react-router" +# +# Can also be dispatched manually with base/installable branches to provide +# `experimental` branches from PRs or otherwise. + +name: Preview Build + +on: + push: + branches: + - dev + workflow_dispatch: + inputs: + baseBranch: + description: Base Branch + required: true + installableBranch: + description: Installable Branch + required: true + +concurrency: + # Include `event_name` here because when a pull_request is merged (closed), the + # `github.ref` goes back to `ref/heads/dev` which will conflict with the run on + # `dev` from the merged PR + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + preview: + if: github.repository == 'remix-run/react-router' + runs-on: ubuntu-latest + steps: + - name: Checkout (push) + if: github.event_name == 'push' + uses: actions/checkout@v4 + + - name: Checkout (workflow_dispatch) + if: github.event_name == 'workflow_dispatch' + uses: actions/checkout@v4 + with: + ref: ${{ inputs.baseBranch }} + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version-file: "package.json" + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Setup git + run: | + git config --local user.email "hello@remix.run" + git config --local user.name "Remix Run Bot" + + # Build and force push over the preview/dev branch + - name: Build/push branch (push) + if: github.event_name == 'push' + run: | + pnpm run setup-installable-branch preview/dev + git push --force --set-upstream origin preview/dev + echo "💿 pushed installable branch: https://github.com/$GITHUB_REPOSITORY/commit/$(git rev-parse HEAD)" + + # Build and normal push for experimental releases to avoid unintended force + # pushes over remote branches in case of a branch name collision + - name: Build/push branch (workflow_dispatch) + if: github.event_name == 'workflow_dispatch' + run: | + pnpm run setup-installable-branch ${{ inputs.installableBranch }} + git push --set-upstream origin ${{ inputs.installableBranch }} + echo "💿 pushed installable branch: https://github.com/$GITHUB_REPOSITORY/commit/$(git rev-parse HEAD)"