docusaurus/packages/docusaurus/src/webpack/plugins/CleanWebpackPlugin.ts

258 lines
7.1 KiB
TypeScript

/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* The MIT License (MIT)
* Copyright (c) 2015 John Agan
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// Forked from https://github.com/johnagan/clean-webpack-plugin
// Modified to optimize performance for Docusaurus specific use case
// More context: https://github.com/facebook/docusaurus/pull/1839
import {Compiler, Stats} from 'webpack';
import path from 'path';
import {sync as delSync} from 'del';
export interface Options {
/**
* Write Logs to Console
* (Always enabled when dry is true)
*
* default: false
*/
verbose?: boolean;
/**
* Automatically remove all unused webpack assets on rebuild
*
* default: true
*/
cleanStaleWebpackAssets?: boolean;
/**
* Do not allow removal of current webpack assets
*
* default: true
*/
protectWebpackAssets?: boolean;
/**
* Removes files once prior to Webpack compilation
* Not included in rebuilds (watch mode)
*
* Use !negative patterns to exclude files
*
* default: ['**\/*']
*/
cleanOnceBeforeBuildPatterns?: string[];
}
class CleanWebpackPlugin {
private readonly verbose: boolean;
private readonly cleanStaleWebpackAssets: boolean;
private readonly protectWebpackAssets: boolean;
private readonly cleanOnceBeforeBuildPatterns: string[];
private currentAssets: string[];
private initialClean: boolean;
private outputPath: string;
constructor(options: Options = {}) {
this.verbose = options.verbose === true || false;
this.cleanStaleWebpackAssets =
options.cleanStaleWebpackAssets === true ||
options.cleanStaleWebpackAssets === false
? options.cleanStaleWebpackAssets
: true;
this.protectWebpackAssets =
options.protectWebpackAssets === true ||
options.protectWebpackAssets === false
? options.protectWebpackAssets
: true;
this.cleanOnceBeforeBuildPatterns = Array.isArray(
options.cleanOnceBeforeBuildPatterns,
)
? options.cleanOnceBeforeBuildPatterns
: ['**/*'];
/**
* Store webpack build assets
*/
this.currentAssets = [];
/**
* Only used with cleanOnceBeforeBuildPatterns
*/
this.initialClean = false;
this.outputPath = '';
this.apply = this.apply.bind(this);
this.handleInitial = this.handleInitial.bind(this);
this.handleDone = this.handleDone.bind(this);
this.removeFiles = this.removeFiles.bind(this);
}
apply(compiler: Compiler): void {
if (!compiler.options.output || !compiler.options.output.path) {
// eslint-disable-next-line no-console
console.warn(
'clean-webpack-plugin: options.output.path not defined. Plugin disabled...',
);
return;
}
this.outputPath = compiler.options.output.path;
const {hooks} = compiler;
if (this.cleanOnceBeforeBuildPatterns.length !== 0) {
hooks.compile.tap('clean-webpack-plugin', () => {
this.handleInitial();
});
}
hooks.done.tap('clean-webpack-plugin', (stats) => {
this.handleDone(stats);
});
}
/**
* Initially remove files from output directory prior to build.
*
* Only happens once.
*
* Warning: It is recommended to initially clean your build directory outside of webpack to minimize unexpected behavior.
*/
handleInitial(): void {
if (this.initialClean) {
return;
}
this.initialClean = true;
this.removeFiles(this.cleanOnceBeforeBuildPatterns);
}
handleDone(stats: Stats): void {
/**
* Do nothing if there is a webpack error
*/
if (stats.hasErrors()) {
if (this.verbose) {
// eslint-disable-next-line no-console
console.warn('clean-webpack-plugin: pausing due to webpack errors');
}
return;
}
/**
* Fetch Webpack's output asset files
*/
const statsAssets =
stats.toJson({
all: false,
assets: true,
}).assets || [];
const assets = statsAssets.map((asset: {name: string}) => {
return asset.name;
});
/**
* Get all files that were in the previous build but not the current
*
* (relies on del's cwd: outputPath option)
*/
const staleFiles = this.currentAssets.filter((previousAsset) => {
return assets.includes(previousAsset) === false;
});
/**
* Save assets for next compilation
*/
this.currentAssets = assets.sort();
const removePatterns: string[] = [];
/**
* Remove unused webpack assets
*/
if (this.cleanStaleWebpackAssets === true && staleFiles.length !== 0) {
removePatterns.push(...staleFiles);
}
if (removePatterns.length !== 0) {
this.removeFiles(removePatterns);
}
}
removeFiles(patterns: string[]): void {
try {
const deleted = delSync(patterns, {
force: false,
// Change context to build directory
cwd: this.outputPath,
dryRun: false,
dot: true,
ignore: this.protectWebpackAssets ? this.currentAssets : [],
});
/**
* Log if verbose is enabled
*/
if (this.verbose) {
deleted.forEach((file) => {
const filename = path.relative(process.cwd(), file);
/**
* Use console.warn over .log
* https://github.com/webpack/webpack/issues/1904
* https://github.com/johnagan/clean-webpack-plugin/issues/11
*/
// eslint-disable-next-line no-console
console.warn(`clean-webpack-plugin: removed ${filename}`);
});
}
} catch (error) {
const needsForce = /Cannot delete files\/folders outside the current working directory\./.test(
error.message,
);
if (needsForce) {
const message =
'clean-webpack-plugin: Cannot delete files/folders outside the current working directory. Can be overridden with the `dangerouslyAllowCleanPatternsOutsideProject` option.';
throw new Error(message);
}
throw error;
}
}
}
export default CleanWebpackPlugin;