@remix-run/react
Advanced tools
Comparing version 0.0.0-nightly-ccefed3-20230621 to 0.0.0-nightly-cd403b516-20240809
/** | ||
* @remix-run/react v0.0.0-nightly-ccefed3-20230621 | ||
* @remix-run/react v0.0.0-nightly-cd403b516-20240809 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
@@ -1,4 +0,4 @@ | ||
import type { HydrationState } from "@remix-run/router"; | ||
import type { HydrationState, Router } from "@remix-run/router"; | ||
import type { ReactElement } from "react"; | ||
import type { EntryContext, FutureConfig } from "./entry"; | ||
import type { AssetsManifest, FutureConfig } from "./entry"; | ||
import type { RouteModules } from "./routeModules"; | ||
@@ -8,4 +8,9 @@ declare global { | ||
url: string; | ||
basename?: string; | ||
state: HydrationState; | ||
criticalCss?: string; | ||
future: FutureConfig; | ||
isSpaMode: boolean; | ||
stream: ReadableStream<Uint8Array> | undefined; | ||
streamController: ReadableStreamDefaultController<Uint8Array>; | ||
a?: number; | ||
@@ -17,5 +22,7 @@ dev?: { | ||
}; | ||
var __remixRouter: Router; | ||
var __remixRouteModules: RouteModules; | ||
var __remixManifest: EntryContext["manifest"]; | ||
var __remixManifest: AssetsManifest; | ||
var __remixRevalidation: number | undefined; | ||
var __remixClearCriticalCss: (() => void) | undefined; | ||
var $RefreshRuntime$: { | ||
@@ -27,7 +34,2 @@ performReactRefresh: () => void; | ||
} | ||
declare global { | ||
interface ImportMeta { | ||
hot: any; | ||
} | ||
} | ||
/** | ||
@@ -34,0 +36,0 @@ * The entry point for a Remix app when it is rendered in the browser (in |
/** | ||
* @remix-run/react v0.0.0-nightly-ccefed3-20230621 | ||
* @remix-run/react v0.0.0-nightly-cd403b516-20240809 | ||
* | ||
@@ -15,3 +15,5 @@ * Copyright (c) Remix Software Inc. | ||
var router$1 = require('@remix-run/router'); | ||
var React = require('react'); | ||
var reactRouter = require('react-router'); | ||
var reactRouterDom = require('react-router-dom'); | ||
@@ -22,2 +24,5 @@ var components = require('./components.js'); | ||
var routes = require('./routes.js'); | ||
var singleFetch = require('./single-fetch.js'); | ||
var invariant = require('./invariant.js'); | ||
var fogOfWar = require('./fog-of-war.js'); | ||
@@ -48,3 +53,18 @@ function _interopNamespace(e) { | ||
let stateDecodingPromise; | ||
let router; | ||
let routerInitialized = false; | ||
let hmrRouterReadyResolve; | ||
// There's a race condition with HMR where the remix:manifest is signaled before | ||
// the router is assigned in the RemixBrowser component. This promise gates the | ||
// HMR handler until the router is ready | ||
new Promise(resolve => { | ||
// body of a promise is executed immediately, so this can be resolved outside | ||
// of the promise body | ||
hmrRouterReadyResolve = resolve; | ||
}).catch(() => { | ||
// This is a noop catch handler to avoid unhandled promise rejection warnings | ||
// in the console. The promise is never rejected. | ||
return undefined; | ||
}); | ||
@@ -58,37 +78,157 @@ /** | ||
if (!router) { | ||
let routes$1 = routes.createClientRoutes(window.__remixManifest.routes, window.__remixRouteModules, window.__remixContext.future); | ||
let hydrationData = window.__remixContext.state; | ||
if (hydrationData && hydrationData.errors) { | ||
// Hard reload if the path we tried to load is not the current path. | ||
// This is usually the result of 2 rapid back/forward clicks from an | ||
// external site into a Remix app, where we initially start the load for | ||
// one URL and while the JS chunks are loading a second forward click moves | ||
// us to a new URL. Avoid comparing search params because of CDNs which | ||
// can be configured to ignore certain params and only pathname is relevant | ||
// towards determining the route matches. | ||
let initialPathname = window.__remixContext.url; | ||
let hydratedPathname = window.location.pathname; | ||
if (initialPathname !== hydratedPathname && !window.__remixContext.isSpaMode) { | ||
let errorMsg = `Initial URL (${initialPathname}) does not match URL at time of hydration ` + `(${hydratedPathname}), reloading page...`; | ||
console.error(errorMsg); | ||
window.location.reload(); | ||
// Get out of here so the reload can happen - don't create the router | ||
// since it'll then kick off unnecessary route.lazy() loads | ||
return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null); | ||
} | ||
// When single fetch is enabled, we need to suspend until the initial state | ||
// snapshot is decoded into window.__remixContext.state | ||
if (window.__remixContext.future.unstable_singleFetch) { | ||
// Note: `stateDecodingPromise` is not coupled to `router` - we'll reach this | ||
// code potentially many times waiting for our state to arrive, but we'll | ||
// then only get past here and create the `router` one time | ||
if (!stateDecodingPromise) { | ||
let stream = window.__remixContext.stream; | ||
invariant(stream, "No stream found for single fetch decoding"); | ||
window.__remixContext.stream = undefined; | ||
stateDecodingPromise = singleFetch.decodeViaTurboStream(stream, window).then(value => { | ||
window.__remixContext.state = value.value; | ||
stateDecodingPromise.value = true; | ||
}).catch(e => { | ||
stateDecodingPromise.error = e; | ||
}); | ||
} | ||
if (stateDecodingPromise.error) { | ||
throw stateDecodingPromise.error; | ||
} | ||
if (!stateDecodingPromise.value) { | ||
throw stateDecodingPromise; | ||
} | ||
} | ||
let routes$1 = routes.createClientRoutes(window.__remixManifest.routes, window.__remixRouteModules, window.__remixContext.state, window.__remixContext.future, window.__remixContext.isSpaMode); | ||
let hydrationData = undefined; | ||
if (!window.__remixContext.isSpaMode) { | ||
// Create a shallow clone of `loaderData` we can mutate for partial hydration. | ||
// When a route exports a `clientLoader` and a `HydrateFallback`, the SSR will | ||
// render the fallback so we need the client to do the same for hydration. | ||
// The server loader data has already been exposed to these route `clientLoader`'s | ||
// in `createClientRoutes` above, so we need to clear out the version we pass to | ||
// `createBrowserRouter` so it initializes and runs the client loaders. | ||
hydrationData = { | ||
...hydrationData, | ||
errors: errors.deserializeErrors(hydrationData.errors) | ||
...window.__remixContext.state, | ||
loaderData: { | ||
...window.__remixContext.state.loaderData | ||
} | ||
}; | ||
let initialMatches = reactRouterDom.matchRoutes(routes$1, window.location, window.__remixContext.basename); | ||
if (initialMatches) { | ||
for (let match of initialMatches) { | ||
let routeId = match.route.id; | ||
let route = window.__remixRouteModules[routeId]; | ||
let manifestRoute = window.__remixManifest.routes[routeId]; | ||
// Clear out the loaderData to avoid rendering the route component when the | ||
// route opted into clientLoader hydration and either: | ||
// * gave us a HydrateFallback | ||
// * or doesn't have a server loader and we have no data to render | ||
if (route && routes.shouldHydrateRouteLoader(manifestRoute, route, window.__remixContext.isSpaMode) && (route.HydrateFallback || !manifestRoute.hasLoader)) { | ||
hydrationData.loaderData[routeId] = undefined; | ||
} else if (manifestRoute && !manifestRoute.hasLoader) { | ||
// Since every Remix route gets a `loader` on the client side to load | ||
// the route JS module, we need to add a `null` value to `loaderData` | ||
// for any routes that don't have server loaders so our partial | ||
// hydration logic doesn't kick off the route module loaders during | ||
// hydration | ||
hydrationData.loaderData[routeId] = null; | ||
} | ||
} | ||
} | ||
if (hydrationData && hydrationData.errors) { | ||
hydrationData.errors = errors.deserializeErrors(hydrationData.errors); | ||
} | ||
} | ||
router = reactRouterDom.createBrowserRouter(routes$1, { | ||
let { | ||
enabled: isFogOfWarEnabled, | ||
patchRoutesOnMiss | ||
} = fogOfWar.initFogOfWar(window.__remixManifest, window.__remixRouteModules, window.__remixContext.future, window.__remixContext.isSpaMode, window.__remixContext.basename); | ||
// We don't use createBrowserRouter here because we need fine-grained control | ||
// over initialization to support synchronous `clientLoader` flows. | ||
router = router$1.createRouter({ | ||
routes: routes$1, | ||
history: router$1.createBrowserHistory(), | ||
basename: window.__remixContext.basename, | ||
future: { | ||
v7_normalizeFormMethod: true, | ||
v7_fetcherPersist: window.__remixContext.future.v3_fetcherPersist, | ||
v7_partialHydration: true, | ||
v7_prependBasename: true, | ||
v7_relativeSplatPath: window.__remixContext.future.v3_relativeSplatPath, | ||
// Single fetch enables this underlying behavior | ||
v7_skipActionErrorRevalidation: window.__remixContext.future.unstable_singleFetch === true | ||
}, | ||
hydrationData, | ||
future: { | ||
// Pass through the Remix future flag to avoid a v1 breaking change in | ||
// useNavigation() - users can control the casing via the flag in v1. | ||
// useFetcher still always uppercases in the back-compat layer in v1. | ||
// In v2 we can just always pass true here and remove the back-compat | ||
// layer | ||
v7_normalizeFormMethod: window.__remixContext.future.v2_normalizeFormMethod | ||
} | ||
mapRouteProperties: reactRouter.UNSAFE_mapRouteProperties, | ||
unstable_dataStrategy: window.__remixContext.future.unstable_singleFetch ? singleFetch.getSingleFetchDataStrategy(window.__remixManifest, window.__remixRouteModules) : undefined, | ||
...(isFogOfWarEnabled ? { | ||
unstable_patchRoutesOnMiss: patchRoutesOnMiss | ||
} : {}) | ||
}); | ||
// Hard reload if the URL we tried to load is not the current URL. | ||
// This is usually the result of 2 rapid backwards/forward clicks from an | ||
// external site into a Remix app, where we initially start the load for | ||
// one URL and while the JS chunks are loading a second forward click moves | ||
// us to a new URL | ||
let initialUrl = window.__remixContext.url; | ||
let hydratedUrl = window.location.pathname + window.location.search; | ||
if (initialUrl !== hydratedUrl) { | ||
let errorMsg = `Initial URL (${initialUrl}) does not match URL at time of hydration ` + `(${hydratedUrl}), reloading page...`; | ||
console.error(errorMsg); | ||
window.location.reload(); | ||
// We can call initialize() immediately if the router doesn't have any | ||
// loaders to run on hydration | ||
if (router.state.initialized) { | ||
routerInitialized = true; | ||
router.initialize(); | ||
} | ||
// @ts-ignore | ||
router.createRoutesForHMR = routes.createClientRoutesWithHMRRevalidationOptOut; | ||
window.__remixRouter = router; | ||
// Notify that the router is ready for HMR | ||
if (hmrRouterReadyResolve) { | ||
hmrRouterReadyResolve(router); | ||
} | ||
} | ||
// Critical CSS can become stale after code changes, e.g. styles might be | ||
// removed from a component, but the styles will still be present in the | ||
// server HTML. This allows our HMR logic to clear the critical CSS state. | ||
// eslint-disable-next-line react-hooks/rules-of-hooks | ||
let [criticalCss, setCriticalCss] = React__namespace.useState(process.env.NODE_ENV === "development" ? window.__remixContext.criticalCss : undefined); | ||
if (process.env.NODE_ENV === "development") { | ||
window.__remixClearCriticalCss = () => setCriticalCss(undefined); | ||
} | ||
// This is due to the short circuit return above when the pathname doesn't | ||
// match and we force a hard reload. This is an exceptional scenario in which | ||
// we can't hydrate anyway. | ||
// eslint-disable-next-line react-hooks/rules-of-hooks | ||
let [location, setLocation] = React__namespace.useState(router.state.location); | ||
// eslint-disable-next-line react-hooks/rules-of-hooks | ||
React__namespace.useLayoutEffect(() => { | ||
// If we had to run clientLoaders on hydration, we delay initialization until | ||
// after we've hydrated to avoid hydration issues from synchronous client loaders | ||
if (!routerInitialized) { | ||
routerInitialized = true; | ||
router.initialize(); | ||
} | ||
}, []); | ||
// eslint-disable-next-line react-hooks/rules-of-hooks | ||
React__namespace.useLayoutEffect(() => { | ||
return router.subscribe(newState => { | ||
@@ -101,2 +241,5 @@ if (newState.location !== location) { | ||
// eslint-disable-next-line react-hooks/rules-of-hooks | ||
fogOfWar.useFogOFWarDiscovery(router, window.__remixManifest, window.__remixRouteModules, window.__remixContext.future, window.__remixContext.isSpaMode); | ||
// We need to include a wrapper RemixErrorBoundary here in case the root error | ||
@@ -106,20 +249,26 @@ // boundary also throws and we need to bubble up outside of the router entirely. | ||
// out of there | ||
return /*#__PURE__*/React__namespace.createElement(components.RemixContext.Provider, { | ||
value: { | ||
manifest: window.__remixManifest, | ||
routeModules: window.__remixRouteModules, | ||
future: window.__remixContext.future | ||
} | ||
}, /*#__PURE__*/React__namespace.createElement(errorBoundaries.RemixErrorBoundary, { | ||
location: location, | ||
component: errorBoundaries.RemixRootDefaultErrorBoundary | ||
}, /*#__PURE__*/React__namespace.createElement(reactRouterDom.RouterProvider, { | ||
router: router, | ||
fallbackElement: null, | ||
future: { | ||
v7_startTransition: true | ||
} | ||
}))); | ||
return ( | ||
/*#__PURE__*/ | ||
// This fragment is important to ensure we match the <RemixServer> JSX | ||
// structure so that useId values hydrate correctly | ||
React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(components.RemixContext.Provider, { | ||
value: { | ||
manifest: window.__remixManifest, | ||
routeModules: window.__remixRouteModules, | ||
future: window.__remixContext.future, | ||
criticalCss, | ||
isSpaMode: window.__remixContext.isSpaMode | ||
} | ||
}, /*#__PURE__*/React__namespace.createElement(errorBoundaries.RemixErrorBoundary, { | ||
location: location | ||
}, /*#__PURE__*/React__namespace.createElement(reactRouterDom.RouterProvider, { | ||
router: router, | ||
fallbackElement: null, | ||
future: { | ||
v7_startTransition: true | ||
} | ||
}))), window.__remixContext.future.unstable_singleFetch ? /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null) : null) | ||
); | ||
} | ||
exports.RemixBrowser = RemixBrowser; |
import * as React from "react"; | ||
import type { LinkProps, NavLinkProps, FormProps, Params, SubmitFunction } from "react-router-dom"; | ||
import type { UIMatch as UIMatchRR } from "@remix-run/router"; | ||
import type { FetcherWithComponents, FormProps, LinkProps, NavLinkProps } from "react-router-dom"; | ||
import { useFetcher as useFetcherRR } from "react-router-dom"; | ||
import type { SerializeFrom } from "@remix-run/server-runtime"; | ||
@@ -7,26 +9,31 @@ import type { AppData } from "./data"; | ||
import type { PrefetchPageDescriptor } from "./links"; | ||
import type { Transition, Fetcher } from "./transition"; | ||
import type { RouteHandle } from "./routeModules"; | ||
export declare const RemixContext: React.Context<RemixContextObject | undefined>; | ||
export declare function RemixRoute({ id }: { | ||
id: string; | ||
}): JSX.Element; | ||
export declare function RemixRouteError({ id }: { | ||
id: string; | ||
}): JSX.Element; | ||
export declare function useRemixContext(): RemixContextObject; | ||
/** | ||
* Defines the discovery behavior of the link: | ||
* | ||
* - "render": Eagerly discover when the link is rendered (default) | ||
* - "none": No eager discovery - discover when the link is clicked | ||
*/ | ||
export type DiscoverBehavior = "render" | "none"; | ||
/** | ||
* Defines the prefetching behavior of the link: | ||
* | ||
* - "none": Never fetched | ||
* - "intent": Fetched when the user focuses or hovers the link | ||
* - "render": Fetched when the link is rendered | ||
* - "none": Never fetched | ||
* - "viewport": Fetched when the link is in the viewport | ||
*/ | ||
type PrefetchBehavior = "intent" | "render" | "none" | "viewport"; | ||
export interface RemixLinkProps extends LinkProps { | ||
discover?: DiscoverBehavior; | ||
prefetch?: PrefetchBehavior; | ||
} | ||
export interface RemixNavLinkProps extends NavLinkProps { | ||
discover?: DiscoverBehavior; | ||
prefetch?: PrefetchBehavior; | ||
} | ||
/** | ||
* A special kind of `<Link>` that knows whether or not it is "active". | ||
* A special kind of `<Link>` that knows whether it is "active". | ||
* | ||
@@ -45,2 +52,13 @@ * @see https://remix.run/components/nav-link | ||
export { Link }; | ||
export interface RemixFormProps extends FormProps { | ||
discover?: DiscoverBehavior; | ||
} | ||
/** | ||
* This component renders a form tag and is the primary way the user will | ||
* submit information via your website. | ||
* | ||
* @see https://remix.run/components/form | ||
*/ | ||
declare let Form: React.ForwardRefExoticComponent<RemixFormProps & React.RefAttributes<HTMLFormElement>>; | ||
export { Form }; | ||
export declare function composeEventHandlers<EventType extends React.SyntheticEvent | Event>(theirHandler: ((event: EventType) => any) | undefined, ourHandler: (event: EventType) => any): (event: EventType) => any; | ||
@@ -52,5 +70,5 @@ /** | ||
*/ | ||
export declare function Links(): JSX.Element; | ||
export declare function Links(): React.JSX.Element; | ||
/** | ||
* This component renders all of the `<link rel="prefetch">` and | ||
* This component renders all the `<link rel="prefetch">` and | ||
* `<link rel="modulepreload"/>` tags for all the assets (data, modules, css) of | ||
@@ -63,4 +81,9 @@ * a given page. | ||
*/ | ||
export declare function PrefetchPageLinks({ page, ...dataLinkProps }: PrefetchPageDescriptor): JSX.Element | null; | ||
export declare function Meta(): JSX.Element; | ||
export declare function PrefetchPageLinks({ page, ...dataLinkProps }: PrefetchPageDescriptor): React.JSX.Element | null; | ||
/** | ||
* Renders HTML tags related to metadata for the current route. | ||
* | ||
* @see https://remix.run/components/meta | ||
*/ | ||
export declare function Meta(): React.JSX.Element; | ||
export interface AwaitProps<Resolve> { | ||
@@ -71,3 +94,3 @@ children: React.ReactNode | ((value: Awaited<Resolve>) => React.ReactNode); | ||
} | ||
export declare function Await<Resolve>(props: AwaitProps<Resolve>): JSX.Element; | ||
export declare function Await<Resolve>(props: AwaitProps<Resolve>): React.JSX.Element; | ||
export type ScriptProps = Omit<React.HTMLProps<HTMLScriptElement>, "children" | "async" | "defer" | "src" | "type" | "noModule" | "dangerouslySetInnerHTML" | "suppressHydrationWarning">; | ||
@@ -84,33 +107,12 @@ /** | ||
*/ | ||
export declare function Scripts(props: ScriptProps): JSX.Element | null; | ||
export interface RouteMatch { | ||
/** | ||
* The id of the matched route | ||
*/ | ||
id: string; | ||
/** | ||
* The pathname of the matched route | ||
*/ | ||
pathname: string; | ||
/** | ||
* The dynamic parameters of the matched route | ||
* | ||
* @see https://remix.run/file-conventions/routes-files#dynamic-route-parameters | ||
*/ | ||
params: Params<string>; | ||
/** | ||
* Any route data associated with the matched route | ||
*/ | ||
data: any; | ||
/** | ||
* The exported `handle` object of the matched route. | ||
* | ||
* @see https://remix.run/route/handle | ||
*/ | ||
handle: undefined | { | ||
[key: string]: any; | ||
}; | ||
} | ||
export declare function useMatches(): RouteMatch[]; | ||
export declare function Scripts(props: ScriptProps): React.JSX.Element | null; | ||
export type UIMatch<D = AppData, H = RouteHandle> = UIMatchRR<SerializeFrom<D>, H>; | ||
/** | ||
* Returns the active route matches, useful for accessing loaderData for | ||
* parent/child routes or the route "handle" property | ||
* | ||
* @see https://remix.run/hooks/use-matches | ||
*/ | ||
export declare function useMatches(): UIMatch[]; | ||
/** | ||
* Returns the JSON parsed data from the current route's `loader`. | ||
@@ -122,2 +124,8 @@ * | ||
/** | ||
* Returns the loaderData for the given routeId. | ||
* | ||
* @see https://remix.run/hooks/use-route-loader-data | ||
*/ | ||
export declare function useRouteLoaderData<T = AppData>(routeId: string): SerializeFrom<T> | undefined; | ||
/** | ||
* Returns the JSON parsed data from the current route's `action`. | ||
@@ -129,33 +137,20 @@ * | ||
/** | ||
* Returns everything you need to know about a page transition to build pending | ||
* navigation indicators and optimistic UI on data mutations. | ||
* Interacts with route loaders and actions without causing a navigation. Great | ||
* for any interaction that stays on the same page. | ||
* | ||
* @deprecated in favor of useNavigation | ||
* | ||
* @see https://remix.run/hooks/use-transition | ||
* @see https://remix.run/hooks/use-fetcher | ||
*/ | ||
export declare function useTransition(): Transition; | ||
export declare function useFetcher<TData = AppData>(opts?: Parameters<typeof useFetcherRR>[0]): FetcherWithComponents<SerializeFrom<TData>>; | ||
/** | ||
* Provides all fetchers currently on the page. Useful for layouts and parent | ||
* routes that need to provide pending/optimistic UI regarding the fetch. | ||
* This component connects your app to the Remix asset server and | ||
* automatically reloads the page when files change in development. | ||
* In production, it renders null, so you can safely render it always in your root route. | ||
* | ||
* @see https://remix.run/api/remix#usefetchers | ||
* @see https://remix.run/docs/components/live-reload | ||
*/ | ||
export declare function useFetchers(): Fetcher[]; | ||
export type FetcherWithComponents<TData> = Fetcher<TData> & { | ||
Form: React.ForwardRefExoticComponent<FormProps & React.RefAttributes<HTMLFormElement>>; | ||
submit: SubmitFunction; | ||
load: (href: string) => void; | ||
}; | ||
/** | ||
* Interacts with route loaders and actions without causing a navigation. Great | ||
* for any interaction that stays on the same page. | ||
* | ||
* @see https://remix.run/hooks/use-fetcher | ||
*/ | ||
export declare function useFetcher<TData = any>(): FetcherWithComponents<SerializeFrom<TData>>; | ||
export declare const LiveReload: (() => null) | (({ port, timeoutMs, nonce, }: { | ||
export declare const LiveReload: ({ origin, port, timeoutMs, nonce, }: { | ||
origin?: string | undefined; | ||
port?: number | undefined; | ||
timeoutMs?: number | undefined; | ||
nonce?: string | undefined; | ||
}) => JSX.Element); | ||
}) => React.JSX.Element | null; |
/** | ||
* @remix-run/react v0.0.0-nightly-ccefed3-20230621 | ||
* @remix-run/react v0.0.0-nightly-cd403b516-20240809 | ||
* | ||
@@ -18,8 +18,7 @@ * Copyright (c) Remix Software Inc. | ||
var reactRouterDom = require('react-router-dom'); | ||
var errorBoundaries = require('./errorBoundaries.js'); | ||
var invariant = require('./invariant.js'); | ||
var links = require('./links.js'); | ||
var markup = require('./markup.js'); | ||
var transition = require('./transition.js'); | ||
var warnings = require('./warnings.js'); | ||
var singleFetch = require('./single-fetch.js'); | ||
var fogOfWar = require('./fog-of-war.js'); | ||
@@ -69,92 +68,20 @@ function _interopNamespace(e) { | ||
//////////////////////////////////////////////////////////////////////////////// | ||
// RemixRoute | ||
function RemixRoute({ | ||
id | ||
}) { | ||
let { | ||
routeModules, | ||
future | ||
} = useRemixContext(); | ||
invariant(routeModules, "Cannot initialize 'routeModules'. This normally occurs when you have server code in your client modules.\n" + "Check this link for more details:\nhttps://remix.run/pages/gotchas#server-code-in-client-bundles"); | ||
let { | ||
default: Component, | ||
ErrorBoundary, | ||
CatchBoundary | ||
} = routeModules[id]; | ||
// Default Component to Outlet if we expose boundary UI components | ||
if (!Component && (ErrorBoundary || !future.v2_errorBoundary && CatchBoundary)) { | ||
Component = reactRouterDom.Outlet; | ||
} | ||
invariant(Component, `Route "${id}" has no component! Please go add a \`default\` export in the route module file.\n` + "If you were trying to navigate or submit to a resource route, use `<a>` instead of `<Link>` or `<Form reloadDocument>`."); | ||
return /*#__PURE__*/React__namespace.createElement(Component, null); | ||
} | ||
function RemixRouteError({ | ||
id | ||
}) { | ||
let { | ||
future, | ||
routeModules | ||
} = useRemixContext(); | ||
// This checks prevent cryptic error messages such as: 'Cannot read properties of undefined (reading 'root')' | ||
invariant(routeModules, "Cannot initialize 'routeModules'. This normally occurs when you have server code in your client modules.\n" + "Check this link for more details:\nhttps://remix.run/pages/gotchas#server-code-in-client-bundles"); | ||
let error = reactRouterDom.useRouteError(); | ||
let { | ||
CatchBoundary, | ||
ErrorBoundary | ||
} = routeModules[id]; | ||
if (future.v2_errorBoundary) { | ||
// Provide defaults for the root route if they are not present | ||
if (id === "root") { | ||
ErrorBoundary || (ErrorBoundary = errorBoundaries.V2_RemixRootDefaultErrorBoundary); | ||
} | ||
if (ErrorBoundary) { | ||
// TODO: Unsure if we can satisfy the typings here | ||
// @ts-expect-error | ||
return /*#__PURE__*/React__namespace.createElement(ErrorBoundary, null); | ||
} | ||
throw error; | ||
} | ||
// Provide defaults for the root route if they are not present | ||
if (id === "root") { | ||
CatchBoundary || (CatchBoundary = errorBoundaries.RemixRootDefaultCatchBoundary); | ||
ErrorBoundary || (ErrorBoundary = errorBoundaries.RemixRootDefaultErrorBoundary); | ||
} | ||
if (reactRouterDom.isRouteErrorResponse(error)) { | ||
let tError = error; | ||
if (!!(tError !== null && tError !== void 0 && tError.error) && tError.status !== 404 && ErrorBoundary) { | ||
// Internal framework-thrown ErrorResponses | ||
return /*#__PURE__*/React__namespace.createElement(ErrorBoundary, { | ||
error: tError.error | ||
}); | ||
} | ||
if (CatchBoundary) { | ||
// User-thrown ErrorResponses | ||
return /*#__PURE__*/React__namespace.createElement(errorBoundaries.RemixCatchBoundary, { | ||
catch: error, | ||
component: CatchBoundary | ||
}); | ||
} | ||
} | ||
if (error instanceof Error && ErrorBoundary) { | ||
// User- or framework-thrown Errors | ||
return /*#__PURE__*/React__namespace.createElement(ErrorBoundary, { | ||
error: error | ||
}); | ||
} | ||
throw error; | ||
} | ||
//////////////////////////////////////////////////////////////////////////////// | ||
// Public API | ||
/** | ||
* Defines the discovery behavior of the link: | ||
* | ||
* - "render": Eagerly discover when the link is rendered (default) | ||
* - "none": No eager discovery - discover when the link is clicked | ||
*/ | ||
/** | ||
* Defines the prefetching behavior of the link: | ||
* | ||
* - "none": Never fetched | ||
* - "intent": Fetched when the user focuses or hovers the link | ||
* - "render": Fetched when the link is rendered | ||
* - "none": Never fetched | ||
* - "viewport": Fetched when the link is in the viewport | ||
*/ | ||
function usePrefetchBehavior(prefetch, theirElementProps) { | ||
@@ -220,5 +147,8 @@ let [maybePrefetch, setMaybePrefetch] = React__namespace.useState(false); | ||
const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i; | ||
function getDiscoverAttr(discover, isAbsolute, reloadDocument) { | ||
return discover === "render" && !isAbsolute && !reloadDocument ? "true" : undefined; | ||
} | ||
/** | ||
* A special kind of `<Link>` that knows whether or not it is "active". | ||
* A special kind of `<Link>` that knows whether it is "active". | ||
* | ||
@@ -230,2 +160,3 @@ * @see https://remix.run/components/nav-link | ||
prefetch = "none", | ||
discover = "render", | ||
...props | ||
@@ -238,3 +169,4 @@ }, forwardedRef) => { | ||
ref: mergeRefs(forwardedRef, ref), | ||
to: to | ||
to: to, | ||
"data-discover": getDiscoverAttr(discover, isAbsolute, props.reloadDocument) | ||
})), shouldPrefetch && !isAbsolute ? /*#__PURE__*/React__namespace.createElement(PrefetchPageLinks, { | ||
@@ -255,2 +187,3 @@ page: href | ||
prefetch = "none", | ||
discover = "render", | ||
...props | ||
@@ -263,3 +196,4 @@ }, forwardedRef) => { | ||
ref: mergeRefs(forwardedRef, ref), | ||
to: to | ||
to: to, | ||
"data-discover": getDiscoverAttr(discover, isAbsolute, props.reloadDocument) | ||
})), shouldPrefetch && !isAbsolute ? /*#__PURE__*/React__namespace.createElement(PrefetchPageLinks, { | ||
@@ -270,2 +204,19 @@ page: href | ||
Link.displayName = "Link"; | ||
/** | ||
* This component renders a form tag and is the primary way the user will | ||
* submit information via your website. | ||
* | ||
* @see https://remix.run/components/form | ||
*/ | ||
let Form = /*#__PURE__*/React__namespace.forwardRef(({ | ||
discover = "render", | ||
...props | ||
}, forwardedRef) => { | ||
let isAbsolute = typeof props.action === "string" && ABSOLUTE_URL_REGEX.test(props.action); | ||
return /*#__PURE__*/React__namespace.createElement(reactRouterDom.Form, _rollupPluginBabelHelpers["extends"]({}, props, { | ||
ref: forwardedRef, | ||
"data-discover": getDiscoverAttr(discover, isAbsolute, props.reloadDocument) | ||
})); | ||
}); | ||
Form.displayName = "Form"; | ||
function composeEventHandlers(theirHandler, ourHandler) { | ||
@@ -279,7 +230,19 @@ return event => { | ||
} | ||
let linksWarning = "⚠️ REMIX FUTURE CHANGE: The behavior of links `imagesizes` and `imagesrcset` will be changing in v2. " + "Only the React camel case versions will be valid. Please change to `imageSizes` and `imageSrcSet`. " + "For instructions on making this change see " + "https://remix.run/docs/en/v1.15.0/pages/v2#links-imagesizes-and-imagesrcset"; | ||
let useTransitionWarning = "⚠️ REMIX FUTURE CHANGE: `useTransition` will be removed in v2 in favor of `useNavigation`. " + "You can prepare for this change at your convenience by updating to `useNavigation`. " + "For instructions on making this change see " + "https://remix.run/docs/en/v1.15.0/pages/v2#usetransition"; | ||
let fetcherTypeWarning = "⚠️ REMIX FUTURE CHANGE: `fetcher.type` will be removed in v2. " + "Please use `fetcher.state`, `fetcher.formData`, and `fetcher.data` to achieve the same UX. " + "For instructions on making this change see " + "https://remix.run/docs/en/v1.15.0/pages/v2#usefetcher"; | ||
let fetcherSubmissionWarning = "⚠️ REMIX FUTURE CHANGE : `fetcher.submission` will be removed in v2. " + "The submission fields are now part of the fetcher object itself (`fetcher.formData`). " + "For instructions on making this change see " + "https://remix.run/docs/en/v1.15.0/pages/v2#usefetcher"; | ||
// Return the matches actively being displayed: | ||
// - In SPA Mode we only SSR/hydrate the root match, and include all matches | ||
// after hydration. This lets the router handle initial match loads via lazy(). | ||
// - When an error boundary is rendered, we slice off matches up to the | ||
// boundary for <Links>/<Meta> | ||
function getActiveMatches(matches, errors, isSpaMode) { | ||
if (isSpaMode && !isHydrated) { | ||
return [matches[0]]; | ||
} | ||
if (errors) { | ||
let errorIdx = matches.findIndex(m => errors[m.route.id] !== undefined); | ||
return matches.slice(0, errorIdx + 1); | ||
} | ||
return matches; | ||
} | ||
/** | ||
@@ -292,4 +255,6 @@ * Renders the `<link>` tags for the current routes. | ||
let { | ||
isSpaMode, | ||
manifest, | ||
routeModules | ||
routeModules, | ||
criticalCss | ||
} = useRemixContext(); | ||
@@ -300,48 +265,20 @@ let { | ||
} = useDataRouterStateContext(); | ||
let matches = errors ? routerMatches.slice(0, routerMatches.findIndex(m => errors[m.route.id]) + 1) : routerMatches; | ||
let links$1 = React__namespace.useMemo(() => links.getLinksForMatches(matches, routeModules, manifest), [matches, routeModules, manifest]); | ||
React__namespace.useEffect(() => { | ||
if (links$1.some(link => "imagesizes" in link || "imagesrcset" in link)) { | ||
warnings.logDeprecationOnce(linksWarning); | ||
let matches = getActiveMatches(routerMatches, errors, isSpaMode); | ||
let keyedLinks = React__namespace.useMemo(() => links.getKeyedLinksForMatches(matches, routeModules, manifest), [matches, routeModules, manifest]); | ||
return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, criticalCss ? /*#__PURE__*/React__namespace.createElement("style", { | ||
dangerouslySetInnerHTML: { | ||
__html: criticalCss | ||
} | ||
}, [links$1]); | ||
return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, links$1.map(link => { | ||
if (links.isPageLinkDescriptor(link)) { | ||
return /*#__PURE__*/React__namespace.createElement(PrefetchPageLinks, _rollupPluginBabelHelpers["extends"]({ | ||
key: link.page | ||
}, link)); | ||
} | ||
let imageSrcSet = null; | ||
// In React 17, <link imageSrcSet> and <link imageSizes> will warn | ||
// because the DOM attributes aren't recognized, so users need to pass | ||
// them in all lowercase to forward the attributes to the node without a | ||
// warning. Normalize so that either property can be used in Remix. | ||
if ("useId" in React__namespace) { | ||
if (link.imagesrcset) { | ||
link.imageSrcSet = imageSrcSet = link.imagesrcset; | ||
delete link.imagesrcset; | ||
} | ||
if (link.imagesizes) { | ||
link.imageSizes = link.imagesizes; | ||
delete link.imagesizes; | ||
} | ||
} else { | ||
if (link.imageSrcSet) { | ||
link.imagesrcset = imageSrcSet = link.imageSrcSet; | ||
delete link.imageSrcSet; | ||
} | ||
if (link.imageSizes) { | ||
link.imagesizes = link.imageSizes; | ||
delete link.imageSizes; | ||
} | ||
} | ||
return /*#__PURE__*/React__namespace.createElement("link", _rollupPluginBabelHelpers["extends"]({ | ||
key: link.rel + (link.href || "") + (imageSrcSet || "") | ||
}, link)); | ||
})); | ||
}) : null, keyedLinks.map(({ | ||
key, | ||
link | ||
}) => links.isPageLinkDescriptor(link) ? /*#__PURE__*/React__namespace.createElement(PrefetchPageLinks, _rollupPluginBabelHelpers["extends"]({ | ||
key: key | ||
}, link)) : /*#__PURE__*/React__namespace.createElement("link", _rollupPluginBabelHelpers["extends"]({ | ||
key: key | ||
}, link)))); | ||
} | ||
/** | ||
* This component renders all of the `<link rel="prefetch">` and | ||
* This component renders all the `<link rel="prefetch">` and | ||
* `<link rel="modulepreload"/>` tags for all the assets (data, modules, css) of | ||
@@ -361,3 +298,3 @@ * a given page. | ||
} = useDataRouterContext(); | ||
let matches = React__namespace.useMemo(() => reactRouterDom.matchRoutes(router.routes, page), [router.routes, page]); | ||
let matches = React__namespace.useMemo(() => reactRouterDom.matchRoutes(router.routes, page, router.basename), [router.routes, page, router.basename]); | ||
if (!matches) { | ||
@@ -372,3 +309,3 @@ console.warn(`Tried to prefetch ${page} but no routes matched.`); | ||
} | ||
function usePrefetchedStylesheets(matches) { | ||
function useKeyedPrefetchLinks(matches) { | ||
let { | ||
@@ -378,7 +315,9 @@ manifest, | ||
} = useRemixContext(); | ||
let [styleLinks, setStyleLinks] = React__namespace.useState([]); | ||
let [keyedPrefetchLinks, setKeyedPrefetchLinks] = React__namespace.useState([]); | ||
React__namespace.useEffect(() => { | ||
let interrupted = false; | ||
links.getStylesheetPrefetchLinks(matches, manifest, routeModules).then(links => { | ||
if (!interrupted) setStyleLinks(links); | ||
void links.getKeyedPrefetchLinks(matches, manifest, routeModules).then(links => { | ||
if (!interrupted) { | ||
setKeyedPrefetchLinks(links); | ||
} | ||
}); | ||
@@ -389,3 +328,3 @@ return () => { | ||
}, [matches, manifest, routeModules]); | ||
return styleLinks; | ||
return keyedPrefetchLinks; | ||
} | ||
@@ -399,3 +338,5 @@ function PrefetchPageLinksImpl({ | ||
let { | ||
manifest | ||
future, | ||
manifest, | ||
routeModules | ||
} = useRemixContext(); | ||
@@ -412,13 +353,32 @@ let { | ||
// just the manifest like the other links in here. | ||
let styleLinks = usePrefetchedStylesheets(newMatchesForAssets); | ||
return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, dataHrefs.map(href => /*#__PURE__*/React__namespace.createElement("link", _rollupPluginBabelHelpers["extends"]({ | ||
let keyedPrefetchLinks = useKeyedPrefetchLinks(newMatchesForAssets); | ||
let linksToRender = null; | ||
if (!future.unstable_singleFetch) { | ||
// Non-single-fetch prefetching | ||
linksToRender = dataHrefs.map(href => /*#__PURE__*/React__namespace.createElement("link", _rollupPluginBabelHelpers["extends"]({ | ||
key: href, | ||
rel: "prefetch", | ||
as: "fetch", | ||
href: href | ||
}, linkProps))); | ||
} else if (newMatchesForData.length > 0) { | ||
// Single-fetch with routes that require data | ||
let url = singleFetch.addRevalidationParam(manifest, routeModules, nextMatches.map(m => m.route), newMatchesForData.map(m => m.route), singleFetch.singleFetchUrl(page)); | ||
if (url.searchParams.get("_routes") !== "") { | ||
linksToRender = /*#__PURE__*/React__namespace.createElement("link", _rollupPluginBabelHelpers["extends"]({ | ||
key: url.pathname + url.search, | ||
rel: "prefetch", | ||
as: "fetch", | ||
href: url.pathname + url.search | ||
}, linkProps)); | ||
} | ||
} else ; | ||
return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, linksToRender, moduleHrefs.map(href => /*#__PURE__*/React__namespace.createElement("link", _rollupPluginBabelHelpers["extends"]({ | ||
key: href, | ||
rel: "prefetch", | ||
as: "fetch", | ||
href: href | ||
}, linkProps))), moduleHrefs.map(href => /*#__PURE__*/React__namespace.createElement("link", _rollupPluginBabelHelpers["extends"]({ | ||
key: href, | ||
rel: "modulepreload", | ||
href: href | ||
}, linkProps))), styleLinks.map(link => | ||
}, linkProps))), keyedPrefetchLinks.map(({ | ||
key, | ||
link | ||
}) => | ||
/*#__PURE__*/ | ||
@@ -428,3 +388,3 @@ // these don't spread `linkProps` because they are full link descriptors | ||
React__namespace.createElement("link", _rollupPluginBabelHelpers["extends"]({ | ||
key: link.href | ||
key: key | ||
}, link)))); | ||
@@ -434,8 +394,9 @@ } | ||
/** | ||
* Renders the `<title>` and `<meta>` tags for the current routes. | ||
* Renders HTML tags related to metadata for the current route. | ||
* | ||
* @see https://remix.run/components/meta | ||
*/ | ||
function V1Meta() { | ||
function Meta() { | ||
let { | ||
isSpaMode, | ||
routeModules | ||
@@ -449,92 +410,7 @@ } = useRemixContext(); | ||
let location = reactRouterDom.useLocation(); | ||
let matches = errors ? routerMatches.slice(0, routerMatches.findIndex(m => errors[m.route.id]) + 1) : routerMatches; | ||
let meta = {}; | ||
let parentsData = {}; | ||
for (let match of matches) { | ||
let routeId = match.route.id; | ||
let data = loaderData[routeId]; | ||
let params = match.params; | ||
let routeModule = routeModules[routeId]; | ||
if (routeModule.meta) { | ||
let routeMeta = typeof routeModule.meta === "function" ? routeModule.meta({ | ||
data, | ||
parentsData, | ||
params, | ||
location | ||
}) : routeModule.meta; | ||
if (routeMeta && Array.isArray(routeMeta)) { | ||
throw new Error("The route at " + match.route.path + " returns an array. This is only supported with the `v2_meta` future flag " + "in the Remix config. Either set the flag to `true` or update the route's " + "meta function to return an object." + "\n\nTo reference the v1 meta function API, see https://remix.run/route/meta" | ||
// TODO: Add link to the docs once they are written | ||
// + "\n\nTo reference future flags and the v2 meta API, see https://remix.run/file-conventions/remix-config#future-v2-meta." | ||
); | ||
} | ||
Object.assign(meta, routeMeta); | ||
} | ||
parentsData[routeId] = data; | ||
let _matches = getActiveMatches(routerMatches, errors, isSpaMode); | ||
let error = null; | ||
if (errors) { | ||
error = errors[_matches[_matches.length - 1].route.id]; | ||
} | ||
return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, Object.entries(meta).map(([name, value]) => { | ||
if (!value) { | ||
return null; | ||
} | ||
if (["charset", "charSet"].includes(name)) { | ||
return /*#__PURE__*/React__namespace.createElement("meta", { | ||
key: "charSet", | ||
charSet: value | ||
}); | ||
} | ||
if (name === "title") { | ||
return /*#__PURE__*/React__namespace.createElement("title", { | ||
key: "title" | ||
}, String(value)); | ||
} | ||
// Open Graph tags use the `property` attribute, while other meta tags | ||
// use `name`. See https://ogp.me/ | ||
// | ||
// Namespaced attributes: | ||
// - https://ogp.me/#type_music | ||
// - https://ogp.me/#type_video | ||
// - https://ogp.me/#type_article | ||
// - https://ogp.me/#type_book | ||
// - https://ogp.me/#type_profile | ||
// | ||
// Facebook specific tags begin with `fb:` and also use the `property` | ||
// attribute. | ||
// | ||
// Twitter specific tags begin with `twitter:` but they use `name`, so | ||
// they are excluded. | ||
let isOpenGraphTag = /^(og|music|video|article|book|profile|fb):.+$/.test(name); | ||
return [value].flat().map(content => { | ||
if (isOpenGraphTag) { | ||
return /*#__PURE__*/React__namespace.createElement("meta", { | ||
property: name, | ||
content: content, | ||
key: name + content | ||
}); | ||
} | ||
if (typeof content === "string") { | ||
return /*#__PURE__*/React__namespace.createElement("meta", { | ||
name: name, | ||
content: content, | ||
key: name + content | ||
}); | ||
} | ||
return /*#__PURE__*/React__namespace.createElement("meta", _rollupPluginBabelHelpers["extends"]({ | ||
key: name + JSON.stringify(content) | ||
}, content)); | ||
}); | ||
})); | ||
} | ||
function V2Meta() { | ||
let { | ||
routeModules | ||
} = useRemixContext(); | ||
let { | ||
errors, | ||
matches: routerMatches, | ||
loaderData | ||
} = useDataRouterStateContext(); | ||
let location = reactRouterDom.useLocation(); | ||
let _matches = errors ? routerMatches.slice(0, routerMatches.findIndex(m => errors[m.route.id]) + 1) : routerMatches; | ||
let meta = []; | ||
@@ -557,10 +433,3 @@ let leafMeta = null; | ||
handle: _match.route.handle, | ||
// TODO: Remove in v2. Only leaving it for now because we used it in | ||
// examples and there's no reason to crash someone's build for one line. | ||
// They'll get a TS error from the type updates anyway. | ||
// @ts-expect-error | ||
get route() { | ||
console.warn("The meta function in " + _match.route.path + " accesses the `route` property on `matches`. This is deprecated and will be removed in Remix version 2. See"); | ||
return _match.route; | ||
} | ||
error | ||
}; | ||
@@ -573,7 +442,8 @@ matches[i] = match; | ||
location, | ||
matches | ||
matches, | ||
error | ||
}) : Array.isArray(routeModule.meta) ? [...routeModule.meta] : routeModule.meta; | ||
} else if (leafMeta) { | ||
// We only assign the route's meta to the nearest leaf if there is no meta | ||
// export in the route. The meta function may return a falsey value which | ||
// export in the route. The meta function may return a falsy value which | ||
// is effectively the same as an empty array. | ||
@@ -584,6 +454,3 @@ routeMeta = [...leafMeta]; | ||
if (!Array.isArray(routeMeta)) { | ||
throw new Error("The `v2_meta` API is enabled in the Remix config, but the route at " + _match.route.path + " returns an invalid value. In v2, all route meta functions must " + "return an array of meta objects." + | ||
// TODO: Add link to the docs once they are written | ||
// "\n\nTo reference future flags and the v2 meta API, see https://remix.run/file-conventions/remix-config#future-v2-meta." + | ||
"\n\nTo reference the v1 meta function API, see https://remix.run/route/meta"); | ||
throw new Error("The route at " + _match.route.path + " returns an invalid value. All route meta functions must " + "return an array of meta objects." + "\n\nTo reference the meta function API, see https://remix.run/route/meta"); | ||
} | ||
@@ -600,4 +467,6 @@ match.meta = routeMeta; | ||
if ("tagName" in metaProps) { | ||
let tagName = metaProps.tagName; | ||
delete metaProps.tagName; | ||
let { | ||
tagName, | ||
...rest | ||
} = metaProps; | ||
if (!isValidMetaTag(tagName)) { | ||
@@ -609,4 +478,4 @@ console.warn(`A meta object uses an invalid tagName: ${tagName}. Expected either 'link' or 'meta'`); | ||
return /*#__PURE__*/React__namespace.createElement(Comp, _rollupPluginBabelHelpers["extends"]({ | ||
key: JSON.stringify(metaProps) | ||
}, metaProps)); | ||
key: JSON.stringify(rest) | ||
}, rest)); | ||
} | ||
@@ -619,3 +488,3 @@ if ("title" in metaProps) { | ||
if ("charset" in metaProps) { | ||
metaProps.charSet ?? (metaProps.charSet = metaProps.charset); | ||
metaProps.charSet ??= metaProps.charset; | ||
delete metaProps.charset; | ||
@@ -630,13 +499,14 @@ } | ||
if ("script:ld+json" in metaProps) { | ||
let json = null; | ||
try { | ||
json = JSON.stringify(metaProps["script:ld+json"]); | ||
} catch (err) {} | ||
return json != null && /*#__PURE__*/React__namespace.createElement("script", { | ||
key: "script:ld+json", | ||
type: "application/ld+json", | ||
dangerouslySetInnerHTML: { | ||
__html: JSON.stringify(metaProps["script:ld+json"]) | ||
} | ||
}); | ||
let json = JSON.stringify(metaProps["script:ld+json"]); | ||
return /*#__PURE__*/React__namespace.createElement("script", { | ||
key: `script:ld+json:${json}`, | ||
type: "application/ld+json", | ||
dangerouslySetInnerHTML: { | ||
__html: json | ||
} | ||
}); | ||
} catch (err) { | ||
return null; | ||
} | ||
} | ||
@@ -651,8 +521,2 @@ return /*#__PURE__*/React__namespace.createElement("meta", _rollupPluginBabelHelpers["extends"]({ | ||
} | ||
function Meta() { | ||
let { | ||
future | ||
} = useRemixContext(); | ||
return future !== null && future !== void 0 && future.v2_meta ? /*#__PURE__*/React__namespace.createElement(V2Meta, null) : /*#__PURE__*/React__namespace.createElement(V1Meta, null); | ||
} | ||
function Await(props) { | ||
@@ -681,3 +545,7 @@ return /*#__PURE__*/React__namespace.createElement(reactRouterDom.Await, props); | ||
serverHandoffString, | ||
abortDelay | ||
abortDelay, | ||
serializeError, | ||
isSpaMode, | ||
future, | ||
renderMeta | ||
} = useRemixContext(); | ||
@@ -690,13 +558,62 @@ let { | ||
let { | ||
matches | ||
matches: routerMatches | ||
} = useDataRouterStateContext(); | ||
let navigation = reactRouterDom.useNavigation(); | ||
let enableFogOfWar = fogOfWar.isFogOfWarEnabled(future, isSpaMode); | ||
// Let <RemixServer> know that we hydrated and we should render the single | ||
// fetch streaming scripts | ||
if (renderMeta) { | ||
renderMeta.didRenderScripts = true; | ||
} | ||
let matches = getActiveMatches(routerMatches, null, isSpaMode); | ||
React__namespace.useEffect(() => { | ||
isHydrated = true; | ||
}, []); | ||
let serializePreResolvedErrorImp = (key, error) => { | ||
let toSerialize; | ||
if (serializeError && error instanceof Error) { | ||
toSerialize = serializeError(error); | ||
} else { | ||
toSerialize = error; | ||
} | ||
return `${JSON.stringify(key)}:__remixContext.p(!1, ${markup.escapeHtml(JSON.stringify(toSerialize))})`; | ||
}; | ||
let serializePreresolvedDataImp = (routeId, key, data) => { | ||
let serializedData; | ||
try { | ||
serializedData = JSON.stringify(data); | ||
} catch (error) { | ||
return serializePreResolvedErrorImp(key, error); | ||
} | ||
return `${JSON.stringify(key)}:__remixContext.p(${markup.escapeHtml(serializedData)})`; | ||
}; | ||
let serializeErrorImp = (routeId, key, error) => { | ||
let toSerialize; | ||
if (serializeError && error instanceof Error) { | ||
toSerialize = serializeError(error); | ||
} else { | ||
toSerialize = error; | ||
} | ||
return `__remixContext.r(${JSON.stringify(routeId)}, ${JSON.stringify(key)}, !1, ${markup.escapeHtml(JSON.stringify(toSerialize))})`; | ||
}; | ||
let serializeDataImp = (routeId, key, data) => { | ||
let serializedData; | ||
try { | ||
serializedData = JSON.stringify(data); | ||
} catch (error) { | ||
return serializeErrorImp(routeId, key, error); | ||
} | ||
return `__remixContext.r(${JSON.stringify(routeId)}, ${JSON.stringify(key)}, ${markup.escapeHtml(serializedData)})`; | ||
}; | ||
let deferredScripts = []; | ||
let initialScripts = React__namespace.useMemo(() => { | ||
var _manifest$hmr; | ||
let contextScript = staticContext ? `window.__remixContext = ${serverHandoffString};` : " "; | ||
let activeDeferreds = staticContext === null || staticContext === void 0 ? void 0 : staticContext.activeDeferreds; | ||
let streamScript = future.unstable_singleFetch ? | ||
// prettier-ignore | ||
"window.__remixContext.stream = new ReadableStream({" + "start(controller){" + "window.__remixContext.streamController = controller;" + "}" + "}).pipeThrough(new TextEncoderStream());" : ""; | ||
let contextScript = staticContext ? `window.__remixContext = ${serverHandoffString};${streamScript}` : " "; | ||
// When single fetch is enabled, deferred is handled by turbo-stream | ||
let activeDeferreds = future.unstable_singleFetch ? undefined : staticContext === null || staticContext === void 0 ? void 0 : staticContext.activeDeferreds; | ||
// This sets up the __remixContext with utility functions used by the | ||
@@ -710,3 +627,3 @@ // deferred scripts. | ||
// the promise created by __remixContext.n. | ||
// - __remixContext.t is a a map or routeId to keys to an object containing `e` and `r` methods | ||
// - __remixContext.t is a map or routeId to keys to an object containing `e` and `r` methods | ||
// to resolve or reject the promise created by __remixContext.n. | ||
@@ -724,3 +641,5 @@ // - __remixContext.a is the active number of deferred scripts that should be rendered to match | ||
dataKey: key, | ||
scriptProps: props | ||
scriptProps: props, | ||
serializeData: serializeDataImp, | ||
serializeError: serializeErrorImp | ||
})); | ||
@@ -731,15 +650,5 @@ return `${JSON.stringify(key)}:__remixContext.n(${JSON.stringify(routeId)}, ${JSON.stringify(key)})`; | ||
if (typeof trackedPromise._error !== "undefined") { | ||
let toSerialize = process.env.NODE_ENV === "development" ? { | ||
message: trackedPromise._error.message, | ||
stack: trackedPromise._error.stack | ||
} : { | ||
message: "Unexpected Server Error", | ||
stack: undefined | ||
}; | ||
return `${JSON.stringify(key)}:__remixContext.p(!1, ${markup.escapeHtml(JSON.stringify(toSerialize))})`; | ||
return serializePreResolvedErrorImp(key, trackedPromise._error); | ||
} else { | ||
if (typeof trackedPromise._data === "undefined") { | ||
throw new Error(`The deferred data for ${key} was not resolved, did you forget to return data from a deferred promise?`); | ||
} | ||
return `${JSON.stringify(key)}:__remixContext.p(${markup.escapeHtml(JSON.stringify(trackedPromise._data))})`; | ||
return serializePreresolvedDataImp(routeId, key, trackedPromise._data); | ||
} | ||
@@ -750,4 +659,7 @@ } | ||
}).join("\n") + (deferredScripts.length > 0 ? `__remixContext.a=${deferredScripts.length};` : ""); | ||
let routeModulesScript = !isStatic ? " " : `${(_manifest$hmr = manifest.hmr) !== null && _manifest$hmr !== void 0 && _manifest$hmr.runtime ? `import ${JSON.stringify(manifest.hmr.runtime)};` : ""}import ${JSON.stringify(manifest.url)}; | ||
let routeModulesScript = !isStatic ? " " : `${(_manifest$hmr = manifest.hmr) !== null && _manifest$hmr !== void 0 && _manifest$hmr.runtime ? `import ${JSON.stringify(manifest.hmr.runtime)};` : ""}${enableFogOfWar ? "" : `import ${JSON.stringify(manifest.url)}`}; | ||
${matches.map((match, index) => `import * as route${index} from ${JSON.stringify(manifest.routes[match.route.id].module)};`).join("\n")} | ||
${enableFogOfWar ? | ||
// Inline a minimal manifest with the SSR matches | ||
`window.__remixManifest = ${JSON.stringify(fogOfWar.getPartialManifest(manifest, router), null, 2)};` : ""} | ||
window.__remixRouteModules = {${matches.map((match, index) => `${JSON.stringify(match.route.id)}:route${index}`).join(",")}}; | ||
@@ -775,18 +687,9 @@ | ||
key: i, | ||
scriptProps: props | ||
scriptProps: props, | ||
serializeData: serializeDataImp, | ||
serializeError: serializeErrorImp | ||
})); | ||
} | ||
} | ||
// avoid waterfall when importing the next route module | ||
let nextMatches = React__namespace.useMemo(() => { | ||
if (navigation.location) { | ||
// FIXME: can probably use transitionManager `nextMatches` | ||
let matches = reactRouterDom.matchRoutes(router.routes, navigation.location); | ||
invariant(matches, `No routes match path "${navigation.location.pathname}"`); | ||
return matches; | ||
} | ||
return []; | ||
}, [navigation.location, router.routes]); | ||
let routePreloads = matches.concat(nextMatches).map(match => { | ||
let routePreloads = matches.map(match => { | ||
let route = manifest.routes[match.route.id]; | ||
@@ -796,4 +699,8 @@ return (route.imports || []).concat([route.module]); | ||
let preloads = isHydrated ? [] : manifest.entry.imports.concat(routePreloads); | ||
return isHydrated ? null : /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement("link", { | ||
return isHydrated ? null : /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, !enableFogOfWar ? /*#__PURE__*/React__namespace.createElement("link", { | ||
rel: "modulepreload", | ||
href: manifest.url, | ||
crossOrigin: props.crossOrigin | ||
}) : null, /*#__PURE__*/React__namespace.createElement("link", { | ||
rel: "modulepreload", | ||
href: manifest.entry.module, | ||
@@ -812,3 +719,5 @@ crossOrigin: props.crossOrigin | ||
routeId, | ||
scriptProps | ||
scriptProps, | ||
serializeData, | ||
serializeError | ||
}) { | ||
@@ -835,11 +744,14 @@ if (typeof document === "undefined" && deferredData && dataKey && routeId) { | ||
routeId: routeId, | ||
scriptProps: scriptProps | ||
scriptProps: scriptProps, | ||
serializeError: serializeError | ||
}), | ||
children: data => /*#__PURE__*/React__namespace.createElement("script", _rollupPluginBabelHelpers["extends"]({}, scriptProps, { | ||
async: true, | ||
suppressHydrationWarning: true, | ||
dangerouslySetInnerHTML: { | ||
__html: `__remixContext.r(${JSON.stringify(routeId)}, ${JSON.stringify(dataKey)}, ${markup.escapeHtml(JSON.stringify(data))});` | ||
} | ||
})) | ||
children: data => { | ||
return /*#__PURE__*/React__namespace.createElement("script", _rollupPluginBabelHelpers["extends"]({}, scriptProps, { | ||
async: true, | ||
suppressHydrationWarning: true, | ||
dangerouslySetInnerHTML: { | ||
__html: serializeData(routeId, dataKey, data) | ||
} | ||
})); | ||
} | ||
}) : /*#__PURE__*/React__namespace.createElement("script", _rollupPluginBabelHelpers["extends"]({}, scriptProps, { | ||
@@ -856,16 +768,10 @@ async: true, | ||
routeId, | ||
scriptProps | ||
scriptProps, | ||
serializeError | ||
}) { | ||
let error = reactRouterDom.useAsyncError(); | ||
let toSerialize = process.env.NODE_ENV === "development" ? { | ||
message: error.message, | ||
stack: error.stack | ||
} : { | ||
message: "Unexpected Server Error", | ||
stack: undefined | ||
}; | ||
return /*#__PURE__*/React__namespace.createElement("script", _rollupPluginBabelHelpers["extends"]({}, scriptProps, { | ||
suppressHydrationWarning: true, | ||
dangerouslySetInnerHTML: { | ||
__html: `__remixContext.r(${JSON.stringify(routeId)}, ${JSON.stringify(dataKey)}, !1, ${markup.escapeHtml(JSON.stringify(toSerialize))});` | ||
__html: serializeError(routeId, dataKey, error) | ||
} | ||
@@ -877,22 +783,10 @@ })); | ||
} | ||
// TODO: Can this be re-exported from RR? | ||
/** | ||
* Returns the active route matches, useful for accessing loaderData for | ||
* parent/child routes or the route "handle" property | ||
* | ||
* @see https://remix.run/hooks/use-matches | ||
*/ | ||
function useMatches() { | ||
let { | ||
routeModules | ||
} = useRemixContext(); | ||
let matches = reactRouterDom.useMatches(); | ||
return React__namespace.useMemo(() => matches.map(match => { | ||
let remixMatch = { | ||
id: match.id, | ||
pathname: match.pathname, | ||
params: match.params, | ||
data: match.data, | ||
// Need to grab handle here since we don't have it at client-side route | ||
// creation time | ||
handle: routeModules[match.id].handle | ||
}; | ||
return remixMatch; | ||
}), [matches, routeModules]); | ||
return reactRouterDom.useMatches(); | ||
} | ||
@@ -910,195 +804,20 @@ | ||
/** | ||
* Returns the JSON parsed data from the current route's `action`. | ||
* Returns the loaderData for the given routeId. | ||
* | ||
* @see https://remix.run/hooks/use-action-data | ||
* @see https://remix.run/hooks/use-route-loader-data | ||
*/ | ||
function useActionData() { | ||
return reactRouterDom.useActionData(); | ||
function useRouteLoaderData(routeId) { | ||
return reactRouterDom.useRouteLoaderData(routeId); | ||
} | ||
/** | ||
* Returns everything you need to know about a page transition to build pending | ||
* navigation indicators and optimistic UI on data mutations. | ||
* Returns the JSON parsed data from the current route's `action`. | ||
* | ||
* @deprecated in favor of useNavigation | ||
* | ||
* @see https://remix.run/hooks/use-transition | ||
* @see https://remix.run/hooks/use-action-data | ||
*/ | ||
function useTransition() { | ||
let navigation = reactRouterDom.useNavigation(); | ||
React__namespace.useEffect(() => { | ||
warnings.logDeprecationOnce(useTransitionWarning); | ||
}, []); | ||
return React__namespace.useMemo(() => convertNavigationToTransition(navigation), [navigation]); | ||
function useActionData() { | ||
return reactRouterDom.useActionData(); | ||
} | ||
function convertNavigationToTransition(navigation) { | ||
let { | ||
location, | ||
state, | ||
formMethod, | ||
formAction, | ||
formEncType, | ||
formData | ||
} = navigation; | ||
if (!location) { | ||
return transition.IDLE_TRANSITION; | ||
} | ||
let isActionSubmission = formMethod != null && ["POST", "PUT", "PATCH", "DELETE"].includes(formMethod.toUpperCase()); | ||
if (state === "submitting" && formMethod && formAction && formEncType && formData) { | ||
if (isActionSubmission) { | ||
// Actively submitting to an action | ||
let transition = { | ||
location, | ||
state, | ||
submission: { | ||
method: formMethod.toUpperCase(), | ||
action: formAction, | ||
encType: formEncType, | ||
formData: formData, | ||
key: "" | ||
}, | ||
type: "actionSubmission" | ||
}; | ||
return transition; | ||
} else { | ||
// @remix-run/router doesn't mark loader submissions as state: "submitting" | ||
invariant(false, "Encountered an unexpected navigation scenario in useTransition()"); | ||
} | ||
} | ||
if (state === "loading") { | ||
let { | ||
_isRedirect, | ||
_isFetchActionRedirect | ||
} = location.state || {}; | ||
if (formMethod && formAction && formEncType && formData) { | ||
if (!_isRedirect) { | ||
if (isActionSubmission) { | ||
// We're reloading the same location after an action submission | ||
let transition = { | ||
location, | ||
state, | ||
submission: { | ||
method: formMethod.toUpperCase(), | ||
action: formAction, | ||
encType: formEncType, | ||
formData: formData, | ||
key: "" | ||
}, | ||
type: "actionReload" | ||
}; | ||
return transition; | ||
} else { | ||
// The new router fixes a bug in useTransition where the submission | ||
// "action" represents the request URL not the state of the <form> in | ||
// the DOM. Back-port it here to maintain behavior, but useNavigation | ||
// will fix this bug. | ||
let url = new URL(formAction, window.location.origin); | ||
// This typing override should be safe since this is only running for | ||
// GET submissions and over in @remix-run/router we have an invariant | ||
// if you have any non-string values in your FormData when we attempt | ||
// to convert them to URLSearchParams | ||
url.search = new URLSearchParams(formData.entries()).toString(); | ||
// Actively "submitting" to a loader | ||
let transition = { | ||
location, | ||
state: "submitting", | ||
submission: { | ||
method: formMethod.toUpperCase(), | ||
action: url.pathname + url.search, | ||
encType: formEncType, | ||
formData: formData, | ||
key: "" | ||
}, | ||
type: "loaderSubmission" | ||
}; | ||
return transition; | ||
} | ||
} else { | ||
// Redirecting after a submission | ||
if (isActionSubmission) { | ||
let transition = { | ||
location, | ||
state, | ||
submission: { | ||
method: formMethod.toUpperCase(), | ||
action: formAction, | ||
encType: formEncType, | ||
formData: formData, | ||
key: "" | ||
}, | ||
type: "actionRedirect" | ||
}; | ||
return transition; | ||
} else { | ||
let transition = { | ||
location, | ||
state, | ||
submission: { | ||
method: formMethod.toUpperCase(), | ||
action: formAction, | ||
encType: formEncType, | ||
formData: formData, | ||
key: "" | ||
}, | ||
type: "loaderSubmissionRedirect" | ||
}; | ||
return transition; | ||
} | ||
} | ||
} else if (_isRedirect) { | ||
if (_isFetchActionRedirect) { | ||
let transition = { | ||
location, | ||
state, | ||
submission: undefined, | ||
type: "fetchActionRedirect" | ||
}; | ||
return transition; | ||
} else { | ||
let transition = { | ||
location, | ||
state, | ||
submission: undefined, | ||
type: "normalRedirect" | ||
}; | ||
return transition; | ||
} | ||
} | ||
} | ||
// If no scenarios above match, then it's a normal load! | ||
let transition$1 = { | ||
location, | ||
state: "loading", | ||
submission: undefined, | ||
type: "normalLoad" | ||
}; | ||
return transition$1; | ||
} | ||
/** | ||
* Provides all fetchers currently on the page. Useful for layouts and parent | ||
* routes that need to provide pending/optimistic UI regarding the fetch. | ||
* | ||
* @see https://remix.run/api/remix#usefetchers | ||
*/ | ||
function useFetchers() { | ||
let fetchers = reactRouterDom.useFetchers(); | ||
return fetchers.map(f => { | ||
let fetcher = convertRouterFetcherToRemixFetcher({ | ||
state: f.state, | ||
data: f.data, | ||
formMethod: f.formMethod, | ||
formAction: f.formAction, | ||
formData: f.formData, | ||
formEncType: f.formEncType, | ||
" _hasFetcherDoneAnything ": f[" _hasFetcherDoneAnything "] | ||
}); | ||
addFetcherDeprecationWarnings(fetcher); | ||
return fetcher; | ||
}); | ||
} | ||
/** | ||
* Interacts with route loaders and actions without causing a navigation. Great | ||
@@ -1109,205 +828,18 @@ * for any interaction that stays on the same page. | ||
*/ | ||
function useFetcher() { | ||
let fetcherRR = reactRouterDom.useFetcher(); | ||
return React__namespace.useMemo(() => { | ||
let remixFetcher = convertRouterFetcherToRemixFetcher({ | ||
state: fetcherRR.state, | ||
data: fetcherRR.data, | ||
formMethod: fetcherRR.formMethod, | ||
formAction: fetcherRR.formAction, | ||
formData: fetcherRR.formData, | ||
formEncType: fetcherRR.formEncType, | ||
" _hasFetcherDoneAnything ": fetcherRR[" _hasFetcherDoneAnything "] | ||
}); | ||
let fetcherWithComponents = { | ||
...remixFetcher, | ||
load: fetcherRR.load, | ||
submit: fetcherRR.submit, | ||
Form: fetcherRR.Form | ||
}; | ||
addFetcherDeprecationWarnings(fetcherWithComponents); | ||
return fetcherWithComponents; | ||
}, [fetcherRR]); | ||
function useFetcher(opts = {}) { | ||
return reactRouterDom.useFetcher(opts); | ||
} | ||
function addFetcherDeprecationWarnings(fetcher) { | ||
let type = fetcher.type; | ||
Object.defineProperty(fetcher, "type", { | ||
get() { | ||
warnings.logDeprecationOnce(fetcherTypeWarning); | ||
return type; | ||
}, | ||
set(value) { | ||
// Devs should *not* be doing this but we don't want to break their | ||
// current app if they are | ||
type = value; | ||
}, | ||
// These settings should make this behave like a normal object `type` field | ||
configurable: true, | ||
enumerable: true | ||
}); | ||
let submission = fetcher.submission; | ||
Object.defineProperty(fetcher, "submission", { | ||
get() { | ||
warnings.logDeprecationOnce(fetcherSubmissionWarning); | ||
return submission; | ||
}, | ||
set(value) { | ||
// Devs should *not* be doing this but we don't want to break their | ||
// current app if they are | ||
submission = value; | ||
}, | ||
// These settings should make this behave like a normal object `type` field | ||
configurable: true, | ||
enumerable: true | ||
}); | ||
} | ||
function convertRouterFetcherToRemixFetcher(fetcherRR) { | ||
let { | ||
state, | ||
formMethod, | ||
formAction, | ||
formEncType, | ||
formData, | ||
data | ||
} = fetcherRR; | ||
let isActionSubmission = formMethod != null && ["POST", "PUT", "PATCH", "DELETE"].includes(formMethod.toUpperCase()); | ||
if (state === "idle") { | ||
if (fetcherRR[" _hasFetcherDoneAnything "] === true) { | ||
let fetcher = { | ||
state: "idle", | ||
type: "done", | ||
formMethod: undefined, | ||
formAction: undefined, | ||
formData: undefined, | ||
formEncType: undefined, | ||
submission: undefined, | ||
data | ||
}; | ||
return fetcher; | ||
} else { | ||
let fetcher = transition.IDLE_FETCHER; | ||
return fetcher; | ||
} | ||
} | ||
if (state === "submitting" && formMethod && formAction && formEncType && formData) { | ||
if (isActionSubmission) { | ||
// Actively submitting to an action | ||
let fetcher = { | ||
state, | ||
type: "actionSubmission", | ||
formMethod: formMethod.toUpperCase(), | ||
formAction: formAction, | ||
formEncType: formEncType, | ||
formData: formData, | ||
submission: { | ||
method: formMethod.toUpperCase(), | ||
action: formAction, | ||
encType: formEncType, | ||
formData: formData, | ||
key: "" | ||
}, | ||
data | ||
}; | ||
return fetcher; | ||
} else { | ||
// @remix-run/router doesn't mark loader submissions as state: "submitting" | ||
invariant(false, "Encountered an unexpected fetcher scenario in useFetcher()"); | ||
} | ||
} | ||
if (state === "loading") { | ||
if (formMethod && formAction && formEncType && formData) { | ||
if (isActionSubmission) { | ||
if (data) { | ||
// In a loading state but we have data - must be an actionReload | ||
let fetcher = { | ||
state, | ||
type: "actionReload", | ||
formMethod: formMethod.toUpperCase(), | ||
formAction: formAction, | ||
formEncType: formEncType, | ||
formData: formData, | ||
submission: { | ||
method: formMethod.toUpperCase(), | ||
action: formAction, | ||
encType: formEncType, | ||
formData: formData, | ||
key: "" | ||
}, | ||
data | ||
}; | ||
return fetcher; | ||
} else { | ||
let fetcher = { | ||
state, | ||
type: "actionRedirect", | ||
formMethod: formMethod.toUpperCase(), | ||
formAction: formAction, | ||
formEncType: formEncType, | ||
formData: formData, | ||
submission: { | ||
method: formMethod.toUpperCase(), | ||
action: formAction, | ||
encType: formEncType, | ||
formData: formData, | ||
key: "" | ||
}, | ||
data: undefined | ||
}; | ||
return fetcher; | ||
} | ||
} else { | ||
// The new router fixes a bug in useTransition where the submission | ||
// "action" represents the request URL not the state of the <form> in | ||
// the DOM. Back-port it here to maintain behavior, but useNavigation | ||
// will fix this bug. | ||
let url = new URL(formAction, window.location.origin); | ||
// This typing override should be safe since this is only running for | ||
// GET submissions and over in @remix-run/router we have an invariant | ||
// if you have any non-string values in your FormData when we attempt | ||
// to convert them to URLSearchParams | ||
url.search = new URLSearchParams(formData.entries()).toString(); | ||
// Actively "submitting" to a loader | ||
let fetcher = { | ||
state: "submitting", | ||
type: "loaderSubmission", | ||
formMethod: formMethod.toUpperCase(), | ||
formAction: formAction, | ||
formEncType: formEncType, | ||
formData: formData, | ||
submission: { | ||
method: formMethod.toUpperCase(), | ||
action: url.pathname + url.search, | ||
encType: formEncType, | ||
formData: formData, | ||
key: "" | ||
}, | ||
data | ||
}; | ||
return fetcher; | ||
} | ||
} | ||
} | ||
// If all else fails, it's a normal load! | ||
let fetcher = { | ||
state: "loading", | ||
type: "normalLoad", | ||
formMethod: undefined, | ||
formAction: undefined, | ||
formData: undefined, | ||
formEncType: undefined, | ||
submission: undefined, | ||
data | ||
}; | ||
return fetcher; | ||
} | ||
/** | ||
* This component connects your app to the Remix asset server and | ||
* automatically reloads the page when files change in development. | ||
* In production, it renders null, so you can safely render it always in your root route. | ||
* | ||
* @see https://remix.run/docs/components/live-reload | ||
*/ | ||
const LiveReload = | ||
// Dead Code Elimination magic for production builds. | ||
// This way devs don't have to worry about doing the NODE_ENV check themselves. | ||
// If running an un-bundled server outside of `remix dev` you will still need | ||
// to set the REMIX_DEV_SERVER_WS_PORT manually. | ||
const LiveReload = process.env.NODE_ENV !== "development" ? () => null : function LiveReload({ | ||
// TODO: remove REMIX_DEV_SERVER_WS_PORT in v2 | ||
process.env.NODE_ENV !== "development" ? () => null : function LiveReload({ | ||
origin, | ||
port, | ||
@@ -1317,2 +849,3 @@ timeoutMs = 1000, | ||
}) { | ||
origin ??= process.env.REMIX_DEV_ORIGIN; | ||
let js = String.raw; | ||
@@ -1325,7 +858,14 @@ return /*#__PURE__*/React__namespace.createElement("script", { | ||
function remixLiveReloadConnect(config) { | ||
let protocol = location.protocol === "https:" ? "wss:" : "ws:"; | ||
let host = location.hostname; | ||
let port = ${port} || (window.__remixContext && window.__remixContext.dev && window.__remixContext.dev.port) || ${Number(process.env.REMIX_DEV_SERVER_WS_PORT || 8002)}; | ||
let socketPath = protocol + "//" + host + ":" + port + "/socket"; | ||
let ws = new WebSocket(socketPath); | ||
let LIVE_RELOAD_ORIGIN = ${JSON.stringify(origin)}; | ||
let protocol = | ||
LIVE_RELOAD_ORIGIN ? new URL(LIVE_RELOAD_ORIGIN).protocol.replace(/^http/, "ws") : | ||
location.protocol === "https:" ? "wss:" : "ws:"; // remove in v2? | ||
let hostname = LIVE_RELOAD_ORIGIN ? new URL(LIVE_RELOAD_ORIGIN).hostname : location.hostname; | ||
let url = new URL(protocol + "//" + hostname + "/socket"); | ||
url.port = | ||
${port} || | ||
(LIVE_RELOAD_ORIGIN ? new URL(LIVE_RELOAD_ORIGIN).port : 8002); | ||
let ws = new WebSocket(url.href); | ||
ws.onmessage = async (message) => { | ||
@@ -1361,3 +901,3 @@ let event = JSON.parse(message.data); | ||
if (accepted) { | ||
console.log("[HMR] Updated accepted by", update.id); | ||
console.log("[HMR] Update accepted by", update.id); | ||
updateAccepted = true; | ||
@@ -1372,3 +912,3 @@ } | ||
if (accepted) { | ||
console.log("[HMR] Updated accepted by", "remix:manifest"); | ||
console.log("[HMR] Update accepted by", "remix:manifest"); | ||
updateAccepted = true; | ||
@@ -1378,3 +918,3 @@ } | ||
if (!updateAccepted) { | ||
console.log("[HMR] Updated rejected, reloading..."); | ||
console.log("[HMR] Update rejected, reloading..."); | ||
window.location.reload(); | ||
@@ -1424,2 +964,3 @@ } | ||
exports.Await = Await; | ||
exports.Form = Form; | ||
exports.Link = Link; | ||
@@ -1432,4 +973,2 @@ exports.Links = Links; | ||
exports.RemixContext = RemixContext; | ||
exports.RemixRoute = RemixRoute; | ||
exports.RemixRouteError = RemixRouteError; | ||
exports.Scripts = Scripts; | ||
@@ -1439,5 +978,5 @@ exports.composeEventHandlers = composeEventHandlers; | ||
exports.useFetcher = useFetcher; | ||
exports.useFetchers = useFetchers; | ||
exports.useLoaderData = useLoaderData; | ||
exports.useMatches = useMatches; | ||
exports.useTransition = useTransition; | ||
exports.useRemixContext = useRemixContext; | ||
exports.useRouteLoaderData = useRouteLoaderData; |
import { UNSAFE_DeferredData as DeferredData } from "@remix-run/router"; | ||
/** | ||
* Data for a route that was returned from a `loader()`. | ||
* | ||
* Note: This moves to unknown in ReactRouter and eventually likely in Remix | ||
*/ | ||
export type AppData = any; | ||
export type AppData = unknown; | ||
export declare function isCatchResponse(response: Response): boolean; | ||
export declare function isErrorResponse(response: any): response is Response; | ||
export declare function isNetworkErrorResponse(response: any): response is Response; | ||
export declare function isRedirectResponse(response: Response): boolean; | ||
export declare function isDeferredResponse(response: Response): boolean; | ||
export declare function isResponse(value: any): value is Response; | ||
export declare function isDeferredData(value: any): value is DeferredData; | ||
export declare function fetchData(request: Request, routeId: string, retry?: number): Promise<Response | Error>; | ||
export declare function createRequestInit(request: Request): Promise<RequestInit>; | ||
export declare function parseDeferredReadableStream(stream: ReadableStream<Uint8Array>): Promise<DeferredData>; |
/** | ||
* @remix-run/react v0.0.0-nightly-ccefed3-20230621 | ||
* @remix-run/react v0.0.0-nightly-cd403b516-20240809 | ||
* | ||
@@ -19,4 +19,2 @@ * Copyright (c) Remix Software Inc. | ||
* Data for a route that was returned from a `loader()`. | ||
* | ||
* Note: This moves to unknown in ReactRouter and eventually likely in Remix | ||
*/ | ||
@@ -30,2 +28,14 @@ | ||
} | ||
function isNetworkErrorResponse(response) { | ||
// If we reach the Remix server, we can safely identify response types via the | ||
// X-Remix-Error/X-Remix-Catch headers. However, if we never reach the Remix | ||
// server, and instead receive a 4xx/5xx from somewhere in between (like | ||
// Cloudflare), then we get a false negative in the isErrorResponse check and | ||
// we incorrectly assume that the user returns the 4xx/5xx response and | ||
// consider it successful. To alleviate this, we add X-Remix-Response to any | ||
// non-Error/non-Catch responses coming back from the server. If we don't | ||
// see this, we can conclude that a 4xx/5xx response never actually reached | ||
// the Remix server and we can bubble it up as an error. | ||
return isResponse(response) && response.status >= 400 && response.headers.get("X-Remix-Error") == null && response.headers.get("X-Remix-Catch") == null && response.headers.get("X-Remix-Response") == null; | ||
} | ||
function isRedirectResponse(response) { | ||
@@ -38,16 +48,12 @@ return response.headers.get("X-Remix-Redirect") != null; | ||
} | ||
function isResponse(value) { | ||
return value != null && typeof value.status === "number" && typeof value.statusText === "string" && typeof value.headers === "object" && typeof value.body !== "undefined"; | ||
} | ||
function isDeferredData(value) { | ||
let deferred = value; | ||
return deferred && typeof deferred === "object" && typeof deferred.data === "object" && typeof deferred.subscribe === "function" && typeof deferred.cancel === "function" && typeof deferred.resolveData === "function"; | ||
} | ||
async function fetchData(request, routeId, retry = 0) { | ||
let url = new URL(request.url); | ||
url.searchParams.set("_data", routeId); | ||
let init = { | ||
signal: request.signal | ||
}; | ||
if (request.method !== "GET") { | ||
init.method = request.method; | ||
let contentType = request.headers.get("Content-Type"); | ||
init.body = | ||
// Check between word boundaries instead of startsWith() due to the last | ||
// paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type | ||
contentType && /\bapplication\/x-www-form-urlencoded\b/.test(contentType) ? new URLSearchParams(await request.text()) : await request.formData(); | ||
} | ||
if (retry > 0) { | ||
@@ -58,2 +64,3 @@ // Retry up to 3 times waiting 50, 250, 1250 ms | ||
} | ||
let init = await createRequestInit(request); | ||
let revalidation = window.__remixRevalidation; | ||
@@ -72,4 +79,38 @@ let response = await fetch(url.href, init).catch(error => { | ||
} | ||
if (isNetworkErrorResponse(response)) { | ||
let text = await response.text(); | ||
let error = new Error(text); | ||
error.stack = undefined; | ||
return error; | ||
} | ||
return response; | ||
} | ||
async function createRequestInit(request) { | ||
let init = { | ||
signal: request.signal | ||
}; | ||
if (request.method !== "GET") { | ||
init.method = request.method; | ||
let contentType = request.headers.get("Content-Type"); | ||
// Check between word boundaries instead of startsWith() due to the last | ||
// paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type | ||
if (contentType && /\bapplication\/json\b/.test(contentType)) { | ||
init.headers = { | ||
"Content-Type": contentType | ||
}; | ||
init.body = JSON.stringify(await request.json()); | ||
} else if (contentType && /\btext\/plain\b/.test(contentType)) { | ||
init.headers = { | ||
"Content-Type": contentType | ||
}; | ||
init.body = await request.text(); | ||
} else if (contentType && /\bapplication\/x-www-form-urlencoded\b/.test(contentType)) { | ||
init.body = new URLSearchParams(await request.text()); | ||
} else { | ||
init.body = await request.formData(); | ||
} | ||
} | ||
return init; | ||
} | ||
const DEFERRED_VALUE_PLACEHOLDER_PREFIX = "__deferred_promise:"; | ||
@@ -114,3 +155,3 @@ async function parseDeferredReadableStream(stream) { | ||
// Read the rest of the stream and resolve deferred promises | ||
(async () => { | ||
void (async () => { | ||
try { | ||
@@ -236,7 +277,11 @@ for await (let section of sectionReader) { | ||
exports.createRequestInit = createRequestInit; | ||
exports.fetchData = fetchData; | ||
exports.isCatchResponse = isCatchResponse; | ||
exports.isDeferredData = isDeferredData; | ||
exports.isDeferredResponse = isDeferredResponse; | ||
exports.isErrorResponse = isErrorResponse; | ||
exports.isNetworkErrorResponse = isNetworkErrorResponse; | ||
exports.isRedirectResponse = isRedirectResponse; | ||
exports.isResponse = isResponse; | ||
exports.parseDeferredReadableStream = parseDeferredReadableStream; |
import type { StaticHandlerContext } from "@remix-run/router"; | ||
import type { RouteManifest, EntryRoute } from "./routes"; | ||
import type { RouteModules } from "./routeModules"; | ||
type SerializedError = { | ||
message: string; | ||
stack?: string; | ||
}; | ||
export interface RemixContextObject { | ||
manifest: AssetsManifest; | ||
routeModules: RouteModules; | ||
criticalCss?: string; | ||
serverHandoffString?: string; | ||
future: FutureConfig; | ||
isSpaMode: boolean; | ||
abortDelay?: number; | ||
dev?: { | ||
port: number; | ||
serializeError?(error: Error): SerializedError; | ||
renderMeta?: { | ||
didRenderScripts?: boolean; | ||
streamCache?: Record<number, Promise<void> & { | ||
result?: { | ||
done: boolean; | ||
value: string; | ||
}; | ||
error?: unknown; | ||
}>; | ||
}; | ||
@@ -16,20 +30,9 @@ } | ||
staticHandlerContext: StaticHandlerContext; | ||
serverHandoffStream?: ReadableStream<Uint8Array>; | ||
} | ||
type Dev = { | ||
port?: number; | ||
appServerPort?: number; | ||
remixRequestHandlerPath?: string; | ||
rebuildPollIntervalMs?: number; | ||
}; | ||
export interface FutureConfig { | ||
v2_dev: boolean | Dev; | ||
/** @deprecated Use the `postcss` config option instead */ | ||
unstable_postcss: boolean; | ||
/** @deprecated Use the `tailwind` config option instead */ | ||
unstable_tailwind: boolean; | ||
v2_errorBoundary: boolean; | ||
v2_headers: boolean; | ||
v2_meta: boolean; | ||
v2_normalizeFormMethod: boolean; | ||
v2_routeConvention: boolean; | ||
v3_fetcherPersist: boolean; | ||
v3_relativeSplatPath: boolean; | ||
unstable_lazyRouteDiscovery: boolean; | ||
unstable_singleFetch: boolean; | ||
} | ||
@@ -45,3 +48,3 @@ export interface AssetsManifest { | ||
hmr?: { | ||
timestamp: number; | ||
timestamp?: number; | ||
runtime: string; | ||
@@ -48,0 +51,0 @@ }; |
@@ -1,8 +0,6 @@ | ||
import React from "react"; | ||
import type { ErrorResponse, Location } from "@remix-run/router"; | ||
import type { CatchBoundaryComponent, ErrorBoundaryComponent } from "./routeModules"; | ||
import type { ThrownResponse } from "./errors"; | ||
import * as React from "react"; | ||
import type { Location } from "@remix-run/router"; | ||
type RemixErrorBoundaryProps = React.PropsWithChildren<{ | ||
location: Location; | ||
component: ErrorBoundaryComponent; | ||
isOutsideRemixApp?: boolean; | ||
error?: Error; | ||
@@ -21,5 +19,5 @@ }>; | ||
error: Error | null; | ||
location: Location; | ||
location: Location<any>; | ||
}; | ||
render(): string | number | boolean | React.ReactFragment | JSX.Element | null | undefined; | ||
render(): string | number | boolean | Iterable<React.ReactNode> | React.JSX.Element | null | undefined; | ||
} | ||
@@ -29,23 +27,12 @@ /** | ||
*/ | ||
export declare function RemixRootDefaultErrorBoundary({ error }: { | ||
error: Error; | ||
}): JSX.Element; | ||
export declare function V2_RemixRootDefaultErrorBoundary(): JSX.Element; | ||
/** | ||
* Returns the status code and thrown response data. | ||
* | ||
* @deprecated Please enable the v2_errorBoundary flag | ||
* | ||
* @see https://remix.run/route/catch-boundary | ||
*/ | ||
export declare function useCatch<Result extends ThrownResponse = ThrownResponse>(): Result; | ||
type RemixCatchBoundaryProps = React.PropsWithChildren<{ | ||
component: CatchBoundaryComponent; | ||
catch?: ErrorResponse; | ||
}>; | ||
export declare function RemixCatchBoundary({ catch: catchVal, component: Component, children, }: RemixCatchBoundaryProps): JSX.Element; | ||
/** | ||
* When app's don't provide a root level CatchBoundary, we default to this. | ||
*/ | ||
export declare function RemixRootDefaultCatchBoundary(): JSX.Element; | ||
export declare function RemixRootDefaultErrorBoundary({ error, isOutsideRemixApp, }: { | ||
error: unknown; | ||
isOutsideRemixApp?: boolean; | ||
}): React.JSX.Element; | ||
export declare function BoundaryShell({ title, renderScripts, isOutsideRemixApp, children, }: { | ||
title: string; | ||
renderScripts?: boolean; | ||
isOutsideRemixApp?: boolean; | ||
children: React.ReactNode | React.ReactNode[]; | ||
}): string | number | boolean | Iterable<React.ReactNode> | React.JSX.Element | null | undefined; | ||
export {}; |
/** | ||
* @remix-run/react v0.0.0-nightly-ccefed3-20230621 | ||
* @remix-run/react v0.0.0-nightly-cd403b516-20240809 | ||
* | ||
@@ -17,8 +17,25 @@ * Copyright (c) Remix Software Inc. | ||
var reactRouterDom = require('react-router-dom'); | ||
var components = require('./components.js'); | ||
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } | ||
function _interopNamespace(e) { | ||
if (e && e.__esModule) return e; | ||
var n = Object.create(null); | ||
if (e) { | ||
Object.keys(e).forEach(function (k) { | ||
if (k !== 'default') { | ||
var d = Object.getOwnPropertyDescriptor(e, k); | ||
Object.defineProperty(n, k, d.get ? d : { | ||
enumerable: true, | ||
get: function () { return e[k]; } | ||
}); | ||
} | ||
}); | ||
} | ||
n["default"] = e; | ||
return Object.freeze(n); | ||
} | ||
var React__default = /*#__PURE__*/_interopDefaultLegacy(React); | ||
var React__namespace = /*#__PURE__*/_interopNamespace(React); | ||
class RemixErrorBoundary extends React__default["default"].Component { | ||
class RemixErrorBoundary extends React__namespace.Component { | ||
constructor(props) { | ||
@@ -64,4 +81,5 @@ super(props); | ||
if (this.state.error) { | ||
return /*#__PURE__*/React__default["default"].createElement(this.props.component, { | ||
error: this.state.error | ||
return /*#__PURE__*/React__namespace.createElement(RemixRootDefaultErrorBoundary, { | ||
error: this.state.error, | ||
isOutsideRemixApp: true | ||
}); | ||
@@ -78,25 +96,39 @@ } else { | ||
function RemixRootDefaultErrorBoundary({ | ||
error | ||
error, | ||
isOutsideRemixApp | ||
}) { | ||
// Only log client side to avoid double-logging on the server | ||
React__default["default"].useEffect(() => { | ||
console.error(error); | ||
}, [error]); | ||
return /*#__PURE__*/React__default["default"].createElement("html", { | ||
lang: "en" | ||
}, /*#__PURE__*/React__default["default"].createElement("head", null, /*#__PURE__*/React__default["default"].createElement("meta", { | ||
charSet: "utf-8" | ||
}), /*#__PURE__*/React__default["default"].createElement("meta", { | ||
name: "viewport", | ||
content: "width=device-width, initial-scale=1, viewport-fit=cover" | ||
}), /*#__PURE__*/React__default["default"].createElement("title", null, "Application Error!")), /*#__PURE__*/React__default["default"].createElement("body", null, /*#__PURE__*/React__default["default"].createElement("main", { | ||
style: { | ||
fontFamily: "system-ui, sans-serif", | ||
padding: "2rem" | ||
console.error(error); | ||
let heyDeveloper = /*#__PURE__*/React__namespace.createElement("script", { | ||
dangerouslySetInnerHTML: { | ||
__html: ` | ||
console.log( | ||
"💿 Hey developer 👋. You can provide a way better UX than this when your app throws errors. Check out https://remix.run/guides/errors for more information." | ||
); | ||
` | ||
} | ||
}, /*#__PURE__*/React__default["default"].createElement("h1", { | ||
}); | ||
if (reactRouterDom.isRouteErrorResponse(error)) { | ||
return /*#__PURE__*/React__namespace.createElement(BoundaryShell, { | ||
title: "Unhandled Thrown Response!" | ||
}, /*#__PURE__*/React__namespace.createElement("h1", { | ||
style: { | ||
fontSize: "24px" | ||
} | ||
}, error.status, " ", error.statusText), heyDeveloper); | ||
} | ||
let errorInstance; | ||
if (error instanceof Error) { | ||
errorInstance = error; | ||
} else { | ||
let errorString = error == null ? "Unknown Error" : typeof error === "object" && "toString" in error ? error.toString() : JSON.stringify(error); | ||
errorInstance = new Error(errorString); | ||
} | ||
return /*#__PURE__*/React__namespace.createElement(BoundaryShell, { | ||
title: "Application Error!", | ||
isOutsideRemixApp: isOutsideRemixApp | ||
}, /*#__PURE__*/React__namespace.createElement("h1", { | ||
style: { | ||
fontSize: "24px" | ||
} | ||
}, "Application Error"), error.stack ? /*#__PURE__*/React__default["default"].createElement("pre", { | ||
}, "Application Error"), /*#__PURE__*/React__namespace.createElement("pre", { | ||
style: { | ||
@@ -108,74 +140,42 @@ padding: "2rem", | ||
} | ||
}, error.stack) : null), /*#__PURE__*/React__default["default"].createElement("script", { | ||
dangerouslySetInnerHTML: { | ||
__html: ` | ||
console.log( | ||
"💿 Hey developer👋. You can provide a way better UX than this when your app throws errors. Check out https://remix.run/guides/errors for more information." | ||
); | ||
` | ||
} | ||
}))); | ||
}, errorInstance.stack), heyDeveloper); | ||
} | ||
function V2_RemixRootDefaultErrorBoundary() { | ||
let error = reactRouterDom.useRouteError(); | ||
if (reactRouterDom.isRouteErrorResponse(error)) { | ||
return /*#__PURE__*/React__default["default"].createElement(RemixRootDefaultCatchBoundaryImpl, { | ||
caught: error | ||
}); | ||
} else if (error instanceof Error) { | ||
return /*#__PURE__*/React__default["default"].createElement(RemixRootDefaultErrorBoundary, { | ||
error: error | ||
}); | ||
} else { | ||
let errorString = error == null ? "Unknown Error" : typeof error === "object" && "toString" in error ? error.toString() : JSON.stringify(error); | ||
return /*#__PURE__*/React__default["default"].createElement(RemixRootDefaultErrorBoundary, { | ||
error: new Error(errorString) | ||
}); | ||
} | ||
} | ||
let RemixCatchContext = /*#__PURE__*/React__default["default"].createContext(undefined); | ||
/** | ||
* Returns the status code and thrown response data. | ||
* | ||
* @deprecated Please enable the v2_errorBoundary flag | ||
* | ||
* @see https://remix.run/route/catch-boundary | ||
*/ | ||
function useCatch() { | ||
return React.useContext(RemixCatchContext); | ||
} | ||
function RemixCatchBoundary({ | ||
catch: catchVal, | ||
component: Component, | ||
function BoundaryShell({ | ||
title, | ||
renderScripts, | ||
isOutsideRemixApp, | ||
children | ||
}) { | ||
if (catchVal) { | ||
return /*#__PURE__*/React__default["default"].createElement(RemixCatchContext.Provider, { | ||
value: catchVal | ||
}, /*#__PURE__*/React__default["default"].createElement(Component, null)); | ||
var _routeModules$root; | ||
let { | ||
routeModules | ||
} = components.useRemixContext(); | ||
// Generally speaking, when the root route has a Layout we want to use that | ||
// as the app shell instead of the default `BoundaryShell` wrapper markup below. | ||
// This is true for `loader`/`action` errors, most render errors, and | ||
// `HydrateFallback` scenarios. | ||
// However, render errors thrown from the `Layout` present a bit of an issue | ||
// because if the `Layout` itself throws during the `ErrorBoundary` pass and | ||
// we bubble outside the `RouterProvider` to the wrapping `RemixErrorBoundary`, | ||
// by returning only `children` here we'll be trying to append a `<div>` to | ||
// the `document` and the DOM will throw, putting React into an error/hydration | ||
// loop. | ||
// Instead, if we're ever rendering from the outermost `RemixErrorBoundary` | ||
// during hydration that wraps `RouterProvider`, then we can't trust the | ||
// `Layout` and should fallback to the default app shell so we're always | ||
// returning an `<html>` document. | ||
if ((_routeModules$root = routeModules.root) !== null && _routeModules$root !== void 0 && _routeModules$root.Layout && !isOutsideRemixApp) { | ||
return children; | ||
} | ||
return /*#__PURE__*/React__default["default"].createElement(React__default["default"].Fragment, null, children); | ||
} | ||
/** | ||
* When app's don't provide a root level CatchBoundary, we default to this. | ||
*/ | ||
function RemixRootDefaultCatchBoundary() { | ||
let caught = useCatch(); | ||
return /*#__PURE__*/React__default["default"].createElement(RemixRootDefaultCatchBoundaryImpl, { | ||
caught: caught | ||
}); | ||
} | ||
function RemixRootDefaultCatchBoundaryImpl({ | ||
caught | ||
}) { | ||
return /*#__PURE__*/React__default["default"].createElement("html", { | ||
return /*#__PURE__*/React__namespace.createElement("html", { | ||
lang: "en" | ||
}, /*#__PURE__*/React__default["default"].createElement("head", null, /*#__PURE__*/React__default["default"].createElement("meta", { | ||
}, /*#__PURE__*/React__namespace.createElement("head", null, /*#__PURE__*/React__namespace.createElement("meta", { | ||
charSet: "utf-8" | ||
}), /*#__PURE__*/React__default["default"].createElement("meta", { | ||
}), /*#__PURE__*/React__namespace.createElement("meta", { | ||
name: "viewport", | ||
content: "width=device-width, initial-scale=1, viewport-fit=cover" | ||
}), /*#__PURE__*/React__default["default"].createElement("title", null, "Unhandled Thrown Response!")), /*#__PURE__*/React__default["default"].createElement("body", null, /*#__PURE__*/React__default["default"].createElement("h1", { | ||
content: "width=device-width,initial-scale=1,viewport-fit=cover" | ||
}), /*#__PURE__*/React__namespace.createElement("title", null, title)), /*#__PURE__*/React__namespace.createElement("body", null, /*#__PURE__*/React__namespace.createElement("main", { | ||
style: { | ||
@@ -185,18 +185,7 @@ fontFamily: "system-ui, sans-serif", | ||
} | ||
}, caught.status, " ", caught.statusText), /*#__PURE__*/React__default["default"].createElement("script", { | ||
dangerouslySetInnerHTML: { | ||
__html: ` | ||
console.log( | ||
"💿 Hey developer👋. You can provide a way better UX than this when your app throws 404s (and other responses). Check out https://remix.run/guides/not-found for more information." | ||
); | ||
` | ||
} | ||
}))); | ||
}, children, renderScripts ? /*#__PURE__*/React__namespace.createElement(components.Scripts, null) : null))); | ||
} | ||
exports.RemixCatchBoundary = RemixCatchBoundary; | ||
exports.BoundaryShell = BoundaryShell; | ||
exports.RemixErrorBoundary = RemixErrorBoundary; | ||
exports.RemixRootDefaultCatchBoundary = RemixRootDefaultCatchBoundary; | ||
exports.RemixRootDefaultErrorBoundary = RemixRootDefaultErrorBoundary; | ||
exports.V2_RemixRootDefaultErrorBoundary = V2_RemixRootDefaultErrorBoundary; | ||
exports.useCatch = useCatch; |
import type { Router as RemixRouter } from "@remix-run/router"; | ||
import type { AppData } from "./data"; | ||
/** | ||
* @deprecated in favor of the `ErrorResponse` class in React Router. Please | ||
* enable the `future.v2_errorBoundary` flag to ease your migration to Remix v2. | ||
*/ | ||
export interface ThrownResponse<Status extends number = number, Data = AppData> { | ||
status: Status; | ||
statusText: string; | ||
data: Data; | ||
} | ||
export declare function deserializeErrors(errors: RemixRouter["state"]["errors"]): RemixRouter["state"]["errors"]; |
/** | ||
* @remix-run/react v0.0.0-nightly-ccefed3-20230621 | ||
* @remix-run/react v0.0.0-nightly-cd403b516-20240809 | ||
* | ||
@@ -17,7 +17,2 @@ * Copyright (c) Remix Software Inc. | ||
/** | ||
* @deprecated in favor of the `ErrorResponse` class in React Router. Please | ||
* enable the `future.v2_errorBoundary` flag to ease your migration to Remix v2. | ||
*/ | ||
function deserializeErrors(errors) { | ||
@@ -31,7 +26,23 @@ if (!errors) return null; | ||
if (val && val.__type === "RouteErrorResponse") { | ||
serialized[key] = new router.ErrorResponse(val.status, val.statusText, val.data, val.internal === true); | ||
serialized[key] = new router.UNSAFE_ErrorResponseImpl(val.status, val.statusText, val.data, val.internal === true); | ||
} else if (val && val.__type === "Error") { | ||
let error = new Error(val.message); | ||
error.stack = val.stack; | ||
serialized[key] = error; | ||
// Attempt to reconstruct the right type of Error (i.e., ReferenceError) | ||
if (val.__subType) { | ||
let ErrorConstructor = window[val.__subType]; | ||
if (typeof ErrorConstructor === "function") { | ||
try { | ||
// @ts-expect-error | ||
let error = new ErrorConstructor(val.message); | ||
error.stack = val.stack; | ||
serialized[key] = error; | ||
} catch (e) { | ||
// no-op - fall through and create a normal Error | ||
} | ||
} | ||
} | ||
if (serialized[key] == null) { | ||
let error = new Error(val.message); | ||
error.stack = val.stack; | ||
serialized[key] = error; | ||
} | ||
} else { | ||
@@ -38,0 +49,0 @@ serialized[key] = val; |
/** | ||
* @remix-run/react v0.0.0-nightly-ccefed3-20230621 | ||
* @remix-run/react v0.0.0-nightly-cd403b516-20240809 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
/** | ||
* @remix-run/react v0.0.0-nightly-ccefed3-20230621 | ||
* @remix-run/react v0.0.0-nightly-cd403b516-20240809 | ||
* | ||
@@ -11,8 +11,13 @@ * Copyright (c) Remix Software Inc. | ||
*/ | ||
import { createRouter, createBrowserHistory } from '@remix-run/router'; | ||
import * as React from 'react'; | ||
import { createBrowserRouter, RouterProvider } from 'react-router-dom'; | ||
import { UNSAFE_mapRouteProperties } from 'react-router'; | ||
import { matchRoutes, RouterProvider } from 'react-router-dom'; | ||
import { RemixContext } from './components.js'; | ||
import { RemixErrorBoundary, RemixRootDefaultErrorBoundary } from './errorBoundaries.js'; | ||
import { RemixErrorBoundary } from './errorBoundaries.js'; | ||
import { deserializeErrors } from './errors.js'; | ||
import { createClientRoutesWithHMRRevalidationOptOut, createClientRoutes } from './routes.js'; | ||
import { createClientRoutesWithHMRRevalidationOptOut, createClientRoutes, shouldHydrateRouteLoader } from './routes.js'; | ||
import { decodeViaTurboStream, getSingleFetchDataStrategy } from './single-fetch.js'; | ||
import invariant from './invariant.js'; | ||
import { initFogOfWar, useFogOFWarDiscovery } from './fog-of-war.js'; | ||
@@ -23,5 +28,23 @@ /* eslint-disable prefer-let/prefer-let */ | ||
let stateDecodingPromise; | ||
let router; | ||
let routerInitialized = false; | ||
let hmrAbortController; | ||
let hmrRouterReadyResolve; | ||
// There's a race condition with HMR where the remix:manifest is signaled before | ||
// the router is assigned in the RemixBrowser component. This promise gates the | ||
// HMR handler until the router is ready | ||
let hmrRouterReadyPromise = new Promise(resolve => { | ||
// body of a promise is executed immediately, so this can be resolved outside | ||
// of the promise body | ||
hmrRouterReadyResolve = resolve; | ||
}).catch(() => { | ||
// This is a noop catch handler to avoid unhandled promise rejection warnings | ||
// in the console. The promise is never rejected. | ||
return undefined; | ||
}); | ||
// @ts-expect-error | ||
if (import.meta && import.meta.hot) { | ||
// @ts-expect-error | ||
import.meta.hot.accept("remix:manifest", async ({ | ||
@@ -31,2 +54,8 @@ assetsManifest, | ||
}) => { | ||
let router = await hmrRouterReadyPromise; | ||
// This should never happen, but just in case... | ||
if (!router) { | ||
console.error("Failed to accept HMR update because the router was not ready."); | ||
return; | ||
} | ||
let routeIds = [...new Set(router.state.matches.map(m => m.route.id).concat(Object.keys(window.__remixRouteModules)))]; | ||
@@ -51,4 +80,4 @@ if (hmrAbortController) { | ||
default: imported.default ? ((_window$__remixRouteM = window.__remixRouteModules[id]) === null || _window$__remixRouteM === void 0 ? void 0 : _window$__remixRouteM.default) ?? imported.default : imported.default, | ||
CatchBoundary: imported.CatchBoundary ? ((_window$__remixRouteM2 = window.__remixRouteModules[id]) === null || _window$__remixRouteM2 === void 0 ? void 0 : _window$__remixRouteM2.CatchBoundary) ?? imported.CatchBoundary : imported.CatchBoundary, | ||
ErrorBoundary: imported.ErrorBoundary ? ((_window$__remixRouteM3 = window.__remixRouteModules[id]) === null || _window$__remixRouteM3 === void 0 ? void 0 : _window$__remixRouteM3.ErrorBoundary) ?? imported.ErrorBoundary : imported.ErrorBoundary | ||
ErrorBoundary: imported.ErrorBoundary ? ((_window$__remixRouteM2 = window.__remixRouteModules[id]) === null || _window$__remixRouteM2 === void 0 ? void 0 : _window$__remixRouteM2.ErrorBoundary) ?? imported.ErrorBoundary : imported.ErrorBoundary, | ||
HydrateFallback: imported.HydrateFallback ? ((_window$__remixRouteM3 = window.__remixRouteModules[id]) === null || _window$__remixRouteM3 === void 0 ? void 0 : _window$__remixRouteM3.HydrateFallback) ?? imported.HydrateFallback : imported.HydrateFallback | ||
}]; | ||
@@ -58,3 +87,3 @@ }))).filter(Boolean))); | ||
// Create new routes | ||
let routes = createClientRoutesWithHMRRevalidationOptOut(needsRevalidation, assetsManifest.routes, window.__remixRouteModules, window.__remixContext.future); | ||
let routes = createClientRoutesWithHMRRevalidationOptOut(needsRevalidation, assetsManifest.routes, window.__remixRouteModules, window.__remixContext.state, window.__remixContext.future, window.__remixContext.isSpaMode); | ||
@@ -91,37 +120,157 @@ // This is temporary API and will be more granular before release | ||
if (!router) { | ||
let routes = createClientRoutes(window.__remixManifest.routes, window.__remixRouteModules, window.__remixContext.future); | ||
let hydrationData = window.__remixContext.state; | ||
if (hydrationData && hydrationData.errors) { | ||
// Hard reload if the path we tried to load is not the current path. | ||
// This is usually the result of 2 rapid back/forward clicks from an | ||
// external site into a Remix app, where we initially start the load for | ||
// one URL and while the JS chunks are loading a second forward click moves | ||
// us to a new URL. Avoid comparing search params because of CDNs which | ||
// can be configured to ignore certain params and only pathname is relevant | ||
// towards determining the route matches. | ||
let initialPathname = window.__remixContext.url; | ||
let hydratedPathname = window.location.pathname; | ||
if (initialPathname !== hydratedPathname && !window.__remixContext.isSpaMode) { | ||
let errorMsg = `Initial URL (${initialPathname}) does not match URL at time of hydration ` + `(${hydratedPathname}), reloading page...`; | ||
console.error(errorMsg); | ||
window.location.reload(); | ||
// Get out of here so the reload can happen - don't create the router | ||
// since it'll then kick off unnecessary route.lazy() loads | ||
return /*#__PURE__*/React.createElement(React.Fragment, null); | ||
} | ||
// When single fetch is enabled, we need to suspend until the initial state | ||
// snapshot is decoded into window.__remixContext.state | ||
if (window.__remixContext.future.unstable_singleFetch) { | ||
// Note: `stateDecodingPromise` is not coupled to `router` - we'll reach this | ||
// code potentially many times waiting for our state to arrive, but we'll | ||
// then only get past here and create the `router` one time | ||
if (!stateDecodingPromise) { | ||
let stream = window.__remixContext.stream; | ||
invariant(stream, "No stream found for single fetch decoding"); | ||
window.__remixContext.stream = undefined; | ||
stateDecodingPromise = decodeViaTurboStream(stream, window).then(value => { | ||
window.__remixContext.state = value.value; | ||
stateDecodingPromise.value = true; | ||
}).catch(e => { | ||
stateDecodingPromise.error = e; | ||
}); | ||
} | ||
if (stateDecodingPromise.error) { | ||
throw stateDecodingPromise.error; | ||
} | ||
if (!stateDecodingPromise.value) { | ||
throw stateDecodingPromise; | ||
} | ||
} | ||
let routes = createClientRoutes(window.__remixManifest.routes, window.__remixRouteModules, window.__remixContext.state, window.__remixContext.future, window.__remixContext.isSpaMode); | ||
let hydrationData = undefined; | ||
if (!window.__remixContext.isSpaMode) { | ||
// Create a shallow clone of `loaderData` we can mutate for partial hydration. | ||
// When a route exports a `clientLoader` and a `HydrateFallback`, the SSR will | ||
// render the fallback so we need the client to do the same for hydration. | ||
// The server loader data has already been exposed to these route `clientLoader`'s | ||
// in `createClientRoutes` above, so we need to clear out the version we pass to | ||
// `createBrowserRouter` so it initializes and runs the client loaders. | ||
hydrationData = { | ||
...hydrationData, | ||
errors: deserializeErrors(hydrationData.errors) | ||
...window.__remixContext.state, | ||
loaderData: { | ||
...window.__remixContext.state.loaderData | ||
} | ||
}; | ||
let initialMatches = matchRoutes(routes, window.location, window.__remixContext.basename); | ||
if (initialMatches) { | ||
for (let match of initialMatches) { | ||
let routeId = match.route.id; | ||
let route = window.__remixRouteModules[routeId]; | ||
let manifestRoute = window.__remixManifest.routes[routeId]; | ||
// Clear out the loaderData to avoid rendering the route component when the | ||
// route opted into clientLoader hydration and either: | ||
// * gave us a HydrateFallback | ||
// * or doesn't have a server loader and we have no data to render | ||
if (route && shouldHydrateRouteLoader(manifestRoute, route, window.__remixContext.isSpaMode) && (route.HydrateFallback || !manifestRoute.hasLoader)) { | ||
hydrationData.loaderData[routeId] = undefined; | ||
} else if (manifestRoute && !manifestRoute.hasLoader) { | ||
// Since every Remix route gets a `loader` on the client side to load | ||
// the route JS module, we need to add a `null` value to `loaderData` | ||
// for any routes that don't have server loaders so our partial | ||
// hydration logic doesn't kick off the route module loaders during | ||
// hydration | ||
hydrationData.loaderData[routeId] = null; | ||
} | ||
} | ||
} | ||
if (hydrationData && hydrationData.errors) { | ||
hydrationData.errors = deserializeErrors(hydrationData.errors); | ||
} | ||
} | ||
router = createBrowserRouter(routes, { | ||
let { | ||
enabled: isFogOfWarEnabled, | ||
patchRoutesOnMiss | ||
} = initFogOfWar(window.__remixManifest, window.__remixRouteModules, window.__remixContext.future, window.__remixContext.isSpaMode, window.__remixContext.basename); | ||
// We don't use createBrowserRouter here because we need fine-grained control | ||
// over initialization to support synchronous `clientLoader` flows. | ||
router = createRouter({ | ||
routes, | ||
history: createBrowserHistory(), | ||
basename: window.__remixContext.basename, | ||
future: { | ||
v7_normalizeFormMethod: true, | ||
v7_fetcherPersist: window.__remixContext.future.v3_fetcherPersist, | ||
v7_partialHydration: true, | ||
v7_prependBasename: true, | ||
v7_relativeSplatPath: window.__remixContext.future.v3_relativeSplatPath, | ||
// Single fetch enables this underlying behavior | ||
v7_skipActionErrorRevalidation: window.__remixContext.future.unstable_singleFetch === true | ||
}, | ||
hydrationData, | ||
future: { | ||
// Pass through the Remix future flag to avoid a v1 breaking change in | ||
// useNavigation() - users can control the casing via the flag in v1. | ||
// useFetcher still always uppercases in the back-compat layer in v1. | ||
// In v2 we can just always pass true here and remove the back-compat | ||
// layer | ||
v7_normalizeFormMethod: window.__remixContext.future.v2_normalizeFormMethod | ||
} | ||
mapRouteProperties: UNSAFE_mapRouteProperties, | ||
unstable_dataStrategy: window.__remixContext.future.unstable_singleFetch ? getSingleFetchDataStrategy(window.__remixManifest, window.__remixRouteModules) : undefined, | ||
...(isFogOfWarEnabled ? { | ||
unstable_patchRoutesOnMiss: patchRoutesOnMiss | ||
} : {}) | ||
}); | ||
// Hard reload if the URL we tried to load is not the current URL. | ||
// This is usually the result of 2 rapid backwards/forward clicks from an | ||
// external site into a Remix app, where we initially start the load for | ||
// one URL and while the JS chunks are loading a second forward click moves | ||
// us to a new URL | ||
let initialUrl = window.__remixContext.url; | ||
let hydratedUrl = window.location.pathname + window.location.search; | ||
if (initialUrl !== hydratedUrl) { | ||
let errorMsg = `Initial URL (${initialUrl}) does not match URL at time of hydration ` + `(${hydratedUrl}), reloading page...`; | ||
console.error(errorMsg); | ||
window.location.reload(); | ||
// We can call initialize() immediately if the router doesn't have any | ||
// loaders to run on hydration | ||
if (router.state.initialized) { | ||
routerInitialized = true; | ||
router.initialize(); | ||
} | ||
// @ts-ignore | ||
router.createRoutesForHMR = createClientRoutesWithHMRRevalidationOptOut; | ||
window.__remixRouter = router; | ||
// Notify that the router is ready for HMR | ||
if (hmrRouterReadyResolve) { | ||
hmrRouterReadyResolve(router); | ||
} | ||
} | ||
// Critical CSS can become stale after code changes, e.g. styles might be | ||
// removed from a component, but the styles will still be present in the | ||
// server HTML. This allows our HMR logic to clear the critical CSS state. | ||
// eslint-disable-next-line react-hooks/rules-of-hooks | ||
let [criticalCss, setCriticalCss] = React.useState(process.env.NODE_ENV === "development" ? window.__remixContext.criticalCss : undefined); | ||
if (process.env.NODE_ENV === "development") { | ||
window.__remixClearCriticalCss = () => setCriticalCss(undefined); | ||
} | ||
// This is due to the short circuit return above when the pathname doesn't | ||
// match and we force a hard reload. This is an exceptional scenario in which | ||
// we can't hydrate anyway. | ||
// eslint-disable-next-line react-hooks/rules-of-hooks | ||
let [location, setLocation] = React.useState(router.state.location); | ||
// eslint-disable-next-line react-hooks/rules-of-hooks | ||
React.useLayoutEffect(() => { | ||
// If we had to run clientLoaders on hydration, we delay initialization until | ||
// after we've hydrated to avoid hydration issues from synchronous client loaders | ||
if (!routerInitialized) { | ||
routerInitialized = true; | ||
router.initialize(); | ||
} | ||
}, []); | ||
// eslint-disable-next-line react-hooks/rules-of-hooks | ||
React.useLayoutEffect(() => { | ||
return router.subscribe(newState => { | ||
@@ -134,2 +283,5 @@ if (newState.location !== location) { | ||
// eslint-disable-next-line react-hooks/rules-of-hooks | ||
useFogOFWarDiscovery(router, window.__remixManifest, window.__remixRouteModules, window.__remixContext.future, window.__remixContext.isSpaMode); | ||
// We need to include a wrapper RemixErrorBoundary here in case the root error | ||
@@ -139,20 +291,26 @@ // boundary also throws and we need to bubble up outside of the router entirely. | ||
// out of there | ||
return /*#__PURE__*/React.createElement(RemixContext.Provider, { | ||
value: { | ||
manifest: window.__remixManifest, | ||
routeModules: window.__remixRouteModules, | ||
future: window.__remixContext.future | ||
} | ||
}, /*#__PURE__*/React.createElement(RemixErrorBoundary, { | ||
location: location, | ||
component: RemixRootDefaultErrorBoundary | ||
}, /*#__PURE__*/React.createElement(RouterProvider, { | ||
router: router, | ||
fallbackElement: null, | ||
future: { | ||
v7_startTransition: true | ||
} | ||
}))); | ||
return ( | ||
/*#__PURE__*/ | ||
// This fragment is important to ensure we match the <RemixServer> JSX | ||
// structure so that useId values hydrate correctly | ||
React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(RemixContext.Provider, { | ||
value: { | ||
manifest: window.__remixManifest, | ||
routeModules: window.__remixRouteModules, | ||
future: window.__remixContext.future, | ||
criticalCss, | ||
isSpaMode: window.__remixContext.isSpaMode | ||
} | ||
}, /*#__PURE__*/React.createElement(RemixErrorBoundary, { | ||
location: location | ||
}, /*#__PURE__*/React.createElement(RouterProvider, { | ||
router: router, | ||
fallbackElement: null, | ||
future: { | ||
v7_startTransition: true | ||
} | ||
}))), window.__remixContext.future.unstable_singleFetch ? /*#__PURE__*/React.createElement(React.Fragment, null) : null) | ||
); | ||
} | ||
export { RemixBrowser }; |
/** | ||
* @remix-run/react v0.0.0-nightly-ccefed3-20230621 | ||
* @remix-run/react v0.0.0-nightly-cd403b516-20240809 | ||
* | ||
@@ -13,9 +13,8 @@ * Copyright (c) Remix Software Inc. | ||
import * as React from 'react'; | ||
import { useHref, NavLink as NavLink$1, Link as Link$1, matchRoutes, useLocation, Await as Await$1, useNavigation, useAsyncError, useMatches as useMatches$1, useLoaderData as useLoaderData$1, useActionData as useActionData$1, useFetchers as useFetchers$1, useFetcher as useFetcher$1, UNSAFE_DataRouterContext, UNSAFE_DataRouterStateContext, Outlet, useRouteError, isRouteErrorResponse } from 'react-router-dom'; | ||
import { RemixRootDefaultErrorBoundary, RemixCatchBoundary, V2_RemixRootDefaultErrorBoundary, RemixRootDefaultCatchBoundary } from './errorBoundaries.js'; | ||
import { useHref, NavLink as NavLink$1, Link as Link$1, Form as Form$1, matchRoutes, useLocation, Await as Await$1, useAsyncError, useMatches as useMatches$1, useLoaderData as useLoaderData$1, useRouteLoaderData as useRouteLoaderData$1, useActionData as useActionData$1, useFetcher as useFetcher$1, UNSAFE_DataRouterContext, UNSAFE_DataRouterStateContext } from 'react-router-dom'; | ||
import invariant from './invariant.js'; | ||
import { getLinksForMatches, isPageLinkDescriptor, getNewMatchesForLinks, getDataLinkHrefs, getModuleLinkHrefs, getStylesheetPrefetchLinks } from './links.js'; | ||
import { getKeyedLinksForMatches, isPageLinkDescriptor, getNewMatchesForLinks, getDataLinkHrefs, getModuleLinkHrefs, getKeyedPrefetchLinks } from './links.js'; | ||
import { escapeHtml, createHtml } from './markup.js'; | ||
import { IDLE_TRANSITION, IDLE_FETCHER } from './transition.js'; | ||
import { logDeprecationOnce } from './warnings.js'; | ||
import { addRevalidationParam, singleFetchUrl } from './single-fetch.js'; | ||
import { isFogOfWarEnabled, getPartialManifest } from './fog-of-war.js'; | ||
@@ -45,92 +44,20 @@ function useDataRouterContext() { | ||
//////////////////////////////////////////////////////////////////////////////// | ||
// RemixRoute | ||
function RemixRoute({ | ||
id | ||
}) { | ||
let { | ||
routeModules, | ||
future | ||
} = useRemixContext(); | ||
invariant(routeModules, "Cannot initialize 'routeModules'. This normally occurs when you have server code in your client modules.\n" + "Check this link for more details:\nhttps://remix.run/pages/gotchas#server-code-in-client-bundles"); | ||
let { | ||
default: Component, | ||
ErrorBoundary, | ||
CatchBoundary | ||
} = routeModules[id]; | ||
// Default Component to Outlet if we expose boundary UI components | ||
if (!Component && (ErrorBoundary || !future.v2_errorBoundary && CatchBoundary)) { | ||
Component = Outlet; | ||
} | ||
invariant(Component, `Route "${id}" has no component! Please go add a \`default\` export in the route module file.\n` + "If you were trying to navigate or submit to a resource route, use `<a>` instead of `<Link>` or `<Form reloadDocument>`."); | ||
return /*#__PURE__*/React.createElement(Component, null); | ||
} | ||
function RemixRouteError({ | ||
id | ||
}) { | ||
let { | ||
future, | ||
routeModules | ||
} = useRemixContext(); | ||
// This checks prevent cryptic error messages such as: 'Cannot read properties of undefined (reading 'root')' | ||
invariant(routeModules, "Cannot initialize 'routeModules'. This normally occurs when you have server code in your client modules.\n" + "Check this link for more details:\nhttps://remix.run/pages/gotchas#server-code-in-client-bundles"); | ||
let error = useRouteError(); | ||
let { | ||
CatchBoundary, | ||
ErrorBoundary | ||
} = routeModules[id]; | ||
if (future.v2_errorBoundary) { | ||
// Provide defaults for the root route if they are not present | ||
if (id === "root") { | ||
ErrorBoundary || (ErrorBoundary = V2_RemixRootDefaultErrorBoundary); | ||
} | ||
if (ErrorBoundary) { | ||
// TODO: Unsure if we can satisfy the typings here | ||
// @ts-expect-error | ||
return /*#__PURE__*/React.createElement(ErrorBoundary, null); | ||
} | ||
throw error; | ||
} | ||
// Provide defaults for the root route if they are not present | ||
if (id === "root") { | ||
CatchBoundary || (CatchBoundary = RemixRootDefaultCatchBoundary); | ||
ErrorBoundary || (ErrorBoundary = RemixRootDefaultErrorBoundary); | ||
} | ||
if (isRouteErrorResponse(error)) { | ||
let tError = error; | ||
if (!!(tError !== null && tError !== void 0 && tError.error) && tError.status !== 404 && ErrorBoundary) { | ||
// Internal framework-thrown ErrorResponses | ||
return /*#__PURE__*/React.createElement(ErrorBoundary, { | ||
error: tError.error | ||
}); | ||
} | ||
if (CatchBoundary) { | ||
// User-thrown ErrorResponses | ||
return /*#__PURE__*/React.createElement(RemixCatchBoundary, { | ||
catch: error, | ||
component: CatchBoundary | ||
}); | ||
} | ||
} | ||
if (error instanceof Error && ErrorBoundary) { | ||
// User- or framework-thrown Errors | ||
return /*#__PURE__*/React.createElement(ErrorBoundary, { | ||
error: error | ||
}); | ||
} | ||
throw error; | ||
} | ||
//////////////////////////////////////////////////////////////////////////////// | ||
// Public API | ||
/** | ||
* Defines the discovery behavior of the link: | ||
* | ||
* - "render": Eagerly discover when the link is rendered (default) | ||
* - "none": No eager discovery - discover when the link is clicked | ||
*/ | ||
/** | ||
* Defines the prefetching behavior of the link: | ||
* | ||
* - "none": Never fetched | ||
* - "intent": Fetched when the user focuses or hovers the link | ||
* - "render": Fetched when the link is rendered | ||
* - "none": Never fetched | ||
* - "viewport": Fetched when the link is in the viewport | ||
*/ | ||
function usePrefetchBehavior(prefetch, theirElementProps) { | ||
@@ -196,5 +123,8 @@ let [maybePrefetch, setMaybePrefetch] = React.useState(false); | ||
const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i; | ||
function getDiscoverAttr(discover, isAbsolute, reloadDocument) { | ||
return discover === "render" && !isAbsolute && !reloadDocument ? "true" : undefined; | ||
} | ||
/** | ||
* A special kind of `<Link>` that knows whether or not it is "active". | ||
* A special kind of `<Link>` that knows whether it is "active". | ||
* | ||
@@ -206,2 +136,3 @@ * @see https://remix.run/components/nav-link | ||
prefetch = "none", | ||
discover = "render", | ||
...props | ||
@@ -214,3 +145,4 @@ }, forwardedRef) => { | ||
ref: mergeRefs(forwardedRef, ref), | ||
to: to | ||
to: to, | ||
"data-discover": getDiscoverAttr(discover, isAbsolute, props.reloadDocument) | ||
})), shouldPrefetch && !isAbsolute ? /*#__PURE__*/React.createElement(PrefetchPageLinks, { | ||
@@ -231,2 +163,3 @@ page: href | ||
prefetch = "none", | ||
discover = "render", | ||
...props | ||
@@ -239,3 +172,4 @@ }, forwardedRef) => { | ||
ref: mergeRefs(forwardedRef, ref), | ||
to: to | ||
to: to, | ||
"data-discover": getDiscoverAttr(discover, isAbsolute, props.reloadDocument) | ||
})), shouldPrefetch && !isAbsolute ? /*#__PURE__*/React.createElement(PrefetchPageLinks, { | ||
@@ -246,2 +180,19 @@ page: href | ||
Link.displayName = "Link"; | ||
/** | ||
* This component renders a form tag and is the primary way the user will | ||
* submit information via your website. | ||
* | ||
* @see https://remix.run/components/form | ||
*/ | ||
let Form = /*#__PURE__*/React.forwardRef(({ | ||
discover = "render", | ||
...props | ||
}, forwardedRef) => { | ||
let isAbsolute = typeof props.action === "string" && ABSOLUTE_URL_REGEX.test(props.action); | ||
return /*#__PURE__*/React.createElement(Form$1, _extends({}, props, { | ||
ref: forwardedRef, | ||
"data-discover": getDiscoverAttr(discover, isAbsolute, props.reloadDocument) | ||
})); | ||
}); | ||
Form.displayName = "Form"; | ||
function composeEventHandlers(theirHandler, ourHandler) { | ||
@@ -255,7 +206,19 @@ return event => { | ||
} | ||
let linksWarning = "⚠️ REMIX FUTURE CHANGE: The behavior of links `imagesizes` and `imagesrcset` will be changing in v2. " + "Only the React camel case versions will be valid. Please change to `imageSizes` and `imageSrcSet`. " + "For instructions on making this change see " + "https://remix.run/docs/en/v1.15.0/pages/v2#links-imagesizes-and-imagesrcset"; | ||
let useTransitionWarning = "⚠️ REMIX FUTURE CHANGE: `useTransition` will be removed in v2 in favor of `useNavigation`. " + "You can prepare for this change at your convenience by updating to `useNavigation`. " + "For instructions on making this change see " + "https://remix.run/docs/en/v1.15.0/pages/v2#usetransition"; | ||
let fetcherTypeWarning = "⚠️ REMIX FUTURE CHANGE: `fetcher.type` will be removed in v2. " + "Please use `fetcher.state`, `fetcher.formData`, and `fetcher.data` to achieve the same UX. " + "For instructions on making this change see " + "https://remix.run/docs/en/v1.15.0/pages/v2#usefetcher"; | ||
let fetcherSubmissionWarning = "⚠️ REMIX FUTURE CHANGE : `fetcher.submission` will be removed in v2. " + "The submission fields are now part of the fetcher object itself (`fetcher.formData`). " + "For instructions on making this change see " + "https://remix.run/docs/en/v1.15.0/pages/v2#usefetcher"; | ||
// Return the matches actively being displayed: | ||
// - In SPA Mode we only SSR/hydrate the root match, and include all matches | ||
// after hydration. This lets the router handle initial match loads via lazy(). | ||
// - When an error boundary is rendered, we slice off matches up to the | ||
// boundary for <Links>/<Meta> | ||
function getActiveMatches(matches, errors, isSpaMode) { | ||
if (isSpaMode && !isHydrated) { | ||
return [matches[0]]; | ||
} | ||
if (errors) { | ||
let errorIdx = matches.findIndex(m => errors[m.route.id] !== undefined); | ||
return matches.slice(0, errorIdx + 1); | ||
} | ||
return matches; | ||
} | ||
/** | ||
@@ -268,4 +231,6 @@ * Renders the `<link>` tags for the current routes. | ||
let { | ||
isSpaMode, | ||
manifest, | ||
routeModules | ||
routeModules, | ||
criticalCss | ||
} = useRemixContext(); | ||
@@ -276,48 +241,20 @@ let { | ||
} = useDataRouterStateContext(); | ||
let matches = errors ? routerMatches.slice(0, routerMatches.findIndex(m => errors[m.route.id]) + 1) : routerMatches; | ||
let links = React.useMemo(() => getLinksForMatches(matches, routeModules, manifest), [matches, routeModules, manifest]); | ||
React.useEffect(() => { | ||
if (links.some(link => "imagesizes" in link || "imagesrcset" in link)) { | ||
logDeprecationOnce(linksWarning); | ||
let matches = getActiveMatches(routerMatches, errors, isSpaMode); | ||
let keyedLinks = React.useMemo(() => getKeyedLinksForMatches(matches, routeModules, manifest), [matches, routeModules, manifest]); | ||
return /*#__PURE__*/React.createElement(React.Fragment, null, criticalCss ? /*#__PURE__*/React.createElement("style", { | ||
dangerouslySetInnerHTML: { | ||
__html: criticalCss | ||
} | ||
}, [links]); | ||
return /*#__PURE__*/React.createElement(React.Fragment, null, links.map(link => { | ||
if (isPageLinkDescriptor(link)) { | ||
return /*#__PURE__*/React.createElement(PrefetchPageLinks, _extends({ | ||
key: link.page | ||
}, link)); | ||
} | ||
let imageSrcSet = null; | ||
// In React 17, <link imageSrcSet> and <link imageSizes> will warn | ||
// because the DOM attributes aren't recognized, so users need to pass | ||
// them in all lowercase to forward the attributes to the node without a | ||
// warning. Normalize so that either property can be used in Remix. | ||
if ("useId" in React) { | ||
if (link.imagesrcset) { | ||
link.imageSrcSet = imageSrcSet = link.imagesrcset; | ||
delete link.imagesrcset; | ||
} | ||
if (link.imagesizes) { | ||
link.imageSizes = link.imagesizes; | ||
delete link.imagesizes; | ||
} | ||
} else { | ||
if (link.imageSrcSet) { | ||
link.imagesrcset = imageSrcSet = link.imageSrcSet; | ||
delete link.imageSrcSet; | ||
} | ||
if (link.imageSizes) { | ||
link.imagesizes = link.imageSizes; | ||
delete link.imageSizes; | ||
} | ||
} | ||
return /*#__PURE__*/React.createElement("link", _extends({ | ||
key: link.rel + (link.href || "") + (imageSrcSet || "") | ||
}, link)); | ||
})); | ||
}) : null, keyedLinks.map(({ | ||
key, | ||
link | ||
}) => isPageLinkDescriptor(link) ? /*#__PURE__*/React.createElement(PrefetchPageLinks, _extends({ | ||
key: key | ||
}, link)) : /*#__PURE__*/React.createElement("link", _extends({ | ||
key: key | ||
}, link)))); | ||
} | ||
/** | ||
* This component renders all of the `<link rel="prefetch">` and | ||
* This component renders all the `<link rel="prefetch">` and | ||
* `<link rel="modulepreload"/>` tags for all the assets (data, modules, css) of | ||
@@ -337,3 +274,3 @@ * a given page. | ||
} = useDataRouterContext(); | ||
let matches = React.useMemo(() => matchRoutes(router.routes, page), [router.routes, page]); | ||
let matches = React.useMemo(() => matchRoutes(router.routes, page, router.basename), [router.routes, page, router.basename]); | ||
if (!matches) { | ||
@@ -348,3 +285,3 @@ console.warn(`Tried to prefetch ${page} but no routes matched.`); | ||
} | ||
function usePrefetchedStylesheets(matches) { | ||
function useKeyedPrefetchLinks(matches) { | ||
let { | ||
@@ -354,7 +291,9 @@ manifest, | ||
} = useRemixContext(); | ||
let [styleLinks, setStyleLinks] = React.useState([]); | ||
let [keyedPrefetchLinks, setKeyedPrefetchLinks] = React.useState([]); | ||
React.useEffect(() => { | ||
let interrupted = false; | ||
getStylesheetPrefetchLinks(matches, manifest, routeModules).then(links => { | ||
if (!interrupted) setStyleLinks(links); | ||
void getKeyedPrefetchLinks(matches, manifest, routeModules).then(links => { | ||
if (!interrupted) { | ||
setKeyedPrefetchLinks(links); | ||
} | ||
}); | ||
@@ -365,3 +304,3 @@ return () => { | ||
}, [matches, manifest, routeModules]); | ||
return styleLinks; | ||
return keyedPrefetchLinks; | ||
} | ||
@@ -375,3 +314,5 @@ function PrefetchPageLinksImpl({ | ||
let { | ||
manifest | ||
future, | ||
manifest, | ||
routeModules | ||
} = useRemixContext(); | ||
@@ -388,13 +329,32 @@ let { | ||
// just the manifest like the other links in here. | ||
let styleLinks = usePrefetchedStylesheets(newMatchesForAssets); | ||
return /*#__PURE__*/React.createElement(React.Fragment, null, dataHrefs.map(href => /*#__PURE__*/React.createElement("link", _extends({ | ||
let keyedPrefetchLinks = useKeyedPrefetchLinks(newMatchesForAssets); | ||
let linksToRender = null; | ||
if (!future.unstable_singleFetch) { | ||
// Non-single-fetch prefetching | ||
linksToRender = dataHrefs.map(href => /*#__PURE__*/React.createElement("link", _extends({ | ||
key: href, | ||
rel: "prefetch", | ||
as: "fetch", | ||
href: href | ||
}, linkProps))); | ||
} else if (newMatchesForData.length > 0) { | ||
// Single-fetch with routes that require data | ||
let url = addRevalidationParam(manifest, routeModules, nextMatches.map(m => m.route), newMatchesForData.map(m => m.route), singleFetchUrl(page)); | ||
if (url.searchParams.get("_routes") !== "") { | ||
linksToRender = /*#__PURE__*/React.createElement("link", _extends({ | ||
key: url.pathname + url.search, | ||
rel: "prefetch", | ||
as: "fetch", | ||
href: url.pathname + url.search | ||
}, linkProps)); | ||
} | ||
} else ; | ||
return /*#__PURE__*/React.createElement(React.Fragment, null, linksToRender, moduleHrefs.map(href => /*#__PURE__*/React.createElement("link", _extends({ | ||
key: href, | ||
rel: "prefetch", | ||
as: "fetch", | ||
href: href | ||
}, linkProps))), moduleHrefs.map(href => /*#__PURE__*/React.createElement("link", _extends({ | ||
key: href, | ||
rel: "modulepreload", | ||
href: href | ||
}, linkProps))), styleLinks.map(link => | ||
}, linkProps))), keyedPrefetchLinks.map(({ | ||
key, | ||
link | ||
}) => | ||
/*#__PURE__*/ | ||
@@ -404,3 +364,3 @@ // these don't spread `linkProps` because they are full link descriptors | ||
React.createElement("link", _extends({ | ||
key: link.href | ||
key: key | ||
}, link)))); | ||
@@ -410,8 +370,9 @@ } | ||
/** | ||
* Renders the `<title>` and `<meta>` tags for the current routes. | ||
* Renders HTML tags related to metadata for the current route. | ||
* | ||
* @see https://remix.run/components/meta | ||
*/ | ||
function V1Meta() { | ||
function Meta() { | ||
let { | ||
isSpaMode, | ||
routeModules | ||
@@ -425,92 +386,7 @@ } = useRemixContext(); | ||
let location = useLocation(); | ||
let matches = errors ? routerMatches.slice(0, routerMatches.findIndex(m => errors[m.route.id]) + 1) : routerMatches; | ||
let meta = {}; | ||
let parentsData = {}; | ||
for (let match of matches) { | ||
let routeId = match.route.id; | ||
let data = loaderData[routeId]; | ||
let params = match.params; | ||
let routeModule = routeModules[routeId]; | ||
if (routeModule.meta) { | ||
let routeMeta = typeof routeModule.meta === "function" ? routeModule.meta({ | ||
data, | ||
parentsData, | ||
params, | ||
location | ||
}) : routeModule.meta; | ||
if (routeMeta && Array.isArray(routeMeta)) { | ||
throw new Error("The route at " + match.route.path + " returns an array. This is only supported with the `v2_meta` future flag " + "in the Remix config. Either set the flag to `true` or update the route's " + "meta function to return an object." + "\n\nTo reference the v1 meta function API, see https://remix.run/route/meta" | ||
// TODO: Add link to the docs once they are written | ||
// + "\n\nTo reference future flags and the v2 meta API, see https://remix.run/file-conventions/remix-config#future-v2-meta." | ||
); | ||
} | ||
Object.assign(meta, routeMeta); | ||
} | ||
parentsData[routeId] = data; | ||
let _matches = getActiveMatches(routerMatches, errors, isSpaMode); | ||
let error = null; | ||
if (errors) { | ||
error = errors[_matches[_matches.length - 1].route.id]; | ||
} | ||
return /*#__PURE__*/React.createElement(React.Fragment, null, Object.entries(meta).map(([name, value]) => { | ||
if (!value) { | ||
return null; | ||
} | ||
if (["charset", "charSet"].includes(name)) { | ||
return /*#__PURE__*/React.createElement("meta", { | ||
key: "charSet", | ||
charSet: value | ||
}); | ||
} | ||
if (name === "title") { | ||
return /*#__PURE__*/React.createElement("title", { | ||
key: "title" | ||
}, String(value)); | ||
} | ||
// Open Graph tags use the `property` attribute, while other meta tags | ||
// use `name`. See https://ogp.me/ | ||
// | ||
// Namespaced attributes: | ||
// - https://ogp.me/#type_music | ||
// - https://ogp.me/#type_video | ||
// - https://ogp.me/#type_article | ||
// - https://ogp.me/#type_book | ||
// - https://ogp.me/#type_profile | ||
// | ||
// Facebook specific tags begin with `fb:` and also use the `property` | ||
// attribute. | ||
// | ||
// Twitter specific tags begin with `twitter:` but they use `name`, so | ||
// they are excluded. | ||
let isOpenGraphTag = /^(og|music|video|article|book|profile|fb):.+$/.test(name); | ||
return [value].flat().map(content => { | ||
if (isOpenGraphTag) { | ||
return /*#__PURE__*/React.createElement("meta", { | ||
property: name, | ||
content: content, | ||
key: name + content | ||
}); | ||
} | ||
if (typeof content === "string") { | ||
return /*#__PURE__*/React.createElement("meta", { | ||
name: name, | ||
content: content, | ||
key: name + content | ||
}); | ||
} | ||
return /*#__PURE__*/React.createElement("meta", _extends({ | ||
key: name + JSON.stringify(content) | ||
}, content)); | ||
}); | ||
})); | ||
} | ||
function V2Meta() { | ||
let { | ||
routeModules | ||
} = useRemixContext(); | ||
let { | ||
errors, | ||
matches: routerMatches, | ||
loaderData | ||
} = useDataRouterStateContext(); | ||
let location = useLocation(); | ||
let _matches = errors ? routerMatches.slice(0, routerMatches.findIndex(m => errors[m.route.id]) + 1) : routerMatches; | ||
let meta = []; | ||
@@ -533,10 +409,3 @@ let leafMeta = null; | ||
handle: _match.route.handle, | ||
// TODO: Remove in v2. Only leaving it for now because we used it in | ||
// examples and there's no reason to crash someone's build for one line. | ||
// They'll get a TS error from the type updates anyway. | ||
// @ts-expect-error | ||
get route() { | ||
console.warn("The meta function in " + _match.route.path + " accesses the `route` property on `matches`. This is deprecated and will be removed in Remix version 2. See"); | ||
return _match.route; | ||
} | ||
error | ||
}; | ||
@@ -549,7 +418,8 @@ matches[i] = match; | ||
location, | ||
matches | ||
matches, | ||
error | ||
}) : Array.isArray(routeModule.meta) ? [...routeModule.meta] : routeModule.meta; | ||
} else if (leafMeta) { | ||
// We only assign the route's meta to the nearest leaf if there is no meta | ||
// export in the route. The meta function may return a falsey value which | ||
// export in the route. The meta function may return a falsy value which | ||
// is effectively the same as an empty array. | ||
@@ -560,6 +430,3 @@ routeMeta = [...leafMeta]; | ||
if (!Array.isArray(routeMeta)) { | ||
throw new Error("The `v2_meta` API is enabled in the Remix config, but the route at " + _match.route.path + " returns an invalid value. In v2, all route meta functions must " + "return an array of meta objects." + | ||
// TODO: Add link to the docs once they are written | ||
// "\n\nTo reference future flags and the v2 meta API, see https://remix.run/file-conventions/remix-config#future-v2-meta." + | ||
"\n\nTo reference the v1 meta function API, see https://remix.run/route/meta"); | ||
throw new Error("The route at " + _match.route.path + " returns an invalid value. All route meta functions must " + "return an array of meta objects." + "\n\nTo reference the meta function API, see https://remix.run/route/meta"); | ||
} | ||
@@ -576,4 +443,6 @@ match.meta = routeMeta; | ||
if ("tagName" in metaProps) { | ||
let tagName = metaProps.tagName; | ||
delete metaProps.tagName; | ||
let { | ||
tagName, | ||
...rest | ||
} = metaProps; | ||
if (!isValidMetaTag(tagName)) { | ||
@@ -585,4 +454,4 @@ console.warn(`A meta object uses an invalid tagName: ${tagName}. Expected either 'link' or 'meta'`); | ||
return /*#__PURE__*/React.createElement(Comp, _extends({ | ||
key: JSON.stringify(metaProps) | ||
}, metaProps)); | ||
key: JSON.stringify(rest) | ||
}, rest)); | ||
} | ||
@@ -595,3 +464,3 @@ if ("title" in metaProps) { | ||
if ("charset" in metaProps) { | ||
metaProps.charSet ?? (metaProps.charSet = metaProps.charset); | ||
metaProps.charSet ??= metaProps.charset; | ||
delete metaProps.charset; | ||
@@ -606,13 +475,14 @@ } | ||
if ("script:ld+json" in metaProps) { | ||
let json = null; | ||
try { | ||
json = JSON.stringify(metaProps["script:ld+json"]); | ||
} catch (err) {} | ||
return json != null && /*#__PURE__*/React.createElement("script", { | ||
key: "script:ld+json", | ||
type: "application/ld+json", | ||
dangerouslySetInnerHTML: { | ||
__html: JSON.stringify(metaProps["script:ld+json"]) | ||
} | ||
}); | ||
let json = JSON.stringify(metaProps["script:ld+json"]); | ||
return /*#__PURE__*/React.createElement("script", { | ||
key: `script:ld+json:${json}`, | ||
type: "application/ld+json", | ||
dangerouslySetInnerHTML: { | ||
__html: json | ||
} | ||
}); | ||
} catch (err) { | ||
return null; | ||
} | ||
} | ||
@@ -627,8 +497,2 @@ return /*#__PURE__*/React.createElement("meta", _extends({ | ||
} | ||
function Meta() { | ||
let { | ||
future | ||
} = useRemixContext(); | ||
return future !== null && future !== void 0 && future.v2_meta ? /*#__PURE__*/React.createElement(V2Meta, null) : /*#__PURE__*/React.createElement(V1Meta, null); | ||
} | ||
function Await(props) { | ||
@@ -657,3 +521,7 @@ return /*#__PURE__*/React.createElement(Await$1, props); | ||
serverHandoffString, | ||
abortDelay | ||
abortDelay, | ||
serializeError, | ||
isSpaMode, | ||
future, | ||
renderMeta | ||
} = useRemixContext(); | ||
@@ -666,13 +534,62 @@ let { | ||
let { | ||
matches | ||
matches: routerMatches | ||
} = useDataRouterStateContext(); | ||
let navigation = useNavigation(); | ||
let enableFogOfWar = isFogOfWarEnabled(future, isSpaMode); | ||
// Let <RemixServer> know that we hydrated and we should render the single | ||
// fetch streaming scripts | ||
if (renderMeta) { | ||
renderMeta.didRenderScripts = true; | ||
} | ||
let matches = getActiveMatches(routerMatches, null, isSpaMode); | ||
React.useEffect(() => { | ||
isHydrated = true; | ||
}, []); | ||
let serializePreResolvedErrorImp = (key, error) => { | ||
let toSerialize; | ||
if (serializeError && error instanceof Error) { | ||
toSerialize = serializeError(error); | ||
} else { | ||
toSerialize = error; | ||
} | ||
return `${JSON.stringify(key)}:__remixContext.p(!1, ${escapeHtml(JSON.stringify(toSerialize))})`; | ||
}; | ||
let serializePreresolvedDataImp = (routeId, key, data) => { | ||
let serializedData; | ||
try { | ||
serializedData = JSON.stringify(data); | ||
} catch (error) { | ||
return serializePreResolvedErrorImp(key, error); | ||
} | ||
return `${JSON.stringify(key)}:__remixContext.p(${escapeHtml(serializedData)})`; | ||
}; | ||
let serializeErrorImp = (routeId, key, error) => { | ||
let toSerialize; | ||
if (serializeError && error instanceof Error) { | ||
toSerialize = serializeError(error); | ||
} else { | ||
toSerialize = error; | ||
} | ||
return `__remixContext.r(${JSON.stringify(routeId)}, ${JSON.stringify(key)}, !1, ${escapeHtml(JSON.stringify(toSerialize))})`; | ||
}; | ||
let serializeDataImp = (routeId, key, data) => { | ||
let serializedData; | ||
try { | ||
serializedData = JSON.stringify(data); | ||
} catch (error) { | ||
return serializeErrorImp(routeId, key, error); | ||
} | ||
return `__remixContext.r(${JSON.stringify(routeId)}, ${JSON.stringify(key)}, ${escapeHtml(serializedData)})`; | ||
}; | ||
let deferredScripts = []; | ||
let initialScripts = React.useMemo(() => { | ||
var _manifest$hmr; | ||
let contextScript = staticContext ? `window.__remixContext = ${serverHandoffString};` : " "; | ||
let activeDeferreds = staticContext === null || staticContext === void 0 ? void 0 : staticContext.activeDeferreds; | ||
let streamScript = future.unstable_singleFetch ? | ||
// prettier-ignore | ||
"window.__remixContext.stream = new ReadableStream({" + "start(controller){" + "window.__remixContext.streamController = controller;" + "}" + "}).pipeThrough(new TextEncoderStream());" : ""; | ||
let contextScript = staticContext ? `window.__remixContext = ${serverHandoffString};${streamScript}` : " "; | ||
// When single fetch is enabled, deferred is handled by turbo-stream | ||
let activeDeferreds = future.unstable_singleFetch ? undefined : staticContext === null || staticContext === void 0 ? void 0 : staticContext.activeDeferreds; | ||
// This sets up the __remixContext with utility functions used by the | ||
@@ -686,3 +603,3 @@ // deferred scripts. | ||
// the promise created by __remixContext.n. | ||
// - __remixContext.t is a a map or routeId to keys to an object containing `e` and `r` methods | ||
// - __remixContext.t is a map or routeId to keys to an object containing `e` and `r` methods | ||
// to resolve or reject the promise created by __remixContext.n. | ||
@@ -700,3 +617,5 @@ // - __remixContext.a is the active number of deferred scripts that should be rendered to match | ||
dataKey: key, | ||
scriptProps: props | ||
scriptProps: props, | ||
serializeData: serializeDataImp, | ||
serializeError: serializeErrorImp | ||
})); | ||
@@ -707,15 +626,5 @@ return `${JSON.stringify(key)}:__remixContext.n(${JSON.stringify(routeId)}, ${JSON.stringify(key)})`; | ||
if (typeof trackedPromise._error !== "undefined") { | ||
let toSerialize = process.env.NODE_ENV === "development" ? { | ||
message: trackedPromise._error.message, | ||
stack: trackedPromise._error.stack | ||
} : { | ||
message: "Unexpected Server Error", | ||
stack: undefined | ||
}; | ||
return `${JSON.stringify(key)}:__remixContext.p(!1, ${escapeHtml(JSON.stringify(toSerialize))})`; | ||
return serializePreResolvedErrorImp(key, trackedPromise._error); | ||
} else { | ||
if (typeof trackedPromise._data === "undefined") { | ||
throw new Error(`The deferred data for ${key} was not resolved, did you forget to return data from a deferred promise?`); | ||
} | ||
return `${JSON.stringify(key)}:__remixContext.p(${escapeHtml(JSON.stringify(trackedPromise._data))})`; | ||
return serializePreresolvedDataImp(routeId, key, trackedPromise._data); | ||
} | ||
@@ -726,4 +635,7 @@ } | ||
}).join("\n") + (deferredScripts.length > 0 ? `__remixContext.a=${deferredScripts.length};` : ""); | ||
let routeModulesScript = !isStatic ? " " : `${(_manifest$hmr = manifest.hmr) !== null && _manifest$hmr !== void 0 && _manifest$hmr.runtime ? `import ${JSON.stringify(manifest.hmr.runtime)};` : ""}import ${JSON.stringify(manifest.url)}; | ||
let routeModulesScript = !isStatic ? " " : `${(_manifest$hmr = manifest.hmr) !== null && _manifest$hmr !== void 0 && _manifest$hmr.runtime ? `import ${JSON.stringify(manifest.hmr.runtime)};` : ""}${enableFogOfWar ? "" : `import ${JSON.stringify(manifest.url)}`}; | ||
${matches.map((match, index) => `import * as route${index} from ${JSON.stringify(manifest.routes[match.route.id].module)};`).join("\n")} | ||
${enableFogOfWar ? | ||
// Inline a minimal manifest with the SSR matches | ||
`window.__remixManifest = ${JSON.stringify(getPartialManifest(manifest, router), null, 2)};` : ""} | ||
window.__remixRouteModules = {${matches.map((match, index) => `${JSON.stringify(match.route.id)}:route${index}`).join(",")}}; | ||
@@ -751,18 +663,9 @@ | ||
key: i, | ||
scriptProps: props | ||
scriptProps: props, | ||
serializeData: serializeDataImp, | ||
serializeError: serializeErrorImp | ||
})); | ||
} | ||
} | ||
// avoid waterfall when importing the next route module | ||
let nextMatches = React.useMemo(() => { | ||
if (navigation.location) { | ||
// FIXME: can probably use transitionManager `nextMatches` | ||
let matches = matchRoutes(router.routes, navigation.location); | ||
invariant(matches, `No routes match path "${navigation.location.pathname}"`); | ||
return matches; | ||
} | ||
return []; | ||
}, [navigation.location, router.routes]); | ||
let routePreloads = matches.concat(nextMatches).map(match => { | ||
let routePreloads = matches.map(match => { | ||
let route = manifest.routes[match.route.id]; | ||
@@ -772,4 +675,8 @@ return (route.imports || []).concat([route.module]); | ||
let preloads = isHydrated ? [] : manifest.entry.imports.concat(routePreloads); | ||
return isHydrated ? null : /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("link", { | ||
return isHydrated ? null : /*#__PURE__*/React.createElement(React.Fragment, null, !enableFogOfWar ? /*#__PURE__*/React.createElement("link", { | ||
rel: "modulepreload", | ||
href: manifest.url, | ||
crossOrigin: props.crossOrigin | ||
}) : null, /*#__PURE__*/React.createElement("link", { | ||
rel: "modulepreload", | ||
href: manifest.entry.module, | ||
@@ -788,3 +695,5 @@ crossOrigin: props.crossOrigin | ||
routeId, | ||
scriptProps | ||
scriptProps, | ||
serializeData, | ||
serializeError | ||
}) { | ||
@@ -811,11 +720,14 @@ if (typeof document === "undefined" && deferredData && dataKey && routeId) { | ||
routeId: routeId, | ||
scriptProps: scriptProps | ||
scriptProps: scriptProps, | ||
serializeError: serializeError | ||
}), | ||
children: data => /*#__PURE__*/React.createElement("script", _extends({}, scriptProps, { | ||
async: true, | ||
suppressHydrationWarning: true, | ||
dangerouslySetInnerHTML: { | ||
__html: `__remixContext.r(${JSON.stringify(routeId)}, ${JSON.stringify(dataKey)}, ${escapeHtml(JSON.stringify(data))});` | ||
} | ||
})) | ||
children: data => { | ||
return /*#__PURE__*/React.createElement("script", _extends({}, scriptProps, { | ||
async: true, | ||
suppressHydrationWarning: true, | ||
dangerouslySetInnerHTML: { | ||
__html: serializeData(routeId, dataKey, data) | ||
} | ||
})); | ||
} | ||
}) : /*#__PURE__*/React.createElement("script", _extends({}, scriptProps, { | ||
@@ -832,16 +744,10 @@ async: true, | ||
routeId, | ||
scriptProps | ||
scriptProps, | ||
serializeError | ||
}) { | ||
let error = useAsyncError(); | ||
let toSerialize = process.env.NODE_ENV === "development" ? { | ||
message: error.message, | ||
stack: error.stack | ||
} : { | ||
message: "Unexpected Server Error", | ||
stack: undefined | ||
}; | ||
return /*#__PURE__*/React.createElement("script", _extends({}, scriptProps, { | ||
suppressHydrationWarning: true, | ||
dangerouslySetInnerHTML: { | ||
__html: `__remixContext.r(${JSON.stringify(routeId)}, ${JSON.stringify(dataKey)}, !1, ${escapeHtml(JSON.stringify(toSerialize))});` | ||
__html: serializeError(routeId, dataKey, error) | ||
} | ||
@@ -853,22 +759,10 @@ })); | ||
} | ||
// TODO: Can this be re-exported from RR? | ||
/** | ||
* Returns the active route matches, useful for accessing loaderData for | ||
* parent/child routes or the route "handle" property | ||
* | ||
* @see https://remix.run/hooks/use-matches | ||
*/ | ||
function useMatches() { | ||
let { | ||
routeModules | ||
} = useRemixContext(); | ||
let matches = useMatches$1(); | ||
return React.useMemo(() => matches.map(match => { | ||
let remixMatch = { | ||
id: match.id, | ||
pathname: match.pathname, | ||
params: match.params, | ||
data: match.data, | ||
// Need to grab handle here since we don't have it at client-side route | ||
// creation time | ||
handle: routeModules[match.id].handle | ||
}; | ||
return remixMatch; | ||
}), [matches, routeModules]); | ||
return useMatches$1(); | ||
} | ||
@@ -886,195 +780,20 @@ | ||
/** | ||
* Returns the JSON parsed data from the current route's `action`. | ||
* Returns the loaderData for the given routeId. | ||
* | ||
* @see https://remix.run/hooks/use-action-data | ||
* @see https://remix.run/hooks/use-route-loader-data | ||
*/ | ||
function useActionData() { | ||
return useActionData$1(); | ||
function useRouteLoaderData(routeId) { | ||
return useRouteLoaderData$1(routeId); | ||
} | ||
/** | ||
* Returns everything you need to know about a page transition to build pending | ||
* navigation indicators and optimistic UI on data mutations. | ||
* Returns the JSON parsed data from the current route's `action`. | ||
* | ||
* @deprecated in favor of useNavigation | ||
* | ||
* @see https://remix.run/hooks/use-transition | ||
* @see https://remix.run/hooks/use-action-data | ||
*/ | ||
function useTransition() { | ||
let navigation = useNavigation(); | ||
React.useEffect(() => { | ||
logDeprecationOnce(useTransitionWarning); | ||
}, []); | ||
return React.useMemo(() => convertNavigationToTransition(navigation), [navigation]); | ||
function useActionData() { | ||
return useActionData$1(); | ||
} | ||
function convertNavigationToTransition(navigation) { | ||
let { | ||
location, | ||
state, | ||
formMethod, | ||
formAction, | ||
formEncType, | ||
formData | ||
} = navigation; | ||
if (!location) { | ||
return IDLE_TRANSITION; | ||
} | ||
let isActionSubmission = formMethod != null && ["POST", "PUT", "PATCH", "DELETE"].includes(formMethod.toUpperCase()); | ||
if (state === "submitting" && formMethod && formAction && formEncType && formData) { | ||
if (isActionSubmission) { | ||
// Actively submitting to an action | ||
let transition = { | ||
location, | ||
state, | ||
submission: { | ||
method: formMethod.toUpperCase(), | ||
action: formAction, | ||
encType: formEncType, | ||
formData: formData, | ||
key: "" | ||
}, | ||
type: "actionSubmission" | ||
}; | ||
return transition; | ||
} else { | ||
// @remix-run/router doesn't mark loader submissions as state: "submitting" | ||
invariant(false, "Encountered an unexpected navigation scenario in useTransition()"); | ||
} | ||
} | ||
if (state === "loading") { | ||
let { | ||
_isRedirect, | ||
_isFetchActionRedirect | ||
} = location.state || {}; | ||
if (formMethod && formAction && formEncType && formData) { | ||
if (!_isRedirect) { | ||
if (isActionSubmission) { | ||
// We're reloading the same location after an action submission | ||
let transition = { | ||
location, | ||
state, | ||
submission: { | ||
method: formMethod.toUpperCase(), | ||
action: formAction, | ||
encType: formEncType, | ||
formData: formData, | ||
key: "" | ||
}, | ||
type: "actionReload" | ||
}; | ||
return transition; | ||
} else { | ||
// The new router fixes a bug in useTransition where the submission | ||
// "action" represents the request URL not the state of the <form> in | ||
// the DOM. Back-port it here to maintain behavior, but useNavigation | ||
// will fix this bug. | ||
let url = new URL(formAction, window.location.origin); | ||
// This typing override should be safe since this is only running for | ||
// GET submissions and over in @remix-run/router we have an invariant | ||
// if you have any non-string values in your FormData when we attempt | ||
// to convert them to URLSearchParams | ||
url.search = new URLSearchParams(formData.entries()).toString(); | ||
// Actively "submitting" to a loader | ||
let transition = { | ||
location, | ||
state: "submitting", | ||
submission: { | ||
method: formMethod.toUpperCase(), | ||
action: url.pathname + url.search, | ||
encType: formEncType, | ||
formData: formData, | ||
key: "" | ||
}, | ||
type: "loaderSubmission" | ||
}; | ||
return transition; | ||
} | ||
} else { | ||
// Redirecting after a submission | ||
if (isActionSubmission) { | ||
let transition = { | ||
location, | ||
state, | ||
submission: { | ||
method: formMethod.toUpperCase(), | ||
action: formAction, | ||
encType: formEncType, | ||
formData: formData, | ||
key: "" | ||
}, | ||
type: "actionRedirect" | ||
}; | ||
return transition; | ||
} else { | ||
let transition = { | ||
location, | ||
state, | ||
submission: { | ||
method: formMethod.toUpperCase(), | ||
action: formAction, | ||
encType: formEncType, | ||
formData: formData, | ||
key: "" | ||
}, | ||
type: "loaderSubmissionRedirect" | ||
}; | ||
return transition; | ||
} | ||
} | ||
} else if (_isRedirect) { | ||
if (_isFetchActionRedirect) { | ||
let transition = { | ||
location, | ||
state, | ||
submission: undefined, | ||
type: "fetchActionRedirect" | ||
}; | ||
return transition; | ||
} else { | ||
let transition = { | ||
location, | ||
state, | ||
submission: undefined, | ||
type: "normalRedirect" | ||
}; | ||
return transition; | ||
} | ||
} | ||
} | ||
// If no scenarios above match, then it's a normal load! | ||
let transition = { | ||
location, | ||
state: "loading", | ||
submission: undefined, | ||
type: "normalLoad" | ||
}; | ||
return transition; | ||
} | ||
/** | ||
* Provides all fetchers currently on the page. Useful for layouts and parent | ||
* routes that need to provide pending/optimistic UI regarding the fetch. | ||
* | ||
* @see https://remix.run/api/remix#usefetchers | ||
*/ | ||
function useFetchers() { | ||
let fetchers = useFetchers$1(); | ||
return fetchers.map(f => { | ||
let fetcher = convertRouterFetcherToRemixFetcher({ | ||
state: f.state, | ||
data: f.data, | ||
formMethod: f.formMethod, | ||
formAction: f.formAction, | ||
formData: f.formData, | ||
formEncType: f.formEncType, | ||
" _hasFetcherDoneAnything ": f[" _hasFetcherDoneAnything "] | ||
}); | ||
addFetcherDeprecationWarnings(fetcher); | ||
return fetcher; | ||
}); | ||
} | ||
/** | ||
* Interacts with route loaders and actions without causing a navigation. Great | ||
@@ -1085,205 +804,18 @@ * for any interaction that stays on the same page. | ||
*/ | ||
function useFetcher() { | ||
let fetcherRR = useFetcher$1(); | ||
return React.useMemo(() => { | ||
let remixFetcher = convertRouterFetcherToRemixFetcher({ | ||
state: fetcherRR.state, | ||
data: fetcherRR.data, | ||
formMethod: fetcherRR.formMethod, | ||
formAction: fetcherRR.formAction, | ||
formData: fetcherRR.formData, | ||
formEncType: fetcherRR.formEncType, | ||
" _hasFetcherDoneAnything ": fetcherRR[" _hasFetcherDoneAnything "] | ||
}); | ||
let fetcherWithComponents = { | ||
...remixFetcher, | ||
load: fetcherRR.load, | ||
submit: fetcherRR.submit, | ||
Form: fetcherRR.Form | ||
}; | ||
addFetcherDeprecationWarnings(fetcherWithComponents); | ||
return fetcherWithComponents; | ||
}, [fetcherRR]); | ||
function useFetcher(opts = {}) { | ||
return useFetcher$1(opts); | ||
} | ||
function addFetcherDeprecationWarnings(fetcher) { | ||
let type = fetcher.type; | ||
Object.defineProperty(fetcher, "type", { | ||
get() { | ||
logDeprecationOnce(fetcherTypeWarning); | ||
return type; | ||
}, | ||
set(value) { | ||
// Devs should *not* be doing this but we don't want to break their | ||
// current app if they are | ||
type = value; | ||
}, | ||
// These settings should make this behave like a normal object `type` field | ||
configurable: true, | ||
enumerable: true | ||
}); | ||
let submission = fetcher.submission; | ||
Object.defineProperty(fetcher, "submission", { | ||
get() { | ||
logDeprecationOnce(fetcherSubmissionWarning); | ||
return submission; | ||
}, | ||
set(value) { | ||
// Devs should *not* be doing this but we don't want to break their | ||
// current app if they are | ||
submission = value; | ||
}, | ||
// These settings should make this behave like a normal object `type` field | ||
configurable: true, | ||
enumerable: true | ||
}); | ||
} | ||
function convertRouterFetcherToRemixFetcher(fetcherRR) { | ||
let { | ||
state, | ||
formMethod, | ||
formAction, | ||
formEncType, | ||
formData, | ||
data | ||
} = fetcherRR; | ||
let isActionSubmission = formMethod != null && ["POST", "PUT", "PATCH", "DELETE"].includes(formMethod.toUpperCase()); | ||
if (state === "idle") { | ||
if (fetcherRR[" _hasFetcherDoneAnything "] === true) { | ||
let fetcher = { | ||
state: "idle", | ||
type: "done", | ||
formMethod: undefined, | ||
formAction: undefined, | ||
formData: undefined, | ||
formEncType: undefined, | ||
submission: undefined, | ||
data | ||
}; | ||
return fetcher; | ||
} else { | ||
let fetcher = IDLE_FETCHER; | ||
return fetcher; | ||
} | ||
} | ||
if (state === "submitting" && formMethod && formAction && formEncType && formData) { | ||
if (isActionSubmission) { | ||
// Actively submitting to an action | ||
let fetcher = { | ||
state, | ||
type: "actionSubmission", | ||
formMethod: formMethod.toUpperCase(), | ||
formAction: formAction, | ||
formEncType: formEncType, | ||
formData: formData, | ||
submission: { | ||
method: formMethod.toUpperCase(), | ||
action: formAction, | ||
encType: formEncType, | ||
formData: formData, | ||
key: "" | ||
}, | ||
data | ||
}; | ||
return fetcher; | ||
} else { | ||
// @remix-run/router doesn't mark loader submissions as state: "submitting" | ||
invariant(false, "Encountered an unexpected fetcher scenario in useFetcher()"); | ||
} | ||
} | ||
if (state === "loading") { | ||
if (formMethod && formAction && formEncType && formData) { | ||
if (isActionSubmission) { | ||
if (data) { | ||
// In a loading state but we have data - must be an actionReload | ||
let fetcher = { | ||
state, | ||
type: "actionReload", | ||
formMethod: formMethod.toUpperCase(), | ||
formAction: formAction, | ||
formEncType: formEncType, | ||
formData: formData, | ||
submission: { | ||
method: formMethod.toUpperCase(), | ||
action: formAction, | ||
encType: formEncType, | ||
formData: formData, | ||
key: "" | ||
}, | ||
data | ||
}; | ||
return fetcher; | ||
} else { | ||
let fetcher = { | ||
state, | ||
type: "actionRedirect", | ||
formMethod: formMethod.toUpperCase(), | ||
formAction: formAction, | ||
formEncType: formEncType, | ||
formData: formData, | ||
submission: { | ||
method: formMethod.toUpperCase(), | ||
action: formAction, | ||
encType: formEncType, | ||
formData: formData, | ||
key: "" | ||
}, | ||
data: undefined | ||
}; | ||
return fetcher; | ||
} | ||
} else { | ||
// The new router fixes a bug in useTransition where the submission | ||
// "action" represents the request URL not the state of the <form> in | ||
// the DOM. Back-port it here to maintain behavior, but useNavigation | ||
// will fix this bug. | ||
let url = new URL(formAction, window.location.origin); | ||
// This typing override should be safe since this is only running for | ||
// GET submissions and over in @remix-run/router we have an invariant | ||
// if you have any non-string values in your FormData when we attempt | ||
// to convert them to URLSearchParams | ||
url.search = new URLSearchParams(formData.entries()).toString(); | ||
// Actively "submitting" to a loader | ||
let fetcher = { | ||
state: "submitting", | ||
type: "loaderSubmission", | ||
formMethod: formMethod.toUpperCase(), | ||
formAction: formAction, | ||
formEncType: formEncType, | ||
formData: formData, | ||
submission: { | ||
method: formMethod.toUpperCase(), | ||
action: url.pathname + url.search, | ||
encType: formEncType, | ||
formData: formData, | ||
key: "" | ||
}, | ||
data | ||
}; | ||
return fetcher; | ||
} | ||
} | ||
} | ||
// If all else fails, it's a normal load! | ||
let fetcher = { | ||
state: "loading", | ||
type: "normalLoad", | ||
formMethod: undefined, | ||
formAction: undefined, | ||
formData: undefined, | ||
formEncType: undefined, | ||
submission: undefined, | ||
data | ||
}; | ||
return fetcher; | ||
} | ||
/** | ||
* This component connects your app to the Remix asset server and | ||
* automatically reloads the page when files change in development. | ||
* In production, it renders null, so you can safely render it always in your root route. | ||
* | ||
* @see https://remix.run/docs/components/live-reload | ||
*/ | ||
const LiveReload = | ||
// Dead Code Elimination magic for production builds. | ||
// This way devs don't have to worry about doing the NODE_ENV check themselves. | ||
// If running an un-bundled server outside of `remix dev` you will still need | ||
// to set the REMIX_DEV_SERVER_WS_PORT manually. | ||
const LiveReload = process.env.NODE_ENV !== "development" ? () => null : function LiveReload({ | ||
// TODO: remove REMIX_DEV_SERVER_WS_PORT in v2 | ||
process.env.NODE_ENV !== "development" ? () => null : function LiveReload({ | ||
origin, | ||
port, | ||
@@ -1293,2 +825,9 @@ timeoutMs = 1000, | ||
}) { | ||
// @ts-expect-error | ||
let isViteClient = import.meta && import.meta.env !== undefined; | ||
if (isViteClient) { | ||
console.warn(["`<LiveReload />` is obsolete when using Vite and can conflict with Vite's built-in HMR runtime.", "", "Remove `<LiveReload />` from your code and instead only use `<Scripts />`.", "Then refresh the page to remove lingering scripts from `<LiveReload />`."].join("\n")); | ||
return null; | ||
} | ||
origin ??= process.env.REMIX_DEV_ORIGIN; | ||
let js = String.raw; | ||
@@ -1301,7 +840,14 @@ return /*#__PURE__*/React.createElement("script", { | ||
function remixLiveReloadConnect(config) { | ||
let protocol = location.protocol === "https:" ? "wss:" : "ws:"; | ||
let host = location.hostname; | ||
let port = ${port} || (window.__remixContext && window.__remixContext.dev && window.__remixContext.dev.port) || ${Number(process.env.REMIX_DEV_SERVER_WS_PORT || 8002)}; | ||
let socketPath = protocol + "//" + host + ":" + port + "/socket"; | ||
let ws = new WebSocket(socketPath); | ||
let LIVE_RELOAD_ORIGIN = ${JSON.stringify(origin)}; | ||
let protocol = | ||
LIVE_RELOAD_ORIGIN ? new URL(LIVE_RELOAD_ORIGIN).protocol.replace(/^http/, "ws") : | ||
location.protocol === "https:" ? "wss:" : "ws:"; // remove in v2? | ||
let hostname = LIVE_RELOAD_ORIGIN ? new URL(LIVE_RELOAD_ORIGIN).hostname : location.hostname; | ||
let url = new URL(protocol + "//" + hostname + "/socket"); | ||
url.port = | ||
${port} || | ||
(LIVE_RELOAD_ORIGIN ? new URL(LIVE_RELOAD_ORIGIN).port : 8002); | ||
let ws = new WebSocket(url.href); | ||
ws.onmessage = async (message) => { | ||
@@ -1337,3 +883,3 @@ let event = JSON.parse(message.data); | ||
if (accepted) { | ||
console.log("[HMR] Updated accepted by", update.id); | ||
console.log("[HMR] Update accepted by", update.id); | ||
updateAccepted = true; | ||
@@ -1348,3 +894,3 @@ } | ||
if (accepted) { | ||
console.log("[HMR] Updated accepted by", "remix:manifest"); | ||
console.log("[HMR] Update accepted by", "remix:manifest"); | ||
updateAccepted = true; | ||
@@ -1354,3 +900,3 @@ } | ||
if (!updateAccepted) { | ||
console.log("[HMR] Updated rejected, reloading..."); | ||
console.log("[HMR] Update rejected, reloading..."); | ||
window.location.reload(); | ||
@@ -1399,2 +945,2 @@ } | ||
export { Await, Link, Links, LiveReload, Meta, NavLink, PrefetchPageLinks, RemixContext, RemixRoute, RemixRouteError, Scripts, composeEventHandlers, useActionData, useFetcher, useFetchers, useLoaderData, useMatches, useTransition }; | ||
export { Await, Form, Link, Links, LiveReload, Meta, NavLink, PrefetchPageLinks, RemixContext, Scripts, composeEventHandlers, useActionData, useFetcher, useLoaderData, useMatches, useRemixContext, useRouteLoaderData }; |
/** | ||
* @remix-run/react v0.0.0-nightly-ccefed3-20230621 | ||
* @remix-run/react v0.0.0-nightly-cd403b516-20240809 | ||
* | ||
@@ -15,4 +15,2 @@ * Copyright (c) Remix Software Inc. | ||
* Data for a route that was returned from a `loader()`. | ||
* | ||
* Note: This moves to unknown in ReactRouter and eventually likely in Remix | ||
*/ | ||
@@ -26,2 +24,14 @@ | ||
} | ||
function isNetworkErrorResponse(response) { | ||
// If we reach the Remix server, we can safely identify response types via the | ||
// X-Remix-Error/X-Remix-Catch headers. However, if we never reach the Remix | ||
// server, and instead receive a 4xx/5xx from somewhere in between (like | ||
// Cloudflare), then we get a false negative in the isErrorResponse check and | ||
// we incorrectly assume that the user returns the 4xx/5xx response and | ||
// consider it successful. To alleviate this, we add X-Remix-Response to any | ||
// non-Error/non-Catch responses coming back from the server. If we don't | ||
// see this, we can conclude that a 4xx/5xx response never actually reached | ||
// the Remix server and we can bubble it up as an error. | ||
return isResponse(response) && response.status >= 400 && response.headers.get("X-Remix-Error") == null && response.headers.get("X-Remix-Catch") == null && response.headers.get("X-Remix-Response") == null; | ||
} | ||
function isRedirectResponse(response) { | ||
@@ -34,16 +44,12 @@ return response.headers.get("X-Remix-Redirect") != null; | ||
} | ||
function isResponse(value) { | ||
return value != null && typeof value.status === "number" && typeof value.statusText === "string" && typeof value.headers === "object" && typeof value.body !== "undefined"; | ||
} | ||
function isDeferredData(value) { | ||
let deferred = value; | ||
return deferred && typeof deferred === "object" && typeof deferred.data === "object" && typeof deferred.subscribe === "function" && typeof deferred.cancel === "function" && typeof deferred.resolveData === "function"; | ||
} | ||
async function fetchData(request, routeId, retry = 0) { | ||
let url = new URL(request.url); | ||
url.searchParams.set("_data", routeId); | ||
let init = { | ||
signal: request.signal | ||
}; | ||
if (request.method !== "GET") { | ||
init.method = request.method; | ||
let contentType = request.headers.get("Content-Type"); | ||
init.body = | ||
// Check between word boundaries instead of startsWith() due to the last | ||
// paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type | ||
contentType && /\bapplication\/x-www-form-urlencoded\b/.test(contentType) ? new URLSearchParams(await request.text()) : await request.formData(); | ||
} | ||
if (retry > 0) { | ||
@@ -54,2 +60,3 @@ // Retry up to 3 times waiting 50, 250, 1250 ms | ||
} | ||
let init = await createRequestInit(request); | ||
let revalidation = window.__remixRevalidation; | ||
@@ -68,4 +75,38 @@ let response = await fetch(url.href, init).catch(error => { | ||
} | ||
if (isNetworkErrorResponse(response)) { | ||
let text = await response.text(); | ||
let error = new Error(text); | ||
error.stack = undefined; | ||
return error; | ||
} | ||
return response; | ||
} | ||
async function createRequestInit(request) { | ||
let init = { | ||
signal: request.signal | ||
}; | ||
if (request.method !== "GET") { | ||
init.method = request.method; | ||
let contentType = request.headers.get("Content-Type"); | ||
// Check between word boundaries instead of startsWith() due to the last | ||
// paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type | ||
if (contentType && /\bapplication\/json\b/.test(contentType)) { | ||
init.headers = { | ||
"Content-Type": contentType | ||
}; | ||
init.body = JSON.stringify(await request.json()); | ||
} else if (contentType && /\btext\/plain\b/.test(contentType)) { | ||
init.headers = { | ||
"Content-Type": contentType | ||
}; | ||
init.body = await request.text(); | ||
} else if (contentType && /\bapplication\/x-www-form-urlencoded\b/.test(contentType)) { | ||
init.body = new URLSearchParams(await request.text()); | ||
} else { | ||
init.body = await request.formData(); | ||
} | ||
} | ||
return init; | ||
} | ||
const DEFERRED_VALUE_PLACEHOLDER_PREFIX = "__deferred_promise:"; | ||
@@ -110,3 +151,3 @@ async function parseDeferredReadableStream(stream) { | ||
// Read the rest of the stream and resolve deferred promises | ||
(async () => { | ||
void (async () => { | ||
try { | ||
@@ -232,2 +273,2 @@ for await (let section of sectionReader) { | ||
export { fetchData, isCatchResponse, isDeferredResponse, isErrorResponse, isRedirectResponse, parseDeferredReadableStream }; | ||
export { createRequestInit, fetchData, isCatchResponse, isDeferredData, isDeferredResponse, isErrorResponse, isNetworkErrorResponse, isRedirectResponse, isResponse, parseDeferredReadableStream }; |
/** | ||
* @remix-run/react v0.0.0-nightly-ccefed3-20230621 | ||
* @remix-run/react v0.0.0-nightly-cd403b516-20240809 | ||
* | ||
@@ -11,6 +11,7 @@ * Copyright (c) Remix Software Inc. | ||
*/ | ||
import React__default, { useContext } from 'react'; | ||
import { useRouteError, isRouteErrorResponse } from 'react-router-dom'; | ||
import * as React from 'react'; | ||
import { isRouteErrorResponse } from 'react-router-dom'; | ||
import { useRemixContext, Scripts } from './components.js'; | ||
class RemixErrorBoundary extends React__default.Component { | ||
class RemixErrorBoundary extends React.Component { | ||
constructor(props) { | ||
@@ -56,4 +57,5 @@ super(props); | ||
if (this.state.error) { | ||
return /*#__PURE__*/React__default.createElement(this.props.component, { | ||
error: this.state.error | ||
return /*#__PURE__*/React.createElement(RemixRootDefaultErrorBoundary, { | ||
error: this.state.error, | ||
isOutsideRemixApp: true | ||
}); | ||
@@ -70,25 +72,39 @@ } else { | ||
function RemixRootDefaultErrorBoundary({ | ||
error | ||
error, | ||
isOutsideRemixApp | ||
}) { | ||
// Only log client side to avoid double-logging on the server | ||
React__default.useEffect(() => { | ||
console.error(error); | ||
}, [error]); | ||
return /*#__PURE__*/React__default.createElement("html", { | ||
lang: "en" | ||
}, /*#__PURE__*/React__default.createElement("head", null, /*#__PURE__*/React__default.createElement("meta", { | ||
charSet: "utf-8" | ||
}), /*#__PURE__*/React__default.createElement("meta", { | ||
name: "viewport", | ||
content: "width=device-width, initial-scale=1, viewport-fit=cover" | ||
}), /*#__PURE__*/React__default.createElement("title", null, "Application Error!")), /*#__PURE__*/React__default.createElement("body", null, /*#__PURE__*/React__default.createElement("main", { | ||
style: { | ||
fontFamily: "system-ui, sans-serif", | ||
padding: "2rem" | ||
console.error(error); | ||
let heyDeveloper = /*#__PURE__*/React.createElement("script", { | ||
dangerouslySetInnerHTML: { | ||
__html: ` | ||
console.log( | ||
"💿 Hey developer 👋. You can provide a way better UX than this when your app throws errors. Check out https://remix.run/guides/errors for more information." | ||
); | ||
` | ||
} | ||
}, /*#__PURE__*/React__default.createElement("h1", { | ||
}); | ||
if (isRouteErrorResponse(error)) { | ||
return /*#__PURE__*/React.createElement(BoundaryShell, { | ||
title: "Unhandled Thrown Response!" | ||
}, /*#__PURE__*/React.createElement("h1", { | ||
style: { | ||
fontSize: "24px" | ||
} | ||
}, error.status, " ", error.statusText), heyDeveloper); | ||
} | ||
let errorInstance; | ||
if (error instanceof Error) { | ||
errorInstance = error; | ||
} else { | ||
let errorString = error == null ? "Unknown Error" : typeof error === "object" && "toString" in error ? error.toString() : JSON.stringify(error); | ||
errorInstance = new Error(errorString); | ||
} | ||
return /*#__PURE__*/React.createElement(BoundaryShell, { | ||
title: "Application Error!", | ||
isOutsideRemixApp: isOutsideRemixApp | ||
}, /*#__PURE__*/React.createElement("h1", { | ||
style: { | ||
fontSize: "24px" | ||
} | ||
}, "Application Error"), error.stack ? /*#__PURE__*/React__default.createElement("pre", { | ||
}, "Application Error"), /*#__PURE__*/React.createElement("pre", { | ||
style: { | ||
@@ -100,74 +116,42 @@ padding: "2rem", | ||
} | ||
}, error.stack) : null), /*#__PURE__*/React__default.createElement("script", { | ||
dangerouslySetInnerHTML: { | ||
__html: ` | ||
console.log( | ||
"💿 Hey developer👋. You can provide a way better UX than this when your app throws errors. Check out https://remix.run/guides/errors for more information." | ||
); | ||
` | ||
} | ||
}))); | ||
}, errorInstance.stack), heyDeveloper); | ||
} | ||
function V2_RemixRootDefaultErrorBoundary() { | ||
let error = useRouteError(); | ||
if (isRouteErrorResponse(error)) { | ||
return /*#__PURE__*/React__default.createElement(RemixRootDefaultCatchBoundaryImpl, { | ||
caught: error | ||
}); | ||
} else if (error instanceof Error) { | ||
return /*#__PURE__*/React__default.createElement(RemixRootDefaultErrorBoundary, { | ||
error: error | ||
}); | ||
} else { | ||
let errorString = error == null ? "Unknown Error" : typeof error === "object" && "toString" in error ? error.toString() : JSON.stringify(error); | ||
return /*#__PURE__*/React__default.createElement(RemixRootDefaultErrorBoundary, { | ||
error: new Error(errorString) | ||
}); | ||
} | ||
} | ||
let RemixCatchContext = /*#__PURE__*/React__default.createContext(undefined); | ||
/** | ||
* Returns the status code and thrown response data. | ||
* | ||
* @deprecated Please enable the v2_errorBoundary flag | ||
* | ||
* @see https://remix.run/route/catch-boundary | ||
*/ | ||
function useCatch() { | ||
return useContext(RemixCatchContext); | ||
} | ||
function RemixCatchBoundary({ | ||
catch: catchVal, | ||
component: Component, | ||
function BoundaryShell({ | ||
title, | ||
renderScripts, | ||
isOutsideRemixApp, | ||
children | ||
}) { | ||
if (catchVal) { | ||
return /*#__PURE__*/React__default.createElement(RemixCatchContext.Provider, { | ||
value: catchVal | ||
}, /*#__PURE__*/React__default.createElement(Component, null)); | ||
var _routeModules$root; | ||
let { | ||
routeModules | ||
} = useRemixContext(); | ||
// Generally speaking, when the root route has a Layout we want to use that | ||
// as the app shell instead of the default `BoundaryShell` wrapper markup below. | ||
// This is true for `loader`/`action` errors, most render errors, and | ||
// `HydrateFallback` scenarios. | ||
// However, render errors thrown from the `Layout` present a bit of an issue | ||
// because if the `Layout` itself throws during the `ErrorBoundary` pass and | ||
// we bubble outside the `RouterProvider` to the wrapping `RemixErrorBoundary`, | ||
// by returning only `children` here we'll be trying to append a `<div>` to | ||
// the `document` and the DOM will throw, putting React into an error/hydration | ||
// loop. | ||
// Instead, if we're ever rendering from the outermost `RemixErrorBoundary` | ||
// during hydration that wraps `RouterProvider`, then we can't trust the | ||
// `Layout` and should fallback to the default app shell so we're always | ||
// returning an `<html>` document. | ||
if ((_routeModules$root = routeModules.root) !== null && _routeModules$root !== void 0 && _routeModules$root.Layout && !isOutsideRemixApp) { | ||
return children; | ||
} | ||
return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, children); | ||
} | ||
/** | ||
* When app's don't provide a root level CatchBoundary, we default to this. | ||
*/ | ||
function RemixRootDefaultCatchBoundary() { | ||
let caught = useCatch(); | ||
return /*#__PURE__*/React__default.createElement(RemixRootDefaultCatchBoundaryImpl, { | ||
caught: caught | ||
}); | ||
} | ||
function RemixRootDefaultCatchBoundaryImpl({ | ||
caught | ||
}) { | ||
return /*#__PURE__*/React__default.createElement("html", { | ||
return /*#__PURE__*/React.createElement("html", { | ||
lang: "en" | ||
}, /*#__PURE__*/React__default.createElement("head", null, /*#__PURE__*/React__default.createElement("meta", { | ||
}, /*#__PURE__*/React.createElement("head", null, /*#__PURE__*/React.createElement("meta", { | ||
charSet: "utf-8" | ||
}), /*#__PURE__*/React__default.createElement("meta", { | ||
}), /*#__PURE__*/React.createElement("meta", { | ||
name: "viewport", | ||
content: "width=device-width, initial-scale=1, viewport-fit=cover" | ||
}), /*#__PURE__*/React__default.createElement("title", null, "Unhandled Thrown Response!")), /*#__PURE__*/React__default.createElement("body", null, /*#__PURE__*/React__default.createElement("h1", { | ||
content: "width=device-width,initial-scale=1,viewport-fit=cover" | ||
}), /*#__PURE__*/React.createElement("title", null, title)), /*#__PURE__*/React.createElement("body", null, /*#__PURE__*/React.createElement("main", { | ||
style: { | ||
@@ -177,13 +161,5 @@ fontFamily: "system-ui, sans-serif", | ||
} | ||
}, caught.status, " ", caught.statusText), /*#__PURE__*/React__default.createElement("script", { | ||
dangerouslySetInnerHTML: { | ||
__html: ` | ||
console.log( | ||
"💿 Hey developer👋. You can provide a way better UX than this when your app throws 404s (and other responses). Check out https://remix.run/guides/not-found for more information." | ||
); | ||
` | ||
} | ||
}))); | ||
}, children, renderScripts ? /*#__PURE__*/React.createElement(Scripts, null) : null))); | ||
} | ||
export { RemixCatchBoundary, RemixErrorBoundary, RemixRootDefaultCatchBoundary, RemixRootDefaultErrorBoundary, V2_RemixRootDefaultErrorBoundary, useCatch }; | ||
export { BoundaryShell, RemixErrorBoundary, RemixRootDefaultErrorBoundary }; |
/** | ||
* @remix-run/react v0.0.0-nightly-ccefed3-20230621 | ||
* @remix-run/react v0.0.0-nightly-cd403b516-20240809 | ||
* | ||
@@ -11,9 +11,4 @@ * Copyright (c) Remix Software Inc. | ||
*/ | ||
import { ErrorResponse } from '@remix-run/router'; | ||
import { UNSAFE_ErrorResponseImpl } from '@remix-run/router'; | ||
/** | ||
* @deprecated in favor of the `ErrorResponse` class in React Router. Please | ||
* enable the `future.v2_errorBoundary` flag to ease your migration to Remix v2. | ||
*/ | ||
function deserializeErrors(errors) { | ||
@@ -27,7 +22,23 @@ if (!errors) return null; | ||
if (val && val.__type === "RouteErrorResponse") { | ||
serialized[key] = new ErrorResponse(val.status, val.statusText, val.data, val.internal === true); | ||
serialized[key] = new UNSAFE_ErrorResponseImpl(val.status, val.statusText, val.data, val.internal === true); | ||
} else if (val && val.__type === "Error") { | ||
let error = new Error(val.message); | ||
error.stack = val.stack; | ||
serialized[key] = error; | ||
// Attempt to reconstruct the right type of Error (i.e., ReferenceError) | ||
if (val.__subType) { | ||
let ErrorConstructor = window[val.__subType]; | ||
if (typeof ErrorConstructor === "function") { | ||
try { | ||
// @ts-expect-error | ||
let error = new ErrorConstructor(val.message); | ||
error.stack = val.stack; | ||
serialized[key] = error; | ||
} catch (e) { | ||
// no-op - fall through and create a normal Error | ||
} | ||
} | ||
} | ||
if (serialized[key] == null) { | ||
let error = new Error(val.message); | ||
error.stack = val.stack; | ||
serialized[key] = error; | ||
} | ||
} else { | ||
@@ -34,0 +45,0 @@ serialized[key] = val; |
/** | ||
* @remix-run/react v0.0.0-nightly-ccefed3-20230621 | ||
* @remix-run/react v0.0.0-nightly-cd403b516-20240809 | ||
* | ||
@@ -11,7 +11,8 @@ * Copyright (c) Remix Software Inc. | ||
*/ | ||
export { Navigate, NavigationType, Outlet, Route, Routes, createPath, createRoutesFromChildren, createRoutesFromElements, createSearchParams, generatePath, isRouteErrorResponse, matchPath, matchRoutes, parsePath, renderMatches, resolvePath, unstable_usePrompt, unstable_useViewTransitionState, useAsyncError, useAsyncValue, useBeforeUnload, useBlocker, useFetchers, useFormAction, useHref, useInRouterContext, useLinkClickHandler, useLocation, useMatch, useNavigate, useNavigation, useNavigationType, useOutlet, useOutletContext, useParams, useResolvedPath, useRevalidator, useRouteError, useRoutes, useSearchParams, useSubmit } from 'react-router-dom'; | ||
export { defer, json, redirect, redirectDocument, replace, unstable_data } from '@remix-run/server-runtime'; | ||
export { RemixBrowser } from './browser.js'; | ||
export { Form, Outlet, isRouteErrorResponse, unstable_useBlocker, unstable_usePrompt, useAsyncError, useAsyncValue, useBeforeUnload, useFormAction, useHref, useLocation, useMatch, useNavigate, useNavigation, useNavigationType, useOutlet, useOutletContext, useParams, useResolvedPath, useRevalidator, useRouteError, useRouteLoaderData, useSearchParams, useSubmit } from 'react-router-dom'; | ||
export { Await, Link, Links, LiveReload, Meta, NavLink, PrefetchPageLinks, Scripts, RemixContext as UNSAFE_RemixContext, useActionData, useFetcher, useFetchers, useLoaderData, useMatches, useTransition } from './components.js'; | ||
export { useCatch } from './errorBoundaries.js'; | ||
export { Await, Form, Link, Links, LiveReload, Meta, NavLink, PrefetchPageLinks, Scripts, RemixContext as UNSAFE_RemixContext, useActionData, useFetcher, useLoaderData, useMatches, useRouteLoaderData } from './components.js'; | ||
export { ScrollRestoration } from './scroll-restoration.js'; | ||
export { RemixServer } from './server.js'; | ||
export { defineClientAction as unstable_defineClientAction, defineClientLoader as unstable_defineClientLoader } from './single-fetch.js'; |
/** | ||
* @remix-run/react v0.0.0-nightly-ccefed3-20230621 | ||
* @remix-run/react v0.0.0-nightly-cd403b516-20240809 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
/** | ||
* @remix-run/react v0.0.0-nightly-ccefed3-20230621 | ||
* @remix-run/react v0.0.0-nightly-cd403b516-20240809 | ||
* | ||
@@ -21,2 +21,3 @@ * Copyright (c) Remix Software Inc. | ||
//////////////////////////////////////////////////////////////////////////////// | ||
/** | ||
@@ -26,15 +27,23 @@ * Gets all the links for a set of matches. The modules are assumed to have been | ||
*/ | ||
function getLinksForMatches(matches, routeModules, manifest) { | ||
function getKeyedLinksForMatches(matches, routeModules, manifest) { | ||
let descriptors = matches.map(match => { | ||
var _module$links; | ||
let module = routeModules[match.route.id]; | ||
return ((_module$links = module.links) === null || _module$links === void 0 ? void 0 : _module$links.call(module)) || []; | ||
}).flat(1); | ||
let route = manifest.routes[match.route.id]; | ||
return [route.css ? route.css.map(href => ({ | ||
rel: "stylesheet", | ||
href | ||
})) : [], (module === null || module === void 0 ? void 0 : (_module$links = module.links) === null || _module$links === void 0 ? void 0 : _module$links.call(module)) || []]; | ||
}).flat(2); | ||
let preloads = getCurrentPageModulePreloadHrefs(matches, manifest); | ||
return dedupe(descriptors, preloads); | ||
return dedupeLinkDescriptors(descriptors, preloads); | ||
} | ||
async function prefetchStyleLinks(routeModule) { | ||
if (!routeModule.links) return; | ||
let descriptors = routeModule.links(); | ||
if (!descriptors) return; | ||
async function prefetchStyleLinks(route, routeModule) { | ||
var _route$css, _routeModule$links; | ||
if (!route.css && !routeModule.links || !isPreloadSupported()) return; | ||
let descriptors = [((_route$css = route.css) === null || _route$css === void 0 ? void 0 : _route$css.map(href => ({ | ||
rel: "stylesheet", | ||
href | ||
}))) ?? [], ((_routeModule$links = routeModule.links) === null || _routeModule$links === void 0 ? void 0 : _routeModule$links.call(routeModule)) ?? []].flat(1); | ||
if (descriptors.length === 0) return; | ||
let styleLinks = []; | ||
@@ -51,4 +60,5 @@ for (let descriptor of descriptors) { | ||
// don't block for non-matching media queries | ||
let matchingLinks = styleLinks.filter(link => !link.media || window.matchMedia(link.media).matches); | ||
// don't block for non-matching media queries, or for stylesheets that are | ||
// already in the DOM (active route revalidations) | ||
let matchingLinks = styleLinks.filter(link => (!link.media || window.matchMedia(link.media).matches) && !document.querySelector(`link[rel="stylesheet"][href="${link.href}"]`)); | ||
await Promise.all(matchingLinks.map(prefetchStyleLink)); | ||
@@ -85,13 +95,15 @@ } | ||
function isHtmlLinkDescriptor(object) { | ||
if (object == null) return false; | ||
if (object == null) { | ||
return false; | ||
} | ||
// <link> may not have an href if <link rel="preload"> is used with imagesrcset + imagesizes | ||
// <link> may not have an href if <link rel="preload"> is used with imageSrcSet + imageSizes | ||
// https://github.com/remix-run/remix/issues/184 | ||
// https://html.spec.whatwg.org/commit-snapshots/cb4f5ff75de5f4cbd7013c4abad02f21c77d4d1c/#attr-link-imagesrcset | ||
if (object.href == null) { | ||
return object.rel === "preload" && (typeof object.imageSrcSet === "string" || typeof object.imagesrcset === "string") && (typeof object.imageSizes === "string" || typeof object.imagesizes === "string"); | ||
return object.rel === "preload" && typeof object.imageSrcSet === "string" && typeof object.imageSizes === "string"; | ||
} | ||
return typeof object.rel === "string" && typeof object.href === "string"; | ||
} | ||
async function getStylesheetPrefetchLinks(matches, manifest, routeModules) { | ||
async function getKeyedPrefetchLinks(matches, manifest, routeModules) { | ||
let links = await Promise.all(matches.map(async match => { | ||
@@ -101,10 +113,10 @@ let mod = await loadRouteModule(manifest.routes[match.route.id], routeModules); | ||
})); | ||
return links.flat(1).filter(isHtmlLinkDescriptor).filter(link => link.rel === "stylesheet" || link.rel === "preload").map(link => link.rel === "preload" ? { | ||
return dedupeLinkDescriptors(links.flat(1).filter(isHtmlLinkDescriptor).filter(link => link.rel === "stylesheet" || link.rel === "preload").map(link => link.rel === "stylesheet" ? { | ||
...link, | ||
rel: "prefetch" | ||
rel: "prefetch", | ||
as: "style" | ||
} : { | ||
...link, | ||
rel: "prefetch", | ||
as: "style" | ||
}); | ||
rel: "prefetch" | ||
})); | ||
} | ||
@@ -165,3 +177,3 @@ | ||
let path = parsePathPatch(page); | ||
return dedupeHrefs(matches.filter(match => manifest.routes[match.route.id].hasLoader).map(match => { | ||
return dedupeHrefs(matches.filter(match => manifest.routes[match.route.id].hasLoader && !manifest.routes[match.route.id].hasClientLoader).map(match => { | ||
let { | ||
@@ -203,14 +215,25 @@ pathname, | ||
} | ||
function dedupe(descriptors, preloads) { | ||
function sortKeys(obj) { | ||
let sorted = {}; | ||
let keys = Object.keys(obj).sort(); | ||
for (let key of keys) { | ||
sorted[key] = obj[key]; | ||
} | ||
return sorted; | ||
} | ||
function dedupeLinkDescriptors(descriptors, preloads) { | ||
let set = new Set(); | ||
let preloadsSet = new Set(preloads); | ||
return descriptors.reduce((deduped, descriptor) => { | ||
let alreadyModulePreload = !isPageLinkDescriptor(descriptor) && descriptor.as === "script" && descriptor.href && preloadsSet.has(descriptor.href); | ||
let alreadyModulePreload = preloads && !isPageLinkDescriptor(descriptor) && descriptor.as === "script" && descriptor.href && preloadsSet.has(descriptor.href); | ||
if (alreadyModulePreload) { | ||
return deduped; | ||
} | ||
let str = JSON.stringify(descriptor); | ||
if (!set.has(str)) { | ||
set.add(str); | ||
deduped.push(descriptor); | ||
let key = JSON.stringify(sortKeys(descriptor)); | ||
if (!set.has(key)) { | ||
set.add(key); | ||
deduped.push({ | ||
key, | ||
link: descriptor | ||
}); | ||
} | ||
@@ -228,2 +251,16 @@ return deduped; | ||
export { dedupe, getDataLinkHrefs, getLinksForMatches, getModuleLinkHrefs, getNewMatchesForLinks, getStylesheetPrefetchLinks, isHtmlLinkDescriptor, isPageLinkDescriptor, prefetchStyleLinks }; | ||
// Detect if this browser supports <link rel="preload"> (or has it enabled). | ||
// Originally added to handle the firefox `network.preload` config: | ||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1847811 | ||
let _isPreloadSupported; | ||
function isPreloadSupported() { | ||
if (_isPreloadSupported !== undefined) { | ||
return _isPreloadSupported; | ||
} | ||
let el = document.createElement("link"); | ||
_isPreloadSupported = el.relList.supports("preload"); | ||
el = null; | ||
return _isPreloadSupported; | ||
} | ||
export { getDataLinkHrefs, getKeyedLinksForMatches, getKeyedPrefetchLinks, getModuleLinkHrefs, getNewMatchesForLinks, isPageLinkDescriptor, prefetchStyleLinks }; |
/** | ||
* @remix-run/react v0.0.0-nightly-ccefed3-20230621 | ||
* @remix-run/react v0.0.0-nightly-cd403b516-20240809 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
/** | ||
* @remix-run/react v0.0.0-nightly-ccefed3-20230621 | ||
* @remix-run/react v0.0.0-nightly-cd403b516-20240809 | ||
* | ||
@@ -12,48 +12,38 @@ * Copyright (c) Remix Software Inc. | ||
/** | ||
* A React component that is rendered when the server throws a Response. | ||
* | ||
* @deprecated Please enable the v2_errorBoundary flag | ||
* | ||
* @see https://remix.run/route/catch-boundary | ||
* A function that handles data mutations for a route on the client | ||
*/ | ||
/** | ||
* A React component that is rendered when there is an error on a route. | ||
* | ||
* @deprecated Please enable the v2_errorBoundary flag | ||
* | ||
* @see https://remix.run/route/error-boundary | ||
* Arguments passed to a route `clientAction` function | ||
*/ | ||
/** | ||
* V2 version of the ErrorBoundary that eliminates the distinction between | ||
* Error and Catch Boundaries and behaves like RR 6.4 errorElement and captures | ||
* errors with useRouteError() | ||
* A function that loads data for a route on the client | ||
*/ | ||
/** | ||
* A function that defines `<link>` tags to be inserted into the `<head>` of | ||
* the document on route transitions. | ||
* | ||
* @see https://remix.run/route/meta | ||
* Arguments passed to a route `clientLoader` function | ||
*/ | ||
/** | ||
* A function that returns an object of name + content pairs to use for | ||
* `<meta>` tags for a route. These tags will be merged with (and take | ||
* precedence over) tags from parent routes. | ||
* | ||
* @see https://remix.run/route/meta | ||
* ErrorBoundary to display for this route | ||
*/ | ||
// TODO: Replace in v2 | ||
/** | ||
* `<Route HydrateFallback>` component to render on initial loads | ||
* when client loaders are present | ||
*/ | ||
/** | ||
* A name/content pair used to render `<meta>` tags in a meta function for a | ||
* route. The value can be either a string, which will render a single `<meta>` | ||
* tag, or an array of strings that will render multiple tags with the same | ||
* `name` attribute. | ||
* Optional, root-only `<Route Layout>` component to wrap the root content in. | ||
* Useful for defining the <html>/<head>/<body> document shell shared by the | ||
* Component, HydrateFallback, and ErrorBoundary | ||
*/ | ||
// TODO: Replace in v2 | ||
/** | ||
* A function that defines `<link>` tags to be inserted into the `<head>` of | ||
* the document on route transitions. | ||
* | ||
* @see https://remix.run/route/meta | ||
*/ | ||
@@ -79,6 +69,23 @@ /** | ||
} catch (error) { | ||
// User got caught in the middle of a deploy and the CDN no longer has the | ||
// asset we're trying to import! Reload from the server and the user | ||
// (should) get the new manifest--unless the developer purged the static | ||
// assets, the manifest path, but not the documents 😬 | ||
// If we can't load the route it's likely one of 2 things: | ||
// - User got caught in the middle of a deploy and the CDN no longer has the | ||
// asset we're trying to import! Reload from the server and the user | ||
// (should) get the new manifest--unless the developer purged the static | ||
// assets, the manifest path, but not the documents 😬 | ||
// - Or, the asset trying to be imported has an error (usually in vite dev | ||
// mode), so the best we can do here is log the error for visibility | ||
// (via `Preserve log`) and reload | ||
// Log the error so it can be accessed via the `Preserve Log` setting | ||
console.error(`Error loading route module \`${route.module}\`, reloading page...`); | ||
console.error(error); | ||
if (window.__remixContext.isSpaMode && | ||
// @ts-expect-error | ||
typeof import.meta.hot !== "undefined") { | ||
// In SPA Mode (which implies vite) we don't want to perform a hard reload | ||
// on dev-time errors since it's a vite compilation error and a reload is | ||
// just going to fail with the same issue. Let the UI bubble to the error | ||
// boundary and let them see the error in the overlay or the dev server log | ||
throw error; | ||
} | ||
window.location.reload(); | ||
@@ -91,9 +98,2 @@ return new Promise(() => { | ||
/** | ||
* @deprecated The `unstable_shouldReload` function has been removed, so this | ||
* function will never run and route data will be revalidated on every request. | ||
* Please update the function name to `shouldRevalidate` and use the | ||
* `ShouldRevalidateFunction` interface. | ||
*/ | ||
export { loadRouteModule }; |
/** | ||
* @remix-run/react v0.0.0-nightly-ccefed3-20230621 | ||
* @remix-run/react v0.0.0-nightly-cd403b516-20240809 | ||
* | ||
@@ -12,8 +12,10 @@ * Copyright (c) Remix Software Inc. | ||
import * as React from 'react'; | ||
import { redirect } from 'react-router-dom'; | ||
import { UNSAFE_ErrorResponseImpl } from '@remix-run/router'; | ||
import { useRouteError, redirect } from 'react-router-dom'; | ||
import { loadRouteModule } from './routeModules.js'; | ||
import { fetchData, isRedirectResponse, isCatchResponse, isDeferredResponse, parseDeferredReadableStream } from './data.js'; | ||
import { fetchData, isRedirectResponse, isCatchResponse, isDeferredResponse, parseDeferredReadableStream, isDeferredData, isResponse } from './data.js'; | ||
import { prefetchStyleLinks } from './links.js'; | ||
import { RemixRootDefaultErrorBoundary } from './errorBoundaries.js'; | ||
import { RemixRootDefaultHydrateFallback } from './fallback.js'; | ||
import invariant from './invariant.js'; | ||
import { RemixRoute, RemixRouteError } from './components.js'; | ||
@@ -37,22 +39,61 @@ // NOTE: make sure to change the Route in server-runtime if you change this | ||
} | ||
function createServerRoutes(manifest, routeModules, future, parentId = "", routesByParentId = groupRoutesByParentId(manifest)) { | ||
function getRouteComponents(route, routeModule, isSpaMode) { | ||
let Component = getRouteModuleComponent(routeModule); | ||
// HydrateFallback can only exist on the root route in SPA Mode | ||
let HydrateFallback = routeModule.HydrateFallback && (!isSpaMode || route.id === "root") ? routeModule.HydrateFallback : route.id === "root" ? RemixRootDefaultHydrateFallback : undefined; | ||
let ErrorBoundary = routeModule.ErrorBoundary ? routeModule.ErrorBoundary : route.id === "root" ? () => /*#__PURE__*/React.createElement(RemixRootDefaultErrorBoundary, { | ||
error: useRouteError() | ||
}) : undefined; | ||
if (route.id === "root" && routeModule.Layout) { | ||
return { | ||
...(Component ? { | ||
element: /*#__PURE__*/React.createElement(routeModule.Layout, null, /*#__PURE__*/React.createElement(Component, null)) | ||
} : { | ||
Component | ||
}), | ||
...(ErrorBoundary ? { | ||
errorElement: /*#__PURE__*/React.createElement(routeModule.Layout, null, /*#__PURE__*/React.createElement(ErrorBoundary, null)) | ||
} : { | ||
ErrorBoundary | ||
}), | ||
...(HydrateFallback ? { | ||
hydrateFallbackElement: /*#__PURE__*/React.createElement(routeModule.Layout, null, /*#__PURE__*/React.createElement(HydrateFallback, null)) | ||
} : { | ||
HydrateFallback | ||
}) | ||
}; | ||
} | ||
return { | ||
Component, | ||
ErrorBoundary, | ||
HydrateFallback | ||
}; | ||
} | ||
function createServerRoutes(manifest, routeModules, future, isSpaMode, parentId = "", routesByParentId = groupRoutesByParentId(manifest), spaModeLazyPromise = Promise.resolve({ | ||
Component: () => null | ||
})) { | ||
return (routesByParentId[parentId] || []).map(route => { | ||
let hasErrorBoundary = future.v2_errorBoundary === true ? route.id === "root" || route.hasErrorBoundary : route.id === "root" || route.hasCatchBoundary || route.hasErrorBoundary; | ||
let routeModule = routeModules[route.id]; | ||
invariant(routeModule, "No `routeModule` available to create server routes"); | ||
let dataRoute = { | ||
...getRouteComponents(route, routeModule, isSpaMode), | ||
caseSensitive: route.caseSensitive, | ||
element: /*#__PURE__*/React.createElement(RemixRoute, { | ||
id: route.id | ||
}), | ||
errorElement: hasErrorBoundary ? /*#__PURE__*/React.createElement(RemixRouteError, { | ||
id: route.id | ||
}) : undefined, | ||
id: route.id, | ||
index: route.index, | ||
path: route.path, | ||
handle: routeModules[route.id].handle | ||
// Note: we don't need loader/action/shouldRevalidate on these routes | ||
// since they're for a static render | ||
handle: routeModule.handle, | ||
// For SPA Mode, all routes are lazy except root. However we tell the | ||
// router root is also lazy here too since we don't need a full | ||
// implementation - we just need a `lazy` prop to tell the RR rendering | ||
// where to stop which is always at the root route in SPA mode | ||
lazy: isSpaMode ? () => spaModeLazyPromise : undefined, | ||
// For partial hydration rendering, we need to indicate when the route | ||
// has a loader/clientLoader, but it won't ever be called during the static | ||
// render, so just give it a no-op function so we can render down to the | ||
// proper fallback | ||
loader: route.hasLoader || route.hasClientLoader ? () => null : undefined | ||
// We don't need action/shouldRevalidate on these routes since they're | ||
// for a static render | ||
}; | ||
let children = createServerRoutes(manifest, routeModules, future, route.id, routesByParentId); | ||
let children = createServerRoutes(manifest, routeModules, future, isSpaMode, route.id, routesByParentId, spaModeLazyPromise); | ||
if (children.length > 0) dataRoute.children = children; | ||
@@ -62,27 +103,213 @@ return dataRoute; | ||
} | ||
function createClientRoutesWithHMRRevalidationOptOut(needsRevalidation, manifest, routeModulesCache, future) { | ||
return createClientRoutes(manifest, routeModulesCache, future, "", groupRoutesByParentId(manifest), needsRevalidation); | ||
function createClientRoutesWithHMRRevalidationOptOut(needsRevalidation, manifest, routeModulesCache, initialState, future, isSpaMode) { | ||
return createClientRoutes(manifest, routeModulesCache, initialState, future, isSpaMode, "", groupRoutesByParentId(manifest), needsRevalidation); | ||
} | ||
function createClientRoutes(manifest, routeModulesCache, future, parentId = "", routesByParentId = groupRoutesByParentId(manifest), needsRevalidation) { | ||
function preventInvalidServerHandlerCall(type, route, isSpaMode) { | ||
if (isSpaMode) { | ||
let fn = type === "action" ? "serverAction()" : "serverLoader()"; | ||
let msg = `You cannot call ${fn} in SPA Mode (routeId: "${route.id}")`; | ||
console.error(msg); | ||
throw new UNSAFE_ErrorResponseImpl(400, "Bad Request", new Error(msg), true); | ||
} | ||
let fn = type === "action" ? "serverAction()" : "serverLoader()"; | ||
let msg = `You are trying to call ${fn} on a route that does not have a server ` + `${type} (routeId: "${route.id}")`; | ||
if (type === "loader" && !route.hasLoader || type === "action" && !route.hasAction) { | ||
console.error(msg); | ||
throw new UNSAFE_ErrorResponseImpl(400, "Bad Request", new Error(msg), true); | ||
} | ||
} | ||
function noActionDefinedError(type, routeId) { | ||
let article = type === "clientAction" ? "a" : "an"; | ||
let msg = `Route "${routeId}" does not have ${article} ${type}, but you are trying to ` + `submit to it. To fix this, please add ${article} \`${type}\` function to the route`; | ||
console.error(msg); | ||
throw new UNSAFE_ErrorResponseImpl(405, "Method Not Allowed", new Error(msg), true); | ||
} | ||
function createClientRoutes(manifest, routeModulesCache, initialState, future, isSpaMode, parentId = "", routesByParentId = groupRoutesByParentId(manifest), needsRevalidation) { | ||
return (routesByParentId[parentId] || []).map(route => { | ||
let hasErrorBoundary = future.v2_errorBoundary === true ? route.id === "root" || route.hasErrorBoundary : route.id === "root" || route.hasCatchBoundary || route.hasErrorBoundary; | ||
let routeModule = routeModulesCache[route.id]; | ||
// Fetch data from the server either via single fetch or the standard `?_data` | ||
// request. Unwrap it when called via `serverLoader`/`serverAction` in a | ||
// client handler, otherwise return the raw response for the router to unwrap | ||
async function fetchServerHandlerAndMaybeUnwrap(request, unwrap, singleFetch) { | ||
if (typeof singleFetch === "function") { | ||
let result = await singleFetch(); | ||
return result; | ||
} | ||
let result = await fetchServerHandler(request, route); | ||
return unwrap ? unwrapServerResponse(result) : result; | ||
} | ||
function fetchServerLoader(request, unwrap, singleFetch) { | ||
if (!route.hasLoader) return Promise.resolve(null); | ||
return fetchServerHandlerAndMaybeUnwrap(request, unwrap, singleFetch); | ||
} | ||
function fetchServerAction(request, unwrap, singleFetch) { | ||
if (!route.hasAction) { | ||
throw noActionDefinedError("action", route.id); | ||
} | ||
return fetchServerHandlerAndMaybeUnwrap(request, unwrap, singleFetch); | ||
} | ||
async function prefetchStylesAndCallHandler(handler) { | ||
// Only prefetch links if we exist in the routeModulesCache (critical modules | ||
// and navigating back to pages previously loaded via route.lazy). Initial | ||
// execution of route.lazy (when the module is not in the cache) will handle | ||
// prefetching style links via loadRouteModuleWithBlockingLinks. | ||
let cachedModule = routeModulesCache[route.id]; | ||
let linkPrefetchPromise = cachedModule ? prefetchStyleLinks(route, cachedModule) : Promise.resolve(); | ||
try { | ||
return handler(); | ||
} finally { | ||
await linkPrefetchPromise; | ||
} | ||
} | ||
let dataRoute = { | ||
caseSensitive: route.caseSensitive, | ||
element: /*#__PURE__*/React.createElement(RemixRoute, { | ||
id: route.id | ||
}), | ||
errorElement: hasErrorBoundary ? /*#__PURE__*/React.createElement(RemixRouteError, { | ||
id: route.id | ||
}) : undefined, | ||
id: route.id, | ||
index: route.index, | ||
path: route.path, | ||
// handle gets added in via useMatches since we aren't guaranteed to | ||
// have the route module available here | ||
handle: undefined, | ||
loader: createDataFunction(route, routeModulesCache, false), | ||
action: createDataFunction(route, routeModulesCache, true), | ||
shouldRevalidate: createShouldRevalidate(route, routeModulesCache, needsRevalidation) | ||
path: route.path | ||
}; | ||
let children = createClientRoutes(manifest, routeModulesCache, future, route.id, routesByParentId, needsRevalidation); | ||
if (routeModule) { | ||
var _initialState$loaderD, _initialState$errors, _routeModule$clientLo; | ||
// Use critical path modules directly | ||
Object.assign(dataRoute, { | ||
...dataRoute, | ||
...getRouteComponents(route, routeModule, isSpaMode), | ||
handle: routeModule.handle, | ||
shouldRevalidate: needsRevalidation ? wrapShouldRevalidateForHdr(route.id, routeModule.shouldRevalidate, needsRevalidation) : routeModule.shouldRevalidate | ||
}); | ||
let initialData = initialState === null || initialState === void 0 ? void 0 : (_initialState$loaderD = initialState.loaderData) === null || _initialState$loaderD === void 0 ? void 0 : _initialState$loaderD[route.id]; | ||
let initialError = initialState === null || initialState === void 0 ? void 0 : (_initialState$errors = initialState.errors) === null || _initialState$errors === void 0 ? void 0 : _initialState$errors[route.id]; | ||
let isHydrationRequest = needsRevalidation == null && (((_routeModule$clientLo = routeModule.clientLoader) === null || _routeModule$clientLo === void 0 ? void 0 : _routeModule$clientLo.hydrate) === true || !route.hasLoader); | ||
dataRoute.loader = async ({ | ||
request, | ||
params | ||
}, singleFetch) => { | ||
try { | ||
let result = await prefetchStylesAndCallHandler(async () => { | ||
invariant(routeModule, "No `routeModule` available for critical-route loader"); | ||
if (!routeModule.clientLoader) { | ||
if (isSpaMode) return null; | ||
// Call the server when no client loader exists | ||
return fetchServerLoader(request, false, singleFetch); | ||
} | ||
return routeModule.clientLoader({ | ||
request, | ||
params, | ||
async serverLoader() { | ||
preventInvalidServerHandlerCall("loader", route, isSpaMode); | ||
// On the first call, resolve with the server result | ||
if (isHydrationRequest) { | ||
if (initialError !== undefined) { | ||
throw initialError; | ||
} | ||
return initialData; | ||
} | ||
// Call the server loader for client-side navigations | ||
return fetchServerLoader(request, true, singleFetch); | ||
} | ||
}); | ||
}); | ||
return result; | ||
} finally { | ||
// Whether or not the user calls `serverLoader`, we only let this | ||
// stick around as true for one loader call | ||
isHydrationRequest = false; | ||
} | ||
}; | ||
// Let React Router know whether to run this on hydration | ||
dataRoute.loader.hydrate = shouldHydrateRouteLoader(route, routeModule, isSpaMode); | ||
dataRoute.action = ({ | ||
request, | ||
params | ||
}, singleFetch) => { | ||
return prefetchStylesAndCallHandler(async () => { | ||
invariant(routeModule, "No `routeModule` available for critical-route action"); | ||
if (!routeModule.clientAction) { | ||
if (isSpaMode) { | ||
throw noActionDefinedError("clientAction", route.id); | ||
} | ||
return fetchServerAction(request, false, singleFetch); | ||
} | ||
return routeModule.clientAction({ | ||
request, | ||
params, | ||
async serverAction() { | ||
preventInvalidServerHandlerCall("action", route, isSpaMode); | ||
return fetchServerAction(request, true, singleFetch); | ||
} | ||
}); | ||
}); | ||
}; | ||
} else { | ||
// If the lazy route does not have a client loader/action we want to call | ||
// the server loader/action in parallel with the module load so we add | ||
// loader/action as static props on the route | ||
if (!route.hasClientLoader) { | ||
dataRoute.loader = ({ | ||
request | ||
}, singleFetch) => prefetchStylesAndCallHandler(() => { | ||
if (isSpaMode) return Promise.resolve(null); | ||
return fetchServerLoader(request, false, singleFetch); | ||
}); | ||
} | ||
if (!route.hasClientAction) { | ||
dataRoute.action = ({ | ||
request | ||
}, singleFetch) => prefetchStylesAndCallHandler(() => { | ||
if (isSpaMode) { | ||
throw noActionDefinedError("clientAction", route.id); | ||
} | ||
return fetchServerAction(request, false, singleFetch); | ||
}); | ||
} | ||
// Load all other modules via route.lazy() | ||
dataRoute.lazy = async () => { | ||
let mod = await loadRouteModuleWithBlockingLinks(route, routeModulesCache); | ||
let lazyRoute = { | ||
...mod | ||
}; | ||
if (mod.clientLoader) { | ||
let clientLoader = mod.clientLoader; | ||
lazyRoute.loader = (args, singleFetch) => clientLoader({ | ||
...args, | ||
async serverLoader() { | ||
preventInvalidServerHandlerCall("loader", route, isSpaMode); | ||
return fetchServerLoader(args.request, true, singleFetch); | ||
} | ||
}); | ||
} | ||
if (mod.clientAction) { | ||
let clientAction = mod.clientAction; | ||
lazyRoute.action = (args, singleFetch) => clientAction({ | ||
...args, | ||
async serverAction() { | ||
preventInvalidServerHandlerCall("action", route, isSpaMode); | ||
return fetchServerAction(args.request, true, singleFetch); | ||
} | ||
}); | ||
} | ||
if (needsRevalidation) { | ||
lazyRoute.shouldRevalidate = wrapShouldRevalidateForHdr(route.id, mod.shouldRevalidate, needsRevalidation); | ||
} | ||
return { | ||
...(lazyRoute.loader ? { | ||
loader: lazyRoute.loader | ||
} : {}), | ||
...(lazyRoute.action ? { | ||
action: lazyRoute.action | ||
} : {}), | ||
hasErrorBoundary: lazyRoute.hasErrorBoundary, | ||
shouldRevalidate: lazyRoute.shouldRevalidate, | ||
handle: lazyRoute.handle, | ||
// No need to wrap these in layout since the root route is never | ||
// loaded via route.lazy() | ||
Component: lazyRoute.Component, | ||
ErrorBoundary: lazyRoute.ErrorBoundary | ||
}; | ||
}; | ||
} | ||
let children = createClientRoutes(manifest, routeModulesCache, initialState, future, isSpaMode, route.id, routesByParentId, needsRevalidation); | ||
if (children.length > 0) dataRoute.children = children; | ||
@@ -92,19 +319,13 @@ return dataRoute; | ||
} | ||
function createShouldRevalidate(route, routeModules, needsRevalidation) { | ||
// When an HMR / HDR update happens we opt out of all user-defined | ||
// revalidation logic and force a revalidation on the first call | ||
function wrapShouldRevalidateForHdr(routeId, routeShouldRevalidate, needsRevalidation) { | ||
let handledRevalidation = false; | ||
return function (arg) { | ||
let module = routeModules[route.id]; | ||
invariant(module, `Expected route module to be loaded for ${route.id}`); | ||
// When an HMR / HDR update happens we opt out of all user-defined | ||
// revalidation logic and the do as the dev server tells us the first | ||
// time router.revalidate() is called. | ||
if (needsRevalidation !== undefined && !handledRevalidation) { | ||
return arg => { | ||
if (!handledRevalidation) { | ||
handledRevalidation = true; | ||
return needsRevalidation.has(route.id); | ||
return needsRevalidation.has(routeId); | ||
} | ||
if (module.shouldRevalidate) { | ||
return module.shouldRevalidate(arg); | ||
} | ||
return arg.defaultShouldRevalidate; | ||
return routeShouldRevalidate ? routeShouldRevalidate(arg) : arg.defaultShouldRevalidate; | ||
}; | ||
@@ -114,36 +335,48 @@ } | ||
let routeModule = await loadRouteModule(route, routeModules); | ||
await prefetchStyleLinks(routeModule); | ||
return routeModule; | ||
await prefetchStyleLinks(route, routeModule); | ||
// Include all `browserSafeRouteExports` fields, except `HydrateFallback` | ||
// since those aren't used on lazily loaded routes | ||
return { | ||
Component: getRouteModuleComponent(routeModule), | ||
ErrorBoundary: routeModule.ErrorBoundary, | ||
clientAction: routeModule.clientAction, | ||
clientLoader: routeModule.clientLoader, | ||
handle: routeModule.handle, | ||
links: routeModule.links, | ||
meta: routeModule.meta, | ||
shouldRevalidate: routeModule.shouldRevalidate | ||
}; | ||
} | ||
function createDataFunction(route, routeModules, isAction) { | ||
return async ({ | ||
request | ||
}) => { | ||
let routeModulePromise = loadRouteModuleWithBlockingLinks(route, routeModules); | ||
try { | ||
if (isAction && !route.hasAction) { | ||
let msg = `Route "${route.id}" does not have an action, but you are trying ` + `to submit to it. To fix this, please add an \`action\` function to the route`; | ||
console.error(msg); | ||
throw new Error(msg); | ||
} else if (!isAction && !route.hasLoader) { | ||
return null; | ||
} | ||
let result = await fetchData(request, route.id); | ||
if (result instanceof Error) { | ||
throw result; | ||
} | ||
if (isRedirectResponse(result)) { | ||
throw getRedirect(result); | ||
} | ||
if (isCatchResponse(result)) { | ||
throw result; | ||
} | ||
if (isDeferredResponse(result) && result.body) { | ||
return await parseDeferredReadableStream(result.body); | ||
} | ||
return result; | ||
} finally { | ||
await routeModulePromise; | ||
async function fetchServerHandler(request, route) { | ||
let result = await fetchData(request, route.id); | ||
if (result instanceof Error) { | ||
throw result; | ||
} | ||
if (isRedirectResponse(result)) { | ||
throw getRedirect(result); | ||
} | ||
if (isCatchResponse(result)) { | ||
throw result; | ||
} | ||
if (isDeferredResponse(result) && result.body) { | ||
return await parseDeferredReadableStream(result.body); | ||
} | ||
return result; | ||
} | ||
function unwrapServerResponse(result) { | ||
if (isDeferredData(result)) { | ||
return result.data; | ||
} | ||
if (isResponse(result)) { | ||
let contentType = result.headers.get("Content-Type"); | ||
// Check between word boundaries instead of startsWith() due to the last | ||
// paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type | ||
if (contentType && /\bapplication\/json\b/.test(contentType)) { | ||
return result.json(); | ||
} else { | ||
return result.text(); | ||
} | ||
}; | ||
} | ||
return result; | ||
} | ||
@@ -158,2 +391,10 @@ function getRedirect(response) { | ||
} | ||
let reloadDocument = response.headers.get("X-Remix-Reload-Document"); | ||
if (reloadDocument) { | ||
headers["X-Remix-Reload-Document"] = reloadDocument; | ||
} | ||
let replace = response.headers.get("X-Remix-Replace"); | ||
if (replace) { | ||
headers["X-Remix-Replace"] = replace; | ||
} | ||
return redirect(url, { | ||
@@ -165,2 +406,17 @@ status, | ||
export { createClientRoutes, createClientRoutesWithHMRRevalidationOptOut, createServerRoutes }; | ||
// Our compiler generates the default export as `{}` when no default is provided, | ||
// which can lead us to trying to use that as a Component in RR and calling | ||
// createElement on it. Patching here as a quick fix and hoping it's no longer | ||
// an issue in Vite. | ||
function getRouteModuleComponent(routeModule) { | ||
if (routeModule.default == null) return undefined; | ||
let isEmptyObject = typeof routeModule.default === "object" && Object.keys(routeModule.default).length === 0; | ||
if (!isEmptyObject) { | ||
return routeModule.default; | ||
} | ||
} | ||
function shouldHydrateRouteLoader(route, routeModule, isSpaMode) { | ||
return isSpaMode && route.id !== "root" || routeModule.clientLoader != null && (routeModule.clientLoader.hydrate === true || route.hasLoader !== true); | ||
} | ||
export { createClientRoutes, createClientRoutesWithHMRRevalidationOptOut, createServerRoutes, shouldHydrateRouteLoader }; |
/** | ||
* @remix-run/react v0.0.0-nightly-ccefed3-20230621 | ||
* @remix-run/react v0.0.0-nightly-cd403b516-20240809 | ||
* | ||
@@ -13,4 +13,4 @@ * Copyright (c) Remix Software Inc. | ||
import * as React from 'react'; | ||
import { useLocation, UNSAFE_useScrollRestoration } from 'react-router-dom'; | ||
import { useMatches } from './components.js'; | ||
import { useLocation, useMatches, UNSAFE_useScrollRestoration } from 'react-router-dom'; | ||
import { useRemixContext } from './components.js'; | ||
@@ -29,2 +29,5 @@ let STORAGE_KEY = "positions"; | ||
}) { | ||
let { | ||
isSpaMode | ||
} = useRemixContext(); | ||
let location = useLocation(); | ||
@@ -51,2 +54,8 @@ let matches = useMatches(); | ||
[]); | ||
// In SPA Mode, there's nothing to restore on initial render since we didn't | ||
// render anything on the server | ||
if (isSpaMode) { | ||
return null; | ||
} | ||
let restoreScroll = ((STORAGE_KEY, restoreKey) => { | ||
@@ -53,0 +62,0 @@ if (!window.history.state || !window.history.state.key) { |
/** | ||
* @remix-run/react v0.0.0-nightly-ccefed3-20230621 | ||
* @remix-run/react v0.0.0-nightly-cd403b516-20240809 | ||
* | ||
@@ -14,4 +14,5 @@ * Copyright (c) Remix Software Inc. | ||
import { RemixContext } from './components.js'; | ||
import { RemixErrorBoundary, RemixRootDefaultErrorBoundary } from './errorBoundaries.js'; | ||
import { createServerRoutes } from './routes.js'; | ||
import { RemixErrorBoundary } from './errorBoundaries.js'; | ||
import { createServerRoutes, shouldHydrateRouteLoader } from './routes.js'; | ||
import { StreamTransfer } from './single-fetch.js'; | ||
@@ -26,3 +27,4 @@ /** | ||
url, | ||
abortDelay | ||
abortDelay, | ||
nonce | ||
}) { | ||
@@ -35,17 +37,47 @@ if (typeof url === "string") { | ||
routeModules, | ||
criticalCss, | ||
serverHandoffString | ||
} = context; | ||
let routes = createServerRoutes(manifest.routes, routeModules, context.future); | ||
let router = createStaticRouter(routes, context.staticHandlerContext); | ||
return /*#__PURE__*/React.createElement(RemixContext.Provider, { | ||
let routes = createServerRoutes(manifest.routes, routeModules, context.future, context.isSpaMode); | ||
// Create a shallow clone of `loaderData` we can mutate for partial hydration. | ||
// When a route exports a `clientLoader` and a `HydrateFallback`, we want to | ||
// render the fallback on the server so we clear our the `loaderData` during SSR. | ||
// Is it important not to change the `context` reference here since we use it | ||
// for context._deepestRenderedBoundaryId tracking | ||
context.staticHandlerContext.loaderData = { | ||
...context.staticHandlerContext.loaderData | ||
}; | ||
for (let match of context.staticHandlerContext.matches) { | ||
let routeId = match.route.id; | ||
let route = routeModules[routeId]; | ||
let manifestRoute = context.manifest.routes[routeId]; | ||
// Clear out the loaderData to avoid rendering the route component when the | ||
// route opted into clientLoader hydration and either: | ||
// * gave us a HydrateFallback | ||
// * or doesn't have a server loader and we have no data to render | ||
if (route && shouldHydrateRouteLoader(manifestRoute, route, context.isSpaMode) && (route.HydrateFallback || !manifestRoute.hasLoader)) { | ||
context.staticHandlerContext.loaderData[routeId] = undefined; | ||
} | ||
} | ||
let router = createStaticRouter(routes, context.staticHandlerContext, { | ||
future: { | ||
v7_partialHydration: true, | ||
v7_relativeSplatPath: context.future.v3_relativeSplatPath | ||
} | ||
}); | ||
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(RemixContext.Provider, { | ||
value: { | ||
manifest, | ||
routeModules, | ||
criticalCss, | ||
serverHandoffString, | ||
future: context.future, | ||
abortDelay | ||
isSpaMode: context.isSpaMode, | ||
serializeError: context.serializeError, | ||
abortDelay, | ||
renderMeta: context.renderMeta | ||
} | ||
}, /*#__PURE__*/React.createElement(RemixErrorBoundary, { | ||
location: router.state.location, | ||
component: RemixRootDefaultErrorBoundary | ||
location: router.state.location | ||
}, /*#__PURE__*/React.createElement(StaticRouterProvider, { | ||
@@ -55,5 +87,11 @@ router: router, | ||
hydrate: false | ||
}))); | ||
}))), context.future.unstable_singleFetch && context.serverHandoffStream ? /*#__PURE__*/React.createElement(React.Suspense, null, /*#__PURE__*/React.createElement(StreamTransfer, { | ||
context: context, | ||
identifier: 0, | ||
reader: context.serverHandoffStream.getReader(), | ||
textDecoder: new TextDecoder(), | ||
nonce: nonce | ||
})) : null); | ||
} | ||
export { RemixServer }; |
@@ -0,16 +1,16 @@ | ||
export type { ErrorResponse, Fetcher, FetcherWithComponents, FormEncType, FormMethod, Location, NavigateFunction, Navigation, Params, Path, ShouldRevalidateFunction, ShouldRevalidateFunctionArgs, SubmitFunction, SubmitOptions, Blocker, BlockerFunction, } from "react-router-dom"; | ||
export { createPath, createRoutesFromChildren, createRoutesFromElements, createSearchParams, generatePath, matchPath, matchRoutes, parsePath, renderMatches, resolvePath, Navigate, NavigationType, Outlet, Route, Routes, useAsyncError, useAsyncValue, isRouteErrorResponse, useBeforeUnload, useFetchers, useFormAction, useHref, useInRouterContext, useLinkClickHandler, useLocation, useMatch, useNavigate, useNavigation, useNavigationType, useOutlet, useOutletContext, useParams, useResolvedPath, useRevalidator, useRouteError, useRoutes, useSearchParams, useSubmit, useBlocker, unstable_usePrompt, unstable_useViewTransitionState, } from "react-router-dom"; | ||
export { defer, json, redirect, redirectDocument, replace, unstable_data, } from "@remix-run/server-runtime"; | ||
export type { RemixBrowserProps } from "./browser"; | ||
export { RemixBrowser } from "./browser"; | ||
export type { FormEncType, FormMethod, FormProps, Location, NavigateFunction, Params, Path, ShouldRevalidateFunction, SubmitFunction, SubmitOptions, unstable_Blocker, unstable_BlockerFunction, } from "react-router-dom"; | ||
export { Form, Outlet, useAsyncError, useAsyncValue, isRouteErrorResponse, useBeforeUnload, useFormAction, useHref, useLocation, useMatch, useNavigate, useNavigation, useNavigationType, useOutlet, useOutletContext, useParams, useResolvedPath, useRevalidator, useRouteError, useRouteLoaderData, useSearchParams, useSubmit, unstable_useBlocker, unstable_usePrompt, } from "react-router-dom"; | ||
export type { AwaitProps, FetcherWithComponents, RouteMatch, RemixNavLinkProps as NavLinkProps, RemixLinkProps as LinkProps, } from "./components"; | ||
export { Await, Meta, Links, Scripts, Link, NavLink, PrefetchPageLinks, LiveReload, useTransition, useFetcher, useFetchers, useLoaderData, useMatches, useActionData, RemixContext as UNSAFE_RemixContext, } from "./components"; | ||
export type { ThrownResponse } from "./errors"; | ||
export { useCatch } from "./errorBoundaries"; | ||
export type { AwaitProps, RemixFormProps as FormProps, RemixNavLinkProps as NavLinkProps, RemixLinkProps as LinkProps, UIMatch, } from "./components"; | ||
export { Await, Meta, Links, Scripts, Form, Link, NavLink, PrefetchPageLinks, LiveReload, useFetcher, useLoaderData, useRouteLoaderData, useActionData, useMatches, RemixContext as UNSAFE_RemixContext, } from "./components"; | ||
export type { HtmlLinkDescriptor } from "./links"; | ||
export type { CatchBoundaryComponent, HtmlMetaDescriptor, V2_MetaArgs, V2_MetaDescriptor, V2_MetaFunction, RouteModules as UNSAFE_RouteModules, ShouldReloadFunction, } from "./routeModules"; | ||
export type { ClientActionFunction, ClientActionFunctionArgs, ClientLoaderFunction, ClientLoaderFunctionArgs, MetaArgs, MetaMatch as UNSAFE_MetaMatch, MetaDescriptor, MetaFunction, RouteModules as UNSAFE_RouteModules, } from "./routeModules"; | ||
export { ScrollRestoration } from "./scroll-restoration"; | ||
export type { RemixServerProps } from "./server"; | ||
export { RemixServer } from "./server"; | ||
export type { Fetcher } from "./transition"; | ||
export type { ClientAction as unstable_ClientAction, ClientLoader as unstable_ClientLoader, } from "./single-fetch"; | ||
export { defineClientAction as unstable_defineClientAction, defineClientLoader as unstable_defineClientLoader, } from "./single-fetch"; | ||
export type { FutureConfig as UNSAFE_FutureConfig, AssetsManifest as UNSAFE_AssetsManifest, RemixContextObject as UNSAFE_RemixContextObject, } from "./entry"; | ||
export type { EntryRoute as UNSAFE_EntryRoute, RouteManifest as UNSAFE_RouteManifest, } from "./routes"; |
/** | ||
* @remix-run/react v0.0.0-nightly-ccefed3-20230621 | ||
* @remix-run/react v0.0.0-nightly-cd403b516-20240809 | ||
* | ||
@@ -15,16 +15,20 @@ * Copyright (c) Remix Software Inc. | ||
var reactRouterDom = require('react-router-dom'); | ||
var serverRuntime = require('@remix-run/server-runtime'); | ||
var browser = require('./browser.js'); | ||
var reactRouterDom = require('react-router-dom'); | ||
var components = require('./components.js'); | ||
var errorBoundaries = require('./errorBoundaries.js'); | ||
var scrollRestoration = require('./scroll-restoration.js'); | ||
var server = require('./server.js'); | ||
var singleFetch = require('./single-fetch.js'); | ||
exports.RemixBrowser = browser.RemixBrowser; | ||
Object.defineProperty(exports, 'Form', { | ||
Object.defineProperty(exports, 'Navigate', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.Form; } | ||
get: function () { return reactRouterDom.Navigate; } | ||
}); | ||
Object.defineProperty(exports, 'NavigationType', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.NavigationType; } | ||
}); | ||
Object.defineProperty(exports, 'Outlet', { | ||
@@ -34,2 +38,30 @@ enumerable: true, | ||
}); | ||
Object.defineProperty(exports, 'Route', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.Route; } | ||
}); | ||
Object.defineProperty(exports, 'Routes', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.Routes; } | ||
}); | ||
Object.defineProperty(exports, 'createPath', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.createPath; } | ||
}); | ||
Object.defineProperty(exports, 'createRoutesFromChildren', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.createRoutesFromChildren; } | ||
}); | ||
Object.defineProperty(exports, 'createRoutesFromElements', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.createRoutesFromElements; } | ||
}); | ||
Object.defineProperty(exports, 'createSearchParams', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.createSearchParams; } | ||
}); | ||
Object.defineProperty(exports, 'generatePath', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.generatePath; } | ||
}); | ||
Object.defineProperty(exports, 'isRouteErrorResponse', { | ||
@@ -39,6 +71,22 @@ enumerable: true, | ||
}); | ||
Object.defineProperty(exports, 'unstable_useBlocker', { | ||
Object.defineProperty(exports, 'matchPath', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.unstable_useBlocker; } | ||
get: function () { return reactRouterDom.matchPath; } | ||
}); | ||
Object.defineProperty(exports, 'matchRoutes', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.matchRoutes; } | ||
}); | ||
Object.defineProperty(exports, 'parsePath', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.parsePath; } | ||
}); | ||
Object.defineProperty(exports, 'renderMatches', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.renderMatches; } | ||
}); | ||
Object.defineProperty(exports, 'resolvePath', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.resolvePath; } | ||
}); | ||
Object.defineProperty(exports, 'unstable_usePrompt', { | ||
@@ -48,2 +96,6 @@ enumerable: true, | ||
}); | ||
Object.defineProperty(exports, 'unstable_useViewTransitionState', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.unstable_useViewTransitionState; } | ||
}); | ||
Object.defineProperty(exports, 'useAsyncError', { | ||
@@ -61,2 +113,10 @@ enumerable: true, | ||
}); | ||
Object.defineProperty(exports, 'useBlocker', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.useBlocker; } | ||
}); | ||
Object.defineProperty(exports, 'useFetchers', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.useFetchers; } | ||
}); | ||
Object.defineProperty(exports, 'useFormAction', { | ||
@@ -70,2 +130,10 @@ enumerable: true, | ||
}); | ||
Object.defineProperty(exports, 'useInRouterContext', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.useInRouterContext; } | ||
}); | ||
Object.defineProperty(exports, 'useLinkClickHandler', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.useLinkClickHandler; } | ||
}); | ||
Object.defineProperty(exports, 'useLocation', { | ||
@@ -115,5 +183,5 @@ enumerable: true, | ||
}); | ||
Object.defineProperty(exports, 'useRouteLoaderData', { | ||
Object.defineProperty(exports, 'useRoutes', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.useRouteLoaderData; } | ||
get: function () { return reactRouterDom.useRoutes; } | ||
}); | ||
@@ -128,3 +196,29 @@ Object.defineProperty(exports, 'useSearchParams', { | ||
}); | ||
Object.defineProperty(exports, 'defer', { | ||
enumerable: true, | ||
get: function () { return serverRuntime.defer; } | ||
}); | ||
Object.defineProperty(exports, 'json', { | ||
enumerable: true, | ||
get: function () { return serverRuntime.json; } | ||
}); | ||
Object.defineProperty(exports, 'redirect', { | ||
enumerable: true, | ||
get: function () { return serverRuntime.redirect; } | ||
}); | ||
Object.defineProperty(exports, 'redirectDocument', { | ||
enumerable: true, | ||
get: function () { return serverRuntime.redirectDocument; } | ||
}); | ||
Object.defineProperty(exports, 'replace', { | ||
enumerable: true, | ||
get: function () { return serverRuntime.replace; } | ||
}); | ||
Object.defineProperty(exports, 'unstable_data', { | ||
enumerable: true, | ||
get: function () { return serverRuntime.unstable_data; } | ||
}); | ||
exports.RemixBrowser = browser.RemixBrowser; | ||
exports.Await = components.Await; | ||
exports.Form = components.Form; | ||
exports.Link = components.Link; | ||
@@ -140,8 +234,8 @@ exports.Links = components.Links; | ||
exports.useFetcher = components.useFetcher; | ||
exports.useFetchers = components.useFetchers; | ||
exports.useLoaderData = components.useLoaderData; | ||
exports.useMatches = components.useMatches; | ||
exports.useTransition = components.useTransition; | ||
exports.useCatch = errorBoundaries.useCatch; | ||
exports.useRouteLoaderData = components.useRouteLoaderData; | ||
exports.ScrollRestoration = scrollRestoration.ScrollRestoration; | ||
exports.RemixServer = server.RemixServer; | ||
exports.unstable_defineClientAction = singleFetch.defineClientAction; | ||
exports.unstable_defineClientLoader = singleFetch.defineClientLoader; |
/** | ||
* @remix-run/react v0.0.0-nightly-ccefed3-20230621 | ||
* @remix-run/react v0.0.0-nightly-cd403b516-20240809 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
@@ -5,2 +5,3 @@ import type { AgnosticDataRouteMatch } from "@remix-run/router"; | ||
import type { RouteModules, RouteModule } from "./routeModules"; | ||
import type { EntryRoute } from "./routes"; | ||
type Primitive = null | undefined | string | number | boolean | symbol | bigint; | ||
@@ -99,16 +100,7 @@ type LiteralUnion<LiteralType, BaseType extends Primitive> = LiteralType | (BaseType & Record<never, never>); | ||
*/ | ||
export type HtmlLinkDescriptor = ((HtmlLinkProps & Pick<Required<HtmlLinkProps>, "href">) | (HtmlLinkPreloadImage & Pick<Required<HtmlLinkPreloadImage>, "imageSizes">) | (HtmlLinkPreloadImage & Pick<Required<HtmlLinkPreloadImage>, "href"> & { | ||
export type HtmlLinkDescriptor = (HtmlLinkProps & Pick<Required<HtmlLinkProps>, "href">) | (HtmlLinkPreloadImage & Pick<Required<HtmlLinkPreloadImage>, "imageSizes">) | (HtmlLinkPreloadImage & Pick<Required<HtmlLinkPreloadImage>, "href"> & { | ||
imageSizes?: never; | ||
})) & { | ||
}); | ||
export interface PrefetchPageDescriptor extends Omit<HtmlLinkDescriptor, "href" | "rel" | "type" | "sizes" | "imageSrcSet" | "imageSizes" | "as" | "color" | "title"> { | ||
/** | ||
* @deprecated Use `imageSrcSet` instead. | ||
*/ | ||
imagesrcset?: string; | ||
/** | ||
* @deprecated Use `imageSizes` instead. | ||
*/ | ||
imagesizes?: string; | ||
}; | ||
export interface PrefetchPageDescriptor extends Omit<HtmlLinkDescriptor, "href" | "rel" | "type" | "sizes" | "imageSrcSet" | "imageSizes" | "imagesrcset" | "imagesizes" | "as" | "color" | "title"> { | ||
/** | ||
* The absolute path of the page to prefetch. | ||
@@ -123,11 +115,17 @@ */ | ||
*/ | ||
export declare function getLinksForMatches(matches: AgnosticDataRouteMatch[], routeModules: RouteModules, manifest: AssetsManifest): LinkDescriptor[]; | ||
export declare function prefetchStyleLinks(routeModule: RouteModule): Promise<void>; | ||
export declare function getKeyedLinksForMatches(matches: AgnosticDataRouteMatch[], routeModules: RouteModules, manifest: AssetsManifest): KeyedLinkDescriptor[]; | ||
export declare function prefetchStyleLinks(route: EntryRoute, routeModule: RouteModule): Promise<void>; | ||
export declare function isPageLinkDescriptor(object: any): object is PrefetchPageDescriptor; | ||
export declare function isHtmlLinkDescriptor(object: any): object is HtmlLinkDescriptor; | ||
export declare function getStylesheetPrefetchLinks(matches: AgnosticDataRouteMatch[], manifest: AssetsManifest, routeModules: RouteModules): Promise<HtmlLinkDescriptor[]>; | ||
export type KeyedHtmlLinkDescriptor = { | ||
key: string; | ||
link: HtmlLinkDescriptor; | ||
}; | ||
export declare function getKeyedPrefetchLinks(matches: AgnosticDataRouteMatch[], manifest: AssetsManifest, routeModules: RouteModules): Promise<KeyedHtmlLinkDescriptor[]>; | ||
export declare function getNewMatchesForLinks(page: string, nextMatches: AgnosticDataRouteMatch[], currentMatches: AgnosticDataRouteMatch[], manifest: AssetsManifest, location: Location, mode: "data" | "assets"): AgnosticDataRouteMatch[]; | ||
export declare function getDataLinkHrefs(page: string, matches: AgnosticDataRouteMatch[], manifest: AssetsManifest): string[]; | ||
export declare function getModuleLinkHrefs(matches: AgnosticDataRouteMatch[], manifestPatch: AssetsManifest): string[]; | ||
export declare function dedupe(descriptors: LinkDescriptor[], preloads: string[]): LinkDescriptor[]; | ||
type KeyedLinkDescriptor<Descriptor extends LinkDescriptor = LinkDescriptor> = { | ||
key: string; | ||
link: Descriptor; | ||
}; | ||
export {}; |
/** | ||
* @remix-run/react v0.0.0-nightly-ccefed3-20230621 | ||
* @remix-run/react v0.0.0-nightly-cd403b516-20240809 | ||
* | ||
@@ -25,2 +25,3 @@ * Copyright (c) Remix Software Inc. | ||
//////////////////////////////////////////////////////////////////////////////// | ||
/** | ||
@@ -30,15 +31,23 @@ * Gets all the links for a set of matches. The modules are assumed to have been | ||
*/ | ||
function getLinksForMatches(matches, routeModules, manifest) { | ||
function getKeyedLinksForMatches(matches, routeModules, manifest) { | ||
let descriptors = matches.map(match => { | ||
var _module$links; | ||
let module = routeModules[match.route.id]; | ||
return ((_module$links = module.links) === null || _module$links === void 0 ? void 0 : _module$links.call(module)) || []; | ||
}).flat(1); | ||
let route = manifest.routes[match.route.id]; | ||
return [route.css ? route.css.map(href => ({ | ||
rel: "stylesheet", | ||
href | ||
})) : [], (module === null || module === void 0 ? void 0 : (_module$links = module.links) === null || _module$links === void 0 ? void 0 : _module$links.call(module)) || []]; | ||
}).flat(2); | ||
let preloads = getCurrentPageModulePreloadHrefs(matches, manifest); | ||
return dedupe(descriptors, preloads); | ||
return dedupeLinkDescriptors(descriptors, preloads); | ||
} | ||
async function prefetchStyleLinks(routeModule) { | ||
if (!routeModule.links) return; | ||
let descriptors = routeModule.links(); | ||
if (!descriptors) return; | ||
async function prefetchStyleLinks(route, routeModule) { | ||
var _route$css, _routeModule$links; | ||
if (!route.css && !routeModule.links || !isPreloadSupported()) return; | ||
let descriptors = [((_route$css = route.css) === null || _route$css === void 0 ? void 0 : _route$css.map(href => ({ | ||
rel: "stylesheet", | ||
href | ||
}))) ?? [], ((_routeModule$links = routeModule.links) === null || _routeModule$links === void 0 ? void 0 : _routeModule$links.call(routeModule)) ?? []].flat(1); | ||
if (descriptors.length === 0) return; | ||
let styleLinks = []; | ||
@@ -55,4 +64,5 @@ for (let descriptor of descriptors) { | ||
// don't block for non-matching media queries | ||
let matchingLinks = styleLinks.filter(link => !link.media || window.matchMedia(link.media).matches); | ||
// don't block for non-matching media queries, or for stylesheets that are | ||
// already in the DOM (active route revalidations) | ||
let matchingLinks = styleLinks.filter(link => (!link.media || window.matchMedia(link.media).matches) && !document.querySelector(`link[rel="stylesheet"][href="${link.href}"]`)); | ||
await Promise.all(matchingLinks.map(prefetchStyleLink)); | ||
@@ -89,13 +99,15 @@ } | ||
function isHtmlLinkDescriptor(object) { | ||
if (object == null) return false; | ||
if (object == null) { | ||
return false; | ||
} | ||
// <link> may not have an href if <link rel="preload"> is used with imagesrcset + imagesizes | ||
// <link> may not have an href if <link rel="preload"> is used with imageSrcSet + imageSizes | ||
// https://github.com/remix-run/remix/issues/184 | ||
// https://html.spec.whatwg.org/commit-snapshots/cb4f5ff75de5f4cbd7013c4abad02f21c77d4d1c/#attr-link-imagesrcset | ||
if (object.href == null) { | ||
return object.rel === "preload" && (typeof object.imageSrcSet === "string" || typeof object.imagesrcset === "string") && (typeof object.imageSizes === "string" || typeof object.imagesizes === "string"); | ||
return object.rel === "preload" && typeof object.imageSrcSet === "string" && typeof object.imageSizes === "string"; | ||
} | ||
return typeof object.rel === "string" && typeof object.href === "string"; | ||
} | ||
async function getStylesheetPrefetchLinks(matches, manifest, routeModules$1) { | ||
async function getKeyedPrefetchLinks(matches, manifest, routeModules$1) { | ||
let links = await Promise.all(matches.map(async match => { | ||
@@ -105,10 +117,10 @@ let mod = await routeModules.loadRouteModule(manifest.routes[match.route.id], routeModules$1); | ||
})); | ||
return links.flat(1).filter(isHtmlLinkDescriptor).filter(link => link.rel === "stylesheet" || link.rel === "preload").map(link => link.rel === "preload" ? { | ||
return dedupeLinkDescriptors(links.flat(1).filter(isHtmlLinkDescriptor).filter(link => link.rel === "stylesheet" || link.rel === "preload").map(link => link.rel === "stylesheet" ? { | ||
...link, | ||
rel: "prefetch" | ||
rel: "prefetch", | ||
as: "style" | ||
} : { | ||
...link, | ||
rel: "prefetch", | ||
as: "style" | ||
}); | ||
rel: "prefetch" | ||
})); | ||
} | ||
@@ -169,3 +181,3 @@ | ||
let path = parsePathPatch(page); | ||
return dedupeHrefs(matches.filter(match => manifest.routes[match.route.id].hasLoader).map(match => { | ||
return dedupeHrefs(matches.filter(match => manifest.routes[match.route.id].hasLoader && !manifest.routes[match.route.id].hasClientLoader).map(match => { | ||
let { | ||
@@ -207,14 +219,25 @@ pathname, | ||
} | ||
function dedupe(descriptors, preloads) { | ||
function sortKeys(obj) { | ||
let sorted = {}; | ||
let keys = Object.keys(obj).sort(); | ||
for (let key of keys) { | ||
sorted[key] = obj[key]; | ||
} | ||
return sorted; | ||
} | ||
function dedupeLinkDescriptors(descriptors, preloads) { | ||
let set = new Set(); | ||
let preloadsSet = new Set(preloads); | ||
return descriptors.reduce((deduped, descriptor) => { | ||
let alreadyModulePreload = !isPageLinkDescriptor(descriptor) && descriptor.as === "script" && descriptor.href && preloadsSet.has(descriptor.href); | ||
let alreadyModulePreload = preloads && !isPageLinkDescriptor(descriptor) && descriptor.as === "script" && descriptor.href && preloadsSet.has(descriptor.href); | ||
if (alreadyModulePreload) { | ||
return deduped; | ||
} | ||
let str = JSON.stringify(descriptor); | ||
if (!set.has(str)) { | ||
set.add(str); | ||
deduped.push(descriptor); | ||
let key = JSON.stringify(sortKeys(descriptor)); | ||
if (!set.has(key)) { | ||
set.add(key); | ||
deduped.push({ | ||
key, | ||
link: descriptor | ||
}); | ||
} | ||
@@ -232,10 +255,22 @@ return deduped; | ||
exports.dedupe = dedupe; | ||
// Detect if this browser supports <link rel="preload"> (or has it enabled). | ||
// Originally added to handle the firefox `network.preload` config: | ||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1847811 | ||
let _isPreloadSupported; | ||
function isPreloadSupported() { | ||
if (_isPreloadSupported !== undefined) { | ||
return _isPreloadSupported; | ||
} | ||
let el = document.createElement("link"); | ||
_isPreloadSupported = el.relList.supports("preload"); | ||
el = null; | ||
return _isPreloadSupported; | ||
} | ||
exports.getDataLinkHrefs = getDataLinkHrefs; | ||
exports.getLinksForMatches = getLinksForMatches; | ||
exports.getKeyedLinksForMatches = getKeyedLinksForMatches; | ||
exports.getKeyedPrefetchLinks = getKeyedPrefetchLinks; | ||
exports.getModuleLinkHrefs = getModuleLinkHrefs; | ||
exports.getNewMatchesForLinks = getNewMatchesForLinks; | ||
exports.getStylesheetPrefetchLinks = getStylesheetPrefetchLinks; | ||
exports.isHtmlLinkDescriptor = isHtmlLinkDescriptor; | ||
exports.isPageLinkDescriptor = isPageLinkDescriptor; | ||
exports.prefetchStyleLinks = prefetchStyleLinks; |
/** | ||
* @remix-run/react v0.0.0-nightly-ccefed3-20230621 | ||
* @remix-run/react v0.0.0-nightly-cd403b516-20240809 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
@@ -1,4 +0,3 @@ | ||
import type { ComponentType } from "react"; | ||
import type { RouterState } from "@remix-run/router"; | ||
import type { DataRouteMatch, Params, Location, ShouldRevalidateFunction } from "react-router-dom"; | ||
import type { ComponentType, ReactElement } from "react"; | ||
import type { ActionFunction as RRActionFunction, ActionFunctionArgs as RRActionFunctionArgs, LoaderFunction as RRLoaderFunction, LoaderFunctionArgs as RRLoaderFunctionArgs, DataRouteMatch, Params, Location, ShouldRevalidateFunction } from "react-router-dom"; | ||
import type { LoaderFunction, SerializeFrom } from "@remix-run/server-runtime"; | ||
@@ -8,40 +7,57 @@ import type { AppData } from "./data"; | ||
import type { EntryRoute } from "./routes"; | ||
type RouteData = RouterState["loaderData"]; | ||
export interface RouteModules { | ||
[routeId: string]: RouteModule; | ||
[routeId: string]: RouteModule | undefined; | ||
} | ||
export interface RouteModule { | ||
CatchBoundary?: CatchBoundaryComponent; | ||
ErrorBoundary?: ErrorBoundaryComponent | V2_ErrorBoundaryComponent; | ||
clientAction?: ClientActionFunction; | ||
clientLoader?: ClientLoaderFunction; | ||
ErrorBoundary?: ErrorBoundaryComponent; | ||
HydrateFallback?: HydrateFallbackComponent; | ||
Layout?: LayoutComponent; | ||
default: RouteComponent; | ||
handle?: RouteHandle; | ||
links?: LinksFunction; | ||
meta?: V1_MetaFunction | V1_HtmlMetaDescriptor | V2_MetaFunction | V2_MetaDescriptor[]; | ||
meta?: MetaFunction; | ||
shouldRevalidate?: ShouldRevalidateFunction; | ||
} | ||
/** | ||
* A React component that is rendered when the server throws a Response. | ||
* | ||
* @deprecated Please enable the v2_errorBoundary flag | ||
* | ||
* @see https://remix.run/route/catch-boundary | ||
* A function that handles data mutations for a route on the client | ||
*/ | ||
export type CatchBoundaryComponent = ComponentType<{}>; | ||
export type ClientActionFunction = (args: ClientActionFunctionArgs) => ReturnType<RRActionFunction>; | ||
/** | ||
* A React component that is rendered when there is an error on a route. | ||
* | ||
* @deprecated Please enable the v2_errorBoundary flag | ||
* | ||
* @see https://remix.run/route/error-boundary | ||
* Arguments passed to a route `clientAction` function | ||
*/ | ||
export type ErrorBoundaryComponent = ComponentType<{ | ||
error: Error; | ||
}>; | ||
export type ClientActionFunctionArgs = RRActionFunctionArgs<undefined> & { | ||
serverAction: <T = AppData>() => Promise<SerializeFrom<T>>; | ||
}; | ||
/** | ||
* V2 version of the ErrorBoundary that eliminates the distinction between | ||
* Error and Catch Boundaries and behaves like RR 6.4 errorElement and captures | ||
* errors with useRouteError() | ||
* A function that loads data for a route on the client | ||
*/ | ||
export type V2_ErrorBoundaryComponent = ComponentType; | ||
export type ClientLoaderFunction = ((args: ClientLoaderFunctionArgs) => ReturnType<RRLoaderFunction>) & { | ||
hydrate?: boolean; | ||
}; | ||
/** | ||
* Arguments passed to a route `clientLoader` function | ||
*/ | ||
export type ClientLoaderFunctionArgs = RRLoaderFunctionArgs<undefined> & { | ||
serverLoader: <T = AppData>() => Promise<SerializeFrom<T>>; | ||
}; | ||
/** | ||
* ErrorBoundary to display for this route | ||
*/ | ||
export type ErrorBoundaryComponent = ComponentType; | ||
/** | ||
* `<Route HydrateFallback>` component to render on initial loads | ||
* when client loaders are present | ||
*/ | ||
export type HydrateFallbackComponent = ComponentType; | ||
/** | ||
* Optional, root-only `<Route Layout>` component to wrap the root content in. | ||
* Useful for defining the <html>/<head>/<body> document shell shared by the | ||
* Component, HydrateFallback, and ErrorBoundary | ||
*/ | ||
export type LayoutComponent = ComponentType<{ | ||
children: ReactElement<unknown, ErrorBoundaryComponent | HydrateFallbackComponent | RouteComponent>; | ||
}>; | ||
/** | ||
* A function that defines `<link>` tags to be inserted into the `<head>` of | ||
@@ -55,55 +71,25 @@ * the document on route transitions. | ||
} | ||
/** | ||
* A function that returns an object of name + content pairs to use for | ||
* `<meta>` tags for a route. These tags will be merged with (and take | ||
* precedence over) tags from parent routes. | ||
* | ||
* @see https://remix.run/route/meta | ||
*/ | ||
export interface V1_MetaFunction { | ||
(args: { | ||
data: AppData; | ||
parentsData: RouteData; | ||
params: Params; | ||
location: Location; | ||
}): HtmlMetaDescriptor; | ||
} | ||
export type MetaFunction = V1_MetaFunction; | ||
export interface RouteMatchWithMeta extends DataRouteMatch { | ||
meta: V2_MetaDescriptor[]; | ||
} | ||
export interface V2_MetaMatch<RouteId extends string = string, Loader extends LoaderFunction | unknown = unknown> { | ||
export interface MetaMatch<RouteId extends string = string, Loader extends LoaderFunction | unknown = unknown> { | ||
id: RouteId; | ||
pathname: DataRouteMatch["pathname"]; | ||
data: Loader extends LoaderFunction ? SerializeFrom<Loader> : unknown; | ||
handle?: unknown; | ||
handle?: RouteHandle; | ||
params: DataRouteMatch["params"]; | ||
meta: V2_MetaDescriptor[]; | ||
meta: MetaDescriptor[]; | ||
error?: unknown; | ||
} | ||
export type V2_MetaMatches<MatchLoaders extends Record<string, unknown> = Record<string, unknown>> = Array<{ | ||
[K in keyof MatchLoaders]: V2_MetaMatch<Exclude<K, number | symbol>, MatchLoaders[K]>; | ||
export type MetaMatches<MatchLoaders extends Record<string, LoaderFunction | unknown> = Record<string, unknown>> = Array<{ | ||
[K in keyof MatchLoaders]: MetaMatch<Exclude<K, number | symbol>, MatchLoaders[K]>; | ||
}[keyof MatchLoaders]>; | ||
export interface V2_MetaArgs<Loader extends LoaderFunction | unknown = unknown, MatchLoaders extends Record<string, unknown> = Record<string, unknown>> { | ||
export interface MetaArgs<Loader extends LoaderFunction | unknown = unknown, MatchLoaders extends Record<string, LoaderFunction | unknown> = Record<string, unknown>> { | ||
data: (Loader extends LoaderFunction ? SerializeFrom<Loader> : AppData) | undefined; | ||
params: Params; | ||
location: Location; | ||
matches: V2_MetaMatches<MatchLoaders>; | ||
matches: MetaMatches<MatchLoaders>; | ||
error?: unknown; | ||
} | ||
export interface V2_MetaFunction<Loader extends LoaderFunction | unknown = unknown, MatchLoaders extends Record<string, unknown> = Record<string, unknown>> { | ||
(args: V2_MetaArgs<Loader, MatchLoaders>): V2_MetaDescriptor[] | undefined; | ||
export interface MetaFunction<Loader extends LoaderFunction | unknown = unknown, MatchLoaders extends Record<string, LoaderFunction | unknown> = Record<string, unknown>> { | ||
(args: MetaArgs<Loader, MatchLoaders>): MetaDescriptor[] | undefined; | ||
} | ||
/** | ||
* A name/content pair used to render `<meta>` tags in a meta function for a | ||
* route. The value can be either a string, which will render a single `<meta>` | ||
* tag, or an array of strings that will render multiple tags with the same | ||
* `name` attribute. | ||
*/ | ||
export interface V1_HtmlMetaDescriptor { | ||
charset?: "utf-8"; | ||
charSet?: "utf-8"; | ||
title?: string; | ||
[name: string]: null | string | undefined | Record<string, string> | Array<Record<string, string> | string>; | ||
} | ||
export type HtmlMetaDescriptor = V1_HtmlMetaDescriptor; | ||
export type V2_MetaDescriptor = { | ||
export type MetaDescriptor = { | ||
charSet: "utf-8"; | ||
@@ -146,25 +132,4 @@ } | { | ||
*/ | ||
export type RouteHandle = any; | ||
export type RouteHandle = unknown; | ||
export declare function loadRouteModule(route: EntryRoute, routeModulesCache: RouteModules): Promise<RouteModule>; | ||
/** | ||
* @deprecated The `unstable_shouldReload` function has been removed, so this | ||
* function will never run and route data will be revalidated on every request. | ||
* Please update the function name to `shouldRevalidate` and use the | ||
* `ShouldRevalidateFunction` interface. | ||
*/ | ||
export interface ShouldReloadFunction { | ||
(args: { | ||
url: URL; | ||
prevUrl: URL; | ||
params: Params; | ||
submission?: Submission; | ||
}): boolean; | ||
} | ||
interface Submission { | ||
action: string; | ||
method: string; | ||
formData: FormData; | ||
encType: string; | ||
key: string; | ||
} | ||
export {}; |
/** | ||
* @remix-run/react v0.0.0-nightly-ccefed3-20230621 | ||
* @remix-run/react v0.0.0-nightly-cd403b516-20240809 | ||
* | ||
@@ -34,48 +34,38 @@ * Copyright (c) Remix Software Inc. | ||
/** | ||
* A React component that is rendered when the server throws a Response. | ||
* | ||
* @deprecated Please enable the v2_errorBoundary flag | ||
* | ||
* @see https://remix.run/route/catch-boundary | ||
* A function that handles data mutations for a route on the client | ||
*/ | ||
/** | ||
* A React component that is rendered when there is an error on a route. | ||
* | ||
* @deprecated Please enable the v2_errorBoundary flag | ||
* | ||
* @see https://remix.run/route/error-boundary | ||
* Arguments passed to a route `clientAction` function | ||
*/ | ||
/** | ||
* V2 version of the ErrorBoundary that eliminates the distinction between | ||
* Error and Catch Boundaries and behaves like RR 6.4 errorElement and captures | ||
* errors with useRouteError() | ||
* A function that loads data for a route on the client | ||
*/ | ||
/** | ||
* A function that defines `<link>` tags to be inserted into the `<head>` of | ||
* the document on route transitions. | ||
* | ||
* @see https://remix.run/route/meta | ||
* Arguments passed to a route `clientLoader` function | ||
*/ | ||
/** | ||
* A function that returns an object of name + content pairs to use for | ||
* `<meta>` tags for a route. These tags will be merged with (and take | ||
* precedence over) tags from parent routes. | ||
* | ||
* @see https://remix.run/route/meta | ||
* ErrorBoundary to display for this route | ||
*/ | ||
// TODO: Replace in v2 | ||
/** | ||
* `<Route HydrateFallback>` component to render on initial loads | ||
* when client loaders are present | ||
*/ | ||
/** | ||
* A name/content pair used to render `<meta>` tags in a meta function for a | ||
* route. The value can be either a string, which will render a single `<meta>` | ||
* tag, or an array of strings that will render multiple tags with the same | ||
* `name` attribute. | ||
* Optional, root-only `<Route Layout>` component to wrap the root content in. | ||
* Useful for defining the <html>/<head>/<body> document shell shared by the | ||
* Component, HydrateFallback, and ErrorBoundary | ||
*/ | ||
// TODO: Replace in v2 | ||
/** | ||
* A function that defines `<link>` tags to be inserted into the `<head>` of | ||
* the document on route transitions. | ||
* | ||
* @see https://remix.run/route/meta | ||
*/ | ||
@@ -101,6 +91,23 @@ /** | ||
} catch (error) { | ||
// User got caught in the middle of a deploy and the CDN no longer has the | ||
// asset we're trying to import! Reload from the server and the user | ||
// (should) get the new manifest--unless the developer purged the static | ||
// assets, the manifest path, but not the documents 😬 | ||
// If we can't load the route it's likely one of 2 things: | ||
// - User got caught in the middle of a deploy and the CDN no longer has the | ||
// asset we're trying to import! Reload from the server and the user | ||
// (should) get the new manifest--unless the developer purged the static | ||
// assets, the manifest path, but not the documents 😬 | ||
// - Or, the asset trying to be imported has an error (usually in vite dev | ||
// mode), so the best we can do here is log the error for visibility | ||
// (via `Preserve log`) and reload | ||
// Log the error so it can be accessed via the `Preserve Log` setting | ||
console.error(`Error loading route module \`${route.module}\`, reloading page...`); | ||
console.error(error); | ||
if (window.__remixContext.isSpaMode && | ||
// @ts-expect-error | ||
typeof undefined !== "undefined") { | ||
// In SPA Mode (which implies vite) we don't want to perform a hard reload | ||
// on dev-time errors since it's a vite compilation error and a reload is | ||
// just going to fail with the same issue. Let the UI bubble to the error | ||
// boundary and let them see the error in the overlay or the dev server log | ||
throw error; | ||
} | ||
window.location.reload(); | ||
@@ -113,9 +120,2 @@ return new Promise(() => { | ||
/** | ||
* @deprecated The `unstable_shouldReload` function has been removed, so this | ||
* function will never run and route data will be revalidated on every request. | ||
* Please update the function name to `shouldRevalidate` and use the | ||
* `ShouldRevalidateFunction` interface. | ||
*/ | ||
exports.loadRouteModule = loadRouteModule; |
@@ -0,3 +1,4 @@ | ||
import type { HydrationState } from "@remix-run/router"; | ||
import type { DataRouteObject } from "react-router-dom"; | ||
import type { RouteModules } from "./routeModules"; | ||
import type { RouteModule, RouteModules } from "./routeModules"; | ||
import type { FutureConfig } from "./entry"; | ||
@@ -17,11 +18,16 @@ export interface RouteManifest<Route> { | ||
hasLoader: boolean; | ||
hasCatchBoundary: boolean; | ||
hasClientAction: boolean; | ||
hasClientLoader: boolean; | ||
hasErrorBoundary: boolean; | ||
imports?: string[]; | ||
css?: string[]; | ||
module: string; | ||
parentId?: string; | ||
} | ||
export declare function createServerRoutes(manifest: RouteManifest<EntryRoute>, routeModules: RouteModules, future: FutureConfig, parentId?: string, routesByParentId?: Record<string, Omit<EntryRoute, "children">[]>): DataRouteObject[]; | ||
export declare function createClientRoutesWithHMRRevalidationOptOut(needsRevalidation: Set<string>, manifest: RouteManifest<EntryRoute>, routeModulesCache: RouteModules, future: FutureConfig): DataRouteObject[]; | ||
export declare function createClientRoutes(manifest: RouteManifest<EntryRoute>, routeModulesCache: RouteModules, future: FutureConfig, parentId?: string, routesByParentId?: Record<string, Omit<EntryRoute, "children">[]>, needsRevalidation?: Set<string>): DataRouteObject[]; | ||
export declare function createServerRoutes(manifest: RouteManifest<EntryRoute>, routeModules: RouteModules, future: FutureConfig, isSpaMode: boolean, parentId?: string, routesByParentId?: Record<string, Omit<EntryRoute, "children">[]>, spaModeLazyPromise?: Promise<{ | ||
Component: () => null; | ||
}>): DataRouteObject[]; | ||
export declare function createClientRoutesWithHMRRevalidationOptOut(needsRevalidation: Set<string>, manifest: RouteManifest<EntryRoute>, routeModulesCache: RouteModules, initialState: HydrationState, future: FutureConfig, isSpaMode: boolean): DataRouteObject[]; | ||
export declare function createClientRoutes(manifest: RouteManifest<EntryRoute>, routeModulesCache: RouteModules, initialState: HydrationState | null, future: FutureConfig, isSpaMode: boolean, parentId?: string, routesByParentId?: Record<string, Omit<EntryRoute, "children">[]>, needsRevalidation?: Set<string>): DataRouteObject[]; | ||
export declare function shouldHydrateRouteLoader(route: EntryRoute, routeModule: RouteModule, isSpaMode: boolean): boolean; | ||
export {}; |
/** | ||
* @remix-run/react v0.0.0-nightly-ccefed3-20230621 | ||
* @remix-run/react v0.0.0-nightly-cd403b516-20240809 | ||
* | ||
@@ -16,2 +16,3 @@ * Copyright (c) Remix Software Inc. | ||
var React = require('react'); | ||
var router = require('@remix-run/router'); | ||
var reactRouterDom = require('react-router-dom'); | ||
@@ -21,4 +22,5 @@ var routeModules = require('./routeModules.js'); | ||
var links = require('./links.js'); | ||
var errorBoundaries = require('./errorBoundaries.js'); | ||
var fallback = require('./fallback.js'); | ||
var invariant = require('./invariant.js'); | ||
var components = require('./components.js'); | ||
@@ -62,22 +64,61 @@ function _interopNamespace(e) { | ||
} | ||
function createServerRoutes(manifest, routeModules, future, parentId = "", routesByParentId = groupRoutesByParentId(manifest)) { | ||
function getRouteComponents(route, routeModule, isSpaMode) { | ||
let Component = getRouteModuleComponent(routeModule); | ||
// HydrateFallback can only exist on the root route in SPA Mode | ||
let HydrateFallback = routeModule.HydrateFallback && (!isSpaMode || route.id === "root") ? routeModule.HydrateFallback : route.id === "root" ? fallback.RemixRootDefaultHydrateFallback : undefined; | ||
let ErrorBoundary = routeModule.ErrorBoundary ? routeModule.ErrorBoundary : route.id === "root" ? () => /*#__PURE__*/React__namespace.createElement(errorBoundaries.RemixRootDefaultErrorBoundary, { | ||
error: reactRouterDom.useRouteError() | ||
}) : undefined; | ||
if (route.id === "root" && routeModule.Layout) { | ||
return { | ||
...(Component ? { | ||
element: /*#__PURE__*/React__namespace.createElement(routeModule.Layout, null, /*#__PURE__*/React__namespace.createElement(Component, null)) | ||
} : { | ||
Component | ||
}), | ||
...(ErrorBoundary ? { | ||
errorElement: /*#__PURE__*/React__namespace.createElement(routeModule.Layout, null, /*#__PURE__*/React__namespace.createElement(ErrorBoundary, null)) | ||
} : { | ||
ErrorBoundary | ||
}), | ||
...(HydrateFallback ? { | ||
hydrateFallbackElement: /*#__PURE__*/React__namespace.createElement(routeModule.Layout, null, /*#__PURE__*/React__namespace.createElement(HydrateFallback, null)) | ||
} : { | ||
HydrateFallback | ||
}) | ||
}; | ||
} | ||
return { | ||
Component, | ||
ErrorBoundary, | ||
HydrateFallback | ||
}; | ||
} | ||
function createServerRoutes(manifest, routeModules, future, isSpaMode, parentId = "", routesByParentId = groupRoutesByParentId(manifest), spaModeLazyPromise = Promise.resolve({ | ||
Component: () => null | ||
})) { | ||
return (routesByParentId[parentId] || []).map(route => { | ||
let hasErrorBoundary = future.v2_errorBoundary === true ? route.id === "root" || route.hasErrorBoundary : route.id === "root" || route.hasCatchBoundary || route.hasErrorBoundary; | ||
let routeModule = routeModules[route.id]; | ||
invariant(routeModule, "No `routeModule` available to create server routes"); | ||
let dataRoute = { | ||
...getRouteComponents(route, routeModule, isSpaMode), | ||
caseSensitive: route.caseSensitive, | ||
element: /*#__PURE__*/React__namespace.createElement(components.RemixRoute, { | ||
id: route.id | ||
}), | ||
errorElement: hasErrorBoundary ? /*#__PURE__*/React__namespace.createElement(components.RemixRouteError, { | ||
id: route.id | ||
}) : undefined, | ||
id: route.id, | ||
index: route.index, | ||
path: route.path, | ||
handle: routeModules[route.id].handle | ||
// Note: we don't need loader/action/shouldRevalidate on these routes | ||
// since they're for a static render | ||
handle: routeModule.handle, | ||
// For SPA Mode, all routes are lazy except root. However we tell the | ||
// router root is also lazy here too since we don't need a full | ||
// implementation - we just need a `lazy` prop to tell the RR rendering | ||
// where to stop which is always at the root route in SPA mode | ||
lazy: isSpaMode ? () => spaModeLazyPromise : undefined, | ||
// For partial hydration rendering, we need to indicate when the route | ||
// has a loader/clientLoader, but it won't ever be called during the static | ||
// render, so just give it a no-op function so we can render down to the | ||
// proper fallback | ||
loader: route.hasLoader || route.hasClientLoader ? () => null : undefined | ||
// We don't need action/shouldRevalidate on these routes since they're | ||
// for a static render | ||
}; | ||
let children = createServerRoutes(manifest, routeModules, future, route.id, routesByParentId); | ||
let children = createServerRoutes(manifest, routeModules, future, isSpaMode, route.id, routesByParentId, spaModeLazyPromise); | ||
if (children.length > 0) dataRoute.children = children; | ||
@@ -87,24 +128,213 @@ return dataRoute; | ||
} | ||
function createClientRoutes(manifest, routeModulesCache, future, parentId = "", routesByParentId = groupRoutesByParentId(manifest), needsRevalidation) { | ||
function createClientRoutesWithHMRRevalidationOptOut(needsRevalidation, manifest, routeModulesCache, initialState, future, isSpaMode) { | ||
return createClientRoutes(manifest, routeModulesCache, initialState, future, isSpaMode, "", groupRoutesByParentId(manifest), needsRevalidation); | ||
} | ||
function preventInvalidServerHandlerCall(type, route, isSpaMode) { | ||
if (isSpaMode) { | ||
let fn = type === "action" ? "serverAction()" : "serverLoader()"; | ||
let msg = `You cannot call ${fn} in SPA Mode (routeId: "${route.id}")`; | ||
console.error(msg); | ||
throw new router.UNSAFE_ErrorResponseImpl(400, "Bad Request", new Error(msg), true); | ||
} | ||
let fn = type === "action" ? "serverAction()" : "serverLoader()"; | ||
let msg = `You are trying to call ${fn} on a route that does not have a server ` + `${type} (routeId: "${route.id}")`; | ||
if (type === "loader" && !route.hasLoader || type === "action" && !route.hasAction) { | ||
console.error(msg); | ||
throw new router.UNSAFE_ErrorResponseImpl(400, "Bad Request", new Error(msg), true); | ||
} | ||
} | ||
function noActionDefinedError(type, routeId) { | ||
let article = type === "clientAction" ? "a" : "an"; | ||
let msg = `Route "${routeId}" does not have ${article} ${type}, but you are trying to ` + `submit to it. To fix this, please add ${article} \`${type}\` function to the route`; | ||
console.error(msg); | ||
throw new router.UNSAFE_ErrorResponseImpl(405, "Method Not Allowed", new Error(msg), true); | ||
} | ||
function createClientRoutes(manifest, routeModulesCache, initialState, future, isSpaMode, parentId = "", routesByParentId = groupRoutesByParentId(manifest), needsRevalidation) { | ||
return (routesByParentId[parentId] || []).map(route => { | ||
let hasErrorBoundary = future.v2_errorBoundary === true ? route.id === "root" || route.hasErrorBoundary : route.id === "root" || route.hasCatchBoundary || route.hasErrorBoundary; | ||
let routeModule = routeModulesCache[route.id]; | ||
// Fetch data from the server either via single fetch or the standard `?_data` | ||
// request. Unwrap it when called via `serverLoader`/`serverAction` in a | ||
// client handler, otherwise return the raw response for the router to unwrap | ||
async function fetchServerHandlerAndMaybeUnwrap(request, unwrap, singleFetch) { | ||
if (typeof singleFetch === "function") { | ||
let result = await singleFetch(); | ||
return result; | ||
} | ||
let result = await fetchServerHandler(request, route); | ||
return unwrap ? unwrapServerResponse(result) : result; | ||
} | ||
function fetchServerLoader(request, unwrap, singleFetch) { | ||
if (!route.hasLoader) return Promise.resolve(null); | ||
return fetchServerHandlerAndMaybeUnwrap(request, unwrap, singleFetch); | ||
} | ||
function fetchServerAction(request, unwrap, singleFetch) { | ||
if (!route.hasAction) { | ||
throw noActionDefinedError("action", route.id); | ||
} | ||
return fetchServerHandlerAndMaybeUnwrap(request, unwrap, singleFetch); | ||
} | ||
async function prefetchStylesAndCallHandler(handler) { | ||
// Only prefetch links if we exist in the routeModulesCache (critical modules | ||
// and navigating back to pages previously loaded via route.lazy). Initial | ||
// execution of route.lazy (when the module is not in the cache) will handle | ||
// prefetching style links via loadRouteModuleWithBlockingLinks. | ||
let cachedModule = routeModulesCache[route.id]; | ||
let linkPrefetchPromise = cachedModule ? links.prefetchStyleLinks(route, cachedModule) : Promise.resolve(); | ||
try { | ||
return handler(); | ||
} finally { | ||
await linkPrefetchPromise; | ||
} | ||
} | ||
let dataRoute = { | ||
caseSensitive: route.caseSensitive, | ||
element: /*#__PURE__*/React__namespace.createElement(components.RemixRoute, { | ||
id: route.id | ||
}), | ||
errorElement: hasErrorBoundary ? /*#__PURE__*/React__namespace.createElement(components.RemixRouteError, { | ||
id: route.id | ||
}) : undefined, | ||
id: route.id, | ||
index: route.index, | ||
path: route.path, | ||
// handle gets added in via useMatches since we aren't guaranteed to | ||
// have the route module available here | ||
handle: undefined, | ||
loader: createDataFunction(route, routeModulesCache, false), | ||
action: createDataFunction(route, routeModulesCache, true), | ||
shouldRevalidate: createShouldRevalidate(route, routeModulesCache, needsRevalidation) | ||
path: route.path | ||
}; | ||
let children = createClientRoutes(manifest, routeModulesCache, future, route.id, routesByParentId, needsRevalidation); | ||
if (routeModule) { | ||
var _initialState$loaderD, _initialState$errors, _routeModule$clientLo; | ||
// Use critical path modules directly | ||
Object.assign(dataRoute, { | ||
...dataRoute, | ||
...getRouteComponents(route, routeModule, isSpaMode), | ||
handle: routeModule.handle, | ||
shouldRevalidate: needsRevalidation ? wrapShouldRevalidateForHdr(route.id, routeModule.shouldRevalidate, needsRevalidation) : routeModule.shouldRevalidate | ||
}); | ||
let initialData = initialState === null || initialState === void 0 ? void 0 : (_initialState$loaderD = initialState.loaderData) === null || _initialState$loaderD === void 0 ? void 0 : _initialState$loaderD[route.id]; | ||
let initialError = initialState === null || initialState === void 0 ? void 0 : (_initialState$errors = initialState.errors) === null || _initialState$errors === void 0 ? void 0 : _initialState$errors[route.id]; | ||
let isHydrationRequest = needsRevalidation == null && (((_routeModule$clientLo = routeModule.clientLoader) === null || _routeModule$clientLo === void 0 ? void 0 : _routeModule$clientLo.hydrate) === true || !route.hasLoader); | ||
dataRoute.loader = async ({ | ||
request, | ||
params | ||
}, singleFetch) => { | ||
try { | ||
let result = await prefetchStylesAndCallHandler(async () => { | ||
invariant(routeModule, "No `routeModule` available for critical-route loader"); | ||
if (!routeModule.clientLoader) { | ||
if (isSpaMode) return null; | ||
// Call the server when no client loader exists | ||
return fetchServerLoader(request, false, singleFetch); | ||
} | ||
return routeModule.clientLoader({ | ||
request, | ||
params, | ||
async serverLoader() { | ||
preventInvalidServerHandlerCall("loader", route, isSpaMode); | ||
// On the first call, resolve with the server result | ||
if (isHydrationRequest) { | ||
if (initialError !== undefined) { | ||
throw initialError; | ||
} | ||
return initialData; | ||
} | ||
// Call the server loader for client-side navigations | ||
return fetchServerLoader(request, true, singleFetch); | ||
} | ||
}); | ||
}); | ||
return result; | ||
} finally { | ||
// Whether or not the user calls `serverLoader`, we only let this | ||
// stick around as true for one loader call | ||
isHydrationRequest = false; | ||
} | ||
}; | ||
// Let React Router know whether to run this on hydration | ||
dataRoute.loader.hydrate = shouldHydrateRouteLoader(route, routeModule, isSpaMode); | ||
dataRoute.action = ({ | ||
request, | ||
params | ||
}, singleFetch) => { | ||
return prefetchStylesAndCallHandler(async () => { | ||
invariant(routeModule, "No `routeModule` available for critical-route action"); | ||
if (!routeModule.clientAction) { | ||
if (isSpaMode) { | ||
throw noActionDefinedError("clientAction", route.id); | ||
} | ||
return fetchServerAction(request, false, singleFetch); | ||
} | ||
return routeModule.clientAction({ | ||
request, | ||
params, | ||
async serverAction() { | ||
preventInvalidServerHandlerCall("action", route, isSpaMode); | ||
return fetchServerAction(request, true, singleFetch); | ||
} | ||
}); | ||
}); | ||
}; | ||
} else { | ||
// If the lazy route does not have a client loader/action we want to call | ||
// the server loader/action in parallel with the module load so we add | ||
// loader/action as static props on the route | ||
if (!route.hasClientLoader) { | ||
dataRoute.loader = ({ | ||
request | ||
}, singleFetch) => prefetchStylesAndCallHandler(() => { | ||
if (isSpaMode) return Promise.resolve(null); | ||
return fetchServerLoader(request, false, singleFetch); | ||
}); | ||
} | ||
if (!route.hasClientAction) { | ||
dataRoute.action = ({ | ||
request | ||
}, singleFetch) => prefetchStylesAndCallHandler(() => { | ||
if (isSpaMode) { | ||
throw noActionDefinedError("clientAction", route.id); | ||
} | ||
return fetchServerAction(request, false, singleFetch); | ||
}); | ||
} | ||
// Load all other modules via route.lazy() | ||
dataRoute.lazy = async () => { | ||
let mod = await loadRouteModuleWithBlockingLinks(route, routeModulesCache); | ||
let lazyRoute = { | ||
...mod | ||
}; | ||
if (mod.clientLoader) { | ||
let clientLoader = mod.clientLoader; | ||
lazyRoute.loader = (args, singleFetch) => clientLoader({ | ||
...args, | ||
async serverLoader() { | ||
preventInvalidServerHandlerCall("loader", route, isSpaMode); | ||
return fetchServerLoader(args.request, true, singleFetch); | ||
} | ||
}); | ||
} | ||
if (mod.clientAction) { | ||
let clientAction = mod.clientAction; | ||
lazyRoute.action = (args, singleFetch) => clientAction({ | ||
...args, | ||
async serverAction() { | ||
preventInvalidServerHandlerCall("action", route, isSpaMode); | ||
return fetchServerAction(args.request, true, singleFetch); | ||
} | ||
}); | ||
} | ||
if (needsRevalidation) { | ||
lazyRoute.shouldRevalidate = wrapShouldRevalidateForHdr(route.id, mod.shouldRevalidate, needsRevalidation); | ||
} | ||
return { | ||
...(lazyRoute.loader ? { | ||
loader: lazyRoute.loader | ||
} : {}), | ||
...(lazyRoute.action ? { | ||
action: lazyRoute.action | ||
} : {}), | ||
hasErrorBoundary: lazyRoute.hasErrorBoundary, | ||
shouldRevalidate: lazyRoute.shouldRevalidate, | ||
handle: lazyRoute.handle, | ||
// No need to wrap these in layout since the root route is never | ||
// loaded via route.lazy() | ||
Component: lazyRoute.Component, | ||
ErrorBoundary: lazyRoute.ErrorBoundary | ||
}; | ||
}; | ||
} | ||
let children = createClientRoutes(manifest, routeModulesCache, initialState, future, isSpaMode, route.id, routesByParentId, needsRevalidation); | ||
if (children.length > 0) dataRoute.children = children; | ||
@@ -114,19 +344,13 @@ return dataRoute; | ||
} | ||
function createShouldRevalidate(route, routeModules, needsRevalidation) { | ||
// When an HMR / HDR update happens we opt out of all user-defined | ||
// revalidation logic and force a revalidation on the first call | ||
function wrapShouldRevalidateForHdr(routeId, routeShouldRevalidate, needsRevalidation) { | ||
let handledRevalidation = false; | ||
return function (arg) { | ||
let module = routeModules[route.id]; | ||
invariant(module, `Expected route module to be loaded for ${route.id}`); | ||
// When an HMR / HDR update happens we opt out of all user-defined | ||
// revalidation logic and the do as the dev server tells us the first | ||
// time router.revalidate() is called. | ||
if (needsRevalidation !== undefined && !handledRevalidation) { | ||
return arg => { | ||
if (!handledRevalidation) { | ||
handledRevalidation = true; | ||
return needsRevalidation.has(route.id); | ||
return needsRevalidation.has(routeId); | ||
} | ||
if (module.shouldRevalidate) { | ||
return module.shouldRevalidate(arg); | ||
} | ||
return arg.defaultShouldRevalidate; | ||
return routeShouldRevalidate ? routeShouldRevalidate(arg) : arg.defaultShouldRevalidate; | ||
}; | ||
@@ -136,36 +360,48 @@ } | ||
let routeModule = await routeModules.loadRouteModule(route, routeModules$1); | ||
await links.prefetchStyleLinks(routeModule); | ||
return routeModule; | ||
await links.prefetchStyleLinks(route, routeModule); | ||
// Include all `browserSafeRouteExports` fields, except `HydrateFallback` | ||
// since those aren't used on lazily loaded routes | ||
return { | ||
Component: getRouteModuleComponent(routeModule), | ||
ErrorBoundary: routeModule.ErrorBoundary, | ||
clientAction: routeModule.clientAction, | ||
clientLoader: routeModule.clientLoader, | ||
handle: routeModule.handle, | ||
links: routeModule.links, | ||
meta: routeModule.meta, | ||
shouldRevalidate: routeModule.shouldRevalidate | ||
}; | ||
} | ||
function createDataFunction(route, routeModules, isAction) { | ||
return async ({ | ||
request | ||
}) => { | ||
let routeModulePromise = loadRouteModuleWithBlockingLinks(route, routeModules); | ||
try { | ||
if (isAction && !route.hasAction) { | ||
let msg = `Route "${route.id}" does not have an action, but you are trying ` + `to submit to it. To fix this, please add an \`action\` function to the route`; | ||
console.error(msg); | ||
throw new Error(msg); | ||
} else if (!isAction && !route.hasLoader) { | ||
return null; | ||
} | ||
let result = await data.fetchData(request, route.id); | ||
if (result instanceof Error) { | ||
throw result; | ||
} | ||
if (data.isRedirectResponse(result)) { | ||
throw getRedirect(result); | ||
} | ||
if (data.isCatchResponse(result)) { | ||
throw result; | ||
} | ||
if (data.isDeferredResponse(result) && result.body) { | ||
return await data.parseDeferredReadableStream(result.body); | ||
} | ||
return result; | ||
} finally { | ||
await routeModulePromise; | ||
async function fetchServerHandler(request, route) { | ||
let result = await data.fetchData(request, route.id); | ||
if (result instanceof Error) { | ||
throw result; | ||
} | ||
if (data.isRedirectResponse(result)) { | ||
throw getRedirect(result); | ||
} | ||
if (data.isCatchResponse(result)) { | ||
throw result; | ||
} | ||
if (data.isDeferredResponse(result) && result.body) { | ||
return await data.parseDeferredReadableStream(result.body); | ||
} | ||
return result; | ||
} | ||
function unwrapServerResponse(result) { | ||
if (data.isDeferredData(result)) { | ||
return result.data; | ||
} | ||
if (data.isResponse(result)) { | ||
let contentType = result.headers.get("Content-Type"); | ||
// Check between word boundaries instead of startsWith() due to the last | ||
// paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type | ||
if (contentType && /\bapplication\/json\b/.test(contentType)) { | ||
return result.json(); | ||
} else { | ||
return result.text(); | ||
} | ||
}; | ||
} | ||
return result; | ||
} | ||
@@ -180,2 +416,10 @@ function getRedirect(response) { | ||
} | ||
let reloadDocument = response.headers.get("X-Remix-Reload-Document"); | ||
if (reloadDocument) { | ||
headers["X-Remix-Reload-Document"] = reloadDocument; | ||
} | ||
let replace = response.headers.get("X-Remix-Replace"); | ||
if (replace) { | ||
headers["X-Remix-Replace"] = replace; | ||
} | ||
return reactRouterDom.redirect(url, { | ||
@@ -187,3 +431,20 @@ status, | ||
// Our compiler generates the default export as `{}` when no default is provided, | ||
// which can lead us to trying to use that as a Component in RR and calling | ||
// createElement on it. Patching here as a quick fix and hoping it's no longer | ||
// an issue in Vite. | ||
function getRouteModuleComponent(routeModule) { | ||
if (routeModule.default == null) return undefined; | ||
let isEmptyObject = typeof routeModule.default === "object" && Object.keys(routeModule.default).length === 0; | ||
if (!isEmptyObject) { | ||
return routeModule.default; | ||
} | ||
} | ||
function shouldHydrateRouteLoader(route, routeModule, isSpaMode) { | ||
return isSpaMode && route.id !== "root" || routeModule.clientLoader != null && (routeModule.clientLoader.hydrate === true || route.hasLoader !== true); | ||
} | ||
exports.createClientRoutes = createClientRoutes; | ||
exports.createClientRoutesWithHMRRevalidationOptOut = createClientRoutesWithHMRRevalidationOptOut; | ||
exports.createServerRoutes = createServerRoutes; | ||
exports.shouldHydrateRouteLoader = shouldHydrateRouteLoader; |
@@ -0,1 +1,2 @@ | ||
import * as React from "react"; | ||
import type { ScrollRestorationProps as ScrollRestorationPropsRR } from "react-router-dom"; | ||
@@ -11,2 +12,2 @@ import type { ScriptProps } from "./components"; | ||
getKey?: ScrollRestorationPropsRR["getKey"]; | ||
}): JSX.Element; | ||
}): React.JSX.Element | null; |
/** | ||
* @remix-run/react v0.0.0-nightly-ccefed3-20230621 | ||
* @remix-run/react v0.0.0-nightly-cd403b516-20240809 | ||
* | ||
@@ -52,4 +52,7 @@ * Copyright (c) Remix Software Inc. | ||
}) { | ||
let { | ||
isSpaMode | ||
} = components.useRemixContext(); | ||
let location = reactRouterDom.useLocation(); | ||
let matches = components.useMatches(); | ||
let matches = reactRouterDom.useMatches(); | ||
reactRouterDom.UNSAFE_useScrollRestoration({ | ||
@@ -74,2 +77,8 @@ getKey, | ||
[]); | ||
// In SPA Mode, there's nothing to restore on initial render since we didn't | ||
// render anything on the server | ||
if (isSpaMode) { | ||
return null; | ||
} | ||
let restoreScroll = ((STORAGE_KEY, restoreKey) => { | ||
@@ -76,0 +85,0 @@ if (!window.history.state || !window.history.state.key) { |
@@ -7,2 +7,3 @@ import type { ReactElement } from "react"; | ||
abortDelay?: number; | ||
nonce?: string; | ||
} | ||
@@ -14,2 +15,2 @@ /** | ||
*/ | ||
export declare function RemixServer({ context, url, abortDelay, }: RemixServerProps): ReactElement; | ||
export declare function RemixServer({ context, url, abortDelay, nonce, }: RemixServerProps): ReactElement; |
/** | ||
* @remix-run/react v0.0.0-nightly-ccefed3-20230621 | ||
* @remix-run/react v0.0.0-nightly-cd403b516-20240809 | ||
* | ||
@@ -20,2 +20,3 @@ * Copyright (c) Remix Software Inc. | ||
var routes = require('./routes.js'); | ||
var singleFetch = require('./single-fetch.js'); | ||
@@ -50,3 +51,4 @@ function _interopNamespace(e) { | ||
url, | ||
abortDelay | ||
abortDelay, | ||
nonce | ||
}) { | ||
@@ -59,17 +61,47 @@ if (typeof url === "string") { | ||
routeModules, | ||
criticalCss, | ||
serverHandoffString | ||
} = context; | ||
let routes$1 = routes.createServerRoutes(manifest.routes, routeModules, context.future); | ||
let router = server.createStaticRouter(routes$1, context.staticHandlerContext); | ||
return /*#__PURE__*/React__namespace.createElement(components.RemixContext.Provider, { | ||
let routes$1 = routes.createServerRoutes(manifest.routes, routeModules, context.future, context.isSpaMode); | ||
// Create a shallow clone of `loaderData` we can mutate for partial hydration. | ||
// When a route exports a `clientLoader` and a `HydrateFallback`, we want to | ||
// render the fallback on the server so we clear our the `loaderData` during SSR. | ||
// Is it important not to change the `context` reference here since we use it | ||
// for context._deepestRenderedBoundaryId tracking | ||
context.staticHandlerContext.loaderData = { | ||
...context.staticHandlerContext.loaderData | ||
}; | ||
for (let match of context.staticHandlerContext.matches) { | ||
let routeId = match.route.id; | ||
let route = routeModules[routeId]; | ||
let manifestRoute = context.manifest.routes[routeId]; | ||
// Clear out the loaderData to avoid rendering the route component when the | ||
// route opted into clientLoader hydration and either: | ||
// * gave us a HydrateFallback | ||
// * or doesn't have a server loader and we have no data to render | ||
if (route && routes.shouldHydrateRouteLoader(manifestRoute, route, context.isSpaMode) && (route.HydrateFallback || !manifestRoute.hasLoader)) { | ||
context.staticHandlerContext.loaderData[routeId] = undefined; | ||
} | ||
} | ||
let router = server.createStaticRouter(routes$1, context.staticHandlerContext, { | ||
future: { | ||
v7_partialHydration: true, | ||
v7_relativeSplatPath: context.future.v3_relativeSplatPath | ||
} | ||
}); | ||
return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(components.RemixContext.Provider, { | ||
value: { | ||
manifest, | ||
routeModules, | ||
criticalCss, | ||
serverHandoffString, | ||
future: context.future, | ||
abortDelay | ||
isSpaMode: context.isSpaMode, | ||
serializeError: context.serializeError, | ||
abortDelay, | ||
renderMeta: context.renderMeta | ||
} | ||
}, /*#__PURE__*/React__namespace.createElement(errorBoundaries.RemixErrorBoundary, { | ||
location: router.state.location, | ||
component: errorBoundaries.RemixRootDefaultErrorBoundary | ||
location: router.state.location | ||
}, /*#__PURE__*/React__namespace.createElement(server.StaticRouterProvider, { | ||
@@ -79,5 +111,11 @@ router: router, | ||
hydrate: false | ||
}))); | ||
}))), context.future.unstable_singleFetch && context.serverHandoffStream ? /*#__PURE__*/React__namespace.createElement(React__namespace.Suspense, null, /*#__PURE__*/React__namespace.createElement(singleFetch.StreamTransfer, { | ||
context: context, | ||
identifier: 0, | ||
reader: context.serverHandoffStream.getReader(), | ||
textDecoder: new TextDecoder(), | ||
nonce: nonce | ||
})) : null); | ||
} | ||
exports.RemixServer = RemixServer; |
export declare function warnOnce(condition: boolean, message: string): void; | ||
export declare function logDeprecationOnce(message: string, key?: string): void; |
MIT License | ||
Copyright (c) Remix Software Inc. 2020-2021 | ||
Copyright (c) Shopify Inc. 2022-2023 | ||
Copyright (c) Shopify Inc. 2022-2024 | ||
@@ -6,0 +6,0 @@ Permission is hereby granted, free of charge, to any person obtaining a copy |
{ | ||
"name": "@remix-run/react", | ||
"version": "0.0.0-nightly-ccefed3-20230621", | ||
"version": "0.0.0-nightly-cd403b516-20240809", | ||
"description": "React DOM bindings for Remix", | ||
@@ -19,27 +19,42 @@ "bugs": { | ||
"dependencies": { | ||
"@remix-run/router": "1.6.3", | ||
"react-router-dom": "6.13.0" | ||
"@remix-run/router": "1.19.0", | ||
"@remix-run/server-runtime": "0.0.0-nightly-cd403b516-20240809", | ||
"react-router": "6.26.0", | ||
"react-router-dom": "6.26.0", | ||
"turbo-stream": "2.2.0" | ||
}, | ||
"devDependencies": { | ||
"@remix-run/server-runtime": "0.0.0-nightly-ccefed3-20230621", | ||
"@testing-library/jest-dom": "^5.16.2", | ||
"@remix-run/node": "0.0.0-nightly-cd403b516-20240809", | ||
"@remix-run/react": "0.0.0-nightly-cd403b516-20240809", | ||
"@testing-library/jest-dom": "^5.17.0", | ||
"@testing-library/react": "^13.3.0", | ||
"@types/react": "^18.0.15", | ||
"abort-controller": "^3.0.0", | ||
"@types/react": "^18.2.20", | ||
"jest-environment-jsdom": "^29.6.4", | ||
"react": "^18.2.0", | ||
"react-dom": "^18.2.0" | ||
"react-dom": "^18.2.0", | ||
"typescript": "^5.1.6" | ||
}, | ||
"peerDependencies": { | ||
"react": ">=16.8.0", | ||
"react-dom": ">=16.8.0" | ||
"react": "^18.0.0", | ||
"react-dom": "^18.0.0", | ||
"typescript": "^5.1.0" | ||
}, | ||
"peerDependenciesMeta": { | ||
"typescript": { | ||
"optional": true | ||
} | ||
}, | ||
"engines": { | ||
"node": ">=14.0.0" | ||
"node": ">=18.0.0" | ||
}, | ||
"files": [ | ||
"dist/", | ||
"future/", | ||
"CHANGELOG.md", | ||
"LICENSE.md", | ||
"README.md" | ||
] | ||
} | ||
], | ||
"scripts": { | ||
"tsc": "tsc" | ||
} | ||
} |
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
56
7509
302167
8
9
6
+ Added@remix-run/server-runtime@0.0.0-nightly-cd403b516-20240809
+ Addedreact-router@6.26.0
+ Addedturbo-stream@2.2.0
+ Added@remix-run/router@1.19.0(transitive)
+ Added@remix-run/server-runtime@0.0.0-nightly-cd403b516-20240809(transitive)
+ Added@types/cookie@0.6.0(transitive)
+ Added@web3-storage/multipart-parser@1.0.0(transitive)
+ Addedcookie@0.6.0(transitive)
+ Addedjs-tokens@4.0.0(transitive)
+ Addedloose-envify@1.4.0(transitive)
+ Addedreact@18.3.1(transitive)
+ Addedreact-dom@18.3.1(transitive)
+ Addedreact-router@6.26.0(transitive)
+ Addedreact-router-dom@6.26.0(transitive)
+ Addedscheduler@0.23.2(transitive)
+ Addedset-cookie-parser@2.7.1(transitive)
+ Addedsource-map@0.7.4(transitive)
+ Addedturbo-stream@2.2.0(transitive)
+ Addedtypescript@5.7.3(transitive)
- Removed@remix-run/router@1.6.3(transitive)
- Removedreact@19.0.0(transitive)
- Removedreact-dom@19.0.0(transitive)
- Removedreact-router@6.13.0(transitive)
- Removedreact-router-dom@6.13.0(transitive)
- Removedscheduler@0.25.0(transitive)
Updated@remix-run/router@1.19.0
Updatedreact-router-dom@6.26.0