2020-09-30 23:33:30 +08:00
|
|
|
/*
|
|
|
|
MIT License http://www.opensource.org/licenses/mit-license.php
|
|
|
|
Author Sergey Melyukov @smelukov
|
|
|
|
*/
|
|
|
|
|
|
|
|
"use strict";
|
|
|
|
|
2020-10-07 18:48:51 +08:00
|
|
|
const asyncLib = require("neo-async");
|
2020-09-30 23:33:30 +08:00
|
|
|
const path = require("path");
|
2020-10-07 19:26:23 +08:00
|
|
|
const { validate } = require("schema-utils");
|
2020-10-02 23:08:03 +08:00
|
|
|
const { SyncHook } = require("tapable");
|
|
|
|
const Compilation = require("../lib/Compilation");
|
2020-09-30 23:33:30 +08:00
|
|
|
const schema = require("../schemas/plugins/CleanPlugin.json");
|
2020-10-07 18:48:51 +08:00
|
|
|
const { join } = require("./util/fs");
|
2020-09-30 23:33:30 +08:00
|
|
|
|
|
|
|
/** @typedef {import("../declarations/plugins/CleanPlugin").CleanPluginArgument} CleanPluginArgument */
|
|
|
|
/** @typedef {import("./Compiler")} Compiler */
|
|
|
|
|
2020-10-02 23:08:03 +08:00
|
|
|
/** @typedef {(function(string):boolean)|RegExp} IgnoreItem */
|
|
|
|
/** @typedef {function(IgnoreItem): void} AddToIgnoreCallback */
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @typedef {Object} CleanPluginCompilationHooks
|
|
|
|
* @property {SyncHook<[AddToIgnoreCallback]>} ignore
|
|
|
|
*/
|
|
|
|
|
|
|
|
/** @type {WeakMap<Compilation, CleanPluginCompilationHooks>} */
|
|
|
|
const compilationHooksMap = new WeakMap();
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {IgnoreItem} ignore regexp or function
|
|
|
|
* @param {string} asset asset path
|
|
|
|
* @returns {boolean} true if an asset should be ignored
|
|
|
|
*/
|
|
|
|
const checkToIgnore = (ignore, asset) => {
|
|
|
|
if (ignore instanceof RegExp) {
|
|
|
|
return !!ignore.exec(asset);
|
|
|
|
}
|
|
|
|
|
|
|
|
return ignore(asset);
|
|
|
|
};
|
|
|
|
|
2020-09-30 23:33:30 +08:00
|
|
|
class CleanPlugin {
|
2020-10-02 23:08:03 +08:00
|
|
|
/**
|
|
|
|
* @param {Compilation} compilation the compilation
|
|
|
|
* @returns {CleanPluginCompilationHooks} the attached hooks
|
|
|
|
*/
|
|
|
|
static getCompilationHooks(compilation) {
|
|
|
|
if (!(compilation instanceof Compilation)) {
|
|
|
|
throw new TypeError(
|
|
|
|
"The 'compilation' argument must be an instance of Compilation"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
let hooks = compilationHooksMap.get(compilation);
|
|
|
|
if (hooks === undefined) {
|
|
|
|
hooks = {
|
|
|
|
/** @type {SyncHook<[AddToIgnoreCallback]>} */
|
|
|
|
ignore: new SyncHook(["ignore"])
|
|
|
|
};
|
|
|
|
compilationHooksMap.set(compilation, hooks);
|
|
|
|
}
|
|
|
|
return hooks;
|
|
|
|
}
|
|
|
|
|
2020-10-07 18:48:51 +08:00
|
|
|
/** @param {CleanPluginArgument} [options] options */
|
2020-09-30 23:33:30 +08:00
|
|
|
constructor(options) {
|
|
|
|
this.options = Object.assign({ enabled: true, dry: false }, options || {});
|
|
|
|
|
|
|
|
if (options && typeof options === "object") {
|
2020-10-07 19:26:23 +08:00
|
|
|
validate(schema, options, {
|
2020-09-30 23:33:30 +08:00
|
|
|
name: "Clean Plugin",
|
|
|
|
baseDataPath: "options"
|
|
|
|
});
|
|
|
|
}
|
2020-10-02 23:08:03 +08:00
|
|
|
|
2020-10-07 18:48:51 +08:00
|
|
|
/** @type {IgnoreItem[]} */
|
2020-10-02 23:08:03 +08:00
|
|
|
this.ignoreList = [];
|
|
|
|
|
2020-10-07 18:48:51 +08:00
|
|
|
/** @type {import("./logging/Logger").Logger} */
|
|
|
|
this.logger = null;
|
|
|
|
|
|
|
|
/** @type {import("./util/fs").OutputFileSystem} */
|
|
|
|
this.fs = null;
|
|
|
|
|
|
|
|
/** @type {{files: Set<string>, directories: Set<string>}} */
|
|
|
|
this.fsState = {
|
|
|
|
files: new Set(),
|
|
|
|
directories: new Set()
|
|
|
|
};
|
|
|
|
|
2020-10-02 23:08:03 +08:00
|
|
|
if (this.options.ignore) {
|
|
|
|
this.ignoreList.push(this.options.ignore);
|
|
|
|
}
|
2020-09-30 23:33:30 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {Compiler} compiler the compiler instance
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
apply(compiler) {
|
2020-10-07 18:48:51 +08:00
|
|
|
/**
|
|
|
|
* @param {IgnoreItem} item regexp or function
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
const ignoreFn = item => void this.ignoreList.push(item);
|
|
|
|
const handleAsset = asset => {
|
|
|
|
const parts = asset.split(/[\\/]+/).slice(0, -1);
|
|
|
|
|
|
|
|
this.fsState.files.add(asset);
|
|
|
|
parts.reduce((all, part) => {
|
|
|
|
const directory = path.join(all, part);
|
|
|
|
this.fsState.directories.add(directory);
|
|
|
|
return directory;
|
|
|
|
}, "");
|
2020-09-30 23:33:30 +08:00
|
|
|
};
|
2020-10-07 18:48:51 +08:00
|
|
|
this.logger = compiler.getInfrastructureLogger("webpack.Clean");
|
|
|
|
this.fs = compiler.outputFileSystem;
|
2020-09-30 23:33:30 +08:00
|
|
|
|
2020-10-07 18:48:51 +08:00
|
|
|
compiler.hooks.emit.tapAsync(
|
2020-09-30 23:33:30 +08:00
|
|
|
{
|
|
|
|
name: "CleanPlugin",
|
|
|
|
stage: 100
|
|
|
|
},
|
2020-10-07 18:48:51 +08:00
|
|
|
(compilation, callback) => {
|
|
|
|
this.resetFSState();
|
2020-09-30 23:33:30 +08:00
|
|
|
|
2020-10-02 23:08:03 +08:00
|
|
|
CleanPlugin.getCompilationHooks(compilation).ignore.call(ignoreFn);
|
2020-09-30 23:33:30 +08:00
|
|
|
|
2020-10-02 23:08:03 +08:00
|
|
|
for (const asset in compilation.assets) {
|
2020-10-07 18:48:51 +08:00
|
|
|
if (asset.startsWith("..")) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2020-10-02 23:08:03 +08:00
|
|
|
handleAsset(asset);
|
2020-09-30 23:33:30 +08:00
|
|
|
}
|
|
|
|
|
2020-10-07 18:48:51 +08:00
|
|
|
const outputPath = compilation.getPath(compiler.outputPath, {});
|
|
|
|
this.cleanRecursive(outputPath, callback);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
2020-09-30 23:33:30 +08:00
|
|
|
|
2020-10-07 18:48:51 +08:00
|
|
|
resetFSState() {
|
|
|
|
this.fsState.files.clear();
|
|
|
|
this.fsState.directories.clear();
|
|
|
|
}
|
2020-10-02 23:08:03 +08:00
|
|
|
|
2020-10-07 18:48:51 +08:00
|
|
|
/**
|
|
|
|
* @param {string} p an absolute path
|
|
|
|
* @param {import("./util/fs").Callback} callback callback
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
cleanRecursive(p, callback) {
|
|
|
|
const handleError = (err, callback) => {
|
|
|
|
if (!err) {
|
|
|
|
return callback();
|
|
|
|
}
|
2020-10-02 23:08:03 +08:00
|
|
|
|
2020-10-07 18:48:51 +08:00
|
|
|
if (err.code === "ENOENT" || err.code === "ENOTEMPTY") {
|
|
|
|
return callback();
|
|
|
|
}
|
|
|
|
return callback(err);
|
|
|
|
};
|
|
|
|
|
|
|
|
this.fs.readdir(p, (err, items) => {
|
|
|
|
if (err) {
|
|
|
|
return handleError(err, callback);
|
|
|
|
}
|
|
|
|
|
|
|
|
asyncLib.forEach(
|
|
|
|
items,
|
|
|
|
(item, callback) => {
|
|
|
|
const child = join(this.fs, p, item);
|
|
|
|
|
|
|
|
if (this.ignoreList.some(ignore => checkToIgnore(ignore, child))) {
|
2020-10-02 23:08:03 +08:00
|
|
|
if (this.options.dry) {
|
2020-10-07 18:48:51 +08:00
|
|
|
this.logger.info(`[${child}] was ignored`);
|
2020-10-02 23:08:03 +08:00
|
|
|
}
|
2020-10-07 18:48:51 +08:00
|
|
|
|
|
|
|
return callback();
|
2020-09-30 23:33:30 +08:00
|
|
|
}
|
|
|
|
|
2020-10-07 18:48:51 +08:00
|
|
|
this.fs.stat(child, (err, stat) => {
|
|
|
|
if (err) {
|
|
|
|
return handleError(err, callback);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (stat.isFile()) {
|
|
|
|
if (this.fsState.files.has(child)) {
|
|
|
|
if (this.options.dry) {
|
|
|
|
this.logger.info(`[${child}] was ignored`);
|
|
|
|
}
|
|
|
|
|
|
|
|
return callback();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.options.dry) {
|
|
|
|
this.logger.info(`[${child}] will be removed in non-dry mode`);
|
|
|
|
return callback();
|
|
|
|
} else {
|
|
|
|
return this.fs.unlink(child, callback);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (stat.isDirectory()) {
|
|
|
|
return this.cleanRecursive(child, callback);
|
|
|
|
}
|
|
|
|
|
|
|
|
callback();
|
|
|
|
});
|
|
|
|
},
|
|
|
|
err => {
|
|
|
|
if (err) {
|
|
|
|
return handleError(err, callback);
|
2020-09-30 23:33:30 +08:00
|
|
|
}
|
|
|
|
|
2020-10-07 18:48:51 +08:00
|
|
|
this.fs.rmdir(p, err => handleError(err, callback));
|
|
|
|
}
|
|
|
|
);
|
|
|
|
});
|
2020-09-30 23:33:30 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = CleanPlugin;
|