2017-05-03 15:35:00 +02:00
/ *
MIT License http : //www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @ sokra
* /
"use strict" ;
let nextIdent = 0 ;
2017-10-14 18:40:54 +02:00
2017-05-03 15:35:00 +02:00
class CommonsChunkPlugin {
constructor ( options ) {
if ( arguments . length > 1 ) {
throw new Error ( ` Deprecation notice: CommonsChunkPlugin now only takes a single argument. Either an options
object * or * the name of the chunk .
Example : if your old code looked like this :
new webpack . optimize . CommonsChunkPlugin ( 'vendor' , 'vendor.bundle.js' )
You would change it to :
new webpack . optimize . CommonsChunkPlugin ( { name : 'vendor' , filename : 'vendor.bundle.js' } )
The available options are :
name : string
names : string [ ]
filename : string
minChunks : number
chunks : string [ ]
children : boolean
async : boolean
minSize : number ` );
}
const normalizedOptions = this . normalizeOptions ( options ) ;
this . chunkNames = normalizedOptions . chunkNames ;
this . filenameTemplate = normalizedOptions . filenameTemplate ;
this . minChunks = normalizedOptions . minChunks ;
this . selectedChunks = normalizedOptions . selectedChunks ;
this . children = normalizedOptions . children ;
2017-10-14 18:40:54 +02:00
this . deepChildren = normalizedOptions . deepChildren ;
2017-05-03 15:35:00 +02:00
this . async = normalizedOptions . async ;
this . minSize = normalizedOptions . minSize ;
this . ident = _ _filename + ( nextIdent ++ ) ;
}
normalizeOptions ( options ) {
if ( Array . isArray ( options ) ) {
return {
chunkNames : options ,
} ;
}
if ( typeof options === "string" ) {
return {
chunkNames : [ options ] ,
} ;
}
// options.children and options.chunk may not be used together
if ( options . children && options . chunks ) {
throw new Error ( "You can't and it does not make any sense to use \"children\" and \"chunk\" options together." ) ;
}
/ * *
* options . async and options . filename are also not possible together
* as filename specifies how the chunk is called but "async" implies
* that webpack will take care of loading this file .
* /
if ( options . async && options . filename ) {
2017-08-14 05:01:11 +02:00
throw new Error ( ` You can not specify a filename if you use the "async" option.
You can however specify the name of the async chunk by passing the desired string as the "async" option . ` );
2017-05-03 15:35:00 +02:00
}
/ * *
* Make sure this is either an array or undefined .
* "name" can be a string and
* "names" a string or an array
* /
const chunkNames = options . name || options . names ? [ ] . concat ( options . name || options . names ) : undefined ;
return {
chunkNames : chunkNames ,
filenameTemplate : options . filename ,
minChunks : options . minChunks ,
selectedChunks : options . chunks ,
children : options . children ,
2017-10-14 18:40:54 +02:00
deepChildren : options . deepChildren ,
2017-05-03 15:35:00 +02:00
async : options . async ,
minSize : options . minSize
} ;
}
apply ( compiler ) {
compiler . plugin ( "this-compilation" , ( compilation ) => {
compilation . plugin ( [ "optimize-chunks" , "optimize-extracted-chunks" ] , ( chunks ) => {
// only optimize once
if ( compilation [ this . ident ] ) return ;
compilation [ this . ident ] = true ;
/ * *
* Creates a list of "common" " chunks based on the options .
* The list is made up of preexisting or newly created chunks .
* - If chunk has the name as specified in the chunkNames it is put in the list
* - If no chunk with the name as given in chunkNames exists a new chunk is created and added to the list
*
* These chunks are the "targets" for extracted modules .
* /
const targetChunks = this . getTargetChunks ( chunks , compilation , this . chunkNames , this . children , this . async ) ;
// iterate over all our new chunks
targetChunks . forEach ( ( targetChunk , idx ) => {
/ * *
* These chunks are subject to get "common" modules extracted and moved to the common chunk
* /
const affectedChunks = this . getAffectedChunks ( compilation , chunks , targetChunk , targetChunks , idx , this . selectedChunks , this . async , this . children ) ;
// bail if no chunk is affected
if ( ! affectedChunks ) {
return ;
}
// If we are async create an async chunk now
// override the "commonChunk" with the newly created async one and use it as commonChunk from now on
let asyncChunk ;
if ( this . async ) {
2017-08-14 05:01:11 +02:00
// If async chunk is one of the affected chunks, just use it
asyncChunk = affectedChunks . filter ( c => c . name === this . async ) [ 0 ] ;
// Elsewise create a new one
if ( ! asyncChunk ) {
asyncChunk = this . createAsyncChunk (
compilation ,
targetChunks . length <= 1 || typeof this . async !== "string" ? this . async :
targetChunk . name ? ` ${ this . async } - ${ targetChunk . name } ` :
true ,
targetChunk
) ;
}
2017-05-03 15:35:00 +02:00
targetChunk = asyncChunk ;
}
/ * *
* Check which modules are "common" and could be extracted to a "common" chunk
* /
const extractableModules = this . getExtractableModules ( this . minChunks , affectedChunks , targetChunk ) ;
// If the minSize option is set check if the size extracted from the chunk is reached
// else bail out here.
// As all modules/commons are interlinked with each other, common modules would be extracted
// if we reach this mark at a later common chunk. (quirky I guess).
if ( this . minSize ) {
const modulesSize = this . calculateModulesSize ( extractableModules ) ;
// if too small, bail
if ( modulesSize < this . minSize )
return ;
}
// Remove modules that are moved to commons chunk from their original chunks
// return all chunks that are affected by having modules removed - we need them later (apparently)
const chunksWithExtractedModules = this . extractModulesAndReturnAffectedChunks ( extractableModules , affectedChunks ) ;
// connect all extracted modules with the common chunk
this . addExtractedModulesToTargetChunk ( targetChunk , extractableModules ) ;
// set filenameTemplate for chunk
if ( this . filenameTemplate )
targetChunk . filenameTemplate = this . filenameTemplate ;
// if we are async connect the blocks of the "reallyUsedChunk" - the ones that had modules removed -
// with the commonChunk and get the origins for the asyncChunk (remember "asyncChunk === commonChunk" at this moment).
// bail out
if ( this . async ) {
this . moveExtractedChunkBlocksToTargetChunk ( chunksWithExtractedModules , targetChunk ) ;
asyncChunk . origins = this . extractOriginsOfChunksWithExtractedModules ( chunksWithExtractedModules ) ;
return ;
}
// we are not in "async" mode
// connect used chunks with commonChunk - shouldnt this be reallyUsedChunks here?
this . makeTargetChunkParentOfAffectedChunks ( affectedChunks , targetChunk ) ;
} ) ;
return true ;
} ) ;
} ) ;
}
getTargetChunks ( allChunks , compilation , chunkNames , children , asyncOption ) {
const asyncOrNoSelectedChunk = children || asyncOption ;
// we have specified chunk names
if ( chunkNames ) {
// map chunks by chunkName for quick access
const allChunksNameMap = allChunks . reduce ( ( map , chunk ) => {
if ( chunk . name ) {
map . set ( chunk . name , chunk ) ;
}
return map ;
} , new Map ( ) ) ;
// Ensure we have a chunk per specified chunk name.
// Reuse existing chunks if possible
return chunkNames . map ( chunkName => {
if ( allChunksNameMap . has ( chunkName ) ) {
return allChunksNameMap . get ( chunkName ) ;
}
// add the filtered chunks to the compilation
return compilation . addChunk ( chunkName ) ;
} ) ;
}
// we dont have named chunks specified, so we just take all of them
if ( asyncOrNoSelectedChunk ) {
2017-08-14 05:01:11 +02:00
return allChunks ;
2017-05-03 15:35:00 +02:00
}
/ * *
* No chunk name ( s ) was specified nor is this an async / children commons chunk
* /
throw new Error ( ` You did not specify any valid target chunk settings.
Take a look at the "name" / "names" or async / children option . ` );
}
2017-10-14 18:40:54 +02:00
getAffectedUnnamedChunks ( affectedChunks , targetChunk , asyncOption ) {
let chunks = targetChunk . chunks ;
chunks && chunks . forEach ( ( chunk ) => {
if ( chunk . isInitial ( ) ) {
return ;
}
// If all the parents of a chunk are either
// a) the target chunk we started with
// b) themselves affected chunks
// we can assume that this chunk is an affected chunk too, as there is no way a chunk that
// isn't only depending on the target chunk is a parent of the chunk tested
if ( asyncOption || chunk . parents . every ( ( parentChunk ) => parentChunk === targetChunk || affectedChunks . has ( parentChunk ) ) ) {
// This check not only dedupes the affectedChunks but also guarantees we avoid endless loops
if ( ! affectedChunks . has ( chunk ) || affectedChunks . values ( ) . next ( ) . value === chunk ) {
// We mutate the affected chunks before going deeper, so the deeper levels and other branches
// Have the information of this chunk being affected for their assertion if a chunk should
// not be affected
affectedChunks . add ( chunk ) ;
// We recurse down to all the children of the chunk, applying the same assumption.
// This guarantees that if a chunk should be an affected chunk,
// at the latest the last connection to the same chunk meets the
// condition to add it to the affected chunks.
if ( this . deepChildren === true ) {
this . getAffectedUnnamedChunks ( affectedChunks , chunk , asyncOption ) ;
}
}
}
} ) ;
}
2017-05-03 15:35:00 +02:00
getAffectedChunks ( compilation , allChunks , targetChunk , targetChunks , currentIndex , selectedChunks , asyncOption , children ) {
const asyncOrNoSelectedChunk = children || asyncOption ;
if ( Array . isArray ( selectedChunks ) ) {
return allChunks . filter ( chunk => {
const notCommmonChunk = chunk !== targetChunk ;
const isSelectedChunk = selectedChunks . indexOf ( chunk . name ) > - 1 ;
return notCommmonChunk && isSelectedChunk ;
} ) ;
}
if ( asyncOrNoSelectedChunk ) {
2017-10-14 18:40:54 +02:00
let affectedChunks = new Set ( ) ;
this . getAffectedUnnamedChunks ( affectedChunks , targetChunk , asyncOption ) ;
return Array . from ( affectedChunks ) ;
2017-05-03 15:35:00 +02:00
}
/ * *
* past this point only entry chunks are allowed to become commonChunks
* /
if ( targetChunk . parents . length > 0 ) {
compilation . errors . push ( new Error ( "CommonsChunkPlugin: While running in normal mode it's not allowed to use a non-entry chunk (" + targetChunk . name + ")" ) ) ;
return ;
}
/ * *
* If we find a "targetchunk" that is also a normal chunk ( meaning it is probably specified as an entry )
* and the current target chunk comes after that and the found chunk has a runtime *
* make that chunk be an 'affected' chunk of the current target chunk .
*
* To understand what that means take a look at the "examples/chunkhash" , this basically will
* result in the runtime to be extracted to the current target chunk .
*
* * runtime : the "runtime" is the "webpack" - block you may have seen in the bundles that resolves modules etc .
* /
return allChunks . filter ( ( chunk ) => {
const found = targetChunks . indexOf ( chunk ) ;
if ( found >= currentIndex ) return false ;
return chunk . hasRuntime ( ) ;
} ) ;
}
createAsyncChunk ( compilation , asyncOption , targetChunk ) {
const asyncChunk = compilation . addChunk ( typeof asyncOption === "string" ? asyncOption : undefined ) ;
asyncChunk . chunkReason = "async commons chunk" ;
asyncChunk . extraAsync = true ;
asyncChunk . addParent ( targetChunk ) ;
targetChunk . addChunk ( asyncChunk ) ;
return asyncChunk ;
}
// If minChunks is a function use that
// otherwhise check if a module is used at least minChunks or 2 or usedChunks.length time
getModuleFilter ( minChunks , targetChunk , usedChunksLength ) {
if ( typeof minChunks === "function" ) {
return minChunks ;
}
const minCount = ( minChunks || Math . max ( 2 , usedChunksLength ) ) ;
const isUsedAtLeastMinTimes = ( module , count ) => count >= minCount ;
return isUsedAtLeastMinTimes ;
}
getExtractableModules ( minChunks , usedChunks , targetChunk ) {
if ( minChunks === Infinity ) {
return [ ] ;
}
// count how many chunks contain a module
const commonModulesToCountMap = usedChunks . reduce ( ( map , chunk ) => {
2017-08-14 05:01:11 +02:00
for ( const module of chunk . modulesIterable ) {
2017-05-03 15:35:00 +02:00
const count = map . has ( module ) ? map . get ( module ) : 0 ;
map . set ( module , count + 1 ) ;
}
return map ;
} , new Map ( ) ) ;
// filter by minChunks
const moduleFilterCount = this . getModuleFilter ( minChunks , targetChunk , usedChunks . length ) ;
// filter by condition
const moduleFilterCondition = ( module , chunk ) => {
if ( ! module . chunkCondition ) {
return true ;
}
return module . chunkCondition ( chunk ) ;
} ;
return Array . from ( commonModulesToCountMap ) . filter ( entry => {
const module = entry [ 0 ] ;
const count = entry [ 1 ] ;
// if the module passes both filters, keep it.
return moduleFilterCount ( module , count ) && moduleFilterCondition ( module , targetChunk ) ;
} ) . map ( entry => entry [ 0 ] ) ;
}
calculateModulesSize ( modules ) {
return modules . reduce ( ( totalSize , module ) => totalSize + module . size ( ) , 0 ) ;
}
extractModulesAndReturnAffectedChunks ( reallyUsedModules , usedChunks ) {
return reallyUsedModules . reduce ( ( affectedChunksSet , module ) => {
2017-08-14 05:01:11 +02:00
for ( const chunk of usedChunks ) {
2017-05-03 15:35:00 +02:00
// removeChunk returns true if the chunk was contained and succesfully removed
// false if the module did not have a connection to the chunk in question
if ( module . removeChunk ( chunk ) ) {
affectedChunksSet . add ( chunk ) ;
}
}
return affectedChunksSet ;
} , new Set ( ) ) ;
}
addExtractedModulesToTargetChunk ( chunk , modules ) {
2017-08-14 05:01:11 +02:00
for ( const module of modules ) {
2017-05-03 15:35:00 +02:00
chunk . addModule ( module ) ;
module . addChunk ( chunk ) ;
}
}
makeTargetChunkParentOfAffectedChunks ( usedChunks , commonChunk ) {
2017-08-14 05:01:11 +02:00
for ( const chunk of usedChunks ) {
2017-05-03 15:35:00 +02:00
// set commonChunk as new sole parent
chunk . parents = [ commonChunk ] ;
// add chunk to commonChunk
commonChunk . addChunk ( chunk ) ;
2017-08-14 05:01:11 +02:00
for ( const entrypoint of chunk . entrypoints ) {
2017-05-03 15:35:00 +02:00
entrypoint . insertChunk ( commonChunk , chunk ) ;
}
}
}
moveExtractedChunkBlocksToTargetChunk ( chunks , targetChunk ) {
2017-08-14 05:01:11 +02:00
for ( const chunk of chunks ) {
if ( chunk === targetChunk ) continue ;
for ( const block of chunk . blocks ) {
if ( block . chunks . indexOf ( targetChunk ) === - 1 ) {
block . chunks . unshift ( targetChunk ) ;
}
2017-05-03 15:35:00 +02:00
targetChunk . addBlock ( block ) ;
}
}
}
extractOriginsOfChunksWithExtractedModules ( chunks ) {
const origins = [ ] ;
2017-08-14 05:01:11 +02:00
for ( const chunk of chunks ) {
for ( const origin of chunk . origins ) {
2017-05-03 15:35:00 +02:00
const newOrigin = Object . create ( origin ) ;
newOrigin . reasons = ( origin . reasons || [ ] ) . concat ( "async commons" ) ;
origins . push ( newOrigin ) ;
}
}
return origins ;
}
}
module . exports = CommonsChunkPlugin ;