Skip to content

Commit 7baf6af

Browse files
authored
Merge pull request webpack#6281 from webpack/feature/auto-ccp
Add AutomaticCommonsChunksPlugin and config options and defaults
2 parents 40bfc77 + c784a96 commit 7baf6af

File tree

29 files changed

+632
-32
lines changed

29 files changed

+632
-32
lines changed

lib/Chunk.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,14 @@ class Chunk {
259259
return false;
260260
}
261261

262+
removeEntrypoint(entrypoint) {
263+
if(!this._entrypoints.has(entrypoint)) {
264+
this._entrypoints.remove(entrypoint);
265+
return true;
266+
}
267+
return false;
268+
}
269+
262270
removeModule(module) {
263271
if(this._modules.delete(module)) {
264272
module.removeChunk(this);
@@ -492,6 +500,8 @@ class Chunk {
492500
newChunk._parents.add(parentChunk);
493501
}
494502
for(const entrypoint of this._entrypoints) {
503+
if(entrypoint.getRuntimeChunk() === this)
504+
entrypoint.setRuntimeChunk(this);
495505
entrypoint.insertChunk(newChunk, this);
496506
}
497507
}

lib/Entrypoint.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ class Entrypoint {
88
constructor(name) {
99
this.name = name;
1010
this.chunks = [];
11+
this.runtimeChunk = undefined;
1112
}
1213

1314
unshiftChunk(chunk) {
@@ -35,6 +36,13 @@ class Entrypoint {
3536
}
3637
}
3738

39+
remove() {
40+
for(const chunk of this.chunks) {
41+
chunk.removeEntrypoint(this);
42+
}
43+
this.chunks.length = 0;
44+
}
45+
3846
getFiles() {
3947
const files = new Set();
4048

@@ -47,8 +55,12 @@ class Entrypoint {
4755
return Array.from(files);
4856
}
4957

58+
setRuntimeChunk(chunk) {
59+
this.runtimeChunk = chunk;
60+
}
61+
5062
getRuntimeChunk() {
51-
return this.chunks[0];
63+
return this.runtimeChunk || this.chunks[0];
5264
}
5365
}
5466

lib/Stats.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ class Stats {
412412
initial: chunk.isInitial(),
413413
entry: chunk.hasRuntime(),
414414
recorded: chunk.recorded,
415-
extraAsync: !!chunk.extraAsync,
415+
reason: chunk.chunkReason,
416416
size: chunk.modulesSize(),
417417
names: chunk.name ? [chunk.name] : [],
418418
files: chunk.files.slice(),
@@ -882,6 +882,9 @@ class Stats {
882882
if(chunk.recorded) {
883883
colors.green(" [recorded]");
884884
}
885+
if(chunk.reason) {
886+
colors.yellow(` ${chunk.reason}`);
887+
}
885888
newline();
886889
if(chunk.origins) {
887890
chunk.origins.forEach(origin => {

lib/WebpackOptionsApply.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const SideEffectsFlagPlugin = require("./optimize/SideEffectsFlagPlugin");
5151
const FlagDependencyUsagePlugin = require("./FlagDependencyUsagePlugin");
5252
const FlagDependencyExportsPlugin = require("./FlagDependencyExportsPlugin");
5353
const ModuleConcatenationPlugin = require("./optimize/ModuleConcatenationPlugin");
54+
const AutomaticCommonsChunksPlugin = require("./optimize/AutomaticCommonsChunksPlugin");
5455
const NoEmitOnErrorsPlugin = require("./NoEmitOnErrorsPlugin");
5556
const NamedModulesPlugin = require("./NamedModulesPlugin");
5657
const NamedChunksPlugin = require("./NamedChunksPlugin");
@@ -286,6 +287,53 @@ class WebpackOptionsApply extends OptionsApply {
286287
new FlagDependencyUsagePlugin().apply(compiler);
287288
if(options.optimization.concatenateModules)
288289
new ModuleConcatenationPlugin().apply(compiler);
290+
if(options.optimization.asyncCommonsChunks) {
291+
const accpOptions = Object.assign({}, options.optimization.asyncCommonsChunks, {
292+
initialChunks: false
293+
});
294+
new AutomaticCommonsChunksPlugin(accpOptions).apply(compiler);
295+
}
296+
if(options.optimization.initialCommonsChunks || options.optimization.initialVendorsChunk) {
297+
let nameOption = options.optimization.initialVendorsChunk;
298+
if(nameOption === true) {
299+
nameOption = {
300+
vendors: /[\\/]node_modules[\\/]/
301+
};
302+
}
303+
if(typeof nameOption === "string") {
304+
nameOption = {
305+
[nameOption]: /[\\/]node_modules[\\/]/
306+
};
307+
}
308+
if(nameOption && typeof nameOption === "object") {
309+
nameOption = ((nameOption) => ((module, compilation) => {
310+
if(!module.nameForCondition) return;
311+
const name = module.nameForCondition();
312+
for(const chunkName of Object.keys(nameOption)) {
313+
const regExp = nameOption[chunkName];
314+
if(typeof regExp === "string") {
315+
if(name.startsWith(regExp))
316+
return chunkName;
317+
} else if(typeof regExp === "function") {
318+
const result = regExp(name);
319+
if(typeof result === "string")
320+
return result;
321+
else if(result)
322+
return chunkName;
323+
} else {
324+
if(regExp.test(name))
325+
return chunkName;
326+
}
327+
}
328+
}))(nameOption);
329+
}
330+
const accpOptions = Object.assign({}, options.optimization.initialCommonsChunks, {
331+
initialChunks: true,
332+
onlyNamed: !options.optimization.initialCommonsChunks,
333+
name: nameOption
334+
});
335+
new AutomaticCommonsChunksPlugin(accpOptions).apply(compiler);
336+
}
289337
if(options.optimization.noEmitOnErrors)
290338
new NoEmitOnErrorsPlugin().apply(compiler);
291339
if(options.optimization.namedModules)

lib/WebpackOptionsDefaulter.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,9 @@ class WebpackOptionsDefaulter extends OptionsDefaulter {
164164
this.set("optimization.providedExports", true);
165165
this.set("optimization.usedExports", "make", options => isProductionLikeMode(options));
166166
this.set("optimization.concatenateModules", "make", options => isProductionLikeMode(options));
167+
this.set("optimization.asyncCommonsChunks", true);
168+
this.set("optimization.initialCommonsChunks", false);
169+
this.set("optimization.initialVendorsChunk", false);
167170
this.set("optimization.noEmitOnErrors", "make", options => isProductionLikeMode(options));
168171
this.set("optimization.namedModules", "make", options => options.mode === "development");
169172
this.set("optimization.namedChunks", "make", options => options.mode === "development");
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
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+
};

lib/optimize/MergeDuplicateChunksPlugin.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class MergeDuplicateChunksPlugin {
4747
// when we found duplicates
4848
if(possibleDuplicates !== undefined && possibleDuplicates.size > 0) {
4949
for(const otherChunk of possibleDuplicates) {
50+
if(otherChunk.isInitial() !== chunk.isInitial()) continue;
5051
// merge them
5152
if(chunk.integrate(otherChunk, "duplicate"))
5253
chunks.splice(chunks.indexOf(otherChunk), 1);

lib/webpack.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ exportPlugins(exports, {
106106
exportPlugins(exports.optimize = {}, {
107107
"AggressiveMergingPlugin": () => require("./optimize/AggressiveMergingPlugin"),
108108
"AggressiveSplittingPlugin": () => require("./optimize/AggressiveSplittingPlugin"),
109+
"AutomaticCommonsChunksPlugin": () => require("./optimize/AutomaticCommonsChunksPlugin"),
109110
"CommonsChunkPlugin": () => require("./optimize/CommonsChunkPlugin"),
110111
"ChunkModuleIdRangePlugin": () => require("./optimize/ChunkModuleIdRangePlugin"),
111112
"LimitChunkCountPlugin": () => require("./optimize/LimitChunkCountPlugin"),

0 commit comments

Comments
 (0)