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 28function fill(width: number): string { 29 return Array(width).join(' '); 30} 31 32function formatPaths(config: { filePath: string | null; line?: number; col?: number }) { 33 const filePath = chalk.reset(config.filePath); 34 return ( 35 chalk.dim('(') + 36 filePath + 37 chalk.dim(`:${[config.line, config.col].filter(Boolean).join(':')})`) 38 ); 39} 40 41export async function logMetroErrorWithStack( 42 projectRoot: string, 43 { 44 stack, 45 codeFrame, 46 error, 47 }: { 48 stack: MetroStackFrame[]; 49 codeFrame: CodeFrame; 50 error: Error; 51 } 52) { 53 // process.stdout.write('\u001b[0m'); // Reset attributes 54 // process.stdout.write('\u001bc'); // Reset the terminal 55 56 const { getStackFormattedLocation } = require( 57 resolveFrom(projectRoot, '@expo/metro-runtime/symbolicate') 58 ); 59 60 Log.log(); 61 Log.log(chalk.red('Metro error: ') + error.message); 62 Log.log(); 63 64 if (codeFrame) { 65 const maxWarningLineLength = Math.max(200, process.stdout.columns); 66 67 const lineText = codeFrame.content; 68 const isPreviewTooLong = codeFrame.content 69 .split('\n') 70 .some((line) => line.length > maxWarningLineLength); 71 const column = codeFrame.location?.column; 72 // When the preview is too long, we skip reading the file and attempting to apply 73 // code coloring, this is because it can get very slow. 74 if (isPreviewTooLong) { 75 let previewLine = ''; 76 let cursorLine = ''; 77 78 const formattedPath = formatPaths({ 79 filePath: codeFrame.fileName, 80 line: codeFrame.location?.row, 81 col: codeFrame.location?.column, 82 }); 83 // Create a curtailed preview line like: 84 // `...transition:'fade'},k._updatePropsStack=function(){clearImmediate(k._updateImmediate),k._updateImmediate...` 85 // If there is no text preview or column number, we can't do anything. 86 if (lineText && column != null) { 87 const rangeWindow = Math.round( 88 Math.max(codeFrame.fileName?.length ?? 0, Math.max(80, process.stdout.columns)) / 2 89 ); 90 let minBounds = Math.max(0, column - rangeWindow); 91 const maxBounds = Math.min(minBounds + rangeWindow * 2, lineText.length); 92 previewLine = lineText.slice(minBounds, maxBounds); 93 94 // If we splice content off the start, then we should append `...`. 95 // This is unlikely to happen since we limit the activation size. 96 if (minBounds > 0) { 97 // Adjust the min bounds so the cursor is aligned after we add the "..." 98 minBounds -= 3; 99 previewLine = chalk.dim('...') + previewLine; 100 } 101 if (maxBounds < lineText.length) { 102 previewLine += chalk.dim('...'); 103 } 104 105 // If the column property could be found, then use that to fix the cursor location which is often broken in regex. 106 cursorLine = (column == null ? '' : fill(column) + chalk.reset('^')).slice(minBounds); 107 108 Log.log( 109 [formattedPath, '', previewLine, cursorLine, chalk.dim('(error truncated)')].join('\n') 110 ); 111 } 112 } else { 113 Log.log(codeFrame.content); 114 } 115 } 116 117 if (stack?.length) { 118 Log.log(); 119 Log.log(chalk.bold`Call Stack`); 120 121 const stackProps = stack.map((frame) => { 122 return { 123 title: frame.methodName, 124 subtitle: getStackFormattedLocation(projectRoot, frame), 125 collapse: frame.collapse, 126 }; 127 }); 128 129 stackProps.forEach((frame) => { 130 const position = terminalLink.isSupported 131 ? terminalLink(frame.subtitle, frame.subtitle) 132 : frame.subtitle; 133 let lineItem = chalk.gray(` ${frame.title} (${position})`); 134 if (frame.collapse) { 135 lineItem = chalk.dim(lineItem); 136 } 137 Log.log(lineItem); 138 }); 139 } else { 140 Log.log(chalk.gray(` ${error.stack}`)); 141 } 142} 143 144export async function logMetroError(projectRoot: string, { error }: { error: Error }) { 145 const { LogBoxLog, parseErrorStack } = require( 146 resolveFrom(projectRoot, '@expo/metro-runtime/symbolicate') 147 ); 148 149 const stack = parseErrorStack(error.stack); 150 151 const log = new LogBoxLog({ 152 level: 'static', 153 message: { 154 content: error.message, 155 substitutions: [], 156 }, 157 isComponentError: false, 158 stack, 159 category: 'static', 160 componentStack: [], 161 }); 162 163 await new Promise((res) => log.symbolicate('stack', res)); 164 165 logMetroErrorWithStack(projectRoot, { 166 stack: log.symbolicated?.stack?.stack ?? [], 167 codeFrame: log.codeFrame, 168 error, 169 }); 170} 171 172/** @returns the html required to render the static metro error as an SPA. */ 173export function logFromError({ error, projectRoot }: { error: Error; projectRoot: string }): { 174 symbolicated: any; 175 symbolicate: (type: string, callback: () => void) => void; 176 codeFrame: CodeFrame; 177} { 178 const { LogBoxLog, parseErrorStack } = require( 179 resolveFrom(projectRoot, '@expo/metro-runtime/symbolicate') 180 ); 181 182 const stack = parseErrorStack(error.stack); 183 184 return new LogBoxLog({ 185 level: 'static', 186 message: { 187 content: error.message, 188 substitutions: [], 189 }, 190 isComponentError: false, 191 stack, 192 category: 'static', 193 componentStack: [], 194 }); 195} 196 197/** @returns the html required to render the static metro error as an SPA. */ 198export async function logMetroErrorAsync({ 199 error, 200 projectRoot, 201}: { 202 error: Error; 203 projectRoot: string; 204}) { 205 const log = logFromError({ projectRoot, error }); 206 207 await new Promise<void>((res) => log.symbolicate('stack', res)); 208 209 logMetroErrorWithStack(projectRoot, { 210 stack: log.symbolicated?.stack?.stack ?? [], 211 codeFrame: log.codeFrame, 212 error, 213 }); 214} 215 216/** @returns the html required to render the static metro error as an SPA. */ 217export async function getErrorOverlayHtmlAsync({ 218 error, 219 projectRoot, 220}: { 221 error: Error; 222 projectRoot: string; 223}) { 224 const log = logFromError({ projectRoot, error }); 225 226 await new Promise<void>((res) => log.symbolicate('stack', res)); 227 228 logMetroErrorWithStack(projectRoot, { 229 stack: log.symbolicated?.stack?.stack ?? [], 230 codeFrame: log.codeFrame, 231 error, 232 }); 233 234 const logBoxContext = { 235 selectedLogIndex: 0, 236 isDisabled: false, 237 logs: [log], 238 }; 239 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( 240 logBoxContext 241 )}</script></body></html>`; 242 243 const errorOverlayEntry = await createMetroEndpointAsync( 244 projectRoot, 245 // Keep the URL relative 246 '', 247 resolveFrom(projectRoot, 'expo-router/_error'), 248 { 249 dev: true, 250 platform: 'web', 251 minify: false, 252 environment: 'node', 253 } 254 ); 255 256 const htmlWithJs = html.replace('</body>', `<script src=${errorOverlayEntry}></script></body>`); 257 return htmlWithJs; 258} 259