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
80/** @returns the html required to render the static metro error as an SPA. */
81export async function getErrorOverlayHtmlAsync({
82  error,
83  projectRoot,
84}: {
85  error: Error;
86  projectRoot: string;
87}) {
88  const { LogBoxLog, parseErrorStack } = require(resolveFrom(
89    projectRoot,
90    '@expo/metro-runtime/symbolicate'
91  ));
92  const stack = parseErrorStack(error.stack);
93
94  const log = new LogBoxLog({
95    level: 'static',
96    message: {
97      content: error.message,
98      substitutions: [],
99    },
100    isComponentError: false,
101    stack,
102    category: 'static',
103    componentStack: [],
104  });
105
106  await new Promise((res) => log.symbolicate('stack', res));
107
108  logMetroErrorWithStack(projectRoot, {
109    stack: log.symbolicated?.stack?.stack ?? [],
110    codeFrame: log.codeFrame,
111    error,
112  });
113
114  const logBoxContext = {
115    selectedLogIndex: 0,
116    isDisabled: false,
117    logs: [log],
118  };
119  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(
120    logBoxContext
121  )}</script></body></html>`;
122
123  const errorOverlayEntry = await createMetroEndpointAsync(
124    projectRoot,
125    // Keep the URL relative
126    '',
127    resolveFrom(projectRoot, 'expo-router/_error'),
128    {
129      dev: true,
130      platform: 'web',
131      minify: false,
132      environment: 'node',
133    }
134  );
135
136  const htmlWithJs = html.replace('</body>', `<script src=${errorOverlayEntry}></script></body>`);
137  return htmlWithJs;
138}
139