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