CommonsChunkPlugin.js
14.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
/*
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.deepChildren = normalizedOptions.deepChildren;
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,
deepChildren: options.deepChildren,
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, this.deepChildren);
// 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) {
// If async chunk is one of the affected chunks, just use it
asyncChunk = affectedChunks.filter(c => c.name === this.async)[0];
// Elsewise create a new one
if(!asyncChunk) {
asyncChunk = this.createAsyncChunk(
compilation,
targetChunks.length <= 1 || typeof this.async !== "string" ? this.async :
targetChunk.name ? `${this.async}-${targetChunk.name}` :
true,
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;
}
/**
* 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.`);
}
getAffectedUnnamedChunks(affectedChunks, targetChunk, rootChunk, asyncOption, deepChildrenOption) {
let chunks = targetChunk.chunks;
chunks && chunks.forEach((chunk) => {
if(chunk.isInitial()) {
return;
}
// If all the parents of a chunk are either
// a) the target chunk we started with
// b) themselves affected chunks
// we can assume that this chunk is an affected chunk too, as there is no way a chunk that
// isn't only depending on the target chunk is a parent of the chunk tested
if(asyncOption || chunk.parents.every((parentChunk) => parentChunk === rootChunk || affectedChunks.has(parentChunk))) {
// This check not only dedupes the affectedChunks but also guarantees we avoid endless loops
if(!affectedChunks.has(chunk)) {
// We mutate the affected chunks before going deeper, so the deeper levels and other branches
// have the information of this chunk being affected for their assertion if a chunk should
// not be affected
affectedChunks.add(chunk);
// We recurse down to all the children of the chunk, applying the same assumption.
// This guarantees that if a chunk should be an affected chunk,
// at the latest the last connection to the same chunk meets the
// condition to add it to the affected chunks.
if(deepChildrenOption === true) {
this.getAffectedUnnamedChunks(affectedChunks, chunk, rootChunk, asyncOption, deepChildrenOption);
}
}
}
});
}
getAffectedChunks(compilation, allChunks, targetChunk, targetChunks, currentIndex, selectedChunks, asyncOption, childrenOption, deepChildrenOption) {
const asyncOrNoSelectedChunk = childrenOption || 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) {
let affectedChunks = new Set();
this.getAffectedUnnamedChunks(affectedChunks, targetChunk, targetChunk, asyncOption, deepChildrenOption);
return Array.from(affectedChunks);
}
/**
* 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(const module of chunk.modulesIterable) {
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(const 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(const module of modules) {
chunk.addModule(module);
module.addChunk(chunk);
}
}
makeTargetChunkParentOfAffectedChunks(usedChunks, commonChunk) {
for(const chunk of usedChunks) {
// set commonChunk as new sole parent
chunk.parents = [commonChunk];
// add chunk to commonChunk
commonChunk.addChunk(chunk);
for(const entrypoint of chunk.entrypoints) {
entrypoint.insertChunk(commonChunk, chunk);
}
}
}
moveExtractedChunkBlocksToTargetChunk(chunks, targetChunk) {
for(const chunk of chunks) {
if(chunk === targetChunk) continue;
for(const block of chunk.blocks) {
if(block.chunks.indexOf(targetChunk) === -1) {
block.chunks.unshift(targetChunk);
}
targetChunk.addBlock(block);
}
}
}
extractOriginsOfChunksWithExtractedModules(chunks) {
const origins = [];
for(const chunk of chunks) {
for(const origin of chunk.origins) {
const newOrigin = Object.create(origin);
newOrigin.reasons = (origin.reasons || []).concat("async commons");
origins.push(newOrigin);
}
}
return origins;
}
}
module.exports = CommonsChunkPlugin;