1/**
2 * Copyright © 2023 650 Industries.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7import '@expo/metro-runtime';
8
9import { ServerContainer, ServerContainerRef } from '@react-navigation/native';
10import * as Font from 'expo-font/build/server';
11import React from 'react';
12import ReactDOMServer from 'react-dom/server';
13import { AppRegistry } from 'react-native-web';
14
15import { getRootComponent } from './getRootComponent';
16import { ctx } from '../../_ctx';
17import { ExpoRoot } from '../ExpoRoot';
18import { getNavigationConfig } from '../getLinkingConfig';
19import { getRoutes } from '../getRoutes';
20import { Head } from '../head';
21import { loadStaticParamsAsync } from '../loadStaticParamsAsync';
22
23const debug = require('debug')('expo:router:renderStaticContent');
24
25AppRegistry.registerComponent('App', () => ExpoRoot);
26
27/** Get the linking manifest from a Node.js process. */
28async function getManifest(options: any) {
29  const routeTree = getRoutes(ctx, { preserveApiRoutes: true, ...options });
30
31  if (!routeTree) {
32    throw new Error('No routes found');
33  }
34
35  // Evaluate all static params
36  await loadStaticParamsAsync(routeTree);
37
38  return getNavigationConfig(routeTree);
39}
40
41function resetReactNavigationContexts() {
42  // https://github.com/expo/router/discussions/588
43  // https://github.com/react-navigation/react-navigation/blob/9fe34b445fcb86e5666f61e144007d7540f014fa/packages/elements/src/getNamedContext.tsx#LL3C1-L4C1
44
45  // React Navigation is storing providers in a global, this is fine for the first static render
46  // but subsequent static renders of Stack or Tabs will cause React to throw a warning. To prevent this warning, we'll reset the globals before rendering.
47  const contexts = '__react_navigation__elements_contexts';
48  global[contexts] = new Map<string, React.Context<any>>();
49}
50
51export function getStaticContent(location: URL): string {
52  const headContext: { helmet?: any } = {};
53
54  const ref = React.createRef<ServerContainerRef>();
55
56  const {
57    // NOTE: The `element` that's returned adds two extra Views and
58    // the seemingly unused `RootTagContext.Provider`.
59    element,
60    getStyleElement,
61  } = AppRegistry.getApplication('App', {
62    initialProps: {
63      location,
64      context: ctx,
65      wrapper: ({ children }) => (
66        <Root>
67          <div id="root">{children}</div>
68        </Root>
69      ),
70    },
71  });
72
73  const Root = getRootComponent();
74
75  // Clear any existing static resources from the global scope to attempt to prevent leaking between pages.
76  // This could break if pages are rendered in parallel or if fonts are loaded outside of the React tree
77  Font.resetServerContext();
78
79  // This MUST be run before `ReactDOMServer.renderToString` to prevent
80  // "Warning: Detected multiple renderers concurrently rendering the same context provider. This is currently unsupported."
81  resetReactNavigationContexts();
82
83  const html = ReactDOMServer.renderToString(
84    <Head.Provider context={headContext}>
85      <ServerContainer ref={ref}>{element}</ServerContainer>
86    </Head.Provider>
87  );
88
89  // Eval the CSS after the HTML is rendered so that the CSS is in the same order
90  const css = ReactDOMServer.renderToStaticMarkup(getStyleElement());
91
92  let output = mixHeadComponentsWithStaticResults(headContext.helmet, html);
93
94  output = output.replace('</head>', `${css}</head>`);
95
96  const fonts = Font.getServerResources();
97  debug(`Pushing static fonts: (count: ${fonts.length})`, fonts);
98  // debug('Push static fonts:', fonts)
99  // Inject static fonts loaded with expo-font
100  output = output.replace('</head>', `${fonts.join('')}</head>`);
101
102  return '<!DOCTYPE html>' + output;
103}
104
105function mixHeadComponentsWithStaticResults(helmet: any, html: string) {
106  // Head components
107  for (const key of ['title', 'priority', 'meta', 'link', 'script', 'style'].reverse()) {
108    const result = helmet?.[key]?.toString();
109    if (result) {
110      html = html.replace('<head>', `<head>${result}`);
111    }
112  }
113
114  // attributes
115  html = html.replace('<html ', `<html ${helmet?.htmlAttributes.toString()} `);
116  html = html.replace('<body ', `<body ${helmet?.bodyAttributes.toString()} `);
117
118  return html;
119}
120
121// Re-export for use in server
122export { getManifest };
123