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 function logFromError({ error, projectRoot }: { error: Error; projectRoot: string }): { 111 symbolicated: any; 112 symbolicate: (type: string, callback: () => void) => void; 113 codeFrame: CodeFrame; 114} { 115 const { LogBoxLog, parseErrorStack } = require(resolveFrom( 116 projectRoot, 117 '@expo/metro-runtime/symbolicate' 118 )); 119 120 const stack = parseErrorStack(error.stack); 121 122 return new LogBoxLog({ 123 level: 'static', 124 message: { 125 content: error.message, 126 substitutions: [], 127 }, 128 isComponentError: false, 129 stack, 130 category: 'static', 131 componentStack: [], 132 }); 133} 134 135/** @returns the html required to render the static metro error as an SPA. */ 136export async function logMetroErrorAsync({ 137 error, 138 projectRoot, 139}: { 140 error: Error; 141 projectRoot: string; 142}) { 143 const log = logFromError({ projectRoot, error }); 144 145 await new Promise<void>((res) => log.symbolicate('stack', res)); 146 147 logMetroErrorWithStack(projectRoot, { 148 stack: log.symbolicated?.stack?.stack ?? [], 149 codeFrame: log.codeFrame, 150 error, 151 }); 152} 153 154/** @returns the html required to render the static metro error as an SPA. */ 155export async function getErrorOverlayHtmlAsync({ 156 error, 157 projectRoot, 158}: { 159 error: Error; 160 projectRoot: string; 161}) { 162 const log = logFromError({ projectRoot, error }); 163 164 await new Promise<void>((res) => log.symbolicate('stack', res)); 165 166 logMetroErrorWithStack(projectRoot, { 167 stack: log.symbolicated?.stack?.stack ?? [], 168 codeFrame: log.codeFrame, 169 error, 170 }); 171 172 const logBoxContext = { 173 selectedLogIndex: 0, 174 isDisabled: false, 175 logs: [log], 176 }; 177 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( 178 logBoxContext 179 )}</script></body></html>`; 180 181 const errorOverlayEntry = await createMetroEndpointAsync( 182 projectRoot, 183 // Keep the URL relative 184 '', 185 resolveFrom(projectRoot, 'expo-router/_error'), 186 { 187 dev: true, 188 platform: 'web', 189 minify: false, 190 environment: 'node', 191 } 192 ); 193 194 const htmlWithJs = html.replace('</body>', `<script src=${errorOverlayEntry}></script></body>`); 195 return htmlWithJs; 196} 197