diff --git a/.changeset/new-kiwis-confess.md b/.changeset/new-kiwis-confess.md new file mode 100644 index 0000000000..8816d1b758 --- /dev/null +++ b/.changeset/new-kiwis-confess.md @@ -0,0 +1,5 @@ +--- +"@remix-run/router": patch +--- + +Fix URL creation in Cloudflare Pages or other non-browser-environment diff --git a/package.json b/package.json index d359a2a285..d7b6afc2db 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ }, "filesize": { "packages/router/dist/router.umd.min.js": { - "none": "35 kB" + "none": "35.5 kB" }, "packages/react-router/dist/react-router.production.min.js": { "none": "12.5 kB" diff --git a/packages/router/history.ts b/packages/router/history.ts index 56779c8f9d..e5117817b7 100644 --- a/packages/router/history.ts +++ b/packages/router/history.ts @@ -447,6 +447,20 @@ export function createHashHistory( //#region UTILS //////////////////////////////////////////////////////////////////////////////// +/** + * @private + */ +export function invariant(value: boolean, message?: string): asserts value; +export function invariant( + value: T | null | undefined, + message?: string +): asserts value is T; +export function invariant(value: any, message?: string) { + if (value === false || value === null || typeof value === "undefined") { + throw new Error(message); + } +} + function warning(cond: any, message: string) { if (!cond) { // eslint-disable-next-line no-console @@ -544,7 +558,7 @@ export function parsePath(path: string): Partial { return parsedPath; } -export function createURL(location: Location | string): URL { +export function createClientSideURL(location: Location | string): URL { // window.location.origin is "null" (the literal string value) in Firefox // under certain conditions, notably when serving from a local HTML file // See https://bugzilla.mozilla.org/show_bug.cgi?id=878297 @@ -553,8 +567,12 @@ export function createURL(location: Location | string): URL { typeof window.location !== "undefined" && window.location.origin !== "null" ? window.location.origin - : "unknown://unknown"; + : window.location.href; let href = typeof location === "string" ? location : createPath(location); + invariant( + base, + `No window.location.(origin|href) available to create URL for href: ${href}` + ); return new URL(href, base); } @@ -643,7 +661,9 @@ function getUrlBasedHistory( }, encodeLocation(to) { // Encode a Location the same way window.location would - let url = createURL(typeof to === "string" ? to : createPath(to)); + let url = createClientSideURL( + typeof to === "string" ? to : createPath(to) + ); return { pathname: url.pathname, search: url.search, diff --git a/packages/router/index.ts b/packages/router/index.ts index f9739ba566..23bfca95b1 100644 --- a/packages/router/index.ts +++ b/packages/router/index.ts @@ -32,7 +32,6 @@ export { defer, generatePath, getToPathname, - invariant, isRouteErrorResponse, joinPaths, json, @@ -59,13 +58,13 @@ export type { Path, To, } from "./history"; - export { Action, createBrowserHistory, createPath, createHashHistory, createMemoryHistory, + invariant, parsePath, } from "./history"; diff --git a/packages/router/router.ts b/packages/router/router.ts index 7d3b1d3f76..8bd5ae9f55 100644 --- a/packages/router/router.ts +++ b/packages/router/router.ts @@ -3,7 +3,8 @@ import { Action as HistoryAction, createLocation, createPath, - createURL, + createClientSideURL, + invariant, parsePath, } from "./history"; import type { @@ -28,7 +29,6 @@ import { ResultType, convertRoutesToDataRoutes, getPathContributingMatches, - invariant, isRouteErrorResponse, joinPaths, matchRoutes, @@ -913,7 +913,7 @@ export function createRouter(init: RouterInit): Router { // Create a controller/Request for this navigation pendingNavigationController = new AbortController(); - let request = createRequest( + let request = createClientSideRequest( location, pendingNavigationController.signal, opts && opts.submission @@ -954,7 +954,7 @@ export function createRouter(init: RouterInit): Router { loadingNavigation = navigation; // Create a GET request for the loaders - request = createRequest(request.url, request.signal); + request = new Request(request.url, { signal: request.signal }); } // Call loaders @@ -1299,7 +1299,11 @@ export function createRouter(init: RouterInit): Router { // Call the action for the fetcher let abortController = new AbortController(); - let fetchRequest = createRequest(path, abortController.signal, submission); + let fetchRequest = createClientSideRequest( + path, + abortController.signal, + submission + ); fetchControllers.set(key, abortController); let actionResult = await callLoaderOrAction( @@ -1346,7 +1350,7 @@ export function createRouter(init: RouterInit): Router { // Start the data load for current matches, or the next location if we're // in the middle of a navigation let nextLocation = state.navigation.location || state.location; - let revalidationRequest = createRequest( + let revalidationRequest = createClientSideRequest( nextLocation, abortController.signal ); @@ -1501,7 +1505,7 @@ export function createRouter(init: RouterInit): Router { // Call the loader for this fetcher route match let abortController = new AbortController(); - let fetchRequest = createRequest(path, abortController.signal); + let fetchRequest = createClientSideRequest(path, abortController.signal); fetchControllers.set(key, abortController); let result: DataResult = await callLoaderOrAction( "loader", @@ -1675,7 +1679,7 @@ export function createRouter(init: RouterInit): Router { ...fetchersToLoad.map(([, href, match, fetchMatches]) => callLoaderOrAction( "loader", - createRequest(href, request.signal), + createClientSideRequest(href, request.signal), match, fetchMatches, router.basename @@ -2120,7 +2124,7 @@ export function unstable_createStaticHandler( if (!actionMatch.route.action) { let error = getInternalRouterError(405, { method: request.method, - pathname: createURL(request.url).pathname, + pathname: new URL(request.url).pathname, routeId: actionMatch.route.id, }); if (isRouteRequest) { @@ -2206,7 +2210,7 @@ export function unstable_createStaticHandler( } // Create a GET request for the loaders - let loaderRequest = createRequest(request.url, request.signal); + let loaderRequest = new Request(request.url, { signal: request.signal }); let context = await loadRouteData(loaderRequest, matches); return { @@ -2240,7 +2244,7 @@ export function unstable_createStaticHandler( if (isRouteRequest && !routeMatch?.route.loader) { throw getInternalRouterError(400, { method: request.method, - pathname: createURL(request.url).pathname, + pathname: new URL(request.url).pathname, routeId: routeMatch?.route.id, }); } @@ -2531,9 +2535,9 @@ function shouldRevalidateLoader( isRevalidationRequired: boolean, actionResult: DataResult | undefined ) { - let currentUrl = createURL(currentLocation); + let currentUrl = createClientSideURL(currentLocation); let currentParams = currentMatch.params; - let nextUrl = createURL(location); + let nextUrl = createClientSideURL(location); let nextParams = match.params; // This is the default implementation as to when we revalidate. If the route @@ -2624,7 +2628,10 @@ async function callLoaderOrAction( ); // Check if this an external redirect that goes to a new origin - let external = createURL(location).origin !== createURL("/").origin; + let currentUrl = new URL(request.url); + let currentOrigin = currentUrl.origin; + let newOrigin = new URL(location, currentOrigin).origin; + let external = newOrigin !== currentOrigin; // Support relative routing in internal redirects if (!external) { @@ -2632,8 +2639,11 @@ async function callLoaderOrAction( let routePathnames = getPathContributingMatches(activeMatches).map( (match) => match.pathnameBase ); - let requestPath = createURL(request.url).pathname; - let resolvedLocation = resolveTo(location, routePathnames, requestPath); + let resolvedLocation = resolveTo( + location, + routePathnames, + currentUrl.pathname + ); invariant( createPath(resolvedLocation), `Unable to resolve redirect location: ${location}` @@ -2713,12 +2723,15 @@ async function callLoaderOrAction( return { type: ResultType.data, data: result }; } -function createRequest( +// Utility method for creating the Request instances for loaders/actions during +// client-side navigations and fetches. During SSR we will always have a +// Request instance from the static handler (query/queryRoute) +function createClientSideRequest( location: string | Location, signal: AbortSignal, submission?: Submission ): Request { - let url = createURL(stripHashFromPath(location)).toString(); + let url = createClientSideURL(stripHashFromPath(location)).toString(); let init: RequestInit = { signal }; if (submission) { diff --git a/packages/router/utils.ts b/packages/router/utils.ts index cd35742733..53c970f823 100644 --- a/packages/router/utils.ts +++ b/packages/router/utils.ts @@ -1,5 +1,5 @@ import type { Location, Path, To } from "./history"; -import { parsePath } from "./history"; +import { invariant, parsePath } from "./history"; /** * Map of routeId -> data returned from a loader/action/error @@ -771,20 +771,6 @@ export function stripBasename( return pathname.slice(startIndex) || "/"; } -/** - * @private - */ -export function invariant(value: boolean, message?: string): asserts value; -export function invariant( - value: T | null | undefined, - message?: string -): asserts value is T; -export function invariant(value: any, message?: string) { - if (value === false || value === null || typeof value === "undefined") { - throw new Error(message); - } -} - /** * @private */