webpack/lib/CleanPlugin.js

237 lines
5.9 KiB
JavaScript
Raw Normal View History

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");
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 */
2021-02-02 15:36:58 +08:00
/** @typedef {import("./logging/Logger").Logger} Logger */
/** @typedef {import("./util/fs").OutputFileSystem} OutputFileSystem */
2020-09-30 23:33:30 +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 {
/**
* @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-10-07 20:30:14 +08:00
constructor(options = {}) {
this.options = { dry: false, ...options };
2020-09-30 23:33:30 +08:00
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-07 18:48:51 +08:00
/** @type {IgnoreItem[]} */
this.ignoreList = [];
2021-02-02 15:36:58 +08:00
/** @type {Logger} */
2020-10-07 18:48:51 +08:00
this.logger = null;
2021-02-02 15:36:58 +08:00
/** @type {OutputFileSystem} */
2020-10-07 18:48:51 +08:00
this.fs = null;
/** @type {{files: Set<string>, directories: Set<string>}} */
this.fsState = {
files: new Set(),
directories: new Set()
};
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.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) => {
2021-02-02 15:36:58 +08:00
this.logger = compilation.getLogger("webpack.CleanPlugin");
2020-10-07 18:48:51 +08:00
this.resetFSState();
2020-09-30 23:33:30 +08:00
CleanPlugin.getCompilationHooks(compilation).ignore.call(ignoreFn);
2020-09-30 23:33:30 +08:00
for (const asset in compilation.assets) {
2020-10-07 18:48:51 +08:00
if (asset.startsWith("..")) {
continue;
}
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-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-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(
2021-02-02 15:36:58 +08:00
// pretty strange ts error: Argument of type '(string | Buffer)[] | IDirent[]' is not assignable to parameter of type 'IterableCollection<string | Buffer>'
// seems like that type checker takes into account only "(string | Buffer)[]" from "(string | Buffer)[] | IDirent[]"
// moreover "(string | Buffer | IDirent)[]" !== "(string | Buffer)[] | IDirent[]"
// eslint-disable-next-line no-warning-comments
// @ts-ignore
2020-10-07 18:48:51 +08:00
items,
(item, callback) => {
2021-02-02 15:36:58 +08:00
// eslint-disable-next-line no-warning-comments
// @ts-ignore
item = item.name || item;
const child = join(this.fs, p, item.toString());
2020-10-07 18:48:51 +08:00
if (this.ignoreList.some(ignore => checkToIgnore(ignore, child))) {
if (this.options.dry) {
2020-10-07 20:30:14 +08:00
this.logger.info(`[${child}] will be ignored in non-dry mode`);
}
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) {
2020-10-07 20:30:14 +08:00
this.logger.info(
`[${child}] will be ignored in non-dry mode`
);
2020-10-07 18:48:51 +08:00
}
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;