/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; let nextIdent = 0; 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; 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) { 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.`); } /** * 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, 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) { asyncChunk = this.createAsyncChunk(compilation, this.async, targetChunk); 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) { return allChunks.filter(chunk => !chunk.isInitial()); } /** * 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.`); } 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) { // nothing to do here if(!targetChunk.chunks) { return []; } return targetChunk.chunks.filter((chunk) => { // we can only move modules from this chunk if the "commonChunk" is the only parent return asyncOption || chunk.parents.length === 1; }); } /** * 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) => { for(let module of chunk.modules) { 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) => { 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()); } addExtractedModulesToTargetChunk(chunk, modules) { for(let module of modules) { chunk.addModule(module); module.addChunk(chunk); } } makeTargetChunkParentOfAffectedChunks(usedChunks, commonChunk) { 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); } } } moveExtractedChunkBlocksToTargetChunk(chunks, targetChunk) { for(let chunk of chunks) { for(let block of chunk.blocks) { block.chunks.unshift(targetChunk); targetChunk.addBlock(block); } } } extractOriginsOfChunksWithExtractedModules(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; } } module.exports = CommonsChunkPlugin;