server.mjs 9.6 KB

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