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