|
| 1 | +/* |
| 2 | + MIT License http://www.opensource.org/licenses/mit-license.php |
| 3 | + Author Tobias Koppers @sokra |
| 4 | +*/ |
| 5 | +"use strict"; |
| 6 | + |
| 7 | +const SortableSet = require("../util/SortableSet"); |
| 8 | + |
| 9 | +const sortByIdentifier = (a, b) => { |
| 10 | + if(a.identifier() > b.identifier()) return 1; |
| 11 | + if(a.identifier() < b.identifier()) return -1; |
| 12 | + return 0; |
| 13 | +}; |
| 14 | + |
| 15 | +const getRequests = chunk => { |
| 16 | + return Math.max( |
| 17 | + chunk.mapBlocks(block => block.chunks.length).reduce(Math.max, 0), |
| 18 | + chunk.mapEntrypoints(ep => ep.chunks.length).reduce(Math.max, 0) |
| 19 | + ); |
| 20 | +}; |
| 21 | + |
| 22 | +module.exports = class AutomaticCommonsChunksPlugin { |
| 23 | + constructor(options) { |
| 24 | + this.options = Object.assign({}, { |
| 25 | + initialChunks: false, |
| 26 | + minSize: 30000, |
| 27 | + maxRequests: 4, |
| 28 | + onlyNamed: false, |
| 29 | + name: undefined |
| 30 | + }, options); |
| 31 | + } |
| 32 | + |
| 33 | + apply(compiler) { |
| 34 | + compiler.hooks.compilation.tap("AutomaticCommonsChunksPlugin", compilation => { |
| 35 | + compilation.hooks.optimizeChunksAdvanced.tap("AutomaticCommonsChunksPlugin", chunks => { |
| 36 | + // Give each selected chunk an index (to create strings from chunks) |
| 37 | + const indexMap = new Map(); |
| 38 | + let index = 1; |
| 39 | + for(const chunk of chunks) { |
| 40 | + if(chunk.isInitial() === this.options.initialChunks) |
| 41 | + indexMap.set(chunk, index++); |
| 42 | + } |
| 43 | + // Map a list of chunks to a list of modules |
| 44 | + // For the key the chunk "index" is used, the value is a SortableSet of modules |
| 45 | + const chunksModulesMap = new Map(); |
| 46 | + // Map a list of chunks to a name (not every list of chunks is mapped, only when "name" option is used) |
| 47 | + const chunksNameMap = new Map(); |
| 48 | + // Walk through all modules |
| 49 | + for(const module of compilation.modules) { |
| 50 | + // Get indices of chunks in which this module occurs |
| 51 | + const chunkIndices = Array.from(module.chunksIterable, chunk => indexMap.get(chunk)).filter(Boolean); |
| 52 | + // Get name from "name" option |
| 53 | + let name = this.options.name; |
| 54 | + if(typeof name === "function") |
| 55 | + name = name(module); |
| 56 | + if(name) { |
| 57 | + chunkIndices.push(`[${name}]`); |
| 58 | + } else if(this.options.onlyNamed) { |
| 59 | + // May skip unnamed chunks if "onlyNamed" is used |
| 60 | + continue; |
| 61 | + } |
| 62 | + // skip for modules which are only in one chunk or don't get a name |
| 63 | + if(chunkIndices.length <= 1) continue; |
| 64 | + // Create key for maps |
| 65 | + const key = chunkIndices.sort().join(); |
| 66 | + // Add module to maps |
| 67 | + let modules = chunksModulesMap.get(key); |
| 68 | + if(modules === undefined) { |
| 69 | + chunksModulesMap.set(key, modules = new SortableSet(undefined, sortByIdentifier)); |
| 70 | + if(name) { |
| 71 | + // Note name when used |
| 72 | + chunksNameMap.set(key, name); |
| 73 | + } |
| 74 | + } |
| 75 | + modules.add(module); |
| 76 | + } |
| 77 | + // Get size of module lists and sort them by name and size |
| 78 | + const entries = Array.from(chunksModulesMap.entries(), pair => { |
| 79 | + const modules = pair[1]; |
| 80 | + const size = Array.from(modules, m => m.size()).reduce((a, b) => a + b, 0); |
| 81 | + return { |
| 82 | + key: pair[0], |
| 83 | + modules, |
| 84 | + size |
| 85 | + }; |
| 86 | + }).sort((a, b) => { |
| 87 | + // Sort |
| 88 | + // 1. by chunk name |
| 89 | + const chunkNameA = chunksNameMap.get(a.key); |
| 90 | + const chunkNameB = chunksNameMap.get(b.key); |
| 91 | + if(chunkNameA && !chunkNameB) return -1; |
| 92 | + if(!chunkNameA && chunkNameB) return 1; |
| 93 | + if(chunkNameA && chunkNameB) { |
| 94 | + if(chunkNameA < chunkNameB) return -1; |
| 95 | + if(chunkNameA > chunkNameB) return 1; |
| 96 | + } |
| 97 | + // 2. by total modules size |
| 98 | + const diffSize = b.size - a.size; |
| 99 | + if(diffSize) return diffSize; |
| 100 | + const modulesA = a.modules; |
| 101 | + const modulesB = b.modules; |
| 102 | + // 3. by module identifiers |
| 103 | + const diff = modulesA.size - modulesB.size; |
| 104 | + if(diff) return diff; |
| 105 | + modulesA.sort(); |
| 106 | + modulesB.sort(); |
| 107 | + const aI = modulesA[Symbol.iterator](); |
| 108 | + const bI = modulesB[Symbol.iterator](); |
| 109 | + while(true) { // eslint-disable-line |
| 110 | + const aItem = aI.next(); |
| 111 | + const bItem = bI.next(); |
| 112 | + if(aItem.done) return 0; |
| 113 | + const aModuleIdentifier = aItem.value.identifier(); |
| 114 | + const bModuleIdentifier = bItem.value.identifier(); |
| 115 | + if(aModuleIdentifier > bModuleIdentifier) return -1; |
| 116 | + if(aModuleIdentifier < bModuleIdentifier) return 1; |
| 117 | + } |
| 118 | + }); |
| 119 | + |
| 120 | + let changed = false; |
| 121 | + // Walk though all entries |
| 122 | + for(const item of entries) { |
| 123 | + const chunkName = chunksNameMap.get(item.key); |
| 124 | + // Skip if size is smaller than minimum size |
| 125 | + if(!chunkName && item.size < this.options.minSize) continue; |
| 126 | + // Variable for the new chunk (lazy created) |
| 127 | + let newChunk; |
| 128 | + // Walk through all chunks |
| 129 | + // All modules have the same chunks so we can use the first module |
| 130 | + const firstModule = item.modules.values().next().value; |
| 131 | + for(const chunk of firstModule.chunksIterable) { |
| 132 | + // skip if we address ourself |
| 133 | + if(chunk.name === chunkName) continue; |
| 134 | + // only use selected chunks |
| 135 | + if(!indexMap.get(chunk)) continue; |
| 136 | + // respect max requests when not a named chunk |
| 137 | + if(!chunkName && getRequests(chunk) >= this.options.maxRequests) continue; |
| 138 | + if(newChunk === undefined) { |
| 139 | + // Create the new chunk |
| 140 | + newChunk = compilation.addChunk(chunkName); |
| 141 | + } |
| 142 | + // Add graph connections for splitted chunk |
| 143 | + chunk.split(newChunk); |
| 144 | + // Remove all selected modules from the chunk |
| 145 | + for(const module of item.modules) { |
| 146 | + chunk.removeModule(module); |
| 147 | + module.rewriteChunkInReasons(chunk, [newChunk]); |
| 148 | + } |
| 149 | + } |
| 150 | + // If we successfully creates a new chunk |
| 151 | + if(newChunk) { |
| 152 | + // If the choosen name is already an entry point we remove the entry point |
| 153 | + if(chunkName) { |
| 154 | + const entrypoint = compilation.entrypoints[chunkName]; |
| 155 | + if(entrypoint) { |
| 156 | + delete compilation.entrypoints[chunkName]; |
| 157 | + entrypoint.remove(); |
| 158 | + } |
| 159 | + } |
| 160 | + // Add a note to the chunk |
| 161 | + newChunk.chunkReason = chunkName ? "vendors chunk" : "commons chunk"; |
| 162 | + // Add all modules to the new chunk |
| 163 | + for(const module of item.modules) { |
| 164 | + newChunk.addModule(module); |
| 165 | + module.addChunk(newChunk); |
| 166 | + } |
| 167 | + changed = true; |
| 168 | + } |
| 169 | + } |
| 170 | + if(changed) return true; |
| 171 | + }); |
| 172 | + }); |
| 173 | + } |
| 174 | +}; |
0 commit comments