2013-12-03 16:27:15 +08:00
/ *
MIT License http : //www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @ sokra
* /
2017-01-04 21:50:00 +08:00
"use strict" ;
let nextIdent = 0 ;
class CommonsChunkPlugin {
constructor ( options ) {
if ( arguments . length > 1 ) {
2017-01-05 03:34:12 +08:00
throw new Error ( ` Deprecation notice: CommonsChunkPlugin now only takes a single argument. Either an options
2017-01-10 19:15:41 +08:00
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 ` );
2017-01-04 21:50:00 +08:00
}
2017-01-10 19:15:41 +08:00
2017-02-19 11:30:53 +08:00
const normalizedOptions = this . normalizeOptions ( options ) ;
this . chunkNames = normalizedOptions . chunkNames ;
this . filenameTemplate = this . filenameTemplate ;
this . minChunks = normalizedOptions . minChunks ;
this . selectedChunks = normalizedOptions . selectedChunks ;
this . async = normalizedOptions . async ;
this . minSize = normalizedOptions . minSize ;
this . ident = normalizedOptions . ident ;
}
normalizeOptions ( options ) {
if ( Array . isArray ( options ) ) {
return {
chunkNames : options ,
} ;
}
if ( typeof options === "string" ) {
return {
chunkNames : [ options ] ,
2017-01-04 21:50:00 +08:00
} ;
}
2017-02-19 11:30:53 +08:00
// if "children" is set, set selectedChunks to false instead of specified chunks
// TODO: why? :P
const selectedChunks = options . children ? false : options . chunks ;
const chunkNames = options . name ? [ options . name ] : options . names ;
return {
chunkNames : chunkNames ,
filenameTemplate : options . filename ,
minChunks : options . minChunks ,
selectedChunks : selectedChunks ,
async : options . async ,
minSize : options . minSize ,
ident : _ _filename + ( nextIdent ++ ) ,
} ;
2016-01-22 12:01:00 +08:00
}
2017-02-19 11:30:53 +08:00
2017-02-19 14:34:52 +08:00
getCommonChunks ( allChunks , compilation , chunkNames , selectedChunks , async ) {
const asyncOrNoSelectedChunk = selectedChunks === false || async ;
2017-02-19 11:31:35 +08:00
// we have specified chunk names
2017-02-19 14:34:52 +08:00
if ( chunkNames ) {
2017-02-19 11:31:35 +08:00
// map chunks by chunkName for quick access
const optimizedChunkMap = allChunks . reduce ( ( map , chunk ) => {
map . set ( chunk . name , chunk ) ;
return map ;
} , new Map ( ) ) ;
// Ensure we have a chunk per specified chunk name.
// Reuse existing chunks if possible
2017-02-19 14:34:52 +08:00
return chunkNames . map ( chunkName => {
2017-02-19 11:31:35 +08:00
if ( optimizedChunkMap . has ( chunkName ) ) {
return optimizedChunkMap . 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 ) {
return allChunks ;
}
// that is not supposed to happen, lets throw
throw new Error ( "Invalid chunkNames argument" ) ;
}
2017-02-19 12:59:00 +08:00
getUsedChunks ( compilation , allChunks , commonChunk , commonChunks , currentIndex , selectedChunks , isAsync ) {
const asyncOrNoSelectedChunk = selectedChunks === false || isAsync ;
if ( Array . isArray ( selectedChunks ) ) {
return allChunks . filter ( chunk => {
const notCommmonChunk = chunk !== commonChunk ;
const isSelectedChunk = selectedChunks . indexOf ( chunk . name ) > - 1 ;
return notCommmonChunk && isSelectedChunk ;
} ) ;
}
if ( asyncOrNoSelectedChunk ) {
// nothing to do here
if ( ! commonChunk . chunks ) {
return [ ] ;
}
return commonChunk . chunks . filter ( ( chunk ) => {
// we can only move modules from this chunk if the "commonChunk" is the only parent
return isAsync || chunk . parents . length === 1 ;
} ) ;
}
// this is an entry point - bad
if ( commonChunk . parents . length > 0 ) {
compilation . errors . push ( new Error ( "CommonsChunkPlugin: While running in normal mode it's not allowed to use a non-entry chunk (" + commonChunk . name + ")" ) ) ;
return ;
}
2017-02-19 14:34:52 +08:00
// what is this?
2017-02-19 12:59:00 +08:00
return allChunks . filter ( ( chunk ) => {
const found = commonChunks . indexOf ( chunk ) ;
if ( found >= currentIndex ) return false ;
return chunk . hasRuntime ( ) ;
} ) ;
}
2017-02-19 12:59:31 +08:00
createAsyncChunk ( compilation , asyncOption , commonChunk ) {
const asyncChunk = compilation . addChunk ( typeof asyncOption === "string" ? asyncOption : undefined ) ;
asyncChunk . chunkReason = "async commons chunk" ;
asyncChunk . extraAsync = true ;
asyncChunk . addParent ( commonChunk ) ;
return asyncChunk ;
}
2017-02-19 13:36:11 +08:00
// 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 , commonChunk , usedChunksLength ) {
if ( typeof minChunks === "function" ) {
return minChunks ;
}
const minCount = ( minChunks || Math . max ( 2 , usedChunksLength ) ) ;
const isUsedAtLeastMinTimes = ( module , count ) => count >= minCount ;
return isUsedAtLeastMinTimes ;
}
getReallyUsedModules ( minChunks , usedChunks , commonChunk ) {
if ( minChunks === Infinity ) {
return [ ] ;
}
// count how many chunks contain a module
const commonModulesToCountMap = usedChunks . reduce ( ( map , chunk ) => {
for ( let module of chunk . modules ) {
let 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 , commonChunk , 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 , commonChunk ) ;
} ) . map ( entry => entry [ 0 ] ) ;
}
getModulesSize ( modules ) {
return modules . reduce ( ( count , module ) => count + module . size ( ) , 0 ) ;
}
2017-02-19 13:55:04 +08:00
removeModulesFromUsedChunksAndReturnUsedChunks ( reallyUsedModules , usedChunks ) {
return reallyUsedModules . reduce ( ( affectedChunksSet , module ) => {
for ( let chunk of usedChunks ) {
// 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 ( ) ) ;
}
connectModulesWithCommonChunk ( chunk , modules ) {
for ( let module of modules ) {
chunk . addModule ( module ) ;
module . addChunk ( chunk ) ;
}
}
2017-02-19 14:34:52 +08:00
addCommonChunkAsParentOfAffectedChunks ( usedChunks , commonChunk ) {
2017-02-19 14:11:58 +08:00
for ( let chunk of usedChunks ) {
// set commonChunk as new sole parent
chunk . parents = [ commonChunk ] ;
// add chunk to commonChunk
commonChunk . addChunk ( chunk ) ;
for ( let entrypoint of chunk . entrypoints ) {
entrypoint . insertChunk ( commonChunk , chunk ) ;
}
}
}
connectChunkBlocksWithCommonChunk ( chunks , commonChunk ) {
for ( let chunk of chunks ) {
// only for non initial chunks
// TODO: why?
if ( ! chunk . isInitial ( ) ) {
for ( let block of chunk . blocks ) {
block . chunks . unshift ( commonChunk ) ;
commonChunk . addBlock ( block ) ;
}
}
}
}
getAsyncChunkOrigin ( chunks ) {
const origins = [ ] ;
for ( let chunk of chunks ) {
for ( let origin of chunk . origins ) {
const newOrigin = Object . create ( origin ) ;
newOrigin . reasons = ( origin . reasons || [ ] ) . concat ( "async commons" ) ;
origins . push ( newOrigin ) ;
}
}
return origins ;
}
2017-01-04 21:50:00 +08:00
apply ( compiler ) {
const filenameTemplate = this . filenameTemplate ;
const asyncOption = this . async ;
const minSize = this . minSize ;
2017-02-19 13:36:11 +08:00
const minChunks = this . minChunks ;
2017-01-24 18:47:12 +08:00
const ident = this . ident ;
2017-01-04 21:50:00 +08:00
compiler . plugin ( "this-compilation" , ( compilation ) => {
2017-01-24 19:09:46 +08:00
compilation . plugin ( [ "optimize-chunks" , "optimize-extracted-chunks" ] , ( chunks ) => {
2017-01-04 21:50:00 +08:00
// only optimize once
2017-01-24 18:47:12 +08:00
if ( compilation [ ident ] ) return ;
compilation [ ident ] = true ;
2015-11-21 04:29:32 +08:00
2017-02-19 14:34:52 +08:00
/ * *
* 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
* /
const commonChunks = this . getCommonChunks ( chunks , compilation , this . chunkNames , this . selectedChunks , this . async ) ;
2017-02-19 11:31:35 +08:00
2017-02-19 14:34:52 +08:00
// iterate over all our new chunks
2017-02-19 12:59:00 +08:00
commonChunks . forEach ( ( commonChunk , idx ) => {
2017-02-19 14:34:52 +08:00
// get chunks that are actually used
// TODO: clarify what that means
2017-02-19 12:59:00 +08:00
const usedChunks = this . getUsedChunks ( compilation , chunks , commonChunk , commonChunks , idx , this . selectedChunks , this . async ) ;
2017-02-19 14:34:52 +08:00
2017-02-19 12:59:00 +08:00
// bail as this is an erronous state
if ( ! usedChunks ) {
return ;
2017-01-04 21:50:00 +08:00
}
2017-02-19 12:59:31 +08:00
2017-02-19 14:34:52 +08:00
// 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
2017-01-04 22:56:46 +08:00
let asyncChunk ;
2017-01-04 21:50:00 +08:00
if ( asyncOption ) {
2017-02-19 12:59:31 +08:00
asyncChunk = this . createAsyncChunk ( compilation , this . async , commonChunk ) ;
2017-01-04 21:50:00 +08:00
commonChunk . addChunk ( asyncChunk ) ;
commonChunk = asyncChunk ;
}
2017-02-19 14:34:52 +08:00
// get all modules that suffice the filter e.g. are used at least in "n" chunks
// TODO: what does "really used modules mean here"?
2017-02-19 13:36:11 +08:00
const reallyUsedModules = this . getReallyUsedModules ( minChunks , usedChunks , commonChunk ) ;
2017-02-19 14:34:52 +08:00
// 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).
2017-01-04 21:50:00 +08:00
if ( minSize ) {
2017-02-19 13:36:11 +08:00
const modulesSize = this . getModulesSize ( reallyUsedModules ) ;
// if too small, bail
if ( modulesSize < minSize )
2015-01-12 06:15:11 +08:00
return ;
2017-01-04 21:50:00 +08:00
}
2017-02-19 13:36:11 +08:00
2017-02-19 14:34:52 +08:00
// 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)
2017-02-19 13:55:04 +08:00
const reallyUsedChunks = this . removeModulesFromUsedChunksAndReturnUsedChunks ( reallyUsedModules , usedChunks ) ;
2017-02-19 14:34:52 +08:00
// connect all extracted modules with the common chunk
2017-02-19 13:55:04 +08:00
this . connectModulesWithCommonChunk ( commonChunk , reallyUsedModules ) ;
2017-02-19 14:11:58 +08:00
// set filenameTemplate for chunk
2017-01-04 21:50:00 +08:00
if ( filenameTemplate )
commonChunk . filenameTemplate = filenameTemplate ;
2017-02-19 14:11:58 +08:00
2017-02-19 14:34:52 +08:00
// 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
2017-02-19 14:11:58 +08:00
if ( asyncOption ) {
this . connectChunkBlocksWithCommonChunk ( reallyUsedChunks , commonChunk ) ;
asyncChunk . origins = this . getAsyncChunkOrigin ( reallyUsedChunks ) ;
return ;
}
2017-02-19 14:34:52 +08:00
// we are not in "async" mode
// connect used chunks with commonChunk - shouldnt this be reallyUsedChunks here?
this . addCommonChunkAsParentOfAffectedChunks ( usedChunks , commonChunk ) ;
2017-01-24 19:09:46 +08:00
} ) ;
2017-01-04 21:50:00 +08:00
return true ;
} ) ;
2013-12-03 16:27:15 +08:00
} ) ;
2017-01-04 21:50:00 +08:00
}
}
module . exports = CommonsChunkPlugin ;