1/**
2 * Copyright © 2022 650 Industries.
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 */
7import chalk from 'chalk';
8import resolveFrom from 'resolve-from';
9import { StackFrame } from 'stacktrace-parser';
10import terminalLink from 'terminal-link';
11
12import { Log } from '../../../log';
13import { createMetroEndpointAsync } from '../getStaticRenderFunctions';
14// import type { CodeFrame, MetroStackFrame } from '@expo/metro-runtime/symbolicate';
15
16type CodeFrame = {
17  content: string;
18  location?: {
19    row: number;
20    column: number;
21    [key: string]: any;
22  };
23  fileName: string;
24};
25
26type MetroStackFrame = StackFrame & { collapse?: boolean };
27
28export async function logMetroErrorWithStack(
29  projectRoot: string,
30  {
31    stack,
32    codeFrame,
33    error,
34  }: {
35    stack: MetroStackFrame[];
36    codeFrame: CodeFrame;
37    error: Error;
38  }
39) {
40  const { getStackFormattedLocation } = require(resolveFrom(
41    projectRoot,
42    '@expo/metro-runtime/symbolicate'
43  ));
44
45  Log.log();
46  Log.log(chalk.red('Metro error: ') + error.message);
47  Log.log();
48
49  if (codeFrame) {
50    Log.log(codeFrame.content);
51  }
52
53  if (stack?.length) {
54    Log.log();
55    Log.log(chalk.bold`Call Stack`);
56
57    const stackProps = stack.map((frame) => {
58      return {
59        title: frame.methodName,
60        subtitle: getStackFormattedLocation(projectRoot, frame),
61        collapse: frame.collapse,
62      };
63    });
64
65    stackProps.forEach((frame) => {
66      const position = terminalLink.isSupported
67        ? terminalLink(frame.subtitle, frame.subtitle)
68        : frame.subtitle;
69      let lineItem = chalk.gray(`  ${frame.title} (${position})`);
70      if (frame.collapse) {
71        lineItem = chalk.dim(lineItem);
72      }
73      Log.log(lineItem);
74    });
75  } else {
76    Log.log(chalk.gray(`  ${error.stack}`));
77  }
78}
79
80export async function logMetroError(projectRoot: string, { error }: { error: Error }) {
81  const { LogBoxLog, parseErrorStack } = require(resolveFrom(
82    projectRoot,
83    '@expo/metro-runtime/symbolicate'
84  ));
85
86  const stack = parseErrorStack(error.stack);
87
88  const log = new LogBoxLog({
89    level: 'static',
90    message: {
91      content: error.message,
92      substitutions: [],
93    },
94    isComponentError: false,
95    stack,
96    category: 'static',
97    componentStack: [],
98  });
99
100  await new Promise((res) => log.symbolicate('stack', res));
101
102  logMetroErrorWithStack(projectRoot, {
103    stack: log.symbolicated?.stack?.stack ?? [],
104    codeFrame: log.codeFrame,
105    error,
106  });
107}
108
109/** @returns the html required to render the static metro error as an SPA. */
110export function logFromError({ error, projectRoot }: { error: Error; projectRoot: string }): {
111  symbolicated: any;
112  symbolicate: (type: string, callback: () => void) => void;
113  codeFrame: CodeFrame;
114} {
115  const { LogBoxLog, parseErrorStack } = require(resolveFrom(
116    projectRoot,
117    '@expo/metro-runtime/symbolicate'
118  ));
119
120  const stack = parseErrorStack(error.stack);
121
122  return new LogBoxLog({
123    level: 'static',
124    message: {
125      content: error.message,
126      substitutions: [],
127    },
128    isComponentError: false,
129    stack,
130    category: 'static',
131    componentStack: [],
132  });
133}
134
135/** @returns the html required to render the static metro error as an SPA. */
136export async function logMetroErrorAsync({
137  error,
138  projectRoot,
139}: {
140  error: Error;
141  projectRoot: string;
142}) {
143  const log = logFromError({ projectRoot, error });
144
145  await new Promise<void>((res) => log.symbolicate('stack', res));
146
147  logMetroErrorWithStack(projectRoot, {
148    stack: log.symbolicated?.stack?.stack ?? [],
149    codeFrame: log.codeFrame,
150    error,
151  });
152}
153
154/** @returns the html required to render the static metro error as an SPA. */
155export async function getErrorOverlayHtmlAsync({
156  error,
157  projectRoot,
158}: {
159  error: Error;
160  projectRoot: string;
161}) {
162  const log = logFromError({ projectRoot, error });
163
164  await new Promise<void>((res) => log.symbolicate('stack', res));
165
166  logMetroErrorWithStack(projectRoot, {
167    stack: log.symbolicated?.stack?.stack ?? [],
168    codeFrame: log.codeFrame,
169    error,
170  });
171
172  const logBoxContext = {
173    selectedLogIndex: 0,
174    isDisabled: false,
175    logs: [log],
176  };
177  const html = `<html><head><style>#root,body,html{height:100%}body{overflow:hidden}#root{display:flex}</style></head><body><div id="root"></div><script id="_expo-static-error" type="application/json">${JSON.stringify(
178    logBoxContext
179  )}</script></body></html>`;
180
181  const errorOverlayEntry = await createMetroEndpointAsync(
182    projectRoot,
183    // Keep the URL relative
184    '',
185    resolveFrom(projectRoot, 'expo-router/_error'),
186    {
187      dev: true,
188      platform: 'web',
189      minify: false,
190      environment: 'node',
191    }
192  );
193
194  const htmlWithJs = html.replace('</body>', `<script src=${errorOverlayEntry}></script></body>`);
195  return htmlWithJs;
196}
197