1/** 2 * Copyright © 2022 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 fs from 'fs'; 8import fetch from 'node-fetch'; 9import path from 'path'; 10import requireString from 'require-from-string'; 11import resolveFrom from 'resolve-from'; 12 13import { delayAsync } from '../../utils/delay'; 14import { memoize } from '../../utils/fn'; 15import { profile } from '../../utils/profile'; 16import { getMetroServerRoot } from './middleware/ManifestMiddleware'; 17 18const debug = require('debug')('expo:start:server:node-renderer') as typeof console.log; 19 20function wrapBundle(str: string) { 21 // Skip the metro runtime so debugging is a bit easier. 22 // Replace the __r() call with an export statement. 23 return str.replace(/^(__r\(.*\);)$/m, 'module.exports = $1'); 24} 25 26// TODO(EvanBacon): Group all the code together and version. 27const getRenderModuleId = (projectRoot: string): string => { 28 const moduleId = resolveFrom.silent(projectRoot, 'expo-router/node/render.js'); 29 if (!moduleId) { 30 throw new Error( 31 `A version of expo-router with Node.js support is not installed in the project.` 32 ); 33 } 34 35 return moduleId; 36}; 37 38type StaticRenderOptions = { 39 // Ensure the style format is `css-xxxx` (prod) instead of `css-view-xxxx` (dev) 40 dev?: boolean; 41 minify?: boolean; 42}; 43 44const moveStaticRenderFunction = memoize(async (projectRoot: string, requiredModuleId: string) => { 45 // Copy the file into the project to ensure it works in monorepos. 46 // This means the file cannot have any relative imports. 47 const tempDir = path.join(projectRoot, '.expo/static'); 48 await fs.promises.mkdir(tempDir, { recursive: true }); 49 const moduleId = path.join(tempDir, 'render.js'); 50 await fs.promises.writeFile(moduleId, await fs.promises.readFile(requiredModuleId, 'utf8')); 51 // Sleep to give watchman time to register the file. 52 await delayAsync(50); 53 return moduleId; 54}); 55 56/** @returns the js file contents required to generate the static generation function. */ 57export async function getStaticRenderFunctionsContentAsync( 58 projectRoot: string, 59 devServerUrl: string, 60 { dev = false, minify = false }: StaticRenderOptions = {} 61): Promise<string> { 62 const root = getMetroServerRoot(projectRoot); 63 const requiredModuleId = getRenderModuleId(root); 64 let moduleId = requiredModuleId; 65 66 // Cannot be accessed using Metro's server API, we need to move the file 67 // into the project root and try again. 68 if (path.relative(root, moduleId).startsWith('..')) { 69 moduleId = await moveStaticRenderFunction(projectRoot, requiredModuleId); 70 } 71 72 const serverPath = path.relative(root, moduleId).replace(/\.[jt]sx?$/, '.bundle'); 73 console.log(serverPath); 74 debug('Loading render functions from:', moduleId, moduleId, root); 75 76 const res = await fetch(`${devServerUrl}/${serverPath}?platform=web&dev=${dev}&minify=${minify}`); 77 78 // TODO: Improve error handling 79 if (res.status === 500) { 80 const text = await res.text(); 81 if (text.startsWith('{"originModulePath"')) { 82 const errorObject = JSON.parse(text); 83 throw new Error(errorObject.message); 84 } 85 throw new Error(`[${res.status}]: ${res.statusText}\n${text}`); 86 } 87 88 if (!res.ok) { 89 throw new Error(`Error fetching bundle for static rendering: ${res.status} ${res.statusText}`); 90 } 91 92 const content = await res.text(); 93 94 return wrapBundle(content); 95} 96 97export async function getStaticRenderFunctions( 98 projectRoot: string, 99 devServerUrl: string, 100 options: StaticRenderOptions = {} 101): Promise<any> { 102 const scriptContents = await getStaticRenderFunctionsContentAsync( 103 projectRoot, 104 devServerUrl, 105 options 106 ); 107 return profile(requireString, 'eval-metro-bundle')(scriptContents); 108} 109 110export async function getStaticPageContentsAsync( 111 projectRoot: string, 112 devServerUrl: string, 113 options: StaticRenderOptions = {} 114) { 115 const scriptContents = await getStaticRenderFunctionsContentAsync( 116 projectRoot, 117 devServerUrl, 118 options 119 ); 120 121 const { 122 getStaticContent, 123 // getDataLoader 124 } = profile(requireString, 'eval-metro-bundle')(scriptContents); 125 126 return function loadPageAsync(url: URL) { 127 // const fetchData = getDataLoader(url); 128 129 return { 130 fetchData: false, 131 scriptContents, 132 renderAsync: () => getStaticContent(url), 133 }; 134 }; 135} 136