124228e75SEvan Bacon/**
224228e75SEvan Bacon * Copyright © 2022 650 Industries.
324228e75SEvan Bacon *
424228e75SEvan Bacon * This source code is licensed under the MIT license found in the
524228e75SEvan Bacon * LICENSE file in the root directory of this source tree.
624228e75SEvan Bacon */
724228e75SEvan Baconimport chalk from 'chalk';
824228e75SEvan Baconimport resolveFrom from 'resolve-from';
924228e75SEvan Baconimport { StackFrame } from 'stacktrace-parser';
1024228e75SEvan Baconimport terminalLink from 'terminal-link';
1124228e75SEvan Bacon
1224228e75SEvan Baconimport { Log } from '../../../log';
1324228e75SEvan Baconimport { createMetroEndpointAsync } from '../getStaticRenderFunctions';
1424228e75SEvan Bacon// import type { CodeFrame, MetroStackFrame } from '@expo/metro-runtime/symbolicate';
1524228e75SEvan Bacon
1624228e75SEvan Bacontype CodeFrame = {
1724228e75SEvan Bacon  content: string;
1824228e75SEvan Bacon  location?: {
1924228e75SEvan Bacon    row: number;
2024228e75SEvan Bacon    column: number;
2124228e75SEvan Bacon    [key: string]: any;
2224228e75SEvan Bacon  };
2324228e75SEvan Bacon  fileName: string;
2424228e75SEvan Bacon};
2524228e75SEvan Bacon
2624228e75SEvan Bacontype MetroStackFrame = StackFrame & { collapse?: boolean };
2724228e75SEvan Bacon
2846f023faSEvan Baconfunction fill(width: number): string {
2946f023faSEvan Bacon  return Array(width).join(' ');
3046f023faSEvan Bacon}
3146f023faSEvan Bacon
3246f023faSEvan Baconfunction formatPaths(config: { filePath: string | null; line?: number; col?: number }) {
3346f023faSEvan Bacon  const filePath = chalk.reset(config.filePath);
3446f023faSEvan Bacon  return (
3546f023faSEvan Bacon    chalk.dim('(') +
3646f023faSEvan Bacon    filePath +
3746f023faSEvan Bacon    chalk.dim(`:${[config.line, config.col].filter(Boolean).join(':')})`)
3846f023faSEvan Bacon  );
3946f023faSEvan Bacon}
4046f023faSEvan Bacon
4124228e75SEvan Baconexport async function logMetroErrorWithStack(
4224228e75SEvan Bacon  projectRoot: string,
4324228e75SEvan Bacon  {
4424228e75SEvan Bacon    stack,
4524228e75SEvan Bacon    codeFrame,
4624228e75SEvan Bacon    error,
4724228e75SEvan Bacon  }: {
4824228e75SEvan Bacon    stack: MetroStackFrame[];
4924228e75SEvan Bacon    codeFrame: CodeFrame;
5024228e75SEvan Bacon    error: Error;
5124228e75SEvan Bacon  }
5224228e75SEvan Bacon) {
5346f023faSEvan Bacon  // process.stdout.write('\u001b[0m'); // Reset attributes
5446f023faSEvan Bacon  // process.stdout.write('\u001bc'); // Reset the terminal
5546f023faSEvan Bacon
5605863844SEvan Bacon  const { getStackFormattedLocation } = require(
5705863844SEvan Bacon    resolveFrom(projectRoot, '@expo/metro-runtime/symbolicate')
5805863844SEvan Bacon  );
5924228e75SEvan Bacon
6024228e75SEvan Bacon  Log.log();
6124228e75SEvan Bacon  Log.log(chalk.red('Metro error: ') + error.message);
6224228e75SEvan Bacon  Log.log();
6324228e75SEvan Bacon
6424228e75SEvan Bacon  if (codeFrame) {
6546f023faSEvan Bacon    const maxWarningLineLength = Math.max(200, process.stdout.columns);
6646f023faSEvan Bacon
6746f023faSEvan Bacon    const lineText = codeFrame.content;
68*cd4367edSEvan Bacon    const isPreviewTooLong = codeFrame.content
69*cd4367edSEvan Bacon      .split('\n')
70*cd4367edSEvan Bacon      .some((line) => line.length > maxWarningLineLength);
7146f023faSEvan Bacon    const column = codeFrame.location?.column;
7246f023faSEvan Bacon    // When the preview is too long, we skip reading the file and attempting to apply
7346f023faSEvan Bacon    // code coloring, this is because it can get very slow.
7446f023faSEvan Bacon    if (isPreviewTooLong) {
7546f023faSEvan Bacon      let previewLine = '';
7646f023faSEvan Bacon      let cursorLine = '';
7746f023faSEvan Bacon
7846f023faSEvan Bacon      const formattedPath = formatPaths({
7946f023faSEvan Bacon        filePath: codeFrame.fileName,
8046f023faSEvan Bacon        line: codeFrame.location?.row,
8146f023faSEvan Bacon        col: codeFrame.location?.column,
8246f023faSEvan Bacon      });
8346f023faSEvan Bacon      // Create a curtailed preview line like:
8446f023faSEvan Bacon      // `...transition:'fade'},k._updatePropsStack=function(){clearImmediate(k._updateImmediate),k._updateImmediate...`
8546f023faSEvan Bacon      // If there is no text preview or column number, we can't do anything.
8646f023faSEvan Bacon      if (lineText && column != null) {
8746f023faSEvan Bacon        const rangeWindow = Math.round(
8846f023faSEvan Bacon          Math.max(codeFrame.fileName?.length ?? 0, Math.max(80, process.stdout.columns)) / 2
8946f023faSEvan Bacon        );
9046f023faSEvan Bacon        let minBounds = Math.max(0, column - rangeWindow);
9146f023faSEvan Bacon        const maxBounds = Math.min(minBounds + rangeWindow * 2, lineText.length);
9246f023faSEvan Bacon        previewLine = lineText.slice(minBounds, maxBounds);
9346f023faSEvan Bacon
9446f023faSEvan Bacon        // If we splice content off the start, then we should append `...`.
9546f023faSEvan Bacon        // This is unlikely to happen since we limit the activation size.
9646f023faSEvan Bacon        if (minBounds > 0) {
9746f023faSEvan Bacon          // Adjust the min bounds so the cursor is aligned after we add the "..."
9846f023faSEvan Bacon          minBounds -= 3;
9946f023faSEvan Bacon          previewLine = chalk.dim('...') + previewLine;
10046f023faSEvan Bacon        }
10146f023faSEvan Bacon        if (maxBounds < lineText.length) {
10246f023faSEvan Bacon          previewLine += chalk.dim('...');
10346f023faSEvan Bacon        }
10446f023faSEvan Bacon
10546f023faSEvan Bacon        // If the column property could be found, then use that to fix the cursor location which is often broken in regex.
10646f023faSEvan Bacon        cursorLine = (column == null ? '' : fill(column) + chalk.reset('^')).slice(minBounds);
10746f023faSEvan Bacon
10846f023faSEvan Bacon        Log.log(
10946f023faSEvan Bacon          [formattedPath, '', previewLine, cursorLine, chalk.dim('(error truncated)')].join('\n')
11046f023faSEvan Bacon        );
11146f023faSEvan Bacon      }
11246f023faSEvan Bacon    } else {
11324228e75SEvan Bacon      Log.log(codeFrame.content);
11424228e75SEvan Bacon    }
11546f023faSEvan Bacon  }
11624228e75SEvan Bacon
11724228e75SEvan Bacon  if (stack?.length) {
11824228e75SEvan Bacon    Log.log();
11924228e75SEvan Bacon    Log.log(chalk.bold`Call Stack`);
12024228e75SEvan Bacon
12124228e75SEvan Bacon    const stackProps = stack.map((frame) => {
12224228e75SEvan Bacon      return {
12324228e75SEvan Bacon        title: frame.methodName,
12424228e75SEvan Bacon        subtitle: getStackFormattedLocation(projectRoot, frame),
12524228e75SEvan Bacon        collapse: frame.collapse,
12624228e75SEvan Bacon      };
12724228e75SEvan Bacon    });
12824228e75SEvan Bacon
12924228e75SEvan Bacon    stackProps.forEach((frame) => {
13024228e75SEvan Bacon      const position = terminalLink.isSupported
13124228e75SEvan Bacon        ? terminalLink(frame.subtitle, frame.subtitle)
13224228e75SEvan Bacon        : frame.subtitle;
13324228e75SEvan Bacon      let lineItem = chalk.gray(`  ${frame.title} (${position})`);
13424228e75SEvan Bacon      if (frame.collapse) {
13524228e75SEvan Bacon        lineItem = chalk.dim(lineItem);
13624228e75SEvan Bacon      }
13724228e75SEvan Bacon      Log.log(lineItem);
13824228e75SEvan Bacon    });
13924228e75SEvan Bacon  } else {
14024228e75SEvan Bacon    Log.log(chalk.gray(`  ${error.stack}`));
14124228e75SEvan Bacon  }
14224228e75SEvan Bacon}
14324228e75SEvan Bacon
1449580591fSEvan Baconexport async function logMetroError(projectRoot: string, { error }: { error: Error }) {
14505863844SEvan Bacon  const { LogBoxLog, parseErrorStack } = require(
14605863844SEvan Bacon    resolveFrom(projectRoot, '@expo/metro-runtime/symbolicate')
14705863844SEvan Bacon  );
1489580591fSEvan Bacon
1499580591fSEvan Bacon  const stack = parseErrorStack(error.stack);
1509580591fSEvan Bacon
1519580591fSEvan Bacon  const log = new LogBoxLog({
1529580591fSEvan Bacon    level: 'static',
1539580591fSEvan Bacon    message: {
1549580591fSEvan Bacon      content: error.message,
1559580591fSEvan Bacon      substitutions: [],
1569580591fSEvan Bacon    },
1579580591fSEvan Bacon    isComponentError: false,
1589580591fSEvan Bacon    stack,
1599580591fSEvan Bacon    category: 'static',
1609580591fSEvan Bacon    componentStack: [],
1619580591fSEvan Bacon  });
1629580591fSEvan Bacon
1639580591fSEvan Bacon  await new Promise((res) => log.symbolicate('stack', res));
1649580591fSEvan Bacon
1659580591fSEvan Bacon  logMetroErrorWithStack(projectRoot, {
1669580591fSEvan Bacon    stack: log.symbolicated?.stack?.stack ?? [],
1679580591fSEvan Bacon    codeFrame: log.codeFrame,
1689580591fSEvan Bacon    error,
1699580591fSEvan Bacon  });
1709580591fSEvan Bacon}
1719580591fSEvan Bacon
17224228e75SEvan Bacon/** @returns the html required to render the static metro error as an SPA. */
17385531d53SEvan Baconexport function logFromError({ error, projectRoot }: { error: Error; projectRoot: string }): {
17485531d53SEvan Bacon  symbolicated: any;
17585531d53SEvan Bacon  symbolicate: (type: string, callback: () => void) => void;
17685531d53SEvan Bacon  codeFrame: CodeFrame;
17785531d53SEvan Bacon} {
17805863844SEvan Bacon  const { LogBoxLog, parseErrorStack } = require(
17905863844SEvan Bacon    resolveFrom(projectRoot, '@expo/metro-runtime/symbolicate')
18005863844SEvan Bacon  );
1819580591fSEvan Bacon
18224228e75SEvan Bacon  const stack = parseErrorStack(error.stack);
18324228e75SEvan Bacon
18485531d53SEvan Bacon  return new LogBoxLog({
18524228e75SEvan Bacon    level: 'static',
18624228e75SEvan Bacon    message: {
18724228e75SEvan Bacon      content: error.message,
18824228e75SEvan Bacon      substitutions: [],
18924228e75SEvan Bacon    },
19024228e75SEvan Bacon    isComponentError: false,
19124228e75SEvan Bacon    stack,
19224228e75SEvan Bacon    category: 'static',
19324228e75SEvan Bacon    componentStack: [],
19424228e75SEvan Bacon  });
19585531d53SEvan Bacon}
19624228e75SEvan Bacon
19785531d53SEvan Bacon/** @returns the html required to render the static metro error as an SPA. */
19885531d53SEvan Baconexport async function logMetroErrorAsync({
19985531d53SEvan Bacon  error,
20085531d53SEvan Bacon  projectRoot,
20185531d53SEvan Bacon}: {
20285531d53SEvan Bacon  error: Error;
20385531d53SEvan Bacon  projectRoot: string;
20485531d53SEvan Bacon}) {
20585531d53SEvan Bacon  const log = logFromError({ projectRoot, error });
20685531d53SEvan Bacon
20785531d53SEvan Bacon  await new Promise<void>((res) => log.symbolicate('stack', res));
20885531d53SEvan Bacon
20985531d53SEvan Bacon  logMetroErrorWithStack(projectRoot, {
21085531d53SEvan Bacon    stack: log.symbolicated?.stack?.stack ?? [],
21185531d53SEvan Bacon    codeFrame: log.codeFrame,
21285531d53SEvan Bacon    error,
21385531d53SEvan Bacon  });
21485531d53SEvan Bacon}
21585531d53SEvan Bacon
21685531d53SEvan Bacon/** @returns the html required to render the static metro error as an SPA. */
21785531d53SEvan Baconexport async function getErrorOverlayHtmlAsync({
21885531d53SEvan Bacon  error,
21985531d53SEvan Bacon  projectRoot,
22085531d53SEvan Bacon}: {
22185531d53SEvan Bacon  error: Error;
22285531d53SEvan Bacon  projectRoot: string;
22385531d53SEvan Bacon}) {
22485531d53SEvan Bacon  const log = logFromError({ projectRoot, error });
22585531d53SEvan Bacon
22685531d53SEvan Bacon  await new Promise<void>((res) => log.symbolicate('stack', res));
22724228e75SEvan Bacon
22824228e75SEvan Bacon  logMetroErrorWithStack(projectRoot, {
22924228e75SEvan Bacon    stack: log.symbolicated?.stack?.stack ?? [],
23024228e75SEvan Bacon    codeFrame: log.codeFrame,
23124228e75SEvan Bacon    error,
23224228e75SEvan Bacon  });
23324228e75SEvan Bacon
23424228e75SEvan Bacon  const logBoxContext = {
23524228e75SEvan Bacon    selectedLogIndex: 0,
23624228e75SEvan Bacon    isDisabled: false,
23724228e75SEvan Bacon    logs: [log],
23824228e75SEvan Bacon  };
23924228e75SEvan Bacon  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(
24024228e75SEvan Bacon    logBoxContext
24124228e75SEvan Bacon  )}</script></body></html>`;
24224228e75SEvan Bacon
24324228e75SEvan Bacon  const errorOverlayEntry = await createMetroEndpointAsync(
24424228e75SEvan Bacon    projectRoot,
24524228e75SEvan Bacon    // Keep the URL relative
24624228e75SEvan Bacon    '',
24724228e75SEvan Bacon    resolveFrom(projectRoot, 'expo-router/_error'),
24824228e75SEvan Bacon    {
24924228e75SEvan Bacon      dev: true,
25024228e75SEvan Bacon      platform: 'web',
25124228e75SEvan Bacon      minify: false,
25224228e75SEvan Bacon      environment: 'node',
25324228e75SEvan Bacon    }
25424228e75SEvan Bacon  );
25524228e75SEvan Bacon
25624228e75SEvan Bacon  const htmlWithJs = html.replace('</body>', `<script src=${errorOverlayEntry}></script></body>`);
25724228e75SEvan Bacon  return htmlWithJs;
25824228e75SEvan Bacon}
259