detectDeadCode.js 4.33 KB
Newer Older
邓晓峰's avatar
邓晓峰 committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
/* 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
}