ModuleNotFoundPlugin.js 4.21 KB
/**
 * Copyright (c) 2015-present, Facebook, Inc.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

'use strict';

const chalk = require('chalk');
const findUp = require('find-up');
const path = require('path');

class ModuleNotFoundPlugin {
  constructor(appPath, yarnLockFile) {
    this.appPath = appPath;
    this.yarnLockFile = yarnLockFile;

    this.useYarnCommand = this.useYarnCommand.bind(this);
    this.getRelativePath = this.getRelativePath.bind(this);
    this.prettierError = this.prettierError.bind(this);
  }

  useYarnCommand() {
    try {
      return findUp.sync('yarn.lock', { cwd: this.appPath }) != null;
    } catch (_) {
      return false;
    }
  }

  getRelativePath(_file) {
    let file = path.relative(this.appPath, _file);
    if (file.startsWith('..')) {
      file = _file;
    } else if (!file.startsWith('.')) {
      file = '.' + path.sep + file;
    }
    return file;
  }

  prettierError(err) {
    let { details: _details = '', origin } = err;

    if (origin == null) {
      const caseSensitivity =
        err.message &&
        /\[CaseSensitivePathsPlugin\] `(.*?)` .* `(.*?)`/.exec(err.message);
      if (caseSensitivity) {
        const [, incorrectPath, actualName] = caseSensitivity;
        const actualFile = this.getRelativePath(
          path.join(path.dirname(incorrectPath), actualName)
        );
        const incorrectName = path.basename(incorrectPath);
        err.message = `Cannot find file: '${incorrectName}' does not match the corresponding name on disk: '${actualFile}'.`;
      }
      return err;
    }

    const file = this.getRelativePath(origin.resource);
    let details = _details.split('\n');

    const request = /resolve '(.*?)' in '(.*?)'/.exec(details);
    if (request) {
      const isModule = details[1] && details[1].includes('module');
      const isFile = details[1] && details[1].includes('file');

      let [, target, context] = request;
      context = this.getRelativePath(context);
      if (isModule) {
        const isYarn = this.useYarnCommand();
        details = [
          `Cannot find module: '${target}'. Make sure this package is installed.`,
          '',
          'You can install this package by running: ' +
            (isYarn
              ? chalk.bold(`yarn add ${target}`)
              : chalk.bold(`npm install ${target}`)) +
            '.',
        ];
      } else if (isFile) {
        details = [`Cannot find file '${target}' in '${context}'.`];
      } else {
        details = [err.message];
      }
    } else {
      details = [err.message];
    }
    err.message = [file, ...details].join('\n').replace('Error: ', '');

    const isModuleScopePluginError =
      err.error && err.error.__module_scope_plugin;
    if (isModuleScopePluginError) {
      err.message = err.message.replace('Module not found: ', '');
    }
    return err;
  }

  apply(compiler) {
    const { prettierError } = this;
    compiler.hooks.make.intercept({
      register(tap) {
        if (
          !(tap.name === 'MultiEntryPlugin' || tap.name === 'SingleEntryPlugin')
        ) {
          return tap;
        }
        return Object.assign({}, tap, {
          fn: (compilation, callback) => {
            tap.fn(compilation, (err, ...args) => {
              if (err && err.name === 'ModuleNotFoundError') {
                err = prettierError(err);
              }
              callback(err, ...args);
            });
          },
        });
      },
    });
    compiler.hooks.normalModuleFactory.tap('ModuleNotFoundPlugin', nmf => {
      nmf.hooks.afterResolve.intercept({
        register(tap) {
          if (tap.name !== 'CaseSensitivePathsPlugin') {
            return tap;
          }
          return Object.assign({}, tap, {
            fn: (compilation, callback) => {
              tap.fn(compilation, (err, ...args) => {
                if (
                  err &&
                  err.message &&
                  err.message.includes('CaseSensitivePathsPlugin')
                ) {
                  err = prettierError(err);
                }
                callback(err, ...args);
              });
            },
          });
        },
      });
    });
  }
}

module.exports = ModuleNotFoundPlugin;