The product I am working on now is a combination of two product lines. There are a lot of codes in n generation development. It is very inconvenient to find many project files. So I decided to make a one-click tool to remove discarded code…

The principle is to rely on webpack’s stats.json file

stats.json

Output the dependencies to stats.json by command

webpack --env staging --config webpack.config.js --json > stats.json
Copy the code

Look at the output to stats.json, which is divided into three modules: Assets, chunks, and modules

asset

Asset is a static resource that contains only the name of the output file, while paths like the image are… /, some pictures are even… /images, this is due to a configuration problem with Webpack, which typed static resources into our configuration file /assets/images folder

modules

chunks

As you can see, the circled locations of chunks and modules are related. And the name in modules has a code path

Code implementation

In line with the principle that it is better not to delete, we have the following version of the code

Bin directory entry file, get the parameters passed in by the user

#! /usr/bin/env node
const program = require('commander');
const UnusedFinder=require('.. /lib/UnusedFinder');

program.option('-f, --file'.'Generate unused file name')
program.option('-r, --remove'.'Delete useless files');

program.parse(process.argv);

const args = process.argv.slice(2);

let fileName= ' ';
let remove = false

let arg;
while(args.length){
	arg=args.shift();
	switch(arg){
		case '-f':
		case '--file':
			fileName=args.shift()|| ' ';
			break;
		case '-r':
		case '--remove':
			remove=true;
			break; }}const finder = new UnusedFinder();
finder.start({fileName, remove});
Copy the code

UnusedFinder implementation

// When deleting files, you can customize the directory to be ignored, after all, there are some public hooks that you don't want to delete now
function ask() {
    return inquirer.prompt([{
      type: "input".name: "ignore".message: "Please enter the paths of files ignored to be deleted, separated by commas."
    }])
    .then(asw= > asw);
  }
class UnusedFinder {
    constructor(config = {}) {
        this.statPath = path.join(process.cwd(), './stats.json');
        this.usedFile = new Set(a);this.assetsFile = new Set(a)// Store static resources
        this.allFiles = [];
        this.unUsedFile=[];
        // Delete the SRC folder by default
        this.pattern = config.pattern || './src/**';
    }

    hasStats = () = > {
        if(! existsSync(this.statPath)) {
            throw new Error("Please check execution at project root and generate stats.json file");
        }
    }

    removeFiles = () = > {
        ask().then(answer= > {
            let ignoreFile = []
            const {ignore} = answer
            if(ignore){
                ignoreFile = ignore.split(', ').filter(Boolean)}const task = []
            spinner.start('File deletion at...... ');
            this.unUsedFile.forEach(item= > {
                // Do not delete the files in the root directory
                if(item.split('/').length <= 2) return
                // Style import style is not reflected in stats.json, so it will not be deleted
                if(/^\.\/styles/.test(item)) return;
                // md files are not deleted
                if(item.substr(item.length - 3.3) = = ='.md') return
                // Customize the folder to ignore deletion
                if(ignoreFile.some(fileName= > item.includes(` /${fileName}`))) return
                const url = item.replace('/'.'./src/')
                if(! existsSync(url))return spinner.warn(`${url}File does not exist)
                const promise = rm(url, val= >val)
                task.push(promise)
                })  
                Promise.all(task)
                    .then(() = > {
                        spinner.succeed('File deleted successfully')
                    }).catch((err) = >{
                        spinner.fail('File deletion failed')
                        err.forEach(item= > {
                            error(item)
                        })
                    })
            })
    }

    findUsedModule = () = > {
        // Format the stats.json file
        const statsData = JSON.parse(readFileSync(this.statPath));
        const chunks = statsData.chunks;
        const modules=statsData.modules;
        const assets=statsData.assets;
        chunks.forEach(chunk= > {
            chunk.modules.forEach(value= > {
                // Name will have modules + 1
                / /.. /node_modules/@antv/g-canvas/esm/canvas.js + 1 modules
                const name = value.name.replace(/ \+ [0-9]* modules/g.' ').replace(/ \? . + =. /.' ');
                // node_modules is not necessary
                if (name.indexOf("node_modules") = = = -1) {
                    this.usedFile.add(name);
                }
                value.modules && value.modules.forEach(subModule= > {
                    if (subModule) {
                        const name = subModule.name.replace(/ \+ [0-9]* modules/g.' ').replace(/ \? . + =. /.' ');
                        if (name.indexOf("node_modules") = = = -1) {
                            this.usedFile.add(name); }}})})// The generated module
        modules.forEach(value= >{
            const name = value.name.replace(/ \+ [0-9]* modules/g.' ').replace(/ \? . + =. + /.' ');
            if (name.indexOf("node_modules") = = = -1) {
                this.usedFile.add(name); }});// Static resources generated
        assets.forEach(value= >{
            const name = value.name.split('/')
            // Since the static resources are presented in the path of the packaged dist file, we only match the file name
            if (name.indexOf("node_modules") = = = -1) {
                this.assetsFile.add(name[name.length - 1]); }}); } findAllFiles =() = > {
        const files=glob.sync(this.pattern, {
            nodir: true
        });
        this.allFiles = files.map(item= > {
            return item.replace('./src'.'. ');
        })
    }

    findUnusedFile=() = >{
        this.unUsedFile=this.allFiles.filter(item= >!this.usedFile.has(item));
        this.unUsedFile = this.unUsedFile.filter(item= > {
            const name = item.split('/')
            // If the name of the static resource is included, the file is removed
            if(this.assetsFile.has(name[name.length - 1])){
                return false
            }
            return true
        })
    }

    start = ({fileName, remove}) = > {
        this.hasStats();
        this.findUsedModule();
        this.findAllFiles();
        this.findUnusedFile();
        if(fileName){
            writeFileSync(fileName,JSON.stringify(this.unUsedFile))
        }else{
            warn('Unused file :\nThe ${this.unUsedFile.join('\n')}`)}if(remove){
            this.removeFiles()
        }
    }

}

module.exports=UnusedFinder;
Copy the code

With the current tool, nearly 500 useless business files are deleted with one click. First phase of the code, of course, there are still some problems, such as static resources, style files and so on all have not found a good method to do match, only through the file name to do the fuzzy matching, thus there may be some useless files with the same filtered and not be deleted, if there is a better way, we hope you will be able to communicate in the comments section.