CommonsChunkPlugin.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  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. // If async chunk is one of the affected chunks, just use it
  106. asyncChunk = affectedChunks.filter(c => c.name === this.async)[0];
  107. // Elsewise create a new one
  108. if(!asyncChunk) {
  109. asyncChunk = this.createAsyncChunk(
  110. compilation,
  111. targetChunks.length <= 1 || typeof this.async !== "string" ? this.async :
  112. targetChunk.name ? `${this.async}-${targetChunk.name}` :
  113. true,
  114. targetChunk
  115. );
  116. }
  117. targetChunk = asyncChunk;
  118. }
  119. /**
  120. * Check which modules are "common" and could be extracted to a "common" chunk
  121. */
  122. const extractableModules = this.getExtractableModules(this.minChunks, affectedChunks, targetChunk);
  123. // If the minSize option is set check if the size extracted from the chunk is reached
  124. // else bail out here.
  125. // As all modules/commons are interlinked with each other, common modules would be extracted
  126. // if we reach this mark at a later common chunk. (quirky I guess).
  127. if(this.minSize) {
  128. const modulesSize = this.calculateModulesSize(extractableModules);
  129. // if too small, bail
  130. if(modulesSize < this.minSize)
  131. return;
  132. }
  133. // Remove modules that are moved to commons chunk from their original chunks
  134. // return all chunks that are affected by having modules removed - we need them later (apparently)
  135. const chunksWithExtractedModules = this.extractModulesAndReturnAffectedChunks(extractableModules, affectedChunks);
  136. // connect all extracted modules with the common chunk
  137. this.addExtractedModulesToTargetChunk(targetChunk, extractableModules);
  138. // set filenameTemplate for chunk
  139. if(this.filenameTemplate)
  140. targetChunk.filenameTemplate = this.filenameTemplate;
  141. // if we are async connect the blocks of the "reallyUsedChunk" - the ones that had modules removed -
  142. // with the commonChunk and get the origins for the asyncChunk (remember "asyncChunk === commonChunk" at this moment).
  143. // bail out
  144. if(this.async) {
  145. this.moveExtractedChunkBlocksToTargetChunk(chunksWithExtractedModules, targetChunk);
  146. asyncChunk.origins = this.extractOriginsOfChunksWithExtractedModules(chunksWithExtractedModules);
  147. return;
  148. }
  149. // we are not in "async" mode
  150. // connect used chunks with commonChunk - shouldnt this be reallyUsedChunks here?
  151. this.makeTargetChunkParentOfAffectedChunks(affectedChunks, targetChunk);
  152. });
  153. return true;
  154. });
  155. });
  156. }
  157. getTargetChunks(allChunks, compilation, chunkNames, children, asyncOption) {
  158. const asyncOrNoSelectedChunk = children || asyncOption;
  159. // we have specified chunk names
  160. if(chunkNames) {
  161. // map chunks by chunkName for quick access
  162. const allChunksNameMap = allChunks.reduce((map, chunk) => {
  163. if(chunk.name) {
  164. map.set(chunk.name, chunk);
  165. }
  166. return map;
  167. }, new Map());
  168. // Ensure we have a chunk per specified chunk name.
  169. // Reuse existing chunks if possible
  170. return chunkNames.map(chunkName => {
  171. if(allChunksNameMap.has(chunkName)) {
  172. return allChunksNameMap.get(chunkName);
  173. }
  174. // add the filtered chunks to the compilation
  175. return compilation.addChunk(chunkName);
  176. });
  177. }
  178. // we dont have named chunks specified, so we just take all of them
  179. if(asyncOrNoSelectedChunk) {
  180. return allChunks;
  181. }
  182. /**
  183. * No chunk name(s) was specified nor is this an async/children commons chunk
  184. */
  185. throw new Error(`You did not specify any valid target chunk settings.
  186. Take a look at the "name"/"names" or async/children option.`);
  187. }
  188. getAffectedChunks(compilation, allChunks, targetChunk, targetChunks, currentIndex, selectedChunks, asyncOption, children) {
  189. const asyncOrNoSelectedChunk = children || asyncOption;
  190. if(Array.isArray(selectedChunks)) {
  191. return allChunks.filter(chunk => {
  192. const notCommmonChunk = chunk !== targetChunk;
  193. const isSelectedChunk = selectedChunks.indexOf(chunk.name) > -1;
  194. return notCommmonChunk && isSelectedChunk;
  195. });
  196. }
  197. if(asyncOrNoSelectedChunk) {
  198. // nothing to do here
  199. if(!targetChunk.chunks) {
  200. return [];
  201. }
  202. return targetChunk.chunks.filter((chunk) => {
  203. // we only are interested in on-demand chunks
  204. if(chunk.isInitial())
  205. return false;
  206. // we can only move modules from this chunk if the "commonChunk" is the only parent
  207. if(!asyncOption)
  208. return chunk.parents.length === 1;
  209. return true;
  210. });
  211. }
  212. /**
  213. * past this point only entry chunks are allowed to become commonChunks
  214. */
  215. if(targetChunk.parents.length > 0) {
  216. compilation.errors.push(new Error("CommonsChunkPlugin: While running in normal mode it's not allowed to use a non-entry chunk (" + targetChunk.name + ")"));
  217. return;
  218. }
  219. /**
  220. * If we find a "targetchunk" that is also a normal chunk (meaning it is probably specified as an entry)
  221. * and the current target chunk comes after that and the found chunk has a runtime*
  222. * make that chunk be an 'affected' chunk of the current target chunk.
  223. *
  224. * To understand what that means take a look at the "examples/chunkhash", this basically will
  225. * result in the runtime to be extracted to the current target chunk.
  226. *
  227. * *runtime: the "runtime" is the "webpack"-block you may have seen in the bundles that resolves modules etc.
  228. */
  229. return allChunks.filter((chunk) => {
  230. const found = targetChunks.indexOf(chunk);
  231. if(found >= currentIndex) return false;
  232. return chunk.hasRuntime();
  233. });
  234. }
  235. createAsyncChunk(compilation, asyncOption, targetChunk) {
  236. const asyncChunk = compilation.addChunk(typeof asyncOption === "string" ? asyncOption : undefined);
  237. asyncChunk.chunkReason = "async commons chunk";
  238. asyncChunk.extraAsync = true;
  239. asyncChunk.addParent(targetChunk);
  240. targetChunk.addChunk(asyncChunk);
  241. return asyncChunk;
  242. }
  243. // If minChunks is a function use that
  244. // otherwhise check if a module is used at least minChunks or 2 or usedChunks.length time
  245. getModuleFilter(minChunks, targetChunk, usedChunksLength) {
  246. if(typeof minChunks === "function") {
  247. return minChunks;
  248. }
  249. const minCount = (minChunks || Math.max(2, usedChunksLength));
  250. const isUsedAtLeastMinTimes = (module, count) => count >= minCount;
  251. return isUsedAtLeastMinTimes;
  252. }
  253. getExtractableModules(minChunks, usedChunks, targetChunk) {
  254. if(minChunks === Infinity) {
  255. return [];
  256. }
  257. // count how many chunks contain a module
  258. const commonModulesToCountMap = usedChunks.reduce((map, chunk) => {
  259. for(const module of chunk.modulesIterable) {
  260. const count = map.has(module) ? map.get(module) : 0;
  261. map.set(module, count + 1);
  262. }
  263. return map;
  264. }, new Map());
  265. // filter by minChunks
  266. const moduleFilterCount = this.getModuleFilter(minChunks, targetChunk, usedChunks.length);
  267. // filter by condition
  268. const moduleFilterCondition = (module, chunk) => {
  269. if(!module.chunkCondition) {
  270. return true;
  271. }
  272. return module.chunkCondition(chunk);
  273. };
  274. return Array.from(commonModulesToCountMap).filter(entry => {
  275. const module = entry[0];
  276. const count = entry[1];
  277. // if the module passes both filters, keep it.
  278. return moduleFilterCount(module, count) && moduleFilterCondition(module, targetChunk);
  279. }).map(entry => entry[0]);
  280. }
  281. calculateModulesSize(modules) {
  282. return modules.reduce((totalSize, module) => totalSize + module.size(), 0);
  283. }
  284. extractModulesAndReturnAffectedChunks(reallyUsedModules, usedChunks) {
  285. return reallyUsedModules.reduce((affectedChunksSet, module) => {
  286. for(const chunk of usedChunks) {
  287. // removeChunk returns true if the chunk was contained and succesfully removed
  288. // false if the module did not have a connection to the chunk in question
  289. if(module.removeChunk(chunk)) {
  290. affectedChunksSet.add(chunk);
  291. }
  292. }
  293. return affectedChunksSet;
  294. }, new Set());
  295. }
  296. addExtractedModulesToTargetChunk(chunk, modules) {
  297. for(const module of modules) {
  298. chunk.addModule(module);
  299. module.addChunk(chunk);
  300. }
  301. }
  302. makeTargetChunkParentOfAffectedChunks(usedChunks, commonChunk) {
  303. for(const chunk of usedChunks) {
  304. // set commonChunk as new sole parent
  305. chunk.parents = [commonChunk];
  306. // add chunk to commonChunk
  307. commonChunk.addChunk(chunk);
  308. for(const entrypoint of chunk.entrypoints) {
  309. entrypoint.insertChunk(commonChunk, chunk);
  310. }
  311. }
  312. }
  313. moveExtractedChunkBlocksToTargetChunk(chunks, targetChunk) {
  314. for(const chunk of chunks) {
  315. if(chunk === targetChunk) continue;
  316. for(const block of chunk.blocks) {
  317. if(block.chunks.indexOf(targetChunk) === -1) {
  318. block.chunks.unshift(targetChunk);
  319. }
  320. targetChunk.addBlock(block);
  321. }
  322. }
  323. }
  324. extractOriginsOfChunksWithExtractedModules(chunks) {
  325. const origins = [];
  326. for(const chunk of chunks) {
  327. for(const origin of chunk.origins) {
  328. const newOrigin = Object.create(origin);
  329. newOrigin.reasons = (origin.reasons || []).concat("async commons");
  330. origins.push(newOrigin);
  331. }
  332. }
  333. return origins;
  334. }
  335. }
  336. module.exports = CommonsChunkPlugin;