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 */ 8 9import type { LogBoxLogData } from './LogBoxLog'; 10import parseErrorStack from '../modules/parseErrorStack'; 11import stringifySafe from '../modules/stringifySafe'; 12type ExceptionData = any; 13 14const BABEL_TRANSFORM_ERROR_FORMAT = 15 /^(?:TransformError )?(?:SyntaxError: |ReferenceError: )(.*): (.*) \((\d+):(\d+)\)\n\n([\s\S]+)/; 16const BABEL_CODE_FRAME_ERROR_FORMAT = 17 /^(?:TransformError )?(?:.*):? (?:.*?)(\/.*): ([\s\S]+?)\n([ >]{2}[\d\s]+ \|[\s\S]+|\u{001b}[\s\S]+)/u; 18const METRO_ERROR_FORMAT = 19 /^(?:InternalError Metro has encountered an error:) (.*): (.*) \((\d+):(\d+)\)\n\n([\s\S]+)/u; 20 21export type ExtendedExceptionData = ExceptionData & { 22 isComponentError: boolean; 23 [key: string]: any; 24}; 25export type Category = string; 26export type CodeFrame = { 27 content: string; 28 location?: { 29 row: number; 30 column: number; 31 [key: string]: any; 32 } | null; 33 fileName: string; 34 35 // TODO: When React switched to using call stack frames, 36 // we gained the ability to use the collapse flag, but 37 // it is not integrated into the LogBox UI. 38 collapse?: boolean; 39}; 40 41export type Message = { 42 content: string; 43 substitutions: { 44 length: number; 45 offset: number; 46 }[]; 47}; 48 49export type ComponentStack = CodeFrame[]; 50 51const SUBSTITUTION = '\ufeff%s'; 52 53export function parseInterpolation(args: readonly any[]): { 54 category: Category; 55 message: Message; 56} { 57 const categoryParts: string[] = []; 58 const contentParts: string[] = []; 59 const substitutionOffsets: { length: number; offset: number }[] = []; 60 61 const remaining = [...args]; 62 if (typeof remaining[0] === 'string') { 63 const formatString = String(remaining.shift()); 64 const formatStringParts = formatString.split('%s'); 65 const substitutionCount = formatStringParts.length - 1; 66 const substitutions = remaining.splice(0, substitutionCount); 67 68 let categoryString = ''; 69 let contentString = ''; 70 71 let substitutionIndex = 0; 72 for (const formatStringPart of formatStringParts) { 73 categoryString += formatStringPart; 74 contentString += formatStringPart; 75 76 if (substitutionIndex < substitutionCount) { 77 if (substitutionIndex < substitutions.length) { 78 // Don't stringify a string type. 79 // It adds quotation mark wrappers around the string, 80 // which causes the LogBox to look odd. 81 const substitution = 82 typeof substitutions[substitutionIndex] === 'string' 83 ? substitutions[substitutionIndex] 84 : stringifySafe(substitutions[substitutionIndex]); 85 substitutionOffsets.push({ 86 length: substitution.length, 87 offset: contentString.length, 88 }); 89 90 categoryString += SUBSTITUTION; 91 contentString += substitution; 92 } else { 93 substitutionOffsets.push({ 94 length: 2, 95 offset: contentString.length, 96 }); 97 98 categoryString += '%s'; 99 contentString += '%s'; 100 } 101 102 substitutionIndex++; 103 } 104 } 105 106 categoryParts.push(categoryString); 107 contentParts.push(contentString); 108 } 109 110 const remainingArgs = remaining.map((arg) => { 111 // Don't stringify a string type. 112 // It adds quotation mark wrappers around the string, 113 // which causes the LogBox to look odd. 114 return typeof arg === 'string' ? arg : stringifySafe(arg); 115 }); 116 categoryParts.push(...remainingArgs); 117 contentParts.push(...remainingArgs); 118 119 return { 120 category: categoryParts.join(' '), 121 message: { 122 content: contentParts.join(' '), 123 substitutions: substitutionOffsets, 124 }, 125 }; 126} 127 128function isComponentStack(consoleArgument: string) { 129 const isOldComponentStackFormat = / {4}in/.test(consoleArgument); 130 const isNewComponentStackFormat = / {4}at/.test(consoleArgument); 131 const isNewJSCComponentStackFormat = /@.*\n/.test(consoleArgument); 132 133 return isOldComponentStackFormat || isNewComponentStackFormat || isNewJSCComponentStackFormat; 134} 135 136export function parseComponentStack(message: string): ComponentStack { 137 // In newer versions of React, the component stack is formatted as a call stack frame. 138 // First try to parse the component stack as a call stack frame, and if that doesn't 139 // work then we'll fallback to the old custom component stack format parsing. 140 const stack = parseErrorStack(message); 141 if (stack && stack.length > 0) { 142 return stack.map((frame) => ({ 143 content: frame.methodName, 144 collapse: frame.collapse || false, 145 fileName: frame.file == null ? 'unknown' : frame.file, 146 location: { 147 column: frame.column == null ? -1 : frame.column, 148 row: frame.lineNumber == null ? -1 : frame.lineNumber, 149 }, 150 })); 151 } 152 153 return message 154 .split(/\n {4}in /g) 155 .map((s) => { 156 if (!s) { 157 return null; 158 } 159 const match = s.match(/(.*) \(at (.*\.js):([\d]+)\)/); 160 if (!match) { 161 return null; 162 } 163 164 const [content, fileName, row] = match.slice(1); 165 return { 166 content, 167 fileName, 168 location: { column: -1, row: parseInt(row, 10) }, 169 }; 170 }) 171 .filter(Boolean) as ComponentStack; 172} 173 174export function parseLogBoxException(error: ExtendedExceptionData): LogBoxLogData { 175 const message = error.originalMessage != null ? error.originalMessage : 'Unknown'; 176 177 const metroInternalError = message.match(METRO_ERROR_FORMAT); 178 if (metroInternalError) { 179 const [content, fileName, row, column, codeFrame] = metroInternalError.slice(1); 180 181 return { 182 level: 'fatal', 183 type: 'Metro Error', 184 stack: [], 185 isComponentError: false, 186 componentStack: [], 187 codeFrame: { 188 fileName, 189 location: { 190 row: parseInt(row, 10), 191 column: parseInt(column, 10), 192 }, 193 content: codeFrame, 194 }, 195 message: { 196 content, 197 substitutions: [], 198 }, 199 category: `${fileName}-${row}-${column}`, 200 }; 201 } 202 203 const babelTransformError = message.match(BABEL_TRANSFORM_ERROR_FORMAT); 204 if (babelTransformError) { 205 // Transform errors are thrown from inside the Babel transformer. 206 const [fileName, content, row, column, codeFrame] = babelTransformError.slice(1); 207 208 return { 209 level: 'syntax', 210 stack: [], 211 isComponentError: false, 212 componentStack: [], 213 codeFrame: { 214 fileName, 215 location: { 216 row: parseInt(row, 10), 217 column: parseInt(column, 10), 218 }, 219 content: codeFrame, 220 }, 221 message: { 222 content, 223 substitutions: [], 224 }, 225 category: `${fileName}-${row}-${column}`, 226 }; 227 } 228 229 const babelCodeFrameError = message.match(BABEL_CODE_FRAME_ERROR_FORMAT); 230 231 if (babelCodeFrameError) { 232 // Codeframe errors are thrown from any use of buildCodeFrameError. 233 const [fileName, content, codeFrame] = babelCodeFrameError.slice(1); 234 return { 235 level: 'syntax', 236 stack: [], 237 isComponentError: false, 238 componentStack: [], 239 codeFrame: { 240 fileName, 241 location: null, // We are not given the location. 242 content: codeFrame, 243 }, 244 message: { 245 content, 246 substitutions: [], 247 }, 248 category: `${fileName}-${1}-${1}`, 249 }; 250 } 251 252 if (message.match(/^TransformError /)) { 253 return { 254 level: 'syntax', 255 stack: error.stack, 256 isComponentError: error.isComponentError, 257 componentStack: [], 258 message: { 259 content: message, 260 substitutions: [], 261 }, 262 category: message, 263 }; 264 } 265 266 const componentStack = error.componentStack; 267 if (error.isFatal || error.isComponentError) { 268 return { 269 level: 'fatal', 270 stack: error.stack, 271 isComponentError: error.isComponentError, 272 componentStack: componentStack != null ? parseComponentStack(componentStack) : [], 273 ...parseInterpolation([message]), 274 }; 275 } 276 277 if (componentStack != null) { 278 // It is possible that console errors have a componentStack. 279 return { 280 level: 'error', 281 stack: error.stack, 282 isComponentError: error.isComponentError, 283 componentStack: parseComponentStack(componentStack), 284 ...parseInterpolation([message]), 285 }; 286 } 287 288 // Most `console.error` calls won't have a componentStack. We parse them like 289 // regular logs which have the component stack burried in the message. 290 return { 291 level: 'error', 292 stack: error.stack, 293 isComponentError: error.isComponentError, 294 ...parseLogBoxLog([message]), 295 }; 296} 297 298export function parseLogBoxLog(args: readonly any[]): { 299 componentStack: ComponentStack; 300 category: Category; 301 message: Message; 302} { 303 const message = args[0]; 304 let argsWithoutComponentStack: any[] = []; 305 let componentStack: ComponentStack = []; 306 307 // Extract component stack from warnings like "Some warning%s". 308 if (typeof message === 'string' && message.slice(-2) === '%s' && args.length > 0) { 309 const lastArg = args[args.length - 1]; 310 if (typeof lastArg === 'string' && isComponentStack(lastArg)) { 311 argsWithoutComponentStack = args.slice(0, -1); 312 argsWithoutComponentStack[0] = message.slice(0, -2); 313 componentStack = parseComponentStack(lastArg); 314 } 315 } 316 317 if (componentStack.length === 0) { 318 // Try finding the component stack elsewhere. 319 for (const arg of args) { 320 if (typeof arg === 'string' && isComponentStack(arg)) { 321 // Strip out any messages before the component stack. 322 let messageEndIndex = arg.search(/\n {4}(in|at) /); 323 if (messageEndIndex < 0) { 324 // Handle JSC component stacks. 325 messageEndIndex = arg.search(/\n/); 326 } 327 if (messageEndIndex > 0) { 328 argsWithoutComponentStack.push(arg.slice(0, messageEndIndex)); 329 } 330 331 componentStack = parseComponentStack(arg); 332 } else { 333 argsWithoutComponentStack.push(arg); 334 } 335 } 336 } 337 338 return { 339 ...parseInterpolation(argsWithoutComponentStack), 340 componentStack, 341 }; 342} 343