1/** 2 * Copyright (c) 650 Industries. 3 * Copyright (c) Meta Platforms, Inc. and affiliates. 4 * 5 * This source code is licensed under the MIT license found in the 6 * LICENSE file in the root directory of this source tree. 7 */ 8import UTFSequence from 'react-native/Libraries/UTFSequence'; 9import parseErrorStack from '../modules/parseErrorStack'; 10import stringifySafe from '../modules/stringifySafe'; 11const BABEL_TRANSFORM_ERROR_FORMAT = /^(?:TransformError )?(?:SyntaxError: |ReferenceError: )(.*): (.*) \((\d+):(\d+)\)\n\n([\s\S]+)/; 12const BABEL_CODE_FRAME_ERROR_FORMAT = /^(?:TransformError )?(?:.*):? (?:.*?)(\/.*): ([\s\S]+?)\n([ >]{2}[\d\s]+ \|[\s\S]+|\u{001b}[\s\S]+)/u; 13const METRO_ERROR_FORMAT = /^(?:InternalError Metro has encountered an error:) (.*): (.*) \((\d+):(\d+)\)\n\n([\s\S]+)/u; 14const SUBSTITUTION = UTFSequence.BOM + '%s'; 15export function parseInterpolation(args) { 16 const categoryParts = []; 17 const contentParts = []; 18 const substitutionOffsets = []; 19 const remaining = [...args]; 20 if (typeof remaining[0] === 'string') { 21 const formatString = String(remaining.shift()); 22 const formatStringParts = formatString.split('%s'); 23 const substitutionCount = formatStringParts.length - 1; 24 const substitutions = remaining.splice(0, substitutionCount); 25 let categoryString = ''; 26 let contentString = ''; 27 let substitutionIndex = 0; 28 for (const formatStringPart of formatStringParts) { 29 categoryString += formatStringPart; 30 contentString += formatStringPart; 31 if (substitutionIndex < substitutionCount) { 32 if (substitutionIndex < substitutions.length) { 33 // Don't stringify a string type. 34 // It adds quotation mark wrappers around the string, 35 // which causes the LogBox to look odd. 36 const substitution = typeof substitutions[substitutionIndex] === 'string' 37 ? substitutions[substitutionIndex] 38 : stringifySafe(substitutions[substitutionIndex]); 39 substitutionOffsets.push({ 40 length: substitution.length, 41 offset: contentString.length, 42 }); 43 categoryString += SUBSTITUTION; 44 contentString += substitution; 45 } 46 else { 47 substitutionOffsets.push({ 48 length: 2, 49 offset: contentString.length, 50 }); 51 categoryString += '%s'; 52 contentString += '%s'; 53 } 54 substitutionIndex++; 55 } 56 } 57 categoryParts.push(categoryString); 58 contentParts.push(contentString); 59 } 60 const remainingArgs = remaining.map((arg) => { 61 // Don't stringify a string type. 62 // It adds quotation mark wrappers around the string, 63 // which causes the LogBox to look odd. 64 return typeof arg === 'string' ? arg : stringifySafe(arg); 65 }); 66 categoryParts.push(...remainingArgs); 67 contentParts.push(...remainingArgs); 68 return { 69 category: categoryParts.join(' '), 70 message: { 71 content: contentParts.join(' '), 72 substitutions: substitutionOffsets, 73 }, 74 }; 75} 76function isComponentStack(consoleArgument) { 77 const isOldComponentStackFormat = / {4}in/.test(consoleArgument); 78 const isNewComponentStackFormat = / {4}at/.test(consoleArgument); 79 const isNewJSCComponentStackFormat = /@.*\n/.test(consoleArgument); 80 return isOldComponentStackFormat || isNewComponentStackFormat || isNewJSCComponentStackFormat; 81} 82export function parseComponentStack(message) { 83 // In newer versions of React, the component stack is formatted as a call stack frame. 84 // First try to parse the component stack as a call stack frame, and if that doesn't 85 // work then we'll fallback to the old custom component stack format parsing. 86 const stack = parseErrorStack(message); 87 if (stack && stack.length > 0) { 88 return stack.map((frame) => ({ 89 content: frame.methodName, 90 collapse: frame.collapse || false, 91 fileName: frame.file == null ? 'unknown' : frame.file, 92 location: { 93 column: frame.column == null ? -1 : frame.column, 94 row: frame.lineNumber == null ? -1 : frame.lineNumber, 95 }, 96 })); 97 } 98 return message 99 .split(/\n {4}in /g) 100 .map((s) => { 101 if (!s) { 102 return null; 103 } 104 const match = s.match(/(.*) \(at (.*\.js):([\d]+)\)/); 105 if (!match) { 106 return null; 107 } 108 const [content, fileName, row] = match.slice(1); 109 return { 110 content, 111 fileName, 112 location: { column: -1, row: parseInt(row, 10) }, 113 }; 114 }) 115 .filter(Boolean); 116} 117export function parseLogBoxException(error) { 118 const message = error.originalMessage != null ? error.originalMessage : 'Unknown'; 119 const metroInternalError = message.match(METRO_ERROR_FORMAT); 120 if (metroInternalError) { 121 const [content, fileName, row, column, codeFrame] = metroInternalError.slice(1); 122 return { 123 level: 'fatal', 124 type: 'Metro Error', 125 stack: [], 126 isComponentError: false, 127 componentStack: [], 128 codeFrame: { 129 fileName, 130 location: { 131 row: parseInt(row, 10), 132 column: parseInt(column, 10), 133 }, 134 content: codeFrame, 135 }, 136 message: { 137 content, 138 substitutions: [], 139 }, 140 category: `${fileName}-${row}-${column}`, 141 }; 142 } 143 const babelTransformError = message.match(BABEL_TRANSFORM_ERROR_FORMAT); 144 if (babelTransformError) { 145 // Transform errors are thrown from inside the Babel transformer. 146 const [fileName, content, row, column, codeFrame] = babelTransformError.slice(1); 147 return { 148 level: 'syntax', 149 stack: [], 150 isComponentError: false, 151 componentStack: [], 152 codeFrame: { 153 fileName, 154 location: { 155 row: parseInt(row, 10), 156 column: parseInt(column, 10), 157 }, 158 content: codeFrame, 159 }, 160 message: { 161 content, 162 substitutions: [], 163 }, 164 category: `${fileName}-${row}-${column}`, 165 }; 166 } 167 const babelCodeFrameError = message.match(BABEL_CODE_FRAME_ERROR_FORMAT); 168 if (babelCodeFrameError) { 169 // Codeframe errors are thrown from any use of buildCodeFrameError. 170 const [fileName, content, codeFrame] = babelCodeFrameError.slice(1); 171 return { 172 level: 'syntax', 173 stack: [], 174 isComponentError: false, 175 componentStack: [], 176 codeFrame: { 177 fileName, 178 location: null, 179 content: codeFrame, 180 }, 181 message: { 182 content, 183 substitutions: [], 184 }, 185 category: `${fileName}-${1}-${1}`, 186 }; 187 } 188 if (message.match(/^TransformError /)) { 189 return { 190 level: 'syntax', 191 stack: error.stack, 192 isComponentError: error.isComponentError, 193 componentStack: [], 194 message: { 195 content: message, 196 substitutions: [], 197 }, 198 category: message, 199 }; 200 } 201 const componentStack = error.componentStack; 202 if (error.isFatal || error.isComponentError) { 203 return { 204 level: 'fatal', 205 stack: error.stack, 206 isComponentError: error.isComponentError, 207 componentStack: componentStack != null ? parseComponentStack(componentStack) : [], 208 ...parseInterpolation([message]), 209 }; 210 } 211 if (componentStack != null) { 212 // It is possible that console errors have a componentStack. 213 return { 214 level: 'error', 215 stack: error.stack, 216 isComponentError: error.isComponentError, 217 componentStack: parseComponentStack(componentStack), 218 ...parseInterpolation([message]), 219 }; 220 } 221 // Most `console.error` calls won't have a componentStack. We parse them like 222 // regular logs which have the component stack burried in the message. 223 return { 224 level: 'error', 225 stack: error.stack, 226 isComponentError: error.isComponentError, 227 ...parseLogBoxLog([message]), 228 }; 229} 230export function parseLogBoxLog(args) { 231 const message = args[0]; 232 let argsWithoutComponentStack = []; 233 let componentStack = []; 234 // Extract component stack from warnings like "Some warning%s". 235 if (typeof message === 'string' && message.slice(-2) === '%s' && args.length > 0) { 236 const lastArg = args[args.length - 1]; 237 if (typeof lastArg === 'string' && isComponentStack(lastArg)) { 238 argsWithoutComponentStack = args.slice(0, -1); 239 argsWithoutComponentStack[0] = message.slice(0, -2); 240 componentStack = parseComponentStack(lastArg); 241 } 242 } 243 if (componentStack.length === 0) { 244 // Try finding the component stack elsewhere. 245 for (const arg of args) { 246 if (typeof arg === 'string' && isComponentStack(arg)) { 247 // Strip out any messages before the component stack. 248 let messageEndIndex = arg.search(/\n {4}(in|at) /); 249 if (messageEndIndex < 0) { 250 // Handle JSC component stacks. 251 messageEndIndex = arg.search(/\n/); 252 } 253 if (messageEndIndex > 0) { 254 argsWithoutComponentStack.push(arg.slice(0, messageEndIndex)); 255 } 256 componentStack = parseComponentStack(arg); 257 } 258 else { 259 argsWithoutComponentStack.push(arg); 260 } 261 } 262 } 263 return { 264 ...parseInterpolation(argsWithoutComponentStack), 265 componentStack, 266 }; 267} 268//# sourceMappingURL=parseLogBoxLog.js.map