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 */
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
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-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-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 = [ ] ;
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 ( )
} ;
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 . 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
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 (
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 ) ) ) {
2020-10-02 23:08:03 +08:00
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-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 ) {
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 ;