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.