UNPKG

9.7 kBJavaScriptView Raw
1import * as React from 'react';
2import { Action, UNSAFE_invariant, isRouteErrorResponse, createStaticHandler as createStaticHandler$1, UNSAFE_convertRoutesToDataRoutes, IDLE_NAVIGATION, IDLE_FETCHER, IDLE_BLOCKER } from '@remix-run/router';
3import { UNSAFE_useRoutesImpl, UNSAFE_mapRouteProperties } from 'react-router';
4import { parsePath, Router, UNSAFE_DataRouterContext, UNSAFE_DataRouterStateContext, UNSAFE_FetchersContext, UNSAFE_ViewTransitionContext, createPath } from 'react-router-dom';
5
6/**
7 * A `<Router>` that may not navigate to any other location. This is useful
8 * on the server where there is no stateful UI.
9 */
10function StaticRouter({
11 basename,
12 children,
13 location: locationProp = "/",
14 future
15}) {
16 if (typeof locationProp === "string") {
17 locationProp = parsePath(locationProp);
18 }
19 let action = Action.Pop;
20 let location = {
21 pathname: locationProp.pathname || "/",
22 search: locationProp.search || "",
23 hash: locationProp.hash || "",
24 state: locationProp.state || null,
25 key: locationProp.key || "default"
26 };
27 let staticNavigator = getStatelessNavigator();
28 return /*#__PURE__*/React.createElement(Router, {
29 basename: basename,
30 children: children,
31 location: location,
32 navigationType: action,
33 navigator: staticNavigator,
34 future: future,
35 static: true
36 });
37}
38/**
39 * A Data Router that may not navigate to any other location. This is useful
40 * on the server where there is no stateful UI.
41 */
42function StaticRouterProvider({
43 context,
44 router,
45 hydrate = true,
46 nonce
47}) {
48 !(router && context) ? process.env.NODE_ENV !== "production" ? UNSAFE_invariant(false, "You must provide `router` and `context` to <StaticRouterProvider>") : UNSAFE_invariant(false) : void 0;
49 let dataRouterContext = {
50 router,
51 navigator: getStatelessNavigator(),
52 static: true,
53 staticContext: context,
54 basename: context.basename || "/"
55 };
56 let fetchersContext = new Map();
57 let hydrateScript = "";
58 if (hydrate !== false) {
59 let data = {
60 loaderData: context.loaderData,
61 actionData: context.actionData,
62 errors: serializeErrors(context.errors)
63 };
64 // Use JSON.parse here instead of embedding a raw JS object here to speed
65 // up parsing on the client. Dual-stringify is needed to ensure all quotes
66 // are properly escaped in the resulting string. See:
67 // https://v8.dev/blog/cost-of-javascript-2019#json
68 let json = htmlEscape(JSON.stringify(JSON.stringify(data)));
69 hydrateScript = `window.__staticRouterHydrationData = JSON.parse(${json});`;
70 }
71 let {
72 state
73 } = dataRouterContext.router;
74 return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(UNSAFE_DataRouterContext.Provider, {
75 value: dataRouterContext
76 }, /*#__PURE__*/React.createElement(UNSAFE_DataRouterStateContext.Provider, {
77 value: state
78 }, /*#__PURE__*/React.createElement(UNSAFE_FetchersContext.Provider, {
79 value: fetchersContext
80 }, /*#__PURE__*/React.createElement(UNSAFE_ViewTransitionContext.Provider, {
81 value: {
82 isTransitioning: false
83 }
84 }, /*#__PURE__*/React.createElement(Router, {
85 basename: dataRouterContext.basename,
86 location: state.location,
87 navigationType: state.historyAction,
88 navigator: dataRouterContext.navigator,
89 static: dataRouterContext.static,
90 future: {
91 v7_relativeSplatPath: router.future.v7_relativeSplatPath
92 }
93 }, /*#__PURE__*/React.createElement(DataRoutes, {
94 routes: router.routes,
95 future: router.future,
96 state: state
97 })))))), hydrateScript ? /*#__PURE__*/React.createElement("script", {
98 suppressHydrationWarning: true,
99 nonce: nonce,
100 dangerouslySetInnerHTML: {
101 __html: hydrateScript
102 }
103 }) : null);
104}
105function DataRoutes({
106 routes,
107 future,
108 state
109}) {
110 return UNSAFE_useRoutesImpl(routes, undefined, state, future);
111}
112function serializeErrors(errors) {
113 if (!errors) return null;
114 let entries = Object.entries(errors);
115 let serialized = {};
116 for (let [key, val] of entries) {
117 // Hey you! If you change this, please change the corresponding logic in
118 // deserializeErrors in react-router-dom/index.tsx :)
119 if (isRouteErrorResponse(val)) {
120 serialized[key] = {
121 ...val,
122 __type: "RouteErrorResponse"
123 };
124 } else if (val instanceof Error) {
125 // Do not serialize stack traces from SSR for security reasons
126 serialized[key] = {
127 message: val.message,
128 __type: "Error",
129 // If this is a subclass (i.e., ReferenceError), send up the type so we
130 // can re-create the same type during hydration.
131 ...(val.name !== "Error" ? {
132 __subType: val.name
133 } : {})
134 };
135 } else {
136 serialized[key] = val;
137 }
138 }
139 return serialized;
140}
141function getStatelessNavigator() {
142 return {
143 createHref,
144 encodeLocation,
145 push(to) {
146 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.`);
147 },
148 replace(to) {
149 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.`);
150 },
151 go(delta) {
152 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.`);
153 },
154 back() {
155 throw new Error(`You cannot use navigator.back() on the server because it is a stateless ` + `environment.`);
156 },
157 forward() {
158 throw new Error(`You cannot use navigator.forward() on the server because it is a stateless ` + `environment.`);
159 }
160 };
161}
162function createStaticHandler(routes, opts) {
163 return createStaticHandler$1(routes, {
164 ...opts,
165 mapRouteProperties: UNSAFE_mapRouteProperties
166 });
167}
168function createStaticRouter(routes, context, opts = {}) {
169 let manifest = {};
170 let dataRoutes = UNSAFE_convertRoutesToDataRoutes(routes, UNSAFE_mapRouteProperties, undefined, manifest);
171
172 // Because our context matches may be from a framework-agnostic set of
173 // routes passed to createStaticHandler(), we update them here with our
174 // newly created/enhanced data routes
175 let matches = context.matches.map(match => {
176 let route = manifest[match.route.id] || match.route;
177 return {
178 ...match,
179 route
180 };
181 });
182 let msg = method => `You cannot use router.${method}() on the server because it is a stateless environment`;
183 return {
184 get basename() {
185 return context.basename;
186 },
187 get future() {
188 return {
189 v7_fetcherPersist: false,
190 v7_normalizeFormMethod: false,
191 v7_partialHydration: opts.future?.v7_partialHydration === true,
192 v7_prependBasename: false,
193 v7_relativeSplatPath: opts.future?.v7_relativeSplatPath === true
194 };
195 },
196 get state() {
197 return {
198 historyAction: Action.Pop,
199 location: context.location,
200 matches,
201 loaderData: context.loaderData,
202 actionData: context.actionData,
203 errors: context.errors,
204 initialized: true,
205 navigation: IDLE_NAVIGATION,
206 restoreScrollPosition: null,
207 preventScrollReset: false,
208 revalidation: "idle",
209 fetchers: new Map(),
210 blockers: new Map()
211 };
212 },
213 get routes() {
214 return dataRoutes;
215 },
216 get window() {
217 return undefined;
218 },
219 initialize() {
220 throw msg("initialize");
221 },
222 subscribe() {
223 throw msg("subscribe");
224 },
225 enableScrollRestoration() {
226 throw msg("enableScrollRestoration");
227 },
228 navigate() {
229 throw msg("navigate");
230 },
231 fetch() {
232 throw msg("fetch");
233 },
234 revalidate() {
235 throw msg("revalidate");
236 },
237 createHref,
238 encodeLocation,
239 getFetcher() {
240 return IDLE_FETCHER;
241 },
242 deleteFetcher() {
243 throw msg("deleteFetcher");
244 },
245 dispose() {
246 throw msg("dispose");
247 },
248 getBlocker() {
249 return IDLE_BLOCKER;
250 },
251 deleteBlocker() {
252 throw msg("deleteBlocker");
253 },
254 _internalFetchControllers: new Map(),
255 _internalActiveDeferreds: new Map(),
256 _internalSetRoutes() {
257 throw msg("_internalSetRoutes");
258 }
259 };
260}
261function createHref(to) {
262 return typeof to === "string" ? to : createPath(to);
263}
264function encodeLocation(to) {
265 let href = typeof to === "string" ? to : createPath(to);
266 // Treating this as a full URL will strip any trailing spaces so we need to
267 // pre-encode them since they might be part of a matching splat param from
268 // an ancestor route
269 href = href.replace(/ $/, "%20");
270 let encoded = ABSOLUTE_URL_REGEX.test(href) ? new URL(href) : new URL(href, "http://localhost");
271 return {
272 pathname: encoded.pathname,
273 search: encoded.search,
274 hash: encoded.hash
275 };
276}
277const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
278
279// This utility is based on https://github.com/zertosh/htmlescape
280// License: https://github.com/zertosh/htmlescape/blob/0527ca7156a524d256101bb310a9f970f63078ad/LICENSE
281const ESCAPE_LOOKUP = {
282 "&": "\\u0026",
283 ">": "\\u003e",
284 "<": "\\u003c",
285 "\u2028": "\\u2028",
286 "\u2029": "\\u2029"
287};
288const ESCAPE_REGEX = /[&><\u2028\u2029]/g;
289function htmlEscape(str) {
290 return str.replace(ESCAPE_REGEX, match => ESCAPE_LOOKUP[match]);
291}
292
293export { StaticRouter, StaticRouterProvider, createStaticHandler, createStaticRouter };