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(
41    resolveFrom(projectRoot, '@expo/metro-runtime/symbolicate')
42  );
43
44  Log.log();
45  Log.log(chalk.red('Metro error: ') + error.message);
46  Log.log();
47
48  if (codeFrame) {
49    Log.log(codeFrame.content);
50  }
51
52  if (stack?.length) {
53    Log.log();
54    Log.log(chalk.bold`Call Stack`);
55
56    const stackProps = stack.map((frame) => {
57      return {
58        title: frame.methodName,
59        subtitle: getStackFormattedLocation(projectRoot, frame),
60        collapse: frame.collapse,
61      };
62    });
63
64    stackProps.forEach((frame) => {
65      const position = terminalLink.isSupported
66        ? terminalLink(frame.subtitle, frame.subtitle)
67        : frame.subtitle;
68      let lineItem = chalk.gray(`  ${frame.title} (${position})`);
69      if (frame.collapse) {
70        lineItem = chalk.dim(lineItem);
71      }
72      Log.log(lineItem);
73    });
74  } else {
75    Log.log(chalk.gray(`  ${error.stack}`));
76  }
77}
78
79export async function logMetroError(projectRoot: string, { error }: { error: Error }) {
80  const { LogBoxLog, parseErrorStack } = require(
81    resolveFrom(projectRoot, '@expo/metro-runtime/symbolicate')
82  );
83
84  const stack = parseErrorStack(error.stack);
85
86  const log = new LogBoxLog({
87    level: 'static',
88    message: {
89      content: error.message,
90      substitutions: [],
91    },
92    isComponentError: false,
93    stack,
94    category: 'static',
95    componentStack: [],
96  });
97
98  await new Promise((res) => log.symbolicate('stack', res));
99
100  logMetroErrorWithStack(projectRoot, {
101    stack: log.symbolicated?.stack?.stack ?? [],
102    codeFrame: log.codeFrame,
103    error,
104  });
105}
106
107/** @returns the html required to render the static metro error as an SPA. */
108export function logFromError({ error, projectRoot }: { error: Error; projectRoot: string }): {
109  symbolicated: any;
110  symbolicate: (type: string, callback: () => void) => void;
111  codeFrame: CodeFrame;
112} {
113  const { LogBoxLog, parseErrorStack } = require(
114    resolveFrom(projectRoot, '@expo/metro-runtime/symbolicate')
115  );
116
117  const stack = parseErrorStack(error.stack);
118
119  return new LogBoxLog({
120    level: 'static',
121    message: {
122      content: error.message,
123      substitutions: [],
124    },
125    isComponentError: false,
126    stack,
127    category: 'static',
128    componentStack: [],
129  });
130}
131
132/** @returns the html required to render the static metro error as an SPA. */
133export async function logMetroErrorAsync({
134  error,
135  projectRoot,
136}: {
137  error: Error;
138  projectRoot: string;
139}) {
140  const log = logFromError({ projectRoot, error });
141
142  await new Promise<void>((res) => log.symbolicate('stack', res));
143
144  logMetroErrorWithStack(projectRoot, {
145    stack: log.symbolicated?.stack?.stack ?? [],
146    codeFrame: log.codeFrame,
147    error,
148  });
149}
150
151/** @returns the html required to render the static metro error as an SPA. */
152export async function getErrorOverlayHtmlAsync({
153  error,
154  projectRoot,
155}: {
156  error: Error;
157  projectRoot: string;
158}) {
159  const log = logFromError({ projectRoot, error });
160
161  await new Promise<void>((res) => log.symbolicate('stack', res));
162
163  logMetroErrorWithStack(projectRoot, {
164    stack: log.symbolicated?.stack?.stack ?? [],
165    codeFrame: log.codeFrame,
166    error,
167  });
168
169  const logBoxContext = {
170    selectedLogIndex: 0,
171    isDisabled: false,
172    logs: [log],
173  };
174  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(
175    logBoxContext
176  )}</script></body></html>`;
177
178  const errorOverlayEntry = await createMetroEndpointAsync(
179    projectRoot,
180    // Keep the URL relative
181    '',
182    resolveFrom(projectRoot, 'expo-router/_error'),
183    {
184      dev: true,
185      platform: 'web',
186      minify: false,
187      environment: 'node',
188    }
189  );
190
191  const htmlWithJs = html.replace('</body>', `<script src=${errorOverlayEntry}></script></body>`);
192  return htmlWithJs;
193}
194