@remix-run/react
Advanced tools
Comparing version 0.0.0-nightly-81ec1b7-20220830 to 0.0.0-nightly-825f70ebd-20240914
/** | ||
* @remix-run/react v0.0.0-nightly-81ec1b7-20220830 | ||
* @remix-run/react v0.0.0-nightly-825f70ebd-20240914 | ||
* | ||
@@ -19,3 +19,2 @@ * Copyright (c) Remix Software Inc. | ||
var source = arguments[i]; | ||
for (var key in source) { | ||
@@ -27,3 +26,2 @@ if (Object.prototype.hasOwnProperty.call(source, key)) { | ||
} | ||
return target; | ||
@@ -30,0 +28,0 @@ }; |
@@ -0,8 +1,29 @@ | ||
import type { HydrationState, Router } from "@remix-run/router"; | ||
import type { ReactElement } from "react"; | ||
import type { EntryContext } from "./entry"; | ||
import type { AssetsManifest, FutureConfig } from "./entry"; | ||
import type { RouteModules } from "./routeModules"; | ||
declare global { | ||
var __remixContext: EntryContext; | ||
var __remixContext: { | ||
basename?: string; | ||
state: HydrationState; | ||
criticalCss?: string; | ||
future: FutureConfig; | ||
isSpaMode: boolean; | ||
stream: ReadableStream<Uint8Array> | undefined; | ||
streamController: ReadableStreamDefaultController<Uint8Array>; | ||
a?: number; | ||
dev?: { | ||
port?: number; | ||
hmrRuntime?: string; | ||
}; | ||
}; | ||
var __remixRouter: Router; | ||
var __remixRouteModules: RouteModules; | ||
var __remixManifest: EntryContext["manifest"]; | ||
var __remixManifest: AssetsManifest; | ||
var __remixRevalidation: number | undefined; | ||
var __remixHdrActive: boolean; | ||
var __remixClearCriticalCss: (() => void) | undefined; | ||
var $RefreshRuntime$: { | ||
performReactRefresh: () => void; | ||
}; | ||
} | ||
@@ -9,0 +30,0 @@ export interface RemixBrowserProps { |
/** | ||
* @remix-run/react v0.0.0-nightly-81ec1b7-20220830 | ||
* @remix-run/react v0.0.0-nightly-825f70ebd-20240914 | ||
* | ||
@@ -15,5 +15,13 @@ * Copyright (c) Remix Software Inc. | ||
var history = require('history'); | ||
var router$1 = require('@remix-run/router'); | ||
var React = require('react'); | ||
var reactRouter = require('react-router'); | ||
var reactRouterDom = require('react-router-dom'); | ||
var components = require('./components.js'); | ||
var errorBoundaries = require('./errorBoundaries.js'); | ||
var errors = require('./errors.js'); | ||
var routes = require('./routes.js'); | ||
var singleFetch = require('./single-fetch.js'); | ||
var invariant = require('./invariant.js'); | ||
var fogOfWar = require('./fog-of-war.js'); | ||
@@ -40,4 +48,23 @@ function _interopNamespace(e) { | ||
// TODO: We eventually might not want to import anything directly from `history` | ||
/* eslint-disable prefer-let/prefer-let */ | ||
/* eslint-enable prefer-let/prefer-let */ | ||
let stateDecodingPromise; | ||
let router; | ||
let routerInitialized = false; | ||
let hmrRouterReadyResolve; | ||
// There's a race condition with HMR where the remix:manifest is signaled before | ||
// the router is assigned in the RemixBrowser component. This promise gates the | ||
// HMR handler until the router is ready | ||
new Promise(resolve => { | ||
// body of a promise is executed immediately, so this can be resolved outside | ||
// of the promise body | ||
hmrRouterReadyResolve = resolve; | ||
}).catch(() => { | ||
// This is a noop catch handler to avoid unhandled promise rejection warnings | ||
// in the console. The promise is never rejected. | ||
return undefined; | ||
}); | ||
/** | ||
@@ -49,32 +76,166 @@ * The entry point for a Remix app when it is rendered in the browser (in | ||
function RemixBrowser(_props) { | ||
let historyRef = React__namespace.useRef(); | ||
if (!router) { | ||
// When single fetch is enabled, we need to suspend until the initial state | ||
// snapshot is decoded into window.__remixContext.state | ||
if (window.__remixContext.future.unstable_singleFetch) { | ||
// Note: `stateDecodingPromise` is not coupled to `router` - we'll reach this | ||
// code potentially many times waiting for our state to arrive, but we'll | ||
// then only get past here and create the `router` one time | ||
if (!stateDecodingPromise) { | ||
let stream = window.__remixContext.stream; | ||
invariant(stream, "No stream found for single fetch decoding"); | ||
window.__remixContext.stream = undefined; | ||
stateDecodingPromise = singleFetch.decodeViaTurboStream(stream, window).then(value => { | ||
window.__remixContext.state = value.value; | ||
stateDecodingPromise.value = true; | ||
}).catch(e => { | ||
stateDecodingPromise.error = e; | ||
}); | ||
} | ||
if (stateDecodingPromise.error) { | ||
throw stateDecodingPromise.error; | ||
} | ||
if (!stateDecodingPromise.value) { | ||
throw stateDecodingPromise; | ||
} | ||
} | ||
let routes$1 = routes.createClientRoutes(window.__remixManifest.routes, window.__remixRouteModules, window.__remixContext.state, window.__remixContext.future, window.__remixContext.isSpaMode); | ||
let hydrationData = undefined; | ||
if (!window.__remixContext.isSpaMode) { | ||
// Create a shallow clone of `loaderData` we can mutate for partial hydration. | ||
// When a route exports a `clientLoader` and a `HydrateFallback`, the SSR will | ||
// render the fallback so we need the client to do the same for hydration. | ||
// The server loader data has already been exposed to these route `clientLoader`'s | ||
// in `createClientRoutes` above, so we need to clear out the version we pass to | ||
// `createBrowserRouter` so it initializes and runs the client loaders. | ||
hydrationData = { | ||
...window.__remixContext.state, | ||
loaderData: { | ||
...window.__remixContext.state.loaderData | ||
} | ||
}; | ||
let initialMatches = reactRouterDom.matchRoutes(routes$1, window.location, window.__remixContext.basename); | ||
if (initialMatches) { | ||
for (let match of initialMatches) { | ||
let routeId = match.route.id; | ||
let route = window.__remixRouteModules[routeId]; | ||
let manifestRoute = window.__remixManifest.routes[routeId]; | ||
// Clear out the loaderData to avoid rendering the route component when the | ||
// route opted into clientLoader hydration and either: | ||
// * gave us a HydrateFallback | ||
// * or doesn't have a server loader and we have no data to render | ||
if (route && routes.shouldHydrateRouteLoader(manifestRoute, route, window.__remixContext.isSpaMode) && (route.HydrateFallback || !manifestRoute.hasLoader)) { | ||
hydrationData.loaderData[routeId] = undefined; | ||
} else if (manifestRoute && !manifestRoute.hasLoader) { | ||
// Since every Remix route gets a `loader` on the client side to load | ||
// the route JS module, we need to add a `null` value to `loaderData` | ||
// for any routes that don't have server loaders so our partial | ||
// hydration logic doesn't kick off the route module loaders during | ||
// hydration | ||
hydrationData.loaderData[routeId] = null; | ||
} | ||
} | ||
} | ||
if (hydrationData && hydrationData.errors) { | ||
hydrationData.errors = errors.deserializeErrors(hydrationData.errors); | ||
} | ||
} | ||
if (historyRef.current == null) { | ||
historyRef.current = history.createBrowserHistory({ | ||
window | ||
// We don't use createBrowserRouter here because we need fine-grained control | ||
// over initialization to support synchronous `clientLoader` flows. | ||
router = router$1.createRouter({ | ||
routes: routes$1, | ||
history: router$1.createBrowserHistory(), | ||
basename: window.__remixContext.basename, | ||
future: { | ||
v7_normalizeFormMethod: true, | ||
v7_fetcherPersist: window.__remixContext.future.v3_fetcherPersist, | ||
v7_partialHydration: true, | ||
v7_prependBasename: true, | ||
v7_relativeSplatPath: window.__remixContext.future.v3_relativeSplatPath, | ||
// Single fetch enables this underlying behavior | ||
v7_skipActionErrorRevalidation: window.__remixContext.future.unstable_singleFetch === true | ||
}, | ||
hydrationData, | ||
mapRouteProperties: reactRouter.UNSAFE_mapRouteProperties, | ||
unstable_dataStrategy: window.__remixContext.future.unstable_singleFetch ? singleFetch.getSingleFetchDataStrategy(window.__remixManifest, window.__remixRouteModules, () => router) : undefined, | ||
unstable_patchRoutesOnNavigation: fogOfWar.getPatchRoutesOnNavigationFunction(window.__remixManifest, window.__remixRouteModules, window.__remixContext.future, window.__remixContext.isSpaMode, window.__remixContext.basename) | ||
}); | ||
// We can call initialize() immediately if the router doesn't have any | ||
// loaders to run on hydration | ||
if (router.state.initialized) { | ||
routerInitialized = true; | ||
router.initialize(); | ||
} | ||
// @ts-ignore | ||
router.createRoutesForHMR = routes.createClientRoutesWithHMRRevalidationOptOut; | ||
window.__remixRouter = router; | ||
// Notify that the router is ready for HMR | ||
if (hmrRouterReadyResolve) { | ||
hmrRouterReadyResolve(router); | ||
} | ||
} | ||
let history$1 = historyRef.current; | ||
let [state, dispatch] = React__namespace.useReducer((_, update) => update, { | ||
action: history$1.action, | ||
location: history$1.location | ||
}); | ||
React__namespace.useLayoutEffect(() => history$1.listen(dispatch), [history$1]); | ||
let entryContext = window.__remixContext; | ||
entryContext.manifest = window.__remixManifest; | ||
entryContext.routeModules = window.__remixRouteModules; // In the browser, we don't need this because a) in the case of loader | ||
// errors we already know the order and b) in the case of render errors | ||
// React knows the order and handles error boundaries normally. | ||
// Critical CSS can become stale after code changes, e.g. styles might be | ||
// removed from a component, but the styles will still be present in the | ||
// server HTML. This allows our HMR logic to clear the critical CSS state. | ||
entryContext.appState.trackBoundaries = false; | ||
entryContext.appState.trackCatchBoundaries = false; | ||
return /*#__PURE__*/React__namespace.createElement(components.RemixEntry, { | ||
context: entryContext, | ||
action: state.action, | ||
location: state.location, | ||
navigator: history$1 | ||
}); | ||
let [criticalCss, setCriticalCss] = React__namespace.useState(process.env.NODE_ENV === "development" ? window.__remixContext.criticalCss : undefined); | ||
if (process.env.NODE_ENV === "development") { | ||
window.__remixClearCriticalCss = () => setCriticalCss(undefined); | ||
} | ||
// This is due to the short circuit return above when the pathname doesn't | ||
// match and we force a hard reload. This is an exceptional scenario in which | ||
// we can't hydrate anyway. | ||
let [location, setLocation] = React__namespace.useState(router.state.location); | ||
React__namespace.useLayoutEffect(() => { | ||
// If we had to run clientLoaders on hydration, we delay initialization until | ||
// after we've hydrated to avoid hydration issues from synchronous client loaders | ||
if (!routerInitialized) { | ||
routerInitialized = true; | ||
router.initialize(); | ||
} | ||
}, []); | ||
React__namespace.useLayoutEffect(() => { | ||
return router.subscribe(newState => { | ||
if (newState.location !== location) { | ||
setLocation(newState.location); | ||
} | ||
}); | ||
}, [location]); | ||
fogOfWar.useFogOFWarDiscovery(router, window.__remixManifest, window.__remixRouteModules, window.__remixContext.future, window.__remixContext.isSpaMode); | ||
// We need to include a wrapper RemixErrorBoundary here in case the root error | ||
// boundary also throws and we need to bubble up outside of the router entirely. | ||
// Then we need a stateful location here so the user can back-button navigate | ||
// out of there | ||
return ( | ||
/*#__PURE__*/ | ||
// This fragment is important to ensure we match the <RemixServer> JSX | ||
// structure so that useId values hydrate correctly | ||
React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(components.RemixContext.Provider, { | ||
value: { | ||
manifest: window.__remixManifest, | ||
routeModules: window.__remixRouteModules, | ||
future: window.__remixContext.future, | ||
criticalCss, | ||
isSpaMode: window.__remixContext.isSpaMode | ||
} | ||
}, /*#__PURE__*/React__namespace.createElement(errorBoundaries.RemixErrorBoundary, { | ||
location: location | ||
}, /*#__PURE__*/React__namespace.createElement(reactRouterDom.RouterProvider, { | ||
router: router, | ||
fallbackElement: null, | ||
future: { | ||
v7_startTransition: true | ||
} | ||
}))), window.__remixContext.future.unstable_singleFetch ? /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null) : null) | ||
); | ||
} | ||
exports.RemixBrowser = RemixBrowser; |
@@ -1,58 +0,40 @@ | ||
import type { Action, Location } from "history"; | ||
import type { FormHTMLAttributes } from "react"; | ||
import * as React from "react"; | ||
import type { Navigator, Params } from "react-router"; | ||
import type { LinkProps, NavLinkProps } from "react-router-dom"; | ||
import type { UIMatch as UIMatchRR } from "@remix-run/router"; | ||
import type { FetcherWithComponents, FormProps, LinkProps, NavLinkProps } from "react-router-dom"; | ||
import { useFetcher as useFetcherRR } from "react-router-dom"; | ||
import type { SerializeFrom } from "@remix-run/server-runtime"; | ||
import type { AppData, FormEncType, FormMethod } from "./data"; | ||
import type { EntryContext, AssetsManifest } from "./entry"; | ||
import type { AppState } from "./errors"; | ||
import type { AppData } from "./data"; | ||
import type { RemixContextObject } from "./entry"; | ||
import type { PrefetchPageDescriptor } from "./links"; | ||
import type { ClientRoute } from "./routes"; | ||
import type { RouteData } from "./routeData"; | ||
import type { RouteMatch as BaseRouteMatch } from "./routeMatching"; | ||
import type { RouteModules } from "./routeModules"; | ||
import { createTransitionManager } from "./transition"; | ||
import type { Transition, Fetcher } from "./transition"; | ||
interface RemixEntryContextType { | ||
manifest: AssetsManifest; | ||
matches: BaseRouteMatch<ClientRoute>[]; | ||
routeData: RouteData; | ||
actionData?: RouteData; | ||
pendingLocation?: Location; | ||
appState: AppState; | ||
routeModules: RouteModules; | ||
serverHandoffString?: string; | ||
clientRoutes: ClientRoute[]; | ||
transitionManager: ReturnType<typeof createTransitionManager>; | ||
} | ||
export declare const RemixEntryContext: React.Context<RemixEntryContextType | undefined>; | ||
export declare function RemixEntry({ context: entryContext, action, location: historyLocation, navigator: _navigator, static: staticProp, }: { | ||
context: EntryContext; | ||
action: Action; | ||
location: Location; | ||
navigator: Navigator; | ||
static?: boolean; | ||
}): JSX.Element; | ||
export declare function RemixRoute({ id }: { | ||
id: string; | ||
}): JSX.Element; | ||
import type { RouteHandle } from "./routeModules"; | ||
export declare const RemixContext: React.Context<RemixContextObject | undefined>; | ||
export declare function useRemixContext(): RemixContextObject; | ||
/** | ||
* Defines the discovery behavior of the link: | ||
* | ||
* - "render": Eagerly discover when the link is rendered (default) | ||
* - "none": No eager discovery - discover when the link is clicked | ||
*/ | ||
export type DiscoverBehavior = "render" | "none"; | ||
/** | ||
* Defines the prefetching behavior of the link: | ||
* | ||
* - "none": Never fetched | ||
* - "intent": Fetched when the user focuses or hovers the link | ||
* - "render": Fetched when the link is rendered | ||
* - "none": Never fetched | ||
* - "viewport": Fetched when the link is in the viewport | ||
*/ | ||
declare type PrefetchBehavior = "intent" | "render" | "none"; | ||
type PrefetchBehavior = "intent" | "render" | "none" | "viewport"; | ||
export interface RemixLinkProps extends LinkProps { | ||
discover?: DiscoverBehavior; | ||
prefetch?: PrefetchBehavior; | ||
} | ||
export interface RemixNavLinkProps extends NavLinkProps { | ||
discover?: DiscoverBehavior; | ||
prefetch?: PrefetchBehavior; | ||
} | ||
/** | ||
* A special kind of `<Link>` that knows whether or not it is "active". | ||
* A special kind of `<Link>` that knows whether it is "active". | ||
* | ||
* @see https://remix.run/api/remix#navlink | ||
* @see https://remix.run/components/nav-link | ||
*/ | ||
@@ -65,6 +47,17 @@ declare let NavLink: React.ForwardRefExoticComponent<RemixNavLinkProps & React.RefAttributes<HTMLAnchorElement>>; | ||
* | ||
* @see https://remix.run/api/remix#link | ||
* @see https://remix.run/components/link | ||
*/ | ||
declare let Link: React.ForwardRefExoticComponent<RemixLinkProps & React.RefAttributes<HTMLAnchorElement>>; | ||
export { Link }; | ||
export interface RemixFormProps extends FormProps { | ||
discover?: DiscoverBehavior; | ||
} | ||
/** | ||
* This component renders a form tag and is the primary way the user will | ||
* submit information via your website. | ||
* | ||
* @see https://remix.run/components/form | ||
*/ | ||
declare let Form: React.ForwardRefExoticComponent<RemixFormProps & React.RefAttributes<HTMLFormElement>>; | ||
export { Form }; | ||
export declare function composeEventHandlers<EventType extends React.SyntheticEvent | Event>(theirHandler: ((event: EventType) => any) | undefined, ourHandler: (event: EventType) => any): (event: EventType) => any; | ||
@@ -74,7 +67,7 @@ /** | ||
* | ||
* @see https://remix.run/api/remix#meta-links-scripts | ||
* @see https://remix.run/components/links | ||
*/ | ||
export declare function Links(): JSX.Element; | ||
export declare function Links(): React.JSX.Element; | ||
/** | ||
* This component renders all of the `<link rel="prefetch">` and | ||
* This component renders all the `<link rel="prefetch">` and | ||
* `<link rel="modulepreload"/>` tags for all the assets (data, modules, css) of | ||
@@ -85,12 +78,18 @@ * a given page. | ||
* @param props.page | ||
* @see https://remix.run/api/remix#prefetchpagelinks- | ||
* @see https://remix.run/components/prefetch-page-links | ||
*/ | ||
export declare function PrefetchPageLinks({ page, ...dataLinkProps }: PrefetchPageDescriptor): JSX.Element | null; | ||
export declare function PrefetchPageLinks({ page, ...dataLinkProps }: PrefetchPageDescriptor): React.JSX.Element | null; | ||
/** | ||
* Renders the `<title>` and `<meta>` tags for the current routes. | ||
* Renders HTML tags related to metadata for the current route. | ||
* | ||
* @see https://remix.run/api/remix#meta-links-scripts | ||
* @see https://remix.run/components/meta | ||
*/ | ||
export declare function Meta(): JSX.Element; | ||
declare type ScriptProps = Omit<React.HTMLProps<HTMLScriptElement>, "children" | "async" | "defer" | "src" | "type" | "noModule" | "dangerouslySetInnerHTML" | "suppressHydrationWarning">; | ||
export declare function Meta(): React.JSX.Element; | ||
export interface AwaitProps<Resolve> { | ||
children: React.ReactNode | ((value: Awaited<Resolve>) => React.ReactNode); | ||
errorElement?: React.ReactNode; | ||
resolve: Resolve; | ||
} | ||
export declare function Await<Resolve>(props: AwaitProps<Resolve>): React.JSX.Element; | ||
export type ScriptProps = Omit<React.HTMLProps<HTMLScriptElement>, "children" | "async" | "defer" | "src" | "type" | "noModule" | "dangerouslySetInnerHTML" | "suppressHydrationWarning">; | ||
/** | ||
@@ -104,209 +103,50 @@ * Renders the `<script>` tags needed for the initial render. Bundles for | ||
* | ||
* @see https://remix.run/api/remix#meta-links-scripts | ||
* @see https://remix.run/components/scripts | ||
*/ | ||
export declare function Scripts(props: ScriptProps): JSX.Element; | ||
export interface FormProps extends FormHTMLAttributes<HTMLFormElement> { | ||
/** | ||
* The HTTP verb to use when the form is submit. Supports "get", "post", | ||
* "put", "delete", "patch". | ||
* | ||
* Note: If JavaScript is disabled, you'll need to implement your own "method | ||
* override" to support more than just GET and POST. | ||
*/ | ||
method?: FormMethod; | ||
/** | ||
* Normal `<form action>` but supports React Router's relative paths. | ||
*/ | ||
action?: string; | ||
/** | ||
* Normal `<form encType>`. | ||
* | ||
* Note: Remix defaults to `application/x-www-form-urlencoded` and also | ||
* supports `multipart/form-data`. | ||
*/ | ||
encType?: FormEncType; | ||
/** | ||
* Forces a full document navigation instead of a fetch. | ||
*/ | ||
reloadDocument?: boolean; | ||
/** | ||
* Replaces the current entry in the browser history stack when the form | ||
* navigates. Use this if you don't want the user to be able to click "back" | ||
* to the page with the form on it. | ||
*/ | ||
replace?: boolean; | ||
/** | ||
* A function to call when the form is submitted. If you call | ||
* `event.preventDefault()` then this form will not do anything. | ||
*/ | ||
onSubmit?: React.FormEventHandler<HTMLFormElement>; | ||
} | ||
export declare function Scripts(props: ScriptProps): React.JSX.Element | null; | ||
export type UIMatch<D = AppData, H = RouteHandle> = UIMatchRR<SerializeFrom<D>, H>; | ||
/** | ||
* A Remix-aware `<form>`. It behaves like a normal form except that the | ||
* interaction with the server is with `fetch` instead of new document | ||
* requests, allowing components to add nicer UX to the page as the form is | ||
* submitted and returns with data. | ||
* Returns the active route matches, useful for accessing loaderData for | ||
* parent/child routes or the route "handle" property | ||
* | ||
* @see https://remix.run/api/remix#form | ||
* @see https://remix.run/hooks/use-matches | ||
*/ | ||
declare let Form: React.ForwardRefExoticComponent<FormProps & React.RefAttributes<HTMLFormElement>>; | ||
export { Form }; | ||
interface FormImplProps extends FormProps { | ||
fetchKey?: string; | ||
} | ||
declare let FormImpl: React.ForwardRefExoticComponent<FormImplProps & React.RefAttributes<HTMLFormElement>>; | ||
export { FormImpl }; | ||
export declare function useMatches(): UIMatch[]; | ||
/** | ||
* Resolves a `<form action>` path relative to the current route. | ||
* Returns the JSON parsed data from the current route's `loader`. | ||
* | ||
* @see https://remix.run/api/remix#useformaction | ||
* @see https://remix.run/hooks/use-loader-data | ||
*/ | ||
export declare function useFormAction(action?: string, method?: FormMethod): string; | ||
export interface SubmitOptions { | ||
/** | ||
* The HTTP method used to submit the form. Overrides `<form method>`. | ||
* Defaults to "GET". | ||
*/ | ||
method?: FormMethod; | ||
/** | ||
* The action URL path used to submit the form. Overrides `<form action>`. | ||
* Defaults to the path of the current route. | ||
* | ||
* Note: It is assumed the path is already resolved. If you need to resolve a | ||
* relative path, use `useFormAction`. | ||
*/ | ||
action?: string; | ||
/** | ||
* The action URL used to submit the form. Overrides `<form encType>`. | ||
* Defaults to "application/x-www-form-urlencoded". | ||
*/ | ||
encType?: FormEncType; | ||
/** | ||
* Set `true` to replace the current entry in the browser's history stack | ||
* instead of creating a new one (i.e. stay on "the same page"). Defaults | ||
* to `false`. | ||
*/ | ||
replace?: boolean; | ||
} | ||
export declare function useLoaderData<T = AppData>(): SerializeFrom<T>; | ||
/** | ||
* Submits a HTML `<form>` to the server without reloading the page. | ||
*/ | ||
export interface SubmitFunction { | ||
( | ||
/** | ||
* Specifies the `<form>` to be submitted to the server, a specific | ||
* `<button>` or `<input type="submit">` to use to submit the form, or some | ||
* arbitrary data to submit. | ||
* | ||
* Note: When using a `<button>` its `name` and `value` will also be | ||
* included in the form data that is submitted. | ||
*/ | ||
target: HTMLFormElement | HTMLButtonElement | HTMLInputElement | FormData | URLSearchParams | { | ||
[name: string]: string; | ||
} | null, | ||
/** | ||
* Options that override the `<form>`'s own attributes. Required when | ||
* submitting arbitrary data without a backing `<form>`. | ||
*/ | ||
options?: SubmitOptions): void; | ||
} | ||
/** | ||
* Returns a function that may be used to programmatically submit a form (or | ||
* some arbitrary data) to the server. | ||
* Returns the loaderData for the given routeId. | ||
* | ||
* @see https://remix.run/api/remix#usesubmit | ||
* @see https://remix.run/hooks/use-route-loader-data | ||
*/ | ||
export declare function useSubmit(): SubmitFunction; | ||
export declare function useSubmitImpl(key?: string): SubmitFunction; | ||
export declare function useRouteLoaderData<T = AppData>(routeId: string): SerializeFrom<T> | undefined; | ||
/** | ||
* Setup a callback to be fired on the window's `beforeunload` event. This is | ||
* useful for saving some data to `window.localStorage` just before the page | ||
* refreshes, which automatically happens on the next `<Link>` click when Remix | ||
* detects a new version of the app is available on the server. | ||
* | ||
* Note: The `callback` argument should be a function created with | ||
* `React.useCallback()`. | ||
* | ||
* @see https://remix.run/api/remix#usebeforeunload | ||
*/ | ||
export declare function useBeforeUnload(callback: () => any): void; | ||
export interface RouteMatch { | ||
/** | ||
* The id of the matched route | ||
*/ | ||
id: string; | ||
/** | ||
* The pathname of the matched route | ||
*/ | ||
pathname: string; | ||
/** | ||
* The dynamic parameters of the matched route | ||
* | ||
* @see https://remix.run/docs/api/conventions#dynamic-route-parameters | ||
*/ | ||
params: Params<string>; | ||
/** | ||
* Any route data associated with the matched route | ||
*/ | ||
data: RouteData; | ||
/** | ||
* The exported `handle` object of the matched route. | ||
* | ||
* @see https://remix.run/docs/api/conventions#handle | ||
*/ | ||
handle: undefined | { | ||
[key: string]: any; | ||
}; | ||
} | ||
/** | ||
* Returns the current route matches on the page. This is useful for creating | ||
* layout abstractions with your current routes. | ||
* | ||
* @see https://remix.run/api/remix#usematches | ||
*/ | ||
export declare function useMatches(): RouteMatch[]; | ||
/** | ||
* Returns the JSON parsed data from the current route's `loader`. | ||
* | ||
* @see https://remix.run/api/remix#useloaderdata | ||
*/ | ||
export declare function useLoaderData<T = AppData>(): SerializeFrom<T>; | ||
/** | ||
* Returns the JSON parsed data from the current route's `action`. | ||
* | ||
* @see https://remix.run/api/remix#useactiondata | ||
* @see https://remix.run/hooks/use-action-data | ||
*/ | ||
export declare function useActionData<T = AppData>(): SerializeFrom<T> | undefined; | ||
/** | ||
* Returns everything you need to know about a page transition to build pending | ||
* navigation indicators and optimistic UI on data mutations. | ||
* | ||
* @see https://remix.run/api/remix#usetransition | ||
*/ | ||
export declare function useTransition(): Transition; | ||
export declare type FetcherWithComponents<TData> = Fetcher<TData> & { | ||
Form: React.ForwardRefExoticComponent<FormProps & React.RefAttributes<HTMLFormElement>>; | ||
submit: SubmitFunction; | ||
load: (href: string) => void; | ||
}; | ||
/** | ||
* Interacts with route loaders and actions without causing a navigation. Great | ||
* for any interaction that stays on the same page. | ||
* | ||
* @see https://remix.run/api/remix#usefetcher | ||
* @see https://remix.run/hooks/use-fetcher | ||
*/ | ||
export declare function useFetcher<TData = any>(): FetcherWithComponents<TData>; | ||
export declare function useFetcher<TData = AppData>(opts?: Parameters<typeof useFetcherRR>[0]): FetcherWithComponents<SerializeFrom<TData>>; | ||
/** | ||
* Provides all fetchers currently on the page. Useful for layouts and parent | ||
* routes that need to provide pending/optimistic UI regarding the fetch. | ||
* This component connects your app to the Remix asset server and | ||
* automatically reloads the page when files change in development. | ||
* In production, it renders null, so you can safely render it always in your root route. | ||
* | ||
* @see https://remix.run/api/remix#usefetchers | ||
* @see https://remix.run/docs/components/live-reload | ||
*/ | ||
export declare function useFetchers(): Fetcher[]; | ||
export declare const LiveReload: (() => null) | (({ port, nonce, }: { | ||
export declare const LiveReload: ({ origin, port, timeoutMs, nonce, }: { | ||
origin?: string | undefined; | ||
port?: number | undefined; | ||
/** | ||
* @deprecated this property is no longer relevant. | ||
*/ | ||
timeoutMs?: number | undefined; | ||
nonce?: string | undefined; | ||
}) => JSX.Element); | ||
}) => React.JSX.Element | null; |
/** | ||
* @remix-run/react v0.0.0-nightly-81ec1b7-20220830 | ||
* @remix-run/react v0.0.0-nightly-825f70ebd-20240914 | ||
* | ||
@@ -18,10 +18,7 @@ * Copyright (c) Remix Software Inc. | ||
var reactRouterDom = require('react-router-dom'); | ||
var history = require('history'); | ||
var errorBoundaries = require('./errorBoundaries.js'); | ||
var invariant = require('./invariant.js'); | ||
var links = require('./links.js'); | ||
var markup = require('./markup.js'); | ||
var routes = require('./routes.js'); | ||
var routeMatching = require('./routeMatching.js'); | ||
var transition = require('./transition.js'); | ||
var singleFetch = require('./single-fetch.js'); | ||
var fogOfWar = require('./fog-of-war.js'); | ||
@@ -48,262 +45,41 @@ function _interopNamespace(e) { | ||
const RemixEntryContext = /*#__PURE__*/React__namespace.createContext(undefined); | ||
function useRemixEntryContext() { | ||
let context = React__namespace.useContext(RemixEntryContext); | ||
invariant(context, "You must render this element inside a <Remix> element"); | ||
function useDataRouterContext() { | ||
let context = React__namespace.useContext(reactRouterDom.UNSAFE_DataRouterContext); | ||
invariant(context, "You must render this element inside a <DataRouterContext.Provider> element"); | ||
return context; | ||
} | ||
function RemixEntry({ | ||
context: entryContext, | ||
action, | ||
location: historyLocation, | ||
navigator: _navigator, | ||
static: staticProp = false | ||
}) { | ||
let { | ||
manifest, | ||
routeData: documentLoaderData, | ||
actionData: documentActionData, | ||
routeModules, | ||
serverHandoffString, | ||
appState: entryComponentDidCatchEmulator | ||
} = entryContext; | ||
let clientRoutes = React__namespace.useMemo(() => routes.createClientRoutes(manifest.routes, routeModules, RemixRoute), [manifest, routeModules]); | ||
let [clientState, setClientState] = React__namespace.useState(entryComponentDidCatchEmulator); | ||
let [transitionManager] = React__namespace.useState(() => { | ||
return transition.createTransitionManager({ | ||
routes: clientRoutes, | ||
actionData: documentActionData, | ||
loaderData: documentLoaderData, | ||
location: historyLocation, | ||
catch: entryComponentDidCatchEmulator.catch, | ||
catchBoundaryId: entryComponentDidCatchEmulator.catchBoundaryRouteId, | ||
onRedirect: _navigator.replace | ||
}); | ||
}); | ||
React__namespace.useEffect(() => { | ||
let subscriber = state => { | ||
setClientState({ | ||
catch: state.catch, | ||
error: state.error, | ||
catchBoundaryRouteId: state.catchBoundaryId, | ||
loaderBoundaryRouteId: state.errorBoundaryId, | ||
renderBoundaryRouteId: null, | ||
trackBoundaries: false, | ||
trackCatchBoundaries: false | ||
}); | ||
}; | ||
return transitionManager.subscribe(subscriber); | ||
}, [transitionManager]); // Ensures pushes interrupting pending navigations use replace | ||
// TODO: Move this to React Router | ||
let navigator = React__namespace.useMemo(() => { | ||
let push = (to, state) => { | ||
return transitionManager.getState().transition.state !== "idle" ? _navigator.replace(to, state) : _navigator.push(to, state); | ||
}; | ||
return { ..._navigator, | ||
push | ||
}; | ||
}, [_navigator, transitionManager]); | ||
let { | ||
location, | ||
matches, | ||
loaderData, | ||
actionData | ||
} = transitionManager.getState(); // Send new location to the transition manager | ||
React__namespace.useEffect(() => { | ||
let { | ||
location | ||
} = transitionManager.getState(); | ||
if (historyLocation === location) return; | ||
transitionManager.send({ | ||
type: "navigation", | ||
location: historyLocation, | ||
submission: consumeNextNavigationSubmission(), | ||
action | ||
}); | ||
}, [transitionManager, historyLocation, action]); // If we tried to render and failed, and the app threw before rendering any | ||
// routes, get the error and pass it to the ErrorBoundary to emulate | ||
// `componentDidCatch` | ||
let ssrErrorBeforeRoutesRendered = clientState.error && clientState.renderBoundaryRouteId === null && clientState.loaderBoundaryRouteId === null ? deserializeError(clientState.error) : undefined; | ||
let ssrCatchBeforeRoutesRendered = clientState.catch && clientState.catchBoundaryRouteId === null ? clientState.catch : undefined; | ||
return /*#__PURE__*/React__namespace.createElement(RemixEntryContext.Provider, { | ||
value: { | ||
matches, | ||
manifest, | ||
appState: clientState, | ||
routeModules, | ||
serverHandoffString, | ||
clientRoutes, | ||
routeData: loaderData, | ||
actionData, | ||
transitionManager | ||
} | ||
}, /*#__PURE__*/React__namespace.createElement(errorBoundaries.RemixErrorBoundary, { | ||
location: location, | ||
component: errorBoundaries.RemixRootDefaultErrorBoundary, | ||
error: ssrErrorBeforeRoutesRendered | ||
}, /*#__PURE__*/React__namespace.createElement(errorBoundaries.RemixCatchBoundary, { | ||
location: location, | ||
component: errorBoundaries.RemixRootDefaultCatchBoundary, | ||
catch: ssrCatchBeforeRoutesRendered | ||
}, /*#__PURE__*/React__namespace.createElement(reactRouterDom.Router, { | ||
navigationType: action, | ||
location: location, | ||
navigator: navigator, | ||
static: staticProp | ||
}, /*#__PURE__*/React__namespace.createElement(Routes, null))))); | ||
function useDataRouterStateContext() { | ||
let context = React__namespace.useContext(reactRouterDom.UNSAFE_DataRouterStateContext); | ||
invariant(context, "You must render this element inside a <DataRouterStateContext.Provider> element"); | ||
return context; | ||
} | ||
function deserializeError(data) { | ||
let error = new Error(data.message); | ||
error.stack = data.stack; | ||
return error; | ||
} | ||
//////////////////////////////////////////////////////////////////////////////// | ||
// RemixContext | ||
function Routes() { | ||
// TODO: Add `renderMatches` function to RR that we can use and then we don't | ||
// need this component, we can just `renderMatches` from RemixEntry | ||
let { | ||
clientRoutes | ||
} = useRemixEntryContext(); // fallback to the root if we don't have a match | ||
let element = reactRouterDom.useRoutes(clientRoutes) || clientRoutes[0].element; | ||
return element; | ||
} //////////////////////////////////////////////////////////////////////////////// | ||
// RemixRoute | ||
const RemixRouteContext = /*#__PURE__*/React__namespace.createContext(undefined); | ||
function useRemixRouteContext() { | ||
let context = React__namespace.useContext(RemixRouteContext); | ||
invariant(context, "You must render this element in a remix route element"); | ||
const RemixContext = /*#__PURE__*/React__namespace.createContext(undefined); | ||
RemixContext.displayName = "Remix"; | ||
function useRemixContext() { | ||
let context = React__namespace.useContext(RemixContext); | ||
invariant(context, "You must render this element inside a <Remix> element"); | ||
return context; | ||
} | ||
function DefaultRouteComponent({ | ||
id | ||
}) { | ||
throw new Error(`Route "${id}" has no component! Please go add a \`default\` export in the route module file.\n` + "If you were trying to navigate or submit to a resource route, use `<a>` instead of `<Link>` or `<Form reloadDocument>`."); | ||
} | ||
function RemixRoute({ | ||
id | ||
}) { | ||
let location = reactRouterDom.useLocation(); | ||
let { | ||
routeData, | ||
routeModules, | ||
appState | ||
} = useRemixEntryContext(); // This checks prevent cryptic error messages such as: 'Cannot read properties of undefined (reading 'root')' | ||
invariant(routeData, "Cannot initialize 'routeData'. This normally occurs when you have server code in your client modules.\n" + "Check this link for more details:\nhttps://remix.run/pages/gotchas#server-code-in-client-bundles"); | ||
invariant(routeModules, "Cannot initialize 'routeModules'. This normally occurs when you have server code in your client modules.\n" + "Check this link for more details:\nhttps://remix.run/pages/gotchas#server-code-in-client-bundles"); | ||
let data = routeData[id]; | ||
let { | ||
default: Component, | ||
CatchBoundary, | ||
ErrorBoundary | ||
} = routeModules[id]; | ||
let element = Component ? /*#__PURE__*/React__namespace.createElement(Component, null) : /*#__PURE__*/React__namespace.createElement(DefaultRouteComponent, { | ||
id: id | ||
}); | ||
let context = { | ||
data, | ||
id | ||
}; | ||
if (CatchBoundary) { | ||
// If we tried to render and failed, and this route threw the error, find it | ||
// and pass it to the ErrorBoundary to emulate `componentDidCatch` | ||
let maybeServerCaught = appState.catch && appState.catchBoundaryRouteId === id ? appState.catch : undefined; // This needs to run after we check for the error from a previous render, | ||
// otherwise we will incorrectly render this boundary for a loader error | ||
// deeper in the tree. | ||
if (appState.trackCatchBoundaries) { | ||
appState.catchBoundaryRouteId = id; | ||
} | ||
context = maybeServerCaught ? { | ||
id, | ||
get data() { | ||
console.error("You cannot `useLoaderData` in a catch boundary."); | ||
return undefined; | ||
} | ||
} : { | ||
id, | ||
data | ||
}; | ||
element = /*#__PURE__*/React__namespace.createElement(errorBoundaries.RemixCatchBoundary, { | ||
location: location, | ||
component: CatchBoundary, | ||
catch: maybeServerCaught | ||
}, element); | ||
} // Only wrap in error boundary if the route defined one, otherwise let the | ||
// error bubble to the parent boundary. We could default to using error | ||
// boundaries around every route, but now if the app doesn't want users | ||
// seeing the default Remix ErrorBoundary component, they *must* define an | ||
// error boundary for *every* route and that would be annoying. Might as | ||
// well make it required at that point. | ||
// | ||
// By conditionally wrapping like this, we allow apps to define a top level | ||
// ErrorBoundary component and be done with it. Then, if they want to, they | ||
// can add more specific boundaries by exporting ErrorBoundary components | ||
// for whichever routes they please. | ||
// | ||
// NOTE: this kind of logic will move into React Router | ||
if (ErrorBoundary) { | ||
// If we tried to render and failed, and this route threw the error, find it | ||
// and pass it to the ErrorBoundary to emulate `componentDidCatch` | ||
let maybeServerRenderError = appState.error && (appState.renderBoundaryRouteId === id || appState.loaderBoundaryRouteId === id) ? deserializeError(appState.error) : undefined; // This needs to run after we check for the error from a previous render, | ||
// otherwise we will incorrectly render this boundary for a loader error | ||
// deeper in the tree. | ||
if (appState.trackBoundaries) { | ||
appState.renderBoundaryRouteId = id; | ||
} | ||
context = maybeServerRenderError ? { | ||
id, | ||
get data() { | ||
console.error("You cannot `useLoaderData` in an error boundary."); | ||
return undefined; | ||
} | ||
} : { | ||
id, | ||
data | ||
}; | ||
element = /*#__PURE__*/React__namespace.createElement(errorBoundaries.RemixErrorBoundary, { | ||
location: location, | ||
component: ErrorBoundary, | ||
error: maybeServerRenderError | ||
}, element); | ||
} // It's important for the route context to be above the error boundary so that | ||
// a call to `useLoaderData` doesn't accidentally get the parents route's data. | ||
return /*#__PURE__*/React__namespace.createElement(RemixRouteContext.Provider, { | ||
value: context | ||
}, element); | ||
} //////////////////////////////////////////////////////////////////////////////// | ||
//////////////////////////////////////////////////////////////////////////////// | ||
// Public API | ||
/** | ||
* Defines the discovery behavior of the link: | ||
* | ||
* - "render": Eagerly discover when the link is rendered (default) | ||
* - "none": No eager discovery - discover when the link is clicked | ||
*/ | ||
/** | ||
* Defines the prefetching behavior of the link: | ||
* | ||
* - "none": Never fetched | ||
* - "intent": Fetched when the user focuses or hovers the link | ||
* - "render": Fetched when the link is rendered | ||
* - "none": Never fetched | ||
* - "viewport": Fetched when the link is in the viewport | ||
*/ | ||
@@ -321,2 +97,3 @@ | ||
} = theirElementProps; | ||
let ref = React__namespace.useRef(null); | ||
React__namespace.useEffect(() => { | ||
@@ -326,4 +103,17 @@ if (prefetch === "render") { | ||
} | ||
if (prefetch === "viewport") { | ||
let callback = entries => { | ||
entries.forEach(entry => { | ||
setShouldPrefetch(entry.isIntersecting); | ||
}); | ||
}; | ||
let observer = new IntersectionObserver(callback, { | ||
threshold: 0.5 | ||
}); | ||
if (ref.current) observer.observe(ref.current); | ||
return () => { | ||
observer.disconnect(); | ||
}; | ||
} | ||
}, [prefetch]); | ||
let setIntent = () => { | ||
@@ -334,3 +124,2 @@ if (prefetch === "intent") { | ||
}; | ||
let cancelIntent = () => { | ||
@@ -342,3 +131,2 @@ if (prefetch === "intent") { | ||
}; | ||
React__namespace.useEffect(() => { | ||
@@ -354,3 +142,3 @@ if (maybePrefetch) { | ||
}, [maybePrefetch]); | ||
return [shouldPrefetch, { | ||
return [shouldPrefetch, ref, { | ||
onFocus: composeEventHandlers(onFocus, setIntent), | ||
@@ -363,20 +151,26 @@ onBlur: composeEventHandlers(onBlur, cancelIntent), | ||
} | ||
const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i; | ||
function getDiscoverAttr(discover, isAbsolute, reloadDocument) { | ||
return discover === "render" && !isAbsolute && !reloadDocument ? "true" : undefined; | ||
} | ||
/** | ||
* A special kind of `<Link>` that knows whether or not it is "active". | ||
* A special kind of `<Link>` that knows whether it is "active". | ||
* | ||
* @see https://remix.run/api/remix#navlink | ||
* @see https://remix.run/components/nav-link | ||
*/ | ||
let NavLink = /*#__PURE__*/React__namespace.forwardRef(({ | ||
to, | ||
prefetch = "none", | ||
discover = "render", | ||
...props | ||
}, forwardedRef) => { | ||
let isAbsolute = typeof to === "string" && ABSOLUTE_URL_REGEX.test(to); | ||
let href = reactRouterDom.useHref(to); | ||
let [shouldPrefetch, prefetchHandlers] = usePrefetchBehavior(prefetch, props); | ||
return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(reactRouterDom.NavLink, _rollupPluginBabelHelpers["extends"]({ | ||
ref: forwardedRef, | ||
to: to | ||
}, props, prefetchHandlers)), shouldPrefetch ? /*#__PURE__*/React__namespace.createElement(PrefetchPageLinks, { | ||
let [shouldPrefetch, ref, prefetchHandlers] = usePrefetchBehavior(prefetch, props); | ||
return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(reactRouterDom.NavLink, _rollupPluginBabelHelpers["extends"]({}, props, prefetchHandlers, { | ||
ref: mergeRefs(forwardedRef, ref), | ||
to: to, | ||
"data-discover": getDiscoverAttr(discover, isAbsolute, props.reloadDocument) | ||
})), shouldPrefetch && !isAbsolute ? /*#__PURE__*/React__namespace.createElement(PrefetchPageLinks, { | ||
page: href | ||
@@ -386,2 +180,3 @@ }) : null); | ||
NavLink.displayName = "NavLink"; | ||
/** | ||
@@ -391,16 +186,18 @@ * This component renders an anchor tag and is the primary way the user will | ||
* | ||
* @see https://remix.run/api/remix#link | ||
* @see https://remix.run/components/link | ||
*/ | ||
let Link = /*#__PURE__*/React__namespace.forwardRef(({ | ||
to, | ||
prefetch = "none", | ||
discover = "render", | ||
...props | ||
}, forwardedRef) => { | ||
let isAbsolute = typeof to === "string" && ABSOLUTE_URL_REGEX.test(to); | ||
let href = reactRouterDom.useHref(to); | ||
let [shouldPrefetch, prefetchHandlers] = usePrefetchBehavior(prefetch, props); | ||
return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(reactRouterDom.Link, _rollupPluginBabelHelpers["extends"]({ | ||
ref: forwardedRef, | ||
to: to | ||
}, props, prefetchHandlers)), shouldPrefetch ? /*#__PURE__*/React__namespace.createElement(PrefetchPageLinks, { | ||
let [shouldPrefetch, ref, prefetchHandlers] = usePrefetchBehavior(prefetch, props); | ||
return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(reactRouterDom.Link, _rollupPluginBabelHelpers["extends"]({}, props, prefetchHandlers, { | ||
ref: mergeRefs(forwardedRef, ref), | ||
to: to, | ||
"data-discover": getDiscoverAttr(discover, isAbsolute, props.reloadDocument) | ||
})), shouldPrefetch && !isAbsolute ? /*#__PURE__*/React__namespace.createElement(PrefetchPageLinks, { | ||
page: href | ||
@@ -410,6 +207,22 @@ }) : null); | ||
Link.displayName = "Link"; | ||
/** | ||
* This component renders a form tag and is the primary way the user will | ||
* submit information via your website. | ||
* | ||
* @see https://remix.run/components/form | ||
*/ | ||
let Form = /*#__PURE__*/React__namespace.forwardRef(({ | ||
discover = "render", | ||
...props | ||
}, forwardedRef) => { | ||
let isAbsolute = typeof props.action === "string" && ABSOLUTE_URL_REGEX.test(props.action); | ||
return /*#__PURE__*/React__namespace.createElement(reactRouterDom.Form, _rollupPluginBabelHelpers["extends"]({}, props, { | ||
ref: forwardedRef, | ||
"data-discover": getDiscoverAttr(discover, isAbsolute, props.reloadDocument) | ||
})); | ||
}); | ||
Form.displayName = "Form"; | ||
function composeEventHandlers(theirHandler, ourHandler) { | ||
return event => { | ||
theirHandler && theirHandler(event); | ||
if (!event.defaultPrevented) { | ||
@@ -420,56 +233,53 @@ ourHandler(event); | ||
} | ||
// Return the matches actively being displayed: | ||
// - In SPA Mode we only SSR/hydrate the root match, and include all matches | ||
// after hydration. This lets the router handle initial match loads via lazy(). | ||
// - When an error boundary is rendered, we slice off matches up to the | ||
// boundary for <Links>/<Meta> | ||
function getActiveMatches(matches, errors, isSpaMode) { | ||
if (isSpaMode && !isHydrated) { | ||
return [matches[0]]; | ||
} | ||
if (errors) { | ||
let errorIdx = matches.findIndex(m => errors[m.route.id] !== undefined); | ||
return matches.slice(0, errorIdx + 1); | ||
} | ||
return matches; | ||
} | ||
/** | ||
* Renders the `<link>` tags for the current routes. | ||
* | ||
* @see https://remix.run/api/remix#meta-links-scripts | ||
* @see https://remix.run/components/links | ||
*/ | ||
function Links() { | ||
let { | ||
matches, | ||
isSpaMode, | ||
manifest, | ||
routeModules, | ||
manifest | ||
} = useRemixEntryContext(); | ||
let links$1 = React__namespace.useMemo(() => links.getLinksForMatches(matches, routeModules, manifest), [matches, routeModules, manifest]); | ||
return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, links$1.map(link => { | ||
if (links.isPageLinkDescriptor(link)) { | ||
return /*#__PURE__*/React__namespace.createElement(PrefetchPageLinks, _rollupPluginBabelHelpers["extends"]({ | ||
key: link.page | ||
}, link)); | ||
criticalCss | ||
} = useRemixContext(); | ||
let { | ||
errors, | ||
matches: routerMatches | ||
} = useDataRouterStateContext(); | ||
let matches = getActiveMatches(routerMatches, errors, isSpaMode); | ||
let keyedLinks = React__namespace.useMemo(() => links.getKeyedLinksForMatches(matches, routeModules, manifest), [matches, routeModules, manifest]); | ||
return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, criticalCss ? /*#__PURE__*/React__namespace.createElement("style", { | ||
dangerouslySetInnerHTML: { | ||
__html: criticalCss | ||
} | ||
}) : null, keyedLinks.map(({ | ||
key, | ||
link | ||
}) => links.isPageLinkDescriptor(link) ? /*#__PURE__*/React__namespace.createElement(PrefetchPageLinks, _rollupPluginBabelHelpers["extends"]({ | ||
key: key | ||
}, link)) : /*#__PURE__*/React__namespace.createElement("link", _rollupPluginBabelHelpers["extends"]({ | ||
key: key | ||
}, link)))); | ||
} | ||
let imageSrcSet = null; // In React 17, <link imageSrcSet> and <link imageSizes> will warn | ||
// because the DOM attributes aren't recognized, so users need to pass | ||
// them in all lowercase to forward the attributes to the node without a | ||
// warning. Normalize so that either property can be used in Remix. | ||
if ("useId" in React__namespace) { | ||
if (link.imagesrcset) { | ||
link.imageSrcSet = imageSrcSet = link.imagesrcset; | ||
delete link.imagesrcset; | ||
} | ||
if (link.imagesizes) { | ||
link.imageSizes = link.imagesizes; | ||
delete link.imagesizes; | ||
} | ||
} else { | ||
if (link.imageSrcSet) { | ||
link.imagesrcset = imageSrcSet = link.imageSrcSet; | ||
delete link.imageSrcSet; | ||
} | ||
if (link.imageSizes) { | ||
link.imagesizes = link.imageSizes; | ||
delete link.imageSizes; | ||
} | ||
} | ||
return /*#__PURE__*/React__namespace.createElement("link", _rollupPluginBabelHelpers["extends"]({ | ||
key: link.rel + (link.href || "") + (imageSrcSet || "") | ||
}, link)); | ||
})); | ||
} | ||
/** | ||
* This component renders all of the `<link rel="prefetch">` and | ||
* This component renders all the `<link rel="prefetch">` and | ||
* `<link rel="modulepreload"/>` tags for all the assets (data, modules, css) of | ||
@@ -480,5 +290,4 @@ * a given page. | ||
* @param props.page | ||
* @see https://remix.run/api/remix#prefetchpagelinks- | ||
* @see https://remix.run/components/prefetch-page-links | ||
*/ | ||
function PrefetchPageLinks({ | ||
@@ -489,6 +298,5 @@ page, | ||
let { | ||
clientRoutes | ||
} = useRemixEntryContext(); | ||
let matches = React__namespace.useMemo(() => routeMatching.matchClientRoutes(clientRoutes, page), [clientRoutes, page]); | ||
router | ||
} = useDataRouterContext(); | ||
let matches = React__namespace.useMemo(() => reactRouterDom.matchRoutes(router.routes, page, router.basename), [router.routes, page, router.basename]); | ||
if (!matches) { | ||
@@ -498,3 +306,2 @@ console.warn(`Tried to prefetch ${page} but no routes matched.`); | ||
} | ||
return /*#__PURE__*/React__namespace.createElement(PrefetchPageLinksImpl, _rollupPluginBabelHelpers["extends"]({ | ||
@@ -505,12 +312,14 @@ page: page, | ||
} | ||
function usePrefetchedStylesheets(matches) { | ||
function useKeyedPrefetchLinks(matches) { | ||
let { | ||
manifest, | ||
routeModules | ||
} = useRemixEntryContext(); | ||
let [styleLinks, setStyleLinks] = React__namespace.useState([]); | ||
} = useRemixContext(); | ||
let [keyedPrefetchLinks, setKeyedPrefetchLinks] = React__namespace.useState([]); | ||
React__namespace.useEffect(() => { | ||
let interrupted = false; | ||
links.getStylesheetPrefetchLinks(matches, routeModules).then(links => { | ||
if (!interrupted) setStyleLinks(links); | ||
void links.getKeyedPrefetchLinks(matches, manifest, routeModules).then(links => { | ||
if (!interrupted) { | ||
setKeyedPrefetchLinks(links); | ||
} | ||
}); | ||
@@ -520,6 +329,5 @@ return () => { | ||
}; | ||
}, [matches, routeModules]); | ||
return styleLinks; | ||
}, [matches, manifest, routeModules]); | ||
return keyedPrefetchLinks; | ||
} | ||
function PrefetchPageLinksImpl({ | ||
@@ -532,12 +340,56 @@ page, | ||
let { | ||
matches, | ||
manifest | ||
} = useRemixEntryContext(); | ||
let newMatchesForData = React__namespace.useMemo(() => links.getNewMatchesForLinks(page, nextMatches, matches, location, "data"), [page, nextMatches, matches, location]); | ||
let newMatchesForAssets = React__namespace.useMemo(() => links.getNewMatchesForLinks(page, nextMatches, matches, location, "assets"), [page, nextMatches, matches, location]); | ||
let dataHrefs = React__namespace.useMemo(() => links.getDataLinkHrefs(page, newMatchesForData, manifest), [newMatchesForData, page, manifest]); | ||
let moduleHrefs = React__namespace.useMemo(() => links.getModuleLinkHrefs(newMatchesForAssets, manifest), [newMatchesForAssets, manifest]); // needs to be a hook with async behavior because we need the modules, not | ||
future, | ||
manifest, | ||
routeModules | ||
} = useRemixContext(); | ||
let { | ||
loaderData, | ||
matches | ||
} = useDataRouterStateContext(); | ||
let newMatchesForData = React__namespace.useMemo(() => links.getNewMatchesForLinks(page, nextMatches, matches, manifest, location, "data"), [page, nextMatches, matches, manifest, location]); | ||
let dataHrefs = React__namespace.useMemo(() => { | ||
if (!future.unstable_singleFetch) { | ||
return links.getDataLinkHrefs(page, newMatchesForData, manifest); | ||
} | ||
if (page === location.pathname + location.search + location.hash) { | ||
// Because we opt-into revalidation, don't compute this for the current page | ||
// since it would always trigger a prefetch of the existing loaders | ||
return []; | ||
} | ||
// Single-fetch is harder :) | ||
// This parallels the logic in the single fetch data strategy | ||
let routesParams = new Set(); | ||
let foundOptOutRoute = false; | ||
nextMatches.forEach(m => { | ||
var _routeModules$m$route; | ||
if (!manifest.routes[m.route.id].hasLoader) { | ||
return; | ||
} | ||
if (!newMatchesForData.some(m2 => m2.route.id === m.route.id) && m.route.id in loaderData && (_routeModules$m$route = routeModules[m.route.id]) !== null && _routeModules$m$route !== void 0 && _routeModules$m$route.shouldRevalidate) { | ||
foundOptOutRoute = true; | ||
} else if (manifest.routes[m.route.id].hasClientLoader) { | ||
foundOptOutRoute = true; | ||
} else { | ||
routesParams.add(m.route.id); | ||
} | ||
}); | ||
if (routesParams.size === 0) { | ||
return []; | ||
} | ||
let url = singleFetch.singleFetchUrl(page); | ||
// When one or more routes have opted out, we add a _routes param to | ||
// limit the loaders to those that have a server loader and did not | ||
// opt out | ||
if (foundOptOutRoute && routesParams.size > 0) { | ||
url.searchParams.set("_routes", nextMatches.filter(m => routesParams.has(m.route.id)).map(m => m.route.id).join(",")); | ||
} | ||
return [url.pathname + url.search]; | ||
}, [future.unstable_singleFetch, loaderData, location, manifest, newMatchesForData, nextMatches, page, routeModules]); | ||
let newMatchesForAssets = React__namespace.useMemo(() => links.getNewMatchesForLinks(page, nextMatches, matches, manifest, location, "assets"), [page, nextMatches, matches, manifest, location]); | ||
let moduleHrefs = React__namespace.useMemo(() => links.getModuleLinkHrefs(newMatchesForAssets, manifest), [newMatchesForAssets, manifest]); | ||
// needs to be a hook with async behavior because we need the modules, not | ||
// just the manifest like the other links in here. | ||
let styleLinks = usePrefetchedStylesheets(newMatchesForAssets); | ||
let keyedPrefetchLinks = useKeyedPrefetchLinks(newMatchesForAssets); | ||
return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, dataHrefs.map(href => /*#__PURE__*/React__namespace.createElement("link", _rollupPluginBabelHelpers["extends"]({ | ||
@@ -552,3 +404,6 @@ key: href, | ||
href: href | ||
}, linkProps))), styleLinks.map(link => | ||
}, linkProps))), keyedPrefetchLinks.map(({ | ||
key, | ||
link | ||
}) => | ||
/*#__PURE__*/ | ||
@@ -558,85 +413,129 @@ // these don't spread `linkProps` because they are full link descriptors | ||
React__namespace.createElement("link", _rollupPluginBabelHelpers["extends"]({ | ||
key: link.href | ||
key: key | ||
}, link)))); | ||
} | ||
/** | ||
* Renders the `<title>` and `<meta>` tags for the current routes. | ||
* Renders HTML tags related to metadata for the current route. | ||
* | ||
* @see https://remix.run/api/remix#meta-links-scripts | ||
* @see https://remix.run/components/meta | ||
*/ | ||
function Meta() { | ||
let { | ||
matches, | ||
routeData, | ||
isSpaMode, | ||
routeModules | ||
} = useRemixEntryContext(); | ||
} = useRemixContext(); | ||
let { | ||
errors, | ||
matches: routerMatches, | ||
loaderData | ||
} = useDataRouterStateContext(); | ||
let location = reactRouterDom.useLocation(); | ||
let meta = {}; | ||
let parentsData = {}; | ||
for (let match of matches) { | ||
let routeId = match.route.id; | ||
let data = routeData[routeId]; | ||
let params = match.params; | ||
let _matches = getActiveMatches(routerMatches, errors, isSpaMode); | ||
let error = null; | ||
if (errors) { | ||
error = errors[_matches[_matches.length - 1].route.id]; | ||
} | ||
let meta = []; | ||
let leafMeta = null; | ||
let matches = []; | ||
for (let i = 0; i < _matches.length; i++) { | ||
let _match = _matches[i]; | ||
let routeId = _match.route.id; | ||
let data = loaderData[routeId]; | ||
let params = _match.params; | ||
let routeModule = routeModules[routeId]; | ||
if (routeModule.meta) { | ||
let routeMeta = typeof routeModule.meta === "function" ? routeModule.meta({ | ||
let routeMeta = []; | ||
let match = { | ||
id: routeId, | ||
data, | ||
meta: [], | ||
params: _match.params, | ||
pathname: _match.pathname, | ||
handle: _match.route.handle, | ||
error | ||
}; | ||
matches[i] = match; | ||
if (routeModule !== null && routeModule !== void 0 && routeModule.meta) { | ||
routeMeta = typeof routeModule.meta === "function" ? routeModule.meta({ | ||
data, | ||
parentsData, | ||
params, | ||
location | ||
}) : routeModule.meta; | ||
Object.assign(meta, routeMeta); | ||
location, | ||
matches, | ||
error | ||
}) : Array.isArray(routeModule.meta) ? [...routeModule.meta] : routeModule.meta; | ||
} else if (leafMeta) { | ||
// We only assign the route's meta to the nearest leaf if there is no meta | ||
// export in the route. The meta function may return a falsy value which | ||
// is effectively the same as an empty array. | ||
routeMeta = [...leafMeta]; | ||
} | ||
parentsData[routeId] = data; | ||
routeMeta = routeMeta || []; | ||
if (!Array.isArray(routeMeta)) { | ||
throw new Error("The route at " + _match.route.path + " returns an invalid value. All route meta functions must " + "return an array of meta objects." + "\n\nTo reference the meta function API, see https://remix.run/route/meta"); | ||
} | ||
match.meta = routeMeta; | ||
matches[i] = match; | ||
meta = [...routeMeta]; | ||
leafMeta = meta; | ||
} | ||
return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, Object.entries(meta).map(([name, value]) => { | ||
if (!value) { | ||
return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, meta.flat().map(metaProps => { | ||
if (!metaProps) { | ||
return null; | ||
} | ||
if (["charset", "charSet"].includes(name)) { | ||
return /*#__PURE__*/React__namespace.createElement("meta", { | ||
key: "charset", | ||
charSet: value | ||
}); | ||
if ("tagName" in metaProps) { | ||
let { | ||
tagName, | ||
...rest | ||
} = metaProps; | ||
if (!isValidMetaTag(tagName)) { | ||
console.warn(`A meta object uses an invalid tagName: ${tagName}. Expected either 'link' or 'meta'`); | ||
return null; | ||
} | ||
let Comp = tagName; | ||
return /*#__PURE__*/React__namespace.createElement(Comp, _rollupPluginBabelHelpers["extends"]({ | ||
key: JSON.stringify(rest) | ||
}, rest)); | ||
} | ||
if (name === "title") { | ||
if ("title" in metaProps) { | ||
return /*#__PURE__*/React__namespace.createElement("title", { | ||
key: "title" | ||
}, String(value)); | ||
} // Open Graph tags use the `property` attribute, while other meta tags | ||
// use `name`. See https://ogp.me/ | ||
let isOpenGraphTag = name.startsWith("og:"); | ||
return [value].flat().map(content => { | ||
if (isOpenGraphTag) { | ||
return /*#__PURE__*/React__namespace.createElement("meta", { | ||
property: name, | ||
content: content, | ||
key: name + content | ||
}, String(metaProps.title)); | ||
} | ||
if ("charset" in metaProps) { | ||
metaProps.charSet ??= metaProps.charset; | ||
delete metaProps.charset; | ||
} | ||
if ("charSet" in metaProps && metaProps.charSet != null) { | ||
return typeof metaProps.charSet === "string" ? /*#__PURE__*/React__namespace.createElement("meta", { | ||
key: "charSet", | ||
charSet: metaProps.charSet | ||
}) : null; | ||
} | ||
if ("script:ld+json" in metaProps) { | ||
try { | ||
let json = JSON.stringify(metaProps["script:ld+json"]); | ||
return /*#__PURE__*/React__namespace.createElement("script", { | ||
key: `script:ld+json:${json}`, | ||
type: "application/ld+json", | ||
dangerouslySetInnerHTML: { | ||
__html: json | ||
} | ||
}); | ||
} catch (err) { | ||
return null; | ||
} | ||
if (typeof content === "string") { | ||
return /*#__PURE__*/React__namespace.createElement("meta", { | ||
name: name, | ||
content: content, | ||
key: name + content | ||
}); | ||
} | ||
return /*#__PURE__*/React__namespace.createElement("meta", _rollupPluginBabelHelpers["extends"]({ | ||
key: name + JSON.stringify(content) | ||
}, content)); | ||
}); | ||
} | ||
return /*#__PURE__*/React__namespace.createElement("meta", _rollupPluginBabelHelpers["extends"]({ | ||
key: JSON.stringify(metaProps) | ||
}, metaProps)); | ||
})); | ||
} | ||
function isValidMetaTag(tagName) { | ||
return typeof tagName === "string" && /^(meta|link)$/.test(tagName); | ||
} | ||
function Await(props) { | ||
return /*#__PURE__*/React__namespace.createElement(reactRouterDom.Await, props); | ||
} | ||
/** | ||
@@ -646,5 +545,3 @@ * Tracks whether Remix has finished hydrating or not, so scripts can be skipped | ||
*/ | ||
let isHydrated = false; | ||
/** | ||
@@ -658,3 +555,3 @@ * Renders the `<script>` tags needed for the initial render. Bundles for | ||
* | ||
* @see https://remix.run/api/remix#meta-links-scripts | ||
* @see https://remix.run/components/scripts | ||
*/ | ||
@@ -664,14 +561,117 @@ function Scripts(props) { | ||
manifest, | ||
matches, | ||
pendingLocation, | ||
clientRoutes, | ||
serverHandoffString | ||
} = useRemixEntryContext(); | ||
serverHandoffString, | ||
abortDelay, | ||
serializeError, | ||
isSpaMode, | ||
future, | ||
renderMeta | ||
} = useRemixContext(); | ||
let { | ||
router, | ||
static: isStatic, | ||
staticContext | ||
} = useDataRouterContext(); | ||
let { | ||
matches: routerMatches | ||
} = useDataRouterStateContext(); | ||
let enableFogOfWar = fogOfWar.isFogOfWarEnabled(future, isSpaMode); | ||
// Let <RemixServer> know that we hydrated and we should render the single | ||
// fetch streaming scripts | ||
if (renderMeta) { | ||
renderMeta.didRenderScripts = true; | ||
} | ||
let matches = getActiveMatches(routerMatches, null, isSpaMode); | ||
React__namespace.useEffect(() => { | ||
isHydrated = true; | ||
}, []); | ||
let serializePreResolvedErrorImp = (key, error) => { | ||
let toSerialize; | ||
if (serializeError && error instanceof Error) { | ||
toSerialize = serializeError(error); | ||
} else { | ||
toSerialize = error; | ||
} | ||
return `${JSON.stringify(key)}:__remixContext.p(!1, ${markup.escapeHtml(JSON.stringify(toSerialize))})`; | ||
}; | ||
let serializePreresolvedDataImp = (routeId, key, data) => { | ||
let serializedData; | ||
try { | ||
serializedData = JSON.stringify(data); | ||
} catch (error) { | ||
return serializePreResolvedErrorImp(key, error); | ||
} | ||
return `${JSON.stringify(key)}:__remixContext.p(${markup.escapeHtml(serializedData)})`; | ||
}; | ||
let serializeErrorImp = (routeId, key, error) => { | ||
let toSerialize; | ||
if (serializeError && error instanceof Error) { | ||
toSerialize = serializeError(error); | ||
} else { | ||
toSerialize = error; | ||
} | ||
return `__remixContext.r(${JSON.stringify(routeId)}, ${JSON.stringify(key)}, !1, ${markup.escapeHtml(JSON.stringify(toSerialize))})`; | ||
}; | ||
let serializeDataImp = (routeId, key, data) => { | ||
let serializedData; | ||
try { | ||
serializedData = JSON.stringify(data); | ||
} catch (error) { | ||
return serializeErrorImp(routeId, key, error); | ||
} | ||
return `__remixContext.r(${JSON.stringify(routeId)}, ${JSON.stringify(key)}, ${markup.escapeHtml(serializedData)})`; | ||
}; | ||
let deferredScripts = []; | ||
let initialScripts = React__namespace.useMemo(() => { | ||
let contextScript = serverHandoffString ? `window.__remixContext = ${serverHandoffString};` : ""; | ||
let routeModulesScript = `${matches.map((match, index) => `import ${JSON.stringify(manifest.url)}; | ||
import * as route${index} from ${JSON.stringify(manifest.routes[match.route.id].module)};`).join("\n")} | ||
var _manifest$hmr; | ||
let streamScript = future.unstable_singleFetch ? | ||
// prettier-ignore | ||
"window.__remixContext.stream = new ReadableStream({" + "start(controller){" + "window.__remixContext.streamController = controller;" + "}" + "}).pipeThrough(new TextEncoderStream());" : ""; | ||
let contextScript = staticContext ? `window.__remixContext = ${serverHandoffString};${streamScript}` : " "; | ||
// When single fetch is enabled, deferred is handled by turbo-stream | ||
let activeDeferreds = future.unstable_singleFetch ? undefined : staticContext === null || staticContext === void 0 ? void 0 : staticContext.activeDeferreds; | ||
// This sets up the __remixContext with utility functions used by the | ||
// deferred scripts. | ||
// - __remixContext.p is a function that takes a resolved value or error and returns a promise. | ||
// This is used for transmitting pre-resolved promises from the server to the client. | ||
// - __remixContext.n is a function that takes a routeID and key to returns a promise for later | ||
// resolution by the subsequently streamed chunks. | ||
// - __remixContext.r is a function that takes a routeID, key and value or error and resolves | ||
// the promise created by __remixContext.n. | ||
// - __remixContext.t is a map or routeId to keys to an object containing `e` and `r` methods | ||
// to resolve or reject the promise created by __remixContext.n. | ||
// - __remixContext.a is the active number of deferred scripts that should be rendered to match | ||
// the SSR tree for hydration on the client. | ||
contextScript += !activeDeferreds ? "" : ["__remixContext.p = function(v,e,p,x) {", " if (typeof e !== 'undefined') {", process.env.NODE_ENV === "development" ? " x=new Error(e.message);\n x.stack=e.stack;" : ' x=new Error("Unexpected Server Error");\n x.stack=undefined;', " p=Promise.reject(x);", " } else {", " p=Promise.resolve(v);", " }", " return p;", "};", "__remixContext.n = function(i,k) {", " __remixContext.t = __remixContext.t || {};", " __remixContext.t[i] = __remixContext.t[i] || {};", " let p = new Promise((r, e) => {__remixContext.t[i][k] = {r:(v)=>{r(v);},e:(v)=>{e(v);}};});", typeof abortDelay === "number" ? `setTimeout(() => {if(typeof p._error !== "undefined" || typeof p._data !== "undefined"){return;} __remixContext.t[i][k].e(new Error("Server timeout."))}, ${abortDelay});` : "", " return p;", "};", "__remixContext.r = function(i,k,v,e,p,x) {", " p = __remixContext.t[i][k];", " if (typeof e !== 'undefined') {", process.env.NODE_ENV === "development" ? " x=new Error(e.message);\n x.stack=e.stack;" : ' x=new Error("Unexpected Server Error");\n x.stack=undefined;', " p.e(x);", " } else {", " p.r(v);", " }", "};"].join("\n") + Object.entries(activeDeferreds).map(([routeId, deferredData]) => { | ||
let pendingKeys = new Set(deferredData.pendingKeys); | ||
let promiseKeyValues = deferredData.deferredKeys.map(key => { | ||
if (pendingKeys.has(key)) { | ||
deferredScripts.push( /*#__PURE__*/React__namespace.createElement(DeferredHydrationScript, { | ||
key: `${routeId} | ${key}`, | ||
deferredData: deferredData, | ||
routeId: routeId, | ||
dataKey: key, | ||
scriptProps: props, | ||
serializeData: serializeDataImp, | ||
serializeError: serializeErrorImp | ||
})); | ||
return `${JSON.stringify(key)}:__remixContext.n(${JSON.stringify(routeId)}, ${JSON.stringify(key)})`; | ||
} else { | ||
let trackedPromise = deferredData.data[key]; | ||
if (typeof trackedPromise._error !== "undefined") { | ||
return serializePreResolvedErrorImp(key, trackedPromise._error); | ||
} else { | ||
return serializePreresolvedDataImp(routeId, key, trackedPromise._data); | ||
} | ||
} | ||
}).join(",\n"); | ||
return `Object.assign(__remixContext.state.loaderData[${JSON.stringify(routeId)}], {${promiseKeyValues}});`; | ||
}).join("\n") + (deferredScripts.length > 0 ? `__remixContext.a=${deferredScripts.length};` : ""); | ||
let routeModulesScript = !isStatic ? " " : `${(_manifest$hmr = manifest.hmr) !== null && _manifest$hmr !== void 0 && _manifest$hmr.runtime ? `import ${JSON.stringify(manifest.hmr.runtime)};` : ""}${enableFogOfWar ? "" : `import ${JSON.stringify(manifest.url)}`}; | ||
${matches.map((match, index) => `import * as route${index} from ${JSON.stringify(manifest.routes[match.route.id].module)};`).join("\n")} | ||
${enableFogOfWar ? | ||
// Inline a minimal manifest with the SSR matches | ||
`window.__remixManifest = ${JSON.stringify(fogOfWar.getPartialManifest(manifest, router), null, 2)};` : ""} | ||
window.__remixRouteModules = {${matches.map((match, index) => `${JSON.stringify(match.route.id)}:route${index}`).join(",")}}; | ||
@@ -685,28 +685,33 @@ | ||
})), /*#__PURE__*/React__namespace.createElement("script", _rollupPluginBabelHelpers["extends"]({}, props, { | ||
suppressHydrationWarning: true, | ||
dangerouslySetInnerHTML: markup.createHtml(routeModulesScript), | ||
type: "module", | ||
async: true | ||
}))); // disabled deps array because we are purposefully only rendering this once | ||
}))); | ||
// disabled deps array because we are purposefully only rendering this once | ||
// for hydration, after that we want to just continue rendering the initial | ||
// scripts as they were when the page first loaded | ||
// eslint-disable-next-line | ||
}, []); // avoid waterfall when importing the next route module | ||
let nextMatches = React__namespace.useMemo(() => { | ||
if (pendingLocation) { | ||
// FIXME: can probably use transitionManager `nextMatches` | ||
let matches = routeMatching.matchClientRoutes(clientRoutes, pendingLocation); | ||
invariant(matches, `No routes match path "${pendingLocation.pathname}"`); | ||
return matches; | ||
}, []); | ||
if (!isStatic && typeof __remixContext === "object" && __remixContext.a) { | ||
for (let i = 0; i < __remixContext.a; i++) { | ||
deferredScripts.push( /*#__PURE__*/React__namespace.createElement(DeferredHydrationScript, { | ||
key: i, | ||
scriptProps: props, | ||
serializeData: serializeDataImp, | ||
serializeError: serializeErrorImp | ||
})); | ||
} | ||
return []; | ||
}, [pendingLocation, clientRoutes]); | ||
let routePreloads = matches.concat(nextMatches).map(match => { | ||
} | ||
let routePreloads = matches.map(match => { | ||
let route = manifest.routes[match.route.id]; | ||
return (route.imports || []).concat([route.module]); | ||
}).flat(1); | ||
let preloads = manifest.entry.imports.concat(routePreloads); | ||
return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement("link", { | ||
let preloads = isHydrated ? [] : manifest.entry.imports.concat(routePreloads); | ||
return isHydrated ? null : /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, !enableFogOfWar ? /*#__PURE__*/React__namespace.createElement("link", { | ||
rel: "modulepreload", | ||
href: manifest.url, | ||
crossOrigin: props.crossOrigin | ||
}) : null, /*#__PURE__*/React__namespace.createElement("link", { | ||
rel: "modulepreload", | ||
href: manifest.entry.module, | ||
@@ -719,362 +724,106 @@ crossOrigin: props.crossOrigin | ||
crossOrigin: props.crossOrigin | ||
})), isHydrated ? null : initialScripts); | ||
})), initialScripts, deferredScripts); | ||
} | ||
function dedupe(array) { | ||
return [...new Set(array)]; | ||
} | ||
/** | ||
* A Remix-aware `<form>`. It behaves like a normal form except that the | ||
* interaction with the server is with `fetch` instead of new document | ||
* requests, allowing components to add nicer UX to the page as the form is | ||
* submitted and returns with data. | ||
* | ||
* @see https://remix.run/api/remix#form | ||
*/ | ||
let Form = /*#__PURE__*/React__namespace.forwardRef((props, ref) => { | ||
return /*#__PURE__*/React__namespace.createElement(FormImpl, _rollupPluginBabelHelpers["extends"]({}, props, { | ||
ref: ref | ||
})); | ||
}); | ||
Form.displayName = "Form"; | ||
let FormImpl = /*#__PURE__*/React__namespace.forwardRef(({ | ||
reloadDocument = false, | ||
replace = false, | ||
method = "get", | ||
action, | ||
encType = "application/x-www-form-urlencoded", | ||
fetchKey, | ||
onSubmit, | ||
...props | ||
}, forwardedRef) => { | ||
let submit = useSubmitImpl(fetchKey); | ||
let formMethod = method.toLowerCase() === "get" ? "get" : "post"; | ||
let formAction = useFormAction(action); | ||
return /*#__PURE__*/React__namespace.createElement("form", _rollupPluginBabelHelpers["extends"]({ | ||
ref: forwardedRef, | ||
method: formMethod, | ||
action: formAction, | ||
encType: encType, | ||
onSubmit: reloadDocument ? undefined : event => { | ||
onSubmit && onSubmit(event); | ||
if (event.defaultPrevented) return; | ||
event.preventDefault(); | ||
let submitter = event.nativeEvent.submitter; | ||
submit(submitter || event.currentTarget, { | ||
method, | ||
replace | ||
}); | ||
} | ||
}, props)); | ||
}); | ||
FormImpl.displayName = "FormImpl"; | ||
/** | ||
* Resolves a `<form action>` path relative to the current route. | ||
* | ||
* @see https://remix.run/api/remix#useformaction | ||
*/ | ||
function useFormAction(action, // TODO: Remove method param in v2 as it's no longer needed and is a breaking change | ||
method = "get") { | ||
let { | ||
id | ||
} = useRemixRouteContext(); | ||
let resolvedPath = reactRouterDom.useResolvedPath(action ?? "."); // Previously we set the default action to ".". The problem with this is that | ||
// `useResolvedPath(".")` excludes search params and the hash of the resolved | ||
// URL. This is the intended behavior of when "." is specifically provided as | ||
// the form action, but inconsistent w/ browsers when the action is omitted. | ||
// https://github.com/remix-run/remix/issues/927 | ||
let location = reactRouterDom.useLocation(); | ||
let { | ||
search, | ||
hash | ||
} = resolvedPath; | ||
let isIndexRoute = id.endsWith("/index"); | ||
if (action == null) { | ||
search = location.search; | ||
hash = location.hash; // When grabbing search params from the URL, remove the automatically | ||
// inserted ?index param so we match the useResolvedPath search behavior | ||
// which would not include ?index | ||
if (isIndexRoute) { | ||
let params = new URLSearchParams(search); | ||
params.delete("index"); | ||
search = params.toString() ? `?${params.toString()}` : ""; | ||
} | ||
function DeferredHydrationScript({ | ||
dataKey, | ||
deferredData, | ||
routeId, | ||
scriptProps, | ||
serializeData, | ||
serializeError | ||
}) { | ||
if (typeof document === "undefined" && deferredData && dataKey && routeId) { | ||
invariant(deferredData.pendingKeys.includes(dataKey), `Deferred data for route ${routeId} with key ${dataKey} was not pending but tried to render a script for it.`); | ||
} | ||
if ((action == null || action === ".") && isIndexRoute) { | ||
search = search ? search.replace(/^\?/, "?index&") : "?index"; | ||
} | ||
return history.createPath({ | ||
pathname: resolvedPath.pathname, | ||
search, | ||
hash | ||
}); | ||
} | ||
/** | ||
* Returns a function that may be used to programmatically submit a form (or | ||
* some arbitrary data) to the server. | ||
* | ||
* @see https://remix.run/api/remix#usesubmit | ||
*/ | ||
function useSubmit() { | ||
return useSubmitImpl(); | ||
} | ||
let defaultMethod = "get"; | ||
let defaultEncType = "application/x-www-form-urlencoded"; | ||
function useSubmitImpl(key) { | ||
let navigate = reactRouterDom.useNavigate(); | ||
let defaultAction = useFormAction(); | ||
let { | ||
transitionManager | ||
} = useRemixEntryContext(); | ||
return React__namespace.useCallback((target, options = {}) => { | ||
let method; | ||
let action; | ||
let encType; | ||
let formData; | ||
if (isFormElement(target)) { | ||
let submissionTrigger = options.submissionTrigger; | ||
method = options.method || target.getAttribute("method") || defaultMethod; | ||
action = options.action || target.getAttribute("action") || defaultAction; | ||
encType = options.encType || target.getAttribute("enctype") || defaultEncType; | ||
formData = new FormData(target); | ||
if (submissionTrigger && submissionTrigger.name) { | ||
formData.append(submissionTrigger.name, submissionTrigger.value); | ||
return /*#__PURE__*/React__namespace.createElement(React__namespace.Suspense, { | ||
fallback: | ||
// This makes absolutely no sense. The server renders null as a fallback, | ||
// but when hydrating, we need to render a script tag to avoid a hydration issue. | ||
// To reproduce a hydration mismatch, just render null as a fallback. | ||
typeof document === "undefined" && deferredData && dataKey && routeId ? null : /*#__PURE__*/React__namespace.createElement("script", _rollupPluginBabelHelpers["extends"]({}, scriptProps, { | ||
async: true, | ||
suppressHydrationWarning: true, | ||
dangerouslySetInnerHTML: { | ||
__html: " " | ||
} | ||
} else if (isButtonElement(target) || isInputElement(target) && (target.type === "submit" || target.type === "image")) { | ||
let form = target.form; | ||
if (form == null) { | ||
throw new Error(`Cannot submit a <button> without a <form>`); | ||
} // <button>/<input type="submit"> may override attributes of <form> | ||
method = options.method || target.getAttribute("formmethod") || form.getAttribute("method") || defaultMethod; | ||
action = options.action || target.getAttribute("formaction") || form.getAttribute("action") || defaultAction; | ||
encType = options.encType || target.getAttribute("formenctype") || form.getAttribute("enctype") || defaultEncType; | ||
formData = new FormData(form); // Include name + value from a <button> | ||
if (target.name) { | ||
formData.append(target.name, target.value); | ||
} | ||
} else { | ||
if (isHtmlElement(target)) { | ||
throw new Error(`Cannot submit element that is not <form>, <button>, or ` + `<input type="submit|image">`); | ||
} | ||
method = options.method || "get"; | ||
action = options.action || defaultAction; | ||
encType = options.encType || "application/x-www-form-urlencoded"; | ||
if (target instanceof FormData) { | ||
formData = target; | ||
} else { | ||
formData = new FormData(); | ||
if (target instanceof URLSearchParams) { | ||
for (let [name, value] of target) { | ||
formData.append(name, value); | ||
} | ||
} else if (target != null) { | ||
for (let name of Object.keys(target)) { | ||
formData.append(name, target[name]); | ||
} | ||
})) | ||
}, typeof document === "undefined" && deferredData && dataKey && routeId ? /*#__PURE__*/React__namespace.createElement(Await, { | ||
resolve: deferredData.data[dataKey], | ||
errorElement: /*#__PURE__*/React__namespace.createElement(ErrorDeferredHydrationScript, { | ||
dataKey: dataKey, | ||
routeId: routeId, | ||
scriptProps: scriptProps, | ||
serializeError: serializeError | ||
}), | ||
children: data => { | ||
return /*#__PURE__*/React__namespace.createElement("script", _rollupPluginBabelHelpers["extends"]({}, scriptProps, { | ||
async: true, | ||
suppressHydrationWarning: true, | ||
dangerouslySetInnerHTML: { | ||
__html: serializeData(routeId, dataKey, data) | ||
} | ||
} | ||
})); | ||
} | ||
if (typeof document === "undefined") { | ||
throw new Error("You are calling submit during the server render. " + "Try calling submit within a `useEffect` or callback instead."); | ||
}) : /*#__PURE__*/React__namespace.createElement("script", _rollupPluginBabelHelpers["extends"]({}, scriptProps, { | ||
async: true, | ||
suppressHydrationWarning: true, | ||
dangerouslySetInnerHTML: { | ||
__html: " " | ||
} | ||
let { | ||
protocol, | ||
host | ||
} = window.location; | ||
let url = new URL(action, `${protocol}//${host}`); | ||
if (method.toLowerCase() === "get") { | ||
// Start with a fresh set of params and wipe out the old params to | ||
// match default browser behavior | ||
let params = new URLSearchParams(); | ||
let hasParams = false; | ||
for (let [name, value] of formData) { | ||
if (typeof value === "string") { | ||
hasParams = true; | ||
params.append(name, value); | ||
} else { | ||
throw new Error(`Cannot submit binary form data using GET`); | ||
} | ||
} | ||
url.search = hasParams ? `?${params.toString()}` : ""; | ||
}))); | ||
} | ||
function ErrorDeferredHydrationScript({ | ||
dataKey, | ||
routeId, | ||
scriptProps, | ||
serializeError | ||
}) { | ||
let error = reactRouterDom.useAsyncError(); | ||
return /*#__PURE__*/React__namespace.createElement("script", _rollupPluginBabelHelpers["extends"]({}, scriptProps, { | ||
suppressHydrationWarning: true, | ||
dangerouslySetInnerHTML: { | ||
__html: serializeError(routeId, dataKey, error) | ||
} | ||
let submission = { | ||
formData, | ||
action: url.pathname + url.search, | ||
method: method.toUpperCase(), | ||
encType, | ||
key: Math.random().toString(36).substr(2, 8) | ||
}; | ||
if (key) { | ||
transitionManager.send({ | ||
type: "fetcher", | ||
href: submission.action, | ||
submission, | ||
key | ||
}); | ||
} else { | ||
setNextNavigationSubmission(submission); | ||
navigate(url.pathname + url.search, { | ||
replace: options.replace | ||
}); | ||
} | ||
}, [defaultAction, key, navigate, transitionManager]); | ||
})); | ||
} | ||
let nextNavigationSubmission; | ||
function setNextNavigationSubmission(submission) { | ||
nextNavigationSubmission = submission; | ||
function dedupe(array) { | ||
return [...new Set(array)]; | ||
} | ||
function consumeNextNavigationSubmission() { | ||
let submission = nextNavigationSubmission; | ||
nextNavigationSubmission = undefined; | ||
return submission; | ||
} | ||
function isHtmlElement(object) { | ||
return object != null && typeof object.tagName === "string"; | ||
} | ||
function isButtonElement(object) { | ||
return isHtmlElement(object) && object.tagName.toLowerCase() === "button"; | ||
} | ||
function isFormElement(object) { | ||
return isHtmlElement(object) && object.tagName.toLowerCase() === "form"; | ||
} | ||
function isInputElement(object) { | ||
return isHtmlElement(object) && object.tagName.toLowerCase() === "input"; | ||
} | ||
/** | ||
* Setup a callback to be fired on the window's `beforeunload` event. This is | ||
* useful for saving some data to `window.localStorage` just before the page | ||
* refreshes, which automatically happens on the next `<Link>` click when Remix | ||
* detects a new version of the app is available on the server. | ||
* Returns the active route matches, useful for accessing loaderData for | ||
* parent/child routes or the route "handle" property | ||
* | ||
* Note: The `callback` argument should be a function created with | ||
* `React.useCallback()`. | ||
* | ||
* @see https://remix.run/api/remix#usebeforeunload | ||
* @see https://remix.run/hooks/use-matches | ||
*/ | ||
function useBeforeUnload(callback) { | ||
React__namespace.useEffect(() => { | ||
window.addEventListener("beforeunload", callback); | ||
return () => { | ||
window.removeEventListener("beforeunload", callback); | ||
}; | ||
}, [callback]); | ||
function useMatches() { | ||
return reactRouterDom.useMatches(); | ||
} | ||
/** | ||
* Returns the current route matches on the page. This is useful for creating | ||
* layout abstractions with your current routes. | ||
* Returns the JSON parsed data from the current route's `loader`. | ||
* | ||
* @see https://remix.run/api/remix#usematches | ||
* @see https://remix.run/hooks/use-loader-data | ||
*/ | ||
function useMatches() { | ||
let { | ||
matches, | ||
routeData, | ||
routeModules | ||
} = useRemixEntryContext(); | ||
return React__namespace.useMemo(() => matches.map(match => { | ||
var _routeModules$match$r; | ||
function useLoaderData() { | ||
return reactRouterDom.useLoaderData(); | ||
} | ||
let { | ||
pathname, | ||
params | ||
} = match; | ||
return { | ||
id: match.route.id, | ||
pathname, | ||
params, | ||
data: routeData[match.route.id], | ||
// if the module fails to load or an error/response is thrown, the module | ||
// won't be defined. | ||
handle: (_routeModules$match$r = routeModules[match.route.id]) === null || _routeModules$match$r === void 0 ? void 0 : _routeModules$match$r.handle | ||
}; | ||
}), [matches, routeData, routeModules]); | ||
} | ||
/** | ||
* Returns the JSON parsed data from the current route's `loader`. | ||
* Returns the loaderData for the given routeId. | ||
* | ||
* @see https://remix.run/api/remix#useloaderdata | ||
* @see https://remix.run/hooks/use-route-loader-data | ||
*/ | ||
function useRouteLoaderData(routeId) { | ||
return reactRouterDom.useRouteLoaderData(routeId); | ||
} | ||
function useLoaderData() { | ||
return useRemixRouteContext().data; | ||
} | ||
/** | ||
* Returns the JSON parsed data from the current route's `action`. | ||
* | ||
* @see https://remix.run/api/remix#useactiondata | ||
* @see https://remix.run/hooks/use-action-data | ||
*/ | ||
function useActionData() { | ||
let { | ||
id: routeId | ||
} = useRemixRouteContext(); | ||
let { | ||
transitionManager | ||
} = useRemixEntryContext(); | ||
let { | ||
actionData | ||
} = transitionManager.getState(); | ||
return actionData ? actionData[routeId] : undefined; | ||
return reactRouterDom.useActionData(); | ||
} | ||
/** | ||
* Returns everything you need to know about a page transition to build pending | ||
* navigation indicators and optimistic UI on data mutations. | ||
* | ||
* @see https://remix.run/api/remix#usetransition | ||
*/ | ||
function useTransition() { | ||
let { | ||
transitionManager | ||
} = useRemixEntryContext(); | ||
return transitionManager.getState().transition; | ||
} | ||
function createFetcherForm(fetchKey) { | ||
let FetcherForm = /*#__PURE__*/React__namespace.forwardRef((props, ref) => { | ||
// TODO: make ANOTHER form w/o a fetchKey prop | ||
return /*#__PURE__*/React__namespace.createElement(FormImpl, _rollupPluginBabelHelpers["extends"]({}, props, { | ||
ref: ref, | ||
fetchKey: fetchKey | ||
})); | ||
}); | ||
FetcherForm.displayName = "fetcher.Form"; | ||
return FetcherForm; | ||
} | ||
let fetcherId = 0; | ||
/** | ||
@@ -1084,57 +833,25 @@ * Interacts with route loaders and actions without causing a navigation. Great | ||
* | ||
* @see https://remix.run/api/remix#usefetcher | ||
* @see https://remix.run/hooks/use-fetcher | ||
*/ | ||
function useFetcher() { | ||
let { | ||
transitionManager | ||
} = useRemixEntryContext(); | ||
let [key] = React__namespace.useState(() => String(++fetcherId)); | ||
let [Form] = React__namespace.useState(() => createFetcherForm(key)); | ||
let [load] = React__namespace.useState(() => href => { | ||
transitionManager.send({ | ||
type: "fetcher", | ||
href, | ||
key | ||
}); | ||
}); | ||
let submit = useSubmitImpl(key); | ||
let fetcher = transitionManager.getFetcher(key); | ||
let fetcherWithComponents = React__namespace.useMemo(() => ({ | ||
Form, | ||
submit, | ||
load, | ||
...fetcher | ||
}), [fetcher, Form, submit, load]); | ||
React__namespace.useEffect(() => { | ||
// Is this busted when the React team gets real weird and calls effects | ||
// twice on mount? We really just need to garbage collect here when this | ||
// fetcher is no longer around. | ||
return () => transitionManager.deleteFetcher(key); | ||
}, [transitionManager, key]); | ||
return fetcherWithComponents; | ||
function useFetcher(opts = {}) { | ||
return reactRouterDom.useFetcher(opts); | ||
} | ||
/** | ||
* Provides all fetchers currently on the page. Useful for layouts and parent | ||
* routes that need to provide pending/optimistic UI regarding the fetch. | ||
* This component connects your app to the Remix asset server and | ||
* automatically reloads the page when files change in development. | ||
* In production, it renders null, so you can safely render it always in your root route. | ||
* | ||
* @see https://remix.run/api/remix#usefetchers | ||
* @see https://remix.run/docs/components/live-reload | ||
*/ | ||
function useFetchers() { | ||
let { | ||
transitionManager | ||
} = useRemixEntryContext(); | ||
let { | ||
fetchers | ||
} = transitionManager.getState(); | ||
return [...fetchers.values()]; | ||
} // Dead Code Elimination magic for production builds. | ||
const LiveReload = | ||
// Dead Code Elimination magic for production builds. | ||
// This way devs don't have to worry about doing the NODE_ENV check themselves. | ||
// If running an un-bundled server outside of `remix dev` you will still need | ||
// to set the REMIX_DEV_SERVER_WS_PORT manually. | ||
const LiveReload = process.env.NODE_ENV !== "development" ? () => null : function LiveReload({ | ||
port = Number(process.env.REMIX_DEV_SERVER_WS_PORT || 8002), | ||
process.env.NODE_ENV !== "development" ? () => null : function LiveReload({ | ||
origin, | ||
port, | ||
timeoutMs = 1000, | ||
nonce = undefined | ||
}) { | ||
origin ??= process.env.REMIX_DEV_ORIGIN; | ||
let js = String.raw; | ||
@@ -1147,8 +864,15 @@ return /*#__PURE__*/React__namespace.createElement("script", { | ||
function remixLiveReloadConnect(config) { | ||
let protocol = location.protocol === "https:" ? "wss:" : "ws:"; | ||
let host = location.hostname; | ||
let socketPath = protocol + "//" + host + ":" + ${String(port)} + "/socket"; | ||
let LIVE_RELOAD_ORIGIN = ${JSON.stringify(origin)}; | ||
let protocol = | ||
LIVE_RELOAD_ORIGIN ? new URL(LIVE_RELOAD_ORIGIN).protocol.replace(/^http/, "ws") : | ||
location.protocol === "https:" ? "wss:" : "ws:"; // remove in v2? | ||
let hostname = LIVE_RELOAD_ORIGIN ? new URL(LIVE_RELOAD_ORIGIN).hostname : location.hostname; | ||
let url = new URL(protocol + "//" + hostname + "/socket"); | ||
let ws = new WebSocket(socketPath); | ||
ws.onmessage = (message) => { | ||
url.port = | ||
${port} || | ||
(LIVE_RELOAD_ORIGIN ? new URL(LIVE_RELOAD_ORIGIN).port : 8002); | ||
let ws = new WebSocket(url.href); | ||
ws.onmessage = async (message) => { | ||
let event = JSON.parse(message.data); | ||
@@ -1162,2 +886,42 @@ if (event.type === "LOG") { | ||
} | ||
if (event.type === "HMR") { | ||
if (!window.__hmr__ || !window.__hmr__.contexts) { | ||
console.log("💿 [HMR] No HMR context, reloading window ..."); | ||
window.location.reload(); | ||
return; | ||
} | ||
if (!event.updates || !event.updates.length) return; | ||
let updateAccepted = false; | ||
let needsRevalidation = new Set(); | ||
for (let update of event.updates) { | ||
console.log("[HMR] " + update.reason + " [" + update.id +"]") | ||
if (update.revalidate) { | ||
needsRevalidation.add(update.routeId); | ||
console.log("[HMR] Revalidating [" + update.routeId + "]"); | ||
} | ||
let imported = await import(update.url + '?t=' + event.assetsManifest.hmr.timestamp); | ||
if (window.__hmr__.contexts[update.id]) { | ||
let accepted = window.__hmr__.contexts[update.id].emit( | ||
imported | ||
); | ||
if (accepted) { | ||
console.log("[HMR] Update accepted by", update.id); | ||
updateAccepted = true; | ||
} | ||
} | ||
} | ||
if (event.assetsManifest && window.__hmr__.contexts["remix:manifest"]) { | ||
let accepted = window.__hmr__.contexts["remix:manifest"].emit( | ||
{ needsRevalidation, assetsManifest: event.assetsManifest } | ||
); | ||
if (accepted) { | ||
console.log("[HMR] Update accepted by", "remix:manifest"); | ||
updateAccepted = true; | ||
} | ||
} | ||
if (!updateAccepted) { | ||
console.log("[HMR] Update rejected, reloading..."); | ||
window.location.reload(); | ||
} | ||
} | ||
}; | ||
@@ -1169,11 +933,13 @@ ws.onopen = () => { | ||
}; | ||
ws.onclose = (error) => { | ||
console.log("Remix dev asset server web socket closed. Reconnecting..."); | ||
setTimeout( | ||
() => | ||
remixLiveReloadConnect({ | ||
onOpen: () => window.location.reload(), | ||
}), | ||
1000 | ||
); | ||
ws.onclose = (event) => { | ||
if (event.code === 1006) { | ||
console.log("Remix dev asset server web socket closed. Reconnecting..."); | ||
setTimeout( | ||
() => | ||
remixLiveReloadConnect({ | ||
onOpen: () => window.location.reload(), | ||
}), | ||
${String(timeoutMs)} | ||
); | ||
} | ||
}; | ||
@@ -1190,5 +956,16 @@ ws.onerror = (error) => { | ||
}; | ||
function mergeRefs(...refs) { | ||
return value => { | ||
refs.forEach(ref => { | ||
if (typeof ref === "function") { | ||
ref(value); | ||
} else if (ref != null) { | ||
ref.current = value; | ||
} | ||
}); | ||
}; | ||
} | ||
exports.Await = Await; | ||
exports.Form = Form; | ||
exports.FormImpl = FormImpl; | ||
exports.Link = Link; | ||
@@ -1200,16 +977,10 @@ exports.Links = Links; | ||
exports.PrefetchPageLinks = PrefetchPageLinks; | ||
exports.RemixEntry = RemixEntry; | ||
exports.RemixEntryContext = RemixEntryContext; | ||
exports.RemixRoute = RemixRoute; | ||
exports.RemixContext = RemixContext; | ||
exports.Scripts = Scripts; | ||
exports.composeEventHandlers = composeEventHandlers; | ||
exports.useActionData = useActionData; | ||
exports.useBeforeUnload = useBeforeUnload; | ||
exports.useFetcher = useFetcher; | ||
exports.useFetchers = useFetchers; | ||
exports.useFormAction = useFormAction; | ||
exports.useLoaderData = useLoaderData; | ||
exports.useMatches = useMatches; | ||
exports.useSubmit = useSubmit; | ||
exports.useSubmitImpl = useSubmitImpl; | ||
exports.useTransition = useTransition; | ||
exports.useRemixContext = useRemixContext; | ||
exports.useRouteLoaderData = useRouteLoaderData; |
@@ -1,9 +0,15 @@ | ||
import type { Submission } from "./transition"; | ||
export declare type AppData = any; | ||
export declare type FormMethod = "get" | "post" | "put" | "patch" | "delete"; | ||
export declare type FormEncType = "application/x-www-form-urlencoded" | "multipart/form-data"; | ||
export declare function isCatchResponse(response: any): boolean; | ||
export declare function isErrorResponse(response: any): boolean; | ||
export declare function isRedirectResponse(response: any): boolean; | ||
export declare function fetchData(url: URL, routeId: string, signal: AbortSignal, submission?: Submission): Promise<Response | Error>; | ||
export declare function extractData(response: Response): Promise<AppData>; | ||
import { UNSAFE_DeferredData as DeferredData } from "@remix-run/router"; | ||
/** | ||
* Data for a route that was returned from a `loader()`. | ||
*/ | ||
export type AppData = unknown; | ||
export declare function isCatchResponse(response: Response): boolean; | ||
export declare function isErrorResponse(response: any): response is Response; | ||
export declare function isNetworkErrorResponse(response: any): response is Response; | ||
export declare function isRedirectResponse(response: Response): boolean; | ||
export declare function isDeferredResponse(response: Response): boolean; | ||
export declare function isResponse(value: any): value is Response; | ||
export declare function isDeferredData(value: any): value is DeferredData; | ||
export declare function fetchData(request: Request, routeId: string, retry?: number): Promise<Response | Error>; | ||
export declare function createRequestInit(request: Request): Promise<RequestInit>; | ||
export declare function parseDeferredReadableStream(stream: ReadableStream<Uint8Array>): Promise<DeferredData>; |
278
dist/data.js
/** | ||
* @remix-run/react v0.0.0-nightly-81ec1b7-20220830 | ||
* @remix-run/react v0.0.0-nightly-825f70ebd-20240914 | ||
* | ||
@@ -15,21 +15,56 @@ * Copyright (c) Remix Software Inc. | ||
var invariant = require('./invariant.js'); | ||
var router = require('@remix-run/router'); | ||
/** | ||
* Data for a route that was returned from a `loader()`. | ||
*/ | ||
function isCatchResponse(response) { | ||
return response instanceof Response && response.headers.get("X-Remix-Catch") != null; | ||
return response.headers.get("X-Remix-Catch") != null; | ||
} | ||
function isErrorResponse(response) { | ||
return response instanceof Response && response.headers.get("X-Remix-Error") != null; | ||
return response.headers.get("X-Remix-Error") != null; | ||
} | ||
function isNetworkErrorResponse(response) { | ||
// If we reach the Remix server, we can safely identify response types via the | ||
// X-Remix-Error/X-Remix-Catch headers. However, if we never reach the Remix | ||
// server, and instead receive a 4xx/5xx from somewhere in between (like | ||
// Cloudflare), then we get a false negative in the isErrorResponse check and | ||
// we incorrectly assume that the user returns the 4xx/5xx response and | ||
// consider it successful. To alleviate this, we add X-Remix-Response to any | ||
// non-Error/non-Catch responses coming back from the server. If we don't | ||
// see this, we can conclude that a 4xx/5xx response never actually reached | ||
// the Remix server and we can bubble it up as an error. | ||
return isResponse(response) && response.status >= 400 && response.headers.get("X-Remix-Error") == null && response.headers.get("X-Remix-Catch") == null && response.headers.get("X-Remix-Response") == null; | ||
} | ||
function isRedirectResponse(response) { | ||
return response instanceof Response && response.headers.get("X-Remix-Redirect") != null; | ||
return response.headers.get("X-Remix-Redirect") != null; | ||
} | ||
async function fetchData(url, routeId, signal, submission) { | ||
function isDeferredResponse(response) { | ||
var _response$headers$get; | ||
return !!((_response$headers$get = response.headers.get("Content-Type")) !== null && _response$headers$get !== void 0 && _response$headers$get.match(/text\/remix-deferred/)); | ||
} | ||
function isResponse(value) { | ||
return value != null && typeof value.status === "number" && typeof value.statusText === "string" && typeof value.headers === "object" && typeof value.body !== "undefined"; | ||
} | ||
function isDeferredData(value) { | ||
let deferred = value; | ||
return deferred && typeof deferred === "object" && typeof deferred.data === "object" && typeof deferred.subscribe === "function" && typeof deferred.cancel === "function" && typeof deferred.resolveData === "function"; | ||
} | ||
async function fetchData(request, routeId, retry = 0) { | ||
let url = new URL(request.url); | ||
url.searchParams.set("_data", routeId); | ||
let init = submission ? getActionInit(submission, signal) : { | ||
credentials: "same-origin", | ||
signal | ||
}; | ||
let response = await fetch(url.href, init); | ||
if (retry > 0) { | ||
// Retry up to 3 times waiting 50, 250, 1250 ms | ||
// between retries for a total of 1550 ms before giving up. | ||
await new Promise(resolve => setTimeout(resolve, 5 ** retry * 10)); | ||
} | ||
let init = await createRequestInit(request); | ||
let revalidation = window.__remixRevalidation; | ||
let response = await fetch(url.href, init).catch(error => { | ||
if (typeof revalidation === "number" && revalidation === window.__remixRevalidation && (error === null || error === void 0 ? void 0 : error.name) === "TypeError" && retry < 3) { | ||
return fetchData(request, routeId, retry + 1); | ||
} | ||
throw error; | ||
}); | ||
if (isErrorResponse(response)) { | ||
@@ -41,52 +76,207 @@ let data = await response.json(); | ||
} | ||
if (isNetworkErrorResponse(response)) { | ||
let text = await response.text(); | ||
let error = new Error(text); | ||
error.stack = undefined; | ||
return error; | ||
} | ||
return response; | ||
} | ||
async function extractData(response) { | ||
// This same algorithm is used on the server to interpret load | ||
// results when we render the HTML page. | ||
let contentType = response.headers.get("Content-Type"); | ||
async function createRequestInit(request) { | ||
let init = { | ||
signal: request.signal | ||
}; | ||
if (request.method !== "GET") { | ||
init.method = request.method; | ||
let contentType = request.headers.get("Content-Type"); | ||
if (contentType && /\bapplication\/json\b/.test(contentType)) { | ||
return response.json(); | ||
// Check between word boundaries instead of startsWith() due to the last | ||
// paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type | ||
if (contentType && /\bapplication\/json\b/.test(contentType)) { | ||
init.headers = { | ||
"Content-Type": contentType | ||
}; | ||
init.body = JSON.stringify(await request.json()); | ||
} else if (contentType && /\btext\/plain\b/.test(contentType)) { | ||
init.headers = { | ||
"Content-Type": contentType | ||
}; | ||
init.body = await request.text(); | ||
} else if (contentType && /\bapplication\/x-www-form-urlencoded\b/.test(contentType)) { | ||
init.body = new URLSearchParams(await request.text()); | ||
} else { | ||
init.body = await request.formData(); | ||
} | ||
} | ||
return init; | ||
} | ||
const DEFERRED_VALUE_PLACEHOLDER_PREFIX = "__deferred_promise:"; | ||
async function parseDeferredReadableStream(stream) { | ||
if (!stream) { | ||
throw new Error("parseDeferredReadableStream requires stream argument"); | ||
} | ||
let deferredData; | ||
let deferredResolvers = {}; | ||
try { | ||
let sectionReader = readStreamSections(stream); | ||
return response.text(); | ||
// Read the first section to get the critical data | ||
let initialSectionResult = await sectionReader.next(); | ||
let initialSection = initialSectionResult.value; | ||
if (!initialSection) throw new Error("no critical data"); | ||
let criticalData = JSON.parse(initialSection); | ||
// Setup deferred data and resolvers for later based on the critical data | ||
if (typeof criticalData === "object" && criticalData !== null) { | ||
for (let [eventKey, value] of Object.entries(criticalData)) { | ||
if (typeof value !== "string" || !value.startsWith(DEFERRED_VALUE_PLACEHOLDER_PREFIX)) { | ||
continue; | ||
} | ||
deferredData = deferredData || {}; | ||
deferredData[eventKey] = new Promise((resolve, reject) => { | ||
deferredResolvers[eventKey] = { | ||
resolve: value => { | ||
resolve(value); | ||
delete deferredResolvers[eventKey]; | ||
}, | ||
reject: error => { | ||
reject(error); | ||
delete deferredResolvers[eventKey]; | ||
} | ||
}; | ||
}); | ||
} | ||
} | ||
// Read the rest of the stream and resolve deferred promises | ||
void (async () => { | ||
try { | ||
for await (let section of sectionReader) { | ||
// Determine event type and data | ||
let [event, ...sectionDataStrings] = section.split(":"); | ||
let sectionDataString = sectionDataStrings.join(":"); | ||
let data = JSON.parse(sectionDataString); | ||
if (event === "data") { | ||
for (let [key, value] of Object.entries(data)) { | ||
if (deferredResolvers[key]) { | ||
deferredResolvers[key].resolve(value); | ||
} | ||
} | ||
} else if (event === "error") { | ||
for (let [key, value] of Object.entries(data)) { | ||
let err = new Error(value.message); | ||
err.stack = value.stack; | ||
if (deferredResolvers[key]) { | ||
deferredResolvers[key].reject(err); | ||
} | ||
} | ||
} | ||
} | ||
for (let [key, resolver] of Object.entries(deferredResolvers)) { | ||
resolver.reject(new router.AbortedDeferredError(`Deferred ${key} will never be resolved`)); | ||
} | ||
} catch (error) { | ||
// Reject any existing deferred promises if something blows up | ||
for (let resolver of Object.values(deferredResolvers)) { | ||
resolver.reject(error); | ||
} | ||
} | ||
})(); | ||
return new router.UNSAFE_DeferredData({ | ||
...criticalData, | ||
...deferredData | ||
}); | ||
} catch (error) { | ||
for (let resolver of Object.values(deferredResolvers)) { | ||
resolver.reject(error); | ||
} | ||
throw error; | ||
} | ||
} | ||
async function* readStreamSections(stream) { | ||
let reader = stream.getReader(); | ||
let buffer = []; | ||
let sections = []; | ||
let closed = false; | ||
let encoder = new TextEncoder(); | ||
let decoder = new TextDecoder(); | ||
let readStreamSection = async () => { | ||
if (sections.length > 0) return sections.shift(); | ||
function getActionInit(submission, signal) { | ||
let { | ||
encType, | ||
method, | ||
formData | ||
} = submission; | ||
let headers = undefined; | ||
let body = formData; | ||
// Read from the stream until we have at least one complete section to process | ||
while (!closed && sections.length === 0) { | ||
let chunk = await reader.read(); | ||
if (chunk.done) { | ||
closed = true; | ||
break; | ||
} | ||
// Buffer the raw chunks | ||
buffer.push(chunk.value); | ||
try { | ||
// Attempt to split off a section from the buffer | ||
let bufferedString = decoder.decode(mergeArrays(...buffer)); | ||
let splitSections = bufferedString.split("\n\n"); | ||
if (splitSections.length >= 2) { | ||
// We have a complete section, so add it to the sections array | ||
sections.push(...splitSections.slice(0, -1)); | ||
// Remove the section from the buffer and store the rest for future processing | ||
buffer = [encoder.encode(splitSections.slice(-1).join("\n\n"))]; | ||
} | ||
if (encType === "application/x-www-form-urlencoded") { | ||
body = new URLSearchParams(); | ||
// If we successfully parsed at least one section, break out of reading the stream | ||
// to allow upstream processing of the processable sections | ||
if (sections.length > 0) { | ||
break; | ||
} | ||
} catch { | ||
// If we failed to parse the buffer it was because we failed to decode the stream | ||
// because we are missing bytes that we haven't yet received, so continue reading | ||
// from the stream until we have a complete section | ||
continue; | ||
} | ||
} | ||
for (let [key, value] of formData) { | ||
invariant(typeof value === "string", `File inputs are not supported with encType "application/x-www-form-urlencoded", please use "multipart/form-data" instead.`); | ||
body.append(key, value); | ||
// If we have a complete section, return it | ||
if (sections.length > 0) { | ||
return sections.shift(); | ||
} | ||
headers = { | ||
"Content-Type": encType | ||
}; | ||
} | ||
// If we have no complete section, but we have no more chunks to process, | ||
// split those sections and clear out the buffer as there is no more data | ||
// to process. If this errors, let it bubble up as the stream ended | ||
// without valid data | ||
if (buffer.length > 0) { | ||
let bufferedString = decoder.decode(mergeArrays(...buffer)); | ||
sections = bufferedString.split("\n\n").filter(s => s); | ||
buffer = []; | ||
} | ||
return { | ||
method, | ||
body, | ||
signal, | ||
credentials: "same-origin", | ||
headers | ||
// Return any remaining sections that have been processed | ||
return sections.shift(); | ||
}; | ||
let section = await readStreamSection(); | ||
while (section) { | ||
yield section; | ||
section = await readStreamSection(); | ||
} | ||
} | ||
function mergeArrays(...arrays) { | ||
let out = new Uint8Array(arrays.reduce((total, arr) => total + arr.length, 0)); | ||
let offset = 0; | ||
for (let arr of arrays) { | ||
out.set(arr, offset); | ||
offset += arr.length; | ||
} | ||
return out; | ||
} | ||
exports.extractData = extractData; | ||
exports.createRequestInit = createRequestInit; | ||
exports.fetchData = fetchData; | ||
exports.isCatchResponse = isCatchResponse; | ||
exports.isDeferredData = isDeferredData; | ||
exports.isDeferredResponse = isDeferredResponse; | ||
exports.isErrorResponse = isErrorResponse; | ||
exports.isNetworkErrorResponse = isNetworkErrorResponse; | ||
exports.isRedirectResponse = isRedirectResponse; | ||
exports.isResponse = isResponse; | ||
exports.parseDeferredReadableStream = parseDeferredReadableStream; |
@@ -1,15 +0,38 @@ | ||
import type { AppState } from "./errors"; | ||
import type { StaticHandlerContext } from "@remix-run/router"; | ||
import type { RouteManifest, EntryRoute } from "./routes"; | ||
import type { RouteData } from "./routeData"; | ||
import type { RouteMatch } from "./routeMatching"; | ||
import type { RouteModules } from "./routeModules"; | ||
export interface EntryContext { | ||
appState: AppState; | ||
type SerializedError = { | ||
message: string; | ||
stack?: string; | ||
}; | ||
export interface RemixContextObject { | ||
manifest: AssetsManifest; | ||
matches: RouteMatch<EntryRoute>[]; | ||
routeData: RouteData; | ||
actionData?: RouteData; | ||
routeModules: RouteModules; | ||
criticalCss?: string; | ||
serverHandoffString?: string; | ||
future: FutureConfig; | ||
isSpaMode: boolean; | ||
abortDelay?: number; | ||
serializeError?(error: Error): SerializedError; | ||
renderMeta?: { | ||
didRenderScripts?: boolean; | ||
streamCache?: Record<number, Promise<void> & { | ||
result?: { | ||
done: boolean; | ||
value: string; | ||
}; | ||
error?: unknown; | ||
}>; | ||
}; | ||
} | ||
export interface EntryContext extends RemixContextObject { | ||
staticHandlerContext: StaticHandlerContext; | ||
serverHandoffStream?: ReadableStream<Uint8Array>; | ||
} | ||
export interface FutureConfig { | ||
v3_fetcherPersist: boolean; | ||
v3_relativeSplatPath: boolean; | ||
unstable_lazyRouteDiscovery: boolean; | ||
unstable_singleFetch: boolean; | ||
} | ||
export interface AssetsManifest { | ||
@@ -23,2 +46,7 @@ entry: { | ||
version: string; | ||
hmr?: { | ||
timestamp?: number; | ||
runtime: string; | ||
}; | ||
} | ||
export {}; |
@@ -1,11 +0,9 @@ | ||
import type { Location } from "history"; | ||
import React from "react"; | ||
import type { CatchBoundaryComponent, ErrorBoundaryComponent } from "./routeModules"; | ||
import type { ThrownResponse } from "./errors"; | ||
declare type RemixErrorBoundaryProps = React.PropsWithChildren<{ | ||
import * as React from "react"; | ||
import type { Location } from "@remix-run/router"; | ||
type RemixErrorBoundaryProps = React.PropsWithChildren<{ | ||
location: Location; | ||
component: ErrorBoundaryComponent; | ||
isOutsideRemixApp?: boolean; | ||
error?: Error; | ||
}>; | ||
declare type RemixErrorBoundaryState = { | ||
type RemixErrorBoundaryState = { | ||
error: null | Error; | ||
@@ -21,5 +19,5 @@ location: Location; | ||
error: Error | null; | ||
location: Location; | ||
location: Location<any>; | ||
}; | ||
render(): string | number | boolean | React.ReactFragment | JSX.Element | null | undefined; | ||
render(): string | number | boolean | Iterable<React.ReactNode> | React.JSX.Element | null | undefined; | ||
} | ||
@@ -29,21 +27,12 @@ /** | ||
*/ | ||
export declare function RemixRootDefaultErrorBoundary({ error }: { | ||
error: Error; | ||
}): JSX.Element; | ||
/** | ||
* Returns the status code and thrown response data. | ||
* | ||
* @see https://remix.run/api/conventions#catchboundary | ||
*/ | ||
export declare function useCatch<Result extends ThrownResponse = ThrownResponse>(): Result; | ||
declare type RemixCatchBoundaryProps = React.PropsWithChildren<{ | ||
location: Location; | ||
component: CatchBoundaryComponent; | ||
catch?: ThrownResponse; | ||
}>; | ||
export declare function RemixCatchBoundary({ catch: catchVal, component: Component, children, }: RemixCatchBoundaryProps): JSX.Element; | ||
/** | ||
* When app's don't provide a root level CatchBoundary, we default to this. | ||
*/ | ||
export declare function RemixRootDefaultCatchBoundary(): JSX.Element; | ||
export declare function RemixRootDefaultErrorBoundary({ error, isOutsideRemixApp, }: { | ||
error: unknown; | ||
isOutsideRemixApp?: boolean; | ||
}): React.JSX.Element; | ||
export declare function BoundaryShell({ title, renderScripts, isOutsideRemixApp, children, }: { | ||
title: string; | ||
renderScripts?: boolean; | ||
isOutsideRemixApp?: boolean; | ||
children: React.ReactNode | React.ReactNode[]; | ||
}): string | number | boolean | Iterable<React.ReactNode> | React.JSX.Element | null | undefined; | ||
export {}; |
/** | ||
* @remix-run/react v0.0.0-nightly-81ec1b7-20220830 | ||
* @remix-run/react v0.0.0-nightly-825f70ebd-20240914 | ||
* | ||
@@ -16,9 +16,26 @@ * Copyright (c) Remix Software Inc. | ||
var React = require('react'); | ||
var reactRouterDom = require('react-router-dom'); | ||
var components = require('./components.js'); | ||
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } | ||
function _interopNamespace(e) { | ||
if (e && e.__esModule) return e; | ||
var n = Object.create(null); | ||
if (e) { | ||
Object.keys(e).forEach(function (k) { | ||
if (k !== 'default') { | ||
var d = Object.getOwnPropertyDescriptor(e, k); | ||
Object.defineProperty(n, k, d.get ? d : { | ||
enumerable: true, | ||
get: function () { return e[k]; } | ||
}); | ||
} | ||
}); | ||
} | ||
n["default"] = e; | ||
return Object.freeze(n); | ||
} | ||
var React__default = /*#__PURE__*/_interopDefaultLegacy(React); | ||
var React__namespace = /*#__PURE__*/_interopNamespace(React); | ||
// TODO: We eventually might not want to import anything directly from `history` | ||
class RemixErrorBoundary extends React__default["default"].Component { | ||
class RemixErrorBoundary extends React__namespace.Component { | ||
constructor(props) { | ||
@@ -31,3 +48,2 @@ super(props); | ||
} | ||
static getDerivedStateFromError(error) { | ||
@@ -38,3 +54,2 @@ return { | ||
} | ||
static getDerivedStateFromProps(props, state) { | ||
@@ -55,8 +70,8 @@ // When we get into an error state, the user will likely click "back" to the | ||
}; | ||
} // If we're not changing locations, preserve the location but still surface | ||
} | ||
// If we're not changing locations, preserve the location but still surface | ||
// any new errors that may come through. We retain the existing error, we do | ||
// this because the error provided from the app state may be cleared without | ||
// the location changing. | ||
return { | ||
@@ -67,7 +82,7 @@ error: props.error || state.error, | ||
} | ||
render() { | ||
if (this.state.error) { | ||
return /*#__PURE__*/React__default["default"].createElement(this.props.component, { | ||
error: this.state.error | ||
return /*#__PURE__*/React__namespace.createElement(RemixRootDefaultErrorBoundary, { | ||
error: this.state.error, | ||
isOutsideRemixApp: true | ||
}); | ||
@@ -78,29 +93,45 @@ } else { | ||
} | ||
} | ||
} | ||
/** | ||
* When app's don't provide a root level ErrorBoundary, we default to this. | ||
*/ | ||
function RemixRootDefaultErrorBoundary({ | ||
error | ||
error, | ||
isOutsideRemixApp | ||
}) { | ||
console.error(error); | ||
return /*#__PURE__*/React__default["default"].createElement("html", { | ||
lang: "en" | ||
}, /*#__PURE__*/React__default["default"].createElement("head", null, /*#__PURE__*/React__default["default"].createElement("meta", { | ||
charSet: "utf-8" | ||
}), /*#__PURE__*/React__default["default"].createElement("meta", { | ||
name: "viewport", | ||
content: "width=device-width,initial-scale=1,viewport-fit=cover" | ||
}), /*#__PURE__*/React__default["default"].createElement("title", null, "Application Error!")), /*#__PURE__*/React__default["default"].createElement("body", null, /*#__PURE__*/React__default["default"].createElement("main", { | ||
style: { | ||
fontFamily: "system-ui, sans-serif", | ||
padding: "2rem" | ||
let heyDeveloper = /*#__PURE__*/React__namespace.createElement("script", { | ||
dangerouslySetInnerHTML: { | ||
__html: ` | ||
console.log( | ||
"💿 Hey developer 👋. You can provide a way better UX than this when your app throws errors. Check out https://remix.run/guides/errors for more information." | ||
); | ||
` | ||
} | ||
}, /*#__PURE__*/React__default["default"].createElement("h1", { | ||
}); | ||
if (reactRouterDom.isRouteErrorResponse(error)) { | ||
return /*#__PURE__*/React__namespace.createElement(BoundaryShell, { | ||
title: "Unhandled Thrown Response!" | ||
}, /*#__PURE__*/React__namespace.createElement("h1", { | ||
style: { | ||
fontSize: "24px" | ||
} | ||
}, error.status, " ", error.statusText), heyDeveloper); | ||
} | ||
let errorInstance; | ||
if (error instanceof Error) { | ||
errorInstance = error; | ||
} else { | ||
let errorString = error == null ? "Unknown Error" : typeof error === "object" && "toString" in error ? error.toString() : JSON.stringify(error); | ||
errorInstance = new Error(errorString); | ||
} | ||
return /*#__PURE__*/React__namespace.createElement(BoundaryShell, { | ||
title: "Application Error!", | ||
isOutsideRemixApp: isOutsideRemixApp | ||
}, /*#__PURE__*/React__namespace.createElement("h1", { | ||
style: { | ||
fontSize: "24px" | ||
} | ||
}, "Application Error"), /*#__PURE__*/React__default["default"].createElement("pre", { | ||
}, "Application Error"), /*#__PURE__*/React__namespace.createElement("pre", { | ||
style: { | ||
@@ -112,49 +143,42 @@ padding: "2rem", | ||
} | ||
}, error.stack)), /*#__PURE__*/React__default["default"].createElement("script", { | ||
dangerouslySetInnerHTML: { | ||
__html: ` | ||
console.log( | ||
"💿 Hey developer👋. You can provide a way better UX than this when your app throws errors. Check out https://remix.run/guides/errors for more information." | ||
); | ||
` | ||
} | ||
}))); | ||
}, errorInstance.stack), heyDeveloper); | ||
} | ||
let RemixCatchContext = /*#__PURE__*/React__default["default"].createContext(undefined); | ||
/** | ||
* Returns the status code and thrown response data. | ||
* | ||
* @see https://remix.run/api/conventions#catchboundary | ||
*/ | ||
function useCatch() { | ||
return React.useContext(RemixCatchContext); | ||
} | ||
function RemixCatchBoundary({ | ||
catch: catchVal, | ||
component: Component, | ||
function BoundaryShell({ | ||
title, | ||
renderScripts, | ||
isOutsideRemixApp, | ||
children | ||
}) { | ||
if (catchVal) { | ||
return /*#__PURE__*/React__default["default"].createElement(RemixCatchContext.Provider, { | ||
value: catchVal | ||
}, /*#__PURE__*/React__default["default"].createElement(Component, null)); | ||
} | ||
var _routeModules$root; | ||
let { | ||
routeModules | ||
} = components.useRemixContext(); | ||
return /*#__PURE__*/React__default["default"].createElement(React__default["default"].Fragment, null, children); | ||
} | ||
/** | ||
* When app's don't provide a root level CatchBoundary, we default to this. | ||
*/ | ||
// Generally speaking, when the root route has a Layout we want to use that | ||
// as the app shell instead of the default `BoundaryShell` wrapper markup below. | ||
// This is true for `loader`/`action` errors, most render errors, and | ||
// `HydrateFallback` scenarios. | ||
function RemixRootDefaultCatchBoundary() { | ||
let caught = useCatch(); | ||
return /*#__PURE__*/React__default["default"].createElement("html", { | ||
// However, render errors thrown from the `Layout` present a bit of an issue | ||
// because if the `Layout` itself throws during the `ErrorBoundary` pass and | ||
// we bubble outside the `RouterProvider` to the wrapping `RemixErrorBoundary`, | ||
// by returning only `children` here we'll be trying to append a `<div>` to | ||
// the `document` and the DOM will throw, putting React into an error/hydration | ||
// loop. | ||
// Instead, if we're ever rendering from the outermost `RemixErrorBoundary` | ||
// during hydration that wraps `RouterProvider`, then we can't trust the | ||
// `Layout` and should fallback to the default app shell so we're always | ||
// returning an `<html>` document. | ||
if ((_routeModules$root = routeModules.root) !== null && _routeModules$root !== void 0 && _routeModules$root.Layout && !isOutsideRemixApp) { | ||
return children; | ||
} | ||
return /*#__PURE__*/React__namespace.createElement("html", { | ||
lang: "en" | ||
}, /*#__PURE__*/React__default["default"].createElement("head", null, /*#__PURE__*/React__default["default"].createElement("meta", { | ||
}, /*#__PURE__*/React__namespace.createElement("head", null, /*#__PURE__*/React__namespace.createElement("meta", { | ||
charSet: "utf-8" | ||
}), /*#__PURE__*/React__default["default"].createElement("meta", { | ||
}), /*#__PURE__*/React__namespace.createElement("meta", { | ||
name: "viewport", | ||
content: "width=device-width,initial-scale=1,viewport-fit=cover" | ||
}), /*#__PURE__*/React__default["default"].createElement("title", null, "Unhandled Thrown Response!")), /*#__PURE__*/React__default["default"].createElement("body", null, /*#__PURE__*/React__default["default"].createElement("h1", { | ||
}), /*#__PURE__*/React__namespace.createElement("title", null, title)), /*#__PURE__*/React__namespace.createElement("body", null, /*#__PURE__*/React__namespace.createElement("main", { | ||
style: { | ||
@@ -164,17 +188,7 @@ fontFamily: "system-ui, sans-serif", | ||
} | ||
}, caught.status, " ", caught.statusText), /*#__PURE__*/React__default["default"].createElement("script", { | ||
dangerouslySetInnerHTML: { | ||
__html: ` | ||
console.log( | ||
"💿 Hey developer👋. You can provide a way better UX than this when your app throws 404s (and other responses). Check out https://remix.run/guides/not-found for more information." | ||
); | ||
` | ||
} | ||
}))); | ||
}, children, renderScripts ? /*#__PURE__*/React__namespace.createElement(components.Scripts, null) : null))); | ||
} | ||
exports.RemixCatchBoundary = RemixCatchBoundary; | ||
exports.BoundaryShell = BoundaryShell; | ||
exports.RemixErrorBoundary = RemixErrorBoundary; | ||
exports.RemixRootDefaultCatchBoundary = RemixRootDefaultCatchBoundary; | ||
exports.RemixRootDefaultErrorBoundary = RemixRootDefaultErrorBoundary; | ||
exports.useCatch = useCatch; |
@@ -1,19 +0,2 @@ | ||
import type { AppData } from "./data"; | ||
export interface AppState { | ||
error?: SerializedError; | ||
catch?: ThrownResponse; | ||
catchBoundaryRouteId: string | null; | ||
loaderBoundaryRouteId: string | null; | ||
renderBoundaryRouteId: string | null; | ||
trackBoundaries: boolean; | ||
trackCatchBoundaries: boolean; | ||
} | ||
export interface ThrownResponse<Status extends number = number, Data = AppData> { | ||
status: Status; | ||
statusText: string; | ||
data: Data; | ||
} | ||
export declare type SerializedError = { | ||
message: string; | ||
stack?: string; | ||
}; | ||
import type { Router as RemixRouter } from "@remix-run/router"; | ||
export declare function deserializeErrors(errors: RemixRouter["state"]["errors"]): RemixRouter["state"]["errors"]; |
/** | ||
* @remix-run/react v0.0.0-nightly-81ec1b7-20220830 | ||
* @remix-run/react v0.0.0-nightly-825f70ebd-20240914 | ||
* | ||
@@ -15,3 +15,2 @@ * Copyright (c) Remix Software Inc. | ||
var source = arguments[i]; | ||
for (var key in source) { | ||
@@ -23,3 +22,2 @@ if (Object.prototype.hasOwnProperty.call(source, key)) { | ||
} | ||
return target; | ||
@@ -26,0 +24,0 @@ }; |
/** | ||
* @remix-run/react v0.0.0-nightly-81ec1b7-20220830 | ||
* @remix-run/react v0.0.0-nightly-825f70ebd-20240914 | ||
* | ||
@@ -11,8 +11,99 @@ * Copyright (c) Remix Software Inc. | ||
*/ | ||
import { createBrowserHistory } from 'history'; | ||
import { createRouter, createBrowserHistory } from '@remix-run/router'; | ||
import * as React from 'react'; | ||
import { RemixEntry } from './components.js'; | ||
import { UNSAFE_mapRouteProperties } from 'react-router'; | ||
import { matchRoutes, RouterProvider } from 'react-router-dom'; | ||
import { RemixContext } from './components.js'; | ||
import { RemixErrorBoundary } from './errorBoundaries.js'; | ||
import { deserializeErrors } from './errors.js'; | ||
import { createClientRoutesWithHMRRevalidationOptOut, createClientRoutes, shouldHydrateRouteLoader } from './routes.js'; | ||
import { decodeViaTurboStream, getSingleFetchDataStrategy } from './single-fetch.js'; | ||
import invariant from './invariant.js'; | ||
import { getPatchRoutesOnNavigationFunction, useFogOFWarDiscovery } from './fog-of-war.js'; | ||
// TODO: We eventually might not want to import anything directly from `history` | ||
/* eslint-disable prefer-let/prefer-let */ | ||
/* eslint-enable prefer-let/prefer-let */ | ||
let stateDecodingPromise; | ||
let router; | ||
let routerInitialized = false; | ||
let hmrAbortController; | ||
let hmrRouterReadyResolve; | ||
// There's a race condition with HMR where the remix:manifest is signaled before | ||
// the router is assigned in the RemixBrowser component. This promise gates the | ||
// HMR handler until the router is ready | ||
let hmrRouterReadyPromise = new Promise(resolve => { | ||
// body of a promise is executed immediately, so this can be resolved outside | ||
// of the promise body | ||
hmrRouterReadyResolve = resolve; | ||
}).catch(() => { | ||
// This is a noop catch handler to avoid unhandled promise rejection warnings | ||
// in the console. The promise is never rejected. | ||
return undefined; | ||
}); | ||
// @ts-expect-error | ||
if (import.meta && import.meta.hot) { | ||
// @ts-expect-error | ||
import.meta.hot.accept("remix:manifest", async ({ | ||
assetsManifest, | ||
needsRevalidation | ||
}) => { | ||
let router = await hmrRouterReadyPromise; | ||
// This should never happen, but just in case... | ||
if (!router) { | ||
console.error("Failed to accept HMR update because the router was not ready."); | ||
return; | ||
} | ||
let routeIds = [...new Set(router.state.matches.map(m => m.route.id).concat(Object.keys(window.__remixRouteModules)))]; | ||
if (hmrAbortController) { | ||
hmrAbortController.abort(); | ||
} | ||
hmrAbortController = new AbortController(); | ||
let signal = hmrAbortController.signal; | ||
// Load new route modules that we've seen. | ||
let newRouteModules = Object.assign({}, window.__remixRouteModules, Object.fromEntries((await Promise.all(routeIds.map(async id => { | ||
var _assetsManifest$hmr, _window$__remixRouteM, _window$__remixRouteM2, _window$__remixRouteM3; | ||
if (!assetsManifest.routes[id]) { | ||
return null; | ||
} | ||
let imported = await import(assetsManifest.routes[id].module + `?t=${(_assetsManifest$hmr = assetsManifest.hmr) === null || _assetsManifest$hmr === void 0 ? void 0 : _assetsManifest$hmr.timestamp}`); | ||
return [id, { | ||
...imported, | ||
// react-refresh takes care of updating these in-place, | ||
// if we don't preserve existing values we'll loose state. | ||
default: imported.default ? ((_window$__remixRouteM = window.__remixRouteModules[id]) === null || _window$__remixRouteM === void 0 ? void 0 : _window$__remixRouteM.default) ?? imported.default : imported.default, | ||
ErrorBoundary: imported.ErrorBoundary ? ((_window$__remixRouteM2 = window.__remixRouteModules[id]) === null || _window$__remixRouteM2 === void 0 ? void 0 : _window$__remixRouteM2.ErrorBoundary) ?? imported.ErrorBoundary : imported.ErrorBoundary, | ||
HydrateFallback: imported.HydrateFallback ? ((_window$__remixRouteM3 = window.__remixRouteModules[id]) === null || _window$__remixRouteM3 === void 0 ? void 0 : _window$__remixRouteM3.HydrateFallback) ?? imported.HydrateFallback : imported.HydrateFallback | ||
}]; | ||
}))).filter(Boolean))); | ||
Object.assign(window.__remixRouteModules, newRouteModules); | ||
// Create new routes | ||
let routes = createClientRoutesWithHMRRevalidationOptOut(needsRevalidation, assetsManifest.routes, window.__remixRouteModules, window.__remixContext.state, window.__remixContext.future, window.__remixContext.isSpaMode); | ||
// This is temporary API and will be more granular before release | ||
router._internalSetRoutes(routes); | ||
// Wait for router to be idle before updating the manifest and route modules | ||
// and triggering a react-refresh | ||
let unsub = router.subscribe(state => { | ||
if (state.revalidation === "idle") { | ||
unsub(); | ||
// Abort if a new update comes in while we're waiting for the | ||
// router to be idle. | ||
if (signal.aborted) return; | ||
// Ensure RouterProvider setState has flushed before re-rendering | ||
setTimeout(() => { | ||
Object.assign(window.__remixManifest, assetsManifest); | ||
window.$RefreshRuntime$.performReactRefresh(); | ||
}, 1); | ||
} | ||
}); | ||
window.__remixRevalidation = (window.__remixRevalidation || 0) + 1; | ||
router.revalidate(); | ||
}); | ||
} | ||
/** | ||
@@ -24,32 +115,166 @@ * The entry point for a Remix app when it is rendered in the browser (in | ||
function RemixBrowser(_props) { | ||
let historyRef = React.useRef(); | ||
if (!router) { | ||
// When single fetch is enabled, we need to suspend until the initial state | ||
// snapshot is decoded into window.__remixContext.state | ||
if (window.__remixContext.future.unstable_singleFetch) { | ||
// Note: `stateDecodingPromise` is not coupled to `router` - we'll reach this | ||
// code potentially many times waiting for our state to arrive, but we'll | ||
// then only get past here and create the `router` one time | ||
if (!stateDecodingPromise) { | ||
let stream = window.__remixContext.stream; | ||
invariant(stream, "No stream found for single fetch decoding"); | ||
window.__remixContext.stream = undefined; | ||
stateDecodingPromise = decodeViaTurboStream(stream, window).then(value => { | ||
window.__remixContext.state = value.value; | ||
stateDecodingPromise.value = true; | ||
}).catch(e => { | ||
stateDecodingPromise.error = e; | ||
}); | ||
} | ||
if (stateDecodingPromise.error) { | ||
throw stateDecodingPromise.error; | ||
} | ||
if (!stateDecodingPromise.value) { | ||
throw stateDecodingPromise; | ||
} | ||
} | ||
let routes = createClientRoutes(window.__remixManifest.routes, window.__remixRouteModules, window.__remixContext.state, window.__remixContext.future, window.__remixContext.isSpaMode); | ||
let hydrationData = undefined; | ||
if (!window.__remixContext.isSpaMode) { | ||
// Create a shallow clone of `loaderData` we can mutate for partial hydration. | ||
// When a route exports a `clientLoader` and a `HydrateFallback`, the SSR will | ||
// render the fallback so we need the client to do the same for hydration. | ||
// The server loader data has already been exposed to these route `clientLoader`'s | ||
// in `createClientRoutes` above, so we need to clear out the version we pass to | ||
// `createBrowserRouter` so it initializes and runs the client loaders. | ||
hydrationData = { | ||
...window.__remixContext.state, | ||
loaderData: { | ||
...window.__remixContext.state.loaderData | ||
} | ||
}; | ||
let initialMatches = matchRoutes(routes, window.location, window.__remixContext.basename); | ||
if (initialMatches) { | ||
for (let match of initialMatches) { | ||
let routeId = match.route.id; | ||
let route = window.__remixRouteModules[routeId]; | ||
let manifestRoute = window.__remixManifest.routes[routeId]; | ||
// Clear out the loaderData to avoid rendering the route component when the | ||
// route opted into clientLoader hydration and either: | ||
// * gave us a HydrateFallback | ||
// * or doesn't have a server loader and we have no data to render | ||
if (route && shouldHydrateRouteLoader(manifestRoute, route, window.__remixContext.isSpaMode) && (route.HydrateFallback || !manifestRoute.hasLoader)) { | ||
hydrationData.loaderData[routeId] = undefined; | ||
} else if (manifestRoute && !manifestRoute.hasLoader) { | ||
// Since every Remix route gets a `loader` on the client side to load | ||
// the route JS module, we need to add a `null` value to `loaderData` | ||
// for any routes that don't have server loaders so our partial | ||
// hydration logic doesn't kick off the route module loaders during | ||
// hydration | ||
hydrationData.loaderData[routeId] = null; | ||
} | ||
} | ||
} | ||
if (hydrationData && hydrationData.errors) { | ||
hydrationData.errors = deserializeErrors(hydrationData.errors); | ||
} | ||
} | ||
if (historyRef.current == null) { | ||
historyRef.current = createBrowserHistory({ | ||
window | ||
// We don't use createBrowserRouter here because we need fine-grained control | ||
// over initialization to support synchronous `clientLoader` flows. | ||
router = createRouter({ | ||
routes, | ||
history: createBrowserHistory(), | ||
basename: window.__remixContext.basename, | ||
future: { | ||
v7_normalizeFormMethod: true, | ||
v7_fetcherPersist: window.__remixContext.future.v3_fetcherPersist, | ||
v7_partialHydration: true, | ||
v7_prependBasename: true, | ||
v7_relativeSplatPath: window.__remixContext.future.v3_relativeSplatPath, | ||
// Single fetch enables this underlying behavior | ||
v7_skipActionErrorRevalidation: window.__remixContext.future.unstable_singleFetch === true | ||
}, | ||
hydrationData, | ||
mapRouteProperties: UNSAFE_mapRouteProperties, | ||
unstable_dataStrategy: window.__remixContext.future.unstable_singleFetch ? getSingleFetchDataStrategy(window.__remixManifest, window.__remixRouteModules, () => router) : undefined, | ||
unstable_patchRoutesOnNavigation: getPatchRoutesOnNavigationFunction(window.__remixManifest, window.__remixRouteModules, window.__remixContext.future, window.__remixContext.isSpaMode, window.__remixContext.basename) | ||
}); | ||
// We can call initialize() immediately if the router doesn't have any | ||
// loaders to run on hydration | ||
if (router.state.initialized) { | ||
routerInitialized = true; | ||
router.initialize(); | ||
} | ||
// @ts-ignore | ||
router.createRoutesForHMR = createClientRoutesWithHMRRevalidationOptOut; | ||
window.__remixRouter = router; | ||
// Notify that the router is ready for HMR | ||
if (hmrRouterReadyResolve) { | ||
hmrRouterReadyResolve(router); | ||
} | ||
} | ||
let history = historyRef.current; | ||
let [state, dispatch] = React.useReducer((_, update) => update, { | ||
action: history.action, | ||
location: history.location | ||
}); | ||
React.useLayoutEffect(() => history.listen(dispatch), [history]); | ||
let entryContext = window.__remixContext; | ||
entryContext.manifest = window.__remixManifest; | ||
entryContext.routeModules = window.__remixRouteModules; // In the browser, we don't need this because a) in the case of loader | ||
// errors we already know the order and b) in the case of render errors | ||
// React knows the order and handles error boundaries normally. | ||
// Critical CSS can become stale after code changes, e.g. styles might be | ||
// removed from a component, but the styles will still be present in the | ||
// server HTML. This allows our HMR logic to clear the critical CSS state. | ||
entryContext.appState.trackBoundaries = false; | ||
entryContext.appState.trackCatchBoundaries = false; | ||
return /*#__PURE__*/React.createElement(RemixEntry, { | ||
context: entryContext, | ||
action: state.action, | ||
location: state.location, | ||
navigator: history | ||
}); | ||
let [criticalCss, setCriticalCss] = React.useState(process.env.NODE_ENV === "development" ? window.__remixContext.criticalCss : undefined); | ||
if (process.env.NODE_ENV === "development") { | ||
window.__remixClearCriticalCss = () => setCriticalCss(undefined); | ||
} | ||
// This is due to the short circuit return above when the pathname doesn't | ||
// match and we force a hard reload. This is an exceptional scenario in which | ||
// we can't hydrate anyway. | ||
let [location, setLocation] = React.useState(router.state.location); | ||
React.useLayoutEffect(() => { | ||
// If we had to run clientLoaders on hydration, we delay initialization until | ||
// after we've hydrated to avoid hydration issues from synchronous client loaders | ||
if (!routerInitialized) { | ||
routerInitialized = true; | ||
router.initialize(); | ||
} | ||
}, []); | ||
React.useLayoutEffect(() => { | ||
return router.subscribe(newState => { | ||
if (newState.location !== location) { | ||
setLocation(newState.location); | ||
} | ||
}); | ||
}, [location]); | ||
useFogOFWarDiscovery(router, window.__remixManifest, window.__remixRouteModules, window.__remixContext.future, window.__remixContext.isSpaMode); | ||
// We need to include a wrapper RemixErrorBoundary here in case the root error | ||
// boundary also throws and we need to bubble up outside of the router entirely. | ||
// Then we need a stateful location here so the user can back-button navigate | ||
// out of there | ||
return ( | ||
/*#__PURE__*/ | ||
// This fragment is important to ensure we match the <RemixServer> JSX | ||
// structure so that useId values hydrate correctly | ||
React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(RemixContext.Provider, { | ||
value: { | ||
manifest: window.__remixManifest, | ||
routeModules: window.__remixRouteModules, | ||
future: window.__remixContext.future, | ||
criticalCss, | ||
isSpaMode: window.__remixContext.isSpaMode | ||
} | ||
}, /*#__PURE__*/React.createElement(RemixErrorBoundary, { | ||
location: location | ||
}, /*#__PURE__*/React.createElement(RouterProvider, { | ||
router: router, | ||
fallbackElement: null, | ||
future: { | ||
v7_startTransition: true | ||
} | ||
}))), window.__remixContext.future.unstable_singleFetch ? /*#__PURE__*/React.createElement(React.Fragment, null) : null) | ||
); | ||
} | ||
export { RemixBrowser }; |
/** | ||
* @remix-run/react v0.0.0-nightly-81ec1b7-20220830 | ||
* @remix-run/react v0.0.0-nightly-825f70ebd-20240914 | ||
* | ||
@@ -13,272 +13,48 @@ * Copyright (c) Remix Software Inc. | ||
import * as React from 'react'; | ||
import { useHref, NavLink as NavLink$1, Link as Link$1, useLocation, useResolvedPath, useNavigate, Router, useRoutes } from 'react-router-dom'; | ||
import { createPath } from 'history'; | ||
import { RemixErrorBoundary, RemixRootDefaultErrorBoundary, RemixCatchBoundary, RemixRootDefaultCatchBoundary } from './errorBoundaries.js'; | ||
import { useHref, NavLink as NavLink$1, Link as Link$1, Form as Form$1, matchRoutes, useLocation, Await as Await$1, useAsyncError, useMatches as useMatches$1, useLoaderData as useLoaderData$1, useRouteLoaderData as useRouteLoaderData$1, useActionData as useActionData$1, useFetcher as useFetcher$1, UNSAFE_DataRouterContext, UNSAFE_DataRouterStateContext } from 'react-router-dom'; | ||
import invariant from './invariant.js'; | ||
import { getLinksForMatches, isPageLinkDescriptor, getNewMatchesForLinks, getDataLinkHrefs, getModuleLinkHrefs, getStylesheetPrefetchLinks } from './links.js'; | ||
import { createHtml } from './markup.js'; | ||
import { createClientRoutes } from './routes.js'; | ||
import { matchClientRoutes } from './routeMatching.js'; | ||
import { createTransitionManager } from './transition.js'; | ||
import { getKeyedLinksForMatches, isPageLinkDescriptor, getNewMatchesForLinks, getDataLinkHrefs, getModuleLinkHrefs, getKeyedPrefetchLinks } from './links.js'; | ||
import { escapeHtml, createHtml } from './markup.js'; | ||
import { singleFetchUrl } from './single-fetch.js'; | ||
import { isFogOfWarEnabled, getPartialManifest } from './fog-of-war.js'; | ||
const RemixEntryContext = /*#__PURE__*/React.createContext(undefined); | ||
function useRemixEntryContext() { | ||
let context = React.useContext(RemixEntryContext); | ||
invariant(context, "You must render this element inside a <Remix> element"); | ||
function useDataRouterContext() { | ||
let context = React.useContext(UNSAFE_DataRouterContext); | ||
invariant(context, "You must render this element inside a <DataRouterContext.Provider> element"); | ||
return context; | ||
} | ||
function RemixEntry({ | ||
context: entryContext, | ||
action, | ||
location: historyLocation, | ||
navigator: _navigator, | ||
static: staticProp = false | ||
}) { | ||
let { | ||
manifest, | ||
routeData: documentLoaderData, | ||
actionData: documentActionData, | ||
routeModules, | ||
serverHandoffString, | ||
appState: entryComponentDidCatchEmulator | ||
} = entryContext; | ||
let clientRoutes = React.useMemo(() => createClientRoutes(manifest.routes, routeModules, RemixRoute), [manifest, routeModules]); | ||
let [clientState, setClientState] = React.useState(entryComponentDidCatchEmulator); | ||
let [transitionManager] = React.useState(() => { | ||
return createTransitionManager({ | ||
routes: clientRoutes, | ||
actionData: documentActionData, | ||
loaderData: documentLoaderData, | ||
location: historyLocation, | ||
catch: entryComponentDidCatchEmulator.catch, | ||
catchBoundaryId: entryComponentDidCatchEmulator.catchBoundaryRouteId, | ||
onRedirect: _navigator.replace | ||
}); | ||
}); | ||
React.useEffect(() => { | ||
let subscriber = state => { | ||
setClientState({ | ||
catch: state.catch, | ||
error: state.error, | ||
catchBoundaryRouteId: state.catchBoundaryId, | ||
loaderBoundaryRouteId: state.errorBoundaryId, | ||
renderBoundaryRouteId: null, | ||
trackBoundaries: false, | ||
trackCatchBoundaries: false | ||
}); | ||
}; | ||
return transitionManager.subscribe(subscriber); | ||
}, [transitionManager]); // Ensures pushes interrupting pending navigations use replace | ||
// TODO: Move this to React Router | ||
let navigator = React.useMemo(() => { | ||
let push = (to, state) => { | ||
return transitionManager.getState().transition.state !== "idle" ? _navigator.replace(to, state) : _navigator.push(to, state); | ||
}; | ||
return { ..._navigator, | ||
push | ||
}; | ||
}, [_navigator, transitionManager]); | ||
let { | ||
location, | ||
matches, | ||
loaderData, | ||
actionData | ||
} = transitionManager.getState(); // Send new location to the transition manager | ||
React.useEffect(() => { | ||
let { | ||
location | ||
} = transitionManager.getState(); | ||
if (historyLocation === location) return; | ||
transitionManager.send({ | ||
type: "navigation", | ||
location: historyLocation, | ||
submission: consumeNextNavigationSubmission(), | ||
action | ||
}); | ||
}, [transitionManager, historyLocation, action]); // If we tried to render and failed, and the app threw before rendering any | ||
// routes, get the error and pass it to the ErrorBoundary to emulate | ||
// `componentDidCatch` | ||
let ssrErrorBeforeRoutesRendered = clientState.error && clientState.renderBoundaryRouteId === null && clientState.loaderBoundaryRouteId === null ? deserializeError(clientState.error) : undefined; | ||
let ssrCatchBeforeRoutesRendered = clientState.catch && clientState.catchBoundaryRouteId === null ? clientState.catch : undefined; | ||
return /*#__PURE__*/React.createElement(RemixEntryContext.Provider, { | ||
value: { | ||
matches, | ||
manifest, | ||
appState: clientState, | ||
routeModules, | ||
serverHandoffString, | ||
clientRoutes, | ||
routeData: loaderData, | ||
actionData, | ||
transitionManager | ||
} | ||
}, /*#__PURE__*/React.createElement(RemixErrorBoundary, { | ||
location: location, | ||
component: RemixRootDefaultErrorBoundary, | ||
error: ssrErrorBeforeRoutesRendered | ||
}, /*#__PURE__*/React.createElement(RemixCatchBoundary, { | ||
location: location, | ||
component: RemixRootDefaultCatchBoundary, | ||
catch: ssrCatchBeforeRoutesRendered | ||
}, /*#__PURE__*/React.createElement(Router, { | ||
navigationType: action, | ||
location: location, | ||
navigator: navigator, | ||
static: staticProp | ||
}, /*#__PURE__*/React.createElement(Routes, null))))); | ||
function useDataRouterStateContext() { | ||
let context = React.useContext(UNSAFE_DataRouterStateContext); | ||
invariant(context, "You must render this element inside a <DataRouterStateContext.Provider> element"); | ||
return context; | ||
} | ||
function deserializeError(data) { | ||
let error = new Error(data.message); | ||
error.stack = data.stack; | ||
return error; | ||
} | ||
//////////////////////////////////////////////////////////////////////////////// | ||
// RemixContext | ||
function Routes() { | ||
// TODO: Add `renderMatches` function to RR that we can use and then we don't | ||
// need this component, we can just `renderMatches` from RemixEntry | ||
let { | ||
clientRoutes | ||
} = useRemixEntryContext(); // fallback to the root if we don't have a match | ||
let element = useRoutes(clientRoutes) || clientRoutes[0].element; | ||
return element; | ||
} //////////////////////////////////////////////////////////////////////////////// | ||
// RemixRoute | ||
const RemixRouteContext = /*#__PURE__*/React.createContext(undefined); | ||
function useRemixRouteContext() { | ||
let context = React.useContext(RemixRouteContext); | ||
invariant(context, "You must render this element in a remix route element"); | ||
const RemixContext = /*#__PURE__*/React.createContext(undefined); | ||
RemixContext.displayName = "Remix"; | ||
function useRemixContext() { | ||
let context = React.useContext(RemixContext); | ||
invariant(context, "You must render this element inside a <Remix> element"); | ||
return context; | ||
} | ||
function DefaultRouteComponent({ | ||
id | ||
}) { | ||
throw new Error(`Route "${id}" has no component! Please go add a \`default\` export in the route module file.\n` + "If you were trying to navigate or submit to a resource route, use `<a>` instead of `<Link>` or `<Form reloadDocument>`."); | ||
} | ||
function RemixRoute({ | ||
id | ||
}) { | ||
let location = useLocation(); | ||
let { | ||
routeData, | ||
routeModules, | ||
appState | ||
} = useRemixEntryContext(); // This checks prevent cryptic error messages such as: 'Cannot read properties of undefined (reading 'root')' | ||
invariant(routeData, "Cannot initialize 'routeData'. This normally occurs when you have server code in your client modules.\n" + "Check this link for more details:\nhttps://remix.run/pages/gotchas#server-code-in-client-bundles"); | ||
invariant(routeModules, "Cannot initialize 'routeModules'. This normally occurs when you have server code in your client modules.\n" + "Check this link for more details:\nhttps://remix.run/pages/gotchas#server-code-in-client-bundles"); | ||
let data = routeData[id]; | ||
let { | ||
default: Component, | ||
CatchBoundary, | ||
ErrorBoundary | ||
} = routeModules[id]; | ||
let element = Component ? /*#__PURE__*/React.createElement(Component, null) : /*#__PURE__*/React.createElement(DefaultRouteComponent, { | ||
id: id | ||
}); | ||
let context = { | ||
data, | ||
id | ||
}; | ||
if (CatchBoundary) { | ||
// If we tried to render and failed, and this route threw the error, find it | ||
// and pass it to the ErrorBoundary to emulate `componentDidCatch` | ||
let maybeServerCaught = appState.catch && appState.catchBoundaryRouteId === id ? appState.catch : undefined; // This needs to run after we check for the error from a previous render, | ||
// otherwise we will incorrectly render this boundary for a loader error | ||
// deeper in the tree. | ||
if (appState.trackCatchBoundaries) { | ||
appState.catchBoundaryRouteId = id; | ||
} | ||
context = maybeServerCaught ? { | ||
id, | ||
get data() { | ||
console.error("You cannot `useLoaderData` in a catch boundary."); | ||
return undefined; | ||
} | ||
} : { | ||
id, | ||
data | ||
}; | ||
element = /*#__PURE__*/React.createElement(RemixCatchBoundary, { | ||
location: location, | ||
component: CatchBoundary, | ||
catch: maybeServerCaught | ||
}, element); | ||
} // Only wrap in error boundary if the route defined one, otherwise let the | ||
// error bubble to the parent boundary. We could default to using error | ||
// boundaries around every route, but now if the app doesn't want users | ||
// seeing the default Remix ErrorBoundary component, they *must* define an | ||
// error boundary for *every* route and that would be annoying. Might as | ||
// well make it required at that point. | ||
// | ||
// By conditionally wrapping like this, we allow apps to define a top level | ||
// ErrorBoundary component and be done with it. Then, if they want to, they | ||
// can add more specific boundaries by exporting ErrorBoundary components | ||
// for whichever routes they please. | ||
// | ||
// NOTE: this kind of logic will move into React Router | ||
if (ErrorBoundary) { | ||
// If we tried to render and failed, and this route threw the error, find it | ||
// and pass it to the ErrorBoundary to emulate `componentDidCatch` | ||
let maybeServerRenderError = appState.error && (appState.renderBoundaryRouteId === id || appState.loaderBoundaryRouteId === id) ? deserializeError(appState.error) : undefined; // This needs to run after we check for the error from a previous render, | ||
// otherwise we will incorrectly render this boundary for a loader error | ||
// deeper in the tree. | ||
if (appState.trackBoundaries) { | ||
appState.renderBoundaryRouteId = id; | ||
} | ||
context = maybeServerRenderError ? { | ||
id, | ||
get data() { | ||
console.error("You cannot `useLoaderData` in an error boundary."); | ||
return undefined; | ||
} | ||
} : { | ||
id, | ||
data | ||
}; | ||
element = /*#__PURE__*/React.createElement(RemixErrorBoundary, { | ||
location: location, | ||
component: ErrorBoundary, | ||
error: maybeServerRenderError | ||
}, element); | ||
} // It's important for the route context to be above the error boundary so that | ||
// a call to `useLoaderData` doesn't accidentally get the parents route's data. | ||
return /*#__PURE__*/React.createElement(RemixRouteContext.Provider, { | ||
value: context | ||
}, element); | ||
} //////////////////////////////////////////////////////////////////////////////// | ||
//////////////////////////////////////////////////////////////////////////////// | ||
// Public API | ||
/** | ||
* Defines the discovery behavior of the link: | ||
* | ||
* - "render": Eagerly discover when the link is rendered (default) | ||
* - "none": No eager discovery - discover when the link is clicked | ||
*/ | ||
/** | ||
* Defines the prefetching behavior of the link: | ||
* | ||
* - "none": Never fetched | ||
* - "intent": Fetched when the user focuses or hovers the link | ||
* - "render": Fetched when the link is rendered | ||
* - "none": Never fetched | ||
* - "viewport": Fetched when the link is in the viewport | ||
*/ | ||
@@ -296,2 +72,3 @@ | ||
} = theirElementProps; | ||
let ref = React.useRef(null); | ||
React.useEffect(() => { | ||
@@ -301,4 +78,17 @@ if (prefetch === "render") { | ||
} | ||
if (prefetch === "viewport") { | ||
let callback = entries => { | ||
entries.forEach(entry => { | ||
setShouldPrefetch(entry.isIntersecting); | ||
}); | ||
}; | ||
let observer = new IntersectionObserver(callback, { | ||
threshold: 0.5 | ||
}); | ||
if (ref.current) observer.observe(ref.current); | ||
return () => { | ||
observer.disconnect(); | ||
}; | ||
} | ||
}, [prefetch]); | ||
let setIntent = () => { | ||
@@ -309,3 +99,2 @@ if (prefetch === "intent") { | ||
}; | ||
let cancelIntent = () => { | ||
@@ -317,3 +106,2 @@ if (prefetch === "intent") { | ||
}; | ||
React.useEffect(() => { | ||
@@ -329,3 +117,3 @@ if (maybePrefetch) { | ||
}, [maybePrefetch]); | ||
return [shouldPrefetch, { | ||
return [shouldPrefetch, ref, { | ||
onFocus: composeEventHandlers(onFocus, setIntent), | ||
@@ -338,20 +126,26 @@ onBlur: composeEventHandlers(onBlur, cancelIntent), | ||
} | ||
const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i; | ||
function getDiscoverAttr(discover, isAbsolute, reloadDocument) { | ||
return discover === "render" && !isAbsolute && !reloadDocument ? "true" : undefined; | ||
} | ||
/** | ||
* A special kind of `<Link>` that knows whether or not it is "active". | ||
* A special kind of `<Link>` that knows whether it is "active". | ||
* | ||
* @see https://remix.run/api/remix#navlink | ||
* @see https://remix.run/components/nav-link | ||
*/ | ||
let NavLink = /*#__PURE__*/React.forwardRef(({ | ||
to, | ||
prefetch = "none", | ||
discover = "render", | ||
...props | ||
}, forwardedRef) => { | ||
let isAbsolute = typeof to === "string" && ABSOLUTE_URL_REGEX.test(to); | ||
let href = useHref(to); | ||
let [shouldPrefetch, prefetchHandlers] = usePrefetchBehavior(prefetch, props); | ||
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(NavLink$1, _extends({ | ||
ref: forwardedRef, | ||
to: to | ||
}, props, prefetchHandlers)), shouldPrefetch ? /*#__PURE__*/React.createElement(PrefetchPageLinks, { | ||
let [shouldPrefetch, ref, prefetchHandlers] = usePrefetchBehavior(prefetch, props); | ||
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(NavLink$1, _extends({}, props, prefetchHandlers, { | ||
ref: mergeRefs(forwardedRef, ref), | ||
to: to, | ||
"data-discover": getDiscoverAttr(discover, isAbsolute, props.reloadDocument) | ||
})), shouldPrefetch && !isAbsolute ? /*#__PURE__*/React.createElement(PrefetchPageLinks, { | ||
page: href | ||
@@ -361,2 +155,3 @@ }) : null); | ||
NavLink.displayName = "NavLink"; | ||
/** | ||
@@ -366,16 +161,18 @@ * This component renders an anchor tag and is the primary way the user will | ||
* | ||
* @see https://remix.run/api/remix#link | ||
* @see https://remix.run/components/link | ||
*/ | ||
let Link = /*#__PURE__*/React.forwardRef(({ | ||
to, | ||
prefetch = "none", | ||
discover = "render", | ||
...props | ||
}, forwardedRef) => { | ||
let isAbsolute = typeof to === "string" && ABSOLUTE_URL_REGEX.test(to); | ||
let href = useHref(to); | ||
let [shouldPrefetch, prefetchHandlers] = usePrefetchBehavior(prefetch, props); | ||
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Link$1, _extends({ | ||
ref: forwardedRef, | ||
to: to | ||
}, props, prefetchHandlers)), shouldPrefetch ? /*#__PURE__*/React.createElement(PrefetchPageLinks, { | ||
let [shouldPrefetch, ref, prefetchHandlers] = usePrefetchBehavior(prefetch, props); | ||
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Link$1, _extends({}, props, prefetchHandlers, { | ||
ref: mergeRefs(forwardedRef, ref), | ||
to: to, | ||
"data-discover": getDiscoverAttr(discover, isAbsolute, props.reloadDocument) | ||
})), shouldPrefetch && !isAbsolute ? /*#__PURE__*/React.createElement(PrefetchPageLinks, { | ||
page: href | ||
@@ -385,6 +182,22 @@ }) : null); | ||
Link.displayName = "Link"; | ||
/** | ||
* This component renders a form tag and is the primary way the user will | ||
* submit information via your website. | ||
* | ||
* @see https://remix.run/components/form | ||
*/ | ||
let Form = /*#__PURE__*/React.forwardRef(({ | ||
discover = "render", | ||
...props | ||
}, forwardedRef) => { | ||
let isAbsolute = typeof props.action === "string" && ABSOLUTE_URL_REGEX.test(props.action); | ||
return /*#__PURE__*/React.createElement(Form$1, _extends({}, props, { | ||
ref: forwardedRef, | ||
"data-discover": getDiscoverAttr(discover, isAbsolute, props.reloadDocument) | ||
})); | ||
}); | ||
Form.displayName = "Form"; | ||
function composeEventHandlers(theirHandler, ourHandler) { | ||
return event => { | ||
theirHandler && theirHandler(event); | ||
if (!event.defaultPrevented) { | ||
@@ -395,56 +208,53 @@ ourHandler(event); | ||
} | ||
// Return the matches actively being displayed: | ||
// - In SPA Mode we only SSR/hydrate the root match, and include all matches | ||
// after hydration. This lets the router handle initial match loads via lazy(). | ||
// - When an error boundary is rendered, we slice off matches up to the | ||
// boundary for <Links>/<Meta> | ||
function getActiveMatches(matches, errors, isSpaMode) { | ||
if (isSpaMode && !isHydrated) { | ||
return [matches[0]]; | ||
} | ||
if (errors) { | ||
let errorIdx = matches.findIndex(m => errors[m.route.id] !== undefined); | ||
return matches.slice(0, errorIdx + 1); | ||
} | ||
return matches; | ||
} | ||
/** | ||
* Renders the `<link>` tags for the current routes. | ||
* | ||
* @see https://remix.run/api/remix#meta-links-scripts | ||
* @see https://remix.run/components/links | ||
*/ | ||
function Links() { | ||
let { | ||
matches, | ||
isSpaMode, | ||
manifest, | ||
routeModules, | ||
manifest | ||
} = useRemixEntryContext(); | ||
let links = React.useMemo(() => getLinksForMatches(matches, routeModules, manifest), [matches, routeModules, manifest]); | ||
return /*#__PURE__*/React.createElement(React.Fragment, null, links.map(link => { | ||
if (isPageLinkDescriptor(link)) { | ||
return /*#__PURE__*/React.createElement(PrefetchPageLinks, _extends({ | ||
key: link.page | ||
}, link)); | ||
criticalCss | ||
} = useRemixContext(); | ||
let { | ||
errors, | ||
matches: routerMatches | ||
} = useDataRouterStateContext(); | ||
let matches = getActiveMatches(routerMatches, errors, isSpaMode); | ||
let keyedLinks = React.useMemo(() => getKeyedLinksForMatches(matches, routeModules, manifest), [matches, routeModules, manifest]); | ||
return /*#__PURE__*/React.createElement(React.Fragment, null, criticalCss ? /*#__PURE__*/React.createElement("style", { | ||
dangerouslySetInnerHTML: { | ||
__html: criticalCss | ||
} | ||
}) : null, keyedLinks.map(({ | ||
key, | ||
link | ||
}) => isPageLinkDescriptor(link) ? /*#__PURE__*/React.createElement(PrefetchPageLinks, _extends({ | ||
key: key | ||
}, link)) : /*#__PURE__*/React.createElement("link", _extends({ | ||
key: key | ||
}, link)))); | ||
} | ||
let imageSrcSet = null; // In React 17, <link imageSrcSet> and <link imageSizes> will warn | ||
// because the DOM attributes aren't recognized, so users need to pass | ||
// them in all lowercase to forward the attributes to the node without a | ||
// warning. Normalize so that either property can be used in Remix. | ||
if ("useId" in React) { | ||
if (link.imagesrcset) { | ||
link.imageSrcSet = imageSrcSet = link.imagesrcset; | ||
delete link.imagesrcset; | ||
} | ||
if (link.imagesizes) { | ||
link.imageSizes = link.imagesizes; | ||
delete link.imagesizes; | ||
} | ||
} else { | ||
if (link.imageSrcSet) { | ||
link.imagesrcset = imageSrcSet = link.imageSrcSet; | ||
delete link.imageSrcSet; | ||
} | ||
if (link.imageSizes) { | ||
link.imagesizes = link.imageSizes; | ||
delete link.imageSizes; | ||
} | ||
} | ||
return /*#__PURE__*/React.createElement("link", _extends({ | ||
key: link.rel + (link.href || "") + (imageSrcSet || "") | ||
}, link)); | ||
})); | ||
} | ||
/** | ||
* This component renders all of the `<link rel="prefetch">` and | ||
* This component renders all the `<link rel="prefetch">` and | ||
* `<link rel="modulepreload"/>` tags for all the assets (data, modules, css) of | ||
@@ -455,5 +265,4 @@ * a given page. | ||
* @param props.page | ||
* @see https://remix.run/api/remix#prefetchpagelinks- | ||
* @see https://remix.run/components/prefetch-page-links | ||
*/ | ||
function PrefetchPageLinks({ | ||
@@ -464,6 +273,5 @@ page, | ||
let { | ||
clientRoutes | ||
} = useRemixEntryContext(); | ||
let matches = React.useMemo(() => matchClientRoutes(clientRoutes, page), [clientRoutes, page]); | ||
router | ||
} = useDataRouterContext(); | ||
let matches = React.useMemo(() => matchRoutes(router.routes, page, router.basename), [router.routes, page, router.basename]); | ||
if (!matches) { | ||
@@ -473,3 +281,2 @@ console.warn(`Tried to prefetch ${page} but no routes matched.`); | ||
} | ||
return /*#__PURE__*/React.createElement(PrefetchPageLinksImpl, _extends({ | ||
@@ -480,12 +287,14 @@ page: page, | ||
} | ||
function usePrefetchedStylesheets(matches) { | ||
function useKeyedPrefetchLinks(matches) { | ||
let { | ||
manifest, | ||
routeModules | ||
} = useRemixEntryContext(); | ||
let [styleLinks, setStyleLinks] = React.useState([]); | ||
} = useRemixContext(); | ||
let [keyedPrefetchLinks, setKeyedPrefetchLinks] = React.useState([]); | ||
React.useEffect(() => { | ||
let interrupted = false; | ||
getStylesheetPrefetchLinks(matches, routeModules).then(links => { | ||
if (!interrupted) setStyleLinks(links); | ||
void getKeyedPrefetchLinks(matches, manifest, routeModules).then(links => { | ||
if (!interrupted) { | ||
setKeyedPrefetchLinks(links); | ||
} | ||
}); | ||
@@ -495,6 +304,5 @@ return () => { | ||
}; | ||
}, [matches, routeModules]); | ||
return styleLinks; | ||
}, [matches, manifest, routeModules]); | ||
return keyedPrefetchLinks; | ||
} | ||
function PrefetchPageLinksImpl({ | ||
@@ -507,12 +315,56 @@ page, | ||
let { | ||
matches, | ||
manifest | ||
} = useRemixEntryContext(); | ||
let newMatchesForData = React.useMemo(() => getNewMatchesForLinks(page, nextMatches, matches, location, "data"), [page, nextMatches, matches, location]); | ||
let newMatchesForAssets = React.useMemo(() => getNewMatchesForLinks(page, nextMatches, matches, location, "assets"), [page, nextMatches, matches, location]); | ||
let dataHrefs = React.useMemo(() => getDataLinkHrefs(page, newMatchesForData, manifest), [newMatchesForData, page, manifest]); | ||
let moduleHrefs = React.useMemo(() => getModuleLinkHrefs(newMatchesForAssets, manifest), [newMatchesForAssets, manifest]); // needs to be a hook with async behavior because we need the modules, not | ||
future, | ||
manifest, | ||
routeModules | ||
} = useRemixContext(); | ||
let { | ||
loaderData, | ||
matches | ||
} = useDataRouterStateContext(); | ||
let newMatchesForData = React.useMemo(() => getNewMatchesForLinks(page, nextMatches, matches, manifest, location, "data"), [page, nextMatches, matches, manifest, location]); | ||
let dataHrefs = React.useMemo(() => { | ||
if (!future.unstable_singleFetch) { | ||
return getDataLinkHrefs(page, newMatchesForData, manifest); | ||
} | ||
if (page === location.pathname + location.search + location.hash) { | ||
// Because we opt-into revalidation, don't compute this for the current page | ||
// since it would always trigger a prefetch of the existing loaders | ||
return []; | ||
} | ||
// Single-fetch is harder :) | ||
// This parallels the logic in the single fetch data strategy | ||
let routesParams = new Set(); | ||
let foundOptOutRoute = false; | ||
nextMatches.forEach(m => { | ||
var _routeModules$m$route; | ||
if (!manifest.routes[m.route.id].hasLoader) { | ||
return; | ||
} | ||
if (!newMatchesForData.some(m2 => m2.route.id === m.route.id) && m.route.id in loaderData && (_routeModules$m$route = routeModules[m.route.id]) !== null && _routeModules$m$route !== void 0 && _routeModules$m$route.shouldRevalidate) { | ||
foundOptOutRoute = true; | ||
} else if (manifest.routes[m.route.id].hasClientLoader) { | ||
foundOptOutRoute = true; | ||
} else { | ||
routesParams.add(m.route.id); | ||
} | ||
}); | ||
if (routesParams.size === 0) { | ||
return []; | ||
} | ||
let url = singleFetchUrl(page); | ||
// When one or more routes have opted out, we add a _routes param to | ||
// limit the loaders to those that have a server loader and did not | ||
// opt out | ||
if (foundOptOutRoute && routesParams.size > 0) { | ||
url.searchParams.set("_routes", nextMatches.filter(m => routesParams.has(m.route.id)).map(m => m.route.id).join(",")); | ||
} | ||
return [url.pathname + url.search]; | ||
}, [future.unstable_singleFetch, loaderData, location, manifest, newMatchesForData, nextMatches, page, routeModules]); | ||
let newMatchesForAssets = React.useMemo(() => getNewMatchesForLinks(page, nextMatches, matches, manifest, location, "assets"), [page, nextMatches, matches, manifest, location]); | ||
let moduleHrefs = React.useMemo(() => getModuleLinkHrefs(newMatchesForAssets, manifest), [newMatchesForAssets, manifest]); | ||
// needs to be a hook with async behavior because we need the modules, not | ||
// just the manifest like the other links in here. | ||
let styleLinks = usePrefetchedStylesheets(newMatchesForAssets); | ||
let keyedPrefetchLinks = useKeyedPrefetchLinks(newMatchesForAssets); | ||
return /*#__PURE__*/React.createElement(React.Fragment, null, dataHrefs.map(href => /*#__PURE__*/React.createElement("link", _extends({ | ||
@@ -527,3 +379,6 @@ key: href, | ||
href: href | ||
}, linkProps))), styleLinks.map(link => | ||
}, linkProps))), keyedPrefetchLinks.map(({ | ||
key, | ||
link | ||
}) => | ||
/*#__PURE__*/ | ||
@@ -533,85 +388,129 @@ // these don't spread `linkProps` because they are full link descriptors | ||
React.createElement("link", _extends({ | ||
key: link.href | ||
key: key | ||
}, link)))); | ||
} | ||
/** | ||
* Renders the `<title>` and `<meta>` tags for the current routes. | ||
* Renders HTML tags related to metadata for the current route. | ||
* | ||
* @see https://remix.run/api/remix#meta-links-scripts | ||
* @see https://remix.run/components/meta | ||
*/ | ||
function Meta() { | ||
let { | ||
matches, | ||
routeData, | ||
isSpaMode, | ||
routeModules | ||
} = useRemixEntryContext(); | ||
} = useRemixContext(); | ||
let { | ||
errors, | ||
matches: routerMatches, | ||
loaderData | ||
} = useDataRouterStateContext(); | ||
let location = useLocation(); | ||
let meta = {}; | ||
let parentsData = {}; | ||
for (let match of matches) { | ||
let routeId = match.route.id; | ||
let data = routeData[routeId]; | ||
let params = match.params; | ||
let _matches = getActiveMatches(routerMatches, errors, isSpaMode); | ||
let error = null; | ||
if (errors) { | ||
error = errors[_matches[_matches.length - 1].route.id]; | ||
} | ||
let meta = []; | ||
let leafMeta = null; | ||
let matches = []; | ||
for (let i = 0; i < _matches.length; i++) { | ||
let _match = _matches[i]; | ||
let routeId = _match.route.id; | ||
let data = loaderData[routeId]; | ||
let params = _match.params; | ||
let routeModule = routeModules[routeId]; | ||
if (routeModule.meta) { | ||
let routeMeta = typeof routeModule.meta === "function" ? routeModule.meta({ | ||
let routeMeta = []; | ||
let match = { | ||
id: routeId, | ||
data, | ||
meta: [], | ||
params: _match.params, | ||
pathname: _match.pathname, | ||
handle: _match.route.handle, | ||
error | ||
}; | ||
matches[i] = match; | ||
if (routeModule !== null && routeModule !== void 0 && routeModule.meta) { | ||
routeMeta = typeof routeModule.meta === "function" ? routeModule.meta({ | ||
data, | ||
parentsData, | ||
params, | ||
location | ||
}) : routeModule.meta; | ||
Object.assign(meta, routeMeta); | ||
location, | ||
matches, | ||
error | ||
}) : Array.isArray(routeModule.meta) ? [...routeModule.meta] : routeModule.meta; | ||
} else if (leafMeta) { | ||
// We only assign the route's meta to the nearest leaf if there is no meta | ||
// export in the route. The meta function may return a falsy value which | ||
// is effectively the same as an empty array. | ||
routeMeta = [...leafMeta]; | ||
} | ||
parentsData[routeId] = data; | ||
routeMeta = routeMeta || []; | ||
if (!Array.isArray(routeMeta)) { | ||
throw new Error("The route at " + _match.route.path + " returns an invalid value. All route meta functions must " + "return an array of meta objects." + "\n\nTo reference the meta function API, see https://remix.run/route/meta"); | ||
} | ||
match.meta = routeMeta; | ||
matches[i] = match; | ||
meta = [...routeMeta]; | ||
leafMeta = meta; | ||
} | ||
return /*#__PURE__*/React.createElement(React.Fragment, null, Object.entries(meta).map(([name, value]) => { | ||
if (!value) { | ||
return /*#__PURE__*/React.createElement(React.Fragment, null, meta.flat().map(metaProps => { | ||
if (!metaProps) { | ||
return null; | ||
} | ||
if (["charset", "charSet"].includes(name)) { | ||
return /*#__PURE__*/React.createElement("meta", { | ||
key: "charset", | ||
charSet: value | ||
}); | ||
if ("tagName" in metaProps) { | ||
let { | ||
tagName, | ||
...rest | ||
} = metaProps; | ||
if (!isValidMetaTag(tagName)) { | ||
console.warn(`A meta object uses an invalid tagName: ${tagName}. Expected either 'link' or 'meta'`); | ||
return null; | ||
} | ||
let Comp = tagName; | ||
return /*#__PURE__*/React.createElement(Comp, _extends({ | ||
key: JSON.stringify(rest) | ||
}, rest)); | ||
} | ||
if (name === "title") { | ||
if ("title" in metaProps) { | ||
return /*#__PURE__*/React.createElement("title", { | ||
key: "title" | ||
}, String(value)); | ||
} // Open Graph tags use the `property` attribute, while other meta tags | ||
// use `name`. See https://ogp.me/ | ||
let isOpenGraphTag = name.startsWith("og:"); | ||
return [value].flat().map(content => { | ||
if (isOpenGraphTag) { | ||
return /*#__PURE__*/React.createElement("meta", { | ||
property: name, | ||
content: content, | ||
key: name + content | ||
}, String(metaProps.title)); | ||
} | ||
if ("charset" in metaProps) { | ||
metaProps.charSet ??= metaProps.charset; | ||
delete metaProps.charset; | ||
} | ||
if ("charSet" in metaProps && metaProps.charSet != null) { | ||
return typeof metaProps.charSet === "string" ? /*#__PURE__*/React.createElement("meta", { | ||
key: "charSet", | ||
charSet: metaProps.charSet | ||
}) : null; | ||
} | ||
if ("script:ld+json" in metaProps) { | ||
try { | ||
let json = JSON.stringify(metaProps["script:ld+json"]); | ||
return /*#__PURE__*/React.createElement("script", { | ||
key: `script:ld+json:${json}`, | ||
type: "application/ld+json", | ||
dangerouslySetInnerHTML: { | ||
__html: json | ||
} | ||
}); | ||
} catch (err) { | ||
return null; | ||
} | ||
if (typeof content === "string") { | ||
return /*#__PURE__*/React.createElement("meta", { | ||
name: name, | ||
content: content, | ||
key: name + content | ||
}); | ||
} | ||
return /*#__PURE__*/React.createElement("meta", _extends({ | ||
key: name + JSON.stringify(content) | ||
}, content)); | ||
}); | ||
} | ||
return /*#__PURE__*/React.createElement("meta", _extends({ | ||
key: JSON.stringify(metaProps) | ||
}, metaProps)); | ||
})); | ||
} | ||
function isValidMetaTag(tagName) { | ||
return typeof tagName === "string" && /^(meta|link)$/.test(tagName); | ||
} | ||
function Await(props) { | ||
return /*#__PURE__*/React.createElement(Await$1, props); | ||
} | ||
/** | ||
@@ -621,5 +520,3 @@ * Tracks whether Remix has finished hydrating or not, so scripts can be skipped | ||
*/ | ||
let isHydrated = false; | ||
/** | ||
@@ -633,3 +530,3 @@ * Renders the `<script>` tags needed for the initial render. Bundles for | ||
* | ||
* @see https://remix.run/api/remix#meta-links-scripts | ||
* @see https://remix.run/components/scripts | ||
*/ | ||
@@ -639,14 +536,117 @@ function Scripts(props) { | ||
manifest, | ||
matches, | ||
pendingLocation, | ||
clientRoutes, | ||
serverHandoffString | ||
} = useRemixEntryContext(); | ||
serverHandoffString, | ||
abortDelay, | ||
serializeError, | ||
isSpaMode, | ||
future, | ||
renderMeta | ||
} = useRemixContext(); | ||
let { | ||
router, | ||
static: isStatic, | ||
staticContext | ||
} = useDataRouterContext(); | ||
let { | ||
matches: routerMatches | ||
} = useDataRouterStateContext(); | ||
let enableFogOfWar = isFogOfWarEnabled(future, isSpaMode); | ||
// Let <RemixServer> know that we hydrated and we should render the single | ||
// fetch streaming scripts | ||
if (renderMeta) { | ||
renderMeta.didRenderScripts = true; | ||
} | ||
let matches = getActiveMatches(routerMatches, null, isSpaMode); | ||
React.useEffect(() => { | ||
isHydrated = true; | ||
}, []); | ||
let serializePreResolvedErrorImp = (key, error) => { | ||
let toSerialize; | ||
if (serializeError && error instanceof Error) { | ||
toSerialize = serializeError(error); | ||
} else { | ||
toSerialize = error; | ||
} | ||
return `${JSON.stringify(key)}:__remixContext.p(!1, ${escapeHtml(JSON.stringify(toSerialize))})`; | ||
}; | ||
let serializePreresolvedDataImp = (routeId, key, data) => { | ||
let serializedData; | ||
try { | ||
serializedData = JSON.stringify(data); | ||
} catch (error) { | ||
return serializePreResolvedErrorImp(key, error); | ||
} | ||
return `${JSON.stringify(key)}:__remixContext.p(${escapeHtml(serializedData)})`; | ||
}; | ||
let serializeErrorImp = (routeId, key, error) => { | ||
let toSerialize; | ||
if (serializeError && error instanceof Error) { | ||
toSerialize = serializeError(error); | ||
} else { | ||
toSerialize = error; | ||
} | ||
return `__remixContext.r(${JSON.stringify(routeId)}, ${JSON.stringify(key)}, !1, ${escapeHtml(JSON.stringify(toSerialize))})`; | ||
}; | ||
let serializeDataImp = (routeId, key, data) => { | ||
let serializedData; | ||
try { | ||
serializedData = JSON.stringify(data); | ||
} catch (error) { | ||
return serializeErrorImp(routeId, key, error); | ||
} | ||
return `__remixContext.r(${JSON.stringify(routeId)}, ${JSON.stringify(key)}, ${escapeHtml(serializedData)})`; | ||
}; | ||
let deferredScripts = []; | ||
let initialScripts = React.useMemo(() => { | ||
let contextScript = serverHandoffString ? `window.__remixContext = ${serverHandoffString};` : ""; | ||
let routeModulesScript = `${matches.map((match, index) => `import ${JSON.stringify(manifest.url)}; | ||
import * as route${index} from ${JSON.stringify(manifest.routes[match.route.id].module)};`).join("\n")} | ||
var _manifest$hmr; | ||
let streamScript = future.unstable_singleFetch ? | ||
// prettier-ignore | ||
"window.__remixContext.stream = new ReadableStream({" + "start(controller){" + "window.__remixContext.streamController = controller;" + "}" + "}).pipeThrough(new TextEncoderStream());" : ""; | ||
let contextScript = staticContext ? `window.__remixContext = ${serverHandoffString};${streamScript}` : " "; | ||
// When single fetch is enabled, deferred is handled by turbo-stream | ||
let activeDeferreds = future.unstable_singleFetch ? undefined : staticContext === null || staticContext === void 0 ? void 0 : staticContext.activeDeferreds; | ||
// This sets up the __remixContext with utility functions used by the | ||
// deferred scripts. | ||
// - __remixContext.p is a function that takes a resolved value or error and returns a promise. | ||
// This is used for transmitting pre-resolved promises from the server to the client. | ||
// - __remixContext.n is a function that takes a routeID and key to returns a promise for later | ||
// resolution by the subsequently streamed chunks. | ||
// - __remixContext.r is a function that takes a routeID, key and value or error and resolves | ||
// the promise created by __remixContext.n. | ||
// - __remixContext.t is a map or routeId to keys to an object containing `e` and `r` methods | ||
// to resolve or reject the promise created by __remixContext.n. | ||
// - __remixContext.a is the active number of deferred scripts that should be rendered to match | ||
// the SSR tree for hydration on the client. | ||
contextScript += !activeDeferreds ? "" : ["__remixContext.p = function(v,e,p,x) {", " if (typeof e !== 'undefined') {", process.env.NODE_ENV === "development" ? " x=new Error(e.message);\n x.stack=e.stack;" : ' x=new Error("Unexpected Server Error");\n x.stack=undefined;', " p=Promise.reject(x);", " } else {", " p=Promise.resolve(v);", " }", " return p;", "};", "__remixContext.n = function(i,k) {", " __remixContext.t = __remixContext.t || {};", " __remixContext.t[i] = __remixContext.t[i] || {};", " let p = new Promise((r, e) => {__remixContext.t[i][k] = {r:(v)=>{r(v);},e:(v)=>{e(v);}};});", typeof abortDelay === "number" ? `setTimeout(() => {if(typeof p._error !== "undefined" || typeof p._data !== "undefined"){return;} __remixContext.t[i][k].e(new Error("Server timeout."))}, ${abortDelay});` : "", " return p;", "};", "__remixContext.r = function(i,k,v,e,p,x) {", " p = __remixContext.t[i][k];", " if (typeof e !== 'undefined') {", process.env.NODE_ENV === "development" ? " x=new Error(e.message);\n x.stack=e.stack;" : ' x=new Error("Unexpected Server Error");\n x.stack=undefined;', " p.e(x);", " } else {", " p.r(v);", " }", "};"].join("\n") + Object.entries(activeDeferreds).map(([routeId, deferredData]) => { | ||
let pendingKeys = new Set(deferredData.pendingKeys); | ||
let promiseKeyValues = deferredData.deferredKeys.map(key => { | ||
if (pendingKeys.has(key)) { | ||
deferredScripts.push( /*#__PURE__*/React.createElement(DeferredHydrationScript, { | ||
key: `${routeId} | ${key}`, | ||
deferredData: deferredData, | ||
routeId: routeId, | ||
dataKey: key, | ||
scriptProps: props, | ||
serializeData: serializeDataImp, | ||
serializeError: serializeErrorImp | ||
})); | ||
return `${JSON.stringify(key)}:__remixContext.n(${JSON.stringify(routeId)}, ${JSON.stringify(key)})`; | ||
} else { | ||
let trackedPromise = deferredData.data[key]; | ||
if (typeof trackedPromise._error !== "undefined") { | ||
return serializePreResolvedErrorImp(key, trackedPromise._error); | ||
} else { | ||
return serializePreresolvedDataImp(routeId, key, trackedPromise._data); | ||
} | ||
} | ||
}).join(",\n"); | ||
return `Object.assign(__remixContext.state.loaderData[${JSON.stringify(routeId)}], {${promiseKeyValues}});`; | ||
}).join("\n") + (deferredScripts.length > 0 ? `__remixContext.a=${deferredScripts.length};` : ""); | ||
let routeModulesScript = !isStatic ? " " : `${(_manifest$hmr = manifest.hmr) !== null && _manifest$hmr !== void 0 && _manifest$hmr.runtime ? `import ${JSON.stringify(manifest.hmr.runtime)};` : ""}${enableFogOfWar ? "" : `import ${JSON.stringify(manifest.url)}`}; | ||
${matches.map((match, index) => `import * as route${index} from ${JSON.stringify(manifest.routes[match.route.id].module)};`).join("\n")} | ||
${enableFogOfWar ? | ||
// Inline a minimal manifest with the SSR matches | ||
`window.__remixManifest = ${JSON.stringify(getPartialManifest(manifest, router), null, 2)};` : ""} | ||
window.__remixRouteModules = {${matches.map((match, index) => `${JSON.stringify(match.route.id)}:route${index}`).join(",")}}; | ||
@@ -660,28 +660,33 @@ | ||
})), /*#__PURE__*/React.createElement("script", _extends({}, props, { | ||
suppressHydrationWarning: true, | ||
dangerouslySetInnerHTML: createHtml(routeModulesScript), | ||
type: "module", | ||
async: true | ||
}))); // disabled deps array because we are purposefully only rendering this once | ||
}))); | ||
// disabled deps array because we are purposefully only rendering this once | ||
// for hydration, after that we want to just continue rendering the initial | ||
// scripts as they were when the page first loaded | ||
// eslint-disable-next-line | ||
}, []); // avoid waterfall when importing the next route module | ||
let nextMatches = React.useMemo(() => { | ||
if (pendingLocation) { | ||
// FIXME: can probably use transitionManager `nextMatches` | ||
let matches = matchClientRoutes(clientRoutes, pendingLocation); | ||
invariant(matches, `No routes match path "${pendingLocation.pathname}"`); | ||
return matches; | ||
}, []); | ||
if (!isStatic && typeof __remixContext === "object" && __remixContext.a) { | ||
for (let i = 0; i < __remixContext.a; i++) { | ||
deferredScripts.push( /*#__PURE__*/React.createElement(DeferredHydrationScript, { | ||
key: i, | ||
scriptProps: props, | ||
serializeData: serializeDataImp, | ||
serializeError: serializeErrorImp | ||
})); | ||
} | ||
return []; | ||
}, [pendingLocation, clientRoutes]); | ||
let routePreloads = matches.concat(nextMatches).map(match => { | ||
} | ||
let routePreloads = matches.map(match => { | ||
let route = manifest.routes[match.route.id]; | ||
return (route.imports || []).concat([route.module]); | ||
}).flat(1); | ||
let preloads = manifest.entry.imports.concat(routePreloads); | ||
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("link", { | ||
let preloads = isHydrated ? [] : manifest.entry.imports.concat(routePreloads); | ||
return isHydrated ? null : /*#__PURE__*/React.createElement(React.Fragment, null, !enableFogOfWar ? /*#__PURE__*/React.createElement("link", { | ||
rel: "modulepreload", | ||
href: manifest.url, | ||
crossOrigin: props.crossOrigin | ||
}) : null, /*#__PURE__*/React.createElement("link", { | ||
rel: "modulepreload", | ||
href: manifest.entry.module, | ||
@@ -694,362 +699,106 @@ crossOrigin: props.crossOrigin | ||
crossOrigin: props.crossOrigin | ||
})), isHydrated ? null : initialScripts); | ||
})), initialScripts, deferredScripts); | ||
} | ||
function dedupe(array) { | ||
return [...new Set(array)]; | ||
} | ||
/** | ||
* A Remix-aware `<form>`. It behaves like a normal form except that the | ||
* interaction with the server is with `fetch` instead of new document | ||
* requests, allowing components to add nicer UX to the page as the form is | ||
* submitted and returns with data. | ||
* | ||
* @see https://remix.run/api/remix#form | ||
*/ | ||
let Form = /*#__PURE__*/React.forwardRef((props, ref) => { | ||
return /*#__PURE__*/React.createElement(FormImpl, _extends({}, props, { | ||
ref: ref | ||
})); | ||
}); | ||
Form.displayName = "Form"; | ||
let FormImpl = /*#__PURE__*/React.forwardRef(({ | ||
reloadDocument = false, | ||
replace = false, | ||
method = "get", | ||
action, | ||
encType = "application/x-www-form-urlencoded", | ||
fetchKey, | ||
onSubmit, | ||
...props | ||
}, forwardedRef) => { | ||
let submit = useSubmitImpl(fetchKey); | ||
let formMethod = method.toLowerCase() === "get" ? "get" : "post"; | ||
let formAction = useFormAction(action); | ||
return /*#__PURE__*/React.createElement("form", _extends({ | ||
ref: forwardedRef, | ||
method: formMethod, | ||
action: formAction, | ||
encType: encType, | ||
onSubmit: reloadDocument ? undefined : event => { | ||
onSubmit && onSubmit(event); | ||
if (event.defaultPrevented) return; | ||
event.preventDefault(); | ||
let submitter = event.nativeEvent.submitter; | ||
submit(submitter || event.currentTarget, { | ||
method, | ||
replace | ||
}); | ||
} | ||
}, props)); | ||
}); | ||
FormImpl.displayName = "FormImpl"; | ||
/** | ||
* Resolves a `<form action>` path relative to the current route. | ||
* | ||
* @see https://remix.run/api/remix#useformaction | ||
*/ | ||
function useFormAction(action, // TODO: Remove method param in v2 as it's no longer needed and is a breaking change | ||
method = "get") { | ||
let { | ||
id | ||
} = useRemixRouteContext(); | ||
let resolvedPath = useResolvedPath(action ?? "."); // Previously we set the default action to ".". The problem with this is that | ||
// `useResolvedPath(".")` excludes search params and the hash of the resolved | ||
// URL. This is the intended behavior of when "." is specifically provided as | ||
// the form action, but inconsistent w/ browsers when the action is omitted. | ||
// https://github.com/remix-run/remix/issues/927 | ||
let location = useLocation(); | ||
let { | ||
search, | ||
hash | ||
} = resolvedPath; | ||
let isIndexRoute = id.endsWith("/index"); | ||
if (action == null) { | ||
search = location.search; | ||
hash = location.hash; // When grabbing search params from the URL, remove the automatically | ||
// inserted ?index param so we match the useResolvedPath search behavior | ||
// which would not include ?index | ||
if (isIndexRoute) { | ||
let params = new URLSearchParams(search); | ||
params.delete("index"); | ||
search = params.toString() ? `?${params.toString()}` : ""; | ||
} | ||
function DeferredHydrationScript({ | ||
dataKey, | ||
deferredData, | ||
routeId, | ||
scriptProps, | ||
serializeData, | ||
serializeError | ||
}) { | ||
if (typeof document === "undefined" && deferredData && dataKey && routeId) { | ||
invariant(deferredData.pendingKeys.includes(dataKey), `Deferred data for route ${routeId} with key ${dataKey} was not pending but tried to render a script for it.`); | ||
} | ||
if ((action == null || action === ".") && isIndexRoute) { | ||
search = search ? search.replace(/^\?/, "?index&") : "?index"; | ||
} | ||
return createPath({ | ||
pathname: resolvedPath.pathname, | ||
search, | ||
hash | ||
}); | ||
} | ||
/** | ||
* Returns a function that may be used to programmatically submit a form (or | ||
* some arbitrary data) to the server. | ||
* | ||
* @see https://remix.run/api/remix#usesubmit | ||
*/ | ||
function useSubmit() { | ||
return useSubmitImpl(); | ||
} | ||
let defaultMethod = "get"; | ||
let defaultEncType = "application/x-www-form-urlencoded"; | ||
function useSubmitImpl(key) { | ||
let navigate = useNavigate(); | ||
let defaultAction = useFormAction(); | ||
let { | ||
transitionManager | ||
} = useRemixEntryContext(); | ||
return React.useCallback((target, options = {}) => { | ||
let method; | ||
let action; | ||
let encType; | ||
let formData; | ||
if (isFormElement(target)) { | ||
let submissionTrigger = options.submissionTrigger; | ||
method = options.method || target.getAttribute("method") || defaultMethod; | ||
action = options.action || target.getAttribute("action") || defaultAction; | ||
encType = options.encType || target.getAttribute("enctype") || defaultEncType; | ||
formData = new FormData(target); | ||
if (submissionTrigger && submissionTrigger.name) { | ||
formData.append(submissionTrigger.name, submissionTrigger.value); | ||
return /*#__PURE__*/React.createElement(React.Suspense, { | ||
fallback: | ||
// This makes absolutely no sense. The server renders null as a fallback, | ||
// but when hydrating, we need to render a script tag to avoid a hydration issue. | ||
// To reproduce a hydration mismatch, just render null as a fallback. | ||
typeof document === "undefined" && deferredData && dataKey && routeId ? null : /*#__PURE__*/React.createElement("script", _extends({}, scriptProps, { | ||
async: true, | ||
suppressHydrationWarning: true, | ||
dangerouslySetInnerHTML: { | ||
__html: " " | ||
} | ||
} else if (isButtonElement(target) || isInputElement(target) && (target.type === "submit" || target.type === "image")) { | ||
let form = target.form; | ||
if (form == null) { | ||
throw new Error(`Cannot submit a <button> without a <form>`); | ||
} // <button>/<input type="submit"> may override attributes of <form> | ||
method = options.method || target.getAttribute("formmethod") || form.getAttribute("method") || defaultMethod; | ||
action = options.action || target.getAttribute("formaction") || form.getAttribute("action") || defaultAction; | ||
encType = options.encType || target.getAttribute("formenctype") || form.getAttribute("enctype") || defaultEncType; | ||
formData = new FormData(form); // Include name + value from a <button> | ||
if (target.name) { | ||
formData.append(target.name, target.value); | ||
} | ||
} else { | ||
if (isHtmlElement(target)) { | ||
throw new Error(`Cannot submit element that is not <form>, <button>, or ` + `<input type="submit|image">`); | ||
} | ||
method = options.method || "get"; | ||
action = options.action || defaultAction; | ||
encType = options.encType || "application/x-www-form-urlencoded"; | ||
if (target instanceof FormData) { | ||
formData = target; | ||
} else { | ||
formData = new FormData(); | ||
if (target instanceof URLSearchParams) { | ||
for (let [name, value] of target) { | ||
formData.append(name, value); | ||
} | ||
} else if (target != null) { | ||
for (let name of Object.keys(target)) { | ||
formData.append(name, target[name]); | ||
} | ||
})) | ||
}, typeof document === "undefined" && deferredData && dataKey && routeId ? /*#__PURE__*/React.createElement(Await, { | ||
resolve: deferredData.data[dataKey], | ||
errorElement: /*#__PURE__*/React.createElement(ErrorDeferredHydrationScript, { | ||
dataKey: dataKey, | ||
routeId: routeId, | ||
scriptProps: scriptProps, | ||
serializeError: serializeError | ||
}), | ||
children: data => { | ||
return /*#__PURE__*/React.createElement("script", _extends({}, scriptProps, { | ||
async: true, | ||
suppressHydrationWarning: true, | ||
dangerouslySetInnerHTML: { | ||
__html: serializeData(routeId, dataKey, data) | ||
} | ||
} | ||
})); | ||
} | ||
if (typeof document === "undefined") { | ||
throw new Error("You are calling submit during the server render. " + "Try calling submit within a `useEffect` or callback instead."); | ||
}) : /*#__PURE__*/React.createElement("script", _extends({}, scriptProps, { | ||
async: true, | ||
suppressHydrationWarning: true, | ||
dangerouslySetInnerHTML: { | ||
__html: " " | ||
} | ||
let { | ||
protocol, | ||
host | ||
} = window.location; | ||
let url = new URL(action, `${protocol}//${host}`); | ||
if (method.toLowerCase() === "get") { | ||
// Start with a fresh set of params and wipe out the old params to | ||
// match default browser behavior | ||
let params = new URLSearchParams(); | ||
let hasParams = false; | ||
for (let [name, value] of formData) { | ||
if (typeof value === "string") { | ||
hasParams = true; | ||
params.append(name, value); | ||
} else { | ||
throw new Error(`Cannot submit binary form data using GET`); | ||
} | ||
} | ||
url.search = hasParams ? `?${params.toString()}` : ""; | ||
}))); | ||
} | ||
function ErrorDeferredHydrationScript({ | ||
dataKey, | ||
routeId, | ||
scriptProps, | ||
serializeError | ||
}) { | ||
let error = useAsyncError(); | ||
return /*#__PURE__*/React.createElement("script", _extends({}, scriptProps, { | ||
suppressHydrationWarning: true, | ||
dangerouslySetInnerHTML: { | ||
__html: serializeError(routeId, dataKey, error) | ||
} | ||
let submission = { | ||
formData, | ||
action: url.pathname + url.search, | ||
method: method.toUpperCase(), | ||
encType, | ||
key: Math.random().toString(36).substr(2, 8) | ||
}; | ||
if (key) { | ||
transitionManager.send({ | ||
type: "fetcher", | ||
href: submission.action, | ||
submission, | ||
key | ||
}); | ||
} else { | ||
setNextNavigationSubmission(submission); | ||
navigate(url.pathname + url.search, { | ||
replace: options.replace | ||
}); | ||
} | ||
}, [defaultAction, key, navigate, transitionManager]); | ||
})); | ||
} | ||
let nextNavigationSubmission; | ||
function setNextNavigationSubmission(submission) { | ||
nextNavigationSubmission = submission; | ||
function dedupe(array) { | ||
return [...new Set(array)]; | ||
} | ||
function consumeNextNavigationSubmission() { | ||
let submission = nextNavigationSubmission; | ||
nextNavigationSubmission = undefined; | ||
return submission; | ||
} | ||
function isHtmlElement(object) { | ||
return object != null && typeof object.tagName === "string"; | ||
} | ||
function isButtonElement(object) { | ||
return isHtmlElement(object) && object.tagName.toLowerCase() === "button"; | ||
} | ||
function isFormElement(object) { | ||
return isHtmlElement(object) && object.tagName.toLowerCase() === "form"; | ||
} | ||
function isInputElement(object) { | ||
return isHtmlElement(object) && object.tagName.toLowerCase() === "input"; | ||
} | ||
/** | ||
* Setup a callback to be fired on the window's `beforeunload` event. This is | ||
* useful for saving some data to `window.localStorage` just before the page | ||
* refreshes, which automatically happens on the next `<Link>` click when Remix | ||
* detects a new version of the app is available on the server. | ||
* Returns the active route matches, useful for accessing loaderData for | ||
* parent/child routes or the route "handle" property | ||
* | ||
* Note: The `callback` argument should be a function created with | ||
* `React.useCallback()`. | ||
* | ||
* @see https://remix.run/api/remix#usebeforeunload | ||
* @see https://remix.run/hooks/use-matches | ||
*/ | ||
function useBeforeUnload(callback) { | ||
React.useEffect(() => { | ||
window.addEventListener("beforeunload", callback); | ||
return () => { | ||
window.removeEventListener("beforeunload", callback); | ||
}; | ||
}, [callback]); | ||
function useMatches() { | ||
return useMatches$1(); | ||
} | ||
/** | ||
* Returns the current route matches on the page. This is useful for creating | ||
* layout abstractions with your current routes. | ||
* Returns the JSON parsed data from the current route's `loader`. | ||
* | ||
* @see https://remix.run/api/remix#usematches | ||
* @see https://remix.run/hooks/use-loader-data | ||
*/ | ||
function useMatches() { | ||
let { | ||
matches, | ||
routeData, | ||
routeModules | ||
} = useRemixEntryContext(); | ||
return React.useMemo(() => matches.map(match => { | ||
var _routeModules$match$r; | ||
function useLoaderData() { | ||
return useLoaderData$1(); | ||
} | ||
let { | ||
pathname, | ||
params | ||
} = match; | ||
return { | ||
id: match.route.id, | ||
pathname, | ||
params, | ||
data: routeData[match.route.id], | ||
// if the module fails to load or an error/response is thrown, the module | ||
// won't be defined. | ||
handle: (_routeModules$match$r = routeModules[match.route.id]) === null || _routeModules$match$r === void 0 ? void 0 : _routeModules$match$r.handle | ||
}; | ||
}), [matches, routeData, routeModules]); | ||
} | ||
/** | ||
* Returns the JSON parsed data from the current route's `loader`. | ||
* Returns the loaderData for the given routeId. | ||
* | ||
* @see https://remix.run/api/remix#useloaderdata | ||
* @see https://remix.run/hooks/use-route-loader-data | ||
*/ | ||
function useRouteLoaderData(routeId) { | ||
return useRouteLoaderData$1(routeId); | ||
} | ||
function useLoaderData() { | ||
return useRemixRouteContext().data; | ||
} | ||
/** | ||
* Returns the JSON parsed data from the current route's `action`. | ||
* | ||
* @see https://remix.run/api/remix#useactiondata | ||
* @see https://remix.run/hooks/use-action-data | ||
*/ | ||
function useActionData() { | ||
let { | ||
id: routeId | ||
} = useRemixRouteContext(); | ||
let { | ||
transitionManager | ||
} = useRemixEntryContext(); | ||
let { | ||
actionData | ||
} = transitionManager.getState(); | ||
return actionData ? actionData[routeId] : undefined; | ||
return useActionData$1(); | ||
} | ||
/** | ||
* Returns everything you need to know about a page transition to build pending | ||
* navigation indicators and optimistic UI on data mutations. | ||
* | ||
* @see https://remix.run/api/remix#usetransition | ||
*/ | ||
function useTransition() { | ||
let { | ||
transitionManager | ||
} = useRemixEntryContext(); | ||
return transitionManager.getState().transition; | ||
} | ||
function createFetcherForm(fetchKey) { | ||
let FetcherForm = /*#__PURE__*/React.forwardRef((props, ref) => { | ||
// TODO: make ANOTHER form w/o a fetchKey prop | ||
return /*#__PURE__*/React.createElement(FormImpl, _extends({}, props, { | ||
ref: ref, | ||
fetchKey: fetchKey | ||
})); | ||
}); | ||
FetcherForm.displayName = "fetcher.Form"; | ||
return FetcherForm; | ||
} | ||
let fetcherId = 0; | ||
/** | ||
@@ -1059,57 +808,31 @@ * Interacts with route loaders and actions without causing a navigation. Great | ||
* | ||
* @see https://remix.run/api/remix#usefetcher | ||
* @see https://remix.run/hooks/use-fetcher | ||
*/ | ||
function useFetcher() { | ||
let { | ||
transitionManager | ||
} = useRemixEntryContext(); | ||
let [key] = React.useState(() => String(++fetcherId)); | ||
let [Form] = React.useState(() => createFetcherForm(key)); | ||
let [load] = React.useState(() => href => { | ||
transitionManager.send({ | ||
type: "fetcher", | ||
href, | ||
key | ||
}); | ||
}); | ||
let submit = useSubmitImpl(key); | ||
let fetcher = transitionManager.getFetcher(key); | ||
let fetcherWithComponents = React.useMemo(() => ({ | ||
Form, | ||
submit, | ||
load, | ||
...fetcher | ||
}), [fetcher, Form, submit, load]); | ||
React.useEffect(() => { | ||
// Is this busted when the React team gets real weird and calls effects | ||
// twice on mount? We really just need to garbage collect here when this | ||
// fetcher is no longer around. | ||
return () => transitionManager.deleteFetcher(key); | ||
}, [transitionManager, key]); | ||
return fetcherWithComponents; | ||
function useFetcher(opts = {}) { | ||
return useFetcher$1(opts); | ||
} | ||
/** | ||
* Provides all fetchers currently on the page. Useful for layouts and parent | ||
* routes that need to provide pending/optimistic UI regarding the fetch. | ||
* This component connects your app to the Remix asset server and | ||
* automatically reloads the page when files change in development. | ||
* In production, it renders null, so you can safely render it always in your root route. | ||
* | ||
* @see https://remix.run/api/remix#usefetchers | ||
* @see https://remix.run/docs/components/live-reload | ||
*/ | ||
function useFetchers() { | ||
let { | ||
transitionManager | ||
} = useRemixEntryContext(); | ||
let { | ||
fetchers | ||
} = transitionManager.getState(); | ||
return [...fetchers.values()]; | ||
} // Dead Code Elimination magic for production builds. | ||
const LiveReload = | ||
// Dead Code Elimination magic for production builds. | ||
// This way devs don't have to worry about doing the NODE_ENV check themselves. | ||
// If running an un-bundled server outside of `remix dev` you will still need | ||
// to set the REMIX_DEV_SERVER_WS_PORT manually. | ||
const LiveReload = process.env.NODE_ENV !== "development" ? () => null : function LiveReload({ | ||
port = Number(process.env.REMIX_DEV_SERVER_WS_PORT || 8002), | ||
process.env.NODE_ENV !== "development" ? () => null : function LiveReload({ | ||
origin, | ||
port, | ||
timeoutMs = 1000, | ||
nonce = undefined | ||
}) { | ||
// @ts-expect-error | ||
let isViteClient = import.meta && import.meta.env !== undefined; | ||
if (isViteClient) { | ||
console.warn(["`<LiveReload />` is obsolete when using Vite and can conflict with Vite's built-in HMR runtime.", "", "Remove `<LiveReload />` from your code and instead only use `<Scripts />`.", "Then refresh the page to remove lingering scripts from `<LiveReload />`."].join("\n")); | ||
return null; | ||
} | ||
origin ??= process.env.REMIX_DEV_ORIGIN; | ||
let js = String.raw; | ||
@@ -1122,8 +845,15 @@ return /*#__PURE__*/React.createElement("script", { | ||
function remixLiveReloadConnect(config) { | ||
let protocol = location.protocol === "https:" ? "wss:" : "ws:"; | ||
let host = location.hostname; | ||
let socketPath = protocol + "//" + host + ":" + ${String(port)} + "/socket"; | ||
let LIVE_RELOAD_ORIGIN = ${JSON.stringify(origin)}; | ||
let protocol = | ||
LIVE_RELOAD_ORIGIN ? new URL(LIVE_RELOAD_ORIGIN).protocol.replace(/^http/, "ws") : | ||
location.protocol === "https:" ? "wss:" : "ws:"; // remove in v2? | ||
let hostname = LIVE_RELOAD_ORIGIN ? new URL(LIVE_RELOAD_ORIGIN).hostname : location.hostname; | ||
let url = new URL(protocol + "//" + hostname + "/socket"); | ||
let ws = new WebSocket(socketPath); | ||
ws.onmessage = (message) => { | ||
url.port = | ||
${port} || | ||
(LIVE_RELOAD_ORIGIN ? new URL(LIVE_RELOAD_ORIGIN).port : 8002); | ||
let ws = new WebSocket(url.href); | ||
ws.onmessage = async (message) => { | ||
let event = JSON.parse(message.data); | ||
@@ -1137,2 +867,42 @@ if (event.type === "LOG") { | ||
} | ||
if (event.type === "HMR") { | ||
if (!window.__hmr__ || !window.__hmr__.contexts) { | ||
console.log("💿 [HMR] No HMR context, reloading window ..."); | ||
window.location.reload(); | ||
return; | ||
} | ||
if (!event.updates || !event.updates.length) return; | ||
let updateAccepted = false; | ||
let needsRevalidation = new Set(); | ||
for (let update of event.updates) { | ||
console.log("[HMR] " + update.reason + " [" + update.id +"]") | ||
if (update.revalidate) { | ||
needsRevalidation.add(update.routeId); | ||
console.log("[HMR] Revalidating [" + update.routeId + "]"); | ||
} | ||
let imported = await import(update.url + '?t=' + event.assetsManifest.hmr.timestamp); | ||
if (window.__hmr__.contexts[update.id]) { | ||
let accepted = window.__hmr__.contexts[update.id].emit( | ||
imported | ||
); | ||
if (accepted) { | ||
console.log("[HMR] Update accepted by", update.id); | ||
updateAccepted = true; | ||
} | ||
} | ||
} | ||
if (event.assetsManifest && window.__hmr__.contexts["remix:manifest"]) { | ||
let accepted = window.__hmr__.contexts["remix:manifest"].emit( | ||
{ needsRevalidation, assetsManifest: event.assetsManifest } | ||
); | ||
if (accepted) { | ||
console.log("[HMR] Update accepted by", "remix:manifest"); | ||
updateAccepted = true; | ||
} | ||
} | ||
if (!updateAccepted) { | ||
console.log("[HMR] Update rejected, reloading..."); | ||
window.location.reload(); | ||
} | ||
} | ||
}; | ||
@@ -1144,11 +914,13 @@ ws.onopen = () => { | ||
}; | ||
ws.onclose = (error) => { | ||
console.log("Remix dev asset server web socket closed. Reconnecting..."); | ||
setTimeout( | ||
() => | ||
remixLiveReloadConnect({ | ||
onOpen: () => window.location.reload(), | ||
}), | ||
1000 | ||
); | ||
ws.onclose = (event) => { | ||
if (event.code === 1006) { | ||
console.log("Remix dev asset server web socket closed. Reconnecting..."); | ||
setTimeout( | ||
() => | ||
remixLiveReloadConnect({ | ||
onOpen: () => window.location.reload(), | ||
}), | ||
${String(timeoutMs)} | ||
); | ||
} | ||
}; | ||
@@ -1165,3 +937,14 @@ ws.onerror = (error) => { | ||
}; | ||
function mergeRefs(...refs) { | ||
return value => { | ||
refs.forEach(ref => { | ||
if (typeof ref === "function") { | ||
ref(value); | ||
} else if (ref != null) { | ||
ref.current = value; | ||
} | ||
}); | ||
}; | ||
} | ||
export { Form, FormImpl, Link, Links, LiveReload, Meta, NavLink, PrefetchPageLinks, RemixEntry, RemixEntryContext, RemixRoute, Scripts, composeEventHandlers, useActionData, useBeforeUnload, useFetcher, useFetchers, useFormAction, useLoaderData, useMatches, useSubmit, useSubmitImpl, useTransition }; | ||
export { Await, Form, Link, Links, LiveReload, Meta, NavLink, PrefetchPageLinks, RemixContext, Scripts, composeEventHandlers, useActionData, useFetcher, useLoaderData, useMatches, useRemixContext, useRouteLoaderData }; |
/** | ||
* @remix-run/react v0.0.0-nightly-81ec1b7-20220830 | ||
* @remix-run/react v0.0.0-nightly-825f70ebd-20240914 | ||
* | ||
@@ -11,21 +11,56 @@ * Copyright (c) Remix Software Inc. | ||
*/ | ||
import invariant from './invariant.js'; | ||
import { AbortedDeferredError, UNSAFE_DeferredData } from '@remix-run/router'; | ||
/** | ||
* Data for a route that was returned from a `loader()`. | ||
*/ | ||
function isCatchResponse(response) { | ||
return response instanceof Response && response.headers.get("X-Remix-Catch") != null; | ||
return response.headers.get("X-Remix-Catch") != null; | ||
} | ||
function isErrorResponse(response) { | ||
return response instanceof Response && response.headers.get("X-Remix-Error") != null; | ||
return response.headers.get("X-Remix-Error") != null; | ||
} | ||
function isNetworkErrorResponse(response) { | ||
// If we reach the Remix server, we can safely identify response types via the | ||
// X-Remix-Error/X-Remix-Catch headers. However, if we never reach the Remix | ||
// server, and instead receive a 4xx/5xx from somewhere in between (like | ||
// Cloudflare), then we get a false negative in the isErrorResponse check and | ||
// we incorrectly assume that the user returns the 4xx/5xx response and | ||
// consider it successful. To alleviate this, we add X-Remix-Response to any | ||
// non-Error/non-Catch responses coming back from the server. If we don't | ||
// see this, we can conclude that a 4xx/5xx response never actually reached | ||
// the Remix server and we can bubble it up as an error. | ||
return isResponse(response) && response.status >= 400 && response.headers.get("X-Remix-Error") == null && response.headers.get("X-Remix-Catch") == null && response.headers.get("X-Remix-Response") == null; | ||
} | ||
function isRedirectResponse(response) { | ||
return response instanceof Response && response.headers.get("X-Remix-Redirect") != null; | ||
return response.headers.get("X-Remix-Redirect") != null; | ||
} | ||
async function fetchData(url, routeId, signal, submission) { | ||
function isDeferredResponse(response) { | ||
var _response$headers$get; | ||
return !!((_response$headers$get = response.headers.get("Content-Type")) !== null && _response$headers$get !== void 0 && _response$headers$get.match(/text\/remix-deferred/)); | ||
} | ||
function isResponse(value) { | ||
return value != null && typeof value.status === "number" && typeof value.statusText === "string" && typeof value.headers === "object" && typeof value.body !== "undefined"; | ||
} | ||
function isDeferredData(value) { | ||
let deferred = value; | ||
return deferred && typeof deferred === "object" && typeof deferred.data === "object" && typeof deferred.subscribe === "function" && typeof deferred.cancel === "function" && typeof deferred.resolveData === "function"; | ||
} | ||
async function fetchData(request, routeId, retry = 0) { | ||
let url = new URL(request.url); | ||
url.searchParams.set("_data", routeId); | ||
let init = submission ? getActionInit(submission, signal) : { | ||
credentials: "same-origin", | ||
signal | ||
}; | ||
let response = await fetch(url.href, init); | ||
if (retry > 0) { | ||
// Retry up to 3 times waiting 50, 250, 1250 ms | ||
// between retries for a total of 1550 ms before giving up. | ||
await new Promise(resolve => setTimeout(resolve, 5 ** retry * 10)); | ||
} | ||
let init = await createRequestInit(request); | ||
let revalidation = window.__remixRevalidation; | ||
let response = await fetch(url.href, init).catch(error => { | ||
if (typeof revalidation === "number" && revalidation === window.__remixRevalidation && (error === null || error === void 0 ? void 0 : error.name) === "TypeError" && retry < 3) { | ||
return fetchData(request, routeId, retry + 1); | ||
} | ||
throw error; | ||
}); | ||
if (isErrorResponse(response)) { | ||
@@ -37,48 +72,198 @@ let data = await response.json(); | ||
} | ||
if (isNetworkErrorResponse(response)) { | ||
let text = await response.text(); | ||
let error = new Error(text); | ||
error.stack = undefined; | ||
return error; | ||
} | ||
return response; | ||
} | ||
async function extractData(response) { | ||
// This same algorithm is used on the server to interpret load | ||
// results when we render the HTML page. | ||
let contentType = response.headers.get("Content-Type"); | ||
async function createRequestInit(request) { | ||
let init = { | ||
signal: request.signal | ||
}; | ||
if (request.method !== "GET") { | ||
init.method = request.method; | ||
let contentType = request.headers.get("Content-Type"); | ||
if (contentType && /\bapplication\/json\b/.test(contentType)) { | ||
return response.json(); | ||
// Check between word boundaries instead of startsWith() due to the last | ||
// paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type | ||
if (contentType && /\bapplication\/json\b/.test(contentType)) { | ||
init.headers = { | ||
"Content-Type": contentType | ||
}; | ||
init.body = JSON.stringify(await request.json()); | ||
} else if (contentType && /\btext\/plain\b/.test(contentType)) { | ||
init.headers = { | ||
"Content-Type": contentType | ||
}; | ||
init.body = await request.text(); | ||
} else if (contentType && /\bapplication\/x-www-form-urlencoded\b/.test(contentType)) { | ||
init.body = new URLSearchParams(await request.text()); | ||
} else { | ||
init.body = await request.formData(); | ||
} | ||
} | ||
return init; | ||
} | ||
const DEFERRED_VALUE_PLACEHOLDER_PREFIX = "__deferred_promise:"; | ||
async function parseDeferredReadableStream(stream) { | ||
if (!stream) { | ||
throw new Error("parseDeferredReadableStream requires stream argument"); | ||
} | ||
let deferredData; | ||
let deferredResolvers = {}; | ||
try { | ||
let sectionReader = readStreamSections(stream); | ||
return response.text(); | ||
// Read the first section to get the critical data | ||
let initialSectionResult = await sectionReader.next(); | ||
let initialSection = initialSectionResult.value; | ||
if (!initialSection) throw new Error("no critical data"); | ||
let criticalData = JSON.parse(initialSection); | ||
// Setup deferred data and resolvers for later based on the critical data | ||
if (typeof criticalData === "object" && criticalData !== null) { | ||
for (let [eventKey, value] of Object.entries(criticalData)) { | ||
if (typeof value !== "string" || !value.startsWith(DEFERRED_VALUE_PLACEHOLDER_PREFIX)) { | ||
continue; | ||
} | ||
deferredData = deferredData || {}; | ||
deferredData[eventKey] = new Promise((resolve, reject) => { | ||
deferredResolvers[eventKey] = { | ||
resolve: value => { | ||
resolve(value); | ||
delete deferredResolvers[eventKey]; | ||
}, | ||
reject: error => { | ||
reject(error); | ||
delete deferredResolvers[eventKey]; | ||
} | ||
}; | ||
}); | ||
} | ||
} | ||
// Read the rest of the stream and resolve deferred promises | ||
void (async () => { | ||
try { | ||
for await (let section of sectionReader) { | ||
// Determine event type and data | ||
let [event, ...sectionDataStrings] = section.split(":"); | ||
let sectionDataString = sectionDataStrings.join(":"); | ||
let data = JSON.parse(sectionDataString); | ||
if (event === "data") { | ||
for (let [key, value] of Object.entries(data)) { | ||
if (deferredResolvers[key]) { | ||
deferredResolvers[key].resolve(value); | ||
} | ||
} | ||
} else if (event === "error") { | ||
for (let [key, value] of Object.entries(data)) { | ||
let err = new Error(value.message); | ||
err.stack = value.stack; | ||
if (deferredResolvers[key]) { | ||
deferredResolvers[key].reject(err); | ||
} | ||
} | ||
} | ||
} | ||
for (let [key, resolver] of Object.entries(deferredResolvers)) { | ||
resolver.reject(new AbortedDeferredError(`Deferred ${key} will never be resolved`)); | ||
} | ||
} catch (error) { | ||
// Reject any existing deferred promises if something blows up | ||
for (let resolver of Object.values(deferredResolvers)) { | ||
resolver.reject(error); | ||
} | ||
} | ||
})(); | ||
return new UNSAFE_DeferredData({ | ||
...criticalData, | ||
...deferredData | ||
}); | ||
} catch (error) { | ||
for (let resolver of Object.values(deferredResolvers)) { | ||
resolver.reject(error); | ||
} | ||
throw error; | ||
} | ||
} | ||
async function* readStreamSections(stream) { | ||
let reader = stream.getReader(); | ||
let buffer = []; | ||
let sections = []; | ||
let closed = false; | ||
let encoder = new TextEncoder(); | ||
let decoder = new TextDecoder(); | ||
let readStreamSection = async () => { | ||
if (sections.length > 0) return sections.shift(); | ||
function getActionInit(submission, signal) { | ||
let { | ||
encType, | ||
method, | ||
formData | ||
} = submission; | ||
let headers = undefined; | ||
let body = formData; | ||
// Read from the stream until we have at least one complete section to process | ||
while (!closed && sections.length === 0) { | ||
let chunk = await reader.read(); | ||
if (chunk.done) { | ||
closed = true; | ||
break; | ||
} | ||
// Buffer the raw chunks | ||
buffer.push(chunk.value); | ||
try { | ||
// Attempt to split off a section from the buffer | ||
let bufferedString = decoder.decode(mergeArrays(...buffer)); | ||
let splitSections = bufferedString.split("\n\n"); | ||
if (splitSections.length >= 2) { | ||
// We have a complete section, so add it to the sections array | ||
sections.push(...splitSections.slice(0, -1)); | ||
// Remove the section from the buffer and store the rest for future processing | ||
buffer = [encoder.encode(splitSections.slice(-1).join("\n\n"))]; | ||
} | ||
if (encType === "application/x-www-form-urlencoded") { | ||
body = new URLSearchParams(); | ||
// If we successfully parsed at least one section, break out of reading the stream | ||
// to allow upstream processing of the processable sections | ||
if (sections.length > 0) { | ||
break; | ||
} | ||
} catch { | ||
// If we failed to parse the buffer it was because we failed to decode the stream | ||
// because we are missing bytes that we haven't yet received, so continue reading | ||
// from the stream until we have a complete section | ||
continue; | ||
} | ||
} | ||
for (let [key, value] of formData) { | ||
invariant(typeof value === "string", `File inputs are not supported with encType "application/x-www-form-urlencoded", please use "multipart/form-data" instead.`); | ||
body.append(key, value); | ||
// If we have a complete section, return it | ||
if (sections.length > 0) { | ||
return sections.shift(); | ||
} | ||
headers = { | ||
"Content-Type": encType | ||
}; | ||
} | ||
// If we have no complete section, but we have no more chunks to process, | ||
// split those sections and clear out the buffer as there is no more data | ||
// to process. If this errors, let it bubble up as the stream ended | ||
// without valid data | ||
if (buffer.length > 0) { | ||
let bufferedString = decoder.decode(mergeArrays(...buffer)); | ||
sections = bufferedString.split("\n\n").filter(s => s); | ||
buffer = []; | ||
} | ||
return { | ||
method, | ||
body, | ||
signal, | ||
credentials: "same-origin", | ||
headers | ||
// Return any remaining sections that have been processed | ||
return sections.shift(); | ||
}; | ||
let section = await readStreamSection(); | ||
while (section) { | ||
yield section; | ||
section = await readStreamSection(); | ||
} | ||
} | ||
function mergeArrays(...arrays) { | ||
let out = new Uint8Array(arrays.reduce((total, arr) => total + arr.length, 0)); | ||
let offset = 0; | ||
for (let arr of arrays) { | ||
out.set(arr, offset); | ||
offset += arr.length; | ||
} | ||
return out; | ||
} | ||
export { extractData, fetchData, isCatchResponse, isErrorResponse, isRedirectResponse }; | ||
export { createRequestInit, fetchData, isCatchResponse, isDeferredData, isDeferredResponse, isErrorResponse, isNetworkErrorResponse, isRedirectResponse, isResponse, parseDeferredReadableStream }; |
/** | ||
* @remix-run/react v0.0.0-nightly-81ec1b7-20220830 | ||
* @remix-run/react v0.0.0-nightly-825f70ebd-20240914 | ||
* | ||
@@ -11,6 +11,7 @@ * Copyright (c) Remix Software Inc. | ||
*/ | ||
import React__default, { useContext } from 'react'; | ||
import * as React from 'react'; | ||
import { isRouteErrorResponse } from 'react-router-dom'; | ||
import { useRemixContext, Scripts } from './components.js'; | ||
// TODO: We eventually might not want to import anything directly from `history` | ||
class RemixErrorBoundary extends React__default.Component { | ||
class RemixErrorBoundary extends React.Component { | ||
constructor(props) { | ||
@@ -23,3 +24,2 @@ super(props); | ||
} | ||
static getDerivedStateFromError(error) { | ||
@@ -30,3 +30,2 @@ return { | ||
} | ||
static getDerivedStateFromProps(props, state) { | ||
@@ -47,8 +46,8 @@ // When we get into an error state, the user will likely click "back" to the | ||
}; | ||
} // If we're not changing locations, preserve the location but still surface | ||
} | ||
// If we're not changing locations, preserve the location but still surface | ||
// any new errors that may come through. We retain the existing error, we do | ||
// this because the error provided from the app state may be cleared without | ||
// the location changing. | ||
return { | ||
@@ -59,7 +58,7 @@ error: props.error || state.error, | ||
} | ||
render() { | ||
if (this.state.error) { | ||
return /*#__PURE__*/React__default.createElement(this.props.component, { | ||
error: this.state.error | ||
return /*#__PURE__*/React.createElement(RemixRootDefaultErrorBoundary, { | ||
error: this.state.error, | ||
isOutsideRemixApp: true | ||
}); | ||
@@ -70,29 +69,45 @@ } else { | ||
} | ||
} | ||
} | ||
/** | ||
* When app's don't provide a root level ErrorBoundary, we default to this. | ||
*/ | ||
function RemixRootDefaultErrorBoundary({ | ||
error | ||
error, | ||
isOutsideRemixApp | ||
}) { | ||
console.error(error); | ||
return /*#__PURE__*/React__default.createElement("html", { | ||
lang: "en" | ||
}, /*#__PURE__*/React__default.createElement("head", null, /*#__PURE__*/React__default.createElement("meta", { | ||
charSet: "utf-8" | ||
}), /*#__PURE__*/React__default.createElement("meta", { | ||
name: "viewport", | ||
content: "width=device-width,initial-scale=1,viewport-fit=cover" | ||
}), /*#__PURE__*/React__default.createElement("title", null, "Application Error!")), /*#__PURE__*/React__default.createElement("body", null, /*#__PURE__*/React__default.createElement("main", { | ||
style: { | ||
fontFamily: "system-ui, sans-serif", | ||
padding: "2rem" | ||
let heyDeveloper = /*#__PURE__*/React.createElement("script", { | ||
dangerouslySetInnerHTML: { | ||
__html: ` | ||
console.log( | ||
"💿 Hey developer 👋. You can provide a way better UX than this when your app throws errors. Check out https://remix.run/guides/errors for more information." | ||
); | ||
` | ||
} | ||
}, /*#__PURE__*/React__default.createElement("h1", { | ||
}); | ||
if (isRouteErrorResponse(error)) { | ||
return /*#__PURE__*/React.createElement(BoundaryShell, { | ||
title: "Unhandled Thrown Response!" | ||
}, /*#__PURE__*/React.createElement("h1", { | ||
style: { | ||
fontSize: "24px" | ||
} | ||
}, error.status, " ", error.statusText), heyDeveloper); | ||
} | ||
let errorInstance; | ||
if (error instanceof Error) { | ||
errorInstance = error; | ||
} else { | ||
let errorString = error == null ? "Unknown Error" : typeof error === "object" && "toString" in error ? error.toString() : JSON.stringify(error); | ||
errorInstance = new Error(errorString); | ||
} | ||
return /*#__PURE__*/React.createElement(BoundaryShell, { | ||
title: "Application Error!", | ||
isOutsideRemixApp: isOutsideRemixApp | ||
}, /*#__PURE__*/React.createElement("h1", { | ||
style: { | ||
fontSize: "24px" | ||
} | ||
}, "Application Error"), /*#__PURE__*/React__default.createElement("pre", { | ||
}, "Application Error"), /*#__PURE__*/React.createElement("pre", { | ||
style: { | ||
@@ -104,49 +119,42 @@ padding: "2rem", | ||
} | ||
}, error.stack)), /*#__PURE__*/React__default.createElement("script", { | ||
dangerouslySetInnerHTML: { | ||
__html: ` | ||
console.log( | ||
"💿 Hey developer👋. You can provide a way better UX than this when your app throws errors. Check out https://remix.run/guides/errors for more information." | ||
); | ||
` | ||
} | ||
}))); | ||
}, errorInstance.stack), heyDeveloper); | ||
} | ||
let RemixCatchContext = /*#__PURE__*/React__default.createContext(undefined); | ||
/** | ||
* Returns the status code and thrown response data. | ||
* | ||
* @see https://remix.run/api/conventions#catchboundary | ||
*/ | ||
function useCatch() { | ||
return useContext(RemixCatchContext); | ||
} | ||
function RemixCatchBoundary({ | ||
catch: catchVal, | ||
component: Component, | ||
function BoundaryShell({ | ||
title, | ||
renderScripts, | ||
isOutsideRemixApp, | ||
children | ||
}) { | ||
if (catchVal) { | ||
return /*#__PURE__*/React__default.createElement(RemixCatchContext.Provider, { | ||
value: catchVal | ||
}, /*#__PURE__*/React__default.createElement(Component, null)); | ||
} | ||
var _routeModules$root; | ||
let { | ||
routeModules | ||
} = useRemixContext(); | ||
return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, children); | ||
} | ||
/** | ||
* When app's don't provide a root level CatchBoundary, we default to this. | ||
*/ | ||
// Generally speaking, when the root route has a Layout we want to use that | ||
// as the app shell instead of the default `BoundaryShell` wrapper markup below. | ||
// This is true for `loader`/`action` errors, most render errors, and | ||
// `HydrateFallback` scenarios. | ||
function RemixRootDefaultCatchBoundary() { | ||
let caught = useCatch(); | ||
return /*#__PURE__*/React__default.createElement("html", { | ||
// However, render errors thrown from the `Layout` present a bit of an issue | ||
// because if the `Layout` itself throws during the `ErrorBoundary` pass and | ||
// we bubble outside the `RouterProvider` to the wrapping `RemixErrorBoundary`, | ||
// by returning only `children` here we'll be trying to append a `<div>` to | ||
// the `document` and the DOM will throw, putting React into an error/hydration | ||
// loop. | ||
// Instead, if we're ever rendering from the outermost `RemixErrorBoundary` | ||
// during hydration that wraps `RouterProvider`, then we can't trust the | ||
// `Layout` and should fallback to the default app shell so we're always | ||
// returning an `<html>` document. | ||
if ((_routeModules$root = routeModules.root) !== null && _routeModules$root !== void 0 && _routeModules$root.Layout && !isOutsideRemixApp) { | ||
return children; | ||
} | ||
return /*#__PURE__*/React.createElement("html", { | ||
lang: "en" | ||
}, /*#__PURE__*/React__default.createElement("head", null, /*#__PURE__*/React__default.createElement("meta", { | ||
}, /*#__PURE__*/React.createElement("head", null, /*#__PURE__*/React.createElement("meta", { | ||
charSet: "utf-8" | ||
}), /*#__PURE__*/React__default.createElement("meta", { | ||
}), /*#__PURE__*/React.createElement("meta", { | ||
name: "viewport", | ||
content: "width=device-width,initial-scale=1,viewport-fit=cover" | ||
}), /*#__PURE__*/React__default.createElement("title", null, "Unhandled Thrown Response!")), /*#__PURE__*/React__default.createElement("body", null, /*#__PURE__*/React__default.createElement("h1", { | ||
}), /*#__PURE__*/React.createElement("title", null, title)), /*#__PURE__*/React.createElement("body", null, /*#__PURE__*/React.createElement("main", { | ||
style: { | ||
@@ -156,13 +164,5 @@ fontFamily: "system-ui, sans-serif", | ||
} | ||
}, caught.status, " ", caught.statusText), /*#__PURE__*/React__default.createElement("script", { | ||
dangerouslySetInnerHTML: { | ||
__html: ` | ||
console.log( | ||
"💿 Hey developer👋. You can provide a way better UX than this when your app throws 404s (and other responses). Check out https://remix.run/guides/not-found for more information." | ||
); | ||
` | ||
} | ||
}))); | ||
}, children, renderScripts ? /*#__PURE__*/React.createElement(Scripts, null) : null))); | ||
} | ||
export { RemixCatchBoundary, RemixErrorBoundary, RemixRootDefaultCatchBoundary, RemixRootDefaultErrorBoundary, useCatch }; | ||
export { BoundaryShell, RemixErrorBoundary, RemixRootDefaultErrorBoundary }; |
/** | ||
* @remix-run/react v0.0.0-nightly-81ec1b7-20220830 | ||
* @remix-run/react v0.0.0-nightly-825f70ebd-20240914 | ||
* | ||
@@ -11,7 +11,7 @@ * Copyright (c) Remix Software Inc. | ||
*/ | ||
export { Navigate, NavigationType, Outlet, Route, Routes, createPath, createRoutesFromChildren, createRoutesFromElements, createSearchParams, generatePath, isRouteErrorResponse, matchPath, matchRoutes, parsePath, renderMatches, resolvePath, unstable_usePrompt, unstable_useViewTransitionState, useAsyncError, useAsyncValue, useBeforeUnload, useBlocker, useFetchers, useFormAction, useHref, useInRouterContext, useLinkClickHandler, useLocation, useMatch, useNavigate, useNavigation, useNavigationType, useOutlet, useOutletContext, useParams, useResolvedPath, useRevalidator, useRouteError, useRoutes, useSearchParams, useSubmit } from 'react-router-dom'; | ||
export { defer, json, redirect, redirectDocument, replace, unstable_data } from '@remix-run/server-runtime'; | ||
export { RemixBrowser } from './browser.js'; | ||
export { Outlet, useHref, useLocation, useNavigate, useNavigationType, useOutlet, useOutletContext, useParams, useResolvedPath, useSearchParams } from 'react-router-dom'; | ||
export { Form, Link, Links, LiveReload, Meta, NavLink, PrefetchPageLinks, Scripts, useActionData, useBeforeUnload, useFetcher, useFetchers, useFormAction, useLoaderData, useMatches, useSubmit, useTransition } from './components.js'; | ||
export { useCatch } from './errorBoundaries.js'; | ||
export { Await, Form, Link, Links, LiveReload, Meta, NavLink, PrefetchPageLinks, Scripts, RemixContext as UNSAFE_RemixContext, useActionData, useFetcher, useLoaderData, useMatches, useRouteLoaderData } from './components.js'; | ||
export { ScrollRestoration } from './scroll-restoration.js'; | ||
export { RemixServer } from './server.js'; |
/** | ||
* @remix-run/react v0.0.0-nightly-81ec1b7-20220830 | ||
* @remix-run/react v0.0.0-nightly-825f70ebd-20240914 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
/** | ||
* @remix-run/react v0.0.0-nightly-81ec1b7-20220830 | ||
* @remix-run/react v0.0.0-nightly-825f70ebd-20240914 | ||
* | ||
@@ -11,6 +11,11 @@ * Copyright (c) Remix Software Inc. | ||
*/ | ||
import { parsePath } from 'history'; | ||
import { parsePath } from 'react-router-dom'; | ||
import { loadRouteModule } from './routeModules.js'; | ||
// TODO: We eventually might not want to import anything directly from `history` | ||
/** | ||
* Represents a `<link>` element. | ||
* | ||
* WHATWG Specification: https://html.spec.whatwg.org/multipage/semantics.html#the-link-element | ||
*/ | ||
//////////////////////////////////////////////////////////////////////////////// | ||
@@ -22,21 +27,28 @@ | ||
*/ | ||
function getLinksForMatches(matches, routeModules, manifest) { | ||
function getKeyedLinksForMatches(matches, routeModules, manifest) { | ||
let descriptors = matches.map(match => { | ||
var _module$links; | ||
let module = routeModules[match.route.id]; | ||
return ((_module$links = module.links) === null || _module$links === void 0 ? void 0 : _module$links.call(module)) || []; | ||
}).flat(1); | ||
let route = manifest.routes[match.route.id]; | ||
return [route.css ? route.css.map(href => ({ | ||
rel: "stylesheet", | ||
href | ||
})) : [], (module === null || module === void 0 ? void 0 : (_module$links = module.links) === null || _module$links === void 0 ? void 0 : _module$links.call(module)) || []]; | ||
}).flat(2); | ||
let preloads = getCurrentPageModulePreloadHrefs(matches, manifest); | ||
return dedupe(descriptors, preloads); | ||
return dedupeLinkDescriptors(descriptors, preloads); | ||
} | ||
async function prefetchStyleLinks(routeModule) { | ||
if (!routeModule.links) return; | ||
let descriptors = routeModule.links(); | ||
if (!descriptors) return; | ||
async function prefetchStyleLinks(route, routeModule) { | ||
var _route$css, _routeModule$links; | ||
if (!route.css && !routeModule.links || !isPreloadSupported()) return; | ||
let descriptors = [((_route$css = route.css) === null || _route$css === void 0 ? void 0 : _route$css.map(href => ({ | ||
rel: "stylesheet", | ||
href | ||
}))) ?? [], ((_routeModule$links = routeModule.links) === null || _routeModule$links === void 0 ? void 0 : _routeModule$links.call(routeModule)) ?? []].flat(1); | ||
if (descriptors.length === 0) return; | ||
let styleLinks = []; | ||
for (let descriptor of descriptors) { | ||
if (!isPageLinkDescriptor(descriptor) && descriptor.rel === "stylesheet") { | ||
styleLinks.push({ ...descriptor, | ||
styleLinks.push({ | ||
...descriptor, | ||
rel: "preload", | ||
@@ -46,9 +58,9 @@ as: "style" | ||
} | ||
} // don't block for non-matching media queries | ||
} | ||
let matchingLinks = styleLinks.filter(link => !link.media || window.matchMedia(link.media).matches); | ||
// don't block for non-matching media queries, or for stylesheets that are | ||
// already in the DOM (active route revalidations) | ||
let matchingLinks = styleLinks.filter(link => (!link.media || window.matchMedia(link.media).matches) && !document.querySelector(`link[rel="stylesheet"][href="${link.href}"]`)); | ||
await Promise.all(matchingLinks.map(prefetchStyleLink)); | ||
} | ||
async function prefetchStyleLink(descriptor) { | ||
@@ -58,3 +70,2 @@ return new Promise(resolve => { | ||
Object.assign(link, descriptor); | ||
function removeLink() { | ||
@@ -68,3 +79,2 @@ // if a navigation interrupts this prefetch React will update the <head> | ||
} | ||
link.onload = () => { | ||
@@ -74,3 +84,2 @@ removeLink(); | ||
}; | ||
link.onerror = () => { | ||
@@ -80,8 +89,7 @@ removeLink(); | ||
}; | ||
document.head.appendChild(link); | ||
}); | ||
} //////////////////////////////////////////////////////////////////////////////// | ||
} | ||
//////////////////////////////////////////////////////////////////////////////// | ||
function isPageLinkDescriptor(object) { | ||
@@ -91,28 +99,32 @@ return object != null && typeof object.page === "string"; | ||
function isHtmlLinkDescriptor(object) { | ||
if (object == null) return false; // <link> may not have an href if <link rel="preload"> is used with imagesrcset + imagesizes | ||
if (object == null) { | ||
return false; | ||
} | ||
// <link> may not have an href if <link rel="preload"> is used with imageSrcSet + imageSizes | ||
// https://github.com/remix-run/remix/issues/184 | ||
// https://html.spec.whatwg.org/commit-snapshots/cb4f5ff75de5f4cbd7013c4abad02f21c77d4d1c/#attr-link-imagesrcset | ||
if (object.href == null) { | ||
return object.rel === "preload" && (typeof object.imageSrcSet === "string" || typeof object.imagesrcset === "string") && (typeof object.imageSizes === "string" || typeof object.imagesizes === "string"); | ||
return object.rel === "preload" && typeof object.imageSrcSet === "string" && typeof object.imageSizes === "string"; | ||
} | ||
return typeof object.rel === "string" && typeof object.href === "string"; | ||
} | ||
async function getStylesheetPrefetchLinks(matches, routeModules) { | ||
async function getKeyedPrefetchLinks(matches, manifest, routeModules) { | ||
let links = await Promise.all(matches.map(async match => { | ||
let mod = await loadRouteModule(match.route, routeModules); | ||
let mod = await loadRouteModule(manifest.routes[match.route.id], routeModules); | ||
return mod.links ? mod.links() : []; | ||
})); | ||
return links.flat(1).filter(isHtmlLinkDescriptor).filter(link => link.rel === "stylesheet" || link.rel === "preload").map(link => link.rel === "preload" ? { ...link, | ||
rel: "prefetch" | ||
} : { ...link, | ||
return dedupeLinkDescriptors(links.flat(1).filter(isHtmlLinkDescriptor).filter(link => link.rel === "stylesheet" || link.rel === "preload").map(link => link.rel === "stylesheet" ? { | ||
...link, | ||
rel: "prefetch", | ||
as: "style" | ||
}); | ||
} // This is ridiculously identical to transition.ts `filterMatchesToLoad` | ||
} : { | ||
...link, | ||
rel: "prefetch" | ||
})); | ||
} | ||
function getNewMatchesForLinks(page, nextMatches, currentMatches, location, mode) { | ||
// This is ridiculously identical to transition.ts `filterMatchesToLoad` | ||
function getNewMatchesForLinks(page, nextMatches, currentMatches, manifest, location, mode) { | ||
let path = parsePathPatch(page); | ||
let isNew = (match, index) => { | ||
@@ -122,37 +134,43 @@ if (!currentMatches[index]) return true; | ||
}; | ||
let matchPathChanged = (match, index) => { | ||
var _currentMatches$index; | ||
return (// param change, /users/123 -> /users/456 | ||
currentMatches[index].pathname !== match.pathname || // splat param changed, which is not present in match.path | ||
return ( | ||
// param change, /users/123 -> /users/456 | ||
currentMatches[index].pathname !== match.pathname || | ||
// splat param changed, which is not present in match.path | ||
// e.g. /files/images/avatar.jpg -> files/finances.xls | ||
((_currentMatches$index = currentMatches[index].route.path) === null || _currentMatches$index === void 0 ? void 0 : _currentMatches$index.endsWith("*")) && currentMatches[index].params["*"] !== match.params["*"] | ||
); | ||
}; // NOTE: keep this mostly up-to-date w/ the transition data diff, but this | ||
}; | ||
// NOTE: keep this mostly up-to-date w/ the transition data diff, but this | ||
// version doesn't care about submissions | ||
let newMatches = mode === "data" && location.search !== path.search ? // this is really similar to stuff in transition.ts, maybe somebody smarter | ||
let newMatches = mode === "data" && location.search !== path.search ? | ||
// this is really similar to stuff in transition.ts, maybe somebody smarter | ||
// than me (or in less of a hurry) can share some of it. You're the best. | ||
nextMatches.filter((match, index) => { | ||
if (!match.route.hasLoader) { | ||
let manifestRoute = manifest.routes[match.route.id]; | ||
if (!manifestRoute.hasLoader) { | ||
return false; | ||
} | ||
if (isNew(match, index) || matchPathChanged(match, index)) { | ||
return true; | ||
} | ||
if (match.route.shouldReload) { | ||
return match.route.shouldReload({ | ||
params: match.params, | ||
prevUrl: new URL(location.pathname + location.search + location.hash, window.origin), | ||
url: new URL(page, window.origin) | ||
if (match.route.shouldRevalidate) { | ||
var _currentMatches$; | ||
let routeChoice = match.route.shouldRevalidate({ | ||
currentUrl: new URL(location.pathname + location.search + location.hash, window.origin), | ||
currentParams: ((_currentMatches$ = currentMatches[0]) === null || _currentMatches$ === void 0 ? void 0 : _currentMatches$.params) || {}, | ||
nextUrl: new URL(page, window.origin), | ||
nextParams: match.params, | ||
defaultShouldRevalidate: true | ||
}); | ||
if (typeof routeChoice === "boolean") { | ||
return routeChoice; | ||
} | ||
} | ||
return true; | ||
}) : nextMatches.filter((match, index) => { | ||
return (mode === "assets" || match.route.hasLoader) && (isNew(match, index) || matchPathChanged(match, index)); | ||
let manifestRoute = manifest.routes[match.route.id]; | ||
return (mode === "assets" || manifestRoute.hasLoader) && (isNew(match, index) || matchPathChanged(match, index)); | ||
}); | ||
@@ -163,3 +181,3 @@ return newMatches; | ||
let path = parsePathPatch(page); | ||
return dedupeHrefs(matches.filter(match => manifest.routes[match.route.id].hasLoader).map(match => { | ||
return dedupeHrefs(matches.filter(match => manifest.routes[match.route.id].hasLoader && !manifest.routes[match.route.id].hasClientLoader).map(match => { | ||
let { | ||
@@ -178,13 +196,12 @@ pathname, | ||
let hrefs = [route.module]; | ||
if (route.imports) { | ||
hrefs = hrefs.concat(route.imports); | ||
} | ||
return hrefs; | ||
}).flat(1)); | ||
} // The `<Script>` will render rel=modulepreload for the current page, we don't | ||
} | ||
// The `<Script>` will render rel=modulepreload for the current page, we don't | ||
// need to include them in a page prefetch, this gives us the list to remove | ||
// while deduping. | ||
function getCurrentPageModulePreloadHrefs(matches, manifest) { | ||
@@ -194,36 +211,40 @@ return dedupeHrefs(matches.map(match => { | ||
let hrefs = [route.module]; | ||
if (route.imports) { | ||
hrefs = hrefs.concat(route.imports); | ||
} | ||
return hrefs; | ||
}).flat(1)); | ||
} | ||
function dedupeHrefs(hrefs) { | ||
return [...new Set(hrefs)]; | ||
} | ||
function dedupe(descriptors, preloads) { | ||
function sortKeys(obj) { | ||
let sorted = {}; | ||
let keys = Object.keys(obj).sort(); | ||
for (let key of keys) { | ||
sorted[key] = obj[key]; | ||
} | ||
return sorted; | ||
} | ||
function dedupeLinkDescriptors(descriptors, preloads) { | ||
let set = new Set(); | ||
let preloadsSet = new Set(preloads); | ||
return descriptors.reduce((deduped, descriptor) => { | ||
let alreadyModulePreload = !isPageLinkDescriptor(descriptor) && descriptor.as === "script" && descriptor.href && preloadsSet.has(descriptor.href); | ||
let alreadyModulePreload = preloads && !isPageLinkDescriptor(descriptor) && descriptor.as === "script" && descriptor.href && preloadsSet.has(descriptor.href); | ||
if (alreadyModulePreload) { | ||
return deduped; | ||
} | ||
let str = JSON.stringify(descriptor); | ||
if (!set.has(str)) { | ||
set.add(str); | ||
deduped.push(descriptor); | ||
let key = JSON.stringify(sortKeys(descriptor)); | ||
if (!set.has(key)) { | ||
set.add(key); | ||
deduped.push({ | ||
key, | ||
link: descriptor | ||
}); | ||
} | ||
return deduped; | ||
}, []); | ||
} // https://github.com/remix-run/history/issues/897 | ||
} | ||
// https://github.com/remix-run/history/issues/897 | ||
function parsePathPatch(href) { | ||
@@ -235,2 +256,16 @@ let path = parsePath(href); | ||
export { dedupe, getDataLinkHrefs, getLinksForMatches, getModuleLinkHrefs, getNewMatchesForLinks, getStylesheetPrefetchLinks, isHtmlLinkDescriptor, isPageLinkDescriptor, prefetchStyleLinks }; | ||
// Detect if this browser supports <link rel="preload"> (or has it enabled). | ||
// Originally added to handle the firefox `network.preload` config: | ||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1847811 | ||
let _isPreloadSupported; | ||
function isPreloadSupported() { | ||
if (_isPreloadSupported !== undefined) { | ||
return _isPreloadSupported; | ||
} | ||
let el = document.createElement("link"); | ||
_isPreloadSupported = el.relList.supports("preload"); | ||
el = null; | ||
return _isPreloadSupported; | ||
} | ||
export { getDataLinkHrefs, getKeyedLinksForMatches, getKeyedPrefetchLinks, getModuleLinkHrefs, getNewMatchesForLinks, isPageLinkDescriptor, prefetchStyleLinks }; |
/** | ||
* @remix-run/react v0.0.0-nightly-81ec1b7-20220830 | ||
* @remix-run/react v0.0.0-nightly-825f70ebd-20240914 | ||
* | ||
@@ -11,2 +11,19 @@ * Copyright (c) Remix Software Inc. | ||
*/ | ||
// This escapeHtml utility is based on https://github.com/zertosh/htmlescape | ||
// License: https://github.com/zertosh/htmlescape/blob/0527ca7156a524d256101bb310a9f970f63078ad/LICENSE | ||
// We've chosen to inline the utility here to reduce the number of npm dependencies we have, | ||
// slightly decrease the code size compared the original package and make it esm compatible. | ||
const ESCAPE_LOOKUP = { | ||
"&": "\\u0026", | ||
">": "\\u003e", | ||
"<": "\\u003c", | ||
"\u2028": "\\u2028", | ||
"\u2029": "\\u2029" | ||
}; | ||
const ESCAPE_REGEX = /[&><\u2028\u2029]/g; | ||
function escapeHtml(html) { | ||
return html.replace(ESCAPE_REGEX, match => ESCAPE_LOOKUP[match]); | ||
} | ||
function createHtml(html) { | ||
@@ -18,2 +35,2 @@ return { | ||
export { createHtml }; | ||
export { createHtml, escapeHtml }; |
/** | ||
* @remix-run/react v0.0.0-nightly-81ec1b7-20220830 | ||
* @remix-run/react v0.0.0-nightly-825f70ebd-20240914 | ||
* | ||
@@ -11,52 +11,38 @@ * Copyright (c) Remix Software Inc. | ||
*/ | ||
// TODO: We eventually might not want to import anything directly from `history` | ||
// and leverage `react-router` here instead | ||
// TODO: import/export from react-router-dom | ||
/** | ||
* A function that handles data mutations for a route on the client | ||
*/ | ||
/** | ||
* A React component that is rendered when the server throws a Response. | ||
* | ||
* @see https://remix.run/api/conventions#catchboundary | ||
* Arguments passed to a route `clientAction` function | ||
*/ | ||
/** | ||
* A React component that is rendered when there is an error on a route. | ||
* | ||
* @see https://remix.run/api/conventions#errorboundary | ||
* A function that loads data for a route on the client | ||
*/ | ||
/** | ||
* A function that defines `<link>` tags to be inserted into the `<head>` of | ||
* the document on route transitions. | ||
* | ||
* @see https://remix.run/api/remix#meta-links-scripts | ||
* Arguments passed to a route `clientLoader` function | ||
*/ | ||
/** | ||
* A function that returns an object of name + content pairs to use for | ||
* `<meta>` tags for a route. These tags will be merged with (and take | ||
* precedence over) tags from parent routes. | ||
* | ||
* @see https://remix.run/api/remix#meta-links-scripts | ||
* ErrorBoundary to display for this route | ||
*/ | ||
/** | ||
* A name/content pair used to render `<meta>` tags in a meta function for a | ||
* route. The value can be either a string, which will render a single `<meta>` | ||
* tag, or an array of strings that will render multiple tags with the same | ||
* `name` attribute. | ||
* `<Route HydrateFallback>` component to render on initial loads | ||
* when client loaders are present | ||
*/ | ||
/** | ||
* During client side transitions Remix will optimize reloading of routes that | ||
* are currently on the page by avoiding loading routes that aren't changing. | ||
* However, in some cases, like form submissions or search params Remix doesn't | ||
* know which routes need to be reloaded so it reloads them all to be safe. | ||
* Optional, root-only `<Route Layout>` component to wrap the root content in. | ||
* Useful for defining the <html>/<head>/<body> document shell shared by the | ||
* Component, HydrateFallback, and ErrorBoundary | ||
*/ | ||
/** | ||
* A function that defines `<link>` tags to be inserted into the `<head>` of | ||
* the document on route transitions. | ||
* | ||
* This function lets apps further optimize by returning `false` when Remix is | ||
* about to reload the route. A common case is a root loader with nothing but | ||
* environment variables: after form submissions the root probably doesn't need | ||
* to be reloaded. | ||
* | ||
* @see https://remix.run/api/conventions#unstable_shouldreload | ||
* @see https://remix.run/route/meta | ||
*/ | ||
@@ -71,4 +57,5 @@ | ||
* | ||
* @see https://remix.run/api/conventions#handle | ||
* @see https://remix.run/route/handle | ||
*/ | ||
async function loadRouteModule(route, routeModulesCache) { | ||
@@ -78,16 +65,31 @@ if (route.id in routeModulesCache) { | ||
} | ||
try { | ||
let routeModule = await import( | ||
/* webpackIgnore: true */ | ||
route.module); | ||
let routeModule = await import( /* webpackIgnore: true */route.module); | ||
routeModulesCache[route.id] = routeModule; | ||
return routeModule; | ||
} catch (error) { | ||
// User got caught in the middle of a deploy and the CDN no longer has the | ||
// asset we're trying to import! Reload from the server and the user | ||
// (should) get the new manifest--unless the developer purged the static | ||
// assets, the manifest path, but not the documents 😬 | ||
// If we can't load the route it's likely one of 2 things: | ||
// - User got caught in the middle of a deploy and the CDN no longer has the | ||
// asset we're trying to import! Reload from the server and the user | ||
// (should) get the new manifest--unless the developer purged the static | ||
// assets, the manifest path, but not the documents 😬 | ||
// - Or, the asset trying to be imported has an error (usually in vite dev | ||
// mode), so the best we can do here is log the error for visibility | ||
// (via `Preserve log`) and reload | ||
// Log the error so it can be accessed via the `Preserve Log` setting | ||
console.error(`Error loading route module \`${route.module}\`, reloading page...`); | ||
console.error(error); | ||
if (window.__remixContext.isSpaMode && | ||
// @ts-expect-error | ||
typeof import.meta.hot !== "undefined") { | ||
// In SPA Mode (which implies vite) we don't want to perform a hard reload | ||
// on dev-time errors since it's a vite compilation error and a reload is | ||
// just going to fail with the same issue. Let the UI bubble to the error | ||
// boundary and let them see the error in the overlay or the dev server log | ||
throw error; | ||
} | ||
window.location.reload(); | ||
return new Promise(() => {// check out of this hook cause the DJs never gonna re[s]olve this | ||
return new Promise(() => { | ||
// check out of this hook cause the DJs never gonna re[s]olve this | ||
}); | ||
@@ -94,0 +96,0 @@ } |
/** | ||
* @remix-run/react v0.0.0-nightly-81ec1b7-20220830 | ||
* @remix-run/react v0.0.0-nightly-825f70ebd-20240914 | ||
* | ||
@@ -12,127 +12,405 @@ * Copyright (c) Remix Software Inc. | ||
import * as React from 'react'; | ||
import { UNSAFE_ErrorResponseImpl } from '@remix-run/router'; | ||
import { useRouteError, redirect } from 'react-router-dom'; | ||
import { loadRouteModule } from './routeModules.js'; | ||
import { fetchData, isCatchResponse, extractData, isRedirectResponse } from './data.js'; | ||
import { CatchValue, TransitionRedirect } from './transition.js'; | ||
import { fetchData, isRedirectResponse, isCatchResponse, isDeferredResponse, parseDeferredReadableStream, isDeferredData, isResponse } from './data.js'; | ||
import { prefetchStyleLinks } from './links.js'; | ||
import { RemixRootDefaultErrorBoundary } from './errorBoundaries.js'; | ||
import { RemixRootDefaultHydrateFallback } from './fallback.js'; | ||
import invariant from './invariant.js'; | ||
function createClientRoute(entryRoute, routeModulesCache, Component) { | ||
// NOTE: make sure to change the Route in server-runtime if you change this | ||
// NOTE: make sure to change the EntryRoute in server-runtime if you change this | ||
// Create a map of routes by parentId to use recursively instead of | ||
// repeatedly filtering the manifest. | ||
function groupRoutesByParentId(manifest) { | ||
let routes = {}; | ||
Object.values(manifest).forEach(route => { | ||
let parentId = route.parentId || ""; | ||
if (!routes[parentId]) { | ||
routes[parentId] = []; | ||
} | ||
routes[parentId].push(route); | ||
}); | ||
return routes; | ||
} | ||
function getRouteComponents(route, routeModule, isSpaMode) { | ||
let Component = getRouteModuleComponent(routeModule); | ||
// HydrateFallback can only exist on the root route in SPA Mode | ||
let HydrateFallback = routeModule.HydrateFallback && (!isSpaMode || route.id === "root") ? routeModule.HydrateFallback : route.id === "root" ? RemixRootDefaultHydrateFallback : undefined; | ||
let ErrorBoundary = routeModule.ErrorBoundary ? routeModule.ErrorBoundary : route.id === "root" ? () => /*#__PURE__*/React.createElement(RemixRootDefaultErrorBoundary, { | ||
error: useRouteError() | ||
}) : undefined; | ||
if (route.id === "root" && routeModule.Layout) { | ||
return { | ||
...(Component ? { | ||
element: /*#__PURE__*/React.createElement(routeModule.Layout, null, /*#__PURE__*/React.createElement(Component, null)) | ||
} : { | ||
Component | ||
}), | ||
...(ErrorBoundary ? { | ||
errorElement: /*#__PURE__*/React.createElement(routeModule.Layout, null, /*#__PURE__*/React.createElement(ErrorBoundary, null)) | ||
} : { | ||
ErrorBoundary | ||
}), | ||
...(HydrateFallback ? { | ||
hydrateFallbackElement: /*#__PURE__*/React.createElement(routeModule.Layout, null, /*#__PURE__*/React.createElement(HydrateFallback, null)) | ||
} : { | ||
HydrateFallback | ||
}) | ||
}; | ||
} | ||
return { | ||
caseSensitive: !!entryRoute.caseSensitive, | ||
element: /*#__PURE__*/React.createElement(Component, { | ||
id: entryRoute.id | ||
}), | ||
id: entryRoute.id, | ||
path: entryRoute.path, | ||
index: entryRoute.index, | ||
module: entryRoute.module, | ||
loader: createLoader(entryRoute, routeModulesCache), | ||
action: createAction(entryRoute, routeModulesCache), | ||
shouldReload: createShouldReload(entryRoute, routeModulesCache), | ||
ErrorBoundary: entryRoute.hasErrorBoundary, | ||
CatchBoundary: entryRoute.hasCatchBoundary, | ||
hasLoader: entryRoute.hasLoader | ||
Component, | ||
ErrorBoundary, | ||
HydrateFallback | ||
}; | ||
} | ||
function createClientRoutes(routeManifest, routeModulesCache, Component, parentId) { | ||
return Object.keys(routeManifest).filter(key => routeManifest[key].parentId === parentId).map(key => { | ||
let route = createClientRoute(routeManifest[key], routeModulesCache, Component); | ||
let children = createClientRoutes(routeManifest, routeModulesCache, Component, route.id); | ||
if (children.length > 0) route.children = children; | ||
return route; | ||
function createServerRoutes(manifest, routeModules, future, isSpaMode, parentId = "", routesByParentId = groupRoutesByParentId(manifest), spaModeLazyPromise = Promise.resolve({ | ||
Component: () => null | ||
})) { | ||
return (routesByParentId[parentId] || []).map(route => { | ||
let routeModule = routeModules[route.id]; | ||
invariant(routeModule, "No `routeModule` available to create server routes"); | ||
let dataRoute = { | ||
...getRouteComponents(route, routeModule, isSpaMode), | ||
caseSensitive: route.caseSensitive, | ||
id: route.id, | ||
index: route.index, | ||
path: route.path, | ||
handle: routeModule.handle, | ||
// For SPA Mode, all routes are lazy except root. However we tell the | ||
// router root is also lazy here too since we don't need a full | ||
// implementation - we just need a `lazy` prop to tell the RR rendering | ||
// where to stop which is always at the root route in SPA mode | ||
lazy: isSpaMode ? () => spaModeLazyPromise : undefined, | ||
// For partial hydration rendering, we need to indicate when the route | ||
// has a loader/clientLoader, but it won't ever be called during the static | ||
// render, so just give it a no-op function so we can render down to the | ||
// proper fallback | ||
loader: route.hasLoader || route.hasClientLoader ? () => null : undefined | ||
// We don't need action/shouldRevalidate on these routes since they're | ||
// for a static render | ||
}; | ||
let children = createServerRoutes(manifest, routeModules, future, isSpaMode, route.id, routesByParentId, spaModeLazyPromise); | ||
if (children.length > 0) dataRoute.children = children; | ||
return dataRoute; | ||
}); | ||
} | ||
function createShouldReload(route, routeModules) { | ||
let shouldReload = arg => { | ||
let module = routeModules[route.id]; | ||
invariant(module, `Expected route module to be loaded for ${route.id}`); | ||
if (module.unstable_shouldReload) { | ||
return module.unstable_shouldReload(arg); | ||
} | ||
return true; | ||
}; | ||
return shouldReload; | ||
function createClientRoutesWithHMRRevalidationOptOut(needsRevalidation, manifest, routeModulesCache, initialState, future, isSpaMode) { | ||
return createClientRoutes(manifest, routeModulesCache, initialState, future, isSpaMode, "", groupRoutesByParentId(manifest), needsRevalidation); | ||
} | ||
async function loadRouteModuleWithBlockingLinks(route, routeModules) { | ||
let routeModule = await loadRouteModule(route, routeModules); | ||
await prefetchStyleLinks(routeModule); | ||
return routeModule; | ||
function preventInvalidServerHandlerCall(type, route, isSpaMode) { | ||
if (isSpaMode) { | ||
let fn = type === "action" ? "serverAction()" : "serverLoader()"; | ||
let msg = `You cannot call ${fn} in SPA Mode (routeId: "${route.id}")`; | ||
console.error(msg); | ||
throw new UNSAFE_ErrorResponseImpl(400, "Bad Request", new Error(msg), true); | ||
} | ||
let fn = type === "action" ? "serverAction()" : "serverLoader()"; | ||
let msg = `You are trying to call ${fn} on a route that does not have a server ` + `${type} (routeId: "${route.id}")`; | ||
if (type === "loader" && !route.hasLoader || type === "action" && !route.hasAction) { | ||
console.error(msg); | ||
throw new UNSAFE_ErrorResponseImpl(400, "Bad Request", new Error(msg), true); | ||
} | ||
} | ||
function noActionDefinedError(type, routeId) { | ||
let article = type === "clientAction" ? "a" : "an"; | ||
let msg = `Route "${routeId}" does not have ${article} ${type}, but you are trying to ` + `submit to it. To fix this, please add ${article} \`${type}\` function to the route`; | ||
console.error(msg); | ||
throw new UNSAFE_ErrorResponseImpl(405, "Method Not Allowed", new Error(msg), true); | ||
} | ||
function createClientRoutes(manifest, routeModulesCache, initialState, future, isSpaMode, parentId = "", routesByParentId = groupRoutesByParentId(manifest), needsRevalidation) { | ||
return (routesByParentId[parentId] || []).map(route => { | ||
let routeModule = routeModulesCache[route.id]; | ||
function createLoader(route, routeModules) { | ||
let loader = async ({ | ||
url, | ||
signal, | ||
submission | ||
}) => { | ||
if (route.hasLoader) { | ||
let [result] = await Promise.all([fetchData(url, route.id, signal, submission), loadRouteModuleWithBlockingLinks(route, routeModules)]); | ||
if (result instanceof Error) throw result; | ||
let redirect = await checkRedirect(result); | ||
if (redirect) return redirect; | ||
if (isCatchResponse(result)) { | ||
throw new CatchValue(result.status, result.statusText, await extractData(result)); | ||
// Fetch data from the server either via single fetch or the standard `?_data` | ||
// request. Unwrap it when called via `serverLoader`/`serverAction` in a | ||
// client handler, otherwise return the raw response for the router to unwrap | ||
async function fetchServerHandlerAndMaybeUnwrap(request, unwrap, singleFetch) { | ||
if (typeof singleFetch === "function") { | ||
let result = await singleFetch(); | ||
return result; | ||
} | ||
return extractData(result); | ||
} else { | ||
await loadRouteModuleWithBlockingLinks(route, routeModules); | ||
let result = await fetchServerHandler(request, route); | ||
return unwrap ? unwrapServerResponse(result) : result; | ||
} | ||
}; | ||
function fetchServerLoader(request, unwrap, singleFetch) { | ||
if (!route.hasLoader) return Promise.resolve(null); | ||
return fetchServerHandlerAndMaybeUnwrap(request, unwrap, singleFetch); | ||
} | ||
function fetchServerAction(request, unwrap, singleFetch) { | ||
if (!route.hasAction) { | ||
throw noActionDefinedError("action", route.id); | ||
} | ||
return fetchServerHandlerAndMaybeUnwrap(request, unwrap, singleFetch); | ||
} | ||
async function prefetchStylesAndCallHandler(handler) { | ||
// Only prefetch links if we exist in the routeModulesCache (critical modules | ||
// and navigating back to pages previously loaded via route.lazy). Initial | ||
// execution of route.lazy (when the module is not in the cache) will handle | ||
// prefetching style links via loadRouteModuleWithBlockingLinks. | ||
let cachedModule = routeModulesCache[route.id]; | ||
let linkPrefetchPromise = cachedModule ? prefetchStyleLinks(route, cachedModule) : Promise.resolve(); | ||
try { | ||
return handler(); | ||
} finally { | ||
await linkPrefetchPromise; | ||
} | ||
} | ||
let dataRoute = { | ||
id: route.id, | ||
index: route.index, | ||
path: route.path | ||
}; | ||
if (routeModule) { | ||
var _initialState$loaderD, _initialState$errors, _routeModule$clientLo; | ||
// Use critical path modules directly | ||
Object.assign(dataRoute, { | ||
...dataRoute, | ||
...getRouteComponents(route, routeModule, isSpaMode), | ||
handle: routeModule.handle, | ||
shouldRevalidate: needsRevalidation ? wrapShouldRevalidateForHdr(route.id, routeModule.shouldRevalidate, needsRevalidation) : routeModule.shouldRevalidate | ||
}); | ||
let initialData = initialState === null || initialState === void 0 ? void 0 : (_initialState$loaderD = initialState.loaderData) === null || _initialState$loaderD === void 0 ? void 0 : _initialState$loaderD[route.id]; | ||
let initialError = initialState === null || initialState === void 0 ? void 0 : (_initialState$errors = initialState.errors) === null || _initialState$errors === void 0 ? void 0 : _initialState$errors[route.id]; | ||
let isHydrationRequest = needsRevalidation == null && (((_routeModule$clientLo = routeModule.clientLoader) === null || _routeModule$clientLo === void 0 ? void 0 : _routeModule$clientLo.hydrate) === true || !route.hasLoader); | ||
dataRoute.loader = async ({ | ||
request, | ||
params | ||
}, singleFetch) => { | ||
try { | ||
let result = await prefetchStylesAndCallHandler(async () => { | ||
invariant(routeModule, "No `routeModule` available for critical-route loader"); | ||
if (!routeModule.clientLoader) { | ||
if (isSpaMode) return null; | ||
// Call the server when no client loader exists | ||
return fetchServerLoader(request, false, singleFetch); | ||
} | ||
return routeModule.clientLoader({ | ||
request, | ||
params, | ||
async serverLoader() { | ||
preventInvalidServerHandlerCall("loader", route, isSpaMode); | ||
return loader; | ||
} | ||
// On the first call, resolve with the server result | ||
if (isHydrationRequest) { | ||
if (initialError !== undefined) { | ||
throw initialError; | ||
} | ||
return initialData; | ||
} | ||
function createAction(route, routeModules) { | ||
let action = async ({ | ||
url, | ||
signal, | ||
submission | ||
}) => { | ||
if (!route.hasAction) { | ||
console.error(`Route "${route.id}" does not have an action, but you are trying ` + `to submit to it. To fix this, please add an \`action\` function to the route`); | ||
} | ||
// Call the server loader for client-side navigations | ||
return fetchServerLoader(request, true, singleFetch); | ||
} | ||
}); | ||
}); | ||
return result; | ||
} finally { | ||
// Whether or not the user calls `serverLoader`, we only let this | ||
// stick around as true for one loader call | ||
isHydrationRequest = false; | ||
} | ||
}; | ||
let result = await fetchData(url, route.id, signal, submission); | ||
// Let React Router know whether to run this on hydration | ||
dataRoute.loader.hydrate = shouldHydrateRouteLoader(route, routeModule, isSpaMode); | ||
dataRoute.action = ({ | ||
request, | ||
params | ||
}, singleFetch) => { | ||
return prefetchStylesAndCallHandler(async () => { | ||
invariant(routeModule, "No `routeModule` available for critical-route action"); | ||
if (!routeModule.clientAction) { | ||
if (isSpaMode) { | ||
throw noActionDefinedError("clientAction", route.id); | ||
} | ||
return fetchServerAction(request, false, singleFetch); | ||
} | ||
return routeModule.clientAction({ | ||
request, | ||
params, | ||
async serverAction() { | ||
preventInvalidServerHandlerCall("action", route, isSpaMode); | ||
return fetchServerAction(request, true, singleFetch); | ||
} | ||
}); | ||
}); | ||
}; | ||
} else { | ||
// If the lazy route does not have a client loader/action we want to call | ||
// the server loader/action in parallel with the module load so we add | ||
// loader/action as static props on the route | ||
if (!route.hasClientLoader) { | ||
dataRoute.loader = ({ | ||
request | ||
}, singleFetch) => prefetchStylesAndCallHandler(() => { | ||
if (isSpaMode) return Promise.resolve(null); | ||
return fetchServerLoader(request, false, singleFetch); | ||
}); | ||
} | ||
if (!route.hasClientAction) { | ||
dataRoute.action = ({ | ||
request | ||
}, singleFetch) => prefetchStylesAndCallHandler(() => { | ||
if (isSpaMode) { | ||
throw noActionDefinedError("clientAction", route.id); | ||
} | ||
return fetchServerAction(request, false, singleFetch); | ||
}); | ||
} | ||
if (result instanceof Error) { | ||
throw result; | ||
// Load all other modules via route.lazy() | ||
dataRoute.lazy = async () => { | ||
let mod = await loadRouteModuleWithBlockingLinks(route, routeModulesCache); | ||
let lazyRoute = { | ||
...mod | ||
}; | ||
if (mod.clientLoader) { | ||
let clientLoader = mod.clientLoader; | ||
lazyRoute.loader = (args, singleFetch) => clientLoader({ | ||
...args, | ||
async serverLoader() { | ||
preventInvalidServerHandlerCall("loader", route, isSpaMode); | ||
return fetchServerLoader(args.request, true, singleFetch); | ||
} | ||
}); | ||
} | ||
if (mod.clientAction) { | ||
let clientAction = mod.clientAction; | ||
lazyRoute.action = (args, singleFetch) => clientAction({ | ||
...args, | ||
async serverAction() { | ||
preventInvalidServerHandlerCall("action", route, isSpaMode); | ||
return fetchServerAction(args.request, true, singleFetch); | ||
} | ||
}); | ||
} | ||
if (needsRevalidation) { | ||
lazyRoute.shouldRevalidate = wrapShouldRevalidateForHdr(route.id, mod.shouldRevalidate, needsRevalidation); | ||
} | ||
return { | ||
...(lazyRoute.loader ? { | ||
loader: lazyRoute.loader | ||
} : {}), | ||
...(lazyRoute.action ? { | ||
action: lazyRoute.action | ||
} : {}), | ||
hasErrorBoundary: lazyRoute.hasErrorBoundary, | ||
shouldRevalidate: lazyRoute.shouldRevalidate, | ||
handle: lazyRoute.handle, | ||
// No need to wrap these in layout since the root route is never | ||
// loaded via route.lazy() | ||
Component: lazyRoute.Component, | ||
ErrorBoundary: lazyRoute.ErrorBoundary | ||
}; | ||
}; | ||
} | ||
let children = createClientRoutes(manifest, routeModulesCache, initialState, future, isSpaMode, route.id, routesByParentId, needsRevalidation); | ||
if (children.length > 0) dataRoute.children = children; | ||
return dataRoute; | ||
}); | ||
} | ||
let redirect = await checkRedirect(result); | ||
if (redirect) return redirect; | ||
await loadRouteModuleWithBlockingLinks(route, routeModules); | ||
if (isCatchResponse(result)) { | ||
throw new CatchValue(result.status, result.statusText, await extractData(result)); | ||
// When an HMR / HDR update happens we opt out of all user-defined | ||
// revalidation logic and force a revalidation on the first call | ||
function wrapShouldRevalidateForHdr(routeId, routeShouldRevalidate, needsRevalidation) { | ||
let handledRevalidation = false; | ||
return arg => { | ||
if (!handledRevalidation) { | ||
handledRevalidation = true; | ||
return needsRevalidation.has(routeId); | ||
} | ||
return routeShouldRevalidate ? routeShouldRevalidate(arg) : arg.defaultShouldRevalidate; | ||
}; | ||
} | ||
async function loadRouteModuleWithBlockingLinks(route, routeModules) { | ||
let routeModule = await loadRouteModule(route, routeModules); | ||
await prefetchStyleLinks(route, routeModule); | ||
return extractData(result); | ||
// Include all `browserSafeRouteExports` fields, except `HydrateFallback` | ||
// since those aren't used on lazily loaded routes | ||
return { | ||
Component: getRouteModuleComponent(routeModule), | ||
ErrorBoundary: routeModule.ErrorBoundary, | ||
clientAction: routeModule.clientAction, | ||
clientLoader: routeModule.clientLoader, | ||
handle: routeModule.handle, | ||
links: routeModule.links, | ||
meta: routeModule.meta, | ||
shouldRevalidate: routeModule.shouldRevalidate | ||
}; | ||
return action; | ||
} | ||
async function checkRedirect(response) { | ||
if (isRedirectResponse(response)) { | ||
let url = new URL(response.headers.get("X-Remix-Redirect"), window.location.origin); | ||
if (url.origin !== window.location.origin) { | ||
await new Promise(() => { | ||
window.location.replace(url.href); | ||
}); | ||
async function fetchServerHandler(request, route) { | ||
let result = await fetchData(request, route.id); | ||
if (result instanceof Error) { | ||
throw result; | ||
} | ||
if (isRedirectResponse(result)) { | ||
throw getRedirect(result); | ||
} | ||
if (isCatchResponse(result)) { | ||
throw result; | ||
} | ||
if (isDeferredResponse(result) && result.body) { | ||
return await parseDeferredReadableStream(result.body); | ||
} | ||
return result; | ||
} | ||
function unwrapServerResponse(result) { | ||
if (isDeferredData(result)) { | ||
return result.data; | ||
} | ||
if (isResponse(result)) { | ||
let contentType = result.headers.get("Content-Type"); | ||
// Check between word boundaries instead of startsWith() due to the last | ||
// paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type | ||
if (contentType && /\bapplication\/json\b/.test(contentType)) { | ||
return result.json(); | ||
} else { | ||
return new TransitionRedirect(url.pathname + url.search + url.hash, response.headers.get("X-Remix-Revalidate") !== null); | ||
return result.text(); | ||
} | ||
} | ||
return result; | ||
} | ||
function getRedirect(response) { | ||
let status = parseInt(response.headers.get("X-Remix-Status"), 10) || 302; | ||
let url = response.headers.get("X-Remix-Redirect"); | ||
let headers = {}; | ||
let revalidate = response.headers.get("X-Remix-Revalidate"); | ||
if (revalidate) { | ||
headers["X-Remix-Revalidate"] = revalidate; | ||
} | ||
let reloadDocument = response.headers.get("X-Remix-Reload-Document"); | ||
if (reloadDocument) { | ||
headers["X-Remix-Reload-Document"] = reloadDocument; | ||
} | ||
let replace = response.headers.get("X-Remix-Replace"); | ||
if (replace) { | ||
headers["X-Remix-Replace"] = replace; | ||
} | ||
return redirect(url, { | ||
status, | ||
headers | ||
}); | ||
} | ||
return null; | ||
// Our compiler generates the default export as `{}` when no default is provided, | ||
// which can lead us to trying to use that as a Component in RR and calling | ||
// createElement on it. Patching here as a quick fix and hoping it's no longer | ||
// an issue in Vite. | ||
function getRouteModuleComponent(routeModule) { | ||
if (routeModule.default == null) return undefined; | ||
let isEmptyObject = typeof routeModule.default === "object" && Object.keys(routeModule.default).length === 0; | ||
if (!isEmptyObject) { | ||
return routeModule.default; | ||
} | ||
} | ||
function shouldHydrateRouteLoader(route, routeModule, isSpaMode) { | ||
return isSpaMode && route.id !== "root" || routeModule.clientLoader != null && (routeModule.clientLoader.hydrate === true || route.hasLoader !== true); | ||
} | ||
export { createClientRoute, createClientRoutes }; | ||
export { createClientRoutes, createClientRoutesWithHMRRevalidationOptOut, createServerRoutes, shouldHydrateRouteLoader }; |
/** | ||
* @remix-run/react v0.0.0-nightly-81ec1b7-20220830 | ||
* @remix-run/react v0.0.0-nightly-825f70ebd-20240914 | ||
* | ||
@@ -11,16 +11,9 @@ * Copyright (c) Remix Software Inc. | ||
*/ | ||
import { extends as _extends } from './_virtual/_rollupPluginBabelHelpers.js'; | ||
import * as React from 'react'; | ||
import { useLocation } from 'react-router-dom'; | ||
import { useBeforeUnload, useTransition } from './components.js'; | ||
import { useLocation, useMatches, UNSAFE_useScrollRestoration } from 'react-router-dom'; | ||
import { useRemixContext } from './components.js'; | ||
let STORAGE_KEY = "positions"; | ||
let positions = {}; | ||
if (typeof document !== "undefined") { | ||
let sessionPositions = sessionStorage.getItem(STORAGE_KEY); | ||
if (sessionPositions) { | ||
positions = JSON.parse(sessionPositions); | ||
} | ||
} | ||
/** | ||
@@ -30,20 +23,39 @@ * This component will emulate the browser's scroll restoration on location | ||
* | ||
* @see https://remix.run/api/remix#scrollrestoration | ||
* @see https://remix.run/components/scroll-restoration | ||
*/ | ||
function ScrollRestoration({ | ||
nonce = undefined | ||
getKey, | ||
...props | ||
}) { | ||
useScrollRestoration(); // wait for the browser to restore it on its own | ||
let { | ||
isSpaMode | ||
} = useRemixContext(); | ||
let location = useLocation(); | ||
let matches = useMatches(); | ||
UNSAFE_useScrollRestoration({ | ||
getKey, | ||
storageKey: STORAGE_KEY | ||
}); | ||
React.useEffect(() => { | ||
window.history.scrollRestoration = "manual"; | ||
}, []); // let the browser restore on it's own for refresh | ||
// In order to support `getKey`, we need to compute a "key" here so we can | ||
// hydrate that up so that SSR scroll restoration isn't waiting on React to | ||
// hydrate. *However*, our key on the server is not the same as our key on | ||
// the client! So if the user's getKey implementation returns the SSR | ||
// location key, then let's ignore it and let our inline <script> below pick | ||
// up the client side history state key | ||
let key = React.useMemo(() => { | ||
if (!getKey) return null; | ||
let userKey = getKey(location, matches); | ||
return userKey !== location.key ? userKey : null; | ||
}, | ||
// Nah, we only need this the first time for the SSR render | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
[]); | ||
useBeforeUnload(React.useCallback(() => { | ||
window.history.scrollRestoration = "auto"; | ||
}, [])); | ||
let restoreScroll = (STORAGE_KEY => { | ||
// In SPA Mode, there's nothing to restore on initial render since we didn't | ||
// render anything on the server | ||
if (isSpaMode) { | ||
return null; | ||
} | ||
let restoreScroll = ((STORAGE_KEY, restoreKey) => { | ||
if (!window.history.state || !window.history.state.key) { | ||
@@ -55,7 +67,5 @@ let key = Math.random().toString(32).slice(2); | ||
} | ||
try { | ||
let positions = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || "{}"); | ||
let storedY = positions[window.history.state.key]; | ||
let storedY = positions[restoreKey || window.history.state.key]; | ||
if (typeof storedY === "number") { | ||
@@ -69,76 +79,10 @@ window.scrollTo(0, storedY); | ||
}).toString(); | ||
return /*#__PURE__*/React.createElement("script", { | ||
nonce: nonce, | ||
return /*#__PURE__*/React.createElement("script", _extends({}, props, { | ||
suppressHydrationWarning: true, | ||
dangerouslySetInnerHTML: { | ||
__html: `(${restoreScroll})(${JSON.stringify(STORAGE_KEY)})` | ||
__html: `(${restoreScroll})(${JSON.stringify(STORAGE_KEY)}, ${JSON.stringify(key)})` | ||
} | ||
}); | ||
})); | ||
} | ||
let hydrated = false; | ||
function useScrollRestoration() { | ||
let location = useLocation(); | ||
let transition = useTransition(); | ||
let wasSubmissionRef = React.useRef(false); | ||
React.useEffect(() => { | ||
if (transition.submission) { | ||
wasSubmissionRef.current = true; | ||
} | ||
}, [transition]); | ||
React.useEffect(() => { | ||
if (transition.location) { | ||
positions[location.key] = window.scrollY; | ||
} | ||
}, [transition, location]); | ||
useBeforeUnload(React.useCallback(() => { | ||
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(positions)); | ||
}, [])); | ||
if (typeof document !== "undefined") { | ||
// eslint-disable-next-line | ||
React.useLayoutEffect(() => { | ||
// don't do anything on hydration, the component already did this with an | ||
// inline script. | ||
if (!hydrated) { | ||
hydrated = true; | ||
return; | ||
} | ||
let y = positions[location.key]; // been here before, scroll to it | ||
if (y != undefined) { | ||
window.scrollTo(0, y); | ||
return; | ||
} // try to scroll to the hash | ||
if (location.hash) { | ||
let el = document.getElementById(location.hash.slice(1)); | ||
if (el) { | ||
el.scrollIntoView(); | ||
return; | ||
} | ||
} // don't do anything on submissions | ||
if (wasSubmissionRef.current === true) { | ||
wasSubmissionRef.current = false; | ||
return; | ||
} // otherwise go to the top on new locations | ||
window.scrollTo(0, 0); | ||
}, [location]); | ||
} | ||
React.useEffect(() => { | ||
if (transition.submission) { | ||
wasSubmissionRef.current = true; | ||
} | ||
}, [transition]); | ||
} | ||
export { ScrollRestoration }; |
/** | ||
* @remix-run/react v0.0.0-nightly-81ec1b7-20220830 | ||
* @remix-run/react v0.0.0-nightly-825f70ebd-20240914 | ||
* | ||
@@ -11,8 +11,9 @@ * Copyright (c) Remix Software Inc. | ||
*/ | ||
import { Action, createPath } from 'history'; | ||
import * as React from 'react'; | ||
import { RemixEntry } from './components.js'; | ||
import { createStaticRouter, StaticRouterProvider } from 'react-router-dom/server'; | ||
import { RemixContext } from './components.js'; | ||
import { RemixErrorBoundary } from './errorBoundaries.js'; | ||
import { createServerRoutes, shouldHydrateRouteLoader } from './routes.js'; | ||
import { StreamTransfer } from './single-fetch.js'; | ||
// TODO: We eventually might not want to import anything directly from `history` | ||
/** | ||
@@ -25,3 +26,5 @@ * The entry point for a Remix app when it is rendered on the server (in | ||
context, | ||
url | ||
url, | ||
abortDelay, | ||
nonce | ||
}) { | ||
@@ -31,49 +34,63 @@ if (typeof url === "string") { | ||
} | ||
let { | ||
manifest, | ||
routeModules, | ||
criticalCss, | ||
serverHandoffString | ||
} = context; | ||
let routes = createServerRoutes(manifest.routes, routeModules, context.future, context.isSpaMode); | ||
let location = { | ||
pathname: url.pathname, | ||
search: url.search, | ||
hash: "", | ||
state: null, | ||
key: "default" | ||
// Create a shallow clone of `loaderData` we can mutate for partial hydration. | ||
// When a route exports a `clientLoader` and a `HydrateFallback`, we want to | ||
// render the fallback on the server so we clear our the `loaderData` during SSR. | ||
// Is it important not to change the `context` reference here since we use it | ||
// for context._deepestRenderedBoundaryId tracking | ||
context.staticHandlerContext.loaderData = { | ||
...context.staticHandlerContext.loaderData | ||
}; | ||
let staticNavigator = { | ||
createHref(to) { | ||
return typeof to === "string" ? to : createPath(to); | ||
}, | ||
push(to) { | ||
throw new Error(`You cannot use navigator.push() on the server because it is a stateless ` + `environment. This error was probably triggered when you did a ` + `\`navigate(${JSON.stringify(to)})\` somewhere in your app.`); | ||
}, | ||
replace(to) { | ||
throw new Error(`You cannot use navigator.replace() on the server because it is a stateless ` + `environment. This error was probably triggered when you did a ` + `\`navigate(${JSON.stringify(to)}, { replace: true })\` somewhere ` + `in your app.`); | ||
}, | ||
go(delta) { | ||
throw new Error(`You cannot use navigator.go() on the server because it is a stateless ` + `environment. This error was probably triggered when you did a ` + `\`navigate(${delta})\` somewhere in your app.`); | ||
}, | ||
back() { | ||
throw new Error(`You cannot use navigator.back() on the server because it is a stateless ` + `environment.`); | ||
}, | ||
forward() { | ||
throw new Error(`You cannot use navigator.forward() on the server because it is a stateless ` + `environment.`); | ||
}, | ||
block() { | ||
throw new Error(`You cannot use navigator.block() on the server because it is a stateless ` + `environment.`); | ||
for (let match of context.staticHandlerContext.matches) { | ||
let routeId = match.route.id; | ||
let route = routeModules[routeId]; | ||
let manifestRoute = context.manifest.routes[routeId]; | ||
// Clear out the loaderData to avoid rendering the route component when the | ||
// route opted into clientLoader hydration and either: | ||
// * gave us a HydrateFallback | ||
// * or doesn't have a server loader and we have no data to render | ||
if (route && shouldHydrateRouteLoader(manifestRoute, route, context.isSpaMode) && (route.HydrateFallback || !manifestRoute.hasLoader)) { | ||
context.staticHandlerContext.loaderData[routeId] = undefined; | ||
} | ||
}; | ||
return /*#__PURE__*/React.createElement(RemixEntry, { | ||
} | ||
let router = createStaticRouter(routes, context.staticHandlerContext, { | ||
future: { | ||
v7_partialHydration: true, | ||
v7_relativeSplatPath: context.future.v3_relativeSplatPath | ||
} | ||
}); | ||
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(RemixContext.Provider, { | ||
value: { | ||
manifest, | ||
routeModules, | ||
criticalCss, | ||
serverHandoffString, | ||
future: context.future, | ||
isSpaMode: context.isSpaMode, | ||
serializeError: context.serializeError, | ||
abortDelay, | ||
renderMeta: context.renderMeta | ||
} | ||
}, /*#__PURE__*/React.createElement(RemixErrorBoundary, { | ||
location: router.state.location | ||
}, /*#__PURE__*/React.createElement(StaticRouterProvider, { | ||
router: router, | ||
context: context.staticHandlerContext, | ||
hydrate: false | ||
}))), context.future.unstable_singleFetch && context.serverHandoffStream ? /*#__PURE__*/React.createElement(React.Suspense, null, /*#__PURE__*/React.createElement(StreamTransfer, { | ||
context: context, | ||
action: Action.Pop, | ||
location: location, | ||
navigator: staticNavigator, | ||
static: true | ||
}); | ||
identifier: 0, | ||
reader: context.serverHandoffStream.getReader(), | ||
textDecoder: new TextDecoder(), | ||
nonce: nonce | ||
})) : null); | ||
} | ||
export { RemixServer }; |
@@ -0,15 +1,14 @@ | ||
export type { ErrorResponse, Fetcher, FetcherWithComponents, FormEncType, FormMethod, Location, NavigateFunction, Navigation, Params, Path, ShouldRevalidateFunction, ShouldRevalidateFunctionArgs, SubmitFunction, SubmitOptions, Blocker, BlockerFunction, } from "react-router-dom"; | ||
export { createPath, createRoutesFromChildren, createRoutesFromElements, createSearchParams, generatePath, matchPath, matchRoutes, parsePath, renderMatches, resolvePath, Navigate, NavigationType, Outlet, Route, Routes, useAsyncError, useAsyncValue, isRouteErrorResponse, useBeforeUnload, useFetchers, useFormAction, useHref, useInRouterContext, useLinkClickHandler, useLocation, useMatch, useNavigate, useNavigation, useNavigationType, useOutlet, useOutletContext, useParams, useResolvedPath, useRevalidator, useRouteError, useRoutes, useSearchParams, useSubmit, useBlocker, unstable_usePrompt, unstable_useViewTransitionState, } from "react-router-dom"; | ||
export { defer, json, redirect, redirectDocument, replace, unstable_data, } from "@remix-run/server-runtime"; | ||
export type { RemixBrowserProps } from "./browser"; | ||
export { RemixBrowser } from "./browser"; | ||
export type { Location, NavigateFunction, Params, Path, } from "react-router-dom"; | ||
export { Outlet, useHref, useLocation, useNavigate, useNavigationType, useOutlet, useOutletContext, useParams, useResolvedPath, useSearchParams, } from "react-router-dom"; | ||
export type { FetcherWithComponents, FormProps, RouteMatch, SubmitOptions, SubmitFunction, RemixNavLinkProps as NavLinkProps, RemixLinkProps as LinkProps, } from "./components"; | ||
export { Meta, Links, Scripts, Link, NavLink, Form, PrefetchPageLinks, LiveReload, useFormAction, useSubmit, useTransition, useFetcher, useFetchers, useLoaderData, useActionData, useBeforeUnload, useMatches, } from "./components"; | ||
export type { FormMethod, FormEncType } from "./data"; | ||
export type { ThrownResponse } from "./errors"; | ||
export { useCatch } from "./errorBoundaries"; | ||
export type { AwaitProps, RemixFormProps as FormProps, RemixNavLinkProps as NavLinkProps, RemixLinkProps as LinkProps, UIMatch, } from "./components"; | ||
export { Await, Meta, Links, Scripts, Form, Link, NavLink, PrefetchPageLinks, LiveReload, useFetcher, useLoaderData, useRouteLoaderData, useActionData, useMatches, RemixContext as UNSAFE_RemixContext, } from "./components"; | ||
export type { HtmlLinkDescriptor } from "./links"; | ||
export type { ShouldReloadFunction, HtmlMetaDescriptor } from "./routeModules"; | ||
export type { ClientActionFunction, ClientActionFunctionArgs, ClientLoaderFunction, ClientLoaderFunctionArgs, MetaArgs, MetaMatch as UNSAFE_MetaMatch, MetaDescriptor, MetaFunction, RouteModules as UNSAFE_RouteModules, } from "./routeModules"; | ||
export { ScrollRestoration } from "./scroll-restoration"; | ||
export type { RemixServerProps } from "./server"; | ||
export { RemixServer } from "./server"; | ||
export type { Fetcher } from "./transition"; | ||
export type { 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-81ec1b7-20220830 | ||
* @remix-run/react v0.0.0-nightly-825f70ebd-20240914 | ||
* | ||
@@ -15,6 +15,6 @@ * Copyright (c) Remix Software Inc. | ||
var reactRouterDom = require('react-router-dom'); | ||
var serverRuntime = require('@remix-run/server-runtime'); | ||
var browser = require('./browser.js'); | ||
var reactRouterDom = require('react-router-dom'); | ||
var components = require('./components.js'); | ||
var errorBoundaries = require('./errorBoundaries.js'); | ||
var scrollRestoration = require('./scroll-restoration.js'); | ||
@@ -25,3 +25,10 @@ var server = require('./server.js'); | ||
exports.RemixBrowser = browser.RemixBrowser; | ||
Object.defineProperty(exports, 'Navigate', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.Navigate; } | ||
}); | ||
Object.defineProperty(exports, 'NavigationType', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.NavigationType; } | ||
}); | ||
Object.defineProperty(exports, 'Outlet', { | ||
@@ -31,2 +38,86 @@ enumerable: true, | ||
}); | ||
Object.defineProperty(exports, 'Route', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.Route; } | ||
}); | ||
Object.defineProperty(exports, 'Routes', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.Routes; } | ||
}); | ||
Object.defineProperty(exports, 'createPath', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.createPath; } | ||
}); | ||
Object.defineProperty(exports, 'createRoutesFromChildren', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.createRoutesFromChildren; } | ||
}); | ||
Object.defineProperty(exports, 'createRoutesFromElements', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.createRoutesFromElements; } | ||
}); | ||
Object.defineProperty(exports, 'createSearchParams', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.createSearchParams; } | ||
}); | ||
Object.defineProperty(exports, 'generatePath', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.generatePath; } | ||
}); | ||
Object.defineProperty(exports, 'isRouteErrorResponse', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.isRouteErrorResponse; } | ||
}); | ||
Object.defineProperty(exports, 'matchPath', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.matchPath; } | ||
}); | ||
Object.defineProperty(exports, 'matchRoutes', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.matchRoutes; } | ||
}); | ||
Object.defineProperty(exports, 'parsePath', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.parsePath; } | ||
}); | ||
Object.defineProperty(exports, 'renderMatches', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.renderMatches; } | ||
}); | ||
Object.defineProperty(exports, 'resolvePath', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.resolvePath; } | ||
}); | ||
Object.defineProperty(exports, 'unstable_usePrompt', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.unstable_usePrompt; } | ||
}); | ||
Object.defineProperty(exports, 'unstable_useViewTransitionState', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.unstable_useViewTransitionState; } | ||
}); | ||
Object.defineProperty(exports, 'useAsyncError', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.useAsyncError; } | ||
}); | ||
Object.defineProperty(exports, 'useAsyncValue', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.useAsyncValue; } | ||
}); | ||
Object.defineProperty(exports, 'useBeforeUnload', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.useBeforeUnload; } | ||
}); | ||
Object.defineProperty(exports, 'useBlocker', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.useBlocker; } | ||
}); | ||
Object.defineProperty(exports, 'useFetchers', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.useFetchers; } | ||
}); | ||
Object.defineProperty(exports, 'useFormAction', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.useFormAction; } | ||
}); | ||
Object.defineProperty(exports, 'useHref', { | ||
@@ -36,2 +127,10 @@ enumerable: true, | ||
}); | ||
Object.defineProperty(exports, 'useInRouterContext', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.useInRouterContext; } | ||
}); | ||
Object.defineProperty(exports, 'useLinkClickHandler', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.useLinkClickHandler; } | ||
}); | ||
Object.defineProperty(exports, 'useLocation', { | ||
@@ -41,2 +140,6 @@ enumerable: true, | ||
}); | ||
Object.defineProperty(exports, 'useMatch', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.useMatch; } | ||
}); | ||
Object.defineProperty(exports, 'useNavigate', { | ||
@@ -46,2 +149,6 @@ enumerable: true, | ||
}); | ||
Object.defineProperty(exports, 'useNavigation', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.useNavigation; } | ||
}); | ||
Object.defineProperty(exports, 'useNavigationType', { | ||
@@ -67,2 +174,14 @@ enumerable: true, | ||
}); | ||
Object.defineProperty(exports, 'useRevalidator', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.useRevalidator; } | ||
}); | ||
Object.defineProperty(exports, 'useRouteError', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.useRouteError; } | ||
}); | ||
Object.defineProperty(exports, 'useRoutes', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.useRoutes; } | ||
}); | ||
Object.defineProperty(exports, 'useSearchParams', { | ||
@@ -72,2 +191,32 @@ enumerable: true, | ||
}); | ||
Object.defineProperty(exports, 'useSubmit', { | ||
enumerable: true, | ||
get: function () { return reactRouterDom.useSubmit; } | ||
}); | ||
Object.defineProperty(exports, 'defer', { | ||
enumerable: true, | ||
get: function () { return serverRuntime.defer; } | ||
}); | ||
Object.defineProperty(exports, 'json', { | ||
enumerable: true, | ||
get: function () { return serverRuntime.json; } | ||
}); | ||
Object.defineProperty(exports, 'redirect', { | ||
enumerable: true, | ||
get: function () { return serverRuntime.redirect; } | ||
}); | ||
Object.defineProperty(exports, 'redirectDocument', { | ||
enumerable: true, | ||
get: function () { return serverRuntime.redirectDocument; } | ||
}); | ||
Object.defineProperty(exports, 'replace', { | ||
enumerable: true, | ||
get: function () { return serverRuntime.replace; } | ||
}); | ||
Object.defineProperty(exports, 'unstable_data', { | ||
enumerable: true, | ||
get: function () { return serverRuntime.unstable_data; } | ||
}); | ||
exports.RemixBrowser = browser.RemixBrowser; | ||
exports.Await = components.Await; | ||
exports.Form = components.Form; | ||
@@ -81,13 +230,9 @@ exports.Link = components.Link; | ||
exports.Scripts = components.Scripts; | ||
exports.UNSAFE_RemixContext = components.RemixContext; | ||
exports.useActionData = components.useActionData; | ||
exports.useBeforeUnload = components.useBeforeUnload; | ||
exports.useFetcher = components.useFetcher; | ||
exports.useFetchers = components.useFetchers; | ||
exports.useFormAction = components.useFormAction; | ||
exports.useLoaderData = components.useLoaderData; | ||
exports.useMatches = components.useMatches; | ||
exports.useSubmit = components.useSubmit; | ||
exports.useTransition = components.useTransition; | ||
exports.useCatch = errorBoundaries.useCatch; | ||
exports.useRouteLoaderData = components.useRouteLoaderData; | ||
exports.ScrollRestoration = scrollRestoration.ScrollRestoration; | ||
exports.RemixServer = server.RemixServer; |
/** | ||
* @remix-run/react v0.0.0-nightly-81ec1b7-20220830 | ||
* @remix-run/react v0.0.0-nightly-825f70ebd-20240914 | ||
* | ||
@@ -4,0 +4,0 @@ * Copyright (c) Remix Software Inc. |
@@ -1,8 +0,8 @@ | ||
import type { Location } from "history"; | ||
import type { AgnosticDataRouteMatch } from "@remix-run/router"; | ||
import type { Location } from "react-router-dom"; | ||
import type { AssetsManifest } from "./entry"; | ||
import type { ClientRoute } from "./routes"; | ||
import type { RouteMatch } from "./routeMatching"; | ||
import type { RouteModules, RouteModule } from "./routeModules"; | ||
declare type Primitive = null | undefined | string | number | boolean | symbol | bigint; | ||
declare type LiteralUnion<LiteralType, BaseType extends Primitive> = LiteralType | (BaseType & Record<never, never>); | ||
import type { EntryRoute } from "./routes"; | ||
type Primitive = null | undefined | string | number | boolean | symbol | bigint; | ||
type LiteralUnion<LiteralType, BaseType extends Primitive> = LiteralType | (BaseType & Record<never, never>); | ||
interface HtmlLinkProps { | ||
@@ -99,16 +99,7 @@ /** | ||
*/ | ||
export declare type HtmlLinkDescriptor = ((HtmlLinkProps & Pick<Required<HtmlLinkProps>, "href">) | (HtmlLinkPreloadImage & Pick<Required<HtmlLinkPreloadImage>, "imageSizes">) | (HtmlLinkPreloadImage & Pick<Required<HtmlLinkPreloadImage>, "href"> & { | ||
export type HtmlLinkDescriptor = (HtmlLinkProps & Pick<Required<HtmlLinkProps>, "href">) | (HtmlLinkPreloadImage & Pick<Required<HtmlLinkPreloadImage>, "imageSizes">) | (HtmlLinkPreloadImage & Pick<Required<HtmlLinkPreloadImage>, "href"> & { | ||
imageSizes?: never; | ||
})) & { | ||
}); | ||
export interface PrefetchPageDescriptor extends Omit<HtmlLinkDescriptor, "href" | "rel" | "type" | "sizes" | "imageSrcSet" | "imageSizes" | "as" | "color" | "title"> { | ||
/** | ||
* @deprecated Use `imageSrcSet` instead. | ||
*/ | ||
imagesrcset?: string; | ||
/** | ||
* @deprecated Use `imageSizes` instead. | ||
*/ | ||
imagesizes?: string; | ||
}; | ||
export interface PrefetchPageDescriptor extends Omit<HtmlLinkDescriptor, "href" | "rel" | "type" | "sizes" | "imageSrcSet" | "imageSizes" | "imagesrcset" | "imagesizes" | "as" | "color" | "title"> { | ||
/** | ||
* The absolute path of the page to prefetch. | ||
@@ -118,3 +109,3 @@ */ | ||
} | ||
export declare type LinkDescriptor = HtmlLinkDescriptor | PrefetchPageDescriptor; | ||
export type LinkDescriptor = HtmlLinkDescriptor | PrefetchPageDescriptor; | ||
/** | ||
@@ -124,11 +115,17 @@ * Gets all the links for a set of matches. The modules are assumed to have been | ||
*/ | ||
export declare function getLinksForMatches(matches: RouteMatch<ClientRoute>[], routeModules: RouteModules, manifest: AssetsManifest): LinkDescriptor[]; | ||
export declare function prefetchStyleLinks(routeModule: RouteModule): Promise<void>; | ||
export declare function getKeyedLinksForMatches(matches: AgnosticDataRouteMatch[], routeModules: RouteModules, manifest: AssetsManifest): KeyedLinkDescriptor[]; | ||
export declare function prefetchStyleLinks(route: EntryRoute, routeModule: RouteModule): Promise<void>; | ||
export declare function isPageLinkDescriptor(object: any): object is PrefetchPageDescriptor; | ||
export declare function isHtmlLinkDescriptor(object: any): object is HtmlLinkDescriptor; | ||
export declare function getStylesheetPrefetchLinks(matches: RouteMatch<ClientRoute>[], routeModules: RouteModules): Promise<HtmlLinkDescriptor[]>; | ||
export declare function getNewMatchesForLinks(page: string, nextMatches: RouteMatch<ClientRoute>[], currentMatches: RouteMatch<ClientRoute>[], location: Location, mode: "data" | "assets"): RouteMatch<ClientRoute>[]; | ||
export declare function getDataLinkHrefs(page: string, matches: RouteMatch<ClientRoute>[], manifest: AssetsManifest): string[]; | ||
export declare function getModuleLinkHrefs(matches: RouteMatch<ClientRoute>[], manifestPatch: AssetsManifest): string[]; | ||
export declare function dedupe(descriptors: LinkDescriptor[], preloads: string[]): LinkDescriptor[]; | ||
export type KeyedHtmlLinkDescriptor = { | ||
key: string; | ||
link: HtmlLinkDescriptor; | ||
}; | ||
export declare function getKeyedPrefetchLinks(matches: AgnosticDataRouteMatch[], manifest: AssetsManifest, routeModules: RouteModules): Promise<KeyedHtmlLinkDescriptor[]>; | ||
export declare function getNewMatchesForLinks(page: string, nextMatches: AgnosticDataRouteMatch[], currentMatches: AgnosticDataRouteMatch[], manifest: AssetsManifest, location: Location, mode: "data" | "assets"): AgnosticDataRouteMatch[]; | ||
export declare function getDataLinkHrefs(page: string, matches: AgnosticDataRouteMatch[], manifest: AssetsManifest): string[]; | ||
export declare function getModuleLinkHrefs(matches: AgnosticDataRouteMatch[], manifestPatch: AssetsManifest): string[]; | ||
type KeyedLinkDescriptor<Descriptor extends LinkDescriptor = LinkDescriptor> = { | ||
key: string; | ||
link: Descriptor; | ||
}; | ||
export {}; |
/** | ||
* @remix-run/react v0.0.0-nightly-81ec1b7-20220830 | ||
* @remix-run/react v0.0.0-nightly-825f70ebd-20240914 | ||
* | ||
@@ -15,6 +15,11 @@ * Copyright (c) Remix Software Inc. | ||
var history = require('history'); | ||
var reactRouterDom = require('react-router-dom'); | ||
var routeModules = require('./routeModules.js'); | ||
// TODO: We eventually might not want to import anything directly from `history` | ||
/** | ||
* Represents a `<link>` element. | ||
* | ||
* WHATWG Specification: https://html.spec.whatwg.org/multipage/semantics.html#the-link-element | ||
*/ | ||
//////////////////////////////////////////////////////////////////////////////// | ||
@@ -26,21 +31,28 @@ | ||
*/ | ||
function getLinksForMatches(matches, routeModules, manifest) { | ||
function getKeyedLinksForMatches(matches, routeModules, manifest) { | ||
let descriptors = matches.map(match => { | ||
var _module$links; | ||
let module = routeModules[match.route.id]; | ||
return ((_module$links = module.links) === null || _module$links === void 0 ? void 0 : _module$links.call(module)) || []; | ||
}).flat(1); | ||
let route = manifest.routes[match.route.id]; | ||
return [route.css ? route.css.map(href => ({ | ||
rel: "stylesheet", | ||
href | ||
})) : [], (module === null || module === void 0 ? void 0 : (_module$links = module.links) === null || _module$links === void 0 ? void 0 : _module$links.call(module)) || []]; | ||
}).flat(2); | ||
let preloads = getCurrentPageModulePreloadHrefs(matches, manifest); | ||
return dedupe(descriptors, preloads); | ||
return dedupeLinkDescriptors(descriptors, preloads); | ||
} | ||
async function prefetchStyleLinks(routeModule) { | ||
if (!routeModule.links) return; | ||
let descriptors = routeModule.links(); | ||
if (!descriptors) return; | ||
async function prefetchStyleLinks(route, routeModule) { | ||
var _route$css, _routeModule$links; | ||
if (!route.css && !routeModule.links || !isPreloadSupported()) return; | ||
let descriptors = [((_route$css = route.css) === null || _route$css === void 0 ? void 0 : _route$css.map(href => ({ | ||
rel: "stylesheet", | ||
href | ||
}))) ?? [], ((_routeModule$links = routeModule.links) === null || _routeModule$links === void 0 ? void 0 : _routeModule$links.call(routeModule)) ?? []].flat(1); | ||
if (descriptors.length === 0) return; | ||
let styleLinks = []; | ||
for (let descriptor of descriptors) { | ||
if (!isPageLinkDescriptor(descriptor) && descriptor.rel === "stylesheet") { | ||
styleLinks.push({ ...descriptor, | ||
styleLinks.push({ | ||
...descriptor, | ||
rel: "preload", | ||
@@ -50,9 +62,9 @@ as: "style" | ||
} | ||
} // don't block for non-matching media queries | ||
} | ||
let matchingLinks = styleLinks.filter(link => !link.media || window.matchMedia(link.media).matches); | ||
// don't block for non-matching media queries, or for stylesheets that are | ||
// already in the DOM (active route revalidations) | ||
let matchingLinks = styleLinks.filter(link => (!link.media || window.matchMedia(link.media).matches) && !document.querySelector(`link[rel="stylesheet"][href="${link.href}"]`)); | ||
await Promise.all(matchingLinks.map(prefetchStyleLink)); | ||
} | ||
async function prefetchStyleLink(descriptor) { | ||
@@ -62,3 +74,2 @@ return new Promise(resolve => { | ||
Object.assign(link, descriptor); | ||
function removeLink() { | ||
@@ -72,3 +83,2 @@ // if a navigation interrupts this prefetch React will update the <head> | ||
} | ||
link.onload = () => { | ||
@@ -78,3 +88,2 @@ removeLink(); | ||
}; | ||
link.onerror = () => { | ||
@@ -84,8 +93,7 @@ removeLink(); | ||
}; | ||
document.head.appendChild(link); | ||
}); | ||
} //////////////////////////////////////////////////////////////////////////////// | ||
} | ||
//////////////////////////////////////////////////////////////////////////////// | ||
function isPageLinkDescriptor(object) { | ||
@@ -95,28 +103,32 @@ return object != null && typeof object.page === "string"; | ||
function isHtmlLinkDescriptor(object) { | ||
if (object == null) return false; // <link> may not have an href if <link rel="preload"> is used with imagesrcset + imagesizes | ||
if (object == null) { | ||
return false; | ||
} | ||
// <link> may not have an href if <link rel="preload"> is used with imageSrcSet + imageSizes | ||
// https://github.com/remix-run/remix/issues/184 | ||
// https://html.spec.whatwg.org/commit-snapshots/cb4f5ff75de5f4cbd7013c4abad02f21c77d4d1c/#attr-link-imagesrcset | ||
if (object.href == null) { | ||
return object.rel === "preload" && (typeof object.imageSrcSet === "string" || typeof object.imagesrcset === "string") && (typeof object.imageSizes === "string" || typeof object.imagesizes === "string"); | ||
return object.rel === "preload" && typeof object.imageSrcSet === "string" && typeof object.imageSizes === "string"; | ||
} | ||
return typeof object.rel === "string" && typeof object.href === "string"; | ||
} | ||
async function getStylesheetPrefetchLinks(matches, routeModules$1) { | ||
async function getKeyedPrefetchLinks(matches, manifest, routeModules$1) { | ||
let links = await Promise.all(matches.map(async match => { | ||
let mod = await routeModules.loadRouteModule(match.route, routeModules$1); | ||
let mod = await routeModules.loadRouteModule(manifest.routes[match.route.id], routeModules$1); | ||
return mod.links ? mod.links() : []; | ||
})); | ||
return links.flat(1).filter(isHtmlLinkDescriptor).filter(link => link.rel === "stylesheet" || link.rel === "preload").map(link => link.rel === "preload" ? { ...link, | ||
rel: "prefetch" | ||
} : { ...link, | ||
return dedupeLinkDescriptors(links.flat(1).filter(isHtmlLinkDescriptor).filter(link => link.rel === "stylesheet" || link.rel === "preload").map(link => link.rel === "stylesheet" ? { | ||
...link, | ||
rel: "prefetch", | ||
as: "style" | ||
}); | ||
} // This is ridiculously identical to transition.ts `filterMatchesToLoad` | ||
} : { | ||
...link, | ||
rel: "prefetch" | ||
})); | ||
} | ||
function getNewMatchesForLinks(page, nextMatches, currentMatches, location, mode) { | ||
// This is ridiculously identical to transition.ts `filterMatchesToLoad` | ||
function getNewMatchesForLinks(page, nextMatches, currentMatches, manifest, location, mode) { | ||
let path = parsePathPatch(page); | ||
let isNew = (match, index) => { | ||
@@ -126,37 +138,43 @@ if (!currentMatches[index]) return true; | ||
}; | ||
let matchPathChanged = (match, index) => { | ||
var _currentMatches$index; | ||
return (// param change, /users/123 -> /users/456 | ||
currentMatches[index].pathname !== match.pathname || // splat param changed, which is not present in match.path | ||
return ( | ||
// param change, /users/123 -> /users/456 | ||
currentMatches[index].pathname !== match.pathname || | ||
// splat param changed, which is not present in match.path | ||
// e.g. /files/images/avatar.jpg -> files/finances.xls | ||
((_currentMatches$index = currentMatches[index].route.path) === null || _currentMatches$index === void 0 ? void 0 : _currentMatches$index.endsWith("*")) && currentMatches[index].params["*"] !== match.params["*"] | ||
); | ||
}; // NOTE: keep this mostly up-to-date w/ the transition data diff, but this | ||
}; | ||
// NOTE: keep this mostly up-to-date w/ the transition data diff, but this | ||
// version doesn't care about submissions | ||
let newMatches = mode === "data" && location.search !== path.search ? // this is really similar to stuff in transition.ts, maybe somebody smarter | ||
let newMatches = mode === "data" && location.search !== path.search ? | ||
// this is really similar to stuff in transition.ts, maybe somebody smarter | ||
// than me (or in less of a hurry) can share some of it. You're the best. | ||
nextMatches.filter((match, index) => { | ||
if (!match.route.hasLoader) { | ||
let manifestRoute = manifest.routes[match.route.id]; | ||
if (!manifestRoute.hasLoader) { | ||
return false; | ||
} | ||
if (isNew(match, index) || matchPathChanged(match, index)) { | ||
return true; | ||
} | ||
if (match.route.shouldReload) { | ||
return match.route.shouldReload({ | ||
params: match.params, | ||
prevUrl: new URL(location.pathname + location.search + location.hash, window.origin), | ||
url: new URL(page, window.origin) | ||
if (match.route.shouldRevalidate) { | ||
var _currentMatches$; | ||
let routeChoice = match.route.shouldRevalidate({ | ||
currentUrl: new URL(location.pathname + location.search + location.hash, window.origin), | ||
currentParams: ((_currentMatches$ = currentMatches[0]) === null || _currentMatches$ === void 0 ? void 0 : _currentMatches$.params) || {}, | ||
nextUrl: new URL(page, window.origin), | ||
nextParams: match.params, | ||
defaultShouldRevalidate: true | ||
}); | ||
if (typeof routeChoice === "boolean") { | ||
return routeChoice; | ||
} | ||
} | ||
return true; | ||
}) : nextMatches.filter((match, index) => { | ||
return (mode === "assets" || match.route.hasLoader) && (isNew(match, index) || matchPathChanged(match, index)); | ||
let manifestRoute = manifest.routes[match.route.id]; | ||
return (mode === "assets" || manifestRoute.hasLoader) && (isNew(match, index) || matchPathChanged(match, index)); | ||
}); | ||
@@ -167,3 +185,3 @@ return newMatches; | ||
let path = parsePathPatch(page); | ||
return dedupeHrefs(matches.filter(match => manifest.routes[match.route.id].hasLoader).map(match => { | ||
return dedupeHrefs(matches.filter(match => manifest.routes[match.route.id].hasLoader && !manifest.routes[match.route.id].hasClientLoader).map(match => { | ||
let { | ||
@@ -182,13 +200,12 @@ pathname, | ||
let hrefs = [route.module]; | ||
if (route.imports) { | ||
hrefs = hrefs.concat(route.imports); | ||
} | ||
return hrefs; | ||
}).flat(1)); | ||
} // The `<Script>` will render rel=modulepreload for the current page, we don't | ||
} | ||
// The `<Script>` will render rel=modulepreload for the current page, we don't | ||
// need to include them in a page prefetch, this gives us the list to remove | ||
// while deduping. | ||
function getCurrentPageModulePreloadHrefs(matches, manifest) { | ||
@@ -198,38 +215,42 @@ return dedupeHrefs(matches.map(match => { | ||
let hrefs = [route.module]; | ||
if (route.imports) { | ||
hrefs = hrefs.concat(route.imports); | ||
} | ||
return hrefs; | ||
}).flat(1)); | ||
} | ||
function dedupeHrefs(hrefs) { | ||
return [...new Set(hrefs)]; | ||
} | ||
function dedupe(descriptors, preloads) { | ||
function sortKeys(obj) { | ||
let sorted = {}; | ||
let keys = Object.keys(obj).sort(); | ||
for (let key of keys) { | ||
sorted[key] = obj[key]; | ||
} | ||
return sorted; | ||
} | ||
function dedupeLinkDescriptors(descriptors, preloads) { | ||
let set = new Set(); | ||
let preloadsSet = new Set(preloads); | ||
return descriptors.reduce((deduped, descriptor) => { | ||
let alreadyModulePreload = !isPageLinkDescriptor(descriptor) && descriptor.as === "script" && descriptor.href && preloadsSet.has(descriptor.href); | ||
let alreadyModulePreload = preloads && !isPageLinkDescriptor(descriptor) && descriptor.as === "script" && descriptor.href && preloadsSet.has(descriptor.href); | ||
if (alreadyModulePreload) { | ||
return deduped; | ||
} | ||
let str = JSON.stringify(descriptor); | ||
if (!set.has(str)) { | ||
set.add(str); | ||
deduped.push(descriptor); | ||
let key = JSON.stringify(sortKeys(descriptor)); | ||
if (!set.has(key)) { | ||
set.add(key); | ||
deduped.push({ | ||
key, | ||
link: descriptor | ||
}); | ||
} | ||
return deduped; | ||
}, []); | ||
} // https://github.com/remix-run/history/issues/897 | ||
} | ||
// https://github.com/remix-run/history/issues/897 | ||
function parsePathPatch(href) { | ||
let path = history.parsePath(href); | ||
let path = reactRouterDom.parsePath(href); | ||
if (path.search === undefined) path.search = ""; | ||
@@ -239,10 +260,22 @@ return path; | ||
exports.dedupe = dedupe; | ||
// Detect if this browser supports <link rel="preload"> (or has it enabled). | ||
// Originally added to handle the firefox `network.preload` config: | ||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1847811 | ||
let _isPreloadSupported; | ||
function isPreloadSupported() { | ||
if (_isPreloadSupported !== undefined) { | ||
return _isPreloadSupported; | ||
} | ||
let el = document.createElement("link"); | ||
_isPreloadSupported = el.relList.supports("preload"); | ||
el = null; | ||
return _isPreloadSupported; | ||
} | ||
exports.getDataLinkHrefs = getDataLinkHrefs; | ||
exports.getLinksForMatches = getLinksForMatches; | ||
exports.getKeyedLinksForMatches = getKeyedLinksForMatches; | ||
exports.getKeyedPrefetchLinks = getKeyedPrefetchLinks; | ||
exports.getModuleLinkHrefs = getModuleLinkHrefs; | ||
exports.getNewMatchesForLinks = getNewMatchesForLinks; | ||
exports.getStylesheetPrefetchLinks = getStylesheetPrefetchLinks; | ||
exports.isHtmlLinkDescriptor = isHtmlLinkDescriptor; | ||
exports.isPageLinkDescriptor = isPageLinkDescriptor; | ||
exports.prefetchStyleLinks = prefetchStyleLinks; |
@@ -0,1 +1,2 @@ | ||
export declare function escapeHtml(html: string): string; | ||
export interface SafeHtml { | ||
@@ -2,0 +3,0 @@ __html: string; |
/** | ||
* @remix-run/react v0.0.0-nightly-81ec1b7-20220830 | ||
* @remix-run/react v0.0.0-nightly-825f70ebd-20240914 | ||
* | ||
@@ -15,2 +15,19 @@ * Copyright (c) Remix Software Inc. | ||
// This escapeHtml utility is based on https://github.com/zertosh/htmlescape | ||
// License: https://github.com/zertosh/htmlescape/blob/0527ca7156a524d256101bb310a9f970f63078ad/LICENSE | ||
// We've chosen to inline the utility here to reduce the number of npm dependencies we have, | ||
// slightly decrease the code size compared the original package and make it esm compatible. | ||
const ESCAPE_LOOKUP = { | ||
"&": "\\u0026", | ||
">": "\\u003e", | ||
"<": "\\u003c", | ||
"\u2028": "\\u2028", | ||
"\u2029": "\\u2029" | ||
}; | ||
const ESCAPE_REGEX = /[&><\u2028\u2029]/g; | ||
function escapeHtml(html) { | ||
return html.replace(ESCAPE_REGEX, match => ESCAPE_LOOKUP[match]); | ||
} | ||
function createHtml(html) { | ||
@@ -23,1 +40,2 @@ return { | ||
exports.createHtml = createHtml; | ||
exports.escapeHtml = escapeHtml; |
@@ -1,34 +0,60 @@ | ||
import type { Location } from "history"; | ||
import type { ComponentType } from "react"; | ||
import type { Params } from "react-router"; | ||
import type { ComponentType, ReactElement } from "react"; | ||
import type { ActionFunction as RRActionFunction, ActionFunctionArgs as RRActionFunctionArgs, LoaderFunction as RRLoaderFunction, LoaderFunctionArgs as RRLoaderFunctionArgs, DataRouteMatch, Params, Location, ShouldRevalidateFunction } from "react-router-dom"; | ||
import type { LoaderFunction, SerializeFrom } from "@remix-run/server-runtime"; | ||
import type { AppData } from "./data"; | ||
import type { LinkDescriptor } from "./links"; | ||
import type { ClientRoute, EntryRoute } from "./routes"; | ||
import type { RouteData } from "./routeData"; | ||
import type { Submission } from "./transition"; | ||
import type { EntryRoute } from "./routes"; | ||
export interface RouteModules { | ||
[routeId: string]: RouteModule; | ||
[routeId: string]: RouteModule | undefined; | ||
} | ||
export interface RouteModule { | ||
CatchBoundary?: CatchBoundaryComponent; | ||
clientAction?: ClientActionFunction; | ||
clientLoader?: ClientLoaderFunction; | ||
ErrorBoundary?: ErrorBoundaryComponent; | ||
HydrateFallback?: HydrateFallbackComponent; | ||
Layout?: LayoutComponent; | ||
default: RouteComponent; | ||
handle?: RouteHandle; | ||
links?: LinksFunction; | ||
meta?: MetaFunction | HtmlMetaDescriptor; | ||
unstable_shouldReload?: ShouldReloadFunction; | ||
meta?: MetaFunction; | ||
shouldRevalidate?: ShouldRevalidateFunction; | ||
} | ||
/** | ||
* A React component that is rendered when the server throws a Response. | ||
* | ||
* @see https://remix.run/api/conventions#catchboundary | ||
* A function that handles data mutations for a route on the client | ||
*/ | ||
export declare type CatchBoundaryComponent = ComponentType<{}>; | ||
export type ClientActionFunction = (args: ClientActionFunctionArgs) => ReturnType<RRActionFunction>; | ||
/** | ||
* A React component that is rendered when there is an error on a route. | ||
* | ||
* @see https://remix.run/api/conventions#errorboundary | ||
* Arguments passed to a route `clientAction` function | ||
*/ | ||
export declare type ErrorBoundaryComponent = ComponentType<{ | ||
error: Error; | ||
export type ClientActionFunctionArgs = RRActionFunctionArgs<undefined> & { | ||
serverAction: <T = AppData>() => Promise<SerializeFrom<T>>; | ||
}; | ||
/** | ||
* A function that loads data for a route on the client | ||
*/ | ||
export type ClientLoaderFunction = ((args: ClientLoaderFunctionArgs) => ReturnType<RRLoaderFunction>) & { | ||
hydrate?: boolean; | ||
}; | ||
/** | ||
* Arguments passed to a route `clientLoader` function | ||
*/ | ||
export type ClientLoaderFunctionArgs = RRLoaderFunctionArgs<undefined> & { | ||
serverLoader: <T = AppData>() => Promise<SerializeFrom<T>>; | ||
}; | ||
/** | ||
* ErrorBoundary to display for this route | ||
*/ | ||
export type ErrorBoundaryComponent = ComponentType; | ||
/** | ||
* `<Route HydrateFallback>` component to render on initial loads | ||
* when client loaders are present | ||
*/ | ||
export type HydrateFallbackComponent = ComponentType; | ||
/** | ||
* Optional, root-only `<Route Layout>` component to wrap the root content in. | ||
* Useful for defining the <html>/<head>/<body> document shell shared by the | ||
* Component, HydrateFallback, and ErrorBoundary | ||
*/ | ||
export type LayoutComponent = ComponentType<{ | ||
children: ReactElement<unknown, ErrorBoundaryComponent | HydrateFallbackComponent | RouteComponent>; | ||
}>; | ||
@@ -39,3 +65,3 @@ /** | ||
* | ||
* @see https://remix.run/api/remix#meta-links-scripts | ||
* @see https://remix.run/route/meta | ||
*/ | ||
@@ -45,60 +71,64 @@ export interface LinksFunction { | ||
} | ||
/** | ||
* A function that returns an object of name + content pairs to use for | ||
* `<meta>` tags for a route. These tags will be merged with (and take | ||
* precedence over) tags from parent routes. | ||
* | ||
* @see https://remix.run/api/remix#meta-links-scripts | ||
*/ | ||
export interface MetaFunction { | ||
(args: { | ||
data: AppData; | ||
parentsData: RouteData; | ||
params: Params; | ||
location: Location; | ||
}): HtmlMetaDescriptor | undefined; | ||
export interface MetaMatch<RouteId extends string = string, Loader extends LoaderFunction | unknown = unknown> { | ||
id: RouteId; | ||
pathname: DataRouteMatch["pathname"]; | ||
data: Loader extends LoaderFunction ? SerializeFrom<Loader> : unknown; | ||
handle?: RouteHandle; | ||
params: DataRouteMatch["params"]; | ||
meta: MetaDescriptor[]; | ||
error?: unknown; | ||
} | ||
/** | ||
* A name/content pair used to render `<meta>` tags in a meta function for a | ||
* route. The value can be either a string, which will render a single `<meta>` | ||
* tag, or an array of strings that will render multiple tags with the same | ||
* `name` attribute. | ||
*/ | ||
export interface HtmlMetaDescriptor { | ||
charset?: "utf-8"; | ||
charSet?: "utf-8"; | ||
title?: string; | ||
[name: string]: null | string | undefined | Record<string, string> | Array<Record<string, string> | string>; | ||
export type MetaMatches<MatchLoaders extends Record<string, LoaderFunction | unknown> = Record<string, unknown>> = Array<{ | ||
[K in keyof MatchLoaders]: MetaMatch<Exclude<K, number | symbol>, MatchLoaders[K]>; | ||
}[keyof MatchLoaders]>; | ||
export interface MetaArgs<Loader extends LoaderFunction | unknown = unknown, MatchLoaders extends Record<string, LoaderFunction | unknown> = Record<string, unknown>> { | ||
data: (Loader extends LoaderFunction ? SerializeFrom<Loader> : AppData) | undefined; | ||
params: Params; | ||
location: Location; | ||
matches: MetaMatches<MatchLoaders>; | ||
error?: unknown; | ||
} | ||
/** | ||
* During client side transitions Remix will optimize reloading of routes that | ||
* are currently on the page by avoiding loading routes that aren't changing. | ||
* However, in some cases, like form submissions or search params Remix doesn't | ||
* know which routes need to be reloaded so it reloads them all to be safe. | ||
* | ||
* This function lets apps further optimize by returning `false` when Remix is | ||
* about to reload the route. A common case is a root loader with nothing but | ||
* environment variables: after form submissions the root probably doesn't need | ||
* to be reloaded. | ||
* | ||
* @see https://remix.run/api/conventions#unstable_shouldreload | ||
*/ | ||
export interface ShouldReloadFunction { | ||
(args: { | ||
url: URL; | ||
prevUrl: URL; | ||
params: Params; | ||
submission?: Submission; | ||
}): boolean; | ||
export interface MetaFunction<Loader extends LoaderFunction | unknown = unknown, MatchLoaders extends Record<string, LoaderFunction | unknown> = Record<string, unknown>> { | ||
(args: MetaArgs<Loader, MatchLoaders>): MetaDescriptor[] | undefined; | ||
} | ||
export type MetaDescriptor = { | ||
charSet: "utf-8"; | ||
} | { | ||
title: string; | ||
} | { | ||
name: string; | ||
content: string; | ||
} | { | ||
property: string; | ||
content: string; | ||
} | { | ||
httpEquiv: string; | ||
content: string; | ||
} | { | ||
"script:ld+json": LdJsonObject; | ||
} | { | ||
tagName: "meta" | "link"; | ||
[name: string]: string; | ||
} | { | ||
[name: string]: unknown; | ||
}; | ||
type LdJsonObject = { | ||
[Key in string]: LdJsonValue; | ||
} & { | ||
[Key in string]?: LdJsonValue | undefined; | ||
}; | ||
type LdJsonArray = LdJsonValue[] | readonly LdJsonValue[]; | ||
type LdJsonPrimitive = string | number | boolean | null; | ||
type LdJsonValue = LdJsonPrimitive | LdJsonObject | LdJsonArray; | ||
/** | ||
* A React component that is rendered for a route. | ||
*/ | ||
export declare type RouteComponent = ComponentType<{}>; | ||
export type RouteComponent = ComponentType<{}>; | ||
/** | ||
* An arbitrary object that is associated with a route. | ||
* | ||
* @see https://remix.run/api/conventions#handle | ||
* @see https://remix.run/route/handle | ||
*/ | ||
export declare type RouteHandle = any; | ||
export declare function loadRouteModule(route: EntryRoute | ClientRoute, routeModulesCache: RouteModules): Promise<RouteModule>; | ||
export type RouteHandle = unknown; | ||
export declare function loadRouteModule(route: EntryRoute, routeModulesCache: RouteModules): Promise<RouteModule>; | ||
export {}; |
/** | ||
* @remix-run/react v0.0.0-nightly-81ec1b7-20220830 | ||
* @remix-run/react v0.0.0-nightly-825f70ebd-20240914 | ||
* | ||
@@ -33,52 +33,38 @@ * Copyright (c) Remix Software Inc. | ||
// TODO: We eventually might not want to import anything directly from `history` | ||
// and leverage `react-router` here instead | ||
// TODO: import/export from react-router-dom | ||
/** | ||
* A function that handles data mutations for a route on the client | ||
*/ | ||
/** | ||
* A React component that is rendered when the server throws a Response. | ||
* | ||
* @see https://remix.run/api/conventions#catchboundary | ||
* Arguments passed to a route `clientAction` function | ||
*/ | ||
/** | ||
* A React component that is rendered when there is an error on a route. | ||
* | ||
* @see https://remix.run/api/conventions#errorboundary | ||
* A function that loads data for a route on the client | ||
*/ | ||
/** | ||
* A function that defines `<link>` tags to be inserted into the `<head>` of | ||
* the document on route transitions. | ||
* | ||
* @see https://remix.run/api/remix#meta-links-scripts | ||
* Arguments passed to a route `clientLoader` function | ||
*/ | ||
/** | ||
* A function that returns an object of name + content pairs to use for | ||
* `<meta>` tags for a route. These tags will be merged with (and take | ||
* precedence over) tags from parent routes. | ||
* | ||
* @see https://remix.run/api/remix#meta-links-scripts | ||
* ErrorBoundary to display for this route | ||
*/ | ||
/** | ||
* A name/content pair used to render `<meta>` tags in a meta function for a | ||
* route. The value can be either a string, which will render a single `<meta>` | ||
* tag, or an array of strings that will render multiple tags with the same | ||
* `name` attribute. | ||
* `<Route HydrateFallback>` component to render on initial loads | ||
* when client loaders are present | ||
*/ | ||
/** | ||
* During client side transitions Remix will optimize reloading of routes that | ||
* are currently on the page by avoiding loading routes that aren't changing. | ||
* However, in some cases, like form submissions or search params Remix doesn't | ||
* know which routes need to be reloaded so it reloads them all to be safe. | ||
* Optional, root-only `<Route Layout>` component to wrap the root content in. | ||
* Useful for defining the <html>/<head>/<body> document shell shared by the | ||
* Component, HydrateFallback, and ErrorBoundary | ||
*/ | ||
/** | ||
* A function that defines `<link>` tags to be inserted into the `<head>` of | ||
* the document on route transitions. | ||
* | ||
* This function lets apps further optimize by returning `false` when Remix is | ||
* about to reload the route. A common case is a root loader with nothing but | ||
* environment variables: after form submissions the root probably doesn't need | ||
* to be reloaded. | ||
* | ||
* @see https://remix.run/api/conventions#unstable_shouldreload | ||
* @see https://remix.run/route/meta | ||
*/ | ||
@@ -93,4 +79,5 @@ | ||
* | ||
* @see https://remix.run/api/conventions#handle | ||
* @see https://remix.run/route/handle | ||
*/ | ||
async function loadRouteModule(route, routeModulesCache) { | ||
@@ -100,16 +87,31 @@ if (route.id in routeModulesCache) { | ||
} | ||
try { | ||
let routeModule = await (function (t) { return Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require(t)); }); })( | ||
/* webpackIgnore: true */ | ||
route.module); | ||
let routeModule = await (function (t) { return Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require(t)); }); })( /* webpackIgnore: true */route.module); | ||
routeModulesCache[route.id] = routeModule; | ||
return routeModule; | ||
} catch (error) { | ||
// User got caught in the middle of a deploy and the CDN no longer has the | ||
// asset we're trying to import! Reload from the server and the user | ||
// (should) get the new manifest--unless the developer purged the static | ||
// assets, the manifest path, but not the documents 😬 | ||
// If we can't load the route it's likely one of 2 things: | ||
// - User got caught in the middle of a deploy and the CDN no longer has the | ||
// asset we're trying to import! Reload from the server and the user | ||
// (should) get the new manifest--unless the developer purged the static | ||
// assets, the manifest path, but not the documents 😬 | ||
// - Or, the asset trying to be imported has an error (usually in vite dev | ||
// mode), so the best we can do here is log the error for visibility | ||
// (via `Preserve log`) and reload | ||
// Log the error so it can be accessed via the `Preserve Log` setting | ||
console.error(`Error loading route module \`${route.module}\`, reloading page...`); | ||
console.error(error); | ||
if (window.__remixContext.isSpaMode && | ||
// @ts-expect-error | ||
typeof undefined !== "undefined") { | ||
// In SPA Mode (which implies vite) we don't want to perform a hard reload | ||
// on dev-time errors since it's a vite compilation error and a reload is | ||
// just going to fail with the same issue. Let the UI bubble to the error | ||
// boundary and let them see the error in the overlay or the dev server log | ||
throw error; | ||
} | ||
window.location.reload(); | ||
return new Promise(() => {// check out of this hook cause the DJs never gonna re[s]olve this | ||
return new Promise(() => { | ||
// check out of this hook cause the DJs never gonna re[s]olve this | ||
}); | ||
@@ -116,0 +118,0 @@ } |
@@ -1,5 +0,5 @@ | ||
import type { ComponentType, ReactNode } from "react"; | ||
import type { Params } from "react-router"; | ||
import type { RouteModules, ShouldReloadFunction } from "./routeModules"; | ||
import type { Submission } from "./transition"; | ||
import type { HydrationState } from "@remix-run/router"; | ||
import type { DataRouteObject } from "react-router-dom"; | ||
import type { RouteModule, RouteModules } from "./routeModules"; | ||
import type { FutureConfig } from "./entry"; | ||
export interface RouteManifest<Route> { | ||
@@ -9,6 +9,7 @@ [routeId: string]: Route; | ||
interface Route { | ||
index?: boolean; | ||
caseSensitive?: boolean; | ||
id: string; | ||
parentId?: string; | ||
path?: string; | ||
index?: boolean; | ||
} | ||
@@ -18,45 +19,16 @@ export interface EntryRoute extends Route { | ||
hasLoader: boolean; | ||
hasCatchBoundary: boolean; | ||
hasClientAction: boolean; | ||
hasClientLoader: boolean; | ||
hasErrorBoundary: boolean; | ||
imports?: string[]; | ||
css?: string[]; | ||
module: string; | ||
parentId?: string; | ||
} | ||
export declare type RouteDataFunction = { | ||
(args: { | ||
/** | ||
* Parsed params from the route path | ||
*/ | ||
params: Params; | ||
/** | ||
* The url to be loaded, resolved to the matched route. | ||
*/ | ||
url: URL; | ||
/** | ||
* Will be present if being called from `<Form>` or `useSubmit` | ||
*/ | ||
submission?: Submission; | ||
/** | ||
* Attach this signal to fetch (or whatever else) to abort your | ||
* implementation when a load/action is aborted. | ||
*/ | ||
signal: AbortSignal; | ||
}): Promise<any> | any; | ||
}; | ||
export interface ClientRoute extends Route { | ||
loader?: RouteDataFunction; | ||
action: RouteDataFunction; | ||
shouldReload?: ShouldReloadFunction; | ||
ErrorBoundary?: any; | ||
CatchBoundary?: any; | ||
children?: ClientRoute[]; | ||
element: ReactNode; | ||
module: string; | ||
hasLoader: boolean; | ||
} | ||
declare type RemixRouteComponentType = ComponentType<{ | ||
id: string; | ||
}>; | ||
export declare function createClientRoute(entryRoute: EntryRoute, routeModulesCache: RouteModules, Component: RemixRouteComponentType): ClientRoute; | ||
export declare function createClientRoutes(routeManifest: RouteManifest<EntryRoute>, routeModulesCache: RouteModules, Component: RemixRouteComponentType, parentId?: string): ClientRoute[]; | ||
export declare function createServerRoutes(manifest: RouteManifest<EntryRoute>, routeModules: RouteModules, future: FutureConfig, isSpaMode: boolean, parentId?: string, routesByParentId?: Record<string, Omit<EntryRoute, "children">[]>, spaModeLazyPromise?: Promise<{ | ||
Component: () => null; | ||
}>): DataRouteObject[]; | ||
export declare function createClientRoutesWithHMRRevalidationOptOut(needsRevalidation: Set<string>, manifest: RouteManifest<EntryRoute>, routeModulesCache: RouteModules, initialState: HydrationState, future: FutureConfig, isSpaMode: boolean): DataRouteObject[]; | ||
export declare function createClientRoutes(manifest: RouteManifest<EntryRoute>, routeModulesCache: RouteModules, initialState: HydrationState | null, future: FutureConfig, isSpaMode: boolean, parentId?: string, routesByParentId?: Record<string, Omit<EntryRoute, "children">[]>, needsRevalidation?: Set<string>): DataRouteObject[]; | ||
export declare function shouldHydrateRouteLoader(route: EntryRoute, routeModule: RouteModule, isSpaMode: boolean): boolean; | ||
export {}; |
/** | ||
* @remix-run/react v0.0.0-nightly-81ec1b7-20220830 | ||
* @remix-run/react v0.0.0-nightly-825f70ebd-20240914 | ||
* | ||
@@ -16,6 +16,9 @@ * Copyright (c) Remix Software Inc. | ||
var React = require('react'); | ||
var router = require('@remix-run/router'); | ||
var reactRouterDom = require('react-router-dom'); | ||
var routeModules = require('./routeModules.js'); | ||
var data = require('./data.js'); | ||
var transition = require('./transition.js'); | ||
var links = require('./links.js'); | ||
var errorBoundaries = require('./errorBoundaries.js'); | ||
var fallback = require('./fallback.js'); | ||
var invariant = require('./invariant.js'); | ||
@@ -43,122 +46,399 @@ | ||
function createClientRoute(entryRoute, routeModulesCache, Component) { | ||
// NOTE: make sure to change the Route in server-runtime if you change this | ||
// NOTE: make sure to change the EntryRoute in server-runtime if you change this | ||
// Create a map of routes by parentId to use recursively instead of | ||
// repeatedly filtering the manifest. | ||
function groupRoutesByParentId(manifest) { | ||
let routes = {}; | ||
Object.values(manifest).forEach(route => { | ||
let parentId = route.parentId || ""; | ||
if (!routes[parentId]) { | ||
routes[parentId] = []; | ||
} | ||
routes[parentId].push(route); | ||
}); | ||
return routes; | ||
} | ||
function getRouteComponents(route, routeModule, isSpaMode) { | ||
let Component = getRouteModuleComponent(routeModule); | ||
// HydrateFallback can only exist on the root route in SPA Mode | ||
let HydrateFallback = routeModule.HydrateFallback && (!isSpaMode || route.id === "root") ? routeModule.HydrateFallback : route.id === "root" ? fallback.RemixRootDefaultHydrateFallback : undefined; | ||
let ErrorBoundary = routeModule.ErrorBoundary ? routeModule.ErrorBoundary : route.id === "root" ? () => /*#__PURE__*/React__namespace.createElement(errorBoundaries.RemixRootDefaultErrorBoundary, { | ||
error: reactRouterDom.useRouteError() | ||
}) : undefined; | ||
if (route.id === "root" && routeModule.Layout) { | ||
return { | ||
...(Component ? { | ||
element: /*#__PURE__*/React__namespace.createElement(routeModule.Layout, null, /*#__PURE__*/React__namespace.createElement(Component, null)) | ||
} : { | ||
Component | ||
}), | ||
...(ErrorBoundary ? { | ||
errorElement: /*#__PURE__*/React__namespace.createElement(routeModule.Layout, null, /*#__PURE__*/React__namespace.createElement(ErrorBoundary, null)) | ||
} : { | ||
ErrorBoundary | ||
}), | ||
...(HydrateFallback ? { | ||
hydrateFallbackElement: /*#__PURE__*/React__namespace.createElement(routeModule.Layout, null, /*#__PURE__*/React__namespace.createElement(HydrateFallback, null)) | ||
} : { | ||
HydrateFallback | ||
}) | ||
}; | ||
} | ||
return { | ||
caseSensitive: !!entryRoute.caseSensitive, | ||
element: /*#__PURE__*/React__namespace.createElement(Component, { | ||
id: entryRoute.id | ||
}), | ||
id: entryRoute.id, | ||
path: entryRoute.path, | ||
index: entryRoute.index, | ||
module: entryRoute.module, | ||
loader: createLoader(entryRoute, routeModulesCache), | ||
action: createAction(entryRoute, routeModulesCache), | ||
shouldReload: createShouldReload(entryRoute, routeModulesCache), | ||
ErrorBoundary: entryRoute.hasErrorBoundary, | ||
CatchBoundary: entryRoute.hasCatchBoundary, | ||
hasLoader: entryRoute.hasLoader | ||
Component, | ||
ErrorBoundary, | ||
HydrateFallback | ||
}; | ||
} | ||
function createClientRoutes(routeManifest, routeModulesCache, Component, parentId) { | ||
return Object.keys(routeManifest).filter(key => routeManifest[key].parentId === parentId).map(key => { | ||
let route = createClientRoute(routeManifest[key], routeModulesCache, Component); | ||
let children = createClientRoutes(routeManifest, routeModulesCache, Component, route.id); | ||
if (children.length > 0) route.children = children; | ||
return route; | ||
function createServerRoutes(manifest, routeModules, future, isSpaMode, parentId = "", routesByParentId = groupRoutesByParentId(manifest), spaModeLazyPromise = Promise.resolve({ | ||
Component: () => null | ||
})) { | ||
return (routesByParentId[parentId] || []).map(route => { | ||
let routeModule = routeModules[route.id]; | ||
invariant(routeModule, "No `routeModule` available to create server routes"); | ||
let dataRoute = { | ||
...getRouteComponents(route, routeModule, isSpaMode), | ||
caseSensitive: route.caseSensitive, | ||
id: route.id, | ||
index: route.index, | ||
path: route.path, | ||
handle: routeModule.handle, | ||
// For SPA Mode, all routes are lazy except root. However we tell the | ||
// router root is also lazy here too since we don't need a full | ||
// implementation - we just need a `lazy` prop to tell the RR rendering | ||
// where to stop which is always at the root route in SPA mode | ||
lazy: isSpaMode ? () => spaModeLazyPromise : undefined, | ||
// For partial hydration rendering, we need to indicate when the route | ||
// has a loader/clientLoader, but it won't ever be called during the static | ||
// render, so just give it a no-op function so we can render down to the | ||
// proper fallback | ||
loader: route.hasLoader || route.hasClientLoader ? () => null : undefined | ||
// We don't need action/shouldRevalidate on these routes since they're | ||
// for a static render | ||
}; | ||
let children = createServerRoutes(manifest, routeModules, future, isSpaMode, route.id, routesByParentId, spaModeLazyPromise); | ||
if (children.length > 0) dataRoute.children = children; | ||
return dataRoute; | ||
}); | ||
} | ||
function createShouldReload(route, routeModules) { | ||
let shouldReload = arg => { | ||
let module = routeModules[route.id]; | ||
invariant(module, `Expected route module to be loaded for ${route.id}`); | ||
if (module.unstable_shouldReload) { | ||
return module.unstable_shouldReload(arg); | ||
} | ||
return true; | ||
}; | ||
return shouldReload; | ||
function createClientRoutesWithHMRRevalidationOptOut(needsRevalidation, manifest, routeModulesCache, initialState, future, isSpaMode) { | ||
return createClientRoutes(manifest, routeModulesCache, initialState, future, isSpaMode, "", groupRoutesByParentId(manifest), needsRevalidation); | ||
} | ||
async function loadRouteModuleWithBlockingLinks(route, routeModules$1) { | ||
let routeModule = await routeModules.loadRouteModule(route, routeModules$1); | ||
await links.prefetchStyleLinks(routeModule); | ||
return routeModule; | ||
function preventInvalidServerHandlerCall(type, route, isSpaMode) { | ||
if (isSpaMode) { | ||
let fn = type === "action" ? "serverAction()" : "serverLoader()"; | ||
let msg = `You cannot call ${fn} in SPA Mode (routeId: "${route.id}")`; | ||
console.error(msg); | ||
throw new router.UNSAFE_ErrorResponseImpl(400, "Bad Request", new Error(msg), true); | ||
} | ||
let fn = type === "action" ? "serverAction()" : "serverLoader()"; | ||
let msg = `You are trying to call ${fn} on a route that does not have a server ` + `${type} (routeId: "${route.id}")`; | ||
if (type === "loader" && !route.hasLoader || type === "action" && !route.hasAction) { | ||
console.error(msg); | ||
throw new router.UNSAFE_ErrorResponseImpl(400, "Bad Request", new Error(msg), true); | ||
} | ||
} | ||
function noActionDefinedError(type, routeId) { | ||
let article = type === "clientAction" ? "a" : "an"; | ||
let msg = `Route "${routeId}" does not have ${article} ${type}, but you are trying to ` + `submit to it. To fix this, please add ${article} \`${type}\` function to the route`; | ||
console.error(msg); | ||
throw new router.UNSAFE_ErrorResponseImpl(405, "Method Not Allowed", new Error(msg), true); | ||
} | ||
function createClientRoutes(manifest, routeModulesCache, initialState, future, isSpaMode, parentId = "", routesByParentId = groupRoutesByParentId(manifest), needsRevalidation) { | ||
return (routesByParentId[parentId] || []).map(route => { | ||
let routeModule = routeModulesCache[route.id]; | ||
function createLoader(route, routeModules) { | ||
let loader = async ({ | ||
url, | ||
signal, | ||
submission | ||
}) => { | ||
if (route.hasLoader) { | ||
let [result] = await Promise.all([data.fetchData(url, route.id, signal, submission), loadRouteModuleWithBlockingLinks(route, routeModules)]); | ||
if (result instanceof Error) throw result; | ||
let redirect = await checkRedirect(result); | ||
if (redirect) return redirect; | ||
if (data.isCatchResponse(result)) { | ||
throw new transition.CatchValue(result.status, result.statusText, await data.extractData(result)); | ||
// Fetch data from the server either via single fetch or the standard `?_data` | ||
// request. Unwrap it when called via `serverLoader`/`serverAction` in a | ||
// client handler, otherwise return the raw response for the router to unwrap | ||
async function fetchServerHandlerAndMaybeUnwrap(request, unwrap, singleFetch) { | ||
if (typeof singleFetch === "function") { | ||
let result = await singleFetch(); | ||
return result; | ||
} | ||
return data.extractData(result); | ||
} else { | ||
await loadRouteModuleWithBlockingLinks(route, routeModules); | ||
let result = await fetchServerHandler(request, route); | ||
return unwrap ? unwrapServerResponse(result) : result; | ||
} | ||
}; | ||
function fetchServerLoader(request, unwrap, singleFetch) { | ||
if (!route.hasLoader) return Promise.resolve(null); | ||
return fetchServerHandlerAndMaybeUnwrap(request, unwrap, singleFetch); | ||
} | ||
function fetchServerAction(request, unwrap, singleFetch) { | ||
if (!route.hasAction) { | ||
throw noActionDefinedError("action", route.id); | ||
} | ||
return fetchServerHandlerAndMaybeUnwrap(request, unwrap, singleFetch); | ||
} | ||
async function prefetchStylesAndCallHandler(handler) { | ||
// Only prefetch links if we exist in the routeModulesCache (critical modules | ||
// and navigating back to pages previously loaded via route.lazy). Initial | ||
// execution of route.lazy (when the module is not in the cache) will handle | ||
// prefetching style links via loadRouteModuleWithBlockingLinks. | ||
let cachedModule = routeModulesCache[route.id]; | ||
let linkPrefetchPromise = cachedModule ? links.prefetchStyleLinks(route, cachedModule) : Promise.resolve(); | ||
try { | ||
return handler(); | ||
} finally { | ||
await linkPrefetchPromise; | ||
} | ||
} | ||
let dataRoute = { | ||
id: route.id, | ||
index: route.index, | ||
path: route.path | ||
}; | ||
if (routeModule) { | ||
var _initialState$loaderD, _initialState$errors, _routeModule$clientLo; | ||
// Use critical path modules directly | ||
Object.assign(dataRoute, { | ||
...dataRoute, | ||
...getRouteComponents(route, routeModule, isSpaMode), | ||
handle: routeModule.handle, | ||
shouldRevalidate: needsRevalidation ? wrapShouldRevalidateForHdr(route.id, routeModule.shouldRevalidate, needsRevalidation) : routeModule.shouldRevalidate | ||
}); | ||
let initialData = initialState === null || initialState === void 0 ? void 0 : (_initialState$loaderD = initialState.loaderData) === null || _initialState$loaderD === void 0 ? void 0 : _initialState$loaderD[route.id]; | ||
let initialError = initialState === null || initialState === void 0 ? void 0 : (_initialState$errors = initialState.errors) === null || _initialState$errors === void 0 ? void 0 : _initialState$errors[route.id]; | ||
let isHydrationRequest = needsRevalidation == null && (((_routeModule$clientLo = routeModule.clientLoader) === null || _routeModule$clientLo === void 0 ? void 0 : _routeModule$clientLo.hydrate) === true || !route.hasLoader); | ||
dataRoute.loader = async ({ | ||
request, | ||
params | ||
}, singleFetch) => { | ||
try { | ||
let result = await prefetchStylesAndCallHandler(async () => { | ||
invariant(routeModule, "No `routeModule` available for critical-route loader"); | ||
if (!routeModule.clientLoader) { | ||
if (isSpaMode) return null; | ||
// Call the server when no client loader exists | ||
return fetchServerLoader(request, false, singleFetch); | ||
} | ||
return routeModule.clientLoader({ | ||
request, | ||
params, | ||
async serverLoader() { | ||
preventInvalidServerHandlerCall("loader", route, isSpaMode); | ||
return loader; | ||
} | ||
// On the first call, resolve with the server result | ||
if (isHydrationRequest) { | ||
if (initialError !== undefined) { | ||
throw initialError; | ||
} | ||
return initialData; | ||
} | ||
function createAction(route, routeModules) { | ||
let action = async ({ | ||
url, | ||
signal, | ||
submission | ||
}) => { | ||
if (!route.hasAction) { | ||
console.error(`Route "${route.id}" does not have an action, but you are trying ` + `to submit to it. To fix this, please add an \`action\` function to the route`); | ||
} | ||
// Call the server loader for client-side navigations | ||
return fetchServerLoader(request, true, singleFetch); | ||
} | ||
}); | ||
}); | ||
return result; | ||
} finally { | ||
// Whether or not the user calls `serverLoader`, we only let this | ||
// stick around as true for one loader call | ||
isHydrationRequest = false; | ||
} | ||
}; | ||
let result = await data.fetchData(url, route.id, signal, submission); | ||
// Let React Router know whether to run this on hydration | ||
dataRoute.loader.hydrate = shouldHydrateRouteLoader(route, routeModule, isSpaMode); | ||
dataRoute.action = ({ | ||
request, | ||
params | ||
}, singleFetch) => { | ||
return prefetchStylesAndCallHandler(async () => { | ||
invariant(routeModule, "No `routeModule` available for critical-route action"); | ||
if (!routeModule.clientAction) { | ||
if (isSpaMode) { | ||
throw noActionDefinedError("clientAction", route.id); | ||
} | ||
return fetchServerAction(request, false, singleFetch); | ||
} | ||
return routeModule.clientAction({ | ||
request, | ||
params, | ||
async serverAction() { | ||
preventInvalidServerHandlerCall("action", route, isSpaMode); | ||
return fetchServerAction(request, true, singleFetch); | ||
} | ||
}); | ||
}); | ||
}; | ||
} else { | ||
// If the lazy route does not have a client loader/action we want to call | ||
// the server loader/action in parallel with the module load so we add | ||
// loader/action as static props on the route | ||
if (!route.hasClientLoader) { | ||
dataRoute.loader = ({ | ||
request | ||
}, singleFetch) => prefetchStylesAndCallHandler(() => { | ||
if (isSpaMode) return Promise.resolve(null); | ||
return fetchServerLoader(request, false, singleFetch); | ||
}); | ||
} | ||
if (!route.hasClientAction) { | ||
dataRoute.action = ({ | ||
request | ||
}, singleFetch) => prefetchStylesAndCallHandler(() => { | ||
if (isSpaMode) { | ||
throw noActionDefinedError("clientAction", route.id); | ||
} | ||
return fetchServerAction(request, false, singleFetch); | ||
}); | ||
} | ||
if (result instanceof Error) { | ||
throw result; | ||
// Load all other modules via route.lazy() | ||
dataRoute.lazy = async () => { | ||
let mod = await loadRouteModuleWithBlockingLinks(route, routeModulesCache); | ||
let lazyRoute = { | ||
...mod | ||
}; | ||
if (mod.clientLoader) { | ||
let clientLoader = mod.clientLoader; | ||
lazyRoute.loader = (args, singleFetch) => clientLoader({ | ||
...args, | ||
async serverLoader() { | ||
preventInvalidServerHandlerCall("loader", route, isSpaMode); | ||
return fetchServerLoader(args.request, true, singleFetch); | ||
} | ||
}); | ||
} | ||
if (mod.clientAction) { | ||
let clientAction = mod.clientAction; | ||
lazyRoute.action = (args, singleFetch) => clientAction({ | ||
...args, | ||
async serverAction() { | ||
preventInvalidServerHandlerCall("action", route, isSpaMode); | ||
return fetchServerAction(args.request, true, singleFetch); | ||
} | ||
}); | ||
} | ||
if (needsRevalidation) { | ||
lazyRoute.shouldRevalidate = wrapShouldRevalidateForHdr(route.id, mod.shouldRevalidate, needsRevalidation); | ||
} | ||
return { | ||
...(lazyRoute.loader ? { | ||
loader: lazyRoute.loader | ||
} : {}), | ||
...(lazyRoute.action ? { | ||
action: lazyRoute.action | ||
} : {}), | ||
hasErrorBoundary: lazyRoute.hasErrorBoundary, | ||
shouldRevalidate: lazyRoute.shouldRevalidate, | ||
handle: lazyRoute.handle, | ||
// No need to wrap these in layout since the root route is never | ||
// loaded via route.lazy() | ||
Component: lazyRoute.Component, | ||
ErrorBoundary: lazyRoute.ErrorBoundary | ||
}; | ||
}; | ||
} | ||
let children = createClientRoutes(manifest, routeModulesCache, initialState, future, isSpaMode, route.id, routesByParentId, needsRevalidation); | ||
if (children.length > 0) dataRoute.children = children; | ||
return dataRoute; | ||
}); | ||
} | ||
let redirect = await checkRedirect(result); | ||
if (redirect) return redirect; | ||
await loadRouteModuleWithBlockingLinks(route, routeModules); | ||
if (data.isCatchResponse(result)) { | ||
throw new transition.CatchValue(result.status, result.statusText, await data.extractData(result)); | ||
// When an HMR / HDR update happens we opt out of all user-defined | ||
// revalidation logic and force a revalidation on the first call | ||
function wrapShouldRevalidateForHdr(routeId, routeShouldRevalidate, needsRevalidation) { | ||
let handledRevalidation = false; | ||
return arg => { | ||
if (!handledRevalidation) { | ||
handledRevalidation = true; | ||
return needsRevalidation.has(routeId); | ||
} | ||
return routeShouldRevalidate ? routeShouldRevalidate(arg) : arg.defaultShouldRevalidate; | ||
}; | ||
} | ||
async function loadRouteModuleWithBlockingLinks(route, routeModules$1) { | ||
let routeModule = await routeModules.loadRouteModule(route, routeModules$1); | ||
await links.prefetchStyleLinks(route, routeModule); | ||
return data.extractData(result); | ||
// Include all `browserSafeRouteExports` fields, except `HydrateFallback` | ||
// since those aren't used on lazily loaded routes | ||
return { | ||
Component: getRouteModuleComponent(routeModule), | ||
ErrorBoundary: routeModule.ErrorBoundary, | ||
clientAction: routeModule.clientAction, | ||
clientLoader: routeModule.clientLoader, | ||
handle: routeModule.handle, | ||
links: routeModule.links, | ||
meta: routeModule.meta, | ||
shouldRevalidate: routeModule.shouldRevalidate | ||
}; | ||
return action; | ||
} | ||
async function checkRedirect(response) { | ||
if (data.isRedirectResponse(response)) { | ||
let url = new URL(response.headers.get("X-Remix-Redirect"), window.location.origin); | ||
if (url.origin !== window.location.origin) { | ||
await new Promise(() => { | ||
window.location.replace(url.href); | ||
}); | ||
async function fetchServerHandler(request, route) { | ||
let result = await data.fetchData(request, route.id); | ||
if (result instanceof Error) { | ||
throw result; | ||
} | ||
if (data.isRedirectResponse(result)) { | ||
throw getRedirect(result); | ||
} | ||
if (data.isCatchResponse(result)) { | ||
throw result; | ||
} | ||
if (data.isDeferredResponse(result) && result.body) { | ||
return await data.parseDeferredReadableStream(result.body); | ||
} | ||
return result; | ||
} | ||
function unwrapServerResponse(result) { | ||
if (data.isDeferredData(result)) { | ||
return result.data; | ||
} | ||
if (data.isResponse(result)) { | ||
let contentType = result.headers.get("Content-Type"); | ||
// Check between word boundaries instead of startsWith() due to the last | ||
// paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type | ||
if (contentType && /\bapplication\/json\b/.test(contentType)) { | ||
return result.json(); | ||
} else { | ||
return new transition.TransitionRedirect(url.pathname + url.search + url.hash, response.headers.get("X-Remix-Revalidate") !== null); | ||
return result.text(); | ||
} | ||
} | ||
return result; | ||
} | ||
function getRedirect(response) { | ||
let status = parseInt(response.headers.get("X-Remix-Status"), 10) || 302; | ||
let url = response.headers.get("X-Remix-Redirect"); | ||
let headers = {}; | ||
let revalidate = response.headers.get("X-Remix-Revalidate"); | ||
if (revalidate) { | ||
headers["X-Remix-Revalidate"] = revalidate; | ||
} | ||
let reloadDocument = response.headers.get("X-Remix-Reload-Document"); | ||
if (reloadDocument) { | ||
headers["X-Remix-Reload-Document"] = reloadDocument; | ||
} | ||
let replace = response.headers.get("X-Remix-Replace"); | ||
if (replace) { | ||
headers["X-Remix-Replace"] = replace; | ||
} | ||
return reactRouterDom.redirect(url, { | ||
status, | ||
headers | ||
}); | ||
} | ||
return null; | ||
// Our compiler generates the default export as `{}` when no default is provided, | ||
// which can lead us to trying to use that as a Component in RR and calling | ||
// createElement on it. Patching here as a quick fix and hoping it's no longer | ||
// an issue in Vite. | ||
function getRouteModuleComponent(routeModule) { | ||
if (routeModule.default == null) return undefined; | ||
let isEmptyObject = typeof routeModule.default === "object" && Object.keys(routeModule.default).length === 0; | ||
if (!isEmptyObject) { | ||
return routeModule.default; | ||
} | ||
} | ||
function shouldHydrateRouteLoader(route, routeModule, isSpaMode) { | ||
return isSpaMode && route.id !== "root" || routeModule.clientLoader != null && (routeModule.clientLoader.hydrate === true || route.hasLoader !== true); | ||
} | ||
exports.createClientRoute = createClientRoute; | ||
exports.createClientRoutes = createClientRoutes; | ||
exports.createClientRoutesWithHMRRevalidationOptOut = createClientRoutesWithHMRRevalidationOptOut; | ||
exports.createServerRoutes = createServerRoutes; | ||
exports.shouldHydrateRouteLoader = shouldHydrateRouteLoader; |
@@ -0,1 +1,4 @@ | ||
import * as React from "react"; | ||
import type { ScrollRestorationProps as ScrollRestorationPropsRR } from "react-router-dom"; | ||
import type { ScriptProps } from "./components"; | ||
/** | ||
@@ -5,6 +8,6 @@ * This component will emulate the browser's scroll restoration on location | ||
* | ||
* @see https://remix.run/api/remix#scrollrestoration | ||
* @see https://remix.run/components/scroll-restoration | ||
*/ | ||
export declare function ScrollRestoration({ nonce }: { | ||
nonce?: string; | ||
}): JSX.Element; | ||
export declare function ScrollRestoration({ getKey, ...props }: ScriptProps & { | ||
getKey?: ScrollRestorationPropsRR["getKey"]; | ||
}): React.JSX.Element | null; |
/** | ||
* @remix-run/react v0.0.0-nightly-81ec1b7-20220830 | ||
* @remix-run/react v0.0.0-nightly-825f70ebd-20240914 | ||
* | ||
@@ -15,2 +15,3 @@ * Copyright (c) Remix Software Inc. | ||
var _rollupPluginBabelHelpers = require('./_virtual/_rollupPluginBabelHelpers.js'); | ||
var React = require('react'); | ||
@@ -41,11 +42,3 @@ var reactRouterDom = require('react-router-dom'); | ||
let STORAGE_KEY = "positions"; | ||
let positions = {}; | ||
if (typeof document !== "undefined") { | ||
let sessionPositions = sessionStorage.getItem(STORAGE_KEY); | ||
if (sessionPositions) { | ||
positions = JSON.parse(sessionPositions); | ||
} | ||
} | ||
/** | ||
@@ -55,20 +48,39 @@ * This component will emulate the browser's scroll restoration on location | ||
* | ||
* @see https://remix.run/api/remix#scrollrestoration | ||
* @see https://remix.run/components/scroll-restoration | ||
*/ | ||
function ScrollRestoration({ | ||
nonce = undefined | ||
getKey, | ||
...props | ||
}) { | ||
useScrollRestoration(); // wait for the browser to restore it on its own | ||
let { | ||
isSpaMode | ||
} = components.useRemixContext(); | ||
let location = reactRouterDom.useLocation(); | ||
let matches = reactRouterDom.useMatches(); | ||
reactRouterDom.UNSAFE_useScrollRestoration({ | ||
getKey, | ||
storageKey: STORAGE_KEY | ||
}); | ||
React__namespace.useEffect(() => { | ||
window.history.scrollRestoration = "manual"; | ||
}, []); // let the browser restore on it's own for refresh | ||
// In order to support `getKey`, we need to compute a "key" here so we can | ||
// hydrate that up so that SSR scroll restoration isn't waiting on React to | ||
// hydrate. *However*, our key on the server is not the same as our key on | ||
// the client! So if the user's getKey implementation returns the SSR | ||
// location key, then let's ignore it and let our inline <script> below pick | ||
// up the client side history state key | ||
let key = React__namespace.useMemo(() => { | ||
if (!getKey) return null; | ||
let userKey = getKey(location, matches); | ||
return userKey !== location.key ? userKey : null; | ||
}, | ||
// Nah, we only need this the first time for the SSR render | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
[]); | ||
components.useBeforeUnload(React__namespace.useCallback(() => { | ||
window.history.scrollRestoration = "auto"; | ||
}, [])); | ||
let restoreScroll = (STORAGE_KEY => { | ||
// In SPA Mode, there's nothing to restore on initial render since we didn't | ||
// render anything on the server | ||
if (isSpaMode) { | ||
return null; | ||
} | ||
let restoreScroll = ((STORAGE_KEY, restoreKey) => { | ||
if (!window.history.state || !window.history.state.key) { | ||
@@ -80,7 +92,5 @@ let key = Math.random().toString(32).slice(2); | ||
} | ||
try { | ||
let positions = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || "{}"); | ||
let storedY = positions[window.history.state.key]; | ||
let storedY = positions[restoreKey || window.history.state.key]; | ||
if (typeof storedY === "number") { | ||
@@ -94,76 +104,10 @@ window.scrollTo(0, storedY); | ||
}).toString(); | ||
return /*#__PURE__*/React__namespace.createElement("script", { | ||
nonce: nonce, | ||
return /*#__PURE__*/React__namespace.createElement("script", _rollupPluginBabelHelpers["extends"]({}, props, { | ||
suppressHydrationWarning: true, | ||
dangerouslySetInnerHTML: { | ||
__html: `(${restoreScroll})(${JSON.stringify(STORAGE_KEY)})` | ||
__html: `(${restoreScroll})(${JSON.stringify(STORAGE_KEY)}, ${JSON.stringify(key)})` | ||
} | ||
}); | ||
})); | ||
} | ||
let hydrated = false; | ||
function useScrollRestoration() { | ||
let location = reactRouterDom.useLocation(); | ||
let transition = components.useTransition(); | ||
let wasSubmissionRef = React__namespace.useRef(false); | ||
React__namespace.useEffect(() => { | ||
if (transition.submission) { | ||
wasSubmissionRef.current = true; | ||
} | ||
}, [transition]); | ||
React__namespace.useEffect(() => { | ||
if (transition.location) { | ||
positions[location.key] = window.scrollY; | ||
} | ||
}, [transition, location]); | ||
components.useBeforeUnload(React__namespace.useCallback(() => { | ||
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(positions)); | ||
}, [])); | ||
if (typeof document !== "undefined") { | ||
// eslint-disable-next-line | ||
React__namespace.useLayoutEffect(() => { | ||
// don't do anything on hydration, the component already did this with an | ||
// inline script. | ||
if (!hydrated) { | ||
hydrated = true; | ||
return; | ||
} | ||
let y = positions[location.key]; // been here before, scroll to it | ||
if (y != undefined) { | ||
window.scrollTo(0, y); | ||
return; | ||
} // try to scroll to the hash | ||
if (location.hash) { | ||
let el = document.getElementById(location.hash.slice(1)); | ||
if (el) { | ||
el.scrollIntoView(); | ||
return; | ||
} | ||
} // don't do anything on submissions | ||
if (wasSubmissionRef.current === true) { | ||
wasSubmissionRef.current = false; | ||
return; | ||
} // otherwise go to the top on new locations | ||
window.scrollTo(0, 0); | ||
}, [location]); | ||
} | ||
React__namespace.useEffect(() => { | ||
if (transition.submission) { | ||
wasSubmissionRef.current = true; | ||
} | ||
}, [transition]); | ||
} | ||
exports.ScrollRestoration = ScrollRestoration; |
@@ -6,2 +6,4 @@ import type { ReactElement } from "react"; | ||
url: string | URL; | ||
abortDelay?: number; | ||
nonce?: string; | ||
} | ||
@@ -13,2 +15,2 @@ /** | ||
*/ | ||
export declare function RemixServer({ context, url }: RemixServerProps): ReactElement; | ||
export declare function RemixServer({ context, url, abortDelay, nonce, }: RemixServerProps): ReactElement; |
/** | ||
* @remix-run/react v0.0.0-nightly-81ec1b7-20220830 | ||
* @remix-run/react v0.0.0-nightly-825f70ebd-20240914 | ||
* | ||
@@ -15,5 +15,8 @@ * Copyright (c) Remix Software Inc. | ||
var history = require('history'); | ||
var React = require('react'); | ||
var server = require('react-router-dom/server'); | ||
var components = require('./components.js'); | ||
var errorBoundaries = require('./errorBoundaries.js'); | ||
var routes = require('./routes.js'); | ||
var singleFetch = require('./single-fetch.js'); | ||
@@ -40,4 +43,2 @@ function _interopNamespace(e) { | ||
// TODO: We eventually might not want to import anything directly from `history` | ||
/** | ||
@@ -50,3 +51,5 @@ * The entry point for a Remix app when it is rendered on the server (in | ||
context, | ||
url | ||
url, | ||
abortDelay, | ||
nonce | ||
}) { | ||
@@ -56,49 +59,63 @@ if (typeof url === "string") { | ||
} | ||
let { | ||
manifest, | ||
routeModules, | ||
criticalCss, | ||
serverHandoffString | ||
} = context; | ||
let routes$1 = routes.createServerRoutes(manifest.routes, routeModules, context.future, context.isSpaMode); | ||
let location = { | ||
pathname: url.pathname, | ||
search: url.search, | ||
hash: "", | ||
state: null, | ||
key: "default" | ||
// Create a shallow clone of `loaderData` we can mutate for partial hydration. | ||
// When a route exports a `clientLoader` and a `HydrateFallback`, we want to | ||
// render the fallback on the server so we clear our the `loaderData` during SSR. | ||
// Is it important not to change the `context` reference here since we use it | ||
// for context._deepestRenderedBoundaryId tracking | ||
context.staticHandlerContext.loaderData = { | ||
...context.staticHandlerContext.loaderData | ||
}; | ||
let staticNavigator = { | ||
createHref(to) { | ||
return typeof to === "string" ? to : history.createPath(to); | ||
}, | ||
push(to) { | ||
throw new Error(`You cannot use navigator.push() on the server because it is a stateless ` + `environment. This error was probably triggered when you did a ` + `\`navigate(${JSON.stringify(to)})\` somewhere in your app.`); | ||
}, | ||
replace(to) { | ||
throw new Error(`You cannot use navigator.replace() on the server because it is a stateless ` + `environment. This error was probably triggered when you did a ` + `\`navigate(${JSON.stringify(to)}, { replace: true })\` somewhere ` + `in your app.`); | ||
}, | ||
go(delta) { | ||
throw new Error(`You cannot use navigator.go() on the server because it is a stateless ` + `environment. This error was probably triggered when you did a ` + `\`navigate(${delta})\` somewhere in your app.`); | ||
}, | ||
back() { | ||
throw new Error(`You cannot use navigator.back() on the server because it is a stateless ` + `environment.`); | ||
}, | ||
forward() { | ||
throw new Error(`You cannot use navigator.forward() on the server because it is a stateless ` + `environment.`); | ||
}, | ||
block() { | ||
throw new Error(`You cannot use navigator.block() on the server because it is a stateless ` + `environment.`); | ||
for (let match of context.staticHandlerContext.matches) { | ||
let routeId = match.route.id; | ||
let route = routeModules[routeId]; | ||
let manifestRoute = context.manifest.routes[routeId]; | ||
// Clear out the loaderData to avoid rendering the route component when the | ||
// route opted into clientLoader hydration and either: | ||
// * gave us a HydrateFallback | ||
// * or doesn't have a server loader and we have no data to render | ||
if (route && routes.shouldHydrateRouteLoader(manifestRoute, route, context.isSpaMode) && (route.HydrateFallback || !manifestRoute.hasLoader)) { | ||
context.staticHandlerContext.loaderData[routeId] = undefined; | ||
} | ||
}; | ||
return /*#__PURE__*/React__namespace.createElement(components.RemixEntry, { | ||
} | ||
let router = server.createStaticRouter(routes$1, context.staticHandlerContext, { | ||
future: { | ||
v7_partialHydration: true, | ||
v7_relativeSplatPath: context.future.v3_relativeSplatPath | ||
} | ||
}); | ||
return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(components.RemixContext.Provider, { | ||
value: { | ||
manifest, | ||
routeModules, | ||
criticalCss, | ||
serverHandoffString, | ||
future: context.future, | ||
isSpaMode: context.isSpaMode, | ||
serializeError: context.serializeError, | ||
abortDelay, | ||
renderMeta: context.renderMeta | ||
} | ||
}, /*#__PURE__*/React__namespace.createElement(errorBoundaries.RemixErrorBoundary, { | ||
location: router.state.location | ||
}, /*#__PURE__*/React__namespace.createElement(server.StaticRouterProvider, { | ||
router: router, | ||
context: context.staticHandlerContext, | ||
hydrate: false | ||
}))), context.future.unstable_singleFetch && context.serverHandoffStream ? /*#__PURE__*/React__namespace.createElement(React__namespace.Suspense, null, /*#__PURE__*/React__namespace.createElement(singleFetch.StreamTransfer, { | ||
context: context, | ||
action: history.Action.Pop, | ||
location: location, | ||
navigator: staticNavigator, | ||
static: true | ||
}); | ||
identifier: 0, | ||
reader: context.serverHandoffStream.getReader(), | ||
textDecoder: new TextDecoder(), | ||
nonce: nonce | ||
})) : null); | ||
} | ||
exports.RemixServer = RemixServer; |
@@ -1,7 +0,22 @@ | ||
Copyright 2021 Remix Software Inc. | ||
MIT License | ||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: | ||
Copyright (c) Remix Software Inc. 2020-2021 | ||
Copyright (c) Shopify Inc. 2022-2024 | ||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. | ||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
{ | ||
"name": "@remix-run/react", | ||
"version": "0.0.0-nightly-81ec1b7-20220830", | ||
"version": "0.0.0-nightly-825f70ebd-20240914", | ||
"description": "React DOM bindings for Remix", | ||
@@ -19,27 +19,42 @@ "bugs": { | ||
"dependencies": { | ||
"history": "^5.3.0", | ||
"react-router-dom": "^6.2.2", | ||
"type-fest": "^2.17.0" | ||
"@remix-run/router": "1.19.2", | ||
"@remix-run/server-runtime": "0.0.0-nightly-825f70ebd-20240914", | ||
"react-router": "6.26.2", | ||
"react-router-dom": "6.26.2", | ||
"turbo-stream": "2.4.0" | ||
}, | ||
"devDependencies": { | ||
"@remix-run/server-runtime": "0.0.0-nightly-81ec1b7-20220830", | ||
"@testing-library/jest-dom": "^5.16.2", | ||
"@remix-run/node": "0.0.0-nightly-825f70ebd-20240914", | ||
"@remix-run/react": "0.0.0-nightly-825f70ebd-20240914", | ||
"@testing-library/jest-dom": "^5.17.0", | ||
"@testing-library/react": "^13.3.0", | ||
"abort-controller": "^3.0.0", | ||
"@types/react": "^18.2.20", | ||
"jest-environment-jsdom": "^29.6.4", | ||
"react": "^18.2.0", | ||
"react-dom": "^18.2.0" | ||
"react-dom": "^18.2.0", | ||
"typescript": "^5.1.6" | ||
}, | ||
"peerDependencies": { | ||
"react": ">=16.8", | ||
"react-dom": ">=16.8" | ||
"react": "^18.0.0", | ||
"react-dom": "^18.0.0", | ||
"typescript": "^5.1.0" | ||
}, | ||
"peerDependenciesMeta": { | ||
"typescript": { | ||
"optional": true | ||
} | ||
}, | ||
"engines": { | ||
"node": ">=14" | ||
"node": ">=18.0.0" | ||
}, | ||
"files": [ | ||
"dist/", | ||
"future/", | ||
"CHANGELOG.md", | ||
"LICENSE.md", | ||
"README.md" | ||
] | ||
} | ||
], | ||
"scripts": { | ||
"tsc": "tsc" | ||
} | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
55
7561
301774
8
9
13
7
+ Added@remix-run/router@1.19.2
+ Added@remix-run/server-runtime@0.0.0-nightly-825f70ebd-20240914
+ Addedreact-router@6.26.2
+ Addedturbo-stream@2.4.0
+ Added@remix-run/router@1.19.2(transitive)
+ Added@remix-run/server-runtime@0.0.0-nightly-825f70ebd-20240914(transitive)
+ Added@types/cookie@0.6.0(transitive)
+ Added@web3-storage/multipart-parser@1.0.0(transitive)
+ Addedcookie@0.6.0(transitive)
+ Addedreact-router@6.26.2(transitive)
+ Addedreact-router-dom@6.26.2(transitive)
+ Addedset-cookie-parser@2.7.1(transitive)
+ Addedsource-map@0.7.4(transitive)
+ Addedturbo-stream@2.4.0(transitive)
+ Addedtypescript@5.7.2(transitive)
- Removedhistory@^5.3.0
- Removedtype-fest@^2.17.0
- Removed@babel/runtime@7.26.0(transitive)
- Removed@remix-run/router@1.21.0(transitive)
- Removedhistory@5.3.0(transitive)
- Removedreact-router@6.28.0(transitive)
- Removedreact-router-dom@6.28.0(transitive)
- Removedregenerator-runtime@0.14.1(transitive)
- Removedtype-fest@2.19.0(transitive)
Updatedreact-router-dom@6.26.2