@remix-run/react
Advanced tools
Comparing version 0.0.0-nightly-6ec38fa-20240125 to 0.0.0-nightly-6ee69c13d-20241003
/** | ||
* @remix-run/react v0.0.0-nightly-6ec38fa-20240125 | ||
* @remix-run/react v0.0.0-nightly-6ee69c13d-20241003 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
@@ -1,8 +0,8 @@ | ||
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; | ||
@@ -12,2 +12,4 @@ criticalCss?: string; | ||
isSpaMode: boolean; | ||
stream: ReadableStream<Uint8Array> | undefined; | ||
streamController: ReadableStreamDefaultController<Uint8Array>; | ||
a?: number; | ||
@@ -21,5 +23,6 @@ dev?: { | ||
var __remixRouteModules: RouteModules; | ||
var __remixManifest: EntryContext["manifest"]; | ||
var __remixManifest: AssetsManifest; | ||
var __remixRevalidation: number | undefined; | ||
var __remixClearCriticalCss: () => void; | ||
var __remixHdrActive: boolean; | ||
var __remixClearCriticalCss: (() => void) | undefined; | ||
var $RefreshRuntime$: { | ||
@@ -26,0 +29,0 @@ performReactRefresh: () => void; |
/** | ||
* @remix-run/react v0.0.0-nightly-6ec38fa-20240125 | ||
* @remix-run/react v0.0.0-nightly-6ee69c13d-20241003 | ||
* | ||
@@ -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,2 +52,3 @@ function _interopNamespace(e) { | ||
let stateDecodingPromise; | ||
let router; | ||
@@ -65,4 +69,2 @@ let routerInitialized = false; | ||
}); | ||
// The critical CSS can only be cleared, so the reducer always returns undefined | ||
let criticalCssReducer = () => undefined; | ||
@@ -76,18 +78,25 @@ /** | ||
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 && !window.__remixContext.isSpaMode) { | ||
let errorMsg = `Initial URL (${initialPathname}) does not match URL at time of hydration ` + `(${hydratedPathname}), reloading page...`; | ||
console.error(errorMsg); | ||
window.location.reload(); | ||
// Get out of here so the reload can happen - don't create the router | ||
// since it'll then kick off unnecessary route.lazy() loads | ||
return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null); | ||
// When single fetch is enabled, we need to suspend until the initial state | ||
// snapshot is decoded into window.__remixContext.state | ||
if (window.__remixContext.future.unstable_singleFetch) { | ||
// Note: `stateDecodingPromise` is not coupled to `router` - we'll reach this | ||
// code potentially many times waiting for our state to arrive, but we'll | ||
// then only get past here and create the `router` one time | ||
if (!stateDecodingPromise) { | ||
let stream = window.__remixContext.stream; | ||
invariant(stream, "No stream found for single fetch decoding"); | ||
window.__remixContext.stream = undefined; | ||
stateDecodingPromise = singleFetch.decodeViaTurboStream(stream, window).then(value => { | ||
window.__remixContext.state = value.value; | ||
stateDecodingPromise.value = true; | ||
}).catch(e => { | ||
stateDecodingPromise.error = e; | ||
}); | ||
} | ||
if (stateDecodingPromise.error) { | ||
throw stateDecodingPromise.error; | ||
} | ||
if (!stateDecodingPromise.value) { | ||
throw stateDecodingPromise; | ||
} | ||
} | ||
@@ -109,3 +118,3 @@ let routes$1 = routes.createClientRoutes(window.__remixManifest.routes, window.__remixRouteModules, window.__remixContext.state, window.__remixContext.future, window.__remixContext.isSpaMode); | ||
}; | ||
let initialMatches = reactRouterDom.matchRoutes(routes$1, window.location); | ||
let initialMatches = reactRouterDom.matchRoutes(routes$1, window.location, window.__remixContext.basename); | ||
if (initialMatches) { | ||
@@ -142,2 +151,3 @@ for (let match of initialMatches) { | ||
history: router$1.createBrowserHistory(), | ||
basename: window.__remixContext.basename, | ||
future: { | ||
@@ -148,6 +158,10 @@ 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, () => router) : undefined, | ||
unstable_patchRoutesOnNavigation: fogOfWar.getPatchRoutesOnNavigationFunction(window.__remixManifest, window.__remixRouteModules, window.__remixContext.future, window.__remixContext.isSpaMode, window.__remixContext.basename) | ||
}); | ||
@@ -175,13 +189,13 @@ | ||
// 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(() => { | ||
@@ -195,4 +209,2 @@ // If we had to run clientLoaders on hydration, we delay initialization until | ||
}, []); | ||
// eslint-disable-next-line react-hooks/rules-of-hooks | ||
React__namespace.useLayoutEffect(() => { | ||
@@ -205,2 +217,3 @@ return router.subscribe(newState => { | ||
}, [location]); | ||
fogOfWar.useFogOFWarDiscovery(router, window.__remixManifest, window.__remixRouteModules, window.__remixContext.future, window.__remixContext.isSpaMode); | ||
@@ -211,21 +224,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, | ||
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 | ||
} | ||
}))); | ||
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"; | ||
@@ -13,2 +13,9 @@ import type { SerializeFrom } from "@remix-run/server-runtime"; | ||
/** | ||
* 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: | ||
@@ -23,5 +30,7 @@ * | ||
export interface RemixLinkProps extends LinkProps { | ||
discover?: DiscoverBehavior; | ||
prefetch?: PrefetchBehavior; | ||
} | ||
export interface RemixNavLinkProps extends NavLinkProps { | ||
discover?: DiscoverBehavior; | ||
prefetch?: PrefetchBehavior; | ||
@@ -44,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; | ||
@@ -126,3 +146,3 @@ /** | ||
*/ | ||
export declare const LiveReload: (() => null) | (({ origin, port, timeoutMs, nonce, }: { | ||
export declare const LiveReload: ({ origin, port, timeoutMs, nonce, }: { | ||
origin?: string | undefined; | ||
@@ -132,2 +152,2 @@ port?: number | undefined; | ||
nonce?: string | undefined; | ||
}) => React.JSX.Element); | ||
}) => React.JSX.Element | null; |
/** | ||
* @remix-run/react v0.0.0-nightly-6ec38fa-20240125 | ||
* @remix-run/react v0.0.0-nightly-6ee69c13d-20241003 | ||
* | ||
@@ -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: | ||
@@ -138,2 +147,5 @@ * | ||
const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i; | ||
function getDiscoverAttr(discover, isAbsolute, reloadDocument) { | ||
return discover === "render" && !isAbsolute && !reloadDocument ? "true" : undefined; | ||
} | ||
@@ -148,2 +160,3 @@ /** | ||
prefetch = "none", | ||
discover = "render", | ||
...props | ||
@@ -156,3 +169,4 @@ }, forwardedRef) => { | ||
ref: mergeRefs(forwardedRef, ref), | ||
to: to | ||
to: to, | ||
"data-discover": getDiscoverAttr(discover, isAbsolute, props.reloadDocument) | ||
})), shouldPrefetch && !isAbsolute ? /*#__PURE__*/React__namespace.createElement(PrefetchPageLinks, { | ||
@@ -173,2 +187,3 @@ page: href | ||
prefetch = "none", | ||
discover = "render", | ||
...props | ||
@@ -181,3 +196,4 @@ }, forwardedRef) => { | ||
ref: mergeRefs(forwardedRef, ref), | ||
to: to | ||
to: to, | ||
"data-discover": getDiscoverAttr(discover, isAbsolute, props.reloadDocument) | ||
})), shouldPrefetch && !isAbsolute ? /*#__PURE__*/React__namespace.createElement(PrefetchPageLinks, { | ||
@@ -188,2 +204,19 @@ page: href | ||
Link.displayName = "Link"; | ||
/** | ||
* This component renders a form tag and is the primary way the user will | ||
* submit information via your website. | ||
* | ||
* @see https://remix.run/components/form | ||
*/ | ||
let Form = /*#__PURE__*/React__namespace.forwardRef(({ | ||
discover = "render", | ||
...props | ||
}, forwardedRef) => { | ||
let isAbsolute = typeof props.action === "string" && ABSOLUTE_URL_REGEX.test(props.action); | ||
return /*#__PURE__*/React__namespace.createElement(reactRouterDom.Form, _rollupPluginBabelHelpers["extends"]({}, props, { | ||
ref: forwardedRef, | ||
"data-discover": getDiscoverAttr(discover, isAbsolute, props.reloadDocument) | ||
})); | ||
}); | ||
Form.displayName = "Form"; | ||
function composeEventHandlers(theirHandler, ourHandler) { | ||
@@ -208,3 +241,3 @@ return event => { | ||
if (errors) { | ||
let errorIdx = matches.findIndex(m => errors[m.route.id]); | ||
let errorIdx = matches.findIndex(m => errors[m.route.id] !== undefined); | ||
return matches.slice(0, errorIdx + 1); | ||
@@ -263,3 +296,3 @@ } | ||
} = 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) { | ||
@@ -300,10 +333,51 @@ console.warn(`Tried to prefetch ${page} but no routes matched.`); | ||
let { | ||
manifest | ||
future, | ||
manifest, | ||
routeModules | ||
} = useRemixContext(); | ||
let { | ||
loaderData, | ||
matches | ||
} = useDataRouterStateContext(); | ||
let newMatchesForData = React__namespace.useMemo(() => links.getNewMatchesForLinks(page, nextMatches, matches, manifest, location, "data"), [page, nextMatches, matches, manifest, location]); | ||
let dataHrefs = React__namespace.useMemo(() => { | ||
if (!future.unstable_singleFetch) { | ||
return links.getDataLinkHrefs(page, newMatchesForData, manifest); | ||
} | ||
if (page === location.pathname + location.search + location.hash) { | ||
// Because we opt-into revalidation, don't compute this for the current page | ||
// since it would always trigger a prefetch of the existing loaders | ||
return []; | ||
} | ||
// Single-fetch is harder :) | ||
// This parallels the logic in the single fetch data strategy | ||
let routesParams = new Set(); | ||
let foundOptOutRoute = false; | ||
nextMatches.forEach(m => { | ||
var _routeModules$m$route; | ||
if (!manifest.routes[m.route.id].hasLoader) { | ||
return; | ||
} | ||
if (!newMatchesForData.some(m2 => m2.route.id === m.route.id) && m.route.id in loaderData && (_routeModules$m$route = routeModules[m.route.id]) !== null && _routeModules$m$route !== void 0 && _routeModules$m$route.shouldRevalidate) { | ||
foundOptOutRoute = true; | ||
} else if (manifest.routes[m.route.id].hasClientLoader) { | ||
foundOptOutRoute = true; | ||
} else { | ||
routesParams.add(m.route.id); | ||
} | ||
}); | ||
if (routesParams.size === 0) { | ||
return []; | ||
} | ||
let url = singleFetch.singleFetchUrl(page); | ||
// When one or more routes have opted out, we add a _routes param to | ||
// limit the loaders to those that have a server loader and did not | ||
// opt out | ||
if (foundOptOutRoute && routesParams.size > 0) { | ||
url.searchParams.set("_routes", nextMatches.filter(m => routesParams.has(m.route.id)).map(m => m.route.id).join(",")); | ||
} | ||
return [url.pathname + url.search]; | ||
}, [future.unstable_singleFetch, loaderData, location, manifest, newMatchesForData, nextMatches, page, routeModules]); | ||
let newMatchesForAssets = React__namespace.useMemo(() => links.getNewMatchesForLinks(page, nextMatches, matches, manifest, location, "assets"), [page, nextMatches, matches, manifest, location]); | ||
let dataHrefs = React__namespace.useMemo(() => links.getDataLinkHrefs(page, newMatchesForData, manifest), [newMatchesForData, page, manifest]); | ||
let moduleHrefs = React__namespace.useMemo(() => links.getModuleLinkHrefs(newMatchesForAssets, manifest), [newMatchesForAssets, manifest]); | ||
@@ -479,3 +553,5 @@ | ||
serializeError, | ||
isSpaMode | ||
isSpaMode, | ||
future, | ||
renderMeta | ||
} = useRemixContext(); | ||
@@ -490,3 +566,9 @@ let { | ||
} = 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); | ||
@@ -535,4 +617,10 @@ React__namespace.useEffect(() => { | ||
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 | ||
@@ -575,4 +663,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(",")}}; | ||
@@ -606,14 +697,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]; | ||
@@ -623,7 +703,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", | ||
@@ -762,3 +842,3 @@ href: manifest.entry.module, | ||
process.env.NODE_ENV !== "development" ? () => null : function LiveReload({ | ||
origin = process.env.REMIX_DEV_ORIGIN, | ||
origin, | ||
port, | ||
@@ -768,2 +848,3 @@ timeoutMs = 1000, | ||
}) { | ||
origin ??= process.env.REMIX_DEV_ORIGIN; | ||
let js = String.raw; | ||
@@ -878,2 +959,3 @@ return /*#__PURE__*/React__namespace.createElement("script", { | ||
exports.Await = Await; | ||
exports.Form = Form; | ||
exports.Link = Link; | ||
@@ -880,0 +962,0 @@ exports.Links = Links; |
@@ -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-6ec38fa-20240125 | ||
* @remix-run/react v0.0.0-nightly-6ee69c13d-20241003 | ||
* | ||
@@ -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; |
@@ -17,5 +17,16 @@ import type { StaticHandlerContext } from "@remix-run/router"; | ||
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>; | ||
} | ||
@@ -25,2 +36,4 @@ export interface FutureConfig { | ||
v3_relativeSplatPath: boolean; | ||
unstable_lazyRouteDiscovery: boolean; | ||
unstable_singleFetch: boolean; | ||
} | ||
@@ -27,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-6ec38fa-20240125 | ||
* @remix-run/react v0.0.0-nightly-6ee69c13d-20241003 | ||
* | ||
@@ -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-6ec38fa-20240125 | ||
* @remix-run/react v0.0.0-nightly-6ee69c13d-20241003 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
/** | ||
* @remix-run/react v0.0.0-nightly-6ec38fa-20240125 | ||
* @remix-run/react v0.0.0-nightly-6ee69c13d-20241003 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
/** | ||
* @remix-run/react v0.0.0-nightly-6ec38fa-20240125 | ||
* @remix-run/react v0.0.0-nightly-6ee69c13d-20241003 | ||
* | ||
@@ -19,2 +19,5 @@ * Copyright (c) Remix Software Inc. | ||
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,2 +28,3 @@ /* eslint-disable prefer-let/prefer-let */ | ||
let stateDecodingPromise; | ||
let router; | ||
@@ -42,4 +46,2 @@ let routerInitialized = false; | ||
}); | ||
// The critical CSS can only be cleared, so the reducer always returns undefined | ||
let criticalCssReducer = () => undefined; | ||
@@ -116,18 +118,25 @@ // @ts-expect-error | ||
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 && !window.__remixContext.isSpaMode) { | ||
let errorMsg = `Initial URL (${initialPathname}) does not match URL at time of hydration ` + `(${hydratedPathname}), reloading page...`; | ||
console.error(errorMsg); | ||
window.location.reload(); | ||
// Get out of here so the reload can happen - don't create the router | ||
// since it'll then kick off unnecessary route.lazy() loads | ||
return /*#__PURE__*/React.createElement(React.Fragment, null); | ||
// When single fetch is enabled, we need to suspend until the initial state | ||
// snapshot is decoded into window.__remixContext.state | ||
if (window.__remixContext.future.unstable_singleFetch) { | ||
// Note: `stateDecodingPromise` is not coupled to `router` - we'll reach this | ||
// code potentially many times waiting for our state to arrive, but we'll | ||
// then only get past here and create the `router` one time | ||
if (!stateDecodingPromise) { | ||
let stream = window.__remixContext.stream; | ||
invariant(stream, "No stream found for single fetch decoding"); | ||
window.__remixContext.stream = undefined; | ||
stateDecodingPromise = decodeViaTurboStream(stream, window).then(value => { | ||
window.__remixContext.state = value.value; | ||
stateDecodingPromise.value = true; | ||
}).catch(e => { | ||
stateDecodingPromise.error = e; | ||
}); | ||
} | ||
if (stateDecodingPromise.error) { | ||
throw stateDecodingPromise.error; | ||
} | ||
if (!stateDecodingPromise.value) { | ||
throw stateDecodingPromise; | ||
} | ||
} | ||
@@ -149,3 +158,3 @@ let routes = createClientRoutes(window.__remixManifest.routes, window.__remixRouteModules, window.__remixContext.state, window.__remixContext.future, window.__remixContext.isSpaMode); | ||
}; | ||
let initialMatches = matchRoutes(routes, window.location); | ||
let initialMatches = matchRoutes(routes, window.location, window.__remixContext.basename); | ||
if (initialMatches) { | ||
@@ -182,2 +191,3 @@ for (let match of initialMatches) { | ||
history: createBrowserHistory(), | ||
basename: window.__remixContext.basename, | ||
future: { | ||
@@ -188,6 +198,10 @@ 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, () => router) : undefined, | ||
unstable_patchRoutesOnNavigation: getPatchRoutesOnNavigationFunction(window.__remixManifest, window.__remixRouteModules, window.__remixContext.future, window.__remixContext.isSpaMode, window.__remixContext.basename) | ||
}); | ||
@@ -215,13 +229,13 @@ | ||
// 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(() => { | ||
@@ -235,4 +249,2 @@ // If we had to run clientLoaders on hydration, we delay initialization until | ||
}, []); | ||
// eslint-disable-next-line react-hooks/rules-of-hooks | ||
React.useLayoutEffect(() => { | ||
@@ -245,2 +257,3 @@ return router.subscribe(newState => { | ||
}, [location]); | ||
useFogOFWarDiscovery(router, window.__remixManifest, window.__remixRouteModules, window.__remixContext.future, window.__remixContext.isSpaMode); | ||
@@ -251,21 +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, | ||
isSpaMode: window.__remixContext.isSpaMode | ||
} | ||
}, /*#__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-6ec38fa-20240125 | ||
* @remix-run/react v0.0.0-nightly-6ee69c13d-20241003 | ||
* | ||
@@ -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 { 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: | ||
@@ -114,2 +123,5 @@ * | ||
const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i; | ||
function getDiscoverAttr(discover, isAbsolute, reloadDocument) { | ||
return discover === "render" && !isAbsolute && !reloadDocument ? "true" : undefined; | ||
} | ||
@@ -124,2 +136,3 @@ /** | ||
prefetch = "none", | ||
discover = "render", | ||
...props | ||
@@ -132,3 +145,4 @@ }, forwardedRef) => { | ||
ref: mergeRefs(forwardedRef, ref), | ||
to: to | ||
to: to, | ||
"data-discover": getDiscoverAttr(discover, isAbsolute, props.reloadDocument) | ||
})), shouldPrefetch && !isAbsolute ? /*#__PURE__*/React.createElement(PrefetchPageLinks, { | ||
@@ -149,2 +163,3 @@ page: href | ||
prefetch = "none", | ||
discover = "render", | ||
...props | ||
@@ -157,3 +172,4 @@ }, forwardedRef) => { | ||
ref: mergeRefs(forwardedRef, ref), | ||
to: to | ||
to: to, | ||
"data-discover": getDiscoverAttr(discover, isAbsolute, props.reloadDocument) | ||
})), shouldPrefetch && !isAbsolute ? /*#__PURE__*/React.createElement(PrefetchPageLinks, { | ||
@@ -164,2 +180,19 @@ page: href | ||
Link.displayName = "Link"; | ||
/** | ||
* This component renders a form tag and is the primary way the user will | ||
* submit information via your website. | ||
* | ||
* @see https://remix.run/components/form | ||
*/ | ||
let Form = /*#__PURE__*/React.forwardRef(({ | ||
discover = "render", | ||
...props | ||
}, forwardedRef) => { | ||
let isAbsolute = typeof props.action === "string" && ABSOLUTE_URL_REGEX.test(props.action); | ||
return /*#__PURE__*/React.createElement(Form$1, _extends({}, props, { | ||
ref: forwardedRef, | ||
"data-discover": getDiscoverAttr(discover, isAbsolute, props.reloadDocument) | ||
})); | ||
}); | ||
Form.displayName = "Form"; | ||
function composeEventHandlers(theirHandler, ourHandler) { | ||
@@ -184,3 +217,3 @@ return event => { | ||
if (errors) { | ||
let errorIdx = matches.findIndex(m => errors[m.route.id]); | ||
let errorIdx = matches.findIndex(m => errors[m.route.id] !== undefined); | ||
return matches.slice(0, errorIdx + 1); | ||
@@ -239,3 +272,3 @@ } | ||
} = 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) { | ||
@@ -276,10 +309,51 @@ console.warn(`Tried to prefetch ${page} but no routes matched.`); | ||
let { | ||
manifest | ||
future, | ||
manifest, | ||
routeModules | ||
} = useRemixContext(); | ||
let { | ||
loaderData, | ||
matches | ||
} = useDataRouterStateContext(); | ||
let newMatchesForData = React.useMemo(() => getNewMatchesForLinks(page, nextMatches, matches, manifest, location, "data"), [page, nextMatches, matches, manifest, location]); | ||
let dataHrefs = React.useMemo(() => { | ||
if (!future.unstable_singleFetch) { | ||
return getDataLinkHrefs(page, newMatchesForData, manifest); | ||
} | ||
if (page === location.pathname + location.search + location.hash) { | ||
// Because we opt-into revalidation, don't compute this for the current page | ||
// since it would always trigger a prefetch of the existing loaders | ||
return []; | ||
} | ||
// Single-fetch is harder :) | ||
// This parallels the logic in the single fetch data strategy | ||
let routesParams = new Set(); | ||
let foundOptOutRoute = false; | ||
nextMatches.forEach(m => { | ||
var _routeModules$m$route; | ||
if (!manifest.routes[m.route.id].hasLoader) { | ||
return; | ||
} | ||
if (!newMatchesForData.some(m2 => m2.route.id === m.route.id) && m.route.id in loaderData && (_routeModules$m$route = routeModules[m.route.id]) !== null && _routeModules$m$route !== void 0 && _routeModules$m$route.shouldRevalidate) { | ||
foundOptOutRoute = true; | ||
} else if (manifest.routes[m.route.id].hasClientLoader) { | ||
foundOptOutRoute = true; | ||
} else { | ||
routesParams.add(m.route.id); | ||
} | ||
}); | ||
if (routesParams.size === 0) { | ||
return []; | ||
} | ||
let url = singleFetchUrl(page); | ||
// When one or more routes have opted out, we add a _routes param to | ||
// limit the loaders to those that have a server loader and did not | ||
// opt out | ||
if (foundOptOutRoute && routesParams.size > 0) { | ||
url.searchParams.set("_routes", nextMatches.filter(m => routesParams.has(m.route.id)).map(m => m.route.id).join(",")); | ||
} | ||
return [url.pathname + url.search]; | ||
}, [future.unstable_singleFetch, loaderData, location, manifest, newMatchesForData, nextMatches, page, routeModules]); | ||
let newMatchesForAssets = React.useMemo(() => getNewMatchesForLinks(page, nextMatches, matches, manifest, location, "assets"), [page, nextMatches, matches, manifest, location]); | ||
let dataHrefs = React.useMemo(() => getDataLinkHrefs(page, newMatchesForData, manifest), [newMatchesForData, page, manifest]); | ||
let moduleHrefs = React.useMemo(() => getModuleLinkHrefs(newMatchesForAssets, manifest), [newMatchesForAssets, manifest]); | ||
@@ -455,3 +529,5 @@ | ||
serializeError, | ||
isSpaMode | ||
isSpaMode, | ||
future, | ||
renderMeta | ||
} = useRemixContext(); | ||
@@ -466,3 +542,9 @@ let { | ||
} = 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); | ||
@@ -511,4 +593,10 @@ React.useEffect(() => { | ||
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 | ||
@@ -551,4 +639,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(",")}}; | ||
@@ -582,14 +673,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]; | ||
@@ -599,7 +679,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", | ||
@@ -738,3 +818,3 @@ href: manifest.entry.module, | ||
process.env.NODE_ENV !== "development" ? () => null : function LiveReload({ | ||
origin = process.env.REMIX_DEV_ORIGIN, | ||
origin, | ||
port, | ||
@@ -744,2 +824,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; | ||
@@ -853,2 +940,2 @@ return /*#__PURE__*/React.createElement("script", { | ||
export { Await, Link, Links, LiveReload, Meta, NavLink, PrefetchPageLinks, RemixContext, Scripts, composeEventHandlers, useActionData, useFetcher, useLoaderData, useMatches, useRemixContext, 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-6ec38fa-20240125 | ||
* @remix-run/react v0.0.0-nightly-6ee69c13d-20241003 | ||
* | ||
@@ -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-6ec38fa-20240125 | ||
* @remix-run/react v0.0.0-nightly-6ee69c13d-20241003 | ||
* | ||
@@ -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-6ec38fa-20240125 | ||
* @remix-run/react v0.0.0-nightly-6ee69c13d-20241003 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
/** | ||
* @remix-run/react v0.0.0-nightly-6ec38fa-20240125 | ||
* @remix-run/react v0.0.0-nightly-6ee69c13d-20241003 | ||
* | ||
@@ -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-6ec38fa-20240125 | ||
* @remix-run/react v0.0.0-nightly-6ee69c13d-20241003 | ||
* | ||
@@ -11,7 +11,7 @@ * Copyright (c) Remix Software Inc. | ||
*/ | ||
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 { defer, json, redirect, redirectDocument } from '@remix-run/server-runtime'; | ||
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 { 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'; |
/** | ||
* @remix-run/react v0.0.0-nightly-6ec38fa-20240125 | ||
* @remix-run/react v0.0.0-nightly-6ee69c13d-20241003 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
/** | ||
* @remix-run/react v0.0.0-nightly-6ec38fa-20240125 | ||
* @remix-run/react v0.0.0-nightly-6ee69c13d-20241003 | ||
* | ||
@@ -172,3 +172,3 @@ * Copyright (c) Remix Software Inc. | ||
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 { | ||
@@ -175,0 +175,0 @@ pathname, |
/** | ||
* @remix-run/react v0.0.0-nightly-6ec38fa-20240125 | ||
* @remix-run/react v0.0.0-nightly-6ee69c13d-20241003 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
/** | ||
* @remix-run/react v0.0.0-nightly-6ec38fa-20240125 | ||
* @remix-run/react v0.0.0-nightly-6ee69c13d-20241003 | ||
* | ||
@@ -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,14 @@ * 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 && | ||
@@ -75,3 +89,2 @@ // @ts-expect-error | ||
// boundary and let them see the error in the overlay or the dev server log | ||
console.error(`Error loading route module \`${route.module}\`:`, error); | ||
throw error; | ||
@@ -78,0 +91,0 @@ } |
/** | ||
* @remix-run/react v0.0.0-nightly-6ec38fa-20240125 | ||
* @remix-run/react v0.0.0-nightly-6ee69c13d-20241003 | ||
* | ||
@@ -38,2 +38,34 @@ * Copyright (c) Remix Software Inc. | ||
} | ||
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({ | ||
@@ -46,9 +78,4 @@ Component: () => null | ||
let dataRoute = { | ||
...getRouteComponents(route, routeModule, isSpaMode), | ||
caseSensitive: route.caseSensitive, | ||
Component: getRouteModuleComponent(routeModule), | ||
// HydrateFallback can only exist on the root route in SPA Mode | ||
HydrateFallback: routeModule.HydrateFallback && (!isSpaMode || route.id === "root") ? 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, | ||
@@ -58,6 +85,7 @@ index: route.index, | ||
handle: routeModule.handle, | ||
// For SPA Mode, all routes are lazy except root. We don't need a full | ||
// implementation here though - just need a `lazy` prop to tell the RR | ||
// rendering where to stop | ||
lazy: isSpaMode && route.id !== "root" ? () => spaModeLazyPromise : undefined, | ||
// 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 | ||
@@ -102,11 +130,23 @@ // has a loader/clientLoader, but it won't ever be called during the static | ||
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) { | ||
throw noActionDefinedError("action", route.id); | ||
} | ||
return fetchServerHandler(request, route); | ||
return fetchServerHandlerAndMaybeUnwrap(request, unwrap, singleFetch); | ||
} | ||
@@ -136,8 +176,3 @@ async function prefetchStylesAndCallHandler(handler) { | ||
...dataRoute, | ||
Component: getRouteModuleComponent(routeModule), | ||
// HydrateFallback can only exist on the root route in SPA Mode | ||
HydrateFallback: routeModule.HydrateFallback && (!isSpaMode || route.id === "root") ? 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, | ||
@@ -152,3 +187,3 @@ shouldRevalidate: needsRevalidation ? wrapShouldRevalidateForHdr(route.id, routeModule.shouldRevalidate, needsRevalidation) : routeModule.shouldRevalidate | ||
params | ||
}) => { | ||
}, singleFetch) => { | ||
try { | ||
@@ -160,3 +195,3 @@ let result = await prefetchStylesAndCallHandler(async () => { | ||
// Call the server when no client loader exists | ||
return fetchServerLoader(request); | ||
return fetchServerLoader(request, false, singleFetch); | ||
} | ||
@@ -178,5 +213,3 @@ return routeModule.clientLoader({ | ||
// Call the server loader for client-side navigations | ||
let result = await fetchServerLoader(request); | ||
let unwrapped = await unwrapServerResponse(result); | ||
return unwrapped; | ||
return fetchServerLoader(request, true, singleFetch); | ||
} | ||
@@ -198,3 +231,3 @@ }); | ||
params | ||
}) => { | ||
}, singleFetch) => { | ||
return prefetchStylesAndCallHandler(async () => { | ||
@@ -206,3 +239,3 @@ invariant(routeModule, "No `routeModule` available for critical-route action"); | ||
} | ||
return fetchServerAction(request); | ||
return fetchServerAction(request, false, singleFetch); | ||
} | ||
@@ -214,5 +247,3 @@ return routeModule.clientAction({ | ||
preventInvalidServerHandlerCall("action", route, isSpaMode); | ||
let result = await fetchServerAction(request); | ||
let unwrapped = await unwrapServerResponse(result); | ||
return unwrapped; | ||
return fetchServerAction(request, true, singleFetch); | ||
} | ||
@@ -229,5 +260,5 @@ }); | ||
request | ||
}) => prefetchStylesAndCallHandler(() => { | ||
}, singleFetch) => prefetchStylesAndCallHandler(() => { | ||
if (isSpaMode) return Promise.resolve(null); | ||
return fetchServerLoader(request); | ||
return fetchServerLoader(request, false, singleFetch); | ||
}); | ||
@@ -238,7 +269,7 @@ } | ||
request | ||
}) => prefetchStylesAndCallHandler(() => { | ||
}, singleFetch) => prefetchStylesAndCallHandler(() => { | ||
if (isSpaMode) { | ||
throw noActionDefinedError("clientAction", route.id); | ||
} | ||
return fetchServerAction(request); | ||
return fetchServerAction(request, false, singleFetch); | ||
}); | ||
@@ -255,9 +286,7 @@ } | ||
let clientLoader = mod.clientLoader; | ||
lazyRoute.loader = args => clientLoader({ | ||
lazyRoute.loader = (args, singleFetch) => clientLoader({ | ||
...args, | ||
async serverLoader() { | ||
preventInvalidServerHandlerCall("loader", route, isSpaMode); | ||
let response = await fetchServerLoader(args.request); | ||
let result = await unwrapServerResponse(response); | ||
return result; | ||
return fetchServerLoader(args.request, true, singleFetch); | ||
} | ||
@@ -268,9 +297,7 @@ }); | ||
let clientAction = mod.clientAction; | ||
lazyRoute.action = args => clientAction({ | ||
lazyRoute.action = (args, singleFetch) => clientAction({ | ||
...args, | ||
async serverAction() { | ||
preventInvalidServerHandlerCall("action", route, isSpaMode); | ||
let response = await fetchServerAction(args.request); | ||
let result = await unwrapServerResponse(response); | ||
return result; | ||
return fetchServerAction(args.request, true, singleFetch); | ||
} | ||
@@ -292,2 +319,4 @@ }); | ||
handle: lazyRoute.handle, | ||
// No need to wrap these in layout since the root route is never | ||
// loaded via route.lazy() | ||
Component: lazyRoute.Component, | ||
@@ -377,2 +406,6 @@ ErrorBoundary: lazyRoute.ErrorBoundary | ||
} | ||
let replace = response.headers.get("X-Remix-Replace"); | ||
if (replace) { | ||
headers["X-Remix-Replace"] = replace; | ||
} | ||
return redirect(url, { | ||
@@ -379,0 +412,0 @@ status, |
/** | ||
* @remix-run/react v0.0.0-nightly-6ec38fa-20240125 | ||
* @remix-run/react v0.0.0-nightly-6ee69c13d-20241003 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
/** | ||
* @remix-run/react v0.0.0-nightly-6ec38fa-20240125 | ||
* @remix-run/react v0.0.0-nightly-6ee69c13d-20241003 | ||
* | ||
@@ -16,2 +16,3 @@ * Copyright (c) Remix Software Inc. | ||
import { createServerRoutes, shouldHydrateRouteLoader } from './routes.js'; | ||
import { StreamTransfer } from './single-fetch.js'; | ||
@@ -26,3 +27,4 @@ /** | ||
url, | ||
abortDelay | ||
abortDelay, | ||
nonce | ||
}) { | ||
@@ -66,3 +68,3 @@ if (typeof url === "string") { | ||
}); | ||
return /*#__PURE__*/React.createElement(RemixContext.Provider, { | ||
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(RemixContext.Provider, { | ||
value: { | ||
@@ -76,3 +78,4 @@ manifest, | ||
serializeError: context.serializeError, | ||
abortDelay | ||
abortDelay, | ||
renderMeta: context.renderMeta | ||
} | ||
@@ -85,5 +88,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-6ec38fa-20240125 | ||
* @remix-run/react v0.0.0-nightly-6ee69c13d-20241003 | ||
* | ||
@@ -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; |
@@ -1,10 +0,10 @@ | ||
export type { ErrorResponse, Fetcher, FetcherWithComponents, FormEncType, FormMethod, FormProps, Location, NavigateFunction, Navigation, Params, Path, ShouldRevalidateFunction, ShouldRevalidateFunctionArgs, SubmitFunction, SubmitOptions, Blocker, 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 { defer, json, redirect, redirectDocument, } from "@remix-run/server-runtime"; | ||
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 { 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"; | ||
@@ -11,0 +11,0 @@ export type { RemixServerProps } from "./server"; |
/** | ||
* @remix-run/react v0.0.0-nightly-6ec38fa-20240125 | ||
* @remix-run/react v0.0.0-nightly-6ee69c13d-20241003 | ||
* | ||
@@ -24,6 +24,10 @@ * Copyright (c) Remix Software Inc. | ||
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 +37,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 +50,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 +83,6 @@ enumerable: true, | ||
}); | ||
Object.defineProperty(exports, 'renderMatches', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.renderMatches; } | ||
}); | ||
Object.defineProperty(exports, 'resolvePath', { | ||
@@ -100,2 +128,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 +181,6 @@ enumerable: true, | ||
}); | ||
Object.defineProperty(exports, 'useRoutes', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.useRoutes; } | ||
}); | ||
Object.defineProperty(exports, 'useSearchParams', { | ||
@@ -170,4 +210,13 @@ enumerable: true, | ||
}); | ||
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; | ||
@@ -174,0 +223,0 @@ exports.Links = components.Links; |
/** | ||
* @remix-run/react v0.0.0-nightly-6ec38fa-20240125 | ||
* @remix-run/react v0.0.0-nightly-6ee69c13d-20241003 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
/** | ||
* @remix-run/react v0.0.0-nightly-6ec38fa-20240125 | ||
* @remix-run/react v0.0.0-nightly-6ee69c13d-20241003 | ||
* | ||
@@ -176,3 +176,3 @@ * Copyright (c) Remix Software Inc. | ||
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 { | ||
@@ -179,0 +179,0 @@ pathname, |
/** | ||
* @remix-run/react v0.0.0-nightly-6ec38fa-20240125 | ||
* @remix-run/react v0.0.0-nightly-6ee69c13d-20241003 | ||
* | ||
@@ -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"; | ||
@@ -15,2 +15,3 @@ import type { LoaderFunction, SerializeFrom } from "@remix-run/server-runtime"; | ||
HydrateFallback?: HydrateFallbackComponent; | ||
Layout?: LayoutComponent; | ||
default: RouteComponent; | ||
@@ -54,2 +55,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 | ||
@@ -56,0 +65,0 @@ * the document on route transitions. |
/** | ||
* @remix-run/react v0.0.0-nightly-6ec38fa-20240125 | ||
* @remix-run/react v0.0.0-nightly-6ee69c13d-20241003 | ||
* | ||
@@ -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,14 @@ * 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 && | ||
@@ -97,3 +111,2 @@ // @ts-expect-error | ||
// boundary and let them see the error in the overlay or the dev server log | ||
console.error(`Error loading route module \`${route.module}\`:`, error); | ||
throw error; | ||
@@ -100,0 +113,0 @@ } |
@@ -30,4 +30,4 @@ import type { HydrationState } from "@remix-run/router"; | ||
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, future: FutureConfig, isSpaMode: boolean, parentId?: string, routesByParentId?: Record<string, Omit<EntryRoute, "children">[]>, needsRevalidation?: Set<string>): 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-6ec38fa-20240125 | ||
* @remix-run/react v0.0.0-nightly-6ee69c13d-20241003 | ||
* | ||
@@ -62,2 +62,34 @@ * Copyright (c) Remix Software Inc. | ||
} | ||
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({ | ||
@@ -70,9 +102,4 @@ Component: () => null | ||
let dataRoute = { | ||
...getRouteComponents(route, routeModule, isSpaMode), | ||
caseSensitive: route.caseSensitive, | ||
Component: getRouteModuleComponent(routeModule), | ||
// HydrateFallback can only exist on the root route in SPA Mode | ||
HydrateFallback: routeModule.HydrateFallback && (!isSpaMode || route.id === "root") ? 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, | ||
@@ -82,6 +109,7 @@ index: route.index, | ||
handle: routeModule.handle, | ||
// For SPA Mode, all routes are lazy except root. We don't need a full | ||
// implementation here though - just need a `lazy` prop to tell the RR | ||
// rendering where to stop | ||
lazy: isSpaMode && route.id !== "root" ? () => spaModeLazyPromise : undefined, | ||
// 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 | ||
@@ -126,11 +154,23 @@ // has a loader/clientLoader, but it won't ever be called during the static | ||
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) { | ||
throw noActionDefinedError("action", route.id); | ||
} | ||
return fetchServerHandler(request, route); | ||
return fetchServerHandlerAndMaybeUnwrap(request, unwrap, singleFetch); | ||
} | ||
@@ -160,8 +200,3 @@ async function prefetchStylesAndCallHandler(handler) { | ||
...dataRoute, | ||
Component: getRouteModuleComponent(routeModule), | ||
// HydrateFallback can only exist on the root route in SPA Mode | ||
HydrateFallback: routeModule.HydrateFallback && (!isSpaMode || route.id === "root") ? 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, | ||
@@ -176,3 +211,3 @@ shouldRevalidate: needsRevalidation ? wrapShouldRevalidateForHdr(route.id, routeModule.shouldRevalidate, needsRevalidation) : routeModule.shouldRevalidate | ||
params | ||
}) => { | ||
}, singleFetch) => { | ||
try { | ||
@@ -184,3 +219,3 @@ let result = await prefetchStylesAndCallHandler(async () => { | ||
// Call the server when no client loader exists | ||
return fetchServerLoader(request); | ||
return fetchServerLoader(request, false, singleFetch); | ||
} | ||
@@ -202,5 +237,3 @@ return routeModule.clientLoader({ | ||
// Call the server loader for client-side navigations | ||
let result = await fetchServerLoader(request); | ||
let unwrapped = await unwrapServerResponse(result); | ||
return unwrapped; | ||
return fetchServerLoader(request, true, singleFetch); | ||
} | ||
@@ -222,3 +255,3 @@ }); | ||
params | ||
}) => { | ||
}, singleFetch) => { | ||
return prefetchStylesAndCallHandler(async () => { | ||
@@ -230,3 +263,3 @@ invariant(routeModule, "No `routeModule` available for critical-route action"); | ||
} | ||
return fetchServerAction(request); | ||
return fetchServerAction(request, false, singleFetch); | ||
} | ||
@@ -238,5 +271,3 @@ return routeModule.clientAction({ | ||
preventInvalidServerHandlerCall("action", route, isSpaMode); | ||
let result = await fetchServerAction(request); | ||
let unwrapped = await unwrapServerResponse(result); | ||
return unwrapped; | ||
return fetchServerAction(request, true, singleFetch); | ||
} | ||
@@ -253,5 +284,5 @@ }); | ||
request | ||
}) => prefetchStylesAndCallHandler(() => { | ||
}, singleFetch) => prefetchStylesAndCallHandler(() => { | ||
if (isSpaMode) return Promise.resolve(null); | ||
return fetchServerLoader(request); | ||
return fetchServerLoader(request, false, singleFetch); | ||
}); | ||
@@ -262,7 +293,7 @@ } | ||
request | ||
}) => prefetchStylesAndCallHandler(() => { | ||
}, singleFetch) => prefetchStylesAndCallHandler(() => { | ||
if (isSpaMode) { | ||
throw noActionDefinedError("clientAction", route.id); | ||
} | ||
return fetchServerAction(request); | ||
return fetchServerAction(request, false, singleFetch); | ||
}); | ||
@@ -279,9 +310,7 @@ } | ||
let clientLoader = mod.clientLoader; | ||
lazyRoute.loader = args => clientLoader({ | ||
lazyRoute.loader = (args, singleFetch) => clientLoader({ | ||
...args, | ||
async serverLoader() { | ||
preventInvalidServerHandlerCall("loader", route, isSpaMode); | ||
let response = await fetchServerLoader(args.request); | ||
let result = await unwrapServerResponse(response); | ||
return result; | ||
return fetchServerLoader(args.request, true, singleFetch); | ||
} | ||
@@ -292,9 +321,7 @@ }); | ||
let clientAction = mod.clientAction; | ||
lazyRoute.action = args => clientAction({ | ||
lazyRoute.action = (args, singleFetch) => clientAction({ | ||
...args, | ||
async serverAction() { | ||
preventInvalidServerHandlerCall("action", route, isSpaMode); | ||
let response = await fetchServerAction(args.request); | ||
let result = await unwrapServerResponse(response); | ||
return result; | ||
return fetchServerAction(args.request, true, singleFetch); | ||
} | ||
@@ -316,2 +343,4 @@ }); | ||
handle: lazyRoute.handle, | ||
// No need to wrap these in layout since the root route is never | ||
// loaded via route.lazy() | ||
Component: lazyRoute.Component, | ||
@@ -401,2 +430,6 @@ ErrorBoundary: lazyRoute.ErrorBoundary | ||
} | ||
let replace = response.headers.get("X-Remix-Replace"); | ||
if (replace) { | ||
headers["X-Remix-Replace"] = replace; | ||
} | ||
return reactRouterDom.redirect(url, { | ||
@@ -403,0 +436,0 @@ status, |
/** | ||
* @remix-run/react v0.0.0-nightly-6ec38fa-20240125 | ||
* @remix-run/react v0.0.0-nightly-6ee69c13d-20241003 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
@@ -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-6ec38fa-20240125 | ||
* @remix-run/react v0.0.0-nightly-6ee69c13d-20241003 | ||
* | ||
@@ -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 | ||
}) { | ||
@@ -90,3 +92,3 @@ if (typeof url === "string") { | ||
}); | ||
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: { | ||
@@ -100,3 +102,4 @@ manifest, | ||
serializeError: context.serializeError, | ||
abortDelay | ||
abortDelay, | ||
renderMeta: context.renderMeta | ||
} | ||
@@ -109,5 +112,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; |
{ | ||
"name": "@remix-run/react", | ||
"version": "0.0.0-nightly-6ec38fa-20240125", | ||
"version": "0.0.0-nightly-6ee69c13d-20241003", | ||
"description": "React DOM bindings for Remix", | ||
@@ -19,8 +19,11 @@ "bugs": { | ||
"dependencies": { | ||
"@remix-run/router": "1.14.2", | ||
"@remix-run/server-runtime": "0.0.0-nightly-6ec38fa-20240125", | ||
"react-router": "6.21.3", | ||
"react-router-dom": "6.21.3" | ||
"@remix-run/router": "1.19.2", | ||
"@remix-run/server-runtime": "0.0.0-nightly-6ee69c13d-20241003", | ||
"react-router": "6.26.2", | ||
"react-router-dom": "6.26.2", | ||
"turbo-stream": "2.4.0" | ||
}, | ||
"devDependencies": { | ||
"@remix-run/node": "0.0.0-nightly-6ee69c13d-20241003", | ||
"@remix-run/react": "0.0.0-nightly-6ee69c13d-20241003", | ||
"@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
55
7561
301774
8
9
13
6
+ Addedturbo-stream@2.4.0
+ Added@remix-run/router@1.19.2(transitive)
+ Added@remix-run/server-runtime@0.0.0-nightly-6ee69c13d-20241003(transitive)
+ Addedreact-router@6.26.2(transitive)
+ Addedreact-router-dom@6.26.2(transitive)
+ Addedturbo-stream@2.4.0(transitive)
- Removed@remix-run/router@1.14.2(transitive)
- Removed@remix-run/server-runtime@0.0.0-nightly-6ec38fa-20240125(transitive)
- Removedreact-router@6.21.3(transitive)
- Removedreact-router-dom@6.21.3(transitive)
Updated@remix-run/router@1.19.2
Updated@remix-run/server-runtime@0.0.0-nightly-6ee69c13d-20241003
Updatedreact-router@6.26.2
Updatedreact-router-dom@6.26.2