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 chalk from 'chalk'; 8import resolveFrom from 'resolve-from'; 9import { StackFrame } from 'stacktrace-parser'; 10import terminalLink from 'terminal-link'; 11 12import { Log } from '../../../log'; 13import { createMetroEndpointAsync } from '../getStaticRenderFunctions'; 14// import type { CodeFrame, MetroStackFrame } from '@expo/metro-runtime/symbolicate'; 15 16type CodeFrame = { 17 content: string; 18 location?: { 19 row: number; 20 column: number; 21 [key: string]: any; 22 }; 23 fileName: string; 24}; 25 26type MetroStackFrame = StackFrame & { collapse?: boolean }; 27 28export async function logMetroErrorWithStack( 29 projectRoot: string, 30 { 31 stack, 32 codeFrame, 33 error, 34 }: { 35 stack: MetroStackFrame[]; 36 codeFrame: CodeFrame; 37 error: Error; 38 } 39) { 40 const { getStackFormattedLocation } = require(resolveFrom( 41 projectRoot, 42 '@expo/metro-runtime/symbolicate' 43 )); 44 45 Log.log(); 46 Log.log(chalk.red('Metro error: ') + error.message); 47 Log.log(); 48 49 if (codeFrame) { 50 Log.log(codeFrame.content); 51 } 52 53 if (stack?.length) { 54 Log.log(); 55 Log.log(chalk.bold`Call Stack`); 56 57 const stackProps = stack.map((frame) => { 58 return { 59 title: frame.methodName, 60 subtitle: getStackFormattedLocation(projectRoot, frame), 61 collapse: frame.collapse, 62 }; 63 }); 64 65 stackProps.forEach((frame) => { 66 const position = terminalLink.isSupported 67 ? terminalLink(frame.subtitle, frame.subtitle) 68 : frame.subtitle; 69 let lineItem = chalk.gray(` ${frame.title} (${position})`); 70 if (frame.collapse) { 71 lineItem = chalk.dim(lineItem); 72 } 73 Log.log(lineItem); 74 }); 75 } else { 76 Log.log(chalk.gray(` ${error.stack}`)); 77 } 78} 79 80/** @returns the html required to render the static metro error as an SPA. */ 81export async function getErrorOverlayHtmlAsync({ 82 error, 83 projectRoot, 84}: { 85 error: Error; 86 projectRoot: string; 87}) { 88 const { LogBoxLog, parseErrorStack } = require(resolveFrom( 89 projectRoot, 90 '@expo/metro-runtime/symbolicate' 91 )); 92 const stack = parseErrorStack(error.stack); 93 94 const log = new LogBoxLog({ 95 level: 'static', 96 message: { 97 content: error.message, 98 substitutions: [], 99 }, 100 isComponentError: false, 101 stack, 102 category: 'static', 103 componentStack: [], 104 }); 105 106 await new Promise((res) => log.symbolicate('stack', res)); 107 108 logMetroErrorWithStack(projectRoot, { 109 stack: log.symbolicated?.stack?.stack ?? [], 110 codeFrame: log.codeFrame, 111 error, 112 }); 113 114 const logBoxContext = { 115 selectedLogIndex: 0, 116 isDisabled: false, 117 logs: [log], 118 }; 119 const html = `<html><head><style>#root,body,html{height:100%}body{overflow:hidden}#root{display:flex}</style></head><body><div id="root"></div><script id="_expo-static-error" type="application/json">${JSON.stringify( 120 logBoxContext 121 )}</script></body></html>`; 122 123 const errorOverlayEntry = await createMetroEndpointAsync( 124 projectRoot, 125 // Keep the URL relative 126 '', 127 resolveFrom(projectRoot, 'expo-router/_error'), 128 { 129 dev: true, 130 platform: 'web', 131 minify: false, 132 environment: 'node', 133 } 134 ); 135 136 const htmlWithJs = html.replace('</body>', `<script src=${errorOverlayEntry}></script></body>`); 137 return htmlWithJs; 138} 139