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 80export async function logMetroError(projectRoot: string, { error }: { error: Error }) { 81 const { LogBoxLog, parseErrorStack } = require(resolveFrom( 82 projectRoot, 83 '@expo/metro-runtime/symbolicate' 84 )); 85 86 const stack = parseErrorStack(error.stack); 87 88 const log = new LogBoxLog({ 89 level: 'static', 90 message: { 91 content: error.message, 92 substitutions: [], 93 }, 94 isComponentError: false, 95 stack, 96 category: 'static', 97 componentStack: [], 98 }); 99 100 await new Promise((res) => log.symbolicate('stack', res)); 101 102 logMetroErrorWithStack(projectRoot, { 103 stack: log.symbolicated?.stack?.stack ?? [], 104 codeFrame: log.codeFrame, 105 error, 106 }); 107} 108 109/** @returns the html required to render the static metro error as an SPA. */ 110export async function getErrorOverlayHtmlAsync({ 111 error, 112 projectRoot, 113}: { 114 error: Error; 115 projectRoot: string; 116}) { 117 const { LogBoxLog, parseErrorStack } = require(resolveFrom( 118 projectRoot, 119 '@expo/metro-runtime/symbolicate' 120 )); 121 122 const stack = parseErrorStack(error.stack); 123 124 const log = new LogBoxLog({ 125 level: 'static', 126 message: { 127 content: error.message, 128 substitutions: [], 129 }, 130 isComponentError: false, 131 stack, 132 category: 'static', 133 componentStack: [], 134 }); 135 136 await new Promise((res) => log.symbolicate('stack', res)); 137 138 logMetroErrorWithStack(projectRoot, { 139 stack: log.symbolicated?.stack?.stack ?? [], 140 codeFrame: log.codeFrame, 141 error, 142 }); 143 144 const logBoxContext = { 145 selectedLogIndex: 0, 146 isDisabled: false, 147 logs: [log], 148 }; 149 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( 150 logBoxContext 151 )}</script></body></html>`; 152 153 const errorOverlayEntry = await createMetroEndpointAsync( 154 projectRoot, 155 // Keep the URL relative 156 '', 157 resolveFrom(projectRoot, 'expo-router/_error'), 158 { 159 dev: true, 160 platform: 'web', 161 minify: false, 162 environment: 'node', 163 } 164 ); 165 166 const htmlWithJs = html.replace('</body>', `<script src=${errorOverlayEntry}></script></body>`); 167 return htmlWithJs; 168} 169