1// Copyright 2023-present 650 Industries (Expo). All rights reserved.
2import { SymbolicatorConfigT } from 'metro-config';
3import { URL } from 'url';
4
5type CustomizeFrameFunc = SymbolicatorConfigT['customizeFrame'];
6
7// Import only the types here, the values will be imported from the project, at runtime.
8export const INTERNAL_CALLSITES_REGEX = new RegExp(
9  [
10    '/Libraries/Renderer/implementations/.+\\.js$',
11    '/Libraries/BatchedBridge/MessageQueue\\.js$',
12    '/Libraries/YellowBox/.+\\.js$',
13    '/Libraries/LogBox/.+\\.js$',
14    '/Libraries/Core/Timers/.+\\.js$',
15    'node_modules/react-devtools-core/.+\\.js$',
16    'node_modules/react-refresh/.+\\.js$',
17    'node_modules/scheduler/.+\\.js$',
18    // Metro replaces `require()` with a different method,
19    // we want to omit this method from the stack trace.
20    // This is akin to most React tooling.
21    '/metro/.*/polyfills/require.js$',
22    // Hide frames related to a fast refresh.
23    '/metro/.*/lib/bundle-modules/.+\\.js$',
24    'node_modules/react-native/Libraries/Utilities/HMRClient.js$',
25    'node_modules/eventemitter3/index.js',
26    'node_modules/event-target-shim/dist/.+\\.js$',
27    // Ignore the log forwarder used in the expo package.
28    '/expo/build/logs/RemoteConsole.js$',
29    // Improve errors thrown by invariant (ex: `Invariant Violation: "main" has not been registered`).
30    'node_modules/invariant/.+\\.js$',
31    // Remove babel runtime additions
32    'node_modules/regenerator-runtime/.+\\.js$',
33    // Remove react native setImmediate ponyfill
34    'node_modules/promise/setimmediate/.+\\.js$',
35    // Babel helpers that implement language features
36    'node_modules/@babel/runtime/.+\\.js$',
37    // Hide Hermes internal bytecode
38    '/InternalBytecode/InternalBytecode\\.js$',
39    // Block native code invocations
40    `\\[native code\\]`,
41    // Hide react-dom (web)
42    'node_modules/react-dom/.+\\.js$',
43    '@expo/metro-runtime/build/.+\\.js$',
44  ].join('|')
45);
46
47function isUrl(value: string): boolean {
48  try {
49    // eslint-disable-next-line no-new
50    new URL(value);
51    return true;
52  } catch {
53    return false;
54  }
55}
56
57/**
58 * The default frame processor. This is used to modify the stack traces.
59 * This method attempts to collapse all frames that aren't relevant to
60 * the user by default.
61 */
62export function getDefaultCustomizeFrame(): CustomizeFrameFunc {
63  return (frame: Parameters<CustomizeFrameFunc>[0]) => {
64    if (frame.file && isUrl(frame.file)) {
65      return {
66        ...frame,
67        // HACK: This prevents Metro from attempting to read the invalid file URL it sent us.
68        lineNumber: null,
69        column: null,
70        // This prevents the invalid frame from being shown by default.
71        collapse: true,
72      };
73    }
74    let collapse = Boolean(frame.file && INTERNAL_CALLSITES_REGEX.test(frame.file));
75
76    if (!collapse) {
77      // This represents the first frame of the stacktrace.
78      // Often this looks like: `__r(0);`.
79      // The URL will also be unactionable in the app and therefore not very useful to the developer.
80      if (
81        frame.column === 3 &&
82        frame.methodName === 'global code' &&
83        frame.file?.match(/^https?:\/\//g)
84      ) {
85        collapse = true;
86      }
87    }
88
89    return { ...(frame || {}), collapse };
90  };
91}
92