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