123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420 |
- /*
- MIT License http://www.opensource.org/licenses/mit-license.php
- Author Sergey Melyukov @smelukov
- */
- "use strict";
- const asyncLib = require("neo-async");
- const { SyncBailHook } = require("tapable");
- const Compilation = require("../lib/Compilation");
- const createSchemaValidation = require("./util/create-schema-validation");
- const { join } = require("./util/fs");
- const processAsyncTree = require("./util/processAsyncTree");
- /** @typedef {import("../declarations/WebpackOptions").CleanOptions} CleanOptions */
- /** @typedef {import("./Compiler")} Compiler */
- /** @typedef {import("./logging/Logger").Logger} Logger */
- /** @typedef {import("./util/fs").OutputFileSystem} OutputFileSystem */
- /** @typedef {import("./util/fs").StatsCallback} StatsCallback */
- /** @typedef {(function(string):boolean)|RegExp} IgnoreItem */
- /** @typedef {Map<string, number>} Assets */
- /** @typedef {function(IgnoreItem): void} AddToIgnoreCallback */
- /**
- * @typedef {Object} CleanPluginCompilationHooks
- * @property {SyncBailHook<[string], boolean>} keep when returning true the file/directory will be kept during cleaning, returning false will clean it and ignore the following plugins and config
- */
- const validate = createSchemaValidation(
- undefined,
- () => {
- const { definitions } = require("../schemas/WebpackOptions.json");
- return {
- definitions,
- oneOf: [{ $ref: "#/definitions/CleanOptions" }]
- };
- },
- {
- name: "Clean Plugin",
- baseDataPath: "options"
- }
- );
- const _10sec = 10 * 1000;
- /**
- * marge assets map 2 into map 1
- * @param {Assets} as1 assets
- * @param {Assets} as2 assets
- * @returns {void}
- */
- const mergeAssets = (as1, as2) => {
- for (const [key, value1] of as2) {
- const value2 = as1.get(key);
- if (!value2 || value1 > value2) as1.set(key, value1);
- }
- };
- /**
- * @param {OutputFileSystem} fs filesystem
- * @param {string} outputPath output path
- * @param {Map<string, number>} currentAssets filename of the current assets (must not start with .. or ., must only use / as path separator)
- * @param {function((Error | null)=, Set<string>=): void} callback returns the filenames of the assets that shouldn't be there
- * @returns {void}
- */
- const getDiffToFs = (fs, outputPath, currentAssets, callback) => {
- const directories = new Set();
- // get directories of assets
- for (const [asset] of currentAssets) {
- directories.add(asset.replace(/(^|\/)[^/]*$/, ""));
- }
- // and all parent directories
- for (const directory of directories) {
- directories.add(directory.replace(/(^|\/)[^/]*$/, ""));
- }
- const diff = new Set();
- asyncLib.forEachLimit(
- directories,
- 10,
- (directory, callback) => {
- fs.readdir(join(fs, outputPath, directory), (err, entries) => {
- if (err) {
- if (err.code === "ENOENT") return callback();
- if (err.code === "ENOTDIR") {
- diff.add(directory);
- return callback();
- }
- return callback(err);
- }
- for (const entry of entries) {
- const file = /** @type {string} */ (entry);
- const filename = directory ? `${directory}/${file}` : file;
- if (!directories.has(filename) && !currentAssets.has(filename)) {
- diff.add(filename);
- }
- }
- callback();
- });
- },
- err => {
- if (err) return callback(err);
- callback(null, diff);
- }
- );
- };
- /**
- * @param {Assets} currentAssets assets list
- * @param {Assets} oldAssets old assets list
- * @returns {Set<string>} diff
- */
- const getDiffToOldAssets = (currentAssets, oldAssets) => {
- const diff = new Set();
- const now = Date.now();
- for (const [asset, ts] of oldAssets) {
- if (ts >= now) continue;
- if (!currentAssets.has(asset)) diff.add(asset);
- }
- return diff;
- };
- /**
- * @param {OutputFileSystem} fs filesystem
- * @param {string} filename path to file
- * @param {StatsCallback} callback callback for provided filename
- * @returns {void}
- */
- const doStat = (fs, filename, callback) => {
- if ("lstat" in fs) {
- fs.lstat(filename, callback);
- } else {
- fs.stat(filename, callback);
- }
- };
- /**
- * @param {OutputFileSystem} fs filesystem
- * @param {string} outputPath output path
- * @param {boolean} dry only log instead of fs modification
- * @param {Logger} logger logger
- * @param {Set<string>} diff filenames of the assets that shouldn't be there
- * @param {function(string): boolean} isKept check if the entry is ignored
- * @param {function(Error=, Assets=): void} callback callback
- * @returns {void}
- */
- const applyDiff = (fs, outputPath, dry, logger, diff, isKept, callback) => {
- const log = msg => {
- if (dry) {
- logger.info(msg);
- } else {
- logger.log(msg);
- }
- };
- /** @typedef {{ type: "check" | "unlink" | "rmdir", filename: string, parent: { remaining: number, job: Job } | undefined }} Job */
- /** @type {Job[]} */
- const jobs = Array.from(diff.keys(), filename => ({
- type: "check",
- filename,
- parent: undefined
- }));
- /** @type {Assets} */
- const keptAssets = new Map();
- processAsyncTree(
- jobs,
- 10,
- ({ type, filename, parent }, push, callback) => {
- const handleError = err => {
- if (err.code === "ENOENT") {
- log(`${filename} was removed during cleaning by something else`);
- handleParent();
- return callback();
- }
- return callback(err);
- };
- const handleParent = () => {
- if (parent && --parent.remaining === 0) push(parent.job);
- };
- const path = join(fs, outputPath, filename);
- switch (type) {
- case "check":
- if (isKept(filename)) {
- keptAssets.set(filename, 0);
- // do not decrement parent entry as we don't want to delete the parent
- log(`${filename} will be kept`);
- return process.nextTick(callback);
- }
- doStat(fs, path, (err, stats) => {
- if (err) return handleError(err);
- if (!stats.isDirectory()) {
- push({
- type: "unlink",
- filename,
- parent
- });
- return callback();
- }
- fs.readdir(path, (err, entries) => {
- if (err) return handleError(err);
- /** @type {Job} */
- const deleteJob = {
- type: "rmdir",
- filename,
- parent
- };
- if (entries.length === 0) {
- push(deleteJob);
- } else {
- const parentToken = {
- remaining: entries.length,
- job: deleteJob
- };
- for (const entry of entries) {
- const file = /** @type {string} */ (entry);
- if (file.startsWith(".")) {
- log(
- `${filename} will be kept (dot-files will never be removed)`
- );
- continue;
- }
- push({
- type: "check",
- filename: `${filename}/${file}`,
- parent: parentToken
- });
- }
- }
- return callback();
- });
- });
- break;
- case "rmdir":
- log(`${filename} will be removed`);
- if (dry) {
- handleParent();
- return process.nextTick(callback);
- }
- if (!fs.rmdir) {
- logger.warn(
- `${filename} can't be removed because output file system doesn't support removing directories (rmdir)`
- );
- return process.nextTick(callback);
- }
- fs.rmdir(path, err => {
- if (err) return handleError(err);
- handleParent();
- callback();
- });
- break;
- case "unlink":
- log(`${filename} will be removed`);
- if (dry) {
- handleParent();
- return process.nextTick(callback);
- }
- if (!fs.unlink) {
- logger.warn(
- `${filename} can't be removed because output file system doesn't support removing files (rmdir)`
- );
- return process.nextTick(callback);
- }
- fs.unlink(path, err => {
- if (err) return handleError(err);
- handleParent();
- callback();
- });
- break;
- }
- },
- err => {
- if (err) return callback(err);
- callback(undefined, keptAssets);
- }
- );
- };
- /** @type {WeakMap<Compilation, CleanPluginCompilationHooks>} */
- const compilationHooksMap = new WeakMap();
- 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 {SyncBailHook<[string], boolean>} */
- keep: new SyncBailHook(["ignore"])
- };
- compilationHooksMap.set(compilation, hooks);
- }
- return hooks;
- }
- /** @param {CleanOptions} options options */
- constructor(options = {}) {
- validate(options);
- this.options = { dry: false, ...options };
- }
- /**
- * Apply the plugin
- * @param {Compiler} compiler the compiler instance
- * @returns {void}
- */
- apply(compiler) {
- const { dry, keep } = this.options;
- const keepFn =
- typeof keep === "function"
- ? keep
- : typeof keep === "string"
- ? path => path.startsWith(keep)
- : typeof keep === "object" && keep.test
- ? path => keep.test(path)
- : () => false;
- // We assume that no external modification happens while the compiler is active
- // So we can store the old assets and only diff to them to avoid fs access on
- // incremental builds
- /** @type {undefined|Assets} */
- let oldAssets;
- compiler.hooks.emit.tapAsync(
- {
- name: "CleanPlugin",
- stage: 100
- },
- (compilation, callback) => {
- const hooks = CleanPlugin.getCompilationHooks(compilation);
- const logger = compilation.getLogger("webpack.CleanPlugin");
- const fs = compiler.outputFileSystem;
- if (!fs.readdir) {
- return callback(
- new Error(
- "CleanPlugin: Output filesystem doesn't support listing directories (readdir)"
- )
- );
- }
- /** @type {Assets} */
- const currentAssets = new Map();
- const now = Date.now();
- for (const asset of Object.keys(compilation.assets)) {
- if (/^[A-Za-z]:\\|^\/|^\\\\/.test(asset)) continue;
- let normalizedAsset;
- let newNormalizedAsset = asset.replace(/\\/g, "/");
- do {
- normalizedAsset = newNormalizedAsset;
- newNormalizedAsset = normalizedAsset.replace(
- /(^|\/)(?!\.\.)[^/]+\/\.\.\//g,
- "$1"
- );
- } while (newNormalizedAsset !== normalizedAsset);
- if (normalizedAsset.startsWith("../")) continue;
- const assetInfo = compilation.assetsInfo.get(asset);
- if (assetInfo && assetInfo.hotModuleReplacement) {
- currentAssets.set(normalizedAsset, now + _10sec);
- } else {
- currentAssets.set(normalizedAsset, 0);
- }
- }
- const outputPath = compilation.getPath(compiler.outputPath, {});
- const isKept = path => {
- const result = hooks.keep.call(path);
- if (result !== undefined) return result;
- return keepFn(path);
- };
- /**
- * @param {Error=} err err
- * @param {Set<string>=} diff diff
- */
- const diffCallback = (err, diff) => {
- if (err) {
- oldAssets = undefined;
- callback(err);
- return;
- }
- applyDiff(
- fs,
- outputPath,
- dry,
- logger,
- diff,
- isKept,
- (err, keptAssets) => {
- if (err) {
- oldAssets = undefined;
- } else {
- if (oldAssets) mergeAssets(currentAssets, oldAssets);
- oldAssets = currentAssets;
- if (keptAssets) mergeAssets(oldAssets, keptAssets);
- }
- callback(err);
- }
- );
- };
- if (oldAssets) {
- diffCallback(null, getDiffToOldAssets(currentAssets, oldAssets));
- } else {
- getDiffToFs(fs, outputPath, currentAssets, diffCallback);
- }
- }
- );
- }
- }
- module.exports = CleanPlugin;
|