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( 41 resolveFrom(projectRoot, '@expo/metro-runtime/symbolicate') 42 ); 43 44 Log.log(); 45 Log.log(chalk.red('Metro error: ') + error.message); 46 Log.log(); 47 48 if (codeFrame) { 49 Log.log(codeFrame.content); 50 } 51 52 if (stack?.length) { 53 Log.log(); 54 Log.log(chalk.bold`Call Stack`); 55 56 const stackProps = stack.map((frame) => { 57 return { 58 title: frame.methodName, 59 subtitle: getStackFormattedLocation(projectRoot, frame), 60 collapse: frame.collapse, 61 }; 62 }); 63 64 stackProps.forEach((frame) => { 65 const position = terminalLink.isSupported 66 ? terminalLink(frame.subtitle, frame.subtitle) 67 : frame.subtitle; 68 let lineItem = chalk.gray(` ${frame.title} (${position})`); 69 if (frame.collapse) { 70 lineItem = chalk.dim(lineItem); 71 } 72 Log.log(lineItem); 73 }); 74 } else { 75 Log.log(chalk.gray(` ${error.stack}`)); 76 } 77} 78 79export async function logMetroError(projectRoot: string, { error }: { error: Error }) { 80 const { LogBoxLog, parseErrorStack } = require( 81 resolveFrom(projectRoot, '@expo/metro-runtime/symbolicate') 82 ); 83 84 const stack = parseErrorStack(error.stack); 85 86 const log = new LogBoxLog({ 87 level: 'static', 88 message: { 89 content: error.message, 90 substitutions: [], 91 }, 92 isComponentError: false, 93 stack, 94 category: 'static', 95 componentStack: [], 96 }); 97 98 await new Promise((res) => log.symbolicate('stack', res)); 99 100 logMetroErrorWithStack(projectRoot, { 101 stack: log.symbolicated?.stack?.stack ?? [], 102 codeFrame: log.codeFrame, 103 error, 104 }); 105} 106 107/** @returns the html required to render the static metro error as an SPA. */ 108export function logFromError({ error, projectRoot }: { error: Error; projectRoot: string }): { 109 symbolicated: any; 110 symbolicate: (type: string, callback: () => void) => void; 111 codeFrame: CodeFrame; 112} { 113 const { LogBoxLog, parseErrorStack } = require( 114 resolveFrom(projectRoot, '@expo/metro-runtime/symbolicate') 115 ); 116 117 const stack = parseErrorStack(error.stack); 118 119 return new LogBoxLog({ 120 level: 'static', 121 message: { 122 content: error.message, 123 substitutions: [], 124 }, 125 isComponentError: false, 126 stack, 127 category: 'static', 128 componentStack: [], 129 }); 130} 131 132/** @returns the html required to render the static metro error as an SPA. */ 133export async function logMetroErrorAsync({ 134 error, 135 projectRoot, 136}: { 137 error: Error; 138 projectRoot: string; 139}) { 140 const log = logFromError({ projectRoot, error }); 141 142 await new Promise<void>((res) => log.symbolicate('stack', res)); 143 144 logMetroErrorWithStack(projectRoot, { 145 stack: log.symbolicated?.stack?.stack ?? [], 146 codeFrame: log.codeFrame, 147 error, 148 }); 149} 150 151/** @returns the html required to render the static metro error as an SPA. */ 152export async function getErrorOverlayHtmlAsync({ 153 error, 154 projectRoot, 155}: { 156 error: Error; 157 projectRoot: string; 158}) { 159 const log = logFromError({ projectRoot, error }); 160 161 await new Promise<void>((res) => log.symbolicate('stack', res)); 162 163 logMetroErrorWithStack(projectRoot, { 164 stack: log.symbolicated?.stack?.stack ?? [], 165 codeFrame: log.codeFrame, 166 error, 167 }); 168 169 const logBoxContext = { 170 selectedLogIndex: 0, 171 isDisabled: false, 172 logs: [log], 173 }; 174 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( 175 logBoxContext 176 )}</script></body></html>`; 177 178 const errorOverlayEntry = await createMetroEndpointAsync( 179 projectRoot, 180 // Keep the URL relative 181 '', 182 resolveFrom(projectRoot, 'expo-router/_error'), 183 { 184 dev: true, 185 platform: 'web', 186 minify: false, 187 environment: 'node', 188 } 189 ); 190 191 const htmlWithJs = html.replace('</body>', `<script src=${errorOverlayEntry}></script></body>`); 192 return htmlWithJs; 193} 194