parseLogBoxLog.js 8.8 KB

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