CommonsChunkPlugin.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. let nextIdent = 0;
  7. class CommonsChunkPlugin {
  8. constructor(options) {
  9. if(arguments.length > 1) {
  10. throw new Error(`Deprecation notice: CommonsChunkPlugin now only takes a single argument. Either an options
  11. object *or* the name of the chunk.
  12. Example: if your old code looked like this:
  13. new webpack.optimize.CommonsChunkPlugin('vendor', 'vendor.bundle.js')
  14. You would change it to:
  15. new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', filename: 'vendor.bundle.js' })
  16. The available options are:
  17. name: string
  18. names: string[]
  19. filename: string
  20. minChunks: number
  21. chunks: string[]
  22. children: boolean
  23. async: boolean
  24. minSize: number`);
  25. }
  26. const normalizedOptions = this.normalizeOptions(options);
  27. this.chunkNames = normalizedOptions.chunkNames;
  28. this.filenameTemplate = normalizedOptions.filenameTemplate;
  29. this.minChunks = normalizedOptions.minChunks;
  30. this.selectedChunks = normalizedOptions.selectedChunks;
  31. this.children = normalizedOptions.children;
  32. this.async = normalizedOptions.async;
  33. this.minSize = normalizedOptions.minSize;
  34. this.ident = __filename + (nextIdent++);
  35. }
  36. normalizeOptions(options) {
  37. if(Array.isArray(options)) {
  38. return {
  39. chunkNames: options,
  40. };
  41. }
  42. if(typeof options === "string") {
  43. return {
  44. chunkNames: [options],
  45. };
  46. }
  47. // options.children and options.chunk may not be used together
  48. if(options.children && options.chunks) {
  49. throw new Error("You can't and it does not make any sense to use \"children\" and \"chunk\" options together.");
  50. }
  51. /**
  52. * options.async and options.filename are also not possible together
  53. * as filename specifies how the chunk is called but "async" implies
  54. * that webpack will take care of loading this file.
  55. */
  56. if(options.async && options.filename) {
  57. throw new Error(`You can not specify a filename if you use the \"async\" option.
  58. You can however specify the name of the async chunk by passing the desired string as the \"async\" option.`);
  59. }
  60. /**
  61. * Make sure this is either an array or undefined.
  62. * "name" can be a string and
  63. * "names" a string or an array
  64. */
  65. const chunkNames = options.name || options.names ? [].concat(options.name || options.names) : undefined;
  66. return {
  67. chunkNames: chunkNames,
  68. filenameTemplate: options.filename,
  69. minChunks: options.minChunks,
  70. selectedChunks: options.chunks,
  71. children: options.children,
  72. async: options.async,
  73. minSize: options.minSize
  74. };
  75. }
  76. apply(compiler) {
  77. compiler.plugin("this-compilation", (compilation) => {
  78. compilation.plugin(["optimize-chunks", "optimize-extracted-chunks"], (chunks) => {
  79. // only optimize once
  80. if(compilation[this.ident]) return;
  81. compilation[this.ident] = true;
  82. /**
  83. * Creates a list of "common"" chunks based on the options.
  84. * The list is made up of preexisting or newly created chunks.
  85. * - If chunk has the name as specified in the chunkNames it is put in the list
  86. * - If no chunk with the name as given in chunkNames exists a new chunk is created and added to the list
  87. *
  88. * These chunks are the "targets" for extracted modules.
  89. */
  90. const targetChunks = this.getTargetChunks(chunks, compilation, this.chunkNames, this.children, this.async);
  91. // iterate over all our new chunks
  92. targetChunks.forEach((targetChunk, idx) => {
  93. /**
  94. * These chunks are subject to get "common" modules extracted and moved to the common chunk
  95. */
  96. const affectedChunks = this.getAffectedChunks(compilation, chunks, targetChunk, targetChunks, idx, this.selectedChunks, this.async, this.children);
  97. // bail if no chunk is affected
  98. if(!affectedChunks) {
  99. return;
  100. }
  101. // If we are async create an async chunk now
  102. // override the "commonChunk" with the newly created async one and use it as commonChunk from now on
  103. let asyncChunk;
  104. if(this.async) {
  105. asyncChunk = this.createAsyncChunk(compilation, this.async, targetChunk);
  106. targetChunk = asyncChunk;
  107. }
  108. /**
  109. * Check which modules are "common" and could be extracted to a "common" chunk
  110. */
  111. const extractableModules = this.getExtractableModules(this.minChunks, affectedChunks, targetChunk);
  112. // If the minSize option is set check if the size extracted from the chunk is reached
  113. // else bail out here.
  114. // As all modules/commons are interlinked with each other, common modules would be extracted
  115. // if we reach this mark at a later common chunk. (quirky I guess).
  116. if(this.minSize) {
  117. const modulesSize = this.calculateModulesSize(extractableModules);
  118. // if too small, bail
  119. if(modulesSize < this.minSize)
  120. return;
  121. }
  122. // Remove modules that are moved to commons chunk from their original chunks
  123. // return all chunks that are affected by having modules removed - we need them later (apparently)
  124. const chunksWithExtractedModules = this.extractModulesAndReturnAffectedChunks(extractableModules, affectedChunks);
  125. // connect all extracted modules with the common chunk
  126. this.addExtractedModulesToTargetChunk(targetChunk, extractableModules);
  127. // set filenameTemplate for chunk
  128. if(this.filenameTemplate)
  129. targetChunk.filenameTemplate = this.filenameTemplate;
  130. // if we are async connect the blocks of the "reallyUsedChunk" - the ones that had modules removed -
  131. // with the commonChunk and get the origins for the asyncChunk (remember "asyncChunk === commonChunk" at this moment).
  132. // bail out
  133. if(this.async) {
  134. this.moveExtractedChunkBlocksToTargetChunk(chunksWithExtractedModules, targetChunk);
  135. asyncChunk.origins = this.extractOriginsOfChunksWithExtractedModules(chunksWithExtractedModules);
  136. return;
  137. }
  138. // we are not in "async" mode
  139. // connect used chunks with commonChunk - shouldnt this be reallyUsedChunks here?
  140. this.makeTargetChunkParentOfAffectedChunks(affectedChunks, targetChunk);
  141. });
  142. return true;
  143. });
  144. });
  145. }
  146. getTargetChunks(allChunks, compilation, chunkNames, children, asyncOption) {
  147. const asyncOrNoSelectedChunk = children || asyncOption;
  148. // we have specified chunk names
  149. if(chunkNames) {
  150. // map chunks by chunkName for quick access
  151. const allChunksNameMap = allChunks.reduce((map, chunk) => {
  152. if(chunk.name) {
  153. map.set(chunk.name, chunk);
  154. }
  155. return map;
  156. }, new Map());
  157. // Ensure we have a chunk per specified chunk name.
  158. // Reuse existing chunks if possible
  159. return chunkNames.map(chunkName => {
  160. if(allChunksNameMap.has(chunkName)) {
  161. return allChunksNameMap.get(chunkName);
  162. }
  163. // add the filtered chunks to the compilation
  164. return compilation.addChunk(chunkName);
  165. });
  166. }
  167. // we dont have named chunks specified, so we just take all of them
  168. if(asyncOrNoSelectedChunk) {
  169. return allChunks.filter(chunk => !chunk.isInitial());
  170. }
  171. /**
  172. * No chunk name(s) was specified nor is this an async/children commons chunk
  173. */
  174. throw new Error(`You did not specify any valid target chunk settings.
  175. Take a look at the "name"/"names" or async/children option.`);
  176. }
  177. getAffectedChunks(compilation, allChunks, targetChunk, targetChunks, currentIndex, selectedChunks, asyncOption, children) {
  178. const asyncOrNoSelectedChunk = children || asyncOption;
  179. if(Array.isArray(selectedChunks)) {
  180. return allChunks.filter(chunk => {
  181. const notCommmonChunk = chunk !== targetChunk;
  182. const isSelectedChunk = selectedChunks.indexOf(chunk.name) > -1;
  183. return notCommmonChunk && isSelectedChunk;
  184. });
  185. }
  186. if(asyncOrNoSelectedChunk) {
  187. // nothing to do here
  188. if(!targetChunk.chunks) {
  189. return [];
  190. }
  191. return targetChunk.chunks.filter((chunk) => {
  192. // we can only move modules from this chunk if the "commonChunk" is the only parent
  193. return asyncOption || chunk.parents.length === 1;
  194. });
  195. }
  196. /**
  197. * past this point only entry chunks are allowed to become commonChunks
  198. */
  199. if(targetChunk.parents.length > 0) {
  200. compilation.errors.push(new Error("CommonsChunkPlugin: While running in normal mode it's not allowed to use a non-entry chunk (" + targetChunk.name + ")"));
  201. return;
  202. }
  203. /**
  204. * If we find a "targetchunk" that is also a normal chunk (meaning it is probably specified as an entry)
  205. * and the current target chunk comes after that and the found chunk has a runtime*
  206. * make that chunk be an 'affected' chunk of the current target chunk.
  207. *
  208. * To understand what that means take a look at the "examples/chunkhash", this basically will
  209. * result in the runtime to be extracted to the current target chunk.
  210. *
  211. * *runtime: the "runtime" is the "webpack"-block you may have seen in the bundles that resolves modules etc.
  212. */
  213. return allChunks.filter((chunk) => {
  214. const found = targetChunks.indexOf(chunk);
  215. if(found >= currentIndex) return false;
  216. return chunk.hasRuntime();
  217. });
  218. }
  219. createAsyncChunk(compilation, asyncOption, targetChunk) {
  220. const asyncChunk = compilation.addChunk(typeof asyncOption === "string" ? asyncOption : undefined);
  221. asyncChunk.chunkReason = "async commons chunk";
  222. asyncChunk.extraAsync = true;
  223. asyncChunk.addParent(targetChunk);
  224. targetChunk.addChunk(asyncChunk);
  225. return asyncChunk;
  226. }
  227. // If minChunks is a function use that
  228. // otherwhise check if a module is used at least minChunks or 2 or usedChunks.length time
  229. getModuleFilter(minChunks, targetChunk, usedChunksLength) {
  230. if(typeof minChunks === "function") {
  231. return minChunks;
  232. }
  233. const minCount = (minChunks || Math.max(2, usedChunksLength));
  234. const isUsedAtLeastMinTimes = (module, count) => count >= minCount;
  235. return isUsedAtLeastMinTimes;
  236. }
  237. getExtractableModules(minChunks, usedChunks, targetChunk) {
  238. if(minChunks === Infinity) {
  239. return [];
  240. }
  241. // count how many chunks contain a module
  242. const commonModulesToCountMap = usedChunks.reduce((map, chunk) => {
  243. for(let module of chunk.modules) {
  244. const count = map.has(module) ? map.get(module) : 0;
  245. map.set(module, count + 1);
  246. }
  247. return map;
  248. }, new Map());
  249. // filter by minChunks
  250. const moduleFilterCount = this.getModuleFilter(minChunks, targetChunk, usedChunks.length);
  251. // filter by condition
  252. const moduleFilterCondition = (module, chunk) => {
  253. if(!module.chunkCondition) {
  254. return true;
  255. }
  256. return module.chunkCondition(chunk);
  257. };
  258. return Array.from(commonModulesToCountMap).filter(entry => {
  259. const module = entry[0];
  260. const count = entry[1];
  261. // if the module passes both filters, keep it.
  262. return moduleFilterCount(module, count) && moduleFilterCondition(module, targetChunk);
  263. }).map(entry => entry[0]);
  264. }
  265. calculateModulesSize(modules) {
  266. return modules.reduce((totalSize, module) => totalSize + module.size(), 0);
  267. }
  268. extractModulesAndReturnAffectedChunks(reallyUsedModules, usedChunks) {
  269. return reallyUsedModules.reduce((affectedChunksSet, module) => {
  270. for(let chunk of usedChunks) {
  271. // removeChunk returns true if the chunk was contained and succesfully removed
  272. // false if the module did not have a connection to the chunk in question
  273. if(module.removeChunk(chunk)) {
  274. affectedChunksSet.add(chunk);
  275. }
  276. }
  277. return affectedChunksSet;
  278. }, new Set());
  279. }
  280. addExtractedModulesToTargetChunk(chunk, modules) {
  281. for(let module of modules) {
  282. chunk.addModule(module);
  283. module.addChunk(chunk);
  284. }
  285. }
  286. makeTargetChunkParentOfAffectedChunks(usedChunks, commonChunk) {
  287. for(let chunk of usedChunks) {
  288. // set commonChunk as new sole parent
  289. chunk.parents = [commonChunk];
  290. // add chunk to commonChunk
  291. commonChunk.addChunk(chunk);
  292. for(let entrypoint of chunk.entrypoints) {
  293. entrypoint.insertChunk(commonChunk, chunk);
  294. }
  295. }
  296. }
  297. moveExtractedChunkBlocksToTargetChunk(chunks, targetChunk) {
  298. for(let chunk of chunks) {
  299. for(let block of chunk.blocks) {
  300. block.chunks.unshift(targetChunk);
  301. targetChunk.addBlock(block);
  302. }
  303. }
  304. }
  305. extractOriginsOfChunksWithExtractedModules(chunks) {
  306. const origins = [];
  307. for(let chunk of chunks) {
  308. for(let origin of chunk.origins) {
  309. const newOrigin = Object.create(origin);
  310. newOrigin.reasons = (origin.reasons || []).concat("async commons");
  311. origins.push(newOrigin);
  312. }
  313. }
  314. return origins;
  315. }
  316. }
  317. module.exports = CommonsChunkPlugin;