1/**
2 * Copyright (c) 2022 Expo, Inc.
3 * Copyright (c) 2015-present, Facebook, Inc.
4 *
5 * This source code is licensed under the MIT license found in the
6 * LICENSE file in the root directory of this source tree.
7 *
8 * Based on https://github.com/facebook/create-react-app/blob/b172b5e/packages/react-dev-utils/ModuleNotFoundPlugin.js
9 * But with Node LTS support.
10 */
11
12import type { Stats } from 'webpack';
13
14const friendlySyntaxErrorLabel = 'Syntax error:';
15
16function isLikelyASyntaxError(message: string): boolean {
17  return message.indexOf(friendlySyntaxErrorLabel) !== -1;
18}
19
20// Cleans up webpack error messages.
21function formatMessage(message: string | { message: string } | { message: string }[]) {
22  let lines: string[] = [];
23
24  if (typeof message === 'string') {
25    lines = message.split('\n');
26  } else if ('message' in message) {
27    lines = message['message'].split('\n');
28  } else if (Array.isArray(message)) {
29    message.forEach((message) => {
30      if ('message' in message) {
31        lines = message['message'].split('\n');
32      }
33    });
34  }
35
36  // Strip webpack-added headers off errors/warnings
37  // https://github.com/webpack/webpack/blob/master/lib/ModuleError.js
38  lines = lines.filter((line) => !/Module [A-z ]+\(from/.test(line));
39
40  // Transform parsing error into syntax error
41  // TODO: move this to our ESLint formatter?
42  lines = lines.map((line) => {
43    const parsingError = /Line (\d+):(?:(\d+):)?\s*Parsing error: (.+)$/.exec(line);
44    if (!parsingError) {
45      return line;
46    }
47    const [, errorLine, errorColumn, errorMessage] = parsingError;
48    return `${friendlySyntaxErrorLabel} ${errorMessage} (${errorLine}:${errorColumn})`;
49  });
50
51  message = lines.join('\n');
52  // Smoosh syntax errors (commonly found in CSS)
53  message = message.replace(
54    /SyntaxError\s+\((\d+):(\d+)\)\s*(.+?)\n/g,
55    `${friendlySyntaxErrorLabel} $3 ($1:$2)\n`
56  );
57  // Clean up export errors
58  message = message.replace(
59    /^.*export '(.+?)' was not found in '(.+?)'.*$/gm,
60    `Attempted import error: '$1' is not exported from '$2'.`
61  );
62  message = message.replace(
63    /^.*export 'default' \(imported as '(.+?)'\) was not found in '(.+?)'.*$/gm,
64    `Attempted import error: '$2' does not contain a default export (imported as '$1').`
65  );
66  message = message.replace(
67    /^.*export '(.+?)' \(imported as '(.+?)'\) was not found in '(.+?)'.*$/gm,
68    `Attempted import error: '$1' is not exported from '$3' (imported as '$2').`
69  );
70  lines = message.split('\n');
71
72  // Remove leading newline
73  if (lines.length > 2 && lines[1].trim() === '') {
74    lines.splice(1, 1);
75  }
76  // Clean up file name
77  lines[0] = lines[0].replace(/^(.*) \d+:\d+-\d+$/, '$1');
78
79  // Cleans up verbose "module not found" messages for files and packages.
80  if (lines[1] && lines[1].indexOf('Module not found: ') === 0) {
81    lines = [
82      lines[0],
83      lines[1]
84        .replace('Error: ', '')
85        .replace('Module not found: Cannot find file:', 'Cannot find file:'),
86    ];
87  }
88
89  // Add helpful message for users trying to use Sass for the first time
90  if (lines[1] && lines[1].match(/Cannot find module.+sass/)) {
91    lines[1] = 'To import Sass files, you first need to install sass.\n';
92    lines[1] += 'Run `npm install sass` or `yarn add sass` inside your workspace.';
93  }
94
95  message = lines.join('\n');
96  // Internal stacks are generally useless so we strip them... with the
97  // exception of stacks containing `webpack:` because they're normally
98  // from user code generated by webpack. For more information see
99  // https://github.com/facebook/create-react-app/pull/1050
100  message = message.replace(/^\s*at\s((?!webpack:).)*:\d+:\d+[\s)]*(\n|$)/gm, ''); // at ... ...:x:y
101  message = message.replace(/^\s*at\s<anonymous>(\n|$)/gm, ''); // at <anonymous>
102  lines = message.split('\n');
103
104  // Remove duplicated newlines
105  lines = lines.filter(
106    (line, index, arr) => index === 0 || line.trim() !== '' || line.trim() !== arr[index - 1].trim()
107  );
108
109  // Reassemble the message
110  message = lines.join('\n');
111  return message.trim();
112}
113
114export function formatWebpackMessages(json?: Stats.ToJsonOutput) {
115  const formattedErrors = json?.errors?.map(formatMessage);
116  const formattedWarnings = json?.warnings?.map(formatMessage);
117  const result = { errors: formattedErrors, warnings: formattedWarnings };
118  if (result.errors?.some(isLikelyASyntaxError)) {
119    // If there are any syntax errors, show just them.
120    result.errors = result.errors.filter(isLikelyASyntaxError);
121  }
122  return result;
123}
124