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