detectDeadCode.js 4.33 KB
/* eslint-disable */
const { chalk, glob } = require('@umijs/utils');
const path = require('path');
const { Chunk, Compilation, Module, NormalModule } = require('../compiled/webpack');

const disabledFolders = ['node_modules', '.panda', '.panda-production', 'dist'];

const detectDeadCode = (compilation, options) => {
  const assets = getWebpackAssets(compilation);
  const compiledFilesDictionary = convertFilesToDict(assets);
  const includedFiles = getPattern(options)
    .map(pattern => glob.sync(pattern))
    .flat();

  const unusedFiles = options.detectUnusedFiles ? includedFiles.filter(file => !compiledFilesDictionary[file]) : [];
  const unusedExportMap = options.detectUnusedExport
    ? getUnusedExportMap(convertFilesToDict(includedFiles), compilation)
    : {};

  logUnusedFiles(unusedFiles);
  logUnusedExportMap(unusedExportMap);

  const hasUnusedThings = unusedFiles.length || Object.keys(unusedExportMap).length;
  if (hasUnusedThings && options.failOnHint) {
    process.exit(2);
  }
};

const getPattern = options =>
  options.patterns
    .map(pattern => path.resolve(options.context || '', pattern))
    .concat(options.exclude.map(pattern => path.resolve(options.context || '', `!${pattern}`)))
    .map(convertToUnixPath);

const getUnusedExportMap = (includedFileMap, compilation) => {
  const unusedExportMap = {};

  compilation.chunks.forEach(chunk => {
    compilation.chunkGraph.getChunkModules(chunk).forEach(module => {
      outputUnusedExportMap(compilation, chunk, module, includedFileMap, unusedExportMap);
    });
  });

  return unusedExportMap;
};

const outputUnusedExportMap = (compilation, chunk, module, includedFileMap, unusedExportMap) => {
  if (!(module instanceof NormalModule) || !module.resource) {
    return;
  }

  const path = convertToUnixPath(module.resource);
  if (!/^((?!(node_modules)).)*$/.test(path)) return;

  const providedExports = compilation.chunkGraph.moduleGraph.getProvidedExports(module);
  const usedExports = compilation.chunkGraph.moduleGraph.getUsedExports(module, chunk.runtime);

  if (usedExports !== true && providedExports !== true && includedFileMap[path]) {
    if (usedExports === false) {
      if (providedExports?.length) {
        unusedExportMap[path] = providedExports;
      }
    } else if (providedExports instanceof Array) {
      const unusedExports = providedExports.filter(item => usedExports && !usedExports.has(item));

      if (unusedExports.length) {
        unusedExportMap[path] = unusedExports;
      }
    }
  }
};

const logUnusedExportMap = unusedExportMap => {
  if (!Object.keys(unusedExportMap).length) {
    return;
  }

  let numberOfUnusedExport = 0;
  let logStr = '';

  Object.keys(unusedExportMap).forEach((filePath, fileIndex) => {
    const unusedExports = unusedExportMap[filePath];

    logStr += [
      `\n${fileIndex + 1}. `,
      chalk.yellow(`${filePath}\n`),
      '    >>>  ',
      chalk.yellow(`${unusedExports.join(',  ')}`),
    ].join('');

    numberOfUnusedExport += unusedExports.length;
  });

  console.log(
    chalk.yellow.bold('\nWarning:'),
    chalk.yellow(`There are ${numberOfUnusedExport} unused exports in ${Object.keys(unusedExportMap).length} files:`),
    logStr,
    chalk.red.bold('\nPlease be careful if you want to remove them (¬º-°)¬.\n'),
  );
};

const getWebpackAssets = compilation => {
  const outputPath = compilation.getPath(compilation.compiler.outputPath);
  const assets = [
    ...Array.from(compilation.fileDependencies),
    ...compilation.getAssets().map(asset => path.join(outputPath, asset.name)),
  ];

  return assets;
};

const convertFilesToDict = assets =>
  assets
    .filter(file => Boolean(file) && disabledFolders.every(disabledPath => !file.includes(disabledPath)))
    .reduce((fileDictionary, file) => {
      const unixFile = convertToUnixPath(file);

      fileDictionary[unixFile] = true;

      return fileDictionary;
    }, {});

const logUnusedFiles = unusedFiles => {
  if (!unusedFiles?.length) {
    return;
  }

  console.log(
    chalk.yellow.bold('\nWarning:'),
    chalk.yellow(`There are ${unusedFiles.length} unused files:`),
    ...unusedFiles.map((file, index) => `\n${index + 1}. ${chalk.yellow(file)}`),
    chalk.red.bold('\nPlease be careful if you want to remove them (¬º-°)¬.\n'),
  );
};

const convertToUnixPath = path => path.replace(/\\+/g, '/');

module.exports = {
  detectDeadCode,
  disabledFolders
}