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