@remix-run/react
Advanced tools
Comparing version 0.0.0-nightly-1ac5c50dd-20240726 to 0.0.0-nightly-1afe78c3e-20250122
/** | ||
* @remix-run/react v0.0.0-nightly-1ac5c50dd-20240726 | ||
* @remix-run/react v0.0.0-nightly-1afe78c3e-20250122 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
@@ -7,3 +7,2 @@ import type { HydrationState, Router } from "@remix-run/router"; | ||
var __remixContext: { | ||
ssrMatches: string[]; | ||
basename?: string; | ||
@@ -26,2 +25,3 @@ state: HydrationState; | ||
var __remixRevalidation: number | undefined; | ||
var __remixHdrActive: boolean; | ||
var __remixClearCriticalCss: (() => void) | undefined; | ||
@@ -28,0 +28,0 @@ var $RefreshRuntime$: { |
/** | ||
* @remix-run/react v0.0.0-nightly-1ac5c50dd-20240726 | ||
* @remix-run/react v0.0.0-nightly-1afe78c3e-20250122 | ||
* | ||
@@ -77,3 +77,3 @@ * Copyright (c) Remix Software Inc. | ||
// snapshot is decoded into window.__remixContext.state | ||
if (window.__remixContext.future.unstable_singleFetch) { | ||
if (window.__remixContext.future.v3_singleFetch) { | ||
// Note: `stateDecodingPromise` is not coupled to `router` - we'll reach this | ||
@@ -116,24 +116,2 @@ // code potentially many times waiting for our state to arrive, but we'll | ||
let initialMatches = reactRouterDom.matchRoutes(routes$1, window.location, window.__remixContext.basename); | ||
// Hard reload if the matches we rendered on the server aren't the matches | ||
// we matched in the client, otherwise we'll try to hydrate without the | ||
// right modules and throw a hydration error, which can put React into an | ||
// infinite hydration loop when hydrating the full `<html>` document. | ||
// 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. | ||
let ssrMatches = window.__remixContext.ssrMatches; | ||
let hasDifferentSSRMatches = (initialMatches || []).length !== ssrMatches.length || !(initialMatches || []).every((m, i) => ssrMatches[i] === m.route.id); | ||
if (hasDifferentSSRMatches && !window.__remixContext.isSpaMode) { | ||
let ssr = ssrMatches.join(","); | ||
let client = (initialMatches || []).map(m => m.route.id).join(","); | ||
let errorMsg = `SSR Matches (${ssr}) do not match client matches (${client}) at ` + `time of hydration , 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); | ||
} | ||
if (initialMatches) { | ||
@@ -164,6 +142,2 @@ for (let match of initialMatches) { | ||
} | ||
let { | ||
enabled: isFogOfWarEnabled, | ||
patchRoutesOnMiss | ||
} = fogOfWar.initFogOfWar(window.__remixManifest, window.__remixRouteModules, window.__remixContext.future, window.__remixContext.isSpaMode, window.__remixContext.basename); | ||
@@ -183,10 +157,8 @@ // We don't use createBrowserRouter here because we need fine-grained control | ||
// Single fetch enables this underlying behavior | ||
v7_skipActionErrorRevalidation: window.__remixContext.future.unstable_singleFetch === true | ||
v7_skipActionErrorRevalidation: window.__remixContext.future.v3_singleFetch === true | ||
}, | ||
hydrationData, | ||
mapRouteProperties: reactRouter.UNSAFE_mapRouteProperties, | ||
unstable_dataStrategy: window.__remixContext.future.unstable_singleFetch ? singleFetch.getSingleFetchDataStrategy(window.__remixManifest, window.__remixRouteModules) : undefined, | ||
...(isFogOfWarEnabled ? { | ||
unstable_patchRoutesOnMiss: patchRoutesOnMiss | ||
} : {}) | ||
dataStrategy: window.__remixContext.future.v3_singleFetch ? singleFetch.getSingleFetchDataStrategy(window.__remixManifest, window.__remixRouteModules, () => router) : undefined, | ||
patchRoutesOnNavigation: fogOfWar.getPatchRoutesOnNavigationFunction(window.__remixManifest, window.__remixRouteModules, window.__remixContext.future, window.__remixContext.isSpaMode, window.__remixContext.basename) | ||
}); | ||
@@ -214,3 +186,3 @@ | ||
// server HTML. This allows our HMR logic to clear the critical CSS state. | ||
// eslint-disable-next-line react-hooks/rules-of-hooks | ||
let [criticalCss, setCriticalCss] = React__namespace.useState(process.env.NODE_ENV === "development" ? window.__remixContext.criticalCss : undefined); | ||
@@ -224,6 +196,4 @@ if (process.env.NODE_ENV === "development") { | ||
// 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(() => { | ||
@@ -237,4 +207,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(() => { | ||
@@ -247,4 +215,2 @@ return router.subscribe(newState => { | ||
}, [location]); | ||
// eslint-disable-next-line react-hooks/rules-of-hooks | ||
fogOfWar.useFogOFWarDiscovery(router, window.__remixManifest, window.__remixRouteModules, window.__remixContext.future, window.__remixContext.isSpaMode); | ||
@@ -276,3 +242,3 @@ | ||
} | ||
}))), window.__remixContext.future.unstable_singleFetch ? /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null) : null) | ||
}))), window.__remixContext.future.v3_singleFetch ? /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null) : null) | ||
); | ||
@@ -279,0 +245,0 @@ } |
/** | ||
* @remix-run/react v0.0.0-nightly-1ac5c50dd-20240726 | ||
* @remix-run/react v0.0.0-nightly-1afe78c3e-20250122 | ||
* | ||
@@ -328,7 +328,46 @@ * Copyright (c) Remix Software Inc. | ||
let { | ||
loaderData, | ||
matches | ||
} = useDataRouterStateContext(); | ||
let newMatchesForData = React__namespace.useMemo(() => links.getNewMatchesForLinks(page, nextMatches, matches, manifest, location, "data"), [page, nextMatches, matches, manifest, location]); | ||
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 newMatchesForData = React__namespace.useMemo(() => links.getNewMatchesForLinks(page, nextMatches, matches, manifest, location, future, "data"), [page, nextMatches, matches, manifest, location, future]); | ||
let dataHrefs = React__namespace.useMemo(() => { | ||
if (!future.v3_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.v3_singleFetch, loaderData, location, manifest, newMatchesForData, nextMatches, page, routeModules]); | ||
let newMatchesForAssets = React__namespace.useMemo(() => links.getNewMatchesForLinks(page, nextMatches, matches, manifest, location, future, "assets"), [page, nextMatches, matches, manifest, location, future]); | ||
let moduleHrefs = React__namespace.useMemo(() => links.getModuleLinkHrefs(newMatchesForAssets, manifest), [newMatchesForAssets, manifest]); | ||
@@ -339,25 +378,9 @@ | ||
let keyedPrefetchLinks = useKeyedPrefetchLinks(newMatchesForAssets); | ||
let linksToRender = null; | ||
if (!future.unstable_singleFetch) { | ||
// Non-single-fetch prefetching | ||
linksToRender = dataHrefs.map(href => /*#__PURE__*/React__namespace.createElement("link", _rollupPluginBabelHelpers["extends"]({ | ||
key: href, | ||
rel: "prefetch", | ||
as: "fetch", | ||
href: href | ||
}, linkProps))); | ||
} else if (newMatchesForData.length > 0) { | ||
// Single-fetch with routes that require data | ||
let url = singleFetch.addRevalidationParam(manifest, routeModules, nextMatches.map(m => m.route), newMatchesForData.map(m => m.route), singleFetch.singleFetchUrl(page)); | ||
if (url.searchParams.get("_routes") !== "") { | ||
linksToRender = /*#__PURE__*/React__namespace.createElement("link", _rollupPluginBabelHelpers["extends"]({ | ||
key: url.pathname + url.search, | ||
rel: "prefetch", | ||
as: "fetch", | ||
href: url.pathname + url.search | ||
}, linkProps)); | ||
} | ||
} else ; | ||
return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, linksToRender, moduleHrefs.map(href => /*#__PURE__*/React__namespace.createElement("link", _rollupPluginBabelHelpers["extends"]({ | ||
return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, dataHrefs.map(href => /*#__PURE__*/React__namespace.createElement("link", _rollupPluginBabelHelpers["extends"]({ | ||
key: href, | ||
rel: "prefetch", | ||
as: "fetch", | ||
href: href | ||
}, linkProps))), moduleHrefs.map(href => /*#__PURE__*/React__namespace.createElement("link", _rollupPluginBabelHelpers["extends"]({ | ||
key: href, | ||
rel: "modulepreload", | ||
@@ -583,3 +606,3 @@ href: href | ||
var _manifest$hmr; | ||
let streamScript = future.unstable_singleFetch ? | ||
let streamScript = future.v3_singleFetch ? | ||
// prettier-ignore | ||
@@ -590,3 +613,3 @@ "window.__remixContext.stream = new ReadableStream({" + "start(controller){" + "window.__remixContext.streamController = controller;" + "}" + "}).pipeThrough(new TextEncoderStream());" : ""; | ||
// 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; | ||
let activeDeferreds = future.v3_singleFetch ? undefined : staticContext === null || staticContext === void 0 ? void 0 : staticContext.activeDeferreds; | ||
@@ -593,0 +616,0 @@ // This sets up the __remixContext with utility functions used by the |
/** | ||
* @remix-run/react v0.0.0-nightly-1ac5c50dd-20240726 | ||
* @remix-run/react v0.0.0-nightly-1afe78c3e-20250122 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
@@ -35,4 +35,4 @@ import type { StaticHandlerContext } from "@remix-run/router"; | ||
v3_relativeSplatPath: boolean; | ||
unstable_lazyRouteDiscovery: boolean; | ||
unstable_singleFetch: boolean; | ||
v3_lazyRouteDiscovery: boolean; | ||
v3_singleFetch: boolean; | ||
} | ||
@@ -39,0 +39,0 @@ export interface AssetsManifest { |
/** | ||
* @remix-run/react v0.0.0-nightly-1ac5c50dd-20240726 | ||
* @remix-run/react v0.0.0-nightly-1afe78c3e-20250122 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
/** | ||
* @remix-run/react v0.0.0-nightly-1ac5c50dd-20240726 | ||
* @remix-run/react v0.0.0-nightly-1afe78c3e-20250122 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
/** | ||
* @remix-run/react v0.0.0-nightly-1ac5c50dd-20240726 | ||
* @remix-run/react v0.0.0-nightly-1afe78c3e-20250122 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
/** | ||
* @remix-run/react v0.0.0-nightly-1ac5c50dd-20240726 | ||
* @remix-run/react v0.0.0-nightly-1afe78c3e-20250122 | ||
* | ||
@@ -21,3 +21,3 @@ * Copyright (c) Remix Software Inc. | ||
import invariant from './invariant.js'; | ||
import { initFogOfWar, useFogOFWarDiscovery } from './fog-of-war.js'; | ||
import { getPatchRoutesOnNavigationFunction, useFogOFWarDiscovery } from './fog-of-war.js'; | ||
@@ -118,3 +118,3 @@ /* eslint-disable prefer-let/prefer-let */ | ||
// snapshot is decoded into window.__remixContext.state | ||
if (window.__remixContext.future.unstable_singleFetch) { | ||
if (window.__remixContext.future.v3_singleFetch) { | ||
// Note: `stateDecodingPromise` is not coupled to `router` - we'll reach this | ||
@@ -157,24 +157,2 @@ // code potentially many times waiting for our state to arrive, but we'll | ||
let initialMatches = matchRoutes(routes, window.location, window.__remixContext.basename); | ||
// Hard reload if the matches we rendered on the server aren't the matches | ||
// we matched in the client, otherwise we'll try to hydrate without the | ||
// right modules and throw a hydration error, which can put React into an | ||
// infinite hydration loop when hydrating the full `<html>` document. | ||
// 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. | ||
let ssrMatches = window.__remixContext.ssrMatches; | ||
let hasDifferentSSRMatches = (initialMatches || []).length !== ssrMatches.length || !(initialMatches || []).every((m, i) => ssrMatches[i] === m.route.id); | ||
if (hasDifferentSSRMatches && !window.__remixContext.isSpaMode) { | ||
let ssr = ssrMatches.join(","); | ||
let client = (initialMatches || []).map(m => m.route.id).join(","); | ||
let errorMsg = `SSR Matches (${ssr}) do not match client matches (${client}) at ` + `time of hydration , 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); | ||
} | ||
if (initialMatches) { | ||
@@ -205,6 +183,2 @@ for (let match of initialMatches) { | ||
} | ||
let { | ||
enabled: isFogOfWarEnabled, | ||
patchRoutesOnMiss | ||
} = initFogOfWar(window.__remixManifest, window.__remixRouteModules, window.__remixContext.future, window.__remixContext.isSpaMode, window.__remixContext.basename); | ||
@@ -224,10 +198,8 @@ // We don't use createBrowserRouter here because we need fine-grained control | ||
// Single fetch enables this underlying behavior | ||
v7_skipActionErrorRevalidation: window.__remixContext.future.unstable_singleFetch === true | ||
v7_skipActionErrorRevalidation: window.__remixContext.future.v3_singleFetch === true | ||
}, | ||
hydrationData, | ||
mapRouteProperties: UNSAFE_mapRouteProperties, | ||
unstable_dataStrategy: window.__remixContext.future.unstable_singleFetch ? getSingleFetchDataStrategy(window.__remixManifest, window.__remixRouteModules) : undefined, | ||
...(isFogOfWarEnabled ? { | ||
unstable_patchRoutesOnMiss: patchRoutesOnMiss | ||
} : {}) | ||
dataStrategy: window.__remixContext.future.v3_singleFetch ? getSingleFetchDataStrategy(window.__remixManifest, window.__remixRouteModules, () => router) : undefined, | ||
patchRoutesOnNavigation: getPatchRoutesOnNavigationFunction(window.__remixManifest, window.__remixRouteModules, window.__remixContext.future, window.__remixContext.isSpaMode, window.__remixContext.basename) | ||
}); | ||
@@ -255,3 +227,3 @@ | ||
// server HTML. This allows our HMR logic to clear the critical CSS state. | ||
// eslint-disable-next-line react-hooks/rules-of-hooks | ||
let [criticalCss, setCriticalCss] = React.useState(process.env.NODE_ENV === "development" ? window.__remixContext.criticalCss : undefined); | ||
@@ -265,6 +237,4 @@ if (process.env.NODE_ENV === "development") { | ||
// 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(() => { | ||
@@ -278,4 +248,2 @@ // If we had to run clientLoaders on hydration, we delay initialization until | ||
}, []); | ||
// eslint-disable-next-line react-hooks/rules-of-hooks | ||
React.useLayoutEffect(() => { | ||
@@ -288,4 +256,2 @@ return router.subscribe(newState => { | ||
}, [location]); | ||
// eslint-disable-next-line react-hooks/rules-of-hooks | ||
useFogOFWarDiscovery(router, window.__remixManifest, window.__remixRouteModules, window.__remixContext.future, window.__remixContext.isSpaMode); | ||
@@ -317,3 +283,3 @@ | ||
} | ||
}))), window.__remixContext.future.unstable_singleFetch ? /*#__PURE__*/React.createElement(React.Fragment, null) : null) | ||
}))), window.__remixContext.future.v3_singleFetch ? /*#__PURE__*/React.createElement(React.Fragment, null) : null) | ||
); | ||
@@ -320,0 +286,0 @@ } |
/** | ||
* @remix-run/react v0.0.0-nightly-1ac5c50dd-20240726 | ||
* @remix-run/react v0.0.0-nightly-1afe78c3e-20250122 | ||
* | ||
@@ -17,3 +17,3 @@ * Copyright (c) Remix Software Inc. | ||
import { escapeHtml, createHtml } from './markup.js'; | ||
import { addRevalidationParam, singleFetchUrl } from './single-fetch.js'; | ||
import { singleFetchUrl } from './single-fetch.js'; | ||
import { isFogOfWarEnabled, getPartialManifest } from './fog-of-war.js'; | ||
@@ -305,7 +305,46 @@ | ||
let { | ||
loaderData, | ||
matches | ||
} = useDataRouterStateContext(); | ||
let newMatchesForData = React.useMemo(() => getNewMatchesForLinks(page, nextMatches, matches, manifest, location, "data"), [page, nextMatches, matches, manifest, location]); | ||
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 newMatchesForData = React.useMemo(() => getNewMatchesForLinks(page, nextMatches, matches, manifest, location, future, "data"), [page, nextMatches, matches, manifest, location, future]); | ||
let dataHrefs = React.useMemo(() => { | ||
if (!future.v3_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.v3_singleFetch, loaderData, location, manifest, newMatchesForData, nextMatches, page, routeModules]); | ||
let newMatchesForAssets = React.useMemo(() => getNewMatchesForLinks(page, nextMatches, matches, manifest, location, future, "assets"), [page, nextMatches, matches, manifest, location, future]); | ||
let moduleHrefs = React.useMemo(() => getModuleLinkHrefs(newMatchesForAssets, manifest), [newMatchesForAssets, manifest]); | ||
@@ -316,25 +355,9 @@ | ||
let keyedPrefetchLinks = useKeyedPrefetchLinks(newMatchesForAssets); | ||
let linksToRender = null; | ||
if (!future.unstable_singleFetch) { | ||
// Non-single-fetch prefetching | ||
linksToRender = dataHrefs.map(href => /*#__PURE__*/React.createElement("link", _extends({ | ||
key: href, | ||
rel: "prefetch", | ||
as: "fetch", | ||
href: href | ||
}, linkProps))); | ||
} else if (newMatchesForData.length > 0) { | ||
// Single-fetch with routes that require data | ||
let url = addRevalidationParam(manifest, routeModules, nextMatches.map(m => m.route), newMatchesForData.map(m => m.route), singleFetchUrl(page)); | ||
if (url.searchParams.get("_routes") !== "") { | ||
linksToRender = /*#__PURE__*/React.createElement("link", _extends({ | ||
key: url.pathname + url.search, | ||
rel: "prefetch", | ||
as: "fetch", | ||
href: url.pathname + url.search | ||
}, linkProps)); | ||
} | ||
} else ; | ||
return /*#__PURE__*/React.createElement(React.Fragment, null, linksToRender, moduleHrefs.map(href => /*#__PURE__*/React.createElement("link", _extends({ | ||
return /*#__PURE__*/React.createElement(React.Fragment, null, dataHrefs.map(href => /*#__PURE__*/React.createElement("link", _extends({ | ||
key: href, | ||
rel: "prefetch", | ||
as: "fetch", | ||
href: href | ||
}, linkProps))), moduleHrefs.map(href => /*#__PURE__*/React.createElement("link", _extends({ | ||
key: href, | ||
rel: "modulepreload", | ||
@@ -560,3 +583,3 @@ href: href | ||
var _manifest$hmr; | ||
let streamScript = future.unstable_singleFetch ? | ||
let streamScript = future.v3_singleFetch ? | ||
// prettier-ignore | ||
@@ -567,3 +590,3 @@ "window.__remixContext.stream = new ReadableStream({" + "start(controller){" + "window.__remixContext.streamController = controller;" + "}" + "}).pipeThrough(new TextEncoderStream());" : ""; | ||
// 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; | ||
let activeDeferreds = future.v3_singleFetch ? undefined : staticContext === null || staticContext === void 0 ? void 0 : staticContext.activeDeferreds; | ||
@@ -570,0 +593,0 @@ // This sets up the __remixContext with utility functions used by the |
/** | ||
* @remix-run/react v0.0.0-nightly-1ac5c50dd-20240726 | ||
* @remix-run/react v0.0.0-nightly-1afe78c3e-20250122 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
/** | ||
* @remix-run/react v0.0.0-nightly-1ac5c50dd-20240726 | ||
* @remix-run/react v0.0.0-nightly-1afe78c3e-20250122 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
/** | ||
* @remix-run/react v0.0.0-nightly-1ac5c50dd-20240726 | ||
* @remix-run/react v0.0.0-nightly-1afe78c3e-20250122 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
/** | ||
* @remix-run/react v0.0.0-nightly-1ac5c50dd-20240726 | ||
* @remix-run/react v0.0.0-nightly-1afe78c3e-20250122 | ||
* | ||
@@ -27,4 +27,5 @@ * Copyright (c) Remix Software Inc. | ||
"💿 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." | ||
); | ||
@@ -31,0 +32,0 @@ ` |
/** | ||
* @remix-run/react v0.0.0-nightly-1ac5c50dd-20240726 | ||
* @remix-run/react v0.0.0-nightly-1afe78c3e-20250122 | ||
* | ||
@@ -15,8 +15,15 @@ * Copyright (c) Remix Software Inc. | ||
// Currently rendered links that may need prefetching | ||
const nextPaths = new Set(); | ||
// FIFO queue of previously discovered routes to prevent re-calling on | ||
// subsequent navigations to the same path | ||
const discoveredPathsMaxSize = 1000; | ||
const discoveredPaths = new Set(); | ||
// 7.5k to come in under the ~8k limit for most browsers | ||
// https://stackoverflow.com/a/417184 | ||
const URL_LIMIT = 7680; | ||
let fogOfWar = null; | ||
function isFogOfWarEnabled(future, isSpaMode) { | ||
return future.unstable_lazyRouteDiscovery === true && !isSpaMode; | ||
return future.v3_lazyRouteDiscovery === true && !isSpaMode; | ||
} | ||
@@ -52,24 +59,14 @@ function getPartialManifest(manifest, router) { | ||
} | ||
function initFogOfWar(manifest, routeModules, future, isSpaMode, basename) { | ||
function getPatchRoutesOnNavigationFunction(manifest, routeModules, future, isSpaMode, basename) { | ||
if (!isFogOfWarEnabled(future, isSpaMode)) { | ||
return { | ||
enabled: false | ||
}; | ||
return undefined; | ||
} | ||
fogOfWar = { | ||
nextPaths: new Set(), | ||
knownGoodPaths: new Set(), | ||
known404Paths: new Set() | ||
}; | ||
return { | ||
enabled: true, | ||
patchRoutesOnMiss: async ({ | ||
path, | ||
patch | ||
}) => { | ||
if (fogOfWar.known404Paths.has(path) || fogOfWar.knownGoodPaths.has(path)) { | ||
return; | ||
} | ||
await fetchAndApplyManifestPatches([path], fogOfWar, manifest, routeModules, future, isSpaMode, basename, patch); | ||
return async ({ | ||
path, | ||
patch | ||
}) => { | ||
if (discoveredPaths.has(path)) { | ||
return; | ||
} | ||
await fetchAndApplyManifestPatches([path], manifest, routeModules, future, isSpaMode, basename, patch); | ||
}; | ||
@@ -92,11 +89,5 @@ } | ||
let url = new URL(path, window.location.origin); | ||
let { | ||
knownGoodPaths, | ||
known404Paths, | ||
nextPaths | ||
} = fogOfWar; | ||
if (knownGoodPaths.has(url.pathname) || known404Paths.has(url.pathname)) { | ||
return; | ||
if (!discoveredPaths.has(url.pathname)) { | ||
nextPaths.add(url.pathname); | ||
} | ||
nextPaths.add(url.pathname); | ||
} | ||
@@ -106,3 +97,9 @@ | ||
async function fetchPatches() { | ||
let lazyPaths = getFogOfWarPaths(fogOfWar); | ||
let lazyPaths = Array.from(nextPaths.keys()).filter(path => { | ||
if (discoveredPaths.has(path)) { | ||
nextPaths.delete(path); | ||
return false; | ||
} | ||
return true; | ||
}); | ||
if (lazyPaths.length === 0) { | ||
@@ -112,3 +109,3 @@ return; | ||
try { | ||
await fetchAndApplyManifestPatches(lazyPaths, fogOfWar, manifest, routeModules, future, isSpaMode, router.basename, router.patchRoutes); | ||
await fetchAndApplyManifestPatches(lazyPaths, manifest, routeModules, future, isSpaMode, router.basename, router.patchRoutes); | ||
} catch (e) { | ||
@@ -155,30 +152,7 @@ console.error("Failed to fetch manifest patches", e); | ||
} | ||
function getFogOfWarPaths(fogOfWar, router) { | ||
let { | ||
knownGoodPaths, | ||
known404Paths, | ||
nextPaths | ||
} = fogOfWar; | ||
return Array.from(nextPaths.keys()).filter(path => { | ||
if (knownGoodPaths.has(path)) { | ||
nextPaths.delete(path); | ||
return false; | ||
} | ||
if (known404Paths.has(path)) { | ||
nextPaths.delete(path); | ||
return false; | ||
} | ||
return true; | ||
}); | ||
} | ||
async function fetchAndApplyManifestPatches(paths, _fogOfWar, manifest, routeModules, future, isSpaMode, basename, patchRoutes) { | ||
let { | ||
nextPaths, | ||
knownGoodPaths, | ||
known404Paths | ||
} = _fogOfWar; | ||
async function fetchAndApplyManifestPatches(paths, manifest, routeModules, future, isSpaMode, basename, patchRoutes) { | ||
let manifestPath = `${basename ?? "/"}/__manifest`.replace(/\/+/g, "/"); | ||
let url = new URL(manifestPath, window.location.origin); | ||
paths.sort().forEach(path => url.searchParams.append("p", path)); | ||
url.searchParams.set("version", manifest.version); | ||
paths.forEach(path => url.searchParams.append("p", path)); | ||
@@ -198,9 +172,7 @@ // If the URL is nearing the ~8k limit on GET requests, skip this optimization | ||
} | ||
let data = await res.json(); | ||
let serverPatches = await res.json(); | ||
// Capture this before we apply the patches to the manifest | ||
// Patch routes we don't know about yet into the manifest | ||
let knownRoutes = new Set(Object.keys(manifest.routes)); | ||
// Patch routes we don't know about yet into the manifest | ||
let patches = Object.values(data.patches).reduce((acc, route) => !knownRoutes.has(route.id) ? Object.assign(acc, { | ||
let patches = Object.values(serverPatches).reduce((acc, route) => !knownRoutes.has(route.id) ? Object.assign(acc, { | ||
[route.id]: route | ||
@@ -210,8 +182,5 @@ }) : acc, {}); | ||
// Track legit 404s so we don't try to fetch them again | ||
data.notFoundPaths.forEach(p => known404Paths.add(p)); | ||
// Track discovered paths so we don't have to fetch them again | ||
paths.forEach(p => addToFifoQueue(p, discoveredPaths)); | ||
// Track matched paths so we don't have to fetch them again | ||
paths.forEach(p => knownGoodPaths.add(p)); | ||
// Identify all parentIds for which we have new children to add and patch | ||
@@ -227,2 +196,9 @@ // in their new children | ||
} | ||
function addToFifoQueue(path, queue) { | ||
if (queue.size >= discoveredPathsMaxSize) { | ||
let first = queue.values().next().value; | ||
if (typeof first === "string") queue.delete(first); | ||
} | ||
queue.add(path); | ||
} | ||
@@ -239,2 +215,2 @@ // Thanks Josh! | ||
export { fetchAndApplyManifestPatches, getPartialManifest, initFogOfWar, isFogOfWarEnabled, useFogOFWarDiscovery }; | ||
export { fetchAndApplyManifestPatches, getPartialManifest, getPatchRoutesOnNavigationFunction, isFogOfWarEnabled, useFogOFWarDiscovery }; |
/** | ||
* @remix-run/react v0.0.0-nightly-1ac5c50dd-20240726 | ||
* @remix-run/react v0.0.0-nightly-1afe78c3e-20250122 | ||
* | ||
@@ -11,4 +11,4 @@ * Copyright (c) Remix Software Inc. | ||
*/ | ||
export { Navigate, NavigationType, Outlet, Route, Routes, createPath, createRoutesFromChildren, createRoutesFromElements, createSearchParams, generatePath, isRouteErrorResponse, matchPath, matchRoutes, parsePath, renderMatches, resolvePath, unstable_usePrompt, unstable_useViewTransitionState, useAsyncError, useAsyncValue, useBeforeUnload, useBlocker, useFetchers, useFormAction, useHref, useInRouterContext, useLinkClickHandler, useLocation, useMatch, useNavigate, useNavigation, useNavigationType, useOutlet, useOutletContext, useParams, useResolvedPath, useRevalidator, useRouteError, useRoutes, useSearchParams, useSubmit } from 'react-router-dom'; | ||
export { defer, json, redirect, redirectDocument, replace, unstable_data } from '@remix-run/server-runtime'; | ||
export { Navigate, NavigationType, Outlet, Route, Routes, createPath, createRoutesFromChildren, createRoutesFromElements, createSearchParams, generatePath, isRouteErrorResponse, matchPath, matchRoutes, parsePath, renderMatches, resolvePath, unstable_usePrompt, useAsyncError, useAsyncValue, useBeforeUnload, useBlocker, useFetchers, useFormAction, useHref, useInRouterContext, useLinkClickHandler, useLocation, useMatch, useNavigate, useNavigation, useNavigationType, useOutlet, useOutletContext, useParams, useResolvedPath, useRevalidator, useRouteError, useRoutes, useSearchParams, useSubmit, useViewTransitionState } from 'react-router-dom'; | ||
export { data, defer, json, redirect, redirectDocument, replace } from '@remix-run/server-runtime'; | ||
export { RemixBrowser } from './browser.js'; | ||
@@ -18,2 +18,1 @@ export { Await, Form, Link, Links, LiveReload, Meta, NavLink, PrefetchPageLinks, Scripts, RemixContext as UNSAFE_RemixContext, useActionData, useFetcher, useLoaderData, useMatches, useRouteLoaderData } from './components.js'; | ||
export { RemixServer } from './server.js'; | ||
export { defineClientAction as unstable_defineClientAction, defineClientLoader as unstable_defineClientLoader } from './single-fetch.js'; |
/** | ||
* @remix-run/react v0.0.0-nightly-1ac5c50dd-20240726 | ||
* @remix-run/react v0.0.0-nightly-1afe78c3e-20250122 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
/** | ||
* @remix-run/react v0.0.0-nightly-1ac5c50dd-20240726 | ||
* @remix-run/react v0.0.0-nightly-1afe78c3e-20250122 | ||
* | ||
@@ -120,3 +120,3 @@ * Copyright (c) Remix Software Inc. | ||
// This is ridiculously identical to transition.ts `filterMatchesToLoad` | ||
function getNewMatchesForLinks(page, nextMatches, currentMatches, manifest, location, mode) { | ||
function getNewMatchesForLinks(page, nextMatches, currentMatches, manifest, location, future, mode) { | ||
let path = parsePathPatch(page); | ||
@@ -140,3 +140,3 @@ let isNew = (match, index) => { | ||
// version doesn't care about submissions | ||
let newMatches = mode === "data" && location.search !== path.search ? | ||
let newMatches = mode === "data" && (future.v3_singleFetch || location.search !== path.search) ? | ||
// this is really similar to stuff in transition.ts, maybe somebody smarter | ||
@@ -152,2 +152,7 @@ // than me (or in less of a hurry) can share some of it. You're the best. | ||
} | ||
// For reused routes on GET navigations, by default: | ||
// - Single fetch always revalidates | ||
// - Multi fetch revalidates if search params changed | ||
let defaultShouldRevalidate = future.v3_singleFetch || location.search !== path.search; | ||
if (match.route.shouldRevalidate) { | ||
@@ -160,3 +165,3 @@ var _currentMatches$; | ||
nextParams: match.params, | ||
defaultShouldRevalidate: true | ||
defaultShouldRevalidate | ||
}); | ||
@@ -167,3 +172,3 @@ if (typeof routeChoice === "boolean") { | ||
} | ||
return true; | ||
return defaultShouldRevalidate; | ||
}) : nextMatches.filter((match, index) => { | ||
@@ -170,0 +175,0 @@ let manifestRoute = manifest.routes[match.route.id]; |
/** | ||
* @remix-run/react v0.0.0-nightly-1ac5c50dd-20240726 | ||
* @remix-run/react v0.0.0-nightly-1afe78c3e-20250122 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
/** | ||
* @remix-run/react v0.0.0-nightly-1ac5c50dd-20240726 | ||
* @remix-run/react v0.0.0-nightly-1afe78c3e-20250122 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
/** | ||
* @remix-run/react v0.0.0-nightly-1ac5c50dd-20240726 | ||
* @remix-run/react v0.0.0-nightly-1afe78c3e-20250122 | ||
* | ||
@@ -174,3 +174,3 @@ * Copyright (c) Remix Software Inc. | ||
handle: routeModule.handle, | ||
shouldRevalidate: needsRevalidation ? wrapShouldRevalidateForHdr(route.id, routeModule.shouldRevalidate, needsRevalidation) : routeModule.shouldRevalidate | ||
shouldRevalidate: getShouldRevalidateFunction(future, routeModule, route.id, needsRevalidation) | ||
}); | ||
@@ -200,6 +200,9 @@ let initialData = initialState === null || initialState === void 0 ? void 0 : (_initialState$loaderD = initialState.loaderData) === null || _initialState$loaderD === void 0 ? void 0 : _initialState$loaderD[route.id]; | ||
if (isHydrationRequest) { | ||
if (initialData !== undefined) { | ||
return initialData; | ||
} | ||
if (initialError !== undefined) { | ||
throw initialError; | ||
} | ||
return initialData; | ||
return null; | ||
} | ||
@@ -293,5 +296,2 @@ | ||
} | ||
if (needsRevalidation) { | ||
lazyRoute.shouldRevalidate = wrapShouldRevalidateForHdr(route.id, mod.shouldRevalidate, needsRevalidation); | ||
} | ||
return { | ||
@@ -305,3 +305,3 @@ ...(lazyRoute.loader ? { | ||
hasErrorBoundary: lazyRoute.hasErrorBoundary, | ||
shouldRevalidate: lazyRoute.shouldRevalidate, | ||
shouldRevalidate: getShouldRevalidateFunction(future, lazyRoute, route.id, needsRevalidation), | ||
handle: lazyRoute.handle, | ||
@@ -320,3 +320,20 @@ // No need to wrap these in layout since the root route is never | ||
} | ||
function getShouldRevalidateFunction(future, route, routeId, needsRevalidation) { | ||
// During HDR we force revalidation for updated routes | ||
if (needsRevalidation) { | ||
return wrapShouldRevalidateForHdr(routeId, route.shouldRevalidate, needsRevalidation); | ||
} | ||
// Single fetch revalidates by default, so override the RR default value which | ||
// matches the multi-fetch behavior with `true` | ||
if (future.v3_singleFetch && route.shouldRevalidate) { | ||
let fn = route.shouldRevalidate; | ||
return opts => fn({ | ||
...opts, | ||
defaultShouldRevalidate: true | ||
}); | ||
} | ||
return route.shouldRevalidate; | ||
} | ||
// When an HMR / HDR update happens we opt out of all user-defined | ||
@@ -323,0 +340,0 @@ // revalidation logic and force a revalidation on the first call |
/** | ||
* @remix-run/react v0.0.0-nightly-1ac5c50dd-20240726 | ||
* @remix-run/react v0.0.0-nightly-1afe78c3e-20250122 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
/** | ||
* @remix-run/react v0.0.0-nightly-1ac5c50dd-20240726 | ||
* @remix-run/react v0.0.0-nightly-1afe78c3e-20250122 | ||
* | ||
@@ -84,3 +84,3 @@ * Copyright (c) Remix Software Inc. | ||
hydrate: false | ||
}))), context.future.unstable_singleFetch && context.serverHandoffStream ? /*#__PURE__*/React.createElement(React.Suspense, null, /*#__PURE__*/React.createElement(StreamTransfer, { | ||
}))), context.future.v3_singleFetch && context.serverHandoffStream ? /*#__PURE__*/React.createElement(React.Suspense, null, /*#__PURE__*/React.createElement(StreamTransfer, { | ||
context: context, | ||
@@ -87,0 +87,0 @@ identifier: 0, |
/** | ||
* @remix-run/react v0.0.0-nightly-1ac5c50dd-20240726 | ||
* @remix-run/react v0.0.0-nightly-1afe78c3e-20250122 | ||
* | ||
@@ -12,3 +12,3 @@ * Copyright (c) Remix Software Inc. | ||
import * as React from 'react'; | ||
import { UNSAFE_ErrorResponseImpl, isRouteErrorResponse, unstable_data, redirect } from '@remix-run/router'; | ||
import { isRouteErrorResponse, data, UNSAFE_ErrorResponseImpl, redirect } from '@remix-run/router'; | ||
import { UNSAFE_SingleFetchRedirectSymbol } from '@remix-run/server-runtime'; | ||
@@ -20,9 +20,2 @@ import { decode } from 'turbo-stream'; | ||
// clientLoader | ||
let defineClientLoader = clientLoader => clientLoader; | ||
// clientAction | ||
let defineClientAction = clientAction => clientAction; | ||
// StreamTransfer recursively renders down chunks of the `serverHandoffStream` | ||
@@ -94,40 +87,56 @@ // into the client-side `streamController` | ||
} | ||
function getSingleFetchDataStrategy(manifest, routeModules) { | ||
function getSingleFetchDataStrategy(manifest, routeModules, getRouter) { | ||
return async ({ | ||
request, | ||
matches | ||
}) => request.method !== "GET" ? singleFetchActionStrategy(request, matches) : singleFetchLoaderStrategy(manifest, routeModules, request, matches); | ||
matches, | ||
fetcherKey | ||
}) => { | ||
// Actions are simple and behave the same for navigations and fetchers | ||
if (request.method !== "GET") { | ||
return singleFetchActionStrategy(request, matches); | ||
} | ||
// Fetcher loads are singular calls to one loader | ||
if (fetcherKey) { | ||
return singleFetchLoaderFetcherStrategy(request, matches); | ||
} | ||
// Navigational loads are more complex... | ||
return singleFetchLoaderNavigationStrategy(manifest, routeModules, getRouter(), request, matches); | ||
}; | ||
} | ||
// Actions are simple since they're singular calls to the server | ||
function singleFetchActionStrategy(request, matches) { | ||
return Promise.all(matches.map(async m => { | ||
let actionStatus; | ||
let result = await m.resolve(async handler => { | ||
let result = await handler(async () => { | ||
let url = singleFetchUrl(request.url); | ||
let init = await createRequestInit(request); | ||
let { | ||
data, | ||
status | ||
} = await fetchAndDecode(url, init); | ||
actionStatus = status; | ||
return unwrapSingleFetchResult(data, m.route.id); | ||
}); | ||
return { | ||
type: "data", | ||
result | ||
}; | ||
// Actions are simple since they're singular calls to the server for both | ||
// navigations and fetchers) | ||
async function singleFetchActionStrategy(request, matches) { | ||
let actionMatch = matches.find(m => m.shouldLoad); | ||
invariant(actionMatch, "No action match found"); | ||
let actionStatus = undefined; | ||
let result = await actionMatch.resolve(async handler => { | ||
let result = await handler(async () => { | ||
let url = singleFetchUrl(request.url); | ||
let init = await createRequestInit(request); | ||
let { | ||
data, | ||
status | ||
} = await fetchAndDecode(url, init); | ||
actionStatus = status; | ||
return unwrapSingleFetchResult(data, actionMatch.route.id); | ||
}); | ||
if (isResponse(result.result) || isRouteErrorResponse(result.result)) { | ||
return result; | ||
} | ||
return result; | ||
}); | ||
if (isResponse(result.result) || isRouteErrorResponse(result.result)) { | ||
return { | ||
[actionMatch.route.id]: result | ||
}; | ||
} | ||
// For non-responses, proxy along the statusCode via unstable_data() | ||
// (most notably for skipping action error revalidation) | ||
return { | ||
// For non-responses, proxy along the statusCode via data() | ||
// (most notably for skipping action error revalidation) | ||
return { | ||
[actionMatch.route.id]: { | ||
type: result.type, | ||
result: unstable_data(result.result, actionStatus) | ||
}; | ||
})); | ||
result: data(result.result, actionStatus) | ||
} | ||
}; | ||
} | ||
@@ -137,37 +146,144 @@ | ||
// create a singular promise for all server-loader routes to latch onto. | ||
function singleFetchLoaderStrategy(manifest, routeModules, request, matches) { | ||
let singleFetchPromise; | ||
return Promise.all(matches.map(async m => m.resolve(async handler => { | ||
let result; | ||
let url = stripIndexParam(singleFetchUrl(request.url)); | ||
let init = await createRequestInit(request); | ||
async function singleFetchLoaderNavigationStrategy(manifest, routeModules, router, request, matches) { | ||
// Track which routes need a server load - in case we need to tack on a | ||
// `_routes` param | ||
let routesParams = new Set(); | ||
// When a route has a client loader, it calls it's singular server loader | ||
// We only add `_routes` when one or more routes opts out of a load via | ||
// `shouldRevalidate` or `clientLoader` | ||
let foundOptOutRoute = false; | ||
// Deferreds for each route so we can be sure they've all loaded via | ||
// `match.resolve()`, and a singular promise that can tell us all routes | ||
// have been resolved | ||
let routeDfds = matches.map(() => createDeferred()); | ||
let routesLoadedPromise = Promise.all(routeDfds.map(d => d.promise)); | ||
// Deferred that we'll use for the call to the server that each match can | ||
// await and parse out it's specific result | ||
let singleFetchDfd = createDeferred(); | ||
// Base URL and RequestInit for calls to the server | ||
let url = stripIndexParam(singleFetchUrl(request.url)); | ||
let init = await createRequestInit(request); | ||
// We'll build up this results object as we loop through matches | ||
let results = {}; | ||
let resolvePromise = Promise.all(matches.map(async (m, i) => m.resolve(async handler => { | ||
routeDfds[i].resolve(); | ||
if (!m.shouldLoad) { | ||
var _routeModules$m$route; | ||
// If we're not yet initialized and this is the initial load, respect | ||
// `shouldLoad` because we're only dealing with `clientLoader.hydrate` | ||
// routes which will fall into the `clientLoader` section below. | ||
if (!router.state.initialized) { | ||
return; | ||
} | ||
// Otherwise, we opt out if we currently have data, a `loader`, and a | ||
// `shouldRevalidate` function. This implies that the user opted out | ||
// via `shouldRevalidate` | ||
if (m.route.id in router.state.loaderData && manifest.routes[m.route.id].hasLoader && (_routeModules$m$route = routeModules[m.route.id]) !== null && _routeModules$m$route !== void 0 && _routeModules$m$route.shouldRevalidate) { | ||
foundOptOutRoute = true; | ||
return; | ||
} | ||
} | ||
// When a route has a client loader, it opts out of the singular call and | ||
// calls it's server loader via `serverLoader()` using a `?_routes` param | ||
if (manifest.routes[m.route.id].hasClientLoader) { | ||
result = await handler(async () => { | ||
url.searchParams.set("_routes", m.route.id); | ||
let { | ||
data | ||
} = await fetchAndDecode(url, init); | ||
if (manifest.routes[m.route.id].hasLoader) { | ||
foundOptOutRoute = true; | ||
} | ||
try { | ||
let result = await fetchSingleLoader(handler, url, init, m.route.id); | ||
results[m.route.id] = { | ||
type: "data", | ||
result | ||
}; | ||
} catch (e) { | ||
results[m.route.id] = { | ||
type: "error", | ||
result: e | ||
}; | ||
} | ||
return; | ||
} | ||
// Load this route on the server if it has a loader | ||
if (manifest.routes[m.route.id].hasLoader) { | ||
routesParams.add(m.route.id); | ||
} | ||
// Lump this match in with the others on a singular promise | ||
try { | ||
let result = await handler(async () => { | ||
let data = await singleFetchDfd.promise; | ||
return unwrapSingleFetchResults(data, m.route.id); | ||
}); | ||
} else { | ||
result = await handler(async () => { | ||
// Otherwise we let multiple routes hook onto the same promise | ||
if (!singleFetchPromise) { | ||
url = addRevalidationParam(manifest, routeModules, matches.map(m => m.route), matches.filter(m => m.shouldLoad).map(m => m.route), url); | ||
singleFetchPromise = fetchAndDecode(url, init).then(({ | ||
data | ||
}) => data); | ||
} | ||
let results = await singleFetchPromise; | ||
return unwrapSingleFetchResults(results, m.route.id); | ||
}); | ||
results[m.route.id] = { | ||
type: "data", | ||
result | ||
}; | ||
} catch (e) { | ||
results[m.route.id] = { | ||
type: "error", | ||
result: e | ||
}; | ||
} | ||
return { | ||
type: "data", | ||
result | ||
}; | ||
}))); | ||
// Wait for all routes to resolve above before we make the HTTP call | ||
await routesLoadedPromise; | ||
// We can skip the server call: | ||
// - On initial hydration - only clientLoaders can pass through via `clientLoader.hydrate` | ||
// - If there are no routes to fetch from the server | ||
// | ||
// One exception - if we are performing an HDR revalidation we have to call | ||
// the server in case a new loader has shown up that the manifest doesn't yet | ||
// know about | ||
if ((!router.state.initialized || routesParams.size === 0) && !window.__remixHdrActive) { | ||
singleFetchDfd.resolve({}); | ||
} else { | ||
try { | ||
// 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", matches.filter(m => routesParams.has(m.route.id)).map(m => m.route.id).join(",")); | ||
} | ||
let data = await fetchAndDecode(url, init); | ||
singleFetchDfd.resolve(data.data); | ||
} catch (e) { | ||
singleFetchDfd.reject(e); | ||
} | ||
} | ||
await resolvePromise; | ||
return results; | ||
} | ||
// Fetcher loader calls are much simpler than navigational loader calls | ||
async function singleFetchLoaderFetcherStrategy(request, matches) { | ||
let fetcherMatch = matches.find(m => m.shouldLoad); | ||
invariant(fetcherMatch, "No fetcher match found"); | ||
let result = await fetcherMatch.resolve(async handler => { | ||
let url = stripIndexParam(singleFetchUrl(request.url)); | ||
let init = await createRequestInit(request); | ||
return fetchSingleLoader(handler, url, init, fetcherMatch.route.id); | ||
}); | ||
return { | ||
[fetcherMatch.route.id]: result | ||
}; | ||
} | ||
function fetchSingleLoader(handler, url, init, routeId) { | ||
return handler(async () => { | ||
let singleLoaderUrl = new URL(url); | ||
singleLoaderUrl.searchParams.set("_routes", routeId); | ||
let { | ||
data | ||
} = await fetchAndDecode(singleLoaderUrl, init); | ||
return unwrapSingleFetchResults(data, routeId); | ||
}); | ||
} | ||
function stripIndexParam(url) { | ||
@@ -187,42 +303,2 @@ let indexValues = url.searchParams.getAll("index"); | ||
} | ||
// Determine which routes we want to load so we can add a `?_routes` search param | ||
// for fine-grained revalidation if necessary. There's some nuance to this decision: | ||
// | ||
// - The presence of `shouldRevalidate` and `clientLoader` functions are the only | ||
// way to trigger fine-grained single fetch loader calls. without either of | ||
// these on the route matches we just always ask for the full `.data` request. | ||
// - If any routes have a `shouldRevalidate` or `clientLoader` then we do a | ||
// comparison of the routes we matched and the routes we're aiming to load | ||
// - If they don't match up, then we add the `_routes` param or fine-grained | ||
// loading | ||
// - This is used by the single fetch implementation above and by the | ||
// `<PrefetchPageLinksImpl>` component so we can prefetch routes using the | ||
// same logic | ||
function addRevalidationParam(manifest, routeModules, matchedRoutes, loadRoutes, url) { | ||
let genRouteIds = arr => arr.filter(id => manifest.routes[id].hasLoader).join(","); | ||
// Look at the `routeModules` for `shouldRevalidate` here instead of the manifest | ||
// since HDR adds a wrapper for `shouldRevalidate` even if the route didn't have one | ||
// initially. | ||
// TODO: We probably can get rid of that wrapper once we're strictly on on | ||
// single-fetch in v3 and just leverage a needsRevalidation data structure here | ||
// to determine what to fetch | ||
let needsParam = matchedRoutes.some(r => { | ||
var _routeModules$r$id, _manifest$routes$r$id; | ||
return ((_routeModules$r$id = routeModules[r.id]) === null || _routeModules$r$id === void 0 ? void 0 : _routeModules$r$id.shouldRevalidate) || ((_manifest$routes$r$id = manifest.routes[r.id]) === null || _manifest$routes$r$id === void 0 ? void 0 : _manifest$routes$r$id.hasClientLoader); | ||
}); | ||
if (!needsParam) { | ||
return url; | ||
} | ||
let matchedIds = genRouteIds(matchedRoutes.map(r => r.id)); | ||
let loadIds = genRouteIds(loadRoutes.filter(r => { | ||
var _manifest$routes$r$id2; | ||
return !((_manifest$routes$r$id2 = manifest.routes[r.id]) !== null && _manifest$routes$r$id2 !== void 0 && _manifest$routes$r$id2.hasClientLoader); | ||
}).map(r => r.id)); | ||
if (matchedIds !== loadIds) { | ||
url.searchParams.set("_routes", loadIds); | ||
} | ||
return url; | ||
} | ||
function singleFetchUrl(reqUrl) { | ||
@@ -239,7 +315,31 @@ let url = typeof reqUrl === "string" ? new URL(reqUrl, window.location.origin) : reqUrl; | ||
let res = await fetch(url, init); | ||
// Don't do a hard check against the header here. We'll get `text/x-turbo` | ||
// Don't do a hard check against the header here. We'll get `text/x-script` | ||
// when we have a running server, but if folks want to prerender `.data` files | ||
// and serve them from a CDN we should let them come back with whatever | ||
// Content-Type their CDN provides and not force them to make sure `.data` | ||
// files are served as `text/x-turbo`. We'll throw if we can't decode anyway. | ||
// files are served as `text/x-script`. We'll throw if we can't decode anyway. | ||
// some status codes are not permitted to have bodies, so we want to just | ||
// treat those as "no data" instead of throwing an exception. | ||
// 304 is not included here because the browser should fill those responses | ||
// with the cached body content. | ||
let NO_BODY_STATUS_CODES = new Set([100, 101, 204, 205]); | ||
if (NO_BODY_STATUS_CODES.has(res.status)) { | ||
if (!init.method || init.method === "GET") { | ||
// SingleFetchResults can just have no routeId keys which will result | ||
// in no data for all routes | ||
return { | ||
status: res.status, | ||
data: {} | ||
}; | ||
} else { | ||
// SingleFetchResult is for a singular route and can specify no data | ||
return { | ||
status: res.status, | ||
data: { | ||
data: null | ||
} | ||
}; | ||
} | ||
} | ||
invariant(res.body, "No response body to decode"); | ||
@@ -292,2 +392,13 @@ try { | ||
} | ||
}, (type, value) => { | ||
if (type === "SingleFetchFallback") { | ||
return { | ||
value: undefined | ||
}; | ||
} | ||
if (type === "SingleFetchClassInstance") { | ||
return { | ||
value | ||
}; | ||
} | ||
}] | ||
@@ -317,3 +428,3 @@ }); | ||
} | ||
return redirect(result.redirect, { | ||
throw redirect(result.redirect, { | ||
status: result.status, | ||
@@ -328,3 +439,28 @@ headers | ||
} | ||
function createDeferred() { | ||
let resolve; | ||
let reject; | ||
let promise = new Promise((res, rej) => { | ||
resolve = async val => { | ||
res(val); | ||
try { | ||
await promise; | ||
} catch (e) {} | ||
}; | ||
reject = async error => { | ||
rej(error); | ||
try { | ||
await promise; | ||
} catch (e) {} | ||
}; | ||
}); | ||
return { | ||
promise, | ||
//@ts-ignore | ||
resolve, | ||
//@ts-ignore | ||
reject | ||
}; | ||
} | ||
export { StreamTransfer, addRevalidationParam, decodeViaTurboStream, defineClientAction, defineClientLoader, getSingleFetchDataStrategy, singleFetchUrl }; | ||
export { StreamTransfer, decodeViaTurboStream, getSingleFetchDataStrategy, singleFetchUrl }; |
/** | ||
* @remix-run/react v0.0.0-nightly-1ac5c50dd-20240726 | ||
* @remix-run/react v0.0.0-nightly-1afe78c3e-20250122 | ||
* | ||
@@ -51,4 +51,5 @@ * Copyright (c) Remix Software Inc. | ||
"💿 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." | ||
); | ||
@@ -55,0 +56,0 @@ ` |
import type { Router } from "@remix-run/router"; | ||
import type { unstable_PatchRoutesOnMissFunction } from "react-router"; | ||
import type { PatchRoutesOnNavigationFunction } from "react-router"; | ||
import type { AssetsManifest, FutureConfig } from "./entry"; | ||
@@ -12,7 +12,2 @@ import type { RouteModules } from "./routeModules"; | ||
} | ||
type FogOfWarInfo = { | ||
nextPaths: Set<string>; | ||
knownGoodPaths: Set<string>; | ||
known404Paths: Set<string>; | ||
}; | ||
export declare function isFogOfWarEnabled(future: FutureConfig, isSpaMode: boolean): boolean; | ||
@@ -32,8 +27,4 @@ export declare function getPartialManifest(manifest: AssetsManifest, router: Router): { | ||
}; | ||
export declare function initFogOfWar(manifest: AssetsManifest, routeModules: RouteModules, future: FutureConfig, isSpaMode: boolean, basename: string | undefined): { | ||
enabled: boolean; | ||
patchRoutesOnMiss?: unstable_PatchRoutesOnMissFunction; | ||
}; | ||
export declare function getPatchRoutesOnNavigationFunction(manifest: AssetsManifest, routeModules: RouteModules, future: FutureConfig, isSpaMode: boolean, basename: string | undefined): PatchRoutesOnNavigationFunction | undefined; | ||
export declare function useFogOFWarDiscovery(router: Router, manifest: AssetsManifest, routeModules: RouteModules, future: FutureConfig, isSpaMode: boolean): void; | ||
export declare function fetchAndApplyManifestPatches(paths: string[], _fogOfWar: FogOfWarInfo, manifest: AssetsManifest, routeModules: RouteModules, future: FutureConfig, isSpaMode: boolean, basename: string | undefined, patchRoutes: Router["patchRoutes"]): Promise<void>; | ||
export {}; | ||
export declare function fetchAndApplyManifestPatches(paths: string[], manifest: AssetsManifest, routeModules: RouteModules, future: FutureConfig, isSpaMode: boolean, basename: string | undefined, patchRoutes: Router["patchRoutes"]): Promise<void>; |
/** | ||
* @remix-run/react v0.0.0-nightly-1ac5c50dd-20240726 | ||
* @remix-run/react v0.0.0-nightly-1afe78c3e-20250122 | ||
* | ||
@@ -39,8 +39,15 @@ * Copyright (c) Remix Software Inc. | ||
// Currently rendered links that may need prefetching | ||
const nextPaths = new Set(); | ||
// FIFO queue of previously discovered routes to prevent re-calling on | ||
// subsequent navigations to the same path | ||
const discoveredPathsMaxSize = 1000; | ||
const discoveredPaths = new Set(); | ||
// 7.5k to come in under the ~8k limit for most browsers | ||
// https://stackoverflow.com/a/417184 | ||
const URL_LIMIT = 7680; | ||
let fogOfWar = null; | ||
function isFogOfWarEnabled(future, isSpaMode) { | ||
return future.unstable_lazyRouteDiscovery === true && !isSpaMode; | ||
return future.v3_lazyRouteDiscovery === true && !isSpaMode; | ||
} | ||
@@ -76,24 +83,14 @@ function getPartialManifest(manifest, router$1) { | ||
} | ||
function initFogOfWar(manifest, routeModules, future, isSpaMode, basename) { | ||
function getPatchRoutesOnNavigationFunction(manifest, routeModules, future, isSpaMode, basename) { | ||
if (!isFogOfWarEnabled(future, isSpaMode)) { | ||
return { | ||
enabled: false | ||
}; | ||
return undefined; | ||
} | ||
fogOfWar = { | ||
nextPaths: new Set(), | ||
knownGoodPaths: new Set(), | ||
known404Paths: new Set() | ||
}; | ||
return { | ||
enabled: true, | ||
patchRoutesOnMiss: async ({ | ||
path, | ||
patch | ||
}) => { | ||
if (fogOfWar.known404Paths.has(path) || fogOfWar.knownGoodPaths.has(path)) { | ||
return; | ||
} | ||
await fetchAndApplyManifestPatches([path], fogOfWar, manifest, routeModules, future, isSpaMode, basename, patch); | ||
return async ({ | ||
path, | ||
patch | ||
}) => { | ||
if (discoveredPaths.has(path)) { | ||
return; | ||
} | ||
await fetchAndApplyManifestPatches([path], manifest, routeModules, future, isSpaMode, basename, patch); | ||
}; | ||
@@ -116,11 +113,5 @@ } | ||
let url = new URL(path, window.location.origin); | ||
let { | ||
knownGoodPaths, | ||
known404Paths, | ||
nextPaths | ||
} = fogOfWar; | ||
if (knownGoodPaths.has(url.pathname) || known404Paths.has(url.pathname)) { | ||
return; | ||
if (!discoveredPaths.has(url.pathname)) { | ||
nextPaths.add(url.pathname); | ||
} | ||
nextPaths.add(url.pathname); | ||
} | ||
@@ -130,3 +121,9 @@ | ||
async function fetchPatches() { | ||
let lazyPaths = getFogOfWarPaths(fogOfWar); | ||
let lazyPaths = Array.from(nextPaths.keys()).filter(path => { | ||
if (discoveredPaths.has(path)) { | ||
nextPaths.delete(path); | ||
return false; | ||
} | ||
return true; | ||
}); | ||
if (lazyPaths.length === 0) { | ||
@@ -136,3 +133,3 @@ return; | ||
try { | ||
await fetchAndApplyManifestPatches(lazyPaths, fogOfWar, manifest, routeModules, future, isSpaMode, router.basename, router.patchRoutes); | ||
await fetchAndApplyManifestPatches(lazyPaths, manifest, routeModules, future, isSpaMode, router.basename, router.patchRoutes); | ||
} catch (e) { | ||
@@ -179,30 +176,7 @@ console.error("Failed to fetch manifest patches", e); | ||
} | ||
function getFogOfWarPaths(fogOfWar, router) { | ||
let { | ||
knownGoodPaths, | ||
known404Paths, | ||
nextPaths | ||
} = fogOfWar; | ||
return Array.from(nextPaths.keys()).filter(path => { | ||
if (knownGoodPaths.has(path)) { | ||
nextPaths.delete(path); | ||
return false; | ||
} | ||
if (known404Paths.has(path)) { | ||
nextPaths.delete(path); | ||
return false; | ||
} | ||
return true; | ||
}); | ||
} | ||
async function fetchAndApplyManifestPatches(paths, _fogOfWar, manifest, routeModules, future, isSpaMode, basename, patchRoutes) { | ||
let { | ||
nextPaths, | ||
knownGoodPaths, | ||
known404Paths | ||
} = _fogOfWar; | ||
async function fetchAndApplyManifestPatches(paths, manifest, routeModules, future, isSpaMode, basename, patchRoutes) { | ||
let manifestPath = `${basename ?? "/"}/__manifest`.replace(/\/+/g, "/"); | ||
let url = new URL(manifestPath, window.location.origin); | ||
paths.sort().forEach(path => url.searchParams.append("p", path)); | ||
url.searchParams.set("version", manifest.version); | ||
paths.forEach(path => url.searchParams.append("p", path)); | ||
@@ -222,9 +196,7 @@ // If the URL is nearing the ~8k limit on GET requests, skip this optimization | ||
} | ||
let data = await res.json(); | ||
let serverPatches = await res.json(); | ||
// Capture this before we apply the patches to the manifest | ||
// Patch routes we don't know about yet into the manifest | ||
let knownRoutes = new Set(Object.keys(manifest.routes)); | ||
// Patch routes we don't know about yet into the manifest | ||
let patches = Object.values(data.patches).reduce((acc, route) => !knownRoutes.has(route.id) ? Object.assign(acc, { | ||
let patches = Object.values(serverPatches).reduce((acc, route) => !knownRoutes.has(route.id) ? Object.assign(acc, { | ||
[route.id]: route | ||
@@ -234,8 +206,5 @@ }) : acc, {}); | ||
// Track legit 404s so we don't try to fetch them again | ||
data.notFoundPaths.forEach(p => known404Paths.add(p)); | ||
// Track discovered paths so we don't have to fetch them again | ||
paths.forEach(p => addToFifoQueue(p, discoveredPaths)); | ||
// Track matched paths so we don't have to fetch them again | ||
paths.forEach(p => knownGoodPaths.add(p)); | ||
// Identify all parentIds for which we have new children to add and patch | ||
@@ -251,2 +220,9 @@ // in their new children | ||
} | ||
function addToFifoQueue(path, queue) { | ||
if (queue.size >= discoveredPathsMaxSize) { | ||
let first = queue.values().next().value; | ||
if (typeof first === "string") queue.delete(first); | ||
} | ||
queue.add(path); | ||
} | ||
@@ -265,4 +241,4 @@ // Thanks Josh! | ||
exports.getPartialManifest = getPartialManifest; | ||
exports.initFogOfWar = initFogOfWar; | ||
exports.getPatchRoutesOnNavigationFunction = getPatchRoutesOnNavigationFunction; | ||
exports.isFogOfWarEnabled = isFogOfWarEnabled; | ||
exports.useFogOFWarDiscovery = useFogOFWarDiscovery; |
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 { 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, useViewTransitionState, unstable_usePrompt, } from "react-router-dom"; | ||
export { defer, json, redirect, redirectDocument, replace, data, } from "@remix-run/server-runtime"; | ||
export type { RemixBrowserProps } from "./browser"; | ||
@@ -13,5 +13,3 @@ export { RemixBrowser } from "./browser"; | ||
export { RemixServer } from "./server"; | ||
export type { ClientAction as unstable_ClientAction, ClientLoader as unstable_ClientLoader, } from "./single-fetch"; | ||
export { defineClientAction as unstable_defineClientAction, defineClientLoader as unstable_defineClientLoader, } from "./single-fetch"; | ||
export type { FutureConfig as UNSAFE_FutureConfig, AssetsManifest as UNSAFE_AssetsManifest, RemixContextObject as UNSAFE_RemixContextObject, } from "./entry"; | ||
export type { EntryRoute as UNSAFE_EntryRoute, RouteManifest as UNSAFE_RouteManifest, } from "./routes"; |
/** | ||
* @remix-run/react v0.0.0-nightly-1ac5c50dd-20240726 | ||
* @remix-run/react v0.0.0-nightly-1afe78c3e-20250122 | ||
* | ||
@@ -21,3 +21,2 @@ * Copyright (c) Remix Software Inc. | ||
var server = require('./server.js'); | ||
var singleFetch = require('./single-fetch.js'); | ||
@@ -94,6 +93,2 @@ | ||
}); | ||
Object.defineProperty(exports, 'unstable_useViewTransitionState', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.unstable_useViewTransitionState; } | ||
}); | ||
Object.defineProperty(exports, 'useAsyncError', { | ||
@@ -191,2 +186,10 @@ enumerable: true, | ||
}); | ||
Object.defineProperty(exports, 'useViewTransitionState', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.useViewTransitionState; } | ||
}); | ||
Object.defineProperty(exports, 'data', { | ||
enumerable: true, | ||
get: function () { return serverRuntime.data; } | ||
}); | ||
Object.defineProperty(exports, 'defer', { | ||
@@ -212,6 +215,2 @@ enumerable: true, | ||
}); | ||
Object.defineProperty(exports, 'unstable_data', { | ||
enumerable: true, | ||
get: function () { return serverRuntime.unstable_data; } | ||
}); | ||
exports.RemixBrowser = browser.RemixBrowser; | ||
@@ -235,3 +234,1 @@ exports.Await = components.Await; | ||
exports.RemixServer = server.RemixServer; | ||
exports.unstable_defineClientAction = singleFetch.defineClientAction; | ||
exports.unstable_defineClientLoader = singleFetch.defineClientLoader; |
/** | ||
* @remix-run/react v0.0.0-nightly-1ac5c50dd-20240726 | ||
* @remix-run/react v0.0.0-nightly-1afe78c3e-20250122 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
import type { AgnosticDataRouteMatch } from "@remix-run/router"; | ||
import type { Location } from "react-router-dom"; | ||
import type { AssetsManifest } from "./entry"; | ||
import type { AssetsManifest, FutureConfig } from "./entry"; | ||
import type { RouteModules, RouteModule } from "./routeModules"; | ||
@@ -121,3 +121,3 @@ import type { EntryRoute } from "./routes"; | ||
export declare function getKeyedPrefetchLinks(matches: AgnosticDataRouteMatch[], manifest: AssetsManifest, routeModules: RouteModules): Promise<KeyedHtmlLinkDescriptor[]>; | ||
export declare function getNewMatchesForLinks(page: string, nextMatches: AgnosticDataRouteMatch[], currentMatches: AgnosticDataRouteMatch[], manifest: AssetsManifest, location: Location, mode: "data" | "assets"): AgnosticDataRouteMatch[]; | ||
export declare function getNewMatchesForLinks(page: string, nextMatches: AgnosticDataRouteMatch[], currentMatches: AgnosticDataRouteMatch[], manifest: AssetsManifest, location: Location, future: FutureConfig, mode: "data" | "assets"): AgnosticDataRouteMatch[]; | ||
export declare function getDataLinkHrefs(page: string, matches: AgnosticDataRouteMatch[], manifest: AssetsManifest): string[]; | ||
@@ -124,0 +124,0 @@ export declare function getModuleLinkHrefs(matches: AgnosticDataRouteMatch[], manifestPatch: AssetsManifest): string[]; |
/** | ||
* @remix-run/react v0.0.0-nightly-1ac5c50dd-20240726 | ||
* @remix-run/react v0.0.0-nightly-1afe78c3e-20250122 | ||
* | ||
@@ -124,3 +124,3 @@ * Copyright (c) Remix Software Inc. | ||
// This is ridiculously identical to transition.ts `filterMatchesToLoad` | ||
function getNewMatchesForLinks(page, nextMatches, currentMatches, manifest, location, mode) { | ||
function getNewMatchesForLinks(page, nextMatches, currentMatches, manifest, location, future, mode) { | ||
let path = parsePathPatch(page); | ||
@@ -144,3 +144,3 @@ let isNew = (match, index) => { | ||
// version doesn't care about submissions | ||
let newMatches = mode === "data" && location.search !== path.search ? | ||
let newMatches = mode === "data" && (future.v3_singleFetch || location.search !== path.search) ? | ||
// this is really similar to stuff in transition.ts, maybe somebody smarter | ||
@@ -156,2 +156,7 @@ // than me (or in less of a hurry) can share some of it. You're the best. | ||
} | ||
// For reused routes on GET navigations, by default: | ||
// - Single fetch always revalidates | ||
// - Multi fetch revalidates if search params changed | ||
let defaultShouldRevalidate = future.v3_singleFetch || location.search !== path.search; | ||
if (match.route.shouldRevalidate) { | ||
@@ -164,3 +169,3 @@ var _currentMatches$; | ||
nextParams: match.params, | ||
defaultShouldRevalidate: true | ||
defaultShouldRevalidate | ||
}); | ||
@@ -171,3 +176,3 @@ if (typeof routeChoice === "boolean") { | ||
} | ||
return true; | ||
return defaultShouldRevalidate; | ||
}) : nextMatches.filter((match, index) => { | ||
@@ -174,0 +179,0 @@ let manifestRoute = manifest.routes[match.route.id]; |
/** | ||
* @remix-run/react v0.0.0-nightly-1ac5c50dd-20240726 | ||
* @remix-run/react v0.0.0-nightly-1afe78c3e-20250122 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
/** | ||
* @remix-run/react v0.0.0-nightly-1ac5c50dd-20240726 | ||
* @remix-run/react v0.0.0-nightly-1afe78c3e-20250122 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
/** | ||
* @remix-run/react v0.0.0-nightly-1ac5c50dd-20240726 | ||
* @remix-run/react v0.0.0-nightly-1afe78c3e-20250122 | ||
* | ||
@@ -198,3 +198,3 @@ * Copyright (c) Remix Software Inc. | ||
handle: routeModule.handle, | ||
shouldRevalidate: needsRevalidation ? wrapShouldRevalidateForHdr(route.id, routeModule.shouldRevalidate, needsRevalidation) : routeModule.shouldRevalidate | ||
shouldRevalidate: getShouldRevalidateFunction(future, routeModule, route.id, needsRevalidation) | ||
}); | ||
@@ -224,6 +224,9 @@ let initialData = initialState === null || initialState === void 0 ? void 0 : (_initialState$loaderD = initialState.loaderData) === null || _initialState$loaderD === void 0 ? void 0 : _initialState$loaderD[route.id]; | ||
if (isHydrationRequest) { | ||
if (initialData !== undefined) { | ||
return initialData; | ||
} | ||
if (initialError !== undefined) { | ||
throw initialError; | ||
} | ||
return initialData; | ||
return null; | ||
} | ||
@@ -317,5 +320,2 @@ | ||
} | ||
if (needsRevalidation) { | ||
lazyRoute.shouldRevalidate = wrapShouldRevalidateForHdr(route.id, mod.shouldRevalidate, needsRevalidation); | ||
} | ||
return { | ||
@@ -329,3 +329,3 @@ ...(lazyRoute.loader ? { | ||
hasErrorBoundary: lazyRoute.hasErrorBoundary, | ||
shouldRevalidate: lazyRoute.shouldRevalidate, | ||
shouldRevalidate: getShouldRevalidateFunction(future, lazyRoute, route.id, needsRevalidation), | ||
handle: lazyRoute.handle, | ||
@@ -344,3 +344,20 @@ // No need to wrap these in layout since the root route is never | ||
} | ||
function getShouldRevalidateFunction(future, route, routeId, needsRevalidation) { | ||
// During HDR we force revalidation for updated routes | ||
if (needsRevalidation) { | ||
return wrapShouldRevalidateForHdr(routeId, route.shouldRevalidate, needsRevalidation); | ||
} | ||
// Single fetch revalidates by default, so override the RR default value which | ||
// matches the multi-fetch behavior with `true` | ||
if (future.v3_singleFetch && route.shouldRevalidate) { | ||
let fn = route.shouldRevalidate; | ||
return opts => fn({ | ||
...opts, | ||
defaultShouldRevalidate: true | ||
}); | ||
} | ||
return route.shouldRevalidate; | ||
} | ||
// When an HMR / HDR update happens we opt out of all user-defined | ||
@@ -347,0 +364,0 @@ // revalidation logic and force a revalidation on the first call |
/** | ||
* @remix-run/react v0.0.0-nightly-1ac5c50dd-20240726 | ||
* @remix-run/react v0.0.0-nightly-1afe78c3e-20250122 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
/** | ||
* @remix-run/react v0.0.0-nightly-1ac5c50dd-20240726 | ||
* @remix-run/react v0.0.0-nightly-1afe78c3e-20250122 | ||
* | ||
@@ -108,3 +108,3 @@ * Copyright (c) Remix Software Inc. | ||
hydrate: false | ||
}))), context.future.unstable_singleFetch && context.serverHandoffStream ? /*#__PURE__*/React__namespace.createElement(React__namespace.Suspense, null, /*#__PURE__*/React__namespace.createElement(singleFetch.StreamTransfer, { | ||
}))), context.future.v3_singleFetch && context.serverHandoffStream ? /*#__PURE__*/React__namespace.createElement(React__namespace.Suspense, null, /*#__PURE__*/React__namespace.createElement(singleFetch.StreamTransfer, { | ||
context: context, | ||
@@ -111,0 +111,0 @@ identifier: 0, |
import * as React from "react"; | ||
import type { ActionFunctionArgs as RRActionArgs, LoaderFunctionArgs as RRLoaderArgs, unstable_DataStrategyFunction as DataStrategyFunction } from "@remix-run/router"; | ||
import type { unstable_Action, unstable_Loader, unstable_Serialize } from "@remix-run/server-runtime"; | ||
import type { DataRouteObject } from "react-router-dom"; | ||
import type { DataStrategyFunction, Router as RemixRouter } from "@remix-run/router"; | ||
import type { AssetsManifest, EntryContext } from "./entry"; | ||
import type { RouteModules } from "./routeModules"; | ||
type ClientLoaderArgs = RRLoaderArgs<undefined> & { | ||
serverLoader: <T extends unstable_Loader>() => Promise<unstable_Serialize<T>>; | ||
}; | ||
export type ClientLoader = (args: ClientLoaderArgs) => unknown; | ||
export declare let defineClientLoader: <T extends ClientLoader>(clientLoader: T) => T & { | ||
hydrate?: boolean | undefined; | ||
}; | ||
type ClientActionArgs = RRActionArgs<undefined> & { | ||
serverAction: <T extends unstable_Action>() => Promise<unstable_Serialize<T>>; | ||
}; | ||
export type ClientAction = (args: ClientActionArgs) => unknown; | ||
export declare let defineClientAction: <T extends ClientAction>(clientAction: T) => T; | ||
import { type RouteModules } from "./routeModules"; | ||
interface StreamTransferProps { | ||
@@ -27,4 +13,3 @@ context: EntryContext; | ||
export declare function StreamTransfer({ context, identifier, reader, textDecoder, nonce, }: StreamTransferProps): React.JSX.Element | null; | ||
export declare function getSingleFetchDataStrategy(manifest: AssetsManifest, routeModules: RouteModules): DataStrategyFunction; | ||
export declare function addRevalidationParam(manifest: AssetsManifest, routeModules: RouteModules, matchedRoutes: DataRouteObject[], loadRoutes: DataRouteObject[], url: URL): URL; | ||
export declare function getSingleFetchDataStrategy(manifest: AssetsManifest, routeModules: RouteModules, getRouter: () => RemixRouter): DataStrategyFunction; | ||
export declare function singleFetchUrl(reqUrl: URL | string): URL; | ||
@@ -31,0 +16,0 @@ export declare function decodeViaTurboStream(body: ReadableStream<Uint8Array>, global: Window | typeof globalThis): Promise<{ |
/** | ||
* @remix-run/react v0.0.0-nightly-1ac5c50dd-20240726 | ||
* @remix-run/react v0.0.0-nightly-1afe78c3e-20250122 | ||
* | ||
@@ -43,9 +43,2 @@ * Copyright (c) Remix Software Inc. | ||
// clientLoader | ||
let defineClientLoader = clientLoader => clientLoader; | ||
// clientAction | ||
let defineClientAction = clientAction => clientAction; | ||
// StreamTransfer recursively renders down chunks of the `serverHandoffStream` | ||
@@ -117,40 +110,56 @@ // into the client-side `streamController` | ||
} | ||
function getSingleFetchDataStrategy(manifest, routeModules) { | ||
function getSingleFetchDataStrategy(manifest, routeModules, getRouter) { | ||
return async ({ | ||
request, | ||
matches | ||
}) => request.method !== "GET" ? singleFetchActionStrategy(request, matches) : singleFetchLoaderStrategy(manifest, routeModules, request, matches); | ||
matches, | ||
fetcherKey | ||
}) => { | ||
// Actions are simple and behave the same for navigations and fetchers | ||
if (request.method !== "GET") { | ||
return singleFetchActionStrategy(request, matches); | ||
} | ||
// Fetcher loads are singular calls to one loader | ||
if (fetcherKey) { | ||
return singleFetchLoaderFetcherStrategy(request, matches); | ||
} | ||
// Navigational loads are more complex... | ||
return singleFetchLoaderNavigationStrategy(manifest, routeModules, getRouter(), request, matches); | ||
}; | ||
} | ||
// Actions are simple since they're singular calls to the server | ||
function singleFetchActionStrategy(request, matches) { | ||
return Promise.all(matches.map(async m => { | ||
let actionStatus; | ||
let result = await m.resolve(async handler => { | ||
let result = await handler(async () => { | ||
let url = singleFetchUrl(request.url); | ||
let init = await data.createRequestInit(request); | ||
let { | ||
data: data$1, | ||
status | ||
} = await fetchAndDecode(url, init); | ||
actionStatus = status; | ||
return unwrapSingleFetchResult(data$1, m.route.id); | ||
}); | ||
return { | ||
type: "data", | ||
result | ||
}; | ||
// Actions are simple since they're singular calls to the server for both | ||
// navigations and fetchers) | ||
async function singleFetchActionStrategy(request, matches) { | ||
let actionMatch = matches.find(m => m.shouldLoad); | ||
invariant(actionMatch, "No action match found"); | ||
let actionStatus = undefined; | ||
let result = await actionMatch.resolve(async handler => { | ||
let result = await handler(async () => { | ||
let url = singleFetchUrl(request.url); | ||
let init = await data.createRequestInit(request); | ||
let { | ||
data: data$1, | ||
status | ||
} = await fetchAndDecode(url, init); | ||
actionStatus = status; | ||
return unwrapSingleFetchResult(data$1, actionMatch.route.id); | ||
}); | ||
if (data.isResponse(result.result) || router.isRouteErrorResponse(result.result)) { | ||
return result; | ||
} | ||
return result; | ||
}); | ||
if (data.isResponse(result.result) || router.isRouteErrorResponse(result.result)) { | ||
return { | ||
[actionMatch.route.id]: result | ||
}; | ||
} | ||
// For non-responses, proxy along the statusCode via unstable_data() | ||
// (most notably for skipping action error revalidation) | ||
return { | ||
// For non-responses, proxy along the statusCode via data() | ||
// (most notably for skipping action error revalidation) | ||
return { | ||
[actionMatch.route.id]: { | ||
type: result.type, | ||
result: router.unstable_data(result.result, actionStatus) | ||
}; | ||
})); | ||
result: router.data(result.result, actionStatus) | ||
} | ||
}; | ||
} | ||
@@ -160,37 +169,144 @@ | ||
// create a singular promise for all server-loader routes to latch onto. | ||
function singleFetchLoaderStrategy(manifest, routeModules, request, matches) { | ||
let singleFetchPromise; | ||
return Promise.all(matches.map(async m => m.resolve(async handler => { | ||
let result; | ||
let url = stripIndexParam(singleFetchUrl(request.url)); | ||
let init = await data.createRequestInit(request); | ||
async function singleFetchLoaderNavigationStrategy(manifest, routeModules, router, request, matches) { | ||
// Track which routes need a server load - in case we need to tack on a | ||
// `_routes` param | ||
let routesParams = new Set(); | ||
// When a route has a client loader, it calls it's singular server loader | ||
// We only add `_routes` when one or more routes opts out of a load via | ||
// `shouldRevalidate` or `clientLoader` | ||
let foundOptOutRoute = false; | ||
// Deferreds for each route so we can be sure they've all loaded via | ||
// `match.resolve()`, and a singular promise that can tell us all routes | ||
// have been resolved | ||
let routeDfds = matches.map(() => createDeferred()); | ||
let routesLoadedPromise = Promise.all(routeDfds.map(d => d.promise)); | ||
// Deferred that we'll use for the call to the server that each match can | ||
// await and parse out it's specific result | ||
let singleFetchDfd = createDeferred(); | ||
// Base URL and RequestInit for calls to the server | ||
let url = stripIndexParam(singleFetchUrl(request.url)); | ||
let init = await data.createRequestInit(request); | ||
// We'll build up this results object as we loop through matches | ||
let results = {}; | ||
let resolvePromise = Promise.all(matches.map(async (m, i) => m.resolve(async handler => { | ||
routeDfds[i].resolve(); | ||
if (!m.shouldLoad) { | ||
var _routeModules$m$route; | ||
// If we're not yet initialized and this is the initial load, respect | ||
// `shouldLoad` because we're only dealing with `clientLoader.hydrate` | ||
// routes which will fall into the `clientLoader` section below. | ||
if (!router.state.initialized) { | ||
return; | ||
} | ||
// Otherwise, we opt out if we currently have data, a `loader`, and a | ||
// `shouldRevalidate` function. This implies that the user opted out | ||
// via `shouldRevalidate` | ||
if (m.route.id in router.state.loaderData && manifest.routes[m.route.id].hasLoader && (_routeModules$m$route = routeModules[m.route.id]) !== null && _routeModules$m$route !== void 0 && _routeModules$m$route.shouldRevalidate) { | ||
foundOptOutRoute = true; | ||
return; | ||
} | ||
} | ||
// When a route has a client loader, it opts out of the singular call and | ||
// calls it's server loader via `serverLoader()` using a `?_routes` param | ||
if (manifest.routes[m.route.id].hasClientLoader) { | ||
result = await handler(async () => { | ||
url.searchParams.set("_routes", m.route.id); | ||
let { | ||
data | ||
} = await fetchAndDecode(url, init); | ||
if (manifest.routes[m.route.id].hasLoader) { | ||
foundOptOutRoute = true; | ||
} | ||
try { | ||
let result = await fetchSingleLoader(handler, url, init, m.route.id); | ||
results[m.route.id] = { | ||
type: "data", | ||
result | ||
}; | ||
} catch (e) { | ||
results[m.route.id] = { | ||
type: "error", | ||
result: e | ||
}; | ||
} | ||
return; | ||
} | ||
// Load this route on the server if it has a loader | ||
if (manifest.routes[m.route.id].hasLoader) { | ||
routesParams.add(m.route.id); | ||
} | ||
// Lump this match in with the others on a singular promise | ||
try { | ||
let result = await handler(async () => { | ||
let data = await singleFetchDfd.promise; | ||
return unwrapSingleFetchResults(data, m.route.id); | ||
}); | ||
} else { | ||
result = await handler(async () => { | ||
// Otherwise we let multiple routes hook onto the same promise | ||
if (!singleFetchPromise) { | ||
url = addRevalidationParam(manifest, routeModules, matches.map(m => m.route), matches.filter(m => m.shouldLoad).map(m => m.route), url); | ||
singleFetchPromise = fetchAndDecode(url, init).then(({ | ||
data | ||
}) => data); | ||
} | ||
let results = await singleFetchPromise; | ||
return unwrapSingleFetchResults(results, m.route.id); | ||
}); | ||
results[m.route.id] = { | ||
type: "data", | ||
result | ||
}; | ||
} catch (e) { | ||
results[m.route.id] = { | ||
type: "error", | ||
result: e | ||
}; | ||
} | ||
return { | ||
type: "data", | ||
result | ||
}; | ||
}))); | ||
// Wait for all routes to resolve above before we make the HTTP call | ||
await routesLoadedPromise; | ||
// We can skip the server call: | ||
// - On initial hydration - only clientLoaders can pass through via `clientLoader.hydrate` | ||
// - If there are no routes to fetch from the server | ||
// | ||
// One exception - if we are performing an HDR revalidation we have to call | ||
// the server in case a new loader has shown up that the manifest doesn't yet | ||
// know about | ||
if ((!router.state.initialized || routesParams.size === 0) && !window.__remixHdrActive) { | ||
singleFetchDfd.resolve({}); | ||
} else { | ||
try { | ||
// 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", matches.filter(m => routesParams.has(m.route.id)).map(m => m.route.id).join(",")); | ||
} | ||
let data = await fetchAndDecode(url, init); | ||
singleFetchDfd.resolve(data.data); | ||
} catch (e) { | ||
singleFetchDfd.reject(e); | ||
} | ||
} | ||
await resolvePromise; | ||
return results; | ||
} | ||
// Fetcher loader calls are much simpler than navigational loader calls | ||
async function singleFetchLoaderFetcherStrategy(request, matches) { | ||
let fetcherMatch = matches.find(m => m.shouldLoad); | ||
invariant(fetcherMatch, "No fetcher match found"); | ||
let result = await fetcherMatch.resolve(async handler => { | ||
let url = stripIndexParam(singleFetchUrl(request.url)); | ||
let init = await data.createRequestInit(request); | ||
return fetchSingleLoader(handler, url, init, fetcherMatch.route.id); | ||
}); | ||
return { | ||
[fetcherMatch.route.id]: result | ||
}; | ||
} | ||
function fetchSingleLoader(handler, url, init, routeId) { | ||
return handler(async () => { | ||
let singleLoaderUrl = new URL(url); | ||
singleLoaderUrl.searchParams.set("_routes", routeId); | ||
let { | ||
data | ||
} = await fetchAndDecode(singleLoaderUrl, init); | ||
return unwrapSingleFetchResults(data, routeId); | ||
}); | ||
} | ||
function stripIndexParam(url) { | ||
@@ -210,42 +326,2 @@ let indexValues = url.searchParams.getAll("index"); | ||
} | ||
// Determine which routes we want to load so we can add a `?_routes` search param | ||
// for fine-grained revalidation if necessary. There's some nuance to this decision: | ||
// | ||
// - The presence of `shouldRevalidate` and `clientLoader` functions are the only | ||
// way to trigger fine-grained single fetch loader calls. without either of | ||
// these on the route matches we just always ask for the full `.data` request. | ||
// - If any routes have a `shouldRevalidate` or `clientLoader` then we do a | ||
// comparison of the routes we matched and the routes we're aiming to load | ||
// - If they don't match up, then we add the `_routes` param or fine-grained | ||
// loading | ||
// - This is used by the single fetch implementation above and by the | ||
// `<PrefetchPageLinksImpl>` component so we can prefetch routes using the | ||
// same logic | ||
function addRevalidationParam(manifest, routeModules, matchedRoutes, loadRoutes, url) { | ||
let genRouteIds = arr => arr.filter(id => manifest.routes[id].hasLoader).join(","); | ||
// Look at the `routeModules` for `shouldRevalidate` here instead of the manifest | ||
// since HDR adds a wrapper for `shouldRevalidate` even if the route didn't have one | ||
// initially. | ||
// TODO: We probably can get rid of that wrapper once we're strictly on on | ||
// single-fetch in v3 and just leverage a needsRevalidation data structure here | ||
// to determine what to fetch | ||
let needsParam = matchedRoutes.some(r => { | ||
var _routeModules$r$id, _manifest$routes$r$id; | ||
return ((_routeModules$r$id = routeModules[r.id]) === null || _routeModules$r$id === void 0 ? void 0 : _routeModules$r$id.shouldRevalidate) || ((_manifest$routes$r$id = manifest.routes[r.id]) === null || _manifest$routes$r$id === void 0 ? void 0 : _manifest$routes$r$id.hasClientLoader); | ||
}); | ||
if (!needsParam) { | ||
return url; | ||
} | ||
let matchedIds = genRouteIds(matchedRoutes.map(r => r.id)); | ||
let loadIds = genRouteIds(loadRoutes.filter(r => { | ||
var _manifest$routes$r$id2; | ||
return !((_manifest$routes$r$id2 = manifest.routes[r.id]) !== null && _manifest$routes$r$id2 !== void 0 && _manifest$routes$r$id2.hasClientLoader); | ||
}).map(r => r.id)); | ||
if (matchedIds !== loadIds) { | ||
url.searchParams.set("_routes", loadIds); | ||
} | ||
return url; | ||
} | ||
function singleFetchUrl(reqUrl) { | ||
@@ -262,7 +338,31 @@ let url = typeof reqUrl === "string" ? new URL(reqUrl, window.location.origin) : reqUrl; | ||
let res = await fetch(url, init); | ||
// Don't do a hard check against the header here. We'll get `text/x-turbo` | ||
// Don't do a hard check against the header here. We'll get `text/x-script` | ||
// when we have a running server, but if folks want to prerender `.data` files | ||
// and serve them from a CDN we should let them come back with whatever | ||
// Content-Type their CDN provides and not force them to make sure `.data` | ||
// files are served as `text/x-turbo`. We'll throw if we can't decode anyway. | ||
// files are served as `text/x-script`. We'll throw if we can't decode anyway. | ||
// some status codes are not permitted to have bodies, so we want to just | ||
// treat those as "no data" instead of throwing an exception. | ||
// 304 is not included here because the browser should fill those responses | ||
// with the cached body content. | ||
let NO_BODY_STATUS_CODES = new Set([100, 101, 204, 205]); | ||
if (NO_BODY_STATUS_CODES.has(res.status)) { | ||
if (!init.method || init.method === "GET") { | ||
// SingleFetchResults can just have no routeId keys which will result | ||
// in no data for all routes | ||
return { | ||
status: res.status, | ||
data: {} | ||
}; | ||
} else { | ||
// SingleFetchResult is for a singular route and can specify no data | ||
return { | ||
status: res.status, | ||
data: { | ||
data: null | ||
} | ||
}; | ||
} | ||
} | ||
invariant(res.body, "No response body to decode"); | ||
@@ -315,2 +415,13 @@ try { | ||
} | ||
}, (type, value) => { | ||
if (type === "SingleFetchFallback") { | ||
return { | ||
value: undefined | ||
}; | ||
} | ||
if (type === "SingleFetchClassInstance") { | ||
return { | ||
value | ||
}; | ||
} | ||
}] | ||
@@ -340,3 +451,3 @@ }); | ||
} | ||
return router.redirect(result.redirect, { | ||
throw router.redirect(result.redirect, { | ||
status: result.status, | ||
@@ -351,9 +462,31 @@ headers | ||
} | ||
function createDeferred() { | ||
let resolve; | ||
let reject; | ||
let promise = new Promise((res, rej) => { | ||
resolve = async val => { | ||
res(val); | ||
try { | ||
await promise; | ||
} catch (e) {} | ||
}; | ||
reject = async error => { | ||
rej(error); | ||
try { | ||
await promise; | ||
} catch (e) {} | ||
}; | ||
}); | ||
return { | ||
promise, | ||
//@ts-ignore | ||
resolve, | ||
//@ts-ignore | ||
reject | ||
}; | ||
} | ||
exports.StreamTransfer = StreamTransfer; | ||
exports.addRevalidationParam = addRevalidationParam; | ||
exports.decodeViaTurboStream = decodeViaTurboStream; | ||
exports.defineClientAction = defineClientAction; | ||
exports.defineClientLoader = defineClientLoader; | ||
exports.getSingleFetchDataStrategy = getSingleFetchDataStrategy; | ||
exports.singleFetchUrl = singleFetchUrl; |
{ | ||
"name": "@remix-run/react", | ||
"version": "0.0.0-nightly-1ac5c50dd-20240726", | ||
"version": "0.0.0-nightly-1afe78c3e-20250122", | ||
"description": "React DOM bindings for Remix", | ||
@@ -19,11 +19,11 @@ "bugs": { | ||
"dependencies": { | ||
"@remix-run/router": "0.0.0-experimental-9ffbba722", | ||
"@remix-run/server-runtime": "0.0.0-nightly-1ac5c50dd-20240726", | ||
"react-router": "0.0.0-experimental-9ffbba722", | ||
"react-router-dom": "0.0.0-experimental-9ffbba722", | ||
"turbo-stream": "2.2.0" | ||
"@remix-run/router": "1.21.0", | ||
"@remix-run/server-runtime": "0.0.0-nightly-1afe78c3e-20250122", | ||
"react-router": "6.28.1", | ||
"react-router-dom": "6.28.1", | ||
"turbo-stream": "2.4.0" | ||
}, | ||
"devDependencies": { | ||
"@remix-run/node": "0.0.0-nightly-1ac5c50dd-20240726", | ||
"@remix-run/react": "0.0.0-nightly-1ac5c50dd-20240726", | ||
"@remix-run/node": "0.0.0-nightly-1afe78c3e-20250122", | ||
"@remix-run/react": "0.0.0-nightly-1afe78c3e-20250122", | ||
"@testing-library/jest-dom": "^5.17.0", | ||
@@ -30,0 +30,0 @@ "@testing-library/react": "^13.3.0", |
304848
7647
55
+ Added@remix-run/router@1.21.0(transitive)
+ Added@remix-run/server-runtime@0.0.0-nightly-1afe78c3e-20250122(transitive)
+ Addedreact-router@6.28.1(transitive)
+ Addedreact-router-dom@6.28.1(transitive)
+ Addedturbo-stream@2.4.0(transitive)
- Removed@remix-run/router@0.0.0-experimental-9ffbba722(transitive)
- Removed@remix-run/server-runtime@0.0.0-nightly-1ac5c50dd-20240726(transitive)
- Removedreact-router@0.0.0-experimental-9ffbba722(transitive)
- Removedreact-router-dom@0.0.0-experimental-9ffbba722(transitive)
- Removedturbo-stream@2.2.0(transitive)
Updated@remix-run/router@1.21.0
Updated@remix-run/server-runtime@0.0.0-nightly-1afe78c3e-20250122
Updatedreact-router@6.28.1
Updatedreact-router-dom@6.28.1
Updatedturbo-stream@2.4.0