@remix-run/react
Advanced tools
Comparing version 0.0.0-nightly-910b900-20231207 to 0.0.0-nightly-91184a743-20240823
/** | ||
* @remix-run/react v0.0.0-nightly-910b900-20231207 | ||
* @remix-run/react v0.0.0-nightly-91184a743-20240823 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
@@ -1,11 +0,14 @@ | ||
import { type HydrationState, type Router } 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"; | ||
declare global { | ||
var __remixContext: { | ||
url: string; | ||
basename?: string; | ||
state: HydrationState; | ||
criticalCss?: string; | ||
future: FutureConfig; | ||
isSpaMode: boolean; | ||
stream: ReadableStream<Uint8Array> | undefined; | ||
streamController: ReadableStreamDefaultController<Uint8Array>; | ||
a?: number; | ||
@@ -19,5 +22,5 @@ dev?: { | ||
var __remixRouteModules: RouteModules; | ||
var __remixManifest: EntryContext["manifest"]; | ||
var __remixManifest: AssetsManifest; | ||
var __remixRevalidation: number | undefined; | ||
var __remixClearCriticalCss: () => void; | ||
var __remixClearCriticalCss: (() => void) | undefined; | ||
var $RefreshRuntime$: { | ||
@@ -29,7 +32,2 @@ performReactRefresh: () => void; | ||
} | ||
declare global { | ||
interface ImportMeta { | ||
hot: any; | ||
} | ||
} | ||
/** | ||
@@ -36,0 +34,0 @@ * The entry point for a Remix app when it is rendered in the browser (in |
/** | ||
* @remix-run/react v0.0.0-nightly-910b900-20231207 | ||
* @remix-run/react v0.0.0-nightly-91184a743-20240823 | ||
* | ||
@@ -23,2 +23,5 @@ * Copyright (c) Remix Software Inc. | ||
var routes = require('./routes.js'); | ||
var singleFetch = require('./single-fetch.js'); | ||
var invariant = require('./invariant.js'); | ||
var fogOfWar = require('./fog-of-war.js'); | ||
@@ -49,4 +52,4 @@ function _interopNamespace(e) { | ||
let stateDecodingPromise; | ||
let router; | ||
let didServerRenderFallback = false; | ||
let routerInitialized = false; | ||
@@ -66,4 +69,2 @@ let hmrRouterReadyResolve; | ||
}); | ||
// The critical CSS can only be cleared, so the reducer always returns undefined | ||
let criticalCssReducer = () => undefined; | ||
@@ -77,55 +78,67 @@ /** | ||
if (!router) { | ||
// 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) { | ||
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); | ||
// 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. | ||
let hydrationData = { | ||
...window.__remixContext.state, | ||
loaderData: { | ||
...window.__remixContext.state.loaderData | ||
} | ||
}; | ||
let initialMatches = reactRouterDom.matchRoutes(routes$1, window.location); | ||
if (initialMatches) { | ||
for (let match of initialMatches) { | ||
let routeId = match.route.id; | ||
let route = window.__remixRouteModules[routeId]; | ||
let manifestRoute = window.__remixManifest.routes[routeId]; | ||
if (route && route.clientLoader && route.HydrateFallback) { | ||
hydrationData.loaderData[routeId] = undefined; | ||
didServerRenderFallback = true; | ||
} 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; | ||
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 = { | ||
...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); | ||
} | ||
} | ||
if (hydrationData && hydrationData.errors) { | ||
hydrationData.errors = errors.deserializeErrors(hydrationData.errors); | ||
} | ||
@@ -137,2 +150,3 @@ // We don't use createBrowserRouter here because we need fine-grained control | ||
history: router$1.createBrowserHistory(), | ||
basename: window.__remixContext.basename, | ||
future: { | ||
@@ -143,11 +157,16 @@ v7_normalizeFormMethod: true, | ||
v7_prependBasename: true, | ||
v7_relativeSplatPath: window.__remixContext.future.v3_relativeSplatPath | ||
v7_relativeSplatPath: window.__remixContext.future.v3_relativeSplatPath, | ||
// Single fetch enables this underlying behavior | ||
v7_skipActionErrorRevalidation: window.__remixContext.future.unstable_singleFetch === true | ||
}, | ||
hydrationData, | ||
mapRouteProperties: reactRouter.UNSAFE_mapRouteProperties | ||
mapRouteProperties: reactRouter.UNSAFE_mapRouteProperties, | ||
unstable_dataStrategy: window.__remixContext.future.unstable_singleFetch ? singleFetch.getSingleFetchDataStrategy(window.__remixManifest, window.__remixRouteModules) : undefined, | ||
unstable_patchRoutesOnNavigation: fogOfWar.getPatchRoutesOnNavigationFunction(window.__remixManifest, window.__remixRouteModules, window.__remixContext.future, window.__remixContext.isSpaMode, window.__remixContext.basename) | ||
}); | ||
// As long as we didn't SSR a `HydrateFallback`, we can initialize immediately since | ||
// there's no initial client-side data loading to perform | ||
if (!didServerRenderFallback) { | ||
// 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(); | ||
@@ -169,18 +188,17 @@ } | ||
// server HTML. This allows our HMR logic to clear the critical CSS state. | ||
// eslint-disable-next-line react-hooks/rules-of-hooks | ||
let [criticalCss, clearCriticalCss] = React__namespace.useReducer(criticalCssReducer, window.__remixContext.criticalCss); | ||
window.__remixClearCriticalCss = clearCriticalCss; | ||
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 rendered a `HydrateFallback` on the server, delay initialization until | ||
// after we've hydrated with the `HydrateFallback` in case the client loaders | ||
// are synchronous | ||
if (didServerRenderFallback && !routerInitialized) { | ||
// 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; | ||
@@ -190,4 +208,2 @@ router.initialize(); | ||
}, []); | ||
// eslint-disable-next-line react-hooks/rules-of-hooks | ||
React__namespace.useLayoutEffect(() => { | ||
@@ -200,2 +216,3 @@ return router.subscribe(newState => { | ||
}, [location]); | ||
fogOfWar.useFogOFWarDiscovery(router, window.__remixManifest, window.__remixRouteModules, window.__remixContext.future, window.__remixContext.isSpaMode); | ||
@@ -206,20 +223,26 @@ // We need to include a wrapper RemixErrorBoundary here in case the root error | ||
// out of there | ||
return /*#__PURE__*/React__namespace.createElement(components.RemixContext.Provider, { | ||
value: { | ||
manifest: window.__remixManifest, | ||
routeModules: window.__remixRouteModules, | ||
future: window.__remixContext.future, | ||
criticalCss | ||
} | ||
}, /*#__PURE__*/React__namespace.createElement(errorBoundaries.RemixErrorBoundary, { | ||
location: location | ||
}, /*#__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 { UIMatch as UIMatchRR } from "@remix-run/router"; | ||
import type { FetcherWithComponents, LinkProps, NavLinkProps } from "react-router-dom"; | ||
import type { FetcherWithComponents, FormProps, LinkProps, NavLinkProps } from "react-router-dom"; | ||
import { useFetcher as useFetcherRR } from "react-router-dom"; | ||
@@ -11,3 +11,11 @@ import type { SerializeFrom } from "@remix-run/server-runtime"; | ||
export declare const RemixContext: React.Context<RemixContextObject | undefined>; | ||
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: | ||
@@ -22,5 +30,7 @@ * | ||
export interface RemixLinkProps extends LinkProps { | ||
discover?: DiscoverBehavior; | ||
prefetch?: PrefetchBehavior; | ||
} | ||
export interface RemixNavLinkProps extends NavLinkProps { | ||
discover?: DiscoverBehavior; | ||
prefetch?: PrefetchBehavior; | ||
@@ -43,2 +53,13 @@ } | ||
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; | ||
@@ -125,3 +146,3 @@ /** | ||
*/ | ||
export declare const LiveReload: (() => null) | (({ origin, port, timeoutMs, nonce, }: { | ||
export declare const LiveReload: ({ origin, port, timeoutMs, nonce, }: { | ||
origin?: string | undefined; | ||
@@ -131,2 +152,2 @@ port?: number | undefined; | ||
nonce?: string | undefined; | ||
}) => React.JSX.Element); | ||
}) => React.JSX.Element | null; |
/** | ||
* @remix-run/react v0.0.0-nightly-910b900-20231207 | ||
* @remix-run/react v0.0.0-nightly-91184a743-20240823 | ||
* | ||
@@ -21,2 +21,4 @@ * Copyright (c) Remix Software Inc. | ||
var markup = require('./markup.js'); | ||
var singleFetch = require('./single-fetch.js'); | ||
var fogOfWar = require('./fog-of-war.js'); | ||
@@ -69,2 +71,9 @@ function _interopNamespace(e) { | ||
/** | ||
* 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: | ||
@@ -77,2 +86,3 @@ * | ||
*/ | ||
function usePrefetchBehavior(prefetch, theirElementProps) { | ||
@@ -138,2 +148,5 @@ 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; | ||
} | ||
@@ -148,2 +161,3 @@ /** | ||
prefetch = "none", | ||
discover = "render", | ||
...props | ||
@@ -156,3 +170,4 @@ }, forwardedRef) => { | ||
ref: mergeRefs(forwardedRef, ref), | ||
to: to | ||
to: to, | ||
"data-discover": getDiscoverAttr(discover, isAbsolute, props.reloadDocument) | ||
})), shouldPrefetch && !isAbsolute ? /*#__PURE__*/React__namespace.createElement(PrefetchPageLinks, { | ||
@@ -173,2 +188,3 @@ page: href | ||
prefetch = "none", | ||
discover = "render", | ||
...props | ||
@@ -181,3 +197,4 @@ }, forwardedRef) => { | ||
ref: mergeRefs(forwardedRef, ref), | ||
to: to | ||
to: to, | ||
"data-discover": getDiscoverAttr(discover, isAbsolute, props.reloadDocument) | ||
})), shouldPrefetch && !isAbsolute ? /*#__PURE__*/React__namespace.createElement(PrefetchPageLinks, { | ||
@@ -188,2 +205,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) { | ||
@@ -198,2 +232,18 @@ return event => { | ||
// 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; | ||
} | ||
/** | ||
@@ -206,2 +256,3 @@ * Renders the `<link>` tags for the current routes. | ||
let { | ||
isSpaMode, | ||
manifest, | ||
@@ -215,3 +266,3 @@ routeModules, | ||
} = useDataRouterStateContext(); | ||
let matches = errors ? routerMatches.slice(0, routerMatches.findIndex(m => errors[m.route.id]) + 1) : routerMatches; | ||
let matches = getActiveMatches(routerMatches, errors, isSpaMode); | ||
let keyedLinks = React__namespace.useMemo(() => links.getKeyedLinksForMatches(matches, routeModules, manifest), [matches, routeModules, manifest]); | ||
@@ -248,3 +299,3 @@ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, criticalCss ? /*#__PURE__*/React__namespace.createElement("style", { | ||
} = 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) { | ||
@@ -285,3 +336,5 @@ console.warn(`Tried to prefetch ${page} but no routes matched.`); | ||
let { | ||
manifest | ||
future, | ||
manifest, | ||
routeModules | ||
} = useRemixContext(); | ||
@@ -299,9 +352,25 @@ let { | ||
let keyedPrefetchLinks = useKeyedPrefetchLinks(newMatchesForAssets); | ||
return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, dataHrefs.map(href => /*#__PURE__*/React__namespace.createElement("link", _rollupPluginBabelHelpers["extends"]({ | ||
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", | ||
@@ -328,2 +397,3 @@ href: href | ||
let { | ||
isSpaMode, | ||
routeModules | ||
@@ -337,8 +407,6 @@ } = useRemixContext(); | ||
let location = reactRouterDom.useLocation(); | ||
let _matches = routerMatches; | ||
let _matches = getActiveMatches(routerMatches, errors, isSpaMode); | ||
let error = null; | ||
if (errors) { | ||
let errorIdx = routerMatches.findIndex(m => errors[m.route.id]); | ||
_matches = routerMatches.slice(0, errorIdx + 1); | ||
error = errors[routerMatches[errorIdx].route.id]; | ||
error = errors[_matches[_matches.length - 1].route.id]; | ||
} | ||
@@ -467,3 +535,6 @@ let meta = []; | ||
abortDelay, | ||
serializeError | ||
serializeError, | ||
isSpaMode, | ||
future, | ||
renderMeta | ||
} = useRemixContext(); | ||
@@ -476,5 +547,12 @@ 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(() => { | ||
@@ -522,4 +600,10 @@ isHydrated = true; | ||
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 | ||
@@ -562,4 +646,7 @@ // deferred scripts. | ||
}).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(",")}}; | ||
@@ -593,14 +680,3 @@ | ||
} | ||
// 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]; | ||
@@ -610,7 +686,7 @@ 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 | ||
}), /*#__PURE__*/React__namespace.createElement("link", { | ||
}) : null, /*#__PURE__*/React__namespace.createElement("link", { | ||
rel: "modulepreload", | ||
@@ -749,3 +825,3 @@ href: manifest.entry.module, | ||
process.env.NODE_ENV !== "development" ? () => null : function LiveReload({ | ||
origin = process.env.REMIX_DEV_ORIGIN, | ||
origin, | ||
port, | ||
@@ -755,2 +831,3 @@ timeoutMs = 1000, | ||
}) { | ||
origin ??= process.env.REMIX_DEV_ORIGIN; | ||
let js = String.raw; | ||
@@ -865,2 +942,3 @@ return /*#__PURE__*/React__namespace.createElement("script", { | ||
exports.Await = Await; | ||
exports.Form = Form; | ||
exports.Link = Link; | ||
@@ -879,2 +957,3 @@ exports.Links = Links; | ||
exports.useMatches = useMatches; | ||
exports.useRemixContext = useRemixContext; | ||
exports.useRouteLoaderData = useRouteLoaderData; |
@@ -14,2 +14,3 @@ import { UNSAFE_DeferredData as DeferredData } from "@remix-run/router"; | ||
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-910b900-20231207 | ||
* @remix-run/react v0.0.0-nightly-91184a743-20240823 | ||
* | ||
@@ -31,3 +31,3 @@ * Copyright (c) Remix Software Inc. | ||
// server, and instead receive a 4xx/5xx from somewhere in between (like | ||
// Cloudflare), then we get a false negative n the isErrorResponse check and | ||
// Cloudflare), then we get a false negative in the isErrorResponse check and | ||
// we incorrectly assume that the user returns the 4xx/5xx response and | ||
@@ -57,27 +57,2 @@ // consider it successful. To alleviate this, we add X-Remix-Response to any | ||
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"); | ||
// 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(); | ||
} | ||
} | ||
if (retry > 0) { | ||
@@ -88,2 +63,3 @@ // Retry up to 3 times waiting 50, 250, 1250 ms | ||
} | ||
let init = await createRequestInit(request); | ||
let revalidation = window.__remixRevalidation; | ||
@@ -110,2 +86,30 @@ let response = await fetch(url.href, init).catch(error => { | ||
} | ||
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:"; | ||
@@ -271,2 +275,3 @@ async function parseDeferredReadableStream(stream) { | ||
exports.createRequestInit = createRequestInit; | ||
exports.fetchData = fetchData; | ||
@@ -273,0 +278,0 @@ exports.isCatchResponse = isCatchResponse; |
@@ -14,7 +14,19 @@ import type { StaticHandlerContext } from "@remix-run/router"; | ||
future: FutureConfig; | ||
isSpaMode: boolean; | ||
abortDelay?: number; | ||
serializeError?(error: Error): SerializedError; | ||
renderMeta?: { | ||
didRenderScripts?: boolean; | ||
streamCache?: Record<number, Promise<void> & { | ||
result?: { | ||
done: boolean; | ||
value: string; | ||
}; | ||
error?: unknown; | ||
}>; | ||
}; | ||
} | ||
export interface EntryContext extends RemixContextObject { | ||
staticHandlerContext: StaticHandlerContext; | ||
serverHandoffStream?: ReadableStream<Uint8Array>; | ||
} | ||
@@ -24,2 +36,4 @@ export interface FutureConfig { | ||
v3_relativeSplatPath: boolean; | ||
unstable_lazyRouteDiscovery: boolean; | ||
unstable_singleFetch: boolean; | ||
} | ||
@@ -26,0 +40,0 @@ export interface AssetsManifest { |
@@ -5,2 +5,3 @@ import * as React from "react"; | ||
location: Location; | ||
isOutsideRemixApp?: boolean; | ||
error?: Error; | ||
@@ -26,5 +27,12 @@ }>; | ||
*/ | ||
export declare function RemixRootDefaultErrorBoundary({ error }: { | ||
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-910b900-20231207 | ||
* @remix-run/react v0.0.0-nightly-91184a743-20240823 | ||
* | ||
@@ -17,2 +17,3 @@ * Copyright (c) Remix Software Inc. | ||
var reactRouterDom = require('react-router-dom'); | ||
var components = require('./components.js'); | ||
@@ -81,3 +82,4 @@ function _interopNamespace(e) { | ||
return /*#__PURE__*/React__namespace.createElement(RemixRootDefaultErrorBoundary, { | ||
error: this.state.error | ||
error: this.state.error, | ||
isOutsideRemixApp: true | ||
}); | ||
@@ -94,5 +96,15 @@ } else { | ||
function RemixRootDefaultErrorBoundary({ | ||
error | ||
error, | ||
isOutsideRemixApp | ||
}) { | ||
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." | ||
); | ||
` | ||
} | ||
}); | ||
if (reactRouterDom.isRouteErrorResponse(error)) { | ||
@@ -103,6 +115,5 @@ return /*#__PURE__*/React__namespace.createElement(BoundaryShell, { | ||
style: { | ||
fontFamily: "system-ui, sans-serif", | ||
padding: "2rem" | ||
fontSize: "24px" | ||
} | ||
}, error.status, " ", error.statusText)); | ||
}, error.status, " ", error.statusText), heyDeveloper); | ||
} | ||
@@ -117,8 +128,4 @@ let errorInstance; | ||
return /*#__PURE__*/React__namespace.createElement(BoundaryShell, { | ||
title: "Application Error!" | ||
}, /*#__PURE__*/React__namespace.createElement("main", { | ||
style: { | ||
fontFamily: "system-ui, sans-serif", | ||
padding: "2rem" | ||
} | ||
title: "Application Error!", | ||
isOutsideRemixApp: isOutsideRemixApp | ||
}, /*#__PURE__*/React__namespace.createElement("h1", { | ||
@@ -135,8 +142,34 @@ style: { | ||
} | ||
}, errorInstance.stack))); | ||
}, errorInstance.stack), heyDeveloper); | ||
} | ||
function BoundaryShell({ | ||
title, | ||
renderScripts, | ||
isOutsideRemixApp, | ||
children | ||
}) { | ||
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__namespace.createElement("html", { | ||
@@ -149,14 +182,12 @@ lang: "en" | ||
content: "width=device-width,initial-scale=1,viewport-fit=cover" | ||
}), /*#__PURE__*/React__namespace.createElement("title", null, title)), /*#__PURE__*/React__namespace.createElement("body", null, children, /*#__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__namespace.createElement("title", null, title)), /*#__PURE__*/React__namespace.createElement("body", null, /*#__PURE__*/React__namespace.createElement("main", { | ||
style: { | ||
fontFamily: "system-ui, sans-serif", | ||
padding: "2rem" | ||
} | ||
}))); | ||
}, children, renderScripts ? /*#__PURE__*/React__namespace.createElement(components.Scripts, null) : null))); | ||
} | ||
exports.BoundaryShell = BoundaryShell; | ||
exports.RemixErrorBoundary = RemixErrorBoundary; | ||
exports.RemixRootDefaultErrorBoundary = RemixRootDefaultErrorBoundary; |
/** | ||
* @remix-run/react v0.0.0-nightly-910b900-20231207 | ||
* @remix-run/react v0.0.0-nightly-91184a743-20240823 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
/** | ||
* @remix-run/react v0.0.0-nightly-910b900-20231207 | ||
* @remix-run/react v0.0.0-nightly-91184a743-20240823 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
/** | ||
* @remix-run/react v0.0.0-nightly-910b900-20231207 | ||
* @remix-run/react v0.0.0-nightly-91184a743-20240823 | ||
* | ||
@@ -18,3 +18,6 @@ * Copyright (c) Remix Software Inc. | ||
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 { getPatchRoutesOnNavigationFunction, useFogOFWarDiscovery } from './fog-of-war.js'; | ||
@@ -25,4 +28,4 @@ /* eslint-disable prefer-let/prefer-let */ | ||
let stateDecodingPromise; | ||
let router; | ||
let didServerRenderFallback = false; | ||
let routerInitialized = false; | ||
@@ -43,5 +46,6 @@ let hmrAbortController; | ||
}); | ||
// The critical CSS can only be cleared, so the reducer always returns undefined | ||
let criticalCssReducer = () => undefined; | ||
// @ts-expect-error | ||
if (import.meta && import.meta.hot) { | ||
// @ts-expect-error | ||
import.meta.hot.accept("remix:manifest", async ({ | ||
@@ -82,3 +86,3 @@ assetsManifest, | ||
// Create new routes | ||
let routes = createClientRoutesWithHMRRevalidationOptOut(needsRevalidation, assetsManifest.routes, window.__remixRouteModules, window.__remixContext.state, window.__remixContext.future); | ||
let routes = createClientRoutesWithHMRRevalidationOptOut(needsRevalidation, assetsManifest.routes, window.__remixRouteModules, window.__remixContext.state, window.__remixContext.future, window.__remixContext.isSpaMode); | ||
@@ -115,55 +119,67 @@ // This is temporary API and will be more granular before release | ||
if (!router) { | ||
// 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) { | ||
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); | ||
// 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. | ||
let hydrationData = { | ||
...window.__remixContext.state, | ||
loaderData: { | ||
...window.__remixContext.state.loaderData | ||
} | ||
}; | ||
let initialMatches = matchRoutes(routes, window.location); | ||
if (initialMatches) { | ||
for (let match of initialMatches) { | ||
let routeId = match.route.id; | ||
let route = window.__remixRouteModules[routeId]; | ||
let manifestRoute = window.__remixManifest.routes[routeId]; | ||
if (route && route.clientLoader && route.HydrateFallback) { | ||
hydrationData.loaderData[routeId] = undefined; | ||
didServerRenderFallback = true; | ||
} 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; | ||
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 = { | ||
...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); | ||
} | ||
} | ||
if (hydrationData && hydrationData.errors) { | ||
hydrationData.errors = deserializeErrors(hydrationData.errors); | ||
} | ||
@@ -175,2 +191,3 @@ // We don't use createBrowserRouter here because we need fine-grained control | ||
history: createBrowserHistory(), | ||
basename: window.__remixContext.basename, | ||
future: { | ||
@@ -181,11 +198,16 @@ v7_normalizeFormMethod: true, | ||
v7_prependBasename: true, | ||
v7_relativeSplatPath: window.__remixContext.future.v3_relativeSplatPath | ||
v7_relativeSplatPath: window.__remixContext.future.v3_relativeSplatPath, | ||
// Single fetch enables this underlying behavior | ||
v7_skipActionErrorRevalidation: window.__remixContext.future.unstable_singleFetch === true | ||
}, | ||
hydrationData, | ||
mapRouteProperties: UNSAFE_mapRouteProperties | ||
mapRouteProperties: UNSAFE_mapRouteProperties, | ||
unstable_dataStrategy: window.__remixContext.future.unstable_singleFetch ? getSingleFetchDataStrategy(window.__remixManifest, window.__remixRouteModules) : undefined, | ||
unstable_patchRoutesOnNavigation: getPatchRoutesOnNavigationFunction(window.__remixManifest, window.__remixRouteModules, window.__remixContext.future, window.__remixContext.isSpaMode, window.__remixContext.basename) | ||
}); | ||
// As long as we didn't SSR a `HydrateFallback`, we can initialize immediately since | ||
// there's no initial client-side data loading to perform | ||
if (!didServerRenderFallback) { | ||
// 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(); | ||
@@ -207,18 +229,17 @@ } | ||
// server HTML. This allows our HMR logic to clear the critical CSS state. | ||
// eslint-disable-next-line react-hooks/rules-of-hooks | ||
let [criticalCss, clearCriticalCss] = React.useReducer(criticalCssReducer, window.__remixContext.criticalCss); | ||
window.__remixClearCriticalCss = clearCriticalCss; | ||
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 rendered a `HydrateFallback` on the server, delay initialization until | ||
// after we've hydrated with the `HydrateFallback` in case the client loaders | ||
// are synchronous | ||
if (didServerRenderFallback && !routerInitialized) { | ||
// 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; | ||
@@ -228,4 +249,2 @@ router.initialize(); | ||
}, []); | ||
// eslint-disable-next-line react-hooks/rules-of-hooks | ||
React.useLayoutEffect(() => { | ||
@@ -238,2 +257,3 @@ return router.subscribe(newState => { | ||
}, [location]); | ||
useFogOFWarDiscovery(router, window.__remixManifest, window.__remixRouteModules, window.__remixContext.future, window.__remixContext.isSpaMode); | ||
@@ -244,20 +264,26 @@ // We need to include a wrapper RemixErrorBoundary here in case the root error | ||
// out of there | ||
return /*#__PURE__*/React.createElement(RemixContext.Provider, { | ||
value: { | ||
manifest: window.__remixManifest, | ||
routeModules: window.__remixRouteModules, | ||
future: window.__remixContext.future, | ||
criticalCss | ||
} | ||
}, /*#__PURE__*/React.createElement(RemixErrorBoundary, { | ||
location: location | ||
}, /*#__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-910b900-20231207 | ||
* @remix-run/react v0.0.0-nightly-91184a743-20240823 | ||
* | ||
@@ -13,6 +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, useRouteLoaderData as useRouteLoaderData$1, useActionData as useActionData$1, useFetcher as useFetcher$1, UNSAFE_DataRouterContext, UNSAFE_DataRouterStateContext } from 'react-router-dom'; | ||
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 { getKeyedLinksForMatches, isPageLinkDescriptor, getNewMatchesForLinks, getDataLinkHrefs, getModuleLinkHrefs, getKeyedPrefetchLinks } from './links.js'; | ||
import { escapeHtml, createHtml } from './markup.js'; | ||
import { addRevalidationParam, singleFetchUrl } from './single-fetch.js'; | ||
import { isFogOfWarEnabled, getPartialManifest } from './fog-of-war.js'; | ||
@@ -45,2 +47,9 @@ function useDataRouterContext() { | ||
/** | ||
* 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: | ||
@@ -53,2 +62,3 @@ * | ||
*/ | ||
function usePrefetchBehavior(prefetch, theirElementProps) { | ||
@@ -114,2 +124,5 @@ 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; | ||
} | ||
@@ -124,2 +137,3 @@ /** | ||
prefetch = "none", | ||
discover = "render", | ||
...props | ||
@@ -132,3 +146,4 @@ }, forwardedRef) => { | ||
ref: mergeRefs(forwardedRef, ref), | ||
to: to | ||
to: to, | ||
"data-discover": getDiscoverAttr(discover, isAbsolute, props.reloadDocument) | ||
})), shouldPrefetch && !isAbsolute ? /*#__PURE__*/React.createElement(PrefetchPageLinks, { | ||
@@ -149,2 +164,3 @@ page: href | ||
prefetch = "none", | ||
discover = "render", | ||
...props | ||
@@ -157,3 +173,4 @@ }, forwardedRef) => { | ||
ref: mergeRefs(forwardedRef, ref), | ||
to: to | ||
to: to, | ||
"data-discover": getDiscoverAttr(discover, isAbsolute, props.reloadDocument) | ||
})), shouldPrefetch && !isAbsolute ? /*#__PURE__*/React.createElement(PrefetchPageLinks, { | ||
@@ -164,2 +181,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) { | ||
@@ -174,2 +208,18 @@ return event => { | ||
// 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; | ||
} | ||
/** | ||
@@ -182,2 +232,3 @@ * Renders the `<link>` tags for the current routes. | ||
let { | ||
isSpaMode, | ||
manifest, | ||
@@ -191,3 +242,3 @@ routeModules, | ||
} = useDataRouterStateContext(); | ||
let matches = errors ? routerMatches.slice(0, routerMatches.findIndex(m => errors[m.route.id]) + 1) : routerMatches; | ||
let matches = getActiveMatches(routerMatches, errors, isSpaMode); | ||
let keyedLinks = React.useMemo(() => getKeyedLinksForMatches(matches, routeModules, manifest), [matches, routeModules, manifest]); | ||
@@ -224,3 +275,3 @@ return /*#__PURE__*/React.createElement(React.Fragment, null, criticalCss ? /*#__PURE__*/React.createElement("style", { | ||
} = 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) { | ||
@@ -261,3 +312,5 @@ console.warn(`Tried to prefetch ${page} but no routes matched.`); | ||
let { | ||
manifest | ||
future, | ||
manifest, | ||
routeModules | ||
} = useRemixContext(); | ||
@@ -275,9 +328,25 @@ let { | ||
let keyedPrefetchLinks = useKeyedPrefetchLinks(newMatchesForAssets); | ||
return /*#__PURE__*/React.createElement(React.Fragment, null, dataHrefs.map(href => /*#__PURE__*/React.createElement("link", _extends({ | ||
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", | ||
@@ -304,2 +373,3 @@ href: href | ||
let { | ||
isSpaMode, | ||
routeModules | ||
@@ -313,8 +383,6 @@ } = useRemixContext(); | ||
let location = useLocation(); | ||
let _matches = routerMatches; | ||
let _matches = getActiveMatches(routerMatches, errors, isSpaMode); | ||
let error = null; | ||
if (errors) { | ||
let errorIdx = routerMatches.findIndex(m => errors[m.route.id]); | ||
_matches = routerMatches.slice(0, errorIdx + 1); | ||
error = errors[routerMatches[errorIdx].route.id]; | ||
error = errors[_matches[_matches.length - 1].route.id]; | ||
} | ||
@@ -443,3 +511,6 @@ let meta = []; | ||
abortDelay, | ||
serializeError | ||
serializeError, | ||
isSpaMode, | ||
future, | ||
renderMeta | ||
} = useRemixContext(); | ||
@@ -452,5 +523,12 @@ 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(() => { | ||
@@ -498,4 +576,10 @@ isHydrated = true; | ||
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 | ||
@@ -538,4 +622,7 @@ // deferred scripts. | ||
}).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(",")}}; | ||
@@ -569,14 +656,3 @@ | ||
} | ||
// 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]; | ||
@@ -586,7 +662,7 @@ 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 | ||
}), /*#__PURE__*/React.createElement("link", { | ||
}) : null, /*#__PURE__*/React.createElement("link", { | ||
rel: "modulepreload", | ||
@@ -725,3 +801,3 @@ href: manifest.entry.module, | ||
process.env.NODE_ENV !== "development" ? () => null : function LiveReload({ | ||
origin = process.env.REMIX_DEV_ORIGIN, | ||
origin, | ||
port, | ||
@@ -731,2 +807,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; | ||
@@ -840,2 +923,2 @@ return /*#__PURE__*/React.createElement("script", { | ||
export { Await, Link, Links, LiveReload, Meta, NavLink, PrefetchPageLinks, RemixContext, Scripts, composeEventHandlers, useActionData, useFetcher, useLoaderData, useMatches, useRouteLoaderData }; | ||
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-910b900-20231207 | ||
* @remix-run/react v0.0.0-nightly-91184a743-20240823 | ||
* | ||
@@ -27,3 +27,3 @@ * Copyright (c) Remix Software Inc. | ||
// server, and instead receive a 4xx/5xx from somewhere in between (like | ||
// Cloudflare), then we get a false negative n the isErrorResponse check and | ||
// Cloudflare), then we get a false negative in the isErrorResponse check and | ||
// we incorrectly assume that the user returns the 4xx/5xx response and | ||
@@ -53,27 +53,2 @@ // consider it successful. To alleviate this, we add X-Remix-Response to any | ||
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"); | ||
// 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(); | ||
} | ||
} | ||
if (retry > 0) { | ||
@@ -84,2 +59,3 @@ // Retry up to 3 times waiting 50, 250, 1250 ms | ||
} | ||
let init = await createRequestInit(request); | ||
let revalidation = window.__remixRevalidation; | ||
@@ -106,2 +82,30 @@ let response = await fetch(url.href, init).catch(error => { | ||
} | ||
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:"; | ||
@@ -267,2 +271,2 @@ async function parseDeferredReadableStream(stream) { | ||
export { fetchData, isCatchResponse, isDeferredData, isDeferredResponse, isErrorResponse, isNetworkErrorResponse, isRedirectResponse, isResponse, parseDeferredReadableStream }; | ||
export { createRequestInit, fetchData, isCatchResponse, isDeferredData, isDeferredResponse, isErrorResponse, isNetworkErrorResponse, isRedirectResponse, isResponse, parseDeferredReadableStream }; |
/** | ||
* @remix-run/react v0.0.0-nightly-910b900-20231207 | ||
* @remix-run/react v0.0.0-nightly-91184a743-20240823 | ||
* | ||
@@ -13,2 +13,3 @@ * Copyright (c) Remix Software Inc. | ||
import { isRouteErrorResponse } from 'react-router-dom'; | ||
import { useRemixContext, Scripts } from './components.js'; | ||
@@ -57,3 +58,4 @@ class RemixErrorBoundary extends React.Component { | ||
return /*#__PURE__*/React.createElement(RemixRootDefaultErrorBoundary, { | ||
error: this.state.error | ||
error: this.state.error, | ||
isOutsideRemixApp: true | ||
}); | ||
@@ -70,5 +72,15 @@ } else { | ||
function RemixRootDefaultErrorBoundary({ | ||
error | ||
error, | ||
isOutsideRemixApp | ||
}) { | ||
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." | ||
); | ||
` | ||
} | ||
}); | ||
if (isRouteErrorResponse(error)) { | ||
@@ -79,6 +91,5 @@ return /*#__PURE__*/React.createElement(BoundaryShell, { | ||
style: { | ||
fontFamily: "system-ui, sans-serif", | ||
padding: "2rem" | ||
fontSize: "24px" | ||
} | ||
}, error.status, " ", error.statusText)); | ||
}, error.status, " ", error.statusText), heyDeveloper); | ||
} | ||
@@ -93,8 +104,4 @@ let errorInstance; | ||
return /*#__PURE__*/React.createElement(BoundaryShell, { | ||
title: "Application Error!" | ||
}, /*#__PURE__*/React.createElement("main", { | ||
style: { | ||
fontFamily: "system-ui, sans-serif", | ||
padding: "2rem" | ||
} | ||
title: "Application Error!", | ||
isOutsideRemixApp: isOutsideRemixApp | ||
}, /*#__PURE__*/React.createElement("h1", { | ||
@@ -111,8 +118,34 @@ style: { | ||
} | ||
}, errorInstance.stack))); | ||
}, errorInstance.stack), heyDeveloper); | ||
} | ||
function BoundaryShell({ | ||
title, | ||
renderScripts, | ||
isOutsideRemixApp, | ||
children | ||
}) { | ||
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.createElement("html", { | ||
@@ -125,13 +158,10 @@ lang: "en" | ||
content: "width=device-width,initial-scale=1,viewport-fit=cover" | ||
}), /*#__PURE__*/React.createElement("title", null, title)), /*#__PURE__*/React.createElement("body", null, children, /*#__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.createElement("title", null, title)), /*#__PURE__*/React.createElement("body", null, /*#__PURE__*/React.createElement("main", { | ||
style: { | ||
fontFamily: "system-ui, sans-serif", | ||
padding: "2rem" | ||
} | ||
}))); | ||
}, children, renderScripts ? /*#__PURE__*/React.createElement(Scripts, null) : null))); | ||
} | ||
export { RemixErrorBoundary, RemixRootDefaultErrorBoundary }; | ||
export { BoundaryShell, RemixErrorBoundary, RemixRootDefaultErrorBoundary }; |
/** | ||
* @remix-run/react v0.0.0-nightly-910b900-20231207 | ||
* @remix-run/react v0.0.0-nightly-91184a743-20240823 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
/** | ||
* @remix-run/react v0.0.0-nightly-910b900-20231207 | ||
* @remix-run/react v0.0.0-nightly-91184a743-20240823 | ||
* | ||
@@ -12,3 +12,3 @@ * Copyright (c) Remix Software Inc. | ||
import * as React from 'react'; | ||
import { Scripts } from './components.js'; | ||
import { BoundaryShell } from './errorBoundaries.js'; | ||
@@ -20,10 +20,6 @@ // If the user sets `clientLoader.hydrate=true` somewhere but does not | ||
function RemixRootDefaultHydrateFallback() { | ||
return /*#__PURE__*/React.createElement("html", { | ||
lang: "en" | ||
}, /*#__PURE__*/React.createElement("head", null, /*#__PURE__*/React.createElement("meta", { | ||
charSet: "utf-8" | ||
}), /*#__PURE__*/React.createElement("meta", { | ||
name: "viewport", | ||
content: "width=device-width,initial-scale=1,viewport-fit=cover" | ||
})), /*#__PURE__*/React.createElement("body", null, /*#__PURE__*/React.createElement(Scripts, null), /*#__PURE__*/React.createElement("script", { | ||
return /*#__PURE__*/React.createElement(BoundaryShell, { | ||
title: "Loading...", | ||
renderScripts: true | ||
}, /*#__PURE__*/React.createElement("script", { | ||
dangerouslySetInnerHTML: { | ||
@@ -33,10 +29,11 @@ __html: ` | ||
"💿 Hey developer 👋. You can provide a way better UX than this " + | ||
"when your app is running \`clientLoader\` functions on hydration. " + | ||
"Check out https://remix.run/route/hydrate-fallback for more information." | ||
"when your app is loading JS modules and/or running \`clientLoader\` " + | ||
"functions. Check out https://remix.run/route/hydrate-fallback " + | ||
"for more information." | ||
); | ||
` | ||
} | ||
}), " ")); | ||
})); | ||
} | ||
export { RemixRootDefaultHydrateFallback }; |
/** | ||
* @remix-run/react v0.0.0-nightly-910b900-20231207 | ||
* @remix-run/react v0.0.0-nightly-91184a743-20240823 | ||
* | ||
@@ -11,6 +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, createPath, generatePath, isRouteErrorResponse, matchPath, matchRoutes, parsePath, resolvePath, unstable_usePrompt, unstable_useViewTransitionState, useAsyncError, useAsyncValue, useBeforeUnload, useBlocker, useFetchers, useFormAction, useHref, useLocation, useMatch, useNavigate, useNavigation, useNavigationType, useOutlet, useOutletContext, useParams, useResolvedPath, useRevalidator, useRouteError, useSearchParams, useSubmit } from 'react-router-dom'; | ||
export { Await, Link, Links, LiveReload, Meta, NavLink, PrefetchPageLinks, Scripts, RemixContext as UNSAFE_RemixContext, useActionData, useFetcher, useLoaderData, useMatches, useRouteLoaderData } from './components.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-910b900-20231207 | ||
* @remix-run/react v0.0.0-nightly-91184a743-20240823 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
/** | ||
* @remix-run/react v0.0.0-nightly-910b900-20231207 | ||
* @remix-run/react v0.0.0-nightly-91184a743-20240823 | ||
* | ||
@@ -21,2 +21,3 @@ * Copyright (c) Remix Software Inc. | ||
//////////////////////////////////////////////////////////////////////////////// | ||
/** | ||
@@ -34,3 +35,3 @@ * Gets all the links for a set of matches. The modules are assumed to have been | ||
href | ||
})) : [], ((_module$links = module.links) === null || _module$links === void 0 ? void 0 : _module$links.call(module)) || []]; | ||
})) : [], (module === null || module === void 0 ? void 0 : (_module$links = module.links) === null || _module$links === void 0 ? void 0 : _module$links.call(module)) || []]; | ||
}).flat(2); | ||
@@ -173,3 +174,3 @@ let preloads = getCurrentPageModulePreloadHrefs(matches, manifest); | ||
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 { | ||
@@ -176,0 +177,0 @@ pathname, |
/** | ||
* @remix-run/react v0.0.0-nightly-910b900-20231207 | ||
* @remix-run/react v0.0.0-nightly-91184a743-20240823 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
/** | ||
* @remix-run/react v0.0.0-nightly-910b900-20231207 | ||
* @remix-run/react v0.0.0-nightly-91184a743-20240823 | ||
* | ||
@@ -37,2 +37,8 @@ * Copyright (c) Remix Software Inc. | ||
/** | ||
* 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 | ||
*/ | ||
/** | ||
* A function that defines `<link>` tags to be inserted into the `<head>` of | ||
@@ -63,6 +69,23 @@ * the document on route transitions. | ||
} 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(); | ||
@@ -69,0 +92,0 @@ return new Promise(() => { |
/** | ||
* @remix-run/react v0.0.0-nightly-910b900-20231207 | ||
* @remix-run/react v0.0.0-nightly-91184a743-20240823 | ||
* | ||
@@ -15,6 +15,7 @@ * Copyright (c) Remix Software Inc. | ||
import { loadRouteModule } from './routeModules.js'; | ||
import { isDeferredData, isResponse, 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'; | ||
@@ -38,16 +39,52 @@ // 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 routeModule = routeModules[route.id]; | ||
invariant(routeModule, "No `routeModule` available to create server routes"); | ||
let dataRoute = { | ||
...getRouteComponents(route, routeModule, isSpaMode), | ||
caseSensitive: route.caseSensitive, | ||
Component: getRouteModuleComponent(routeModule), | ||
HydrateFallback: routeModule.HydrateFallback ? routeModule.HydrateFallback : route.id === "root" ? RemixRootDefaultHydrateFallback : undefined, | ||
ErrorBoundary: routeModule.ErrorBoundary ? routeModule.ErrorBoundary : route.id === "root" ? () => /*#__PURE__*/React.createElement(RemixRootDefaultErrorBoundary, { | ||
error: useRouteError() | ||
}) : undefined, | ||
id: route.id, | ||
index: route.index, | ||
path: route.path, | ||
handle: routeModules[route.id].handle, | ||
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 | ||
@@ -61,4 +98,3 @@ // has a loader/clientLoader, but it won't ever be called during the static | ||
}; | ||
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; | ||
@@ -68,24 +104,57 @@ return dataRoute; | ||
} | ||
function createClientRoutesWithHMRRevalidationOptOut(needsRevalidation, manifest, routeModulesCache, initialState, future) { | ||
return createClientRoutes(manifest, routeModulesCache, initialState, 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, initialState, 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 routeModule = routeModulesCache[route.id]; | ||
async function fetchServerLoader(request) { | ||
if (!route.hasLoader) return null; | ||
return fetchServerHandler(request, route); | ||
// 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; | ||
} | ||
async function fetchServerAction(request) { | ||
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) { | ||
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 UNSAFE_ErrorResponseImpl(405, "Method Not Allowed", new Error(msg), true); | ||
throw noActionDefinedError("action", route.id); | ||
} | ||
return fetchServerHandler(request, route); | ||
return fetchServerHandlerAndMaybeUnwrap(request, unwrap, singleFetch); | ||
} | ||
async function prefetchStylesAndCallHandler(handler) { | ||
// Only prefetch links if we've been loaded into the cache, route.lazy | ||
// will handle initial loads | ||
let linkPrefetchPromise = routeModulesCache[route.id] ? prefetchStyleLinks(route, routeModulesCache[route.id]) : Promise.resolve(); | ||
// 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 { | ||
@@ -103,59 +172,65 @@ return handler(); | ||
if (routeModule) { | ||
var _initialState$loaderD, _initialState$errors, _routeModule$clientLo; | ||
// Use critical path modules directly | ||
Object.assign(dataRoute, { | ||
...dataRoute, | ||
Component: getRouteModuleComponent(routeModule), | ||
HydrateFallback: routeModule.HydrateFallback ? routeModule.HydrateFallback : route.id === "root" ? RemixRootDefaultHydrateFallback : undefined, | ||
ErrorBoundary: routeModule.ErrorBoundary ? routeModule.ErrorBoundary : route.id === "root" ? () => /*#__PURE__*/React.createElement(RemixRootDefaultErrorBoundary, { | ||
error: useRouteError() | ||
}) : undefined, | ||
...getRouteComponents(route, routeModule, isSpaMode), | ||
handle: routeModule.handle, | ||
shouldRevalidate: needsRevalidation ? wrapShouldRevalidateForHdr(route.id, routeModule.shouldRevalidate, needsRevalidation) : routeModule.shouldRevalidate | ||
}); | ||
let initialData = initialState && initialState.loaderData && initialState.loaderData[route.id]; | ||
let isHydrationRequest = needsRevalidation == null && routeModule.clientLoader != null && routeModule.clientLoader.hydrate === true; | ||
dataRoute.loader = ({ | ||
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 | ||
}) => { | ||
return prefetchStylesAndCallHandler(async () => { | ||
if (!routeModule.clientLoader) { | ||
// Call the server when no client loader exists | ||
return fetchServerLoader(request); | ||
} | ||
return routeModule.clientLoader({ | ||
request, | ||
params, | ||
async serverLoader() { | ||
if (isHydrationRequest) { | ||
isHydrationRequest = false; | ||
}, 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); | ||
// Throw an error if a clientLoader tries to call a serverLoader that doesn't exist | ||
if (initialData === undefined) { | ||
throw new Error(`You are trying to call serverLoader() on a route that does " + | ||
"not have a server loader (routeId: "${route.id}")`); | ||
// On the first call, resolve with the server result | ||
if (isHydrationRequest) { | ||
if (initialError !== undefined) { | ||
throw initialError; | ||
} | ||
return initialData; | ||
} | ||
// Otherwise, resolve the hydration clientLoader with the pre-loaded server data | ||
return initialData; | ||
// Call the server loader for client-side navigations | ||
return fetchServerLoader(request, true, singleFetch); | ||
} | ||
// Call the server loader for client-side navigations | ||
let result = await fetchServerLoader(request); | ||
let unwrapped = await unwrapServerResponse(result); | ||
return unwrapped; | ||
} | ||
}); | ||
}); | ||
}); | ||
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 = routeModule.clientLoader != null && (routeModule.clientLoader.hydrate === true || route.hasLoader !== true); | ||
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) { | ||
return fetchServerAction(request); | ||
if (isSpaMode) { | ||
throw noActionDefinedError("clientAction", route.id); | ||
} | ||
return fetchServerAction(request, false, singleFetch); | ||
} | ||
@@ -166,5 +241,4 @@ return routeModule.clientAction({ | ||
async serverAction() { | ||
let result = await fetchServerAction(request); | ||
let unwrapped = await unwrapServerResponse(result); | ||
return unwrapped; | ||
preventInvalidServerHandlerCall("action", route, isSpaMode); | ||
return fetchServerAction(request, true, singleFetch); | ||
} | ||
@@ -181,3 +255,6 @@ }); | ||
request | ||
}) => prefetchStylesAndCallHandler(() => fetchServerLoader(request)); | ||
}, singleFetch) => prefetchStylesAndCallHandler(() => { | ||
if (isSpaMode) return Promise.resolve(null); | ||
return fetchServerLoader(request, false, singleFetch); | ||
}); | ||
} | ||
@@ -187,3 +264,8 @@ if (!route.hasClientAction) { | ||
request | ||
}) => prefetchStylesAndCallHandler(() => fetchServerAction(request)); | ||
}, singleFetch) => prefetchStylesAndCallHandler(() => { | ||
if (isSpaMode) { | ||
throw noActionDefinedError("clientAction", route.id); | ||
} | ||
return fetchServerAction(request, false, singleFetch); | ||
}); | ||
} | ||
@@ -199,8 +281,7 @@ | ||
let clientLoader = mod.clientLoader; | ||
lazyRoute.loader = args => clientLoader({ | ||
lazyRoute.loader = (args, singleFetch) => clientLoader({ | ||
...args, | ||
async serverLoader() { | ||
let response = await fetchServerLoader(args.request); | ||
let result = await unwrapServerResponse(response); | ||
return result; | ||
preventInvalidServerHandlerCall("loader", route, isSpaMode); | ||
return fetchServerLoader(args.request, true, singleFetch); | ||
} | ||
@@ -211,8 +292,7 @@ }); | ||
let clientAction = mod.clientAction; | ||
lazyRoute.action = args => clientAction({ | ||
lazyRoute.action = (args, singleFetch) => clientAction({ | ||
...args, | ||
async serverAction() { | ||
let response = await fetchServerAction(args.request); | ||
let result = await unwrapServerResponse(response); | ||
return result; | ||
preventInvalidServerHandlerCall("action", route, isSpaMode); | ||
return fetchServerAction(args.request, true, singleFetch); | ||
} | ||
@@ -234,2 +314,4 @@ }); | ||
handle: lazyRoute.handle, | ||
// No need to wrap these in layout since the root route is never | ||
// loaded via route.lazy() | ||
Component: lazyRoute.Component, | ||
@@ -240,3 +322,3 @@ ErrorBoundary: lazyRoute.ErrorBoundary | ||
} | ||
let children = createClientRoutes(manifest, routeModulesCache, initialState, future, route.id, routesByParentId, needsRevalidation); | ||
let children = createClientRoutes(manifest, routeModulesCache, initialState, future, isSpaMode, route.id, routesByParentId, needsRevalidation); | ||
if (children.length > 0) dataRoute.children = children; | ||
@@ -320,2 +402,6 @@ return dataRoute; | ||
} | ||
let replace = response.headers.get("X-Remix-Replace"); | ||
if (replace) { | ||
headers["X-Remix-Replace"] = replace; | ||
} | ||
return redirect(url, { | ||
@@ -338,3 +424,6 @@ status, | ||
} | ||
function shouldHydrateRouteLoader(route, routeModule, isSpaMode) { | ||
return isSpaMode && route.id !== "root" || routeModule.clientLoader != null && (routeModule.clientLoader.hydrate === true || route.hasLoader !== true); | ||
} | ||
export { createClientRoutes, createClientRoutesWithHMRRevalidationOptOut, createServerRoutes }; | ||
export { createClientRoutes, createClientRoutesWithHMRRevalidationOptOut, createServerRoutes, shouldHydrateRouteLoader }; |
/** | ||
* @remix-run/react v0.0.0-nightly-910b900-20231207 | ||
* @remix-run/react v0.0.0-nightly-91184a743-20240823 | ||
* | ||
@@ -14,2 +14,3 @@ * Copyright (c) Remix Software Inc. | ||
import { useLocation, useMatches, UNSAFE_useScrollRestoration } from 'react-router-dom'; | ||
import { useRemixContext } from './components.js'; | ||
@@ -28,2 +29,5 @@ let STORAGE_KEY = "positions"; | ||
}) { | ||
let { | ||
isSpaMode | ||
} = useRemixContext(); | ||
let location = useLocation(); | ||
@@ -50,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) => { | ||
@@ -52,0 +62,0 @@ if (!window.history.state || !window.history.state.key) { |
/** | ||
* @remix-run/react v0.0.0-nightly-910b900-20231207 | ||
* @remix-run/react v0.0.0-nightly-91184a743-20240823 | ||
* | ||
@@ -15,3 +15,4 @@ * Copyright (c) Remix Software Inc. | ||
import { RemixErrorBoundary } from './errorBoundaries.js'; | ||
import { createServerRoutes } from './routes.js'; | ||
import { createServerRoutes, shouldHydrateRouteLoader } from './routes.js'; | ||
import { StreamTransfer } from './single-fetch.js'; | ||
@@ -26,3 +27,4 @@ /** | ||
url, | ||
abortDelay | ||
abortDelay, | ||
nonce | ||
}) { | ||
@@ -38,3 +40,3 @@ if (typeof url === "string") { | ||
} = context; | ||
let routes = createServerRoutes(manifest.routes, routeModules, context.future); | ||
let routes = createServerRoutes(manifest.routes, routeModules, context.future, context.isSpaMode); | ||
@@ -53,11 +55,7 @@ // Create a shallow clone of `loaderData` we can mutate for partial hydration. | ||
let manifestRoute = context.manifest.routes[routeId]; | ||
if ( | ||
// This route specifically gave us a HydrateFallback | ||
route && route.clientLoader && route.HydrateFallback || | ||
// This handles routes without a server loader but _with_ a clientLoader | ||
// that will automatically opt-into clientLoader.hydrate=true. The | ||
// staticHandler always puts a `null` in loaderData for non-loader routes | ||
// for proper serialization but we need to set that back to `undefined` | ||
// so _renderMatches will detect a required fallback at this level | ||
manifestRoute && manifestRoute.hasLoader == false && context.staticHandlerContext.loaderData[routeId] === null) { | ||
// 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; | ||
@@ -72,3 +70,3 @@ } | ||
}); | ||
return /*#__PURE__*/React.createElement(RemixContext.Provider, { | ||
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(RemixContext.Provider, { | ||
value: { | ||
@@ -80,4 +78,6 @@ manifest, | ||
future: context.future, | ||
isSpaMode: context.isSpaMode, | ||
serializeError: context.serializeError, | ||
abortDelay | ||
abortDelay, | ||
renderMeta: context.renderMeta | ||
} | ||
@@ -90,5 +90,11 @@ }, /*#__PURE__*/React.createElement(RemixErrorBoundary, { | ||
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 }; |
/** | ||
* @remix-run/react v0.0.0-nightly-910b900-20231207 | ||
* @remix-run/react v0.0.0-nightly-91184a743-20240823 | ||
* | ||
@@ -16,3 +16,3 @@ * Copyright (c) Remix Software Inc. | ||
var React = require('react'); | ||
var components = require('./components.js'); | ||
var errorBoundaries = require('./errorBoundaries.js'); | ||
@@ -44,10 +44,6 @@ function _interopNamespace(e) { | ||
function RemixRootDefaultHydrateFallback() { | ||
return /*#__PURE__*/React__namespace.createElement("html", { | ||
lang: "en" | ||
}, /*#__PURE__*/React__namespace.createElement("head", null, /*#__PURE__*/React__namespace.createElement("meta", { | ||
charSet: "utf-8" | ||
}), /*#__PURE__*/React__namespace.createElement("meta", { | ||
name: "viewport", | ||
content: "width=device-width,initial-scale=1,viewport-fit=cover" | ||
})), /*#__PURE__*/React__namespace.createElement("body", null, /*#__PURE__*/React__namespace.createElement(components.Scripts, null), /*#__PURE__*/React__namespace.createElement("script", { | ||
return /*#__PURE__*/React__namespace.createElement(errorBoundaries.BoundaryShell, { | ||
title: "Loading...", | ||
renderScripts: true | ||
}, /*#__PURE__*/React__namespace.createElement("script", { | ||
dangerouslySetInnerHTML: { | ||
@@ -57,10 +53,11 @@ __html: ` | ||
"💿 Hey developer 👋. You can provide a way better UX than this " + | ||
"when your app is running \`clientLoader\` functions on hydration. " + | ||
"Check out https://remix.run/route/hydrate-fallback for more information." | ||
"when your app is loading JS modules and/or running \`clientLoader\` " + | ||
"functions. Check out https://remix.run/route/hydrate-fallback " + | ||
"for more information." | ||
); | ||
` | ||
} | ||
}), " ")); | ||
})); | ||
} | ||
exports.RemixRootDefaultHydrateFallback = RemixRootDefaultHydrateFallback; |
@@ -0,13 +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 { ErrorResponse, Fetcher, FetcherWithComponents, FormEncType, FormMethod, FormProps, Location, NavigateFunction, Navigation, Params, Path, ShouldRevalidateFunction, ShouldRevalidateFunctionArgs, SubmitFunction, SubmitOptions, unstable_Blocker, unstable_BlockerFunction, } from "react-router-dom"; | ||
export { createPath, generatePath, matchPath, matchRoutes, parsePath, resolvePath, Form, Outlet, useAsyncError, useAsyncValue, isRouteErrorResponse, useBeforeUnload, useFetchers, useFormAction, useHref, useLocation, useMatch, useNavigate, useNavigation, useNavigationType, useOutlet, useOutletContext, useParams, useResolvedPath, useRevalidator, useRouteError, useSearchParams, useSubmit, useBlocker, unstable_usePrompt, unstable_useViewTransitionState, } from "react-router-dom"; | ||
export type { AwaitProps, RemixNavLinkProps as NavLinkProps, RemixLinkProps as LinkProps, UIMatch, } from "./components"; | ||
export { Await, Meta, Links, Scripts, Link, NavLink, PrefetchPageLinks, LiveReload, useFetcher, useLoaderData, useRouteLoaderData, useActionData, useMatches, RemixContext as UNSAFE_RemixContext, } from "./components"; | ||
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 { ClientActionFunction, ClientActionFunctionArgs, ClientLoaderFunction, ClientLoaderFunctionArgs, MetaArgs, MetaDescriptor, MetaFunction, RouteModules as UNSAFE_RouteModules, } 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 { 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-910b900-20231207 | ||
* @remix-run/react v0.0.0-nightly-91184a743-20240823 | ||
* | ||
@@ -15,15 +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 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', { | ||
@@ -33,2 +38,10 @@ 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', { | ||
@@ -38,2 +51,14 @@ enumerable: true, | ||
}); | ||
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', { | ||
@@ -59,2 +84,6 @@ enumerable: true, | ||
}); | ||
Object.defineProperty(exports, 'renderMatches', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.renderMatches; } | ||
}); | ||
Object.defineProperty(exports, 'resolvePath', { | ||
@@ -100,2 +129,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', { | ||
@@ -145,2 +182,6 @@ enumerable: true, | ||
}); | ||
Object.defineProperty(exports, 'useRoutes', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.useRoutes; } | ||
}); | ||
Object.defineProperty(exports, 'useSearchParams', { | ||
@@ -154,3 +195,29 @@ enumerable: true, | ||
}); | ||
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; | ||
@@ -171,1 +238,3 @@ exports.Links = components.Links; | ||
exports.RemixServer = server.RemixServer; | ||
exports.unstable_defineClientAction = singleFetch.defineClientAction; | ||
exports.unstable_defineClientLoader = singleFetch.defineClientLoader; |
/** | ||
* @remix-run/react v0.0.0-nightly-910b900-20231207 | ||
* @remix-run/react v0.0.0-nightly-91184a743-20240823 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
/** | ||
* @remix-run/react v0.0.0-nightly-910b900-20231207 | ||
* @remix-run/react v0.0.0-nightly-91184a743-20240823 | ||
* | ||
@@ -25,2 +25,3 @@ * Copyright (c) Remix Software Inc. | ||
//////////////////////////////////////////////////////////////////////////////// | ||
/** | ||
@@ -38,3 +39,3 @@ * Gets all the links for a set of matches. The modules are assumed to have been | ||
href | ||
})) : [], ((_module$links = module.links) === null || _module$links === void 0 ? void 0 : _module$links.call(module)) || []]; | ||
})) : [], (module === null || module === void 0 ? void 0 : (_module$links = module.links) === null || _module$links === void 0 ? void 0 : _module$links.call(module)) || []]; | ||
}).flat(2); | ||
@@ -177,3 +178,3 @@ let preloads = getCurrentPageModulePreloadHrefs(matches, manifest); | ||
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 { | ||
@@ -180,0 +181,0 @@ pathname, |
/** | ||
* @remix-run/react v0.0.0-nightly-910b900-20231207 | ||
* @remix-run/react v0.0.0-nightly-91184a743-20240823 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
@@ -1,2 +0,2 @@ | ||
import type { ComponentType } from "react"; | ||
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"; | ||
@@ -8,3 +8,3 @@ import type { LoaderFunction, SerializeFrom } from "@remix-run/server-runtime"; | ||
export interface RouteModules { | ||
[routeId: string]: RouteModule; | ||
[routeId: string]: RouteModule | undefined; | ||
} | ||
@@ -16,2 +16,3 @@ export interface RouteModule { | ||
HydrateFallback?: HydrateFallbackComponent; | ||
Layout?: LayoutComponent; | ||
default: RouteComponent; | ||
@@ -55,2 +56,10 @@ handle?: RouteHandle; | ||
/** | ||
* 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 | ||
@@ -57,0 +66,0 @@ * the document on route transitions. |
/** | ||
* @remix-run/react v0.0.0-nightly-910b900-20231207 | ||
* @remix-run/react v0.0.0-nightly-91184a743-20240823 | ||
* | ||
@@ -59,2 +59,8 @@ * Copyright (c) Remix Software Inc. | ||
/** | ||
* 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 | ||
*/ | ||
/** | ||
* A function that defines `<link>` tags to be inserted into the `<head>` of | ||
@@ -85,6 +91,23 @@ * the document on route transitions. | ||
} 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(); | ||
@@ -91,0 +114,0 @@ return new Promise(() => { |
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"; | ||
@@ -26,5 +26,8 @@ export interface RouteManifest<Route> { | ||
} | ||
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, initialState: HydrationState, future: FutureConfig): DataRouteObject[]; | ||
export declare function createClientRoutes(manifest: RouteManifest<EntryRoute>, routeModulesCache: RouteModules, initialState: HydrationState, 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-910b900-20231207 | ||
* @remix-run/react v0.0.0-nightly-91184a743-20240823 | ||
* | ||
@@ -23,2 +23,3 @@ * Copyright (c) Remix Software Inc. | ||
var fallback = require('./fallback.js'); | ||
var invariant = require('./invariant.js'); | ||
@@ -62,16 +63,52 @@ 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 routeModule = routeModules[route.id]; | ||
invariant(routeModule, "No `routeModule` available to create server routes"); | ||
let dataRoute = { | ||
...getRouteComponents(route, routeModule, isSpaMode), | ||
caseSensitive: route.caseSensitive, | ||
Component: getRouteModuleComponent(routeModule), | ||
HydrateFallback: routeModule.HydrateFallback ? routeModule.HydrateFallback : route.id === "root" ? fallback.RemixRootDefaultHydrateFallback : undefined, | ||
ErrorBoundary: routeModule.ErrorBoundary ? routeModule.ErrorBoundary : route.id === "root" ? () => /*#__PURE__*/React__namespace.createElement(errorBoundaries.RemixRootDefaultErrorBoundary, { | ||
error: reactRouterDom.useRouteError() | ||
}) : undefined, | ||
id: route.id, | ||
index: route.index, | ||
path: route.path, | ||
handle: routeModules[route.id].handle, | ||
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 | ||
@@ -85,4 +122,3 @@ // has a loader/clientLoader, but it won't ever be called during the static | ||
}; | ||
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; | ||
@@ -92,24 +128,57 @@ return dataRoute; | ||
} | ||
function createClientRoutesWithHMRRevalidationOptOut(needsRevalidation, manifest, routeModulesCache, initialState, future) { | ||
return createClientRoutes(manifest, routeModulesCache, initialState, 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, initialState, 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 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 routeModule = routeModulesCache[route.id]; | ||
async function fetchServerLoader(request) { | ||
if (!route.hasLoader) return null; | ||
return fetchServerHandler(request, route); | ||
// 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; | ||
} | ||
async function fetchServerAction(request) { | ||
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) { | ||
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 router.UNSAFE_ErrorResponseImpl(405, "Method Not Allowed", new Error(msg), true); | ||
throw noActionDefinedError("action", route.id); | ||
} | ||
return fetchServerHandler(request, route); | ||
return fetchServerHandlerAndMaybeUnwrap(request, unwrap, singleFetch); | ||
} | ||
async function prefetchStylesAndCallHandler(handler) { | ||
// Only prefetch links if we've been loaded into the cache, route.lazy | ||
// will handle initial loads | ||
let linkPrefetchPromise = routeModulesCache[route.id] ? links.prefetchStyleLinks(route, routeModulesCache[route.id]) : Promise.resolve(); | ||
// 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 { | ||
@@ -127,59 +196,65 @@ return handler(); | ||
if (routeModule) { | ||
var _initialState$loaderD, _initialState$errors, _routeModule$clientLo; | ||
// Use critical path modules directly | ||
Object.assign(dataRoute, { | ||
...dataRoute, | ||
Component: getRouteModuleComponent(routeModule), | ||
HydrateFallback: routeModule.HydrateFallback ? routeModule.HydrateFallback : route.id === "root" ? fallback.RemixRootDefaultHydrateFallback : undefined, | ||
ErrorBoundary: routeModule.ErrorBoundary ? routeModule.ErrorBoundary : route.id === "root" ? () => /*#__PURE__*/React__namespace.createElement(errorBoundaries.RemixRootDefaultErrorBoundary, { | ||
error: reactRouterDom.useRouteError() | ||
}) : undefined, | ||
...getRouteComponents(route, routeModule, isSpaMode), | ||
handle: routeModule.handle, | ||
shouldRevalidate: needsRevalidation ? wrapShouldRevalidateForHdr(route.id, routeModule.shouldRevalidate, needsRevalidation) : routeModule.shouldRevalidate | ||
}); | ||
let initialData = initialState && initialState.loaderData && initialState.loaderData[route.id]; | ||
let isHydrationRequest = needsRevalidation == null && routeModule.clientLoader != null && routeModule.clientLoader.hydrate === true; | ||
dataRoute.loader = ({ | ||
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 | ||
}) => { | ||
return prefetchStylesAndCallHandler(async () => { | ||
if (!routeModule.clientLoader) { | ||
// Call the server when no client loader exists | ||
return fetchServerLoader(request); | ||
} | ||
return routeModule.clientLoader({ | ||
request, | ||
params, | ||
async serverLoader() { | ||
if (isHydrationRequest) { | ||
isHydrationRequest = false; | ||
}, 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); | ||
// Throw an error if a clientLoader tries to call a serverLoader that doesn't exist | ||
if (initialData === undefined) { | ||
throw new Error(`You are trying to call serverLoader() on a route that does " + | ||
"not have a server loader (routeId: "${route.id}")`); | ||
// On the first call, resolve with the server result | ||
if (isHydrationRequest) { | ||
if (initialError !== undefined) { | ||
throw initialError; | ||
} | ||
return initialData; | ||
} | ||
// Otherwise, resolve the hydration clientLoader with the pre-loaded server data | ||
return initialData; | ||
// Call the server loader for client-side navigations | ||
return fetchServerLoader(request, true, singleFetch); | ||
} | ||
// Call the server loader for client-side navigations | ||
let result = await fetchServerLoader(request); | ||
let unwrapped = await unwrapServerResponse(result); | ||
return unwrapped; | ||
} | ||
}); | ||
}); | ||
}); | ||
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 = routeModule.clientLoader != null && (routeModule.clientLoader.hydrate === true || route.hasLoader !== true); | ||
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) { | ||
return fetchServerAction(request); | ||
if (isSpaMode) { | ||
throw noActionDefinedError("clientAction", route.id); | ||
} | ||
return fetchServerAction(request, false, singleFetch); | ||
} | ||
@@ -190,5 +265,4 @@ return routeModule.clientAction({ | ||
async serverAction() { | ||
let result = await fetchServerAction(request); | ||
let unwrapped = await unwrapServerResponse(result); | ||
return unwrapped; | ||
preventInvalidServerHandlerCall("action", route, isSpaMode); | ||
return fetchServerAction(request, true, singleFetch); | ||
} | ||
@@ -205,3 +279,6 @@ }); | ||
request | ||
}) => prefetchStylesAndCallHandler(() => fetchServerLoader(request)); | ||
}, singleFetch) => prefetchStylesAndCallHandler(() => { | ||
if (isSpaMode) return Promise.resolve(null); | ||
return fetchServerLoader(request, false, singleFetch); | ||
}); | ||
} | ||
@@ -211,3 +288,8 @@ if (!route.hasClientAction) { | ||
request | ||
}) => prefetchStylesAndCallHandler(() => fetchServerAction(request)); | ||
}, singleFetch) => prefetchStylesAndCallHandler(() => { | ||
if (isSpaMode) { | ||
throw noActionDefinedError("clientAction", route.id); | ||
} | ||
return fetchServerAction(request, false, singleFetch); | ||
}); | ||
} | ||
@@ -223,8 +305,7 @@ | ||
let clientLoader = mod.clientLoader; | ||
lazyRoute.loader = args => clientLoader({ | ||
lazyRoute.loader = (args, singleFetch) => clientLoader({ | ||
...args, | ||
async serverLoader() { | ||
let response = await fetchServerLoader(args.request); | ||
let result = await unwrapServerResponse(response); | ||
return result; | ||
preventInvalidServerHandlerCall("loader", route, isSpaMode); | ||
return fetchServerLoader(args.request, true, singleFetch); | ||
} | ||
@@ -235,8 +316,7 @@ }); | ||
let clientAction = mod.clientAction; | ||
lazyRoute.action = args => clientAction({ | ||
lazyRoute.action = (args, singleFetch) => clientAction({ | ||
...args, | ||
async serverAction() { | ||
let response = await fetchServerAction(args.request); | ||
let result = await unwrapServerResponse(response); | ||
return result; | ||
preventInvalidServerHandlerCall("action", route, isSpaMode); | ||
return fetchServerAction(args.request, true, singleFetch); | ||
} | ||
@@ -258,2 +338,4 @@ }); | ||
handle: lazyRoute.handle, | ||
// No need to wrap these in layout since the root route is never | ||
// loaded via route.lazy() | ||
Component: lazyRoute.Component, | ||
@@ -264,3 +346,3 @@ ErrorBoundary: lazyRoute.ErrorBoundary | ||
} | ||
let children = createClientRoutes(manifest, routeModulesCache, initialState, future, route.id, routesByParentId, needsRevalidation); | ||
let children = createClientRoutes(manifest, routeModulesCache, initialState, future, isSpaMode, route.id, routesByParentId, needsRevalidation); | ||
if (children.length > 0) dataRoute.children = children; | ||
@@ -344,2 +426,6 @@ return dataRoute; | ||
} | ||
let replace = response.headers.get("X-Remix-Replace"); | ||
if (replace) { | ||
headers["X-Remix-Replace"] = replace; | ||
} | ||
return reactRouterDom.redirect(url, { | ||
@@ -362,2 +448,5 @@ status, | ||
} | ||
function shouldHydrateRouteLoader(route, routeModule, isSpaMode) { | ||
return isSpaMode && route.id !== "root" || routeModule.clientLoader != null && (routeModule.clientLoader.hydrate === true || route.hasLoader !== true); | ||
} | ||
@@ -367,1 +456,2 @@ exports.createClientRoutes = createClientRoutes; | ||
exports.createServerRoutes = createServerRoutes; | ||
exports.shouldHydrateRouteLoader = shouldHydrateRouteLoader; |
@@ -12,2 +12,2 @@ import * as React from "react"; | ||
getKey?: ScrollRestorationPropsRR["getKey"]; | ||
}): React.JSX.Element; | ||
}): React.JSX.Element | null; |
/** | ||
* @remix-run/react v0.0.0-nightly-910b900-20231207 | ||
* @remix-run/react v0.0.0-nightly-91184a743-20240823 | ||
* | ||
@@ -18,2 +18,3 @@ * Copyright (c) Remix Software Inc. | ||
var reactRouterDom = require('react-router-dom'); | ||
var components = require('./components.js'); | ||
@@ -52,2 +53,5 @@ function _interopNamespace(e) { | ||
}) { | ||
let { | ||
isSpaMode | ||
} = components.useRemixContext(); | ||
let location = reactRouterDom.useLocation(); | ||
@@ -74,2 +78,8 @@ let matches = reactRouterDom.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) => { | ||
@@ -76,0 +86,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-910b900-20231207 | ||
* @remix-run/react v0.0.0-nightly-91184a743-20240823 | ||
* | ||
@@ -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 | ||
}) { | ||
@@ -62,3 +64,3 @@ if (typeof url === "string") { | ||
} = context; | ||
let routes$1 = routes.createServerRoutes(manifest.routes, routeModules, context.future); | ||
let routes$1 = routes.createServerRoutes(manifest.routes, routeModules, context.future, context.isSpaMode); | ||
@@ -77,11 +79,7 @@ // Create a shallow clone of `loaderData` we can mutate for partial hydration. | ||
let manifestRoute = context.manifest.routes[routeId]; | ||
if ( | ||
// This route specifically gave us a HydrateFallback | ||
route && route.clientLoader && route.HydrateFallback || | ||
// This handles routes without a server loader but _with_ a clientLoader | ||
// that will automatically opt-into clientLoader.hydrate=true. The | ||
// staticHandler always puts a `null` in loaderData for non-loader routes | ||
// for proper serialization but we need to set that back to `undefined` | ||
// so _renderMatches will detect a required fallback at this level | ||
manifestRoute && manifestRoute.hasLoader == false && context.staticHandlerContext.loaderData[routeId] === null) { | ||
// 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; | ||
@@ -96,3 +94,3 @@ } | ||
}); | ||
return /*#__PURE__*/React__namespace.createElement(components.RemixContext.Provider, { | ||
return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(components.RemixContext.Provider, { | ||
value: { | ||
@@ -104,4 +102,6 @@ manifest, | ||
future: context.future, | ||
isSpaMode: context.isSpaMode, | ||
serializeError: context.serializeError, | ||
abortDelay | ||
abortDelay, | ||
renderMeta: context.renderMeta | ||
} | ||
@@ -114,5 +114,11 @@ }, /*#__PURE__*/React__namespace.createElement(errorBoundaries.RemixErrorBoundary, { | ||
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; |
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-910b900-20231207", | ||
"version": "0.0.0-nightly-91184a743-20240823", | ||
"description": "React DOM bindings for Remix", | ||
@@ -19,8 +19,11 @@ "bugs": { | ||
"dependencies": { | ||
"@remix-run/router": "1.14.0-pre.0", | ||
"@remix-run/server-runtime": "0.0.0-nightly-910b900-20231207", | ||
"react-router": "6.21.0-pre.0", | ||
"react-router-dom": "6.21.0-pre.0" | ||
"@remix-run/router": "1.19.1", | ||
"@remix-run/server-runtime": "0.0.0-nightly-91184a743-20240823", | ||
"react-router": "6.26.1", | ||
"react-router-dom": "6.26.1", | ||
"turbo-stream": "2.3.0" | ||
}, | ||
"devDependencies": { | ||
"@remix-run/node": "0.0.0-nightly-91184a743-20240823", | ||
"@remix-run/react": "0.0.0-nightly-91184a743-20240823", | ||
"@testing-library/jest-dom": "^5.17.0", | ||
@@ -49,6 +52,10 @@ "@testing-library/react": "^13.3.0", | ||
"dist/", | ||
"future/", | ||
"CHANGELOG.md", | ||
"LICENSE.md", | ||
"README.md" | ||
] | ||
} | ||
], | ||
"scripts": { | ||
"tsc": "tsc" | ||
} | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
298267
56
7397
8
9
13
6
+ Addedturbo-stream@2.3.0
+ Added@remix-run/router@1.19.1(transitive)
+ Added@remix-run/server-runtime@0.0.0-nightly-91184a743-20240823(transitive)
+ Added@types/cookie@0.6.0(transitive)
+ Addedcookie@0.6.0(transitive)
+ Addedreact-router@6.26.1(transitive)
+ Addedreact-router-dom@6.26.1(transitive)
+ Addedturbo-stream@2.3.0(transitive)
- Removed@remix-run/router@1.14.0-pre.0(transitive)
- Removed@remix-run/server-runtime@0.0.0-nightly-910b900-20231207(transitive)
- Removed@types/cookie@0.5.4(transitive)
- Removedcookie@0.5.0(transitive)
- Removedreact-router@6.21.0-pre.0(transitive)
- Removedreact-router-dom@6.21.0-pre.0(transitive)
Updated@remix-run/router@1.19.1
Updated@remix-run/server-runtime@0.0.0-nightly-91184a743-20240823
Updatedreact-router@6.26.1
Updatedreact-router-dom@6.26.1