124228e75SEvan Bacon/** 224228e75SEvan Bacon * Copyright © 2022 650 Industries. 324228e75SEvan Bacon * 424228e75SEvan Bacon * This source code is licensed under the MIT license found in the 524228e75SEvan Bacon * LICENSE file in the root directory of this source tree. 624228e75SEvan Bacon */ 724228e75SEvan Baconimport chalk from 'chalk'; 824228e75SEvan Baconimport resolveFrom from 'resolve-from'; 924228e75SEvan Baconimport { StackFrame } from 'stacktrace-parser'; 1024228e75SEvan Baconimport terminalLink from 'terminal-link'; 1124228e75SEvan Bacon 1224228e75SEvan Baconimport { Log } from '../../../log'; 1324228e75SEvan Baconimport { createMetroEndpointAsync } from '../getStaticRenderFunctions'; 1424228e75SEvan Bacon// import type { CodeFrame, MetroStackFrame } from '@expo/metro-runtime/symbolicate'; 1524228e75SEvan Bacon 1624228e75SEvan Bacontype CodeFrame = { 1724228e75SEvan Bacon content: string; 1824228e75SEvan Bacon location?: { 1924228e75SEvan Bacon row: number; 2024228e75SEvan Bacon column: number; 2124228e75SEvan Bacon [key: string]: any; 2224228e75SEvan Bacon }; 2324228e75SEvan Bacon fileName: string; 2424228e75SEvan Bacon}; 2524228e75SEvan Bacon 2624228e75SEvan Bacontype MetroStackFrame = StackFrame & { collapse?: boolean }; 2724228e75SEvan Bacon 2846f023faSEvan Baconfunction fill(width: number): string { 2946f023faSEvan Bacon return Array(width).join(' '); 3046f023faSEvan Bacon} 3146f023faSEvan Bacon 3246f023faSEvan Baconfunction formatPaths(config: { filePath: string | null; line?: number; col?: number }) { 3346f023faSEvan Bacon const filePath = chalk.reset(config.filePath); 3446f023faSEvan Bacon return ( 3546f023faSEvan Bacon chalk.dim('(') + 3646f023faSEvan Bacon filePath + 3746f023faSEvan Bacon chalk.dim(`:${[config.line, config.col].filter(Boolean).join(':')})`) 3846f023faSEvan Bacon ); 3946f023faSEvan Bacon} 4046f023faSEvan Bacon 4124228e75SEvan Baconexport async function logMetroErrorWithStack( 4224228e75SEvan Bacon projectRoot: string, 4324228e75SEvan Bacon { 4424228e75SEvan Bacon stack, 4524228e75SEvan Bacon codeFrame, 4624228e75SEvan Bacon error, 4724228e75SEvan Bacon }: { 4824228e75SEvan Bacon stack: MetroStackFrame[]; 4924228e75SEvan Bacon codeFrame: CodeFrame; 5024228e75SEvan Bacon error: Error; 5124228e75SEvan Bacon } 5224228e75SEvan Bacon) { 5346f023faSEvan Bacon // process.stdout.write('\u001b[0m'); // Reset attributes 5446f023faSEvan Bacon // process.stdout.write('\u001bc'); // Reset the terminal 5546f023faSEvan Bacon 5605863844SEvan Bacon const { getStackFormattedLocation } = require( 5705863844SEvan Bacon resolveFrom(projectRoot, '@expo/metro-runtime/symbolicate') 5805863844SEvan Bacon ); 5924228e75SEvan Bacon 6024228e75SEvan Bacon Log.log(); 6124228e75SEvan Bacon Log.log(chalk.red('Metro error: ') + error.message); 6224228e75SEvan Bacon Log.log(); 6324228e75SEvan Bacon 6424228e75SEvan Bacon if (codeFrame) { 6546f023faSEvan Bacon const maxWarningLineLength = Math.max(200, process.stdout.columns); 6646f023faSEvan Bacon 6746f023faSEvan Bacon const lineText = codeFrame.content; 68*cd4367edSEvan Bacon const isPreviewTooLong = codeFrame.content 69*cd4367edSEvan Bacon .split('\n') 70*cd4367edSEvan Bacon .some((line) => line.length > maxWarningLineLength); 7146f023faSEvan Bacon const column = codeFrame.location?.column; 7246f023faSEvan Bacon // When the preview is too long, we skip reading the file and attempting to apply 7346f023faSEvan Bacon // code coloring, this is because it can get very slow. 7446f023faSEvan Bacon if (isPreviewTooLong) { 7546f023faSEvan Bacon let previewLine = ''; 7646f023faSEvan Bacon let cursorLine = ''; 7746f023faSEvan Bacon 7846f023faSEvan Bacon const formattedPath = formatPaths({ 7946f023faSEvan Bacon filePath: codeFrame.fileName, 8046f023faSEvan Bacon line: codeFrame.location?.row, 8146f023faSEvan Bacon col: codeFrame.location?.column, 8246f023faSEvan Bacon }); 8346f023faSEvan Bacon // Create a curtailed preview line like: 8446f023faSEvan Bacon // `...transition:'fade'},k._updatePropsStack=function(){clearImmediate(k._updateImmediate),k._updateImmediate...` 8546f023faSEvan Bacon // If there is no text preview or column number, we can't do anything. 8646f023faSEvan Bacon if (lineText && column != null) { 8746f023faSEvan Bacon const rangeWindow = Math.round( 8846f023faSEvan Bacon Math.max(codeFrame.fileName?.length ?? 0, Math.max(80, process.stdout.columns)) / 2 8946f023faSEvan Bacon ); 9046f023faSEvan Bacon let minBounds = Math.max(0, column - rangeWindow); 9146f023faSEvan Bacon const maxBounds = Math.min(minBounds + rangeWindow * 2, lineText.length); 9246f023faSEvan Bacon previewLine = lineText.slice(minBounds, maxBounds); 9346f023faSEvan Bacon 9446f023faSEvan Bacon // If we splice content off the start, then we should append `...`. 9546f023faSEvan Bacon // This is unlikely to happen since we limit the activation size. 9646f023faSEvan Bacon if (minBounds > 0) { 9746f023faSEvan Bacon // Adjust the min bounds so the cursor is aligned after we add the "..." 9846f023faSEvan Bacon minBounds -= 3; 9946f023faSEvan Bacon previewLine = chalk.dim('...') + previewLine; 10046f023faSEvan Bacon } 10146f023faSEvan Bacon if (maxBounds < lineText.length) { 10246f023faSEvan Bacon previewLine += chalk.dim('...'); 10346f023faSEvan Bacon } 10446f023faSEvan Bacon 10546f023faSEvan Bacon // If the column property could be found, then use that to fix the cursor location which is often broken in regex. 10646f023faSEvan Bacon cursorLine = (column == null ? '' : fill(column) + chalk.reset('^')).slice(minBounds); 10746f023faSEvan Bacon 10846f023faSEvan Bacon Log.log( 10946f023faSEvan Bacon [formattedPath, '', previewLine, cursorLine, chalk.dim('(error truncated)')].join('\n') 11046f023faSEvan Bacon ); 11146f023faSEvan Bacon } 11246f023faSEvan Bacon } else { 11324228e75SEvan Bacon Log.log(codeFrame.content); 11424228e75SEvan Bacon } 11546f023faSEvan Bacon } 11624228e75SEvan Bacon 11724228e75SEvan Bacon if (stack?.length) { 11824228e75SEvan Bacon Log.log(); 11924228e75SEvan Bacon Log.log(chalk.bold`Call Stack`); 12024228e75SEvan Bacon 12124228e75SEvan Bacon const stackProps = stack.map((frame) => { 12224228e75SEvan Bacon return { 12324228e75SEvan Bacon title: frame.methodName, 12424228e75SEvan Bacon subtitle: getStackFormattedLocation(projectRoot, frame), 12524228e75SEvan Bacon collapse: frame.collapse, 12624228e75SEvan Bacon }; 12724228e75SEvan Bacon }); 12824228e75SEvan Bacon 12924228e75SEvan Bacon stackProps.forEach((frame) => { 13024228e75SEvan Bacon const position = terminalLink.isSupported 13124228e75SEvan Bacon ? terminalLink(frame.subtitle, frame.subtitle) 13224228e75SEvan Bacon : frame.subtitle; 13324228e75SEvan Bacon let lineItem = chalk.gray(` ${frame.title} (${position})`); 13424228e75SEvan Bacon if (frame.collapse) { 13524228e75SEvan Bacon lineItem = chalk.dim(lineItem); 13624228e75SEvan Bacon } 13724228e75SEvan Bacon Log.log(lineItem); 13824228e75SEvan Bacon }); 13924228e75SEvan Bacon } else { 14024228e75SEvan Bacon Log.log(chalk.gray(` ${error.stack}`)); 14124228e75SEvan Bacon } 14224228e75SEvan Bacon} 14324228e75SEvan Bacon 1449580591fSEvan Baconexport async function logMetroError(projectRoot: string, { error }: { error: Error }) { 14505863844SEvan Bacon const { LogBoxLog, parseErrorStack } = require( 14605863844SEvan Bacon resolveFrom(projectRoot, '@expo/metro-runtime/symbolicate') 14705863844SEvan Bacon ); 1489580591fSEvan Bacon 1499580591fSEvan Bacon const stack = parseErrorStack(error.stack); 1509580591fSEvan Bacon 1519580591fSEvan Bacon const log = new LogBoxLog({ 1529580591fSEvan Bacon level: 'static', 1539580591fSEvan Bacon message: { 1549580591fSEvan Bacon content: error.message, 1559580591fSEvan Bacon substitutions: [], 1569580591fSEvan Bacon }, 1579580591fSEvan Bacon isComponentError: false, 1589580591fSEvan Bacon stack, 1599580591fSEvan Bacon category: 'static', 1609580591fSEvan Bacon componentStack: [], 1619580591fSEvan Bacon }); 1629580591fSEvan Bacon 1639580591fSEvan Bacon await new Promise((res) => log.symbolicate('stack', res)); 1649580591fSEvan Bacon 1659580591fSEvan Bacon logMetroErrorWithStack(projectRoot, { 1669580591fSEvan Bacon stack: log.symbolicated?.stack?.stack ?? [], 1679580591fSEvan Bacon codeFrame: log.codeFrame, 1689580591fSEvan Bacon error, 1699580591fSEvan Bacon }); 1709580591fSEvan Bacon} 1719580591fSEvan Bacon 17224228e75SEvan Bacon/** @returns the html required to render the static metro error as an SPA. */ 17385531d53SEvan Baconexport function logFromError({ error, projectRoot }: { error: Error; projectRoot: string }): { 17485531d53SEvan Bacon symbolicated: any; 17585531d53SEvan Bacon symbolicate: (type: string, callback: () => void) => void; 17685531d53SEvan Bacon codeFrame: CodeFrame; 17785531d53SEvan Bacon} { 17805863844SEvan Bacon const { LogBoxLog, parseErrorStack } = require( 17905863844SEvan Bacon resolveFrom(projectRoot, '@expo/metro-runtime/symbolicate') 18005863844SEvan Bacon ); 1819580591fSEvan Bacon 18224228e75SEvan Bacon const stack = parseErrorStack(error.stack); 18324228e75SEvan Bacon 18485531d53SEvan Bacon return new LogBoxLog({ 18524228e75SEvan Bacon level: 'static', 18624228e75SEvan Bacon message: { 18724228e75SEvan Bacon content: error.message, 18824228e75SEvan Bacon substitutions: [], 18924228e75SEvan Bacon }, 19024228e75SEvan Bacon isComponentError: false, 19124228e75SEvan Bacon stack, 19224228e75SEvan Bacon category: 'static', 19324228e75SEvan Bacon componentStack: [], 19424228e75SEvan Bacon }); 19585531d53SEvan Bacon} 19624228e75SEvan Bacon 19785531d53SEvan Bacon/** @returns the html required to render the static metro error as an SPA. */ 19885531d53SEvan Baconexport async function logMetroErrorAsync({ 19985531d53SEvan Bacon error, 20085531d53SEvan Bacon projectRoot, 20185531d53SEvan Bacon}: { 20285531d53SEvan Bacon error: Error; 20385531d53SEvan Bacon projectRoot: string; 20485531d53SEvan Bacon}) { 20585531d53SEvan Bacon const log = logFromError({ projectRoot, error }); 20685531d53SEvan Bacon 20785531d53SEvan Bacon await new Promise<void>((res) => log.symbolicate('stack', res)); 20885531d53SEvan Bacon 20985531d53SEvan Bacon logMetroErrorWithStack(projectRoot, { 21085531d53SEvan Bacon stack: log.symbolicated?.stack?.stack ?? [], 21185531d53SEvan Bacon codeFrame: log.codeFrame, 21285531d53SEvan Bacon error, 21385531d53SEvan Bacon }); 21485531d53SEvan Bacon} 21585531d53SEvan Bacon 21685531d53SEvan Bacon/** @returns the html required to render the static metro error as an SPA. */ 21785531d53SEvan Baconexport async function getErrorOverlayHtmlAsync({ 21885531d53SEvan Bacon error, 21985531d53SEvan Bacon projectRoot, 22085531d53SEvan Bacon}: { 22185531d53SEvan Bacon error: Error; 22285531d53SEvan Bacon projectRoot: string; 22385531d53SEvan Bacon}) { 22485531d53SEvan Bacon const log = logFromError({ projectRoot, error }); 22585531d53SEvan Bacon 22685531d53SEvan Bacon await new Promise<void>((res) => log.symbolicate('stack', res)); 22724228e75SEvan Bacon 22824228e75SEvan Bacon logMetroErrorWithStack(projectRoot, { 22924228e75SEvan Bacon stack: log.symbolicated?.stack?.stack ?? [], 23024228e75SEvan Bacon codeFrame: log.codeFrame, 23124228e75SEvan Bacon error, 23224228e75SEvan Bacon }); 23324228e75SEvan Bacon 23424228e75SEvan Bacon const logBoxContext = { 23524228e75SEvan Bacon selectedLogIndex: 0, 23624228e75SEvan Bacon isDisabled: false, 23724228e75SEvan Bacon logs: [log], 23824228e75SEvan Bacon }; 23924228e75SEvan Bacon 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( 24024228e75SEvan Bacon logBoxContext 24124228e75SEvan Bacon )}</script></body></html>`; 24224228e75SEvan Bacon 24324228e75SEvan Bacon const errorOverlayEntry = await createMetroEndpointAsync( 24424228e75SEvan Bacon projectRoot, 24524228e75SEvan Bacon // Keep the URL relative 24624228e75SEvan Bacon '', 24724228e75SEvan Bacon resolveFrom(projectRoot, 'expo-router/_error'), 24824228e75SEvan Bacon { 24924228e75SEvan Bacon dev: true, 25024228e75SEvan Bacon platform: 'web', 25124228e75SEvan Bacon minify: false, 25224228e75SEvan Bacon environment: 'node', 25324228e75SEvan Bacon } 25424228e75SEvan Bacon ); 25524228e75SEvan Bacon 25624228e75SEvan Bacon const htmlWithJs = html.replace('</body>', `<script src=${errorOverlayEntry}></script></body>`); 25724228e75SEvan Bacon return htmlWithJs; 25824228e75SEvan Bacon} 259