From 5b25fcde7b1bfa4bb0db69c694fb3fb5f6737d7b Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 22 Dec 2025 11:11:19 -0500 Subject: [PATCH 01/28] POC of link rewrite --- .../__tests__/router/rewrite-test.ts | 309 ++++++++++++++++++ .../router/utils/data-router-setup.ts | 10 +- packages/react-router/lib/components.tsx | 12 +- packages/react-router/lib/context.ts | 2 + packages/react-router/lib/dom/lib.tsx | 9 + packages/react-router/lib/router/history.ts | 23 ++ packages/react-router/lib/router/router.ts | 105 ++++-- packages/react-router/lib/router/utils.ts | 2 +- 8 files changed, 436 insertions(+), 36 deletions(-) create mode 100644 packages/react-router/__tests__/router/rewrite-test.ts diff --git a/packages/react-router/__tests__/router/rewrite-test.ts b/packages/react-router/__tests__/router/rewrite-test.ts new file mode 100644 index 0000000000..7838709449 --- /dev/null +++ b/packages/react-router/__tests__/router/rewrite-test.ts @@ -0,0 +1,309 @@ +import { cleanup, setup } from "./utils/data-router-setup"; +import { createFormData } from "./utils/utils"; + +describe("rewrite", () => { + // Detect any failures inside the router navigate code + afterEach(() => cleanup()); + + it("loads data from the rewrite location but updates browser URL to the to location", async () => { + let t = setup({ + routes: [ + { path: "/" }, + { id: "gallery", path: "/gallery", loader: true }, + { id: "images", path: "/images/:id", loader: true }, + ], + initialEntries: ["/"], + }); + + // Navigate to /images/123 but load data from /gallery?photo=123 + let A = await t.navigate("/images/123", { + rewrite: "/gallery?photo=123", + }); + + // Should call the gallery loader, not the images loader + expect(A.loaders.gallery).toBeDefined(); + expect(A.loaders.images).toBeUndefined(); + + // The loader should receive the rewrite URL in the request + expect(A.loaders.gallery.stub.mock.calls[0][0].request.url).toMatch( + /\/gallery\?photo=123$/, + ); + + await A.loaders.gallery.resolve("GALLERY DATA"); + + // The browser location should be /images/123, not /gallery?photo=123 + expect(t.router.state.location.pathname).toBe("/images/123"); + expect(t.router.state.location.search).toBe(""); + + // The location state should contain the rewrite information + expect(t.router.state.location.state?._rewrite).toEqual({ + pathname: "/gallery", + search: "?photo=123", + hash: "", + }); + + // The matched routes should be for the rewrite location + expect(t.router.state.matches).toHaveLength(1); + expect(t.router.state.matches[0].route.id).toBe("gallery"); + + // The loader data should be present + expect(t.router.state.loaderData.gallery).toBe("GALLERY DATA"); + + t.router.dispose(); + }); + + it("preserves rewrite on POP navigation", async () => { + let t = setup({ + routes: [ + { path: "/" }, + { id: "gallery", path: "/gallery", loader: true }, + { id: "images", path: "/images/:id", loader: true }, + ], + initialEntries: ["/"], + }); + + // Navigate to /images/123 with rewrite + let A = await t.navigate("/images/123", { + rewrite: "/gallery?photo=123", + }); + await A.loaders.gallery.resolve("GALLERY DATA"); + + // Navigate to home + let B = await t.navigate("/"); + expect(t.router.state.location.pathname).toBe("/"); + + // Go back - should use the rewrite + let promise = t.router.navigate(-1); + + // Wait for navigation to complete + await promise; + + // The browser location should be /images/123 + expect(t.router.state.location.pathname).toBe("/images/123"); + + // The rewrite should still be in the state + expect(t.router.state.location.state?._rewrite).toEqual({ + pathname: "/gallery", + search: "?photo=123", + hash: "", + }); + + // The matched routes should be for the rewrite location + expect(t.router.state.matches).toHaveLength(1); + expect(t.router.state.matches[0].route.id).toBe("gallery"); + + t.router.dispose(); + }); + + it("supports rewrite with replace", async () => { + let t = setup({ + routes: [ + { path: "/" }, + { id: "gallery", path: "/gallery", loader: true }, + { id: "images", path: "/images/:id", loader: true }, + ], + initialEntries: ["/"], + }); + + let A = await t.navigate("/images/123", { + rewrite: "/gallery?photo=123", + replace: true, + }); + + expect(A.loaders.gallery).toBeDefined(); + await A.loaders.gallery.resolve("GALLERY DATA"); + + expect(t.router.state.location.pathname).toBe("/images/123"); + expect(t.router.state.location.state?._rewrite).toEqual({ + pathname: "/gallery", + search: "?photo=123", + hash: "", + }); + + t.router.dispose(); + }); + + it("supports rewrite with hash", async () => { + let t = setup({ + routes: [ + { path: "/" }, + { id: "gallery", path: "/gallery", loader: true }, + { id: "images", path: "/images/:id", loader: true }, + ], + initialEntries: ["/"], + }); + + let A = await t.navigate("/images/123#preview", { + rewrite: "/gallery?photo=123#header", + }); + + expect(A.loaders.gallery).toBeDefined(); + + // The loader should receive the rewrite URL (hash is not preserved in Request URL) + expect(A.loaders.gallery.stub.mock.calls[0][0].request.url).toMatch( + /\/gallery\?photo=123$/, + ); + + await A.loaders.gallery.resolve("GALLERY DATA"); + + // The browser location should have the "to" hash + expect(t.router.state.location.pathname).toBe("/images/123"); + expect(t.router.state.location.hash).toBe("#preview"); + + // The rewrite should have the rewrite hash + expect(t.router.state.location.state?._rewrite).toEqual({ + pathname: "/gallery", + search: "?photo=123", + hash: "#header", + }); + + t.router.dispose(); + }); + + it("supports rewrite with state", async () => { + let t = setup({ + routes: [ + { path: "/" }, + { id: "gallery", path: "/gallery", loader: true }, + { id: "images", path: "/images/:id", loader: true }, + ], + initialEntries: ["/"], + }); + + let A = await t.navigate("/images/123", { + rewrite: "/gallery?photo=123", + state: { customData: "test" }, + }); + + await A.loaders.gallery.resolve("GALLERY DATA"); + + // Both custom state and _rewrite should be present + expect(t.router.state.location.state).toEqual({ + customData: "test", + _rewrite: { + pathname: "/gallery", + search: "?photo=123", + hash: "", + }, + }); + + t.router.dispose(); + }); + + it("supports rewrite with action submission", async () => { + let t = setup({ + routes: [ + { path: "/" }, + { id: "gallery", path: "/gallery", action: true, loader: true }, + { id: "images", path: "/images/:id", action: true }, + ], + initialEntries: ["/"], + }); + + let A = await t.navigate("/images/123", { + rewrite: "/gallery?photo=123", + formMethod: "post", + formData: createFormData({ test: "value" }), + }); + + // Should call the gallery action, not the images action + expect(A.actions.gallery).toBeDefined(); + expect(A.actions.images).toBeUndefined(); + + // The action should receive the rewrite URL + expect(A.actions.gallery.stub.mock.calls[0][0].request.url).toMatch( + /\/gallery\?photo=123$/, + ); + + await A.actions.gallery.resolve("ACTION DATA"); + + // Should then call the gallery loader + expect(A.loaders.gallery).toBeDefined(); + await A.loaders.gallery.resolve("LOADER DATA"); + + // The browser location should be /images/123 + expect(t.router.state.location.pathname).toBe("/images/123"); + expect(t.router.state.location.state?._rewrite).toEqual({ + pathname: "/gallery", + search: "?photo=123", + hash: "", + }); + + t.router.dispose(); + }); + + it("handles rewrite with relative paths", async () => { + let t = setup({ + routes: [ + { + path: "/", + children: [ + { id: "gallery", path: "gallery", loader: true }, + { id: "images", path: "images/:id", loader: true }, + ], + }, + ], + initialEntries: ["/"], + }); + + let A = await t.navigate("/images/123", { + rewrite: "/gallery?photo=123", + }); + + expect(A.loaders.gallery).toBeDefined(); + await A.loaders.gallery.resolve("GALLERY DATA"); + + expect(t.router.state.location.pathname).toBe("/images/123"); + expect(t.router.state.matches[1].route.id).toBe("gallery"); + + t.router.dispose(); + }); + + it("works without rewrite option (normal navigation)", async () => { + let t = setup({ + routes: [ + { path: "/" }, + { id: "gallery", path: "/gallery", loader: true }, + { id: "images", path: "/images/:id", loader: true }, + ], + initialEntries: ["/"], + }); + + // Normal navigation without rewrite + let A = await t.navigate("/images/123"); + + // Should call the images loader + expect(A.loaders.images).toBeDefined(); + expect(A.loaders.gallery).toBeUndefined(); + + await A.loaders.images.resolve("IMAGES DATA"); + + expect(t.router.state.location.pathname).toBe("/images/123"); + expect(t.router.state.location.state?._rewrite).toBeUndefined(); + expect(t.router.state.matches).toHaveLength(1); + expect(t.router.state.matches[0].route.id).toBe("images"); + + t.router.dispose(); + }); + + it("handles 404 on rewrite location", async () => { + let t = setup({ + routes: [ + { path: "/" }, + { id: "images", path: "/images/:id", loader: true }, + ], + initialEntries: ["/"], + }); + + // Rewrite to a non-existent route + let A = await t.navigate("/images/123", { + rewrite: "/nonexistent", + }); + + // Should get a 404 error + expect(t.router.state.errors).toBeDefined(); + expect(t.router.state.location.pathname).toBe("/images/123"); + + t.router.dispose(); + }); +}); diff --git a/packages/react-router/__tests__/router/utils/data-router-setup.ts b/packages/react-router/__tests__/router/utils/data-router-setup.ts index df2c2c2145..1e9e3dad91 100644 --- a/packages/react-router/__tests__/router/utils/data-router-setup.ts +++ b/packages/react-router/__tests__/router/utils/data-router-setup.ts @@ -604,7 +604,15 @@ export function setup({ return helpers; } - helpers = getNavigationHelpers(href, navigationId); + helpers = getNavigationHelpers( + // @ts-expect-error - rewrite is a new option + opts?.rewrite + ? typeof opts.rewrite === "string" + ? opts.rewrite + : "" + : href, + navigationId, + ); shims?.forEach((routeId) => shimHelper(helpers.loaders, "navigation", "loader", routeId), ); diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx index c7b9cdf8b2..d859f6c880 100644 --- a/packages/react-router/lib/components.tsx +++ b/packages/react-router/lib/components.tsx @@ -724,6 +724,16 @@ export function RouterProvider({ [router, navigator, basename, onError], ); + let uiLocation = state.location; + if (state.location.state?._rewrite) { + uiLocation = { + ...state.location, + pathname: state.location.state._rewrite.pathname, + search: state.location.state._rewrite.search, + hash: state.location.state._rewrite.hash, + }; + } + // The fragment and {null} here are important! We need them to keep React 18's // useId happy when we are server-rendering since we may have a + + diff --git a/playground/data/package.json b/playground/data/package.json new file mode 100644 index 0000000000..3a86597ed1 --- /dev/null +++ b/playground/data/package.json @@ -0,0 +1,23 @@ +{ + "name": "@playground/data", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "catalog:", + "react-dom": "catalog:", + "react-router": "workspace:" + }, + "devDependencies": { + "@types/react": "catalog:", + "@types/react-dom": "catalog:", + "@vitejs/plugin-react": "^4", + "typescript": "catalog:", + "vite": "6.4.1" + } +} diff --git a/playground/data/src/main.tsx b/playground/data/src/main.tsx new file mode 100644 index 0000000000..ca7b4bfbea --- /dev/null +++ b/playground/data/src/main.tsx @@ -0,0 +1,136 @@ +import * as React from "react"; +import * as ReactClient from "react-dom/client"; +import { + Link, + createBrowserRouter, + useLoaderData, + useParams, +} from "react-router"; +import { RouterProvider } from "react-router/dom"; + +type Photo = { + id: string; + url: string; + alt: string; +}; + +const PHOTOS: Record = { + "1": { + id: "1", + url: "https://placecats.com/300/200", + alt: "Tabby cat lounging", + }, + "2": { + id: "2", + url: "https://placecats.com/400/300", + alt: "Orange cat playing", + }, + "3": { + id: "3", + url: "https://placecats.com/350/250", + alt: "Black cat sleeping", + }, + "4": { + id: "4", + url: "https://placecats.com/500/400", + alt: "White cat sitting", + }, + "5": { + id: "5", + url: "https://placecats.com/450/350", + alt: "Grey cat looking", + }, +}; + +const router = createBrowserRouter([ + { + id: "index", + path: "/", + loader({ request }) { + let sp = new URL(request.url).searchParams; + let photo = sp.has("photo") ? getPhoto(sp.get("photo")!) : null; + console.log("returning from loader", photo); + return { photo }; + }, + Component() { + let data = useLoaderData() as { photo: Photo | null }; + console.log("data in component", data); + return ( + <> +

Hello React Router (data mode)

+ + {data.photo && ( + + + + )} + + ); + }, + }, + { + id: "image", + path: "/images/:id", + loader({ params }) { + if (!params.id || typeof params.id !== "string") { + throw new Error("Image ID is required"); + } + let photo = getPhoto(params.id); + return { photo }; + }, + Component() { + let data = useLoaderData() as { photo: Photo }; + return ( + <> +

Images Page

+ + + ); + }, + }, +]); + +window.router = router; + +function PhotoDisplay({ photo }: { photo: Photo }) { + return {photo.alt}; +} + +function ImageGallery() { + return ( +
+ {Object.values(PHOTOS).map((photo) => ( + + {photo.alt} + + ))} +
+ ); +} +function getPhoto(id: string): Photo { + if (!(id in PHOTOS)) { + throw new Error("Photo not found"); + } + return PHOTOS[id]; +} + +ReactClient.createRoot(document.getElementById("root")).render( + + + , +); diff --git a/playground/data/tsconfig.json b/playground/data/tsconfig.json new file mode 100644 index 0000000000..227a6c6723 --- /dev/null +++ b/playground/data/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/playground/data/vite.config.ts b/playground/data/vite.config.ts new file mode 100644 index 0000000000..0e43ae8def --- /dev/null +++ b/playground/data/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9a9419e03b..8ce5279d2e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -885,7 +885,7 @@ importers: version: 1.3.3 tsup: specifier: 'catalog:' - version: 8.5.0(@swc/core@1.11.24)(jiti@2.4.2)(postcss@8.5.3)(tsx@4.19.3)(typescript@5.4.5)(yaml@2.8.0) + version: 8.5.0(@swc/core@1.11.24)(jiti@2.4.2)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.4.5)(yaml@2.8.0) typescript: specifier: 'catalog:' version: 5.4.5 @@ -931,7 +931,7 @@ importers: version: 19.1.0(react@19.2.3) tsup: specifier: 'catalog:' - version: 8.5.0(@swc/core@1.11.24)(jiti@2.4.2)(postcss@8.5.3)(tsx@4.19.3)(typescript@5.4.5)(yaml@2.8.0) + version: 8.5.0(@swc/core@1.11.24)(jiti@2.4.2)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.4.5)(yaml@2.8.0) typescript: specifier: 'catalog:' version: 5.4.5 @@ -974,7 +974,7 @@ importers: version: link:../react-router tsup: specifier: 'catalog:' - version: 8.5.0(@swc/core@1.11.24)(jiti@2.4.2)(postcss@8.5.3)(tsx@4.19.3)(typescript@5.4.5)(yaml@2.8.0) + version: 8.5.0(@swc/core@1.11.24)(jiti@2.4.2)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.4.5)(yaml@2.8.0) typescript: specifier: 'catalog:' version: 5.4.5 @@ -992,7 +992,7 @@ importers: version: link:../react-router tsup: specifier: 'catalog:' - version: 8.5.0(@swc/core@1.11.24)(jiti@2.4.2)(postcss@8.5.3)(tsx@4.19.3)(typescript@5.4.5)(yaml@2.8.0) + version: 8.5.0(@swc/core@1.11.24)(jiti@2.4.2)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.4.5)(yaml@2.8.0) typescript: specifier: 'catalog:' version: 5.4.5 @@ -1076,7 +1076,7 @@ importers: version: 0.14.2 react-server-dom-webpack: specifier: 'catalog:' - version: 19.2.3(react-dom@19.3.0-canary-d763f313-20251210(react@19.3.0-canary-d763f313-20251210))(react@19.3.0-canary-d763f313-20251210)(webpack@5.103.0(@swc/core@1.11.24)(esbuild@0.25.4)) + version: 19.2.3(react-dom@19.3.0-canary-d763f313-20251210(react@19.3.0-canary-d763f313-20251210))(react@19.3.0-canary-d763f313-20251210)(webpack@5.103.0(@swc/core@1.11.24)(esbuild@0.27.2)) semver: specifier: ^7.3.7 version: 7.7.2 @@ -1128,10 +1128,10 @@ importers: version: 7.7.0 '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.7(react-dom@19.3.0-canary-d763f313-20251210(react@19.3.0-canary-d763f313-20251210))(react-server-dom-webpack@19.2.3(react-dom@19.3.0-canary-d763f313-20251210(react@19.3.0-canary-d763f313-20251210))(react@19.3.0-canary-d763f313-20251210)(webpack@5.103.0(@swc/core@1.11.24)(esbuild@0.25.4)))(react@19.3.0-canary-d763f313-20251210)(vite@6.4.1(@types/node@20.11.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.0)) + version: 0.5.7(react-dom@19.3.0-canary-d763f313-20251210(react@19.3.0-canary-d763f313-20251210))(react-server-dom-webpack@19.2.3(react-dom@19.3.0-canary-d763f313-20251210(react@19.3.0-canary-d763f313-20251210))(react@19.3.0-canary-d763f313-20251210)(webpack@5.103.0(@swc/core@1.11.24)(esbuild@0.27.2)))(react@19.3.0-canary-d763f313-20251210)(vite@6.4.1(@types/node@20.11.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.0)) esbuild-register: specifier: ^3.6.0 - version: 3.6.0(esbuild@0.25.4) + version: 3.6.0(esbuild@0.27.2) execa: specifier: 5.1.1 version: 5.1.1 @@ -1146,7 +1146,7 @@ importers: version: link:../react-router tsup: specifier: 'catalog:' - version: 8.5.0(@swc/core@1.11.24)(jiti@2.4.2)(postcss@8.5.3)(tsx@4.19.3)(typescript@5.4.5)(yaml@2.8.0) + version: 8.5.0(@swc/core@1.11.24)(jiti@2.4.2)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.4.5)(yaml@2.8.0) typescript: specifier: 'catalog:' version: 5.4.5 @@ -1174,7 +1174,7 @@ importers: version: 19.2.3(react@19.2.3) tsup: specifier: 'catalog:' - version: 8.5.0(@swc/core@1.11.24)(jiti@2.4.2)(postcss@8.5.3)(tsx@4.19.3)(typescript@5.4.5)(yaml@2.8.0) + version: 8.5.0(@swc/core@1.11.24)(jiti@2.4.2)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.4.5)(yaml@2.8.0) typescript: specifier: 'catalog:' version: 5.4.5 @@ -1211,7 +1211,7 @@ importers: version: 6.3.4 tsup: specifier: 'catalog:' - version: 8.5.0(@swc/core@1.11.24)(jiti@2.4.2)(postcss@8.5.3)(tsx@4.19.3)(typescript@5.4.5)(yaml@2.8.0) + version: 8.5.0(@swc/core@1.11.24)(jiti@2.4.2)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.4.5)(yaml@2.8.0) typescript: specifier: 'catalog:' version: 5.4.5 @@ -1230,7 +1230,7 @@ importers: version: link:../react-router-dev tsup: specifier: 'catalog:' - version: 8.5.0(@swc/core@1.11.24)(jiti@2.4.2)(postcss@8.5.3)(tsx@4.19.3)(typescript@5.4.5)(yaml@2.8.0) + version: 8.5.0(@swc/core@1.11.24)(jiti@2.4.2)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.4.5)(yaml@2.8.0) typescript: specifier: 'catalog:' version: 5.4.5 @@ -1249,7 +1249,7 @@ importers: version: link:../react-router tsup: specifier: 'catalog:' - version: 8.5.0(@swc/core@1.11.24)(jiti@2.4.2)(postcss@8.5.3)(tsx@4.19.3)(typescript@5.4.5)(yaml@2.8.0) + version: 8.5.0(@swc/core@1.11.24)(jiti@2.4.2)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.4.5)(yaml@2.8.0) typescript: specifier: 'catalog:' version: 5.4.5 @@ -1264,7 +1264,7 @@ importers: version: link:../react-router-dev tsup: specifier: 'catalog:' - version: 8.5.0(@swc/core@1.11.24)(jiti@2.4.2)(postcss@8.5.3)(tsx@4.19.3)(typescript@5.4.5)(yaml@2.8.0) + version: 8.5.0(@swc/core@1.11.24)(jiti@2.4.2)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.4.5)(yaml@2.8.0) typescript: specifier: 'catalog:' version: 5.4.5 @@ -1316,7 +1316,7 @@ importers: version: 0.5.10 tsup: specifier: 'catalog:' - version: 8.5.0(@swc/core@1.11.24)(jiti@2.4.2)(postcss@8.5.3)(tsx@4.19.3)(typescript@5.4.5)(yaml@2.8.0) + version: 8.5.0(@swc/core@1.11.24)(jiti@2.4.2)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.4.5)(yaml@2.8.0) typescript: specifier: 'catalog:' version: 5.4.5 @@ -1324,6 +1324,34 @@ importers: specifier: 'catalog:' version: 0.14.9 + playground/data: + dependencies: + react: + specifier: 'catalog:' + version: 19.2.3 + react-dom: + specifier: 'catalog:' + version: 19.2.3(react@19.2.3) + react-router: + specifier: 'workspace:' + version: link:../../packages/react-router + devDependencies: + '@types/react': + specifier: ^18.0.27 + version: 18.2.18 + '@types/react-dom': + specifier: ^18.0.10 + version: 18.2.7 + '@vitejs/plugin-react': + specifier: ^4 + version: 4.5.2(vite@6.4.1(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.0)) + typescript: + specifier: 'catalog:' + version: 5.4.5 + vite: + specifier: 6.4.1 + version: 6.4.1(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.0) + playground/framework: dependencies: '@react-router/node': @@ -2788,6 +2816,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.27.2': + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.19.12': resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} engines: {node: '>=12'} @@ -2806,6 +2840,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.27.2': + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.19.12': resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} engines: {node: '>=12'} @@ -2824,6 +2864,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.27.2': + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.19.12': resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} engines: {node: '>=12'} @@ -2842,6 +2888,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.27.2': + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.19.12': resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} engines: {node: '>=12'} @@ -2860,6 +2912,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.27.2': + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.19.12': resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} engines: {node: '>=12'} @@ -2878,6 +2936,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.27.2': + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.19.12': resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} engines: {node: '>=12'} @@ -2896,6 +2960,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.27.2': + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.19.12': resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} engines: {node: '>=12'} @@ -2914,6 +2984,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.27.2': + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.19.12': resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} engines: {node: '>=12'} @@ -2932,6 +3008,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.27.2': + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.19.12': resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} engines: {node: '>=12'} @@ -2950,6 +3032,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.27.2': + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.19.12': resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} engines: {node: '>=12'} @@ -2968,6 +3056,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.27.2': + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.19.12': resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} engines: {node: '>=12'} @@ -2986,6 +3080,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.27.2': + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.19.12': resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} engines: {node: '>=12'} @@ -3004,6 +3104,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.27.2': + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.19.12': resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} engines: {node: '>=12'} @@ -3022,6 +3128,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.27.2': + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.19.12': resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} engines: {node: '>=12'} @@ -3040,6 +3152,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.27.2': + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.19.12': resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} engines: {node: '>=12'} @@ -3058,6 +3176,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.27.2': + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.19.12': resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} engines: {node: '>=12'} @@ -3076,6 +3200,12 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.27.2': + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.25.0': resolution: {integrity: sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==} engines: {node: '>=18'} @@ -3088,6 +3218,12 @@ packages: cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.27.2': + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.19.12': resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} engines: {node: '>=12'} @@ -3106,6 +3242,12 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.27.2': + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.25.0': resolution: {integrity: sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==} engines: {node: '>=18'} @@ -3118,6 +3260,12 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.27.2': + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.19.12': resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} engines: {node: '>=12'} @@ -3136,6 +3284,18 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.27.2': + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.2': + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.19.12': resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} engines: {node: '>=12'} @@ -3154,6 +3314,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.27.2': + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.19.12': resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} engines: {node: '>=12'} @@ -3172,6 +3338,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.27.2': + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.19.12': resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} engines: {node: '>=12'} @@ -3190,6 +3362,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.27.2': + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.19.12': resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} engines: {node: '>=12'} @@ -3208,6 +3386,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.27.2': + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.4.0': resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -5463,6 +5647,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.27.2: + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -7266,6 +7455,11 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + nanoid@3.3.8: resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -7611,6 +7805,10 @@ packages: resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + preferred-pm@3.0.3: resolution: {integrity: sha512-+wZgbxNES/KlJs9q40F/1sfOd/j7f1O9JaHcW5Dsn3aUUOZg3L2bjpVUcKV2jvtElYfoTuQiNeMfQJ4kwUAhCQ==} engines: {node: '>=10'} @@ -10428,6 +10626,9 @@ snapshots: '@esbuild/aix-ppc64@0.25.4': optional: true + '@esbuild/aix-ppc64@0.27.2': + optional: true + '@esbuild/android-arm64@0.19.12': optional: true @@ -10437,6 +10638,9 @@ snapshots: '@esbuild/android-arm64@0.25.4': optional: true + '@esbuild/android-arm64@0.27.2': + optional: true + '@esbuild/android-arm@0.19.12': optional: true @@ -10446,6 +10650,9 @@ snapshots: '@esbuild/android-arm@0.25.4': optional: true + '@esbuild/android-arm@0.27.2': + optional: true + '@esbuild/android-x64@0.19.12': optional: true @@ -10455,6 +10662,9 @@ snapshots: '@esbuild/android-x64@0.25.4': optional: true + '@esbuild/android-x64@0.27.2': + optional: true + '@esbuild/darwin-arm64@0.19.12': optional: true @@ -10464,6 +10674,9 @@ snapshots: '@esbuild/darwin-arm64@0.25.4': optional: true + '@esbuild/darwin-arm64@0.27.2': + optional: true + '@esbuild/darwin-x64@0.19.12': optional: true @@ -10473,6 +10686,9 @@ snapshots: '@esbuild/darwin-x64@0.25.4': optional: true + '@esbuild/darwin-x64@0.27.2': + optional: true + '@esbuild/freebsd-arm64@0.19.12': optional: true @@ -10482,6 +10698,9 @@ snapshots: '@esbuild/freebsd-arm64@0.25.4': optional: true + '@esbuild/freebsd-arm64@0.27.2': + optional: true + '@esbuild/freebsd-x64@0.19.12': optional: true @@ -10491,6 +10710,9 @@ snapshots: '@esbuild/freebsd-x64@0.25.4': optional: true + '@esbuild/freebsd-x64@0.27.2': + optional: true + '@esbuild/linux-arm64@0.19.12': optional: true @@ -10500,6 +10722,9 @@ snapshots: '@esbuild/linux-arm64@0.25.4': optional: true + '@esbuild/linux-arm64@0.27.2': + optional: true + '@esbuild/linux-arm@0.19.12': optional: true @@ -10509,6 +10734,9 @@ snapshots: '@esbuild/linux-arm@0.25.4': optional: true + '@esbuild/linux-arm@0.27.2': + optional: true + '@esbuild/linux-ia32@0.19.12': optional: true @@ -10518,6 +10746,9 @@ snapshots: '@esbuild/linux-ia32@0.25.4': optional: true + '@esbuild/linux-ia32@0.27.2': + optional: true + '@esbuild/linux-loong64@0.19.12': optional: true @@ -10527,6 +10758,9 @@ snapshots: '@esbuild/linux-loong64@0.25.4': optional: true + '@esbuild/linux-loong64@0.27.2': + optional: true + '@esbuild/linux-mips64el@0.19.12': optional: true @@ -10536,6 +10770,9 @@ snapshots: '@esbuild/linux-mips64el@0.25.4': optional: true + '@esbuild/linux-mips64el@0.27.2': + optional: true + '@esbuild/linux-ppc64@0.19.12': optional: true @@ -10545,6 +10782,9 @@ snapshots: '@esbuild/linux-ppc64@0.25.4': optional: true + '@esbuild/linux-ppc64@0.27.2': + optional: true + '@esbuild/linux-riscv64@0.19.12': optional: true @@ -10554,6 +10794,9 @@ snapshots: '@esbuild/linux-riscv64@0.25.4': optional: true + '@esbuild/linux-riscv64@0.27.2': + optional: true + '@esbuild/linux-s390x@0.19.12': optional: true @@ -10563,6 +10806,9 @@ snapshots: '@esbuild/linux-s390x@0.25.4': optional: true + '@esbuild/linux-s390x@0.27.2': + optional: true + '@esbuild/linux-x64@0.19.12': optional: true @@ -10572,12 +10818,18 @@ snapshots: '@esbuild/linux-x64@0.25.4': optional: true + '@esbuild/linux-x64@0.27.2': + optional: true + '@esbuild/netbsd-arm64@0.25.0': optional: true '@esbuild/netbsd-arm64@0.25.4': optional: true + '@esbuild/netbsd-arm64@0.27.2': + optional: true + '@esbuild/netbsd-x64@0.19.12': optional: true @@ -10587,12 +10839,18 @@ snapshots: '@esbuild/netbsd-x64@0.25.4': optional: true + '@esbuild/netbsd-x64@0.27.2': + optional: true + '@esbuild/openbsd-arm64@0.25.0': optional: true '@esbuild/openbsd-arm64@0.25.4': optional: true + '@esbuild/openbsd-arm64@0.27.2': + optional: true + '@esbuild/openbsd-x64@0.19.12': optional: true @@ -10602,6 +10860,12 @@ snapshots: '@esbuild/openbsd-x64@0.25.4': optional: true + '@esbuild/openbsd-x64@0.27.2': + optional: true + + '@esbuild/openharmony-arm64@0.27.2': + optional: true + '@esbuild/sunos-x64@0.19.12': optional: true @@ -10611,6 +10875,9 @@ snapshots: '@esbuild/sunos-x64@0.25.4': optional: true + '@esbuild/sunos-x64@0.27.2': + optional: true + '@esbuild/win32-arm64@0.19.12': optional: true @@ -10620,6 +10887,9 @@ snapshots: '@esbuild/win32-arm64@0.25.4': optional: true + '@esbuild/win32-arm64@0.27.2': + optional: true + '@esbuild/win32-ia32@0.19.12': optional: true @@ -10629,6 +10899,9 @@ snapshots: '@esbuild/win32-ia32@0.25.4': optional: true + '@esbuild/win32-ia32@0.27.2': + optional: true + '@esbuild/win32-x64@0.19.12': optional: true @@ -10638,6 +10911,9 @@ snapshots: '@esbuild/win32-x64@0.25.4': optional: true + '@esbuild/win32-x64@0.27.2': + optional: true + '@eslint-community/eslint-utils@4.4.0(eslint@8.57.0)': dependencies: eslint: 8.57.0 @@ -12105,7 +12381,7 @@ snapshots: optionalDependencies: react-server-dom-webpack: 19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.103.0) - '@vitejs/plugin-rsc@0.5.7(react-dom@19.3.0-canary-d763f313-20251210(react@19.3.0-canary-d763f313-20251210))(react-server-dom-webpack@19.2.3(react-dom@19.3.0-canary-d763f313-20251210(react@19.3.0-canary-d763f313-20251210))(react@19.3.0-canary-d763f313-20251210)(webpack@5.103.0(@swc/core@1.11.24)(esbuild@0.25.4)))(react@19.3.0-canary-d763f313-20251210)(vite@6.4.1(@types/node@20.11.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.0))': + '@vitejs/plugin-rsc@0.5.7(react-dom@19.3.0-canary-d763f313-20251210(react@19.3.0-canary-d763f313-20251210))(react-server-dom-webpack@19.2.3(react-dom@19.3.0-canary-d763f313-20251210(react@19.3.0-canary-d763f313-20251210))(react@19.3.0-canary-d763f313-20251210)(webpack@5.103.0(@swc/core@1.11.24)(esbuild@0.27.2)))(react@19.3.0-canary-d763f313-20251210)(vite@6.4.1(@types/node@20.11.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.0))': dependencies: '@remix-run/node-fetch-server': 0.12.0 es-module-lexer: 2.0.0 @@ -12119,7 +12395,7 @@ snapshots: vite: 6.4.1(@types/node@20.11.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.0) vitefu: 1.1.1(vite@6.4.1(@types/node@20.11.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.0)) optionalDependencies: - react-server-dom-webpack: 19.2.3(react-dom@19.3.0-canary-d763f313-20251210(react@19.3.0-canary-d763f313-20251210))(react@19.3.0-canary-d763f313-20251210)(webpack@5.103.0(@swc/core@1.11.24)(esbuild@0.25.4)) + react-server-dom-webpack: 19.2.3(react-dom@19.3.0-canary-d763f313-20251210(react@19.3.0-canary-d763f313-20251210))(react@19.3.0-canary-d763f313-20251210)(webpack@5.103.0(@swc/core@1.11.24)(esbuild@0.27.2)) '@web3-storage/multipart-parser@1.0.0': {} @@ -13317,10 +13593,10 @@ snapshots: transitivePeerDependencies: - supports-color - esbuild-register@3.6.0(esbuild@0.25.4): + esbuild-register@3.6.0(esbuild@0.27.2): dependencies: debug: 4.4.1 - esbuild: 0.25.4 + esbuild: 0.27.2 transitivePeerDependencies: - supports-color @@ -13406,6 +13682,35 @@ snapshots: '@esbuild/win32-ia32': 0.25.4 '@esbuild/win32-x64': 0.25.4 + esbuild@0.27.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.2 + '@esbuild/android-arm': 0.27.2 + '@esbuild/android-arm64': 0.27.2 + '@esbuild/android-x64': 0.27.2 + '@esbuild/darwin-arm64': 0.27.2 + '@esbuild/darwin-x64': 0.27.2 + '@esbuild/freebsd-arm64': 0.27.2 + '@esbuild/freebsd-x64': 0.27.2 + '@esbuild/linux-arm': 0.27.2 + '@esbuild/linux-arm64': 0.27.2 + '@esbuild/linux-ia32': 0.27.2 + '@esbuild/linux-loong64': 0.27.2 + '@esbuild/linux-mips64el': 0.27.2 + '@esbuild/linux-ppc64': 0.27.2 + '@esbuild/linux-riscv64': 0.27.2 + '@esbuild/linux-s390x': 0.27.2 + '@esbuild/linux-x64': 0.27.2 + '@esbuild/netbsd-arm64': 0.27.2 + '@esbuild/netbsd-x64': 0.27.2 + '@esbuild/openbsd-arm64': 0.27.2 + '@esbuild/openbsd-x64': 0.27.2 + '@esbuild/openharmony-arm64': 0.27.2 + '@esbuild/sunos-x64': 0.27.2 + '@esbuild/win32-arm64': 0.27.2 + '@esbuild/win32-ia32': 0.27.2 + '@esbuild/win32-x64': 0.27.2 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -15997,6 +16302,8 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 + nanoid@3.3.11: {} + nanoid@3.3.8: {} natural-compare-lite@1.4.0: {} @@ -16318,12 +16625,12 @@ snapshots: read-cache: 1.0.0 resolve: 1.22.10 - postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.5.3)(tsx@4.19.3)(yaml@2.8.0): + postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.5.6)(tsx@4.19.3)(yaml@2.8.0): dependencies: lilconfig: 3.1.1 optionalDependencies: jiti: 2.4.2 - postcss: 8.5.3 + postcss: 8.5.6 tsx: 4.19.3 yaml: 2.8.0 @@ -16335,6 +16642,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + preferred-pm@3.0.3: dependencies: find-up: 5.0.0 @@ -16489,13 +16802,13 @@ snapshots: webpack: 5.103.0 webpack-sources: 3.3.3 - react-server-dom-webpack@19.2.3(react-dom@19.3.0-canary-d763f313-20251210(react@19.3.0-canary-d763f313-20251210))(react@19.3.0-canary-d763f313-20251210)(webpack@5.103.0(@swc/core@1.11.24)(esbuild@0.25.4)): + react-server-dom-webpack@19.2.3(react-dom@19.3.0-canary-d763f313-20251210(react@19.3.0-canary-d763f313-20251210))(react@19.3.0-canary-d763f313-20251210)(webpack@5.103.0(@swc/core@1.11.24)(esbuild@0.27.2)): dependencies: acorn-loose: 8.5.2 neo-async: 2.6.2 react: 19.3.0-canary-d763f313-20251210 react-dom: 19.3.0-canary-d763f313-20251210(react@19.3.0-canary-d763f313-20251210) - webpack: 5.103.0(@swc/core@1.11.24)(esbuild@0.25.4) + webpack: 5.103.0(@swc/core@1.11.24)(esbuild@0.27.2) webpack-sources: 3.3.3 react-test-renderer@19.1.0(react@19.2.3): @@ -17306,17 +17619,17 @@ snapshots: term-size@2.2.1: {} - terser-webpack-plugin@5.3.15(@swc/core@1.11.24)(esbuild@0.25.4)(webpack@5.103.0(@swc/core@1.11.24)(esbuild@0.25.4)): + terser-webpack-plugin@5.3.15(@swc/core@1.11.24)(esbuild@0.27.2)(webpack@5.103.0(@swc/core@1.11.24)(esbuild@0.27.2)): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 4.3.3 serialize-javascript: 6.0.2 terser: 5.44.1 - webpack: 5.103.0(@swc/core@1.11.24)(esbuild@0.25.4) + webpack: 5.103.0(@swc/core@1.11.24)(esbuild@0.27.2) optionalDependencies: '@swc/core': 1.11.24 - esbuild: 0.25.4 + esbuild: 0.27.2 terser-webpack-plugin@5.3.15(webpack@5.103.0): dependencies: @@ -17426,7 +17739,7 @@ snapshots: tsscmp@1.0.6: {} - tsup@8.5.0(@swc/core@1.11.24)(jiti@2.4.2)(postcss@8.5.3)(tsx@4.19.3)(typescript@5.4.5)(yaml@2.8.0): + tsup@8.5.0(@swc/core@1.11.24)(jiti@2.4.2)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.4.5)(yaml@2.8.0): dependencies: bundle-require: 5.1.0(esbuild@0.25.0) cac: 6.7.14 @@ -17437,7 +17750,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.4.2)(postcss@8.5.3)(tsx@4.19.3)(yaml@2.8.0) + postcss-load-config: 6.0.1(jiti@2.4.2)(postcss@8.5.6)(tsx@4.19.3)(yaml@2.8.0) resolve-from: 5.0.0 rollup: 4.43.0 source-map: 0.8.0-beta.0 @@ -17447,7 +17760,7 @@ snapshots: tree-kill: 1.2.2 optionalDependencies: '@swc/core': 1.11.24 - postcss: 8.5.3 + postcss: 8.5.6 typescript: 5.4.5 transitivePeerDependencies: - jiti @@ -17950,7 +18263,7 @@ snapshots: esbuild: 0.25.0 fdir: 6.4.6(picomatch@4.0.2) picomatch: 4.0.2 - postcss: 8.5.3 + postcss: 8.5.6 rollup: 4.43.0 tinyglobby: 0.2.14 optionalDependencies: @@ -17967,7 +18280,7 @@ snapshots: esbuild: 0.25.0 fdir: 6.4.6(picomatch@4.0.2) picomatch: 4.0.2 - postcss: 8.5.3 + postcss: 8.5.6 rollup: 4.43.0 tinyglobby: 0.2.14 optionalDependencies: @@ -18079,7 +18392,7 @@ snapshots: - esbuild - uglify-js - webpack@5.103.0(@swc/core@1.11.24)(esbuild@0.25.4): + webpack@5.103.0(@swc/core@1.11.24)(esbuild@0.27.2): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -18103,7 +18416,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.0 - terser-webpack-plugin: 5.3.15(@swc/core@1.11.24)(esbuild@0.25.4)(webpack@5.103.0(@swc/core@1.11.24)(esbuild@0.25.4)) + terser-webpack-plugin: 5.3.15(@swc/core@1.11.24)(esbuild@0.27.2)(webpack@5.103.0(@swc/core@1.11.24)(esbuild@0.27.2)) watchpack: 2.4.4 webpack-sources: 3.3.3 transitivePeerDependencies: From 3a0740268e1c70bc25f2d99fcc05bcc0b2789f18 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 7 Jan 2026 17:11:32 -0500 Subject: [PATCH 03/28] Update playground --- packages/react-router/lib/router/router.ts | 1 - playground/data/src/main.tsx | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/react-router/lib/router/router.ts b/packages/react-router/lib/router/router.ts index 09e2e08975..f3ad1fa856 100644 --- a/packages/react-router/lib/router/router.ts +++ b/packages/react-router/lib/router/router.ts @@ -1220,7 +1220,6 @@ export function createRouter(init: RouterInit): Router { // resolved prior to router creation since we can't go into a fallback // UI for SSR'd apps if (!state.initialized) { - debugger; startNavigation(NavigationType.Pop, state.location, { initialHydration: true, }); diff --git a/playground/data/src/main.tsx b/playground/data/src/main.tsx index ca7b4bfbea..a460ea99cb 100644 --- a/playground/data/src/main.tsx +++ b/playground/data/src/main.tsx @@ -4,6 +4,7 @@ import { Link, createBrowserRouter, useLoaderData, + useLocation, useParams, } from "react-router"; import { RouterProvider } from "react-router/dom"; @@ -54,11 +55,13 @@ const router = createBrowserRouter([ }, Component() { let data = useLoaderData() as { photo: Photo | null }; + let location = useLocation(); console.log("data in component", data); return ( <>

Hello React Router (data mode)

+
useLocation: {location.pathname + location.search}
{data.photo && ( @@ -80,9 +83,11 @@ const router = createBrowserRouter([ }, Component() { let data = useLoaderData() as { photo: Photo }; + let location = useLocation(); return ( <>

Images Page

+
useLocation: {location.pathname + location.search}
); From f66a5334d5888fb16339b6691ca20f2bef894c64 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 8 Jan 2026 12:28:33 -0500 Subject: [PATCH 04/28] Clear rewritten loctions on SSR reloads --- .../react-router/lib/dom/ssr/components.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/react-router/lib/dom/ssr/components.tsx b/packages/react-router/lib/dom/ssr/components.tsx index c4e75cc66a..75e534bb3d 100644 --- a/packages/react-router/lib/dom/ssr/components.tsx +++ b/packages/react-router/lib/dom/ssr/components.tsx @@ -764,6 +764,17 @@ export function Scripts(scriptProps: ScriptsProps): React.JSX.Element | null { return null; } + // We cannot support history-state-driven rewrites with SSR, so if a hard + // reload is performed we remove the rewrite and hydrate according to the + // browser URL + let rewriteScript = ssr + ? [ + "if (window.history.state.usr && window.history.state.usr._rewrite) {", + "window.history.replaceState({ ...window.history.state, usr: { ...window.history.state.usr, _rewrite: null }}, null);", + "}", + ].join("") + : ""; + let streamScript = "window.__reactRouterContext.stream = new ReadableStream({" + "start(controller){" + @@ -772,7 +783,11 @@ export function Scripts(scriptProps: ScriptsProps): React.JSX.Element | null { "}).pipeThrough(new TextEncoderStream());"; let contextScript = staticContext - ? `window.__reactRouterContext = ${serverHandoffString};${streamScript}` + ? [ + `window.__reactRouterContext = ${serverHandoffString};`, + rewriteScript, + streamScript, + ].join("\n") : " "; let routeModulesScript = !isStatic From 527ec48091704feaeacd3014ee2ee45cd6ff8fe8 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 8 Jan 2026 15:47:40 -0500 Subject: [PATCH 05/28] Move rewrite storage out of user state and onto location --- packages/react-router/lib/components.tsx | 14 ++-- packages/react-router/lib/dom/server.tsx | 1 + .../react-router/lib/dom/ssr/components.tsx | 4 +- packages/react-router/lib/hooks.tsx | 1 + packages/react-router/lib/router/history.ts | 61 ++++++++++-------- packages/react-router/lib/router/router.ts | 64 +++++++++---------- 6 files changed, 75 insertions(+), 70 deletions(-) diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx index d859f6c880..f5b081f604 100644 --- a/packages/react-router/lib/components.tsx +++ b/packages/react-router/lib/components.tsx @@ -724,13 +724,13 @@ export function RouterProvider({ [router, navigator, basename, onError], ); - let uiLocation = state.location; - if (state.location.state?._rewrite) { + let uiLocation = state.location.rewrite || state.location; + if (state.location.rewrite) { uiLocation = { ...state.location, - pathname: state.location.state._rewrite.pathname, - search: state.location.state._rewrite.search, - hash: state.location.state._rewrite.hash, + pathname: state.location.rewrite.pathname, + search: state.location.rewrite.search, + hash: state.location.rewrite.hash, }; } @@ -1397,6 +1397,7 @@ export function Router({ hash = "", state = null, key = "default", + rewrite, } = locationProp; let locationContext = React.useMemo(() => { @@ -1413,10 +1414,11 @@ export function Router({ hash, state, key, + rewrite, }, navigationType, }; - }, [basename, pathname, search, hash, state, key, navigationType]); + }, [basename, pathname, search, hash, state, key, rewrite, navigationType]); warning( locationContext != null, diff --git a/packages/react-router/lib/dom/server.tsx b/packages/react-router/lib/dom/server.tsx index 7653755af3..8ffcb0c75e 100644 --- a/packages/react-router/lib/dom/server.tsx +++ b/packages/react-router/lib/dom/server.tsx @@ -83,6 +83,7 @@ export function StaticRouter({ hash: locationProp.hash || "", state: locationProp.state != null ? locationProp.state : null, key: locationProp.key || "default", + rewrite: undefined, }; let staticNavigator = getStatelessNavigator(); diff --git a/packages/react-router/lib/dom/ssr/components.tsx b/packages/react-router/lib/dom/ssr/components.tsx index 75e534bb3d..b2de09d663 100644 --- a/packages/react-router/lib/dom/ssr/components.tsx +++ b/packages/react-router/lib/dom/ssr/components.tsx @@ -769,8 +769,8 @@ export function Scripts(scriptProps: ScriptsProps): React.JSX.Element | null { // browser URL let rewriteScript = ssr ? [ - "if (window.history.state.usr && window.history.state.usr._rewrite) {", - "window.history.replaceState({ ...window.history.state, usr: { ...window.history.state.usr, _rewrite: null }}, null);", + "if (window.history.state && window.history.state.rewrite) {", + "window.history.replaceState({ ...window.history.state, rewrite: undefined }, null);", "}", ].join("") : ""; diff --git a/packages/react-router/lib/hooks.tsx b/packages/react-router/lib/hooks.tsx index 0ab51acae1..419bbc4ddd 100644 --- a/packages/react-router/lib/hooks.tsx +++ b/packages/react-router/lib/hooks.tsx @@ -935,6 +935,7 @@ export function useRoutesImpl( hash: "", state: null, key: "default", + rewrite: undefined, ...location, }, navigationType: NavigationType.Pop, diff --git a/packages/react-router/lib/router/history.ts b/packages/react-router/lib/router/history.ts index 691b2fc028..77a67174ca 100644 --- a/packages/react-router/lib/router/history.ts +++ b/packages/react-router/lib/router/history.ts @@ -69,6 +69,12 @@ export interface Location extends Path { * Note: This value is always "default" on the initial location. */ key: string; + + /** + * The rewritten location used by the router, which differs from the URL shown + * in the browser URL bar + */ + rewrite: Path | undefined; } /** @@ -189,9 +195,22 @@ type HistoryState = { usr: any; key?: string; idx: number; + rewrite?: Path; }; const PopStateEventType = "popstate"; + +function isLocation(obj: unknown): obj is Location { + return ( + typeof obj === "object" && + obj != null && + "pathname" in obj && + "search" in obj && + "hash" in obj && + "key" in obj + ); +} + //#endregion //////////////////////////////////////////////////////////////////////////////// @@ -236,6 +255,7 @@ export function createMemoryHistory( entry, typeof entry === "string" ? null : entry.state, index === 0 ? "default" : undefined, + typeof entry === "string" ? undefined : entry.rewrite, ), ); let index = clampIndex( @@ -254,12 +274,14 @@ export function createMemoryHistory( to: To, state: any = null, key?: string, + rewrite?: Path, ): Location { let location = createLocation( entries ? getCurrentLocation().pathname : "/", to, state, key, + rewrite, ); warning( location.pathname.charAt(0) === "/", @@ -298,7 +320,7 @@ export function createMemoryHistory( }, push(to, state) { action = Action.Push; - let nextLocation = createMemoryLocation(to, state); + let nextLocation = isLocation(to) ? to : createMemoryLocation(to, state); index += 1; entries.splice(index, entries.length, nextLocation); if (v5Compat && listener) { @@ -370,6 +392,7 @@ export function createBrowserHistory( // state defaults to `null` because `window.history.state` does (globalHistory.state && globalHistory.state.usr) || null, (globalHistory.state && globalHistory.state.key) || "default", + (globalHistory.state && globalHistory.state.rewrite) || undefined, ); } @@ -521,6 +544,7 @@ function getHistoryState(location: Location, index: number): HistoryState { usr: location.state, key: location.key, idx: index, + rewrite: location.rewrite, }; } @@ -532,6 +556,7 @@ export function createLocation( to: To, state: any = null, key?: string, + rewrite?: Path, ): Readonly { let location: Readonly = { pathname: typeof current === "string" ? current : current.pathname, @@ -544,33 +569,11 @@ export function createLocation( // But that's a pretty big refactor to the current test suite so going to // keep as is for the time being and just let any incoming keys take precedence key: (to && (to as Location).key) || key || createKey(), + rewrite, }; return location; } -export function getRewritePath(location: Location): Path { - let rewrite = location.state && (location.state as any)._rewrite; - - if ( - typeof rewrite === "object" && - rewrite != null && - "pathname" in rewrite && - typeof rewrite.pathname === "string" && - "search" in rewrite && - typeof rewrite.search === "string" && - "hash" in rewrite && - typeof rewrite.hash === "string" - ) { - return { - pathname: rewrite.pathname, - search: rewrite.search, - hash: rewrite.hash, - }; - } - - return location; -} - /** * Creates a string URL path from the given pathname, search, and hash components. * @@ -659,9 +662,11 @@ function getUrlBasedHistory( } } - function push(to: To, state?: any) { + function push(to: Location | To, state?: any) { action = Action.Push; - let location = createLocation(history.location, to, state); + let location = isLocation(to) + ? to + : createLocation(history.location, to, state); if (validateLocation) validateLocation(location, to); index = getIndex() + 1; @@ -691,7 +696,9 @@ function getUrlBasedHistory( function replace(to: To, state?: any) { action = Action.Replace; - let location = createLocation(history.location, to, state); + let location = isLocation(to) + ? to + : createLocation(history.location, to, state); if (validateLocation) validateLocation(location, to); index = getIndex(); diff --git a/packages/react-router/lib/router/router.ts b/packages/react-router/lib/router/router.ts index f3ad1fa856..f7937dbda9 100644 --- a/packages/react-router/lib/router/router.ts +++ b/packages/react-router/lib/router/router.ts @@ -8,7 +8,6 @@ import { invariant, parsePath, warning, - getRewritePath, } from "./history"; import type { unstable_ClientInstrumentation, @@ -940,11 +939,10 @@ export function createRouter(init: RouterInit): Router { let initialScrollRestored = init.hydrationData != null; let initialLocation = init.history.location; - let rewritePath = getRewritePath(initialLocation); - if (rewritePath) { + if (initialLocation.rewrite) { initialLocation = { ...initialLocation, - ...rewritePath, + ...initialLocation.rewrite, }; } let initialMatches = matchRoutes(dataRoutes, initialLocation, basename); @@ -1525,35 +1523,31 @@ export function createRouter(init: RouterInit): Router { ); let currentLocation = state.location; - let nextLocation = createLocation(state.location, path, opts && opts.state); - - // When using navigate as a PUSH/REPLACE we aren't reading an already-encoded - // URL from window.location, so we need to encode it here so the behavior - // remains the same as POP and non-data-router usages. new URL() does all - // the same encoding we'd get from a history.pushState/window.location read - // without having to touch history - nextLocation = { - ...nextLocation, - ...init.history.encodeLocation(nextLocation), - }; - // If rewrite is provided, normalize and create a separate location for data loading - if (opts?.rewrite) { - let normalizedRewritePath = normalizeTo( - state.location, - state.matches, - basename, - opts.rewrite, - opts?.fromRouteId, - opts?.relative, - ); - Object.assign(nextLocation, { - state: { - ...nextLocation.state, - _rewrite: init.history.encodeLocation(normalizedRewritePath), - }, - }); - } + // If rewrite is provided, normalize and create a separate path for the router + let rewritePath = opts?.rewrite + ? init.history.encodeLocation( + normalizeTo( + state.location, + state.matches, + basename, + opts.rewrite, + opts?.fromRouteId, + opts?.relative, + ), + ) + : undefined; + + let nextLocation = createLocation( + currentLocation, + // When using navigate as a PUSH/REPLACE we aren't reading an already-encoded + // URL from window.location, so we need to encode it here so the behavior + // remains the same as POP and non-data-router usages + init.history.encodeLocation(path), + opts && opts.state, + undefined, + rewritePath, + ); let userReplace = opts && opts.replace != null ? opts.replace : undefined; @@ -1702,7 +1696,7 @@ export function createRouter(init: RouterInit): Router { }, ): Promise { let externalLocation = _location; - let routerPath = getRewritePath(_location); + let routerPath = _location.rewrite || _location; // Abort any in-progress navigations and start a new one. Unset any ongoing // uninterrupted revalidations unless told otherwise, since we want this @@ -1912,7 +1906,7 @@ export function createRouter(init: RouterInit): Router { interruptActiveLoads(); let externalLocation = _location; - let routerPath = getRewritePath(_location); + let routerPath = _location.rewrite || _location; // Put us in a submitting state let navigation = getSubmittingNavigation(externalLocation, submission); @@ -2089,7 +2083,7 @@ export function createRouter(init: RouterInit): Router { callSiteDefaultShouldRevalidate?: boolean, ): Promise { let externalLocation = _location; - let routerPath = getRewritePath(_location); + let routerPath = _location.rewrite || _location; // Figure out the right navigation we want to use for data loading let loadingNavigation = From 40212f0ffe31d1a8e1116d2d3c0595f0f08266a9 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 8 Jan 2026 15:55:12 -0500 Subject: [PATCH 06/28] Move demo to playground/framework for now, to test with and without ssr --- playground/data/src/main.tsx | 129 +------------------- playground/data/tsconfig.json | 30 ++--- playground/framework/app/photos.tsx | 77 ++++++++++++ playground/framework/app/routes.ts | 2 +- playground/framework/app/routes/_index.tsx | 22 +++- playground/framework/app/routes/image.tsx | 24 ++++ playground/framework/app/routes/product.tsx | 9 -- playground/framework/react-router.config.ts | 1 + 8 files changed, 138 insertions(+), 156 deletions(-) create mode 100644 playground/framework/app/photos.tsx create mode 100644 playground/framework/app/routes/image.tsx delete mode 100644 playground/framework/app/routes/product.tsx diff --git a/playground/data/src/main.tsx b/playground/data/src/main.tsx index a460ea99cb..7dd9a9387c 100644 --- a/playground/data/src/main.tsx +++ b/playground/data/src/main.tsx @@ -1,140 +1,23 @@ import * as React from "react"; import * as ReactClient from "react-dom/client"; -import { - Link, - createBrowserRouter, - useLoaderData, - useLocation, - useParams, -} from "react-router"; +import { createBrowserRouter, useLoaderData } from "react-router"; import { RouterProvider } from "react-router/dom"; -type Photo = { - id: string; - url: string; - alt: string; -}; - -const PHOTOS: Record = { - "1": { - id: "1", - url: "https://placecats.com/300/200", - alt: "Tabby cat lounging", - }, - "2": { - id: "2", - url: "https://placecats.com/400/300", - alt: "Orange cat playing", - }, - "3": { - id: "3", - url: "https://placecats.com/350/250", - alt: "Black cat sleeping", - }, - "4": { - id: "4", - url: "https://placecats.com/500/400", - alt: "White cat sitting", - }, - "5": { - id: "5", - url: "https://placecats.com/450/350", - alt: "Grey cat looking", - }, -}; - const router = createBrowserRouter([ { id: "index", path: "/", - loader({ request }) { - let sp = new URL(request.url).searchParams; - let photo = sp.has("photo") ? getPhoto(sp.get("photo")!) : null; - console.log("returning from loader", photo); - return { photo }; + loader() { + return { message: "Hello React Router!" }; }, Component() { - let data = useLoaderData() as { photo: Photo | null }; - let location = useLocation(); - console.log("data in component", data); - return ( - <> -

Hello React Router (data mode)

- -
useLocation: {location.pathname + location.search}
- {data.photo && ( - - - - )} - - ); - }, - }, - { - id: "image", - path: "/images/:id", - loader({ params }) { - if (!params.id || typeof params.id !== "string") { - throw new Error("Image ID is required"); - } - let photo = getPhoto(params.id); - return { photo }; - }, - Component() { - let data = useLoaderData() as { photo: Photo }; - let location = useLocation(); - return ( - <> -

Images Page

-
useLocation: {location.pathname + location.search}
- - - ); + let data = useLoaderData(); + return

{data.message}

; }, }, ]); -window.router = router; - -function PhotoDisplay({ photo }: { photo: Photo }) { - return {photo.alt}; -} - -function ImageGallery() { - return ( -
- {Object.values(PHOTOS).map((photo) => ( - - {photo.alt} - - ))} -
- ); -} -function getPhoto(id: string): Photo { - if (!(id in PHOTOS)) { - throw new Error("Photo not found"); - } - return PHOTOS[id]; -} - -ReactClient.createRoot(document.getElementById("root")).render( +ReactClient.createRoot(document.getElementById("root")!).render( , diff --git a/playground/data/tsconfig.json b/playground/data/tsconfig.json index 227a6c6723..ee47d3b35f 100644 --- a/playground/data/tsconfig.json +++ b/playground/data/tsconfig.json @@ -1,27 +1,17 @@ { + "include": ["src"], "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "jsx": "react-jsx", "target": "ES2022", - "useDefineForClassFields": true, - "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, + "moduleResolution": "Bundler", + "resolveJsonModule": true, "verbatimModuleSyntax": true, - "moduleDetection": "force", - "noEmit": true, - "jsx": "react-jsx", - - /* Linting */ + "esModuleInterop": true, "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "erasableSyntaxOnly": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true - }, - "include": ["src"] + "allowJs": true, + "skipLibCheck": true, + "noEmit": true + } } diff --git a/playground/framework/app/photos.tsx b/playground/framework/app/photos.tsx new file mode 100644 index 0000000000..4b2a015822 --- /dev/null +++ b/playground/framework/app/photos.tsx @@ -0,0 +1,77 @@ +import { Link } from "react-router"; + +export type Photo = { + id: string; + url: string; + alt: string; +}; + +const PHOTOS: Record = { + "1": { + id: "1", + url: "https://placecats.com/300/200", + alt: "Tabby cat lounging", + }, + "2": { + id: "2", + url: "https://placecats.com/400/300", + alt: "Orange cat playing", + }, + "3": { + id: "3", + url: "https://placecats.com/350/250", + alt: "Black cat sleeping", + }, + "4": { + id: "4", + url: "https://placecats.com/500/400", + alt: "White cat sitting", + }, + "5": { + id: "5", + url: "https://placecats.com/450/350", + alt: "Grey cat looking", + }, +}; + +export async function getPhotos() { + return PHOTOS; +} + +export function getPhoto(id: string): Photo { + if (!(id in PHOTOS)) { + throw new Error("Photo not found"); + } + return PHOTOS[id]; +} + +export function PhotoDisplay({ photo }: { photo: Photo }) { + return {photo.alt}; +} + +export function ImageGallery() { + return ( +
+ {Object.values(PHOTOS).map((photo) => ( + + {photo.alt} + + ))} +
+ ); +} diff --git a/playground/framework/app/routes.ts b/playground/framework/app/routes.ts index 6ed053e2d9..b2c66e7f29 100644 --- a/playground/framework/app/routes.ts +++ b/playground/framework/app/routes.ts @@ -2,5 +2,5 @@ import { type RouteConfig, index, route } from "@react-router/dev/routes"; export default [ index("routes/_index.tsx"), - route("products/:id", "routes/product.tsx"), + route("images/:id", "routes/image.tsx"), ] satisfies RouteConfig; diff --git a/playground/framework/app/routes/_index.tsx b/playground/framework/app/routes/_index.tsx index a23689de92..07538b81fe 100644 --- a/playground/framework/app/routes/_index.tsx +++ b/playground/framework/app/routes/_index.tsx @@ -1,9 +1,25 @@ +import { useLoaderData, useLocation } from "react-router"; +import { type Photo, getPhoto, ImageGallery, PhotoDisplay } from "~/photos"; import type { Route } from "./+types/_index"; -export function loader({ params }: Route.LoaderArgs) { - return { planet: "world", date: new Date(), fn: () => 1 }; +export function loader({ request }: Route.LoaderArgs) { + let sp = new URL(request.url).searchParams; + let photo = sp.has("photo") ? getPhoto(sp.get("photo")!) : null; + return { photo }; } export default function Index({ loaderData }: Route.ComponentProps) { - return

Hello, {loaderData.planet}!

; + let data = useLoaderData() as { photo: Photo | null }; + let location = useLocation(); + return ( + <> + +
useLocation: {location.pathname + location.search}
+ {data.photo && ( + + + + )} + + ); } diff --git a/playground/framework/app/routes/image.tsx b/playground/framework/app/routes/image.tsx new file mode 100644 index 0000000000..3f32a381d4 --- /dev/null +++ b/playground/framework/app/routes/image.tsx @@ -0,0 +1,24 @@ +import { useLoaderData, useLocation } from "react-router"; + +import type { Route } from "./+types/image"; +import { type Photo, PhotoDisplay, getPhoto } from "../photos"; + +export function loader({ params }: Route.LoaderArgs) { + if (!params.id || typeof params.id !== "string") { + throw new Error("Image ID is required"); + } + let photo = getPhoto(params.id); + return { photo }; +} + +export default function Component({ loaderData }: Route.ComponentProps) { + let data = useLoaderData() as { photo: Photo }; + let location = useLocation(); + return ( + <> +

Images Page

+
useLocation: {location.pathname + location.search}
+ + + ); +} diff --git a/playground/framework/app/routes/product.tsx b/playground/framework/app/routes/product.tsx deleted file mode 100644 index 101343dad0..0000000000 --- a/playground/framework/app/routes/product.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import type { Route } from "./+types/product"; - -export function loader({ params }: Route.LoaderArgs) { - return { name: `Super cool product #${params.id}` }; -} - -export default function Component({ loaderData }: Route.ComponentProps) { - return

{loaderData.name}

; -} diff --git a/playground/framework/react-router.config.ts b/playground/framework/react-router.config.ts index d0da26c798..d9660f5757 100644 --- a/playground/framework/react-router.config.ts +++ b/playground/framework/react-router.config.ts @@ -4,4 +4,5 @@ export default { future: { unstable_subResourceIntegrity: true, }, + ssr: true, } satisfies Config; From 4d3497b7489cc0ad2370a91ccb860e82a2a9020b Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 8 Jan 2026 16:01:10 -0500 Subject: [PATCH 07/28] Remove data m,ode playground (cherry picked to dev) --- playground/data/index.html | 12 ------------ playground/data/package.json | 23 ----------------------- playground/data/src/main.tsx | 24 ------------------------ playground/data/tsconfig.json | 17 ----------------- playground/data/vite.config.ts | 7 ------- pnpm-lock.yaml | 28 ---------------------------- 6 files changed, 111 deletions(-) delete mode 100644 playground/data/index.html delete mode 100644 playground/data/package.json delete mode 100644 playground/data/src/main.tsx delete mode 100644 playground/data/tsconfig.json delete mode 100644 playground/data/vite.config.ts diff --git a/playground/data/index.html b/playground/data/index.html deleted file mode 100644 index 7e2cc201cf..0000000000 --- a/playground/data/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - React Router (data mode) - - -
- - - diff --git a/playground/data/package.json b/playground/data/package.json deleted file mode 100644 index 3a86597ed1..0000000000 --- a/playground/data/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "@playground/data", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc -b && vite build", - "preview": "vite preview" - }, - "dependencies": { - "react": "catalog:", - "react-dom": "catalog:", - "react-router": "workspace:" - }, - "devDependencies": { - "@types/react": "catalog:", - "@types/react-dom": "catalog:", - "@vitejs/plugin-react": "^4", - "typescript": "catalog:", - "vite": "6.4.1" - } -} diff --git a/playground/data/src/main.tsx b/playground/data/src/main.tsx deleted file mode 100644 index 7dd9a9387c..0000000000 --- a/playground/data/src/main.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import * as React from "react"; -import * as ReactClient from "react-dom/client"; -import { createBrowserRouter, useLoaderData } from "react-router"; -import { RouterProvider } from "react-router/dom"; - -const router = createBrowserRouter([ - { - id: "index", - path: "/", - loader() { - return { message: "Hello React Router!" }; - }, - Component() { - let data = useLoaderData(); - return

{data.message}

; - }, - }, -]); - -ReactClient.createRoot(document.getElementById("root")!).render( - - - , -); diff --git a/playground/data/tsconfig.json b/playground/data/tsconfig.json deleted file mode 100644 index ee47d3b35f..0000000000 --- a/playground/data/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "include": ["src"], - "compilerOptions": { - "lib": ["DOM", "DOM.Iterable", "ES2022"], - "jsx": "react-jsx", - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "Bundler", - "resolveJsonModule": true, - "verbatimModuleSyntax": true, - "esModuleInterop": true, - "strict": true, - "allowJs": true, - "skipLibCheck": true, - "noEmit": true - } -} diff --git a/playground/data/vite.config.ts b/playground/data/vite.config.ts deleted file mode 100644 index 0e43ae8def..0000000000 --- a/playground/data/vite.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; - -// https://vite.dev/config/ -export default defineConfig({ - plugins: [react()], -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8ce5279d2e..743ac11eda 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1324,34 +1324,6 @@ importers: specifier: 'catalog:' version: 0.14.9 - playground/data: - dependencies: - react: - specifier: 'catalog:' - version: 19.2.3 - react-dom: - specifier: 'catalog:' - version: 19.2.3(react@19.2.3) - react-router: - specifier: 'workspace:' - version: link:../../packages/react-router - devDependencies: - '@types/react': - specifier: ^18.0.27 - version: 18.2.18 - '@types/react-dom': - specifier: ^18.0.10 - version: 18.2.7 - '@vitejs/plugin-react': - specifier: ^4 - version: 4.5.2(vite@6.4.1(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.0)) - typescript: - specifier: 'catalog:' - version: 5.4.5 - vite: - specifier: 6.4.1 - version: 6.4.1(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.0) - playground/framework: dependencies: '@react-router/node': From 71f848cd7247f546c4b4970a5fad3b4cf6c329c7 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 8 Jan 2026 16:16:08 -0500 Subject: [PATCH 08/28] Add unstable_ prefix --- .../__tests__/router/rewrite-test.ts | 16 ++--- .../router/utils/data-router-setup.ts | 6 +- packages/react-router/lib/context.ts | 2 +- packages/react-router/lib/dom/lib.tsx | 59 +++++++++++++++++-- packages/react-router/lib/router/router.ts | 6 +- 5 files changed, 68 insertions(+), 21 deletions(-) diff --git a/packages/react-router/__tests__/router/rewrite-test.ts b/packages/react-router/__tests__/router/rewrite-test.ts index 7838709449..9509d1c97b 100644 --- a/packages/react-router/__tests__/router/rewrite-test.ts +++ b/packages/react-router/__tests__/router/rewrite-test.ts @@ -17,7 +17,7 @@ describe("rewrite", () => { // Navigate to /images/123 but load data from /gallery?photo=123 let A = await t.navigate("/images/123", { - rewrite: "/gallery?photo=123", + unstable_rewrite: "/gallery?photo=123", }); // Should call the gallery loader, not the images loader @@ -64,7 +64,7 @@ describe("rewrite", () => { // Navigate to /images/123 with rewrite let A = await t.navigate("/images/123", { - rewrite: "/gallery?photo=123", + unstable_rewrite: "/gallery?photo=123", }); await A.loaders.gallery.resolve("GALLERY DATA"); @@ -106,7 +106,7 @@ describe("rewrite", () => { }); let A = await t.navigate("/images/123", { - rewrite: "/gallery?photo=123", + unstable_rewrite: "/gallery?photo=123", replace: true, }); @@ -134,7 +134,7 @@ describe("rewrite", () => { }); let A = await t.navigate("/images/123#preview", { - rewrite: "/gallery?photo=123#header", + unstable_rewrite: "/gallery?photo=123#header", }); expect(A.loaders.gallery).toBeDefined(); @@ -171,7 +171,7 @@ describe("rewrite", () => { }); let A = await t.navigate("/images/123", { - rewrite: "/gallery?photo=123", + unstable_rewrite: "/gallery?photo=123", state: { customData: "test" }, }); @@ -201,7 +201,7 @@ describe("rewrite", () => { }); let A = await t.navigate("/images/123", { - rewrite: "/gallery?photo=123", + unstable_rewrite: "/gallery?photo=123", formMethod: "post", formData: createFormData({ test: "value" }), }); @@ -247,7 +247,7 @@ describe("rewrite", () => { }); let A = await t.navigate("/images/123", { - rewrite: "/gallery?photo=123", + unstable_rewrite: "/gallery?photo=123", }); expect(A.loaders.gallery).toBeDefined(); @@ -297,7 +297,7 @@ describe("rewrite", () => { // Rewrite to a non-existent route let A = await t.navigate("/images/123", { - rewrite: "/nonexistent", + unstable_rewrite: "/nonexistent", }); // Should get a 404 error diff --git a/packages/react-router/__tests__/router/utils/data-router-setup.ts b/packages/react-router/__tests__/router/utils/data-router-setup.ts index 1e9e3dad91..3e6f9c281b 100644 --- a/packages/react-router/__tests__/router/utils/data-router-setup.ts +++ b/packages/react-router/__tests__/router/utils/data-router-setup.ts @@ -606,9 +606,9 @@ export function setup({ helpers = getNavigationHelpers( // @ts-expect-error - rewrite is a new option - opts?.rewrite - ? typeof opts.rewrite === "string" - ? opts.rewrite + opts?.unstable_rewrite + ? typeof opts.unstable_rewrite === "string" + ? opts.unstable_rewrite : "" : href, navigationId, diff --git a/packages/react-router/lib/context.ts b/packages/react-router/lib/context.ts index 3569d523e6..0a47fd2eb7 100644 --- a/packages/react-router/lib/context.ts +++ b/packages/react-router/lib/context.ts @@ -146,7 +146,7 @@ export interface NavigateOptions { /** Replace the current entry in the history stack instead of pushing a new one */ replace?: boolean; /** Rewrite the url */ - rewrite?: To; + unstable_rewrite?: To; /** Adds persistent client side routing state to the next location */ state?: any; /** If you are using {@link https://api.reactrouter.com/v7/functions/react_router.ScrollRestoration.html }, prevent the scroll position from being reset to the top of the window when navigating */ diff --git a/packages/react-router/lib/dom/lib.tsx b/packages/react-router/lib/dom/lib.tsx index a2c3b7212a..68392900d9 100644 --- a/packages/react-router/lib/dom/lib.tsx +++ b/packages/react-router/lib/dom/lib.tsx @@ -1171,7 +1171,53 @@ export interface LinkProps */ relative?: RelativeRoutingType; - rewrite?: To; + /** + * Rewrite path to "rewrite" the URL the router navigates to internally, while + * reflecting the `to` URL in the browser. Useful for contextual navigations + * such as opening an image in a model on top of a gallery while keeping the + * underlying gallery active. + * + * The rewrite location is managed in history state, so if the user shares a URL + * or opens in a new tab, the rewrite location will not be included and the user + * will land on the non-contextual location. This also means rewrite's are only + * intended for SPA uses and SSR renders will not respect the rewrite. + * + * ```tsx + * // routes/gallery.tsx + * export function clientLoader({ request }: Route.LoaderArgs) { + * let sp = new URL(request.url).searchParams; + * return { + * images: getImages(), + * modalImage: sp.has("image") ? getImage(sp.get("image")!) : null, + * }; + * } + * + * export default function Gallery({ loaderData }: Route.ComponentProps) { + * return ( + * <> + * + * {loaderData.images.map((image) => ( + * + * {image.alt} + * + * ))} + * + * + * {data.modalImage ? ( + * + * {data.modalImage.alt} + * + * ) : null} + * + * ); + * } + * ``` + */ + unstable_rewrite?: To; /** * Can be a string or a partial {@link Path}: @@ -1249,6 +1295,7 @@ const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i; * @param {LinkProps.relative} props.relative n/a * @param {LinkProps.reloadDocument} props.reloadDocument n/a * @param {LinkProps.replace} props.replace n/a + * @param {LinkProps.unstable_rewrite} props.unstable_rewrite [modes: framework, data] n/a * @param {LinkProps.state} props.state n/a * @param {LinkProps.to} props.to n/a * @param {LinkProps.viewTransition} props.viewTransition [modes: framework, data] n/a @@ -1263,7 +1310,7 @@ export const Link = React.forwardRef( relative, reloadDocument, replace, - rewrite, + unstable_rewrite, state, target, to, @@ -2103,7 +2150,7 @@ function useDataRouterState(hookName: DataRouterStateHook) { * to use for the link. Defaults to `"route"`. * @param options.replace Whether to replace the current [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History) * entry instead of pushing a new one. Defaults to `false`. - * @param options.rewrite Rewrite the URL for this navigation. Defaults to `undefined`. + * @param options.unstable_rewrite Rewrite the URL for this navigation. Defaults to `undefined`. * @param options.state The state to add to the [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History) * entry for this navigation. Defaults to `undefined`. * @param options.target The target attribute for the link. Defaults to `undefined`. @@ -2122,7 +2169,7 @@ export function useLinkClickHandler( { target, replace: replaceProp, - rewrite, + unstable_rewrite, state, preventScrollReset, relative, @@ -2132,7 +2179,7 @@ export function useLinkClickHandler( }: { target?: React.HTMLAttributeAnchorTarget; replace?: boolean; - rewrite?: To; + unstable_rewrite?: To; state?: any; preventScrollReset?: boolean; relative?: RelativeRoutingType; @@ -2160,7 +2207,7 @@ export function useLinkClickHandler( let doNavigate = () => navigate(to, { replace, - rewrite, + unstable_rewrite: rewrite, state, preventScrollReset, relative, diff --git a/packages/react-router/lib/router/router.ts b/packages/react-router/lib/router/router.ts index f7937dbda9..aca1319f65 100644 --- a/packages/react-router/lib/router/router.ts +++ b/packages/react-router/lib/router/router.ts @@ -535,7 +535,7 @@ type BaseNavigateOptions = BaseNavigateOrFetchOptions & { state?: any; fromRouteId?: string; viewTransition?: boolean; - rewrite?: To; + unstable_rewrite?: To; }; // Only allowed for submission navigations @@ -1525,13 +1525,13 @@ export function createRouter(init: RouterInit): Router { let currentLocation = state.location; // If rewrite is provided, normalize and create a separate path for the router - let rewritePath = opts?.rewrite + let rewritePath = opts?.unstable_rewrite ? init.history.encodeLocation( normalizeTo( state.location, state.matches, basename, - opts.rewrite, + opts.unstable_rewrite, opts?.fromRouteId, opts?.relative, ), From 298dd543673ef28b6305ce92bc5a2667ffb7dea4 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 8 Jan 2026 16:25:50 -0500 Subject: [PATCH 09/28] Few missed renames --- packages/react-router/lib/dom/lib.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-router/lib/dom/lib.tsx b/packages/react-router/lib/dom/lib.tsx index 68392900d9..10d6b69b31 100644 --- a/packages/react-router/lib/dom/lib.tsx +++ b/packages/react-router/lib/dom/lib.tsx @@ -1337,7 +1337,7 @@ export const Link = React.forwardRef( let internalOnClick = useLinkClickHandler(to, { replace, - rewrite, + unstable_rewrite, state, target, preventScrollReset, @@ -2207,7 +2207,7 @@ export function useLinkClickHandler( let doNavigate = () => navigate(to, { replace, - unstable_rewrite: rewrite, + unstable_rewrite, state, preventScrollReset, relative, @@ -2228,7 +2228,7 @@ export function useLinkClickHandler( navigate, path, replaceProp, - rewrite, + unstable_rewrite, state, target, to, From 3fe27584dbb605b2a511d1229ded536298f3bf8f Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 8 Jan 2026 16:27:06 -0500 Subject: [PATCH 10/28] Add changeset --- .changeset/silly-badgers-cough.md | 40 +++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .changeset/silly-badgers-cough.md diff --git a/.changeset/silly-badgers-cough.md b/.changeset/silly-badgers-cough.md new file mode 100644 index 0000000000..2549218863 --- /dev/null +++ b/.changeset/silly-badgers-cough.md @@ -0,0 +1,40 @@ +--- +"react-router": patch +--- + +[UNSTABLE] Add support for `` which allows users to navigate to one URL in the browser but "rewrite" the url that is processed by the router, permitting contextual routing usages such as displaying an image in a model on top of a gallery + +```tsx +// routes/gallery.tsx +export function clientLoader({ request }: Route.LoaderArgs) { + let sp = new URL(request.url).searchParams; + return { + images: getImages(), + modalImage: sp.has("image") ? getImage(sp.get("image")!) : null, + }; +} + +export default function Gallery({ loaderData }: Route.ComponentProps) { + return ( + <> + + {loaderData.images.map((image) => ( + + {image.alt} + + ))} + + + {data.modalImage ? ( + + {data.modalImage.alt} + + ) : null} + + ); +} +``` From ce02b1701d06294e15819e71c7f4d5a7c0232521 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 8 Jan 2026 16:41:26 -0500 Subject: [PATCH 11/28] Fix tests --- .../__tests__/router/rewrite-test.ts | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/react-router/__tests__/router/rewrite-test.ts b/packages/react-router/__tests__/router/rewrite-test.ts index 9509d1c97b..cbc151dac7 100644 --- a/packages/react-router/__tests__/router/rewrite-test.ts +++ b/packages/react-router/__tests__/router/rewrite-test.ts @@ -36,7 +36,7 @@ describe("rewrite", () => { expect(t.router.state.location.search).toBe(""); // The location state should contain the rewrite information - expect(t.router.state.location.state?._rewrite).toEqual({ + expect(t.router.state.location.rewrite).toEqual({ pathname: "/gallery", search: "?photo=123", hash: "", @@ -82,7 +82,7 @@ describe("rewrite", () => { expect(t.router.state.location.pathname).toBe("/images/123"); // The rewrite should still be in the state - expect(t.router.state.location.state?._rewrite).toEqual({ + expect(t.router.state.location.rewrite).toEqual({ pathname: "/gallery", search: "?photo=123", hash: "", @@ -114,7 +114,7 @@ describe("rewrite", () => { await A.loaders.gallery.resolve("GALLERY DATA"); expect(t.router.state.location.pathname).toBe("/images/123"); - expect(t.router.state.location.state?._rewrite).toEqual({ + expect(t.router.state.location.rewrite).toEqual({ pathname: "/gallery", search: "?photo=123", hash: "", @@ -151,7 +151,7 @@ describe("rewrite", () => { expect(t.router.state.location.hash).toBe("#preview"); // The rewrite should have the rewrite hash - expect(t.router.state.location.state?._rewrite).toEqual({ + expect(t.router.state.location.rewrite).toEqual({ pathname: "/gallery", search: "?photo=123", hash: "#header", @@ -177,14 +177,12 @@ describe("rewrite", () => { await A.loaders.gallery.resolve("GALLERY DATA"); - // Both custom state and _rewrite should be present - expect(t.router.state.location.state).toEqual({ - customData: "test", - _rewrite: { - pathname: "/gallery", - search: "?photo=123", - hash: "", - }, + // Both custom state and rewrite should be present + expect(t.router.state.location.state).toEqual({ customData: "test" }); + expect(t.router.state.location.rewrite).toEqual({ + pathname: "/gallery", + search: "?photo=123", + hash: "", }); t.router.dispose(); @@ -223,7 +221,7 @@ describe("rewrite", () => { // The browser location should be /images/123 expect(t.router.state.location.pathname).toBe("/images/123"); - expect(t.router.state.location.state?._rewrite).toEqual({ + expect(t.router.state.location.rewrite).toEqual({ pathname: "/gallery", search: "?photo=123", hash: "", @@ -279,7 +277,7 @@ describe("rewrite", () => { await A.loaders.images.resolve("IMAGES DATA"); expect(t.router.state.location.pathname).toBe("/images/123"); - expect(t.router.state.location.state?._rewrite).toBeUndefined(); + expect(t.router.state.location.rewrite).toBeUndefined(); expect(t.router.state.matches).toHaveLength(1); expect(t.router.state.matches[0].route.id).toBe("images"); From 3d54c92a077ae2ccc3eabc4a2f809f6aa73098f8 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Fri, 9 Jan 2026 09:57:33 -0500 Subject: [PATCH 12/28] A few missed renames --- .changeset/silly-badgers-cough.md | 2 +- packages/react-router/lib/dom/lib.tsx | 2 +- playground/framework/app/photos.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/silly-badgers-cough.md b/.changeset/silly-badgers-cough.md index 2549218863..0f3532e019 100644 --- a/.changeset/silly-badgers-cough.md +++ b/.changeset/silly-badgers-cough.md @@ -22,7 +22,7 @@ export default function Gallery({ loaderData }: Route.ComponentProps) { {image.alt} diff --git a/packages/react-router/lib/dom/lib.tsx b/packages/react-router/lib/dom/lib.tsx index 10d6b69b31..6cb1a67dfa 100644 --- a/packages/react-router/lib/dom/lib.tsx +++ b/packages/react-router/lib/dom/lib.tsx @@ -1200,7 +1200,7 @@ export interface LinkProps * * {image.alt} * diff --git a/playground/framework/app/photos.tsx b/playground/framework/app/photos.tsx index 4b2a015822..8aedaddcd4 100644 --- a/playground/framework/app/photos.tsx +++ b/playground/framework/app/photos.tsx @@ -63,7 +63,7 @@ export function ImageGallery() { Date: Mon, 12 Jan 2026 10:39:14 -0500 Subject: [PATCH 13/28] Update to fix useLocation + relative routing --- packages/react-router/lib/components.tsx | 12 +----------- packages/react-router/lib/hooks.tsx | 4 ++-- packages/react-router/lib/router/router.ts | 13 ++++++++++++- playground/framework/app/root.tsx | 15 +++------------ playground/framework/app/routes/_index.tsx | 15 ++++++++++++--- playground/framework/app/routes/image.tsx | 4 +--- playground/framework/react-router.config.ts | 2 +- 7 files changed, 32 insertions(+), 33 deletions(-) diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx index f5b081f604..275592b921 100644 --- a/packages/react-router/lib/components.tsx +++ b/packages/react-router/lib/components.tsx @@ -724,16 +724,6 @@ export function RouterProvider({ [router, navigator, basename, onError], ); - let uiLocation = state.location.rewrite || state.location; - if (state.location.rewrite) { - uiLocation = { - ...state.location, - pathname: state.location.rewrite.pathname, - search: state.location.rewrite.search, - hash: state.location.rewrite.hash, - }; - } - // The fragment and {null} here are important! We need them to keep React 18's // useId happy when we are server-rendering since we may have a + + diff --git a/examples/modal-data-router/package-lock.json b/examples/modal-data-router/package-lock.json new file mode 100644 index 0000000000..4764e4af2c --- /dev/null +++ b/examples/modal-data-router/package-lock.json @@ -0,0 +1,1470 @@ +{ + "name": "modal-data-router", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "modal", + "dependencies": { + "react": "^19", + "react-dom": "^19", + "react-router": "0.0.0-experimental-e067e5e52" + }, + "devDependencies": { + "@rollup/plugin-replace": "^6.0.3", + "@types/react": "^19", + "@types/react-dom": "^19", + "@vitejs/plugin-react": "latest", + "typescript": "latest", + "vite": "latest" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", + "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.3.tgz", + "integrity": "sha512-aNtko9OPOwVESUFp3MZfD8Uzxl7JzSeJpd7npIoxCasU37PFbAQRpKglkaKwlHOyeJdrREpo8TW8ldrkYWwvIQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.22.1", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.1.tgz", + "integrity": "sha512-Hkqu7J4ynysSXxmAahpN1jjRwVJ+NdpraFLIWflgjpVob3KNyK3/tIUc7Q7szed8WMp0JNa7Qtd1E9Oo22F9gA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.21.4", + "@babel/generator": "^7.22.0", + "@babel/helper-compilation-targets": "^7.22.1", + "@babel/helper-module-transforms": "^7.22.1", + "@babel/helpers": "^7.22.0", + "@babel/parser": "^7.22.0", + "@babel/template": "^7.21.9", + "@babel/traverse": "^7.22.1", + "@babel/types": "^7.22.0", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.3.tgz", + "integrity": "sha512-C17MW4wlk//ES/CJDL51kPNwl+qiBQyN7b9SKyVp11BLGFeSPoVaHrv+MNt8jwQFhQWowW88z1eeBx3pFz9v8A==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.3", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.22.1", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.1.tgz", + "integrity": "sha512-Rqx13UM3yVB5q0D/KwQ8+SPfX/+Rnsy1Lw1k/UwOC4KC6qrzIQoY3lYnBu5EHKBlEHHcj0M0W8ltPSkD8rqfsQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.0", + "@babel/helper-validator-option": "^7.21.0", + "browserslist": "^4.21.3", + "lru-cache": "^5.1.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.1", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.1.tgz", + "integrity": "sha512-Z2tgopurB/kTbidvzeBrc2To3PUP/9i5MUe+fU6QJCQDyPwSH2oRapkLw3KGECDYSjhQZCNxEvNvZlLw8JjGwA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", + "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.20.7", + "@babel/types": "^7.21.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", + "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.21.4.tgz", + "integrity": "sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.21.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.22.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.1.tgz", + "integrity": "sha512-dxAe9E7ySDGbQdCVOY/4+UcD8M9ZFqZcZhSPsPacvCG4M+9lwtDDQfI2EoaSvmf7W/8yCBkGU0m7Pvt1ru3UZw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.1", + "@babel/helper-module-imports": "^7.21.4", + "@babel/helper-simple-access": "^7.21.5", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.19.1", + "@babel/template": "^7.21.9", + "@babel/traverse": "^7.22.1", + "@babel/types": "^7.22.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.21.5.tgz", + "integrity": "sha512-0WDaIlXKOX/3KfBK/dwP1oQGiPh6rjMkT7HIRv7i5RR2VUMwrx5ZL0dwBkKx7+SW1zwNdgjHd34IMk5ZjTeHVg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.21.5.tgz", + "integrity": "sha512-ENPDAMC1wAjR0uaCUwliBdiSl1KBJAVnMTzXqi64c2MG8MPR6ii4qf7bSXDqSFbr4W6W028/rf5ivoHop5/mkg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.21.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz", + "integrity": "sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz", + "integrity": "sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.3.tgz", + "integrity": "sha512-jBJ7jWblbgr7r6wYZHMdIqKc73ycaTcCaWRq4/2LpuPHcx7xMlZvpGQkOYc9HeSjn6rcx15CPlgVcBtZ4WZJ2w==", + "dev": true, + "dependencies": { + "@babel/template": "^7.21.9", + "@babel/traverse": "^7.22.1", + "@babel/types": "^7.22.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.22.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.4.tgz", + "integrity": "sha512-VLLsx06XkEYqBtE5YGPwfSGwfrjnyPP5oiGty3S8pQLFDFLaS8VwWSIxkTXpcvr5zeYLE6+MBNl2npl/YnfofA==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.21.0.tgz", + "integrity": "sha512-f/Eq+79JEu+KUANFks9UZCcvydOOGMgF7jBrcwjHa5jTZD8JivnhCJYvmlhR/WTXBWonDExPoW0eO/CR4QJirA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.19.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.19.6.tgz", + "integrity": "sha512-RpAi004QyMNisst/pvSanoRdJ4q+jMCWyk9zdw/CyLB9j8RXEahodR6l2GyttDRyEVWZtbN+TpLiHJ3t34LbsQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.21.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.21.9.tgz", + "integrity": "sha512-MK0X5k8NKOuWRamiEfc3KEJiHMTkGZNUjzMipqCGDDc6ijRl/B7RGSKVGncu4Ro/HdyzzY6cmoXuKI2Gffk7vQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.21.4", + "@babel/parser": "^7.21.9", + "@babel/types": "^7.21.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.22.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.4.tgz", + "integrity": "sha512-Tn1pDsjIcI+JcLKq1AVlZEr4226gpuAQTsLMorsYg9tuS/kG7nuwwJ4AB8jfQuEgb/COBwR/DqJxmoiYFu5/rQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.21.4", + "@babel/generator": "^7.22.3", + "@babel/helper-environment-visitor": "^7.22.1", + "@babel/helper-function-name": "^7.21.0", + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/parser": "^7.22.4", + "@babel/types": "^7.22.4", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.22.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.4.tgz", + "integrity": "sha512-Tx9x3UBHTTsMSW85WB2kphxYQVvrZ/t1FxD88IpSgIjiUJlCm9z+xWIDwyo1vffTwSqteqyznB8ZE9vYYk16zA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.21.5", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", + "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", + "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", + "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", + "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", + "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", + "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", + "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", + "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", + "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", + "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", + "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", + "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", + "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", + "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", + "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", + "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", + "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", + "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", + "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", + "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", + "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", + "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@rollup/plugin-replace": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.3.tgz", + "integrity": "sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-replace/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "18.16.16", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.16.tgz", + "integrity": "sha512-NpaM49IGQQAUlBhHMF82QH80J08os4ZmyF9MkpCzWAGuOHqE4gTEbhzd7L3l5LmWuZ6E0OiC1FweQ4tsiW35+g==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-3.1.0.tgz", + "integrity": "sha512-AfgcRL8ZBhAlc3BFdigClmTUMISmmzHn7sB2h9U1odvc5U/MjWXsAaz18b/WoppUTDBzxOJwo2VdClfUcItu9g==", + "dev": true, + "dependencies": { + "@babel/core": "^7.20.12", + "@babel/plugin-transform-react-jsx-self": "^7.18.6", + "@babel/plugin-transform-react-jsx-source": "^7.19.6", + "magic-string": "^0.27.0", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.1.0-beta.0" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/browserslist": { + "version": "4.21.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.7.tgz", + "integrity": "sha512-BauCXrQ7I2ftSqd2mvKHGo85XR0u7Ru3C/Hxsy/0TkfCtjrmAbPdzLGasmoiBxplpDXlPvdjX9u7srIMfgasNA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001489", + "electron-to-chromium": "^1.4.411", + "node-releases": "^2.0.12", + "update-browserslist-db": "^1.0.11" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001494", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001494.tgz", + "integrity": "sha512-sY2B5Qyl46ZzfYDegrl8GBCzdawSLT4ThM9b9F+aDYUrAG2zCOyMbd2Tq34mS1g4ZKBfjRlzOohQMxx28x6wJg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/csstype": { + "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", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.419", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.419.tgz", + "integrity": "sha512-jdie3RiEgygvDTyS2sgjq71B36q2cDSBfPlwzUyuOrfYTNoYWyBxxjGJV/HAu3A2hB0Y+HesvCVkVAFoCKwCSw==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", + "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.19", + "@esbuild/android-arm64": "0.17.19", + "@esbuild/android-x64": "0.17.19", + "@esbuild/darwin-arm64": "0.17.19", + "@esbuild/darwin-x64": "0.17.19", + "@esbuild/freebsd-arm64": "0.17.19", + "@esbuild/freebsd-x64": "0.17.19", + "@esbuild/linux-arm": "0.17.19", + "@esbuild/linux-arm64": "0.17.19", + "@esbuild/linux-ia32": "0.17.19", + "@esbuild/linux-loong64": "0.17.19", + "@esbuild/linux-mips64el": "0.17.19", + "@esbuild/linux-ppc64": "0.17.19", + "@esbuild/linux-riscv64": "0.17.19", + "@esbuild/linux-s390x": "0.17.19", + "@esbuild/linux-x64": "0.17.19", + "@esbuild/netbsd-x64": "0.17.19", + "@esbuild/openbsd-x64": "0.17.19", + "@esbuild/sunos-x64": "0.17.19", + "@esbuild/win32-arm64": "0.17.19", + "@esbuild/win32-ia32": "0.17.19", + "@esbuild/win32-x64": "0.17.19" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", + "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.12.tgz", + "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.4.24", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", + "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-refresh": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", + "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "0.0.0-experimental-e067e5e52", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-0.0.0-experimental-e067e5e52.tgz", + "integrity": "sha512-TmZHpFVD2JQiPuq8Pt4oyKRupuQo0HXkYxNvJ/TI7ZTI9jApuKYKvglbRDnpjl9hcWOk17NqdeCwIphy3nm9eg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/rollup": { + "version": "3.23.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.23.1.tgz", + "integrity": "sha512-ybRdFVHOoljGEFILHLd2g/qateqUdjE6YS41WXq4p3C/WwD3xtWxV4FYWETA1u9TeXQc5K8L8zHE5d/scOvrOQ==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz", + "integrity": "sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==", + "dev": true, + "dependencies": { + "esbuild": "^0.17.5", + "postcss": "^8.4.23", + "rollup": "^3.21.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } +} diff --git a/examples/modal-data-router/package.json b/examples/modal-data-router/package.json new file mode 100644 index 0000000000..428803268a --- /dev/null +++ b/examples/modal-data-router/package.json @@ -0,0 +1,21 @@ +{ + "name": "modal-data-router", + "private": true, + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "serve": "vite preview" + }, + "dependencies": { + "react": "^19", + "react-dom": "^19", + "react-router": "0.0.0-experimental-e067e5e52" + }, + "devDependencies": { + "@types/react": "^19", + "@types/react-dom": "^19", + "@vitejs/plugin-react": "latest", + "typescript": "latest", + "vite": "latest" + } +} diff --git a/examples/modal-data-router/src/App.tsx b/examples/modal-data-router/src/App.tsx new file mode 100644 index 0000000000..6d24db54b8 --- /dev/null +++ b/examples/modal-data-router/src/App.tsx @@ -0,0 +1,380 @@ +import * as React from "react"; +import { createPortal } from "react-dom"; +import { + Link, + Outlet, + RouterProvider, + createBrowserRouter, + useLoaderData, + useNavigate, + useParams, + type LoaderFunctionArgs, +} from "react-router"; + +import { IMAGES, getImageById, loadImage } from "./images"; + +const router = createBrowserRouter([ + { + path: "/", + Component: Root, + children: [ + { + Component: Layout, + children: [ + { + index: true, + Component: Home, + }, + { + path: "gallery", + Component: Gallery, + async loader({ request }: LoaderFunctionArgs) { + // When the gallery route has an img param, we load that image to + // show it in a modal + let searchParams = new URL(request.url).searchParams; + let image = searchParams.has("img") + ? loadImage(searchParams.get("img")!) + : null; + return { image }; + }, + }, + { + path: "img/:id", + Component: ImageView, + async loader({ params }: LoaderFunctionArgs) { + return loadImage(params.id!); + }, + }, + { + path: "*", + Component: NoMatch, + }, + ], + }, + ], + }, +]); + +export default function App() { + return ; +} + +// Root component +function Root() { + return ( +
+

Modal Example

+ +

+ This is an example of how to create a contextual modal navigation with + React Router where the navigation path the user takes determines if the + page is rendered in the modal or not (popularized by pinterest, + instagram, and others in the 2010s). This type of modal is typically + used as a kind of "detail" view to focus on a particular object in a + collection (like a pinterest board) while not taking you completely out + of context of the parent page. But, when the same URL is visited + directly (rather than from the collection page) it renders as it's own + full page instead of in a modal. +

+ +

+ In this example, notice how the URL updates when the modal opens (if you + are viewing the example in StackBlitz you may need to open in a new + browser window). Even though the URL is updated to the specific item in + the modal, the background page is still showing behind it. +

+ +

+ Next, copy and paste the URL to a new browser tab and notice that it + shows that specific item not in a modal, but directly on the page. This + is the view that someone would see if they clicked on a link that you + sent them when you had the modal open. They don't have the context you + did when you opened the modal, so they don't see it in the context of + the background page. +

+ + +
+ ); +} + +function Layout() { + return ( +
+ + +
+ + +
+ ); +} + +function Home() { + return ( +
+

Home

+ +

Featured Images

+
    +
  • + Image 1 +
  • +
  • + Image 2 +
  • +
+
+ ); +} + +function Gallery() { + let data = useLoaderData() as { + image: ReturnType | null; + }; + return ( +
+

Gallery

+
+ {IMAGES.map((image) => ( + // This is the trick! We tell the router to navigate to `/gallery?img=1` + // and we can use the search param to overlay a modal on the gallery. + // But, we also tell the router to mask the URL displayed in the href + // and the URL bar + + {image.title} + + ))} +
+ + {/* Display the modal if data was returned */} + {data.image ? : null} +
+ ); +} + +function ImageView() { + let { id } = useParams<"id">(); + let image = getImageById(Number(id)); + + if (!image) return
Image not found
; + + return ( +
+

{image.title}

+ +
+ ); +} + +function Modal({ image }: { image: ReturnType }) { + let navigate = useNavigate(); + let buttonRef = React.useRef(null); + + function onDismiss() { + navigate(-1); + } + + if (!image) return null; + + return ( + } + > +
+

+ {image.title} +

+ + +
+
+ ); +} + +function NoMatch() { + return ( +
+

Nothing to see here!

+

+ Go to the home page +

+
+ ); +} + +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")); +} diff --git a/examples/modal-data-router/src/images.ts b/examples/modal-data-router/src/images.ts new file mode 100644 index 0000000000..bf13a7da04 --- /dev/null +++ b/examples/modal-data-router/src/images.ts @@ -0,0 +1,34 @@ +export const IMAGES = [ + { + id: 0, + title: "Enjoying a cup of coffee", + src: "https://images.unsplash.com/photo-1631016800696-5ea8801b3c2a?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=400&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTYzMzM2Mzg4Ng&ixlib=rb-1.2.1&q=80&w=400", + }, + { + id: 1, + title: "Magical winter sunrise", + src: "https://images.unsplash.com/photo-1618824834718-92f8469a4dd1?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=400&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTYzMzM2NDAzMw&ixlib=rb-1.2.1&q=80&w=400", + }, + { + id: 2, + title: "Dalmatian and pumpkins", + src: "https://images.unsplash.com/photo-1633289944756-6295be214e16?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=400&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTYzMzM2NDA3Nw&ixlib=rb-1.2.1&q=80&w=400", + }, + { + id: 3, + title: "Fall into Autumn 🍂🐶", + src: "https://images.unsplash.com/photo-1633172905740-2eb6730c95b4?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=400&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTYzMzM2NDEwMg&ixlib=rb-1.2.1&q=80&w=400", + }, +]; + +export function getImageById(id: number) { + return IMAGES.find((image) => image.id === id); +} + +export function loadImage(id: string | number) { + const image = getImageById(Number(id)); + if (!image) { + throw new Response("Image not found", { status: 404 }); + } + return image; +} diff --git a/examples/modal-data-router/src/index.css b/examples/modal-data-router/src/index.css new file mode 100644 index 0000000000..d9e5f08c13 --- /dev/null +++ b/examples/modal-data-router/src/index.css @@ -0,0 +1,26 @@ +html { + box-sizing: border-box; +} + +* { + box-sizing: inherit; +} + +body { + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", + "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: + source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; +} + +[data-reach-dialog-content] { + width: 90%; + max-width: 500px; + border-radius: 8px; +} diff --git a/examples/modal-data-router/src/main.tsx b/examples/modal-data-router/src/main.tsx new file mode 100644 index 0000000000..0c19b95840 --- /dev/null +++ b/examples/modal-data-router/src/main.tsx @@ -0,0 +1,11 @@ +import * as React from "react"; +import { createRoot } from "react-dom/client"; + +import App from "./App"; +import "./index.css"; + +createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/examples/modal-data-router/src/vite-env.d.ts b/examples/modal-data-router/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/modal-data-router/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/modal-data-router/tsconfig.json b/examples/modal-data-router/tsconfig.json new file mode 100644 index 0000000000..f1fa196aad --- /dev/null +++ b/examples/modal-data-router/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "target": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["./src"] +} diff --git a/examples/modal-data-router/vite.config.ts b/examples/modal-data-router/vite.config.ts new file mode 100644 index 0000000000..356646c1cc --- /dev/null +++ b/examples/modal-data-router/vite.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vite"; + +// https://vitejs.dev/config/ +export default defineConfig({ + server: { + port: 3000, + }, +}); From 56b72276e6f5dc2fb45803781573b6877302576a Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 12 Feb 2026 11:13:49 -0500 Subject: [PATCH 24/28] Update docs and switch to masked field on history.state --- examples/modal-data-router/package-lock.json | 85 +--------------- packages/react-router/lib/dom/lib.tsx | 102 +++++++++---------- packages/react-router/lib/router/history.ts | 10 +- 3 files changed, 57 insertions(+), 140 deletions(-) diff --git a/examples/modal-data-router/package-lock.json b/examples/modal-data-router/package-lock.json index 4764e4af2c..48c8d91339 100644 --- a/examples/modal-data-router/package-lock.json +++ b/examples/modal-data-router/package-lock.json @@ -4,14 +4,13 @@ "requires": true, "packages": { "": { - "name": "modal", + "name": "modal-data-router", "dependencies": { "react": "^19", "react-dom": "^19", "react-router": "0.0.0-experimental-e067e5e52" }, "devDependencies": { - "@rollup/plugin-replace": "^6.0.3", "@types/react": "^19", "@types/react-dom": "^19", "@vitejs/plugin-react": "latest", @@ -768,68 +767,6 @@ "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", "dev": true }, - "node_modules/@rollup/plugin-replace": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.3.tgz", - "integrity": "sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "magic-string": "^0.30.3" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-replace/node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/@rollup/pluginutils": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", - "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/node": { "version": "18.16.16", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.16.tgz", @@ -1074,13 +1011,6 @@ "node": ">=0.8.0" } }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true, - "license": "MIT" - }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -1209,19 +1139,6 @@ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", "dev": true }, - "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/postcss": { "version": "8.4.24", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", diff --git a/packages/react-router/lib/dom/lib.tsx b/packages/react-router/lib/dom/lib.tsx index 7de48a136a..1189b57f43 100644 --- a/packages/react-router/lib/dom/lib.tsx +++ b/packages/react-router/lib/dom/lib.tsx @@ -1173,6 +1173,54 @@ export interface LinkProps */ relative?: RelativeRoutingType; + /** + * Can be a string or a partial {@link Path}: + * + * ```tsx + * + * + * + * ``` + */ + to: To; + + /** + * Enables a [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) + * for this navigation. + * + * ```jsx + * + * Click me + * + * ``` + * + * To apply specific styles for the transition, see {@link useViewTransitionState} + */ + viewTransition?: boolean; + + /** + * Specify the default revalidation behavior for the navigation. + * + * ```tsx + * + * ``` + * + * If no `shouldRevalidate` functions are present on the active routes, then this + * value will be used directly. Otherwise it will be passed into `shouldRevalidate` + * so the route can make the final determination on revalidation. This can be + * useful when updating search params and you don't want to trigger a revalidation. + * + * By default (when not specified), loaders will revalidate according to the routers + * standard revalidation behavior. + */ + unstable_defaultShouldRevalidate?: boolean; + /** * Masked path for for this navigation, when you want to navigate the router to * one location but display a separate location in the URL bar. @@ -1221,54 +1269,6 @@ export interface LinkProps * ``` */ unstable_mask?: To; - - /** - * Can be a string or a partial {@link Path}: - * - * ```tsx - * - * - * - * ``` - */ - to: To; - - /** - * Enables a [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) - * for this navigation. - * - * ```jsx - * - * Click me - * - * ``` - * - * To apply specific styles for the transition, see {@link useViewTransitionState} - */ - viewTransition?: boolean; - - /** - * Specify the default revalidation behavior for the navigation. - * - * ```tsx - * - * ``` - * - * If no `shouldRevalidate` functions are present on the active routes, then this - * value will be used directly. Otherwise it will be passed into `shouldRevalidate` - * so the route can make the final determination on revalidation. This can be - * useful when updating search params and you don't want to trigger a revalidation. - * - * By default (when not specified), loaders will revalidate according to the routers - * standard revalidation behavior. - */ - unstable_defaultShouldRevalidate?: boolean; } const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i; @@ -1298,11 +1298,11 @@ const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i; * @param {LinkProps.relative} props.relative n/a * @param {LinkProps.reloadDocument} props.reloadDocument n/a * @param {LinkProps.replace} props.replace n/a - * @param {LinkProps.unstable_mask} props.unstable_mask [modes: framework, data] n/a * @param {LinkProps.state} props.state n/a * @param {LinkProps.to} props.to n/a * @param {LinkProps.viewTransition} props.viewTransition [modes: framework, data] n/a * @param {LinkProps.unstable_defaultShouldRevalidate} props.unstable_defaultShouldRevalidate n/a + * @param {LinkProps.unstable_mask} props.unstable_mask [modes: framework, data] n/a */ export const Link = React.forwardRef( function LinkWithRef( @@ -2185,8 +2185,6 @@ function useDataRouterState(hookName: DataRouterStateHook) { * to use for the link. Defaults to `"route"`. * @param options.replace Whether to replace the current [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History) * entry instead of pushing a new one. Defaults to `false`. - * @param options.unstable_mask Masked location to display in the browser instead - * of the router location. Defaults to `undefined`. * @param options.state The state to add to the [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History) * entry for this navigation. Defaults to `undefined`. * @param options.target The target attribute for the link. Defaults to `undefined`. @@ -2195,6 +2193,8 @@ function useDataRouterState(hookName: DataRouterStateHook) { * {@link useViewTransitionState}. Defaults to `false`. * @param options.unstable_defaultShouldRevalidate Specify the default revalidation * behavior for the navigation. Defaults to `true`. + * @param options.unstable_mask Masked location to display in the browser instead + * of the router location. Defaults to `undefined`. * @param options.unstable_useTransitions Wraps the navigation in * [`React.startTransition`](https://react.dev/reference/react/startTransition) * for concurrent rendering. Defaults to `false`. diff --git a/packages/react-router/lib/router/history.ts b/packages/react-router/lib/router/history.ts index 7fa80bed72..301522fcfd 100644 --- a/packages/react-router/lib/router/history.ts +++ b/packages/react-router/lib/router/history.ts @@ -195,7 +195,7 @@ type HistoryState = { usr: any; key?: string; idx: number; - unstable_maskFrom?: Path; + masked?: Path; }; const PopStateEventType = "popstate"; @@ -386,15 +386,15 @@ export function createBrowserHistory( window: Window, globalHistory: Window["history"], ) { - let maskFrom = globalHistory.state?.unstable_maskFrom; - let { pathname, search, hash } = maskFrom || window.location; + let maskedLocation = (globalHistory.state as HistoryState)?.masked; + let { pathname, search, hash } = maskedLocation || window.location; return createLocation( "", { pathname, search, hash }, // state defaults to `null` because `window.history.state` does (globalHistory.state && globalHistory.state.usr) || null, (globalHistory.state && globalHistory.state.key) || "default", - maskFrom + maskedLocation ? { pathname: window.location.pathname, search: window.location.search, @@ -552,7 +552,7 @@ function getHistoryState(location: Location, index: number): HistoryState { usr: location.state, key: location.key, idx: index, - unstable_maskFrom: location.unstable_mask + masked: location.unstable_mask ? { pathname: location.pathname, search: location.search, From b305a785be45cd812ea2388b8f513e3a31e932d2 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 12 Feb 2026 11:23:04 -0500 Subject: [PATCH 25/28] Update example to latest version --- examples/modal-data-router/package-lock.json | 8 ++++---- examples/modal-data-router/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/modal-data-router/package-lock.json b/examples/modal-data-router/package-lock.json index 48c8d91339..c65e530ac7 100644 --- a/examples/modal-data-router/package-lock.json +++ b/examples/modal-data-router/package-lock.json @@ -8,7 +8,7 @@ "dependencies": { "react": "^19", "react-dom": "^19", - "react-router": "0.0.0-experimental-e067e5e52" + "react-router": "^0.0.0-experimental-56b72276e" }, "devDependencies": { "@types/react": "^19", @@ -1198,9 +1198,9 @@ } }, "node_modules/react-router": { - "version": "0.0.0-experimental-e067e5e52", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-0.0.0-experimental-e067e5e52.tgz", - "integrity": "sha512-TmZHpFVD2JQiPuq8Pt4oyKRupuQo0HXkYxNvJ/TI7ZTI9jApuKYKvglbRDnpjl9hcWOk17NqdeCwIphy3nm9eg==", + "version": "0.0.0-experimental-56b72276e", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-0.0.0-experimental-56b72276e.tgz", + "integrity": "sha512-O0txUxjmjyebH3N20ybmt7p8nCjyAJXWXYm7fXO8xgCCEOXaKco4A1J2OQMrHwI1yLdqd6YpnGW6LjLcQ9HXRQ==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", diff --git a/examples/modal-data-router/package.json b/examples/modal-data-router/package.json index 428803268a..5c3b7a3bd4 100644 --- a/examples/modal-data-router/package.json +++ b/examples/modal-data-router/package.json @@ -9,7 +9,7 @@ "dependencies": { "react": "^19", "react-dom": "^19", - "react-router": "0.0.0-experimental-e067e5e52" + "react-router": "^0.0.0-experimental-56b72276e" }, "devDependencies": { "@types/react": "^19", From 6d530078d54ab72816d1436243a75d5857d8d50a Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 12 Feb 2026 11:24:17 -0500 Subject: [PATCH 26/28] Update SSR reset to use new masked field --- packages/react-router/lib/dom/ssr/components.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-router/lib/dom/ssr/components.tsx b/packages/react-router/lib/dom/ssr/components.tsx index c34fa5488a..9bc3132b7b 100644 --- a/packages/react-router/lib/dom/ssr/components.tsx +++ b/packages/react-router/lib/dom/ssr/components.tsx @@ -793,8 +793,8 @@ export function Scripts(scriptProps: ScriptsProps): React.JSX.Element | null { // browser URL let maskScript = ssr ? [ - "if (window.history.state && window.history.state.unstable_maskFrom) {", - "window.history.replaceState({ ...window.history.state, unstable_maskFrom: undefined }, null);", + "if (window.history.state && window.history.state.masked) {", + "window.history.replaceState({ ...window.history.state, masked: undefined }, null);", "}", ].join("") : ""; From c50fa92b50c554c3a8578425b31349a9cd9de276 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 12 Feb 2026 11:29:29 -0500 Subject: [PATCH 27/28] Consoliodate logic for spa links --- packages/react-router/lib/dom/lib.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/react-router/lib/dom/lib.tsx b/packages/react-router/lib/dom/lib.tsx index 1189b57f43..d3924ca980 100644 --- a/packages/react-router/lib/dom/lib.tsx +++ b/packages/react-router/lib/dom/lib.tsx @@ -1386,17 +1386,16 @@ export const Link = React.forwardRef( } } + let isSpaLink = !(parsed.isExternal || reloadDocument); let link = ( // eslint-disable-next-line jsx-a11y/anchor-has-content Date: Wed, 18 Feb 2026 09:48:07 -0500 Subject: [PATCH 28/28] Move masked ssr removal to HydratedRouter --- .../lib/dom-export/hydrated-router.tsx | 10 ++++++++++ .../react-router/lib/dom/ssr/components.tsx | 17 +---------------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/packages/react-router/lib/dom-export/hydrated-router.tsx b/packages/react-router/lib/dom-export/hydrated-router.tsx index e2dc320796..3d3e4cd080 100644 --- a/packages/react-router/lib/dom-export/hydrated-router.tsx +++ b/packages/react-router/lib/dom-export/hydrated-router.tsx @@ -166,6 +166,16 @@ function createHydratedRouter({ } } + // We cannot support history-state-driven masking with SSR, so if a hard + // reload is performed we remove the mask and hydrate according to the + // browser URL + if (window.history.state && window.history.state.masked) { + window.history.replaceState( + { ...window.history.state, masked: undefined }, + "", + ); + } + // We don't use createBrowserRouter here because we need fine-grained control // over initialization to support synchronous `clientLoader` flows. let router = createRouter({ diff --git a/packages/react-router/lib/dom/ssr/components.tsx b/packages/react-router/lib/dom/ssr/components.tsx index 9bc3132b7b..2e0f101511 100644 --- a/packages/react-router/lib/dom/ssr/components.tsx +++ b/packages/react-router/lib/dom/ssr/components.tsx @@ -788,17 +788,6 @@ export function Scripts(scriptProps: ScriptsProps): React.JSX.Element | null { return null; } - // We cannot support history-state-driven rewrites with SSR, so if a hard - // reload is performed we remove the rewrite and hydrate according to the - // browser URL - let maskScript = ssr - ? [ - "if (window.history.state && window.history.state.masked) {", - "window.history.replaceState({ ...window.history.state, masked: undefined }, null);", - "}", - ].join("") - : ""; - let streamScript = "window.__reactRouterContext.stream = new ReadableStream({" + "start(controller){" + @@ -807,11 +796,7 @@ export function Scripts(scriptProps: ScriptsProps): React.JSX.Element | null { "}).pipeThrough(new TextEncoderStream());"; let contextScript = staticContext - ? [ - `window.__reactRouterContext = ${serverHandoffString};`, - maskScript, - streamScript, - ].join("\n") + ? `window.__reactRouterContext = ${serverHandoffString};${streamScript}` : " "; let routeModulesScript = !isStatic