126ad19fcSEvan Bacon/** 226ad19fcSEvan Bacon * Copyright (c) 650 Industries. 326ad19fcSEvan Bacon * Copyright (c) Meta Platforms, Inc. and affiliates. 426ad19fcSEvan Bacon * 526ad19fcSEvan Bacon * This source code is licensed under the MIT license found in the 626ad19fcSEvan Bacon * LICENSE file in the root directory of this source tree. 726ad19fcSEvan Bacon */ 826ad19fcSEvan Bacon 98a424bebSJames Ideimport type { LogBoxLogData } from './LogBoxLog'; 1026ad19fcSEvan Baconimport parseErrorStack from '../modules/parseErrorStack'; 1126ad19fcSEvan Baconimport stringifySafe from '../modules/stringifySafe'; 1226ad19fcSEvan Bacontype ExceptionData = any; 1326ad19fcSEvan Bacon 1426ad19fcSEvan Baconconst BABEL_TRANSFORM_ERROR_FORMAT = 1526ad19fcSEvan Bacon /^(?:TransformError )?(?:SyntaxError: |ReferenceError: )(.*): (.*) \((\d+):(\d+)\)\n\n([\s\S]+)/; 1626ad19fcSEvan Baconconst BABEL_CODE_FRAME_ERROR_FORMAT = 1726ad19fcSEvan Bacon /^(?:TransformError )?(?:.*):? (?:.*?)(\/.*): ([\s\S]+?)\n([ >]{2}[\d\s]+ \|[\s\S]+|\u{001b}[\s\S]+)/u; 1826ad19fcSEvan Baconconst METRO_ERROR_FORMAT = 1926ad19fcSEvan Bacon /^(?:InternalError Metro has encountered an error:) (.*): (.*) \((\d+):(\d+)\)\n\n([\s\S]+)/u; 2026ad19fcSEvan Bacon 2126ad19fcSEvan Baconexport type ExtendedExceptionData = ExceptionData & { 2226ad19fcSEvan Bacon isComponentError: boolean; 2326ad19fcSEvan Bacon [key: string]: any; 2426ad19fcSEvan Bacon}; 2526ad19fcSEvan Baconexport type Category = string; 2626ad19fcSEvan Baconexport type CodeFrame = { 2726ad19fcSEvan Bacon content: string; 2826ad19fcSEvan Bacon location?: { 2926ad19fcSEvan Bacon row: number; 3026ad19fcSEvan Bacon column: number; 3126ad19fcSEvan Bacon [key: string]: any; 3226ad19fcSEvan Bacon } | null; 3326ad19fcSEvan Bacon fileName: string; 3426ad19fcSEvan Bacon 3526ad19fcSEvan Bacon // TODO: When React switched to using call stack frames, 3626ad19fcSEvan Bacon // we gained the ability to use the collapse flag, but 3726ad19fcSEvan Bacon // it is not integrated into the LogBox UI. 3826ad19fcSEvan Bacon collapse?: boolean; 3926ad19fcSEvan Bacon}; 4026ad19fcSEvan Bacon 4126ad19fcSEvan Baconexport type Message = { 4226ad19fcSEvan Bacon content: string; 4326ad19fcSEvan Bacon substitutions: { 4426ad19fcSEvan Bacon length: number; 4526ad19fcSEvan Bacon offset: number; 4626ad19fcSEvan Bacon }[]; 4726ad19fcSEvan Bacon}; 4826ad19fcSEvan Bacon 4926ad19fcSEvan Baconexport type ComponentStack = CodeFrame[]; 5026ad19fcSEvan Bacon 51*4a8c0978SEvan Baconconst SUBSTITUTION = '\ufeff%s'; 5226ad19fcSEvan Bacon 5326ad19fcSEvan Baconexport function parseInterpolation(args: readonly any[]): { 5426ad19fcSEvan Bacon category: Category; 5526ad19fcSEvan Bacon message: Message; 5626ad19fcSEvan Bacon} { 5726ad19fcSEvan Bacon const categoryParts: string[] = []; 5826ad19fcSEvan Bacon const contentParts: string[] = []; 5926ad19fcSEvan Bacon const substitutionOffsets: { length: number; offset: number }[] = []; 6026ad19fcSEvan Bacon 6126ad19fcSEvan Bacon const remaining = [...args]; 6226ad19fcSEvan Bacon if (typeof remaining[0] === 'string') { 6326ad19fcSEvan Bacon const formatString = String(remaining.shift()); 6426ad19fcSEvan Bacon const formatStringParts = formatString.split('%s'); 6526ad19fcSEvan Bacon const substitutionCount = formatStringParts.length - 1; 6626ad19fcSEvan Bacon const substitutions = remaining.splice(0, substitutionCount); 6726ad19fcSEvan Bacon 6826ad19fcSEvan Bacon let categoryString = ''; 6926ad19fcSEvan Bacon let contentString = ''; 7026ad19fcSEvan Bacon 7126ad19fcSEvan Bacon let substitutionIndex = 0; 7226ad19fcSEvan Bacon for (const formatStringPart of formatStringParts) { 7326ad19fcSEvan Bacon categoryString += formatStringPart; 7426ad19fcSEvan Bacon contentString += formatStringPart; 7526ad19fcSEvan Bacon 7626ad19fcSEvan Bacon if (substitutionIndex < substitutionCount) { 7726ad19fcSEvan Bacon if (substitutionIndex < substitutions.length) { 7826ad19fcSEvan Bacon // Don't stringify a string type. 7926ad19fcSEvan Bacon // It adds quotation mark wrappers around the string, 8026ad19fcSEvan Bacon // which causes the LogBox to look odd. 8126ad19fcSEvan Bacon const substitution = 8226ad19fcSEvan Bacon typeof substitutions[substitutionIndex] === 'string' 8326ad19fcSEvan Bacon ? substitutions[substitutionIndex] 8426ad19fcSEvan Bacon : stringifySafe(substitutions[substitutionIndex]); 8526ad19fcSEvan Bacon substitutionOffsets.push({ 8626ad19fcSEvan Bacon length: substitution.length, 8726ad19fcSEvan Bacon offset: contentString.length, 8826ad19fcSEvan Bacon }); 8926ad19fcSEvan Bacon 9026ad19fcSEvan Bacon categoryString += SUBSTITUTION; 9126ad19fcSEvan Bacon contentString += substitution; 9226ad19fcSEvan Bacon } else { 9326ad19fcSEvan Bacon substitutionOffsets.push({ 9426ad19fcSEvan Bacon length: 2, 9526ad19fcSEvan Bacon offset: contentString.length, 9626ad19fcSEvan Bacon }); 9726ad19fcSEvan Bacon 9826ad19fcSEvan Bacon categoryString += '%s'; 9926ad19fcSEvan Bacon contentString += '%s'; 10026ad19fcSEvan Bacon } 10126ad19fcSEvan Bacon 10226ad19fcSEvan Bacon substitutionIndex++; 10326ad19fcSEvan Bacon } 10426ad19fcSEvan Bacon } 10526ad19fcSEvan Bacon 10626ad19fcSEvan Bacon categoryParts.push(categoryString); 10726ad19fcSEvan Bacon contentParts.push(contentString); 10826ad19fcSEvan Bacon } 10926ad19fcSEvan Bacon 11026ad19fcSEvan Bacon const remainingArgs = remaining.map((arg) => { 11126ad19fcSEvan Bacon // Don't stringify a string type. 11226ad19fcSEvan Bacon // It adds quotation mark wrappers around the string, 11326ad19fcSEvan Bacon // which causes the LogBox to look odd. 11426ad19fcSEvan Bacon return typeof arg === 'string' ? arg : stringifySafe(arg); 11526ad19fcSEvan Bacon }); 11626ad19fcSEvan Bacon categoryParts.push(...remainingArgs); 11726ad19fcSEvan Bacon contentParts.push(...remainingArgs); 11826ad19fcSEvan Bacon 11926ad19fcSEvan Bacon return { 12026ad19fcSEvan Bacon category: categoryParts.join(' '), 12126ad19fcSEvan Bacon message: { 12226ad19fcSEvan Bacon content: contentParts.join(' '), 12326ad19fcSEvan Bacon substitutions: substitutionOffsets, 12426ad19fcSEvan Bacon }, 12526ad19fcSEvan Bacon }; 12626ad19fcSEvan Bacon} 12726ad19fcSEvan Bacon 12826ad19fcSEvan Baconfunction isComponentStack(consoleArgument: string) { 12926ad19fcSEvan Bacon const isOldComponentStackFormat = / {4}in/.test(consoleArgument); 13026ad19fcSEvan Bacon const isNewComponentStackFormat = / {4}at/.test(consoleArgument); 13126ad19fcSEvan Bacon const isNewJSCComponentStackFormat = /@.*\n/.test(consoleArgument); 13226ad19fcSEvan Bacon 13326ad19fcSEvan Bacon return isOldComponentStackFormat || isNewComponentStackFormat || isNewJSCComponentStackFormat; 13426ad19fcSEvan Bacon} 13526ad19fcSEvan Bacon 13626ad19fcSEvan Baconexport function parseComponentStack(message: string): ComponentStack { 13726ad19fcSEvan Bacon // In newer versions of React, the component stack is formatted as a call stack frame. 13826ad19fcSEvan Bacon // First try to parse the component stack as a call stack frame, and if that doesn't 13926ad19fcSEvan Bacon // work then we'll fallback to the old custom component stack format parsing. 14026ad19fcSEvan Bacon const stack = parseErrorStack(message); 14126ad19fcSEvan Bacon if (stack && stack.length > 0) { 14226ad19fcSEvan Bacon return stack.map((frame) => ({ 14326ad19fcSEvan Bacon content: frame.methodName, 14426ad19fcSEvan Bacon collapse: frame.collapse || false, 14526ad19fcSEvan Bacon fileName: frame.file == null ? 'unknown' : frame.file, 14626ad19fcSEvan Bacon location: { 14726ad19fcSEvan Bacon column: frame.column == null ? -1 : frame.column, 14826ad19fcSEvan Bacon row: frame.lineNumber == null ? -1 : frame.lineNumber, 14926ad19fcSEvan Bacon }, 15026ad19fcSEvan Bacon })); 15126ad19fcSEvan Bacon } 15226ad19fcSEvan Bacon 15326ad19fcSEvan Bacon return message 15426ad19fcSEvan Bacon .split(/\n {4}in /g) 15526ad19fcSEvan Bacon .map((s) => { 15626ad19fcSEvan Bacon if (!s) { 15726ad19fcSEvan Bacon return null; 15826ad19fcSEvan Bacon } 15926ad19fcSEvan Bacon const match = s.match(/(.*) \(at (.*\.js):([\d]+)\)/); 16026ad19fcSEvan Bacon if (!match) { 16126ad19fcSEvan Bacon return null; 16226ad19fcSEvan Bacon } 16326ad19fcSEvan Bacon 16426ad19fcSEvan Bacon const [content, fileName, row] = match.slice(1); 16526ad19fcSEvan Bacon return { 16626ad19fcSEvan Bacon content, 16726ad19fcSEvan Bacon fileName, 16826ad19fcSEvan Bacon location: { column: -1, row: parseInt(row, 10) }, 16926ad19fcSEvan Bacon }; 17026ad19fcSEvan Bacon }) 17126ad19fcSEvan Bacon .filter(Boolean) as ComponentStack; 17226ad19fcSEvan Bacon} 17326ad19fcSEvan Bacon 17426ad19fcSEvan Baconexport function parseLogBoxException(error: ExtendedExceptionData): LogBoxLogData { 17526ad19fcSEvan Bacon const message = error.originalMessage != null ? error.originalMessage : 'Unknown'; 17626ad19fcSEvan Bacon 17726ad19fcSEvan Bacon const metroInternalError = message.match(METRO_ERROR_FORMAT); 17826ad19fcSEvan Bacon if (metroInternalError) { 17926ad19fcSEvan Bacon const [content, fileName, row, column, codeFrame] = metroInternalError.slice(1); 18026ad19fcSEvan Bacon 18126ad19fcSEvan Bacon return { 18226ad19fcSEvan Bacon level: 'fatal', 18326ad19fcSEvan Bacon type: 'Metro Error', 18426ad19fcSEvan Bacon stack: [], 18526ad19fcSEvan Bacon isComponentError: false, 18626ad19fcSEvan Bacon componentStack: [], 18726ad19fcSEvan Bacon codeFrame: { 18826ad19fcSEvan Bacon fileName, 18926ad19fcSEvan Bacon location: { 19026ad19fcSEvan Bacon row: parseInt(row, 10), 19126ad19fcSEvan Bacon column: parseInt(column, 10), 19226ad19fcSEvan Bacon }, 19326ad19fcSEvan Bacon content: codeFrame, 19426ad19fcSEvan Bacon }, 19526ad19fcSEvan Bacon message: { 19626ad19fcSEvan Bacon content, 19726ad19fcSEvan Bacon substitutions: [], 19826ad19fcSEvan Bacon }, 19926ad19fcSEvan Bacon category: `${fileName}-${row}-${column}`, 20026ad19fcSEvan Bacon }; 20126ad19fcSEvan Bacon } 20226ad19fcSEvan Bacon 20326ad19fcSEvan Bacon const babelTransformError = message.match(BABEL_TRANSFORM_ERROR_FORMAT); 20426ad19fcSEvan Bacon if (babelTransformError) { 20526ad19fcSEvan Bacon // Transform errors are thrown from inside the Babel transformer. 20626ad19fcSEvan Bacon const [fileName, content, row, column, codeFrame] = babelTransformError.slice(1); 20726ad19fcSEvan Bacon 20826ad19fcSEvan Bacon return { 20926ad19fcSEvan Bacon level: 'syntax', 21026ad19fcSEvan Bacon stack: [], 21126ad19fcSEvan Bacon isComponentError: false, 21226ad19fcSEvan Bacon componentStack: [], 21326ad19fcSEvan Bacon codeFrame: { 21426ad19fcSEvan Bacon fileName, 21526ad19fcSEvan Bacon location: { 21626ad19fcSEvan Bacon row: parseInt(row, 10), 21726ad19fcSEvan Bacon column: parseInt(column, 10), 21826ad19fcSEvan Bacon }, 21926ad19fcSEvan Bacon content: codeFrame, 22026ad19fcSEvan Bacon }, 22126ad19fcSEvan Bacon message: { 22226ad19fcSEvan Bacon content, 22326ad19fcSEvan Bacon substitutions: [], 22426ad19fcSEvan Bacon }, 22526ad19fcSEvan Bacon category: `${fileName}-${row}-${column}`, 22626ad19fcSEvan Bacon }; 22726ad19fcSEvan Bacon } 22826ad19fcSEvan Bacon 22926ad19fcSEvan Bacon const babelCodeFrameError = message.match(BABEL_CODE_FRAME_ERROR_FORMAT); 23026ad19fcSEvan Bacon 23126ad19fcSEvan Bacon if (babelCodeFrameError) { 23226ad19fcSEvan Bacon // Codeframe errors are thrown from any use of buildCodeFrameError. 23326ad19fcSEvan Bacon const [fileName, content, codeFrame] = babelCodeFrameError.slice(1); 23426ad19fcSEvan Bacon return { 23526ad19fcSEvan Bacon level: 'syntax', 23626ad19fcSEvan Bacon stack: [], 23726ad19fcSEvan Bacon isComponentError: false, 23826ad19fcSEvan Bacon componentStack: [], 23926ad19fcSEvan Bacon codeFrame: { 24026ad19fcSEvan Bacon fileName, 24126ad19fcSEvan Bacon location: null, // We are not given the location. 24226ad19fcSEvan Bacon content: codeFrame, 24326ad19fcSEvan Bacon }, 24426ad19fcSEvan Bacon message: { 24526ad19fcSEvan Bacon content, 24626ad19fcSEvan Bacon substitutions: [], 24726ad19fcSEvan Bacon }, 24826ad19fcSEvan Bacon category: `${fileName}-${1}-${1}`, 24926ad19fcSEvan Bacon }; 25026ad19fcSEvan Bacon } 25126ad19fcSEvan Bacon 25226ad19fcSEvan Bacon if (message.match(/^TransformError /)) { 25326ad19fcSEvan Bacon return { 25426ad19fcSEvan Bacon level: 'syntax', 25526ad19fcSEvan Bacon stack: error.stack, 25626ad19fcSEvan Bacon isComponentError: error.isComponentError, 25726ad19fcSEvan Bacon componentStack: [], 25826ad19fcSEvan Bacon message: { 25926ad19fcSEvan Bacon content: message, 26026ad19fcSEvan Bacon substitutions: [], 26126ad19fcSEvan Bacon }, 26226ad19fcSEvan Bacon category: message, 26326ad19fcSEvan Bacon }; 26426ad19fcSEvan Bacon } 26526ad19fcSEvan Bacon 26626ad19fcSEvan Bacon const componentStack = error.componentStack; 26726ad19fcSEvan Bacon if (error.isFatal || error.isComponentError) { 26826ad19fcSEvan Bacon return { 26926ad19fcSEvan Bacon level: 'fatal', 27026ad19fcSEvan Bacon stack: error.stack, 27126ad19fcSEvan Bacon isComponentError: error.isComponentError, 27226ad19fcSEvan Bacon componentStack: componentStack != null ? parseComponentStack(componentStack) : [], 27326ad19fcSEvan Bacon ...parseInterpolation([message]), 27426ad19fcSEvan Bacon }; 27526ad19fcSEvan Bacon } 27626ad19fcSEvan Bacon 27726ad19fcSEvan Bacon if (componentStack != null) { 27826ad19fcSEvan Bacon // It is possible that console errors have a componentStack. 27926ad19fcSEvan Bacon return { 28026ad19fcSEvan Bacon level: 'error', 28126ad19fcSEvan Bacon stack: error.stack, 28226ad19fcSEvan Bacon isComponentError: error.isComponentError, 28326ad19fcSEvan Bacon componentStack: parseComponentStack(componentStack), 28426ad19fcSEvan Bacon ...parseInterpolation([message]), 28526ad19fcSEvan Bacon }; 28626ad19fcSEvan Bacon } 28726ad19fcSEvan Bacon 28826ad19fcSEvan Bacon // Most `console.error` calls won't have a componentStack. We parse them like 28926ad19fcSEvan Bacon // regular logs which have the component stack burried in the message. 29026ad19fcSEvan Bacon return { 29126ad19fcSEvan Bacon level: 'error', 29226ad19fcSEvan Bacon stack: error.stack, 29326ad19fcSEvan Bacon isComponentError: error.isComponentError, 29426ad19fcSEvan Bacon ...parseLogBoxLog([message]), 29526ad19fcSEvan Bacon }; 29626ad19fcSEvan Bacon} 29726ad19fcSEvan Bacon 29826ad19fcSEvan Baconexport function parseLogBoxLog(args: readonly any[]): { 29926ad19fcSEvan Bacon componentStack: ComponentStack; 30026ad19fcSEvan Bacon category: Category; 30126ad19fcSEvan Bacon message: Message; 30226ad19fcSEvan Bacon} { 30326ad19fcSEvan Bacon const message = args[0]; 30426ad19fcSEvan Bacon let argsWithoutComponentStack: any[] = []; 30526ad19fcSEvan Bacon let componentStack: ComponentStack = []; 30626ad19fcSEvan Bacon 30726ad19fcSEvan Bacon // Extract component stack from warnings like "Some warning%s". 30826ad19fcSEvan Bacon if (typeof message === 'string' && message.slice(-2) === '%s' && args.length > 0) { 30926ad19fcSEvan Bacon const lastArg = args[args.length - 1]; 31026ad19fcSEvan Bacon if (typeof lastArg === 'string' && isComponentStack(lastArg)) { 31126ad19fcSEvan Bacon argsWithoutComponentStack = args.slice(0, -1); 31226ad19fcSEvan Bacon argsWithoutComponentStack[0] = message.slice(0, -2); 31326ad19fcSEvan Bacon componentStack = parseComponentStack(lastArg); 31426ad19fcSEvan Bacon } 31526ad19fcSEvan Bacon } 31626ad19fcSEvan Bacon 31726ad19fcSEvan Bacon if (componentStack.length === 0) { 31826ad19fcSEvan Bacon // Try finding the component stack elsewhere. 31926ad19fcSEvan Bacon for (const arg of args) { 32026ad19fcSEvan Bacon if (typeof arg === 'string' && isComponentStack(arg)) { 32126ad19fcSEvan Bacon // Strip out any messages before the component stack. 32226ad19fcSEvan Bacon let messageEndIndex = arg.search(/\n {4}(in|at) /); 32326ad19fcSEvan Bacon if (messageEndIndex < 0) { 32426ad19fcSEvan Bacon // Handle JSC component stacks. 32526ad19fcSEvan Bacon messageEndIndex = arg.search(/\n/); 32626ad19fcSEvan Bacon } 32726ad19fcSEvan Bacon if (messageEndIndex > 0) { 32826ad19fcSEvan Bacon argsWithoutComponentStack.push(arg.slice(0, messageEndIndex)); 32926ad19fcSEvan Bacon } 33026ad19fcSEvan Bacon 33126ad19fcSEvan Bacon componentStack = parseComponentStack(arg); 33226ad19fcSEvan Bacon } else { 33326ad19fcSEvan Bacon argsWithoutComponentStack.push(arg); 33426ad19fcSEvan Bacon } 33526ad19fcSEvan Bacon } 33626ad19fcSEvan Bacon } 33726ad19fcSEvan Bacon 33826ad19fcSEvan Bacon return { 33926ad19fcSEvan Bacon ...parseInterpolation(argsWithoutComponentStack), 34026ad19fcSEvan Bacon componentStack, 34126ad19fcSEvan Bacon }; 34226ad19fcSEvan Bacon} 343