Skip to content

Commit c3b363d

Browse files
sokraevilebottnawi
authored andcommitted
fix(order): use correct order when multiple chunk groups are merged (#246)
1 parent 3d12bc7 commit c3b363d

File tree

17 files changed

+167
-10
lines changed

17 files changed

+167
-10
lines changed

src/index.js

+88-10
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ class MiniCssExtractPlugin {
167167
result.push({
168168
render: () =>
169169
this.renderContentAsset(
170+
compilation,
170171
chunk,
171172
renderedModules,
172173
compilation.runtimeTemplate.requestShortener
@@ -192,6 +193,7 @@ class MiniCssExtractPlugin {
192193
result.push({
193194
render: () =>
194195
this.renderContentAsset(
196+
compilation,
195197
chunk,
196198
renderedModules,
197199
compilation.runtimeTemplate.requestShortener
@@ -381,27 +383,103 @@ class MiniCssExtractPlugin {
381383
return obj;
382384
}
383385

384-
renderContentAsset(chunk, modules, requestShortener) {
385-
// get first chunk group and take ordr from this one
386-
// When a chunk is shared between multiple chunk groups
387-
// with different order this can lead to wrong order
388-
// but it's not possible to create a correct order in
389-
// this case. Don't share chunks if you don't like it.
386+
renderContentAsset(compilation, chunk, modules, requestShortener) {
387+
let usedModules;
388+
390389
const [chunkGroup] = chunk.groupsIterable;
391390
if (typeof chunkGroup.getModuleIndex2 === 'function') {
392-
modules.sort(
393-
(a, b) => chunkGroup.getModuleIndex2(a) - chunkGroup.getModuleIndex2(b)
394-
);
391+
// Store dependencies for modules
392+
const moduleDependencies = new Map(modules.map((m) => [m, new Set()]));
393+
394+
// Get ordered list of modules per chunk group
395+
// This loop also gathers dependencies from the ordered lists
396+
// Lists are in reverse order to allow to use Array.pop()
397+
const modulesByChunkGroup = Array.from(chunk.groupsIterable, (cg) => {
398+
const sortedModules = modules
399+
.map((m) => {
400+
return {
401+
module: m,
402+
index: cg.getModuleIndex2(m),
403+
};
404+
})
405+
.filter((item) => item.index !== undefined)
406+
.sort((a, b) => b.index - a.index)
407+
.map((item) => item.module);
408+
for (let i = 0; i < sortedModules.length; i++) {
409+
const set = moduleDependencies.get(sortedModules[i]);
410+
for (let j = i + 1; j < sortedModules.length; j++) {
411+
set.add(sortedModules[j]);
412+
}
413+
}
414+
415+
return sortedModules;
416+
});
417+
418+
// set with already included modules in correct order
419+
usedModules = new Set();
420+
421+
const unusedModulesFilter = (m) => !usedModules.has(m);
422+
423+
while (usedModules.size < modules.length) {
424+
let success = false;
425+
let bestMatch;
426+
let bestMatchDeps;
427+
// get first module where dependencies are fulfilled
428+
for (const list of modulesByChunkGroup) {
429+
// skip and remove already added modules
430+
while (list.length > 0 && usedModules.has(list[list.length - 1]))
431+
list.pop();
432+
433+
// skip empty lists
434+
if (list.length !== 0) {
435+
const module = list[list.length - 1];
436+
const deps = moduleDependencies.get(module);
437+
// determine dependencies that are not yet included
438+
const failedDeps = Array.from(deps).filter(unusedModulesFilter);
439+
440+
// store best match for fallback behavior
441+
if (!bestMatchDeps || bestMatchDeps.length > failedDeps.length) {
442+
bestMatch = list;
443+
bestMatchDeps = failedDeps;
444+
}
445+
if (failedDeps.length === 0) {
446+
// use this module and remove it from list
447+
usedModules.add(list.pop());
448+
success = true;
449+
break;
450+
}
451+
}
452+
}
453+
454+
if (!success) {
455+
// no module found => there is a conflict
456+
// use list with fewest failed deps
457+
// and emit a warning
458+
const fallbackModule = bestMatch.pop();
459+
compilation.warnings.push(
460+
new Error(
461+
`chunk ${chunk.name || chunk.id} [mini-css-extract-plugin]\n` +
462+
'Conflicting order between:\n' +
463+
` * ${fallbackModule.readableIdentifier(requestShortener)}\n` +
464+
`${bestMatchDeps
465+
.map((m) => ` * ${m.readableIdentifier(requestShortener)}`)
466+
.join('\n')}`
467+
)
468+
);
469+
usedModules.add(fallbackModule);
470+
}
471+
}
395472
} else {
396473
// fallback for older webpack versions
397474
// (to avoid a breaking change)
398475
// TODO remove this in next mayor version
399476
// and increase minimum webpack version to 4.12.0
400477
modules.sort((a, b) => a.index2 - b.index2);
478+
usedModules = modules;
401479
}
402480
const source = new ConcatSource();
403481
const externalsSource = new ConcatSource();
404-
for (const m of modules) {
482+
for (const m of usedModules) {
405483
if (/^@import url/.test(m.content)) {
406484
// HACK for IE
407485
// http://stackoverflow.com/a/14676665/1458162

test/cases/split-chunks-single/a.css

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
body { content: "a"; }

test/cases/split-chunks-single/b.css

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
body { content: "b"; }

test/cases/split-chunks-single/c.css

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
body { content: "c"; }
+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import "./c.css";
2+
import "./d.css";
+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import "./d.css";
2+
import "./h.css";

test/cases/split-chunks-single/d.css

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
body { content: "d"; }

test/cases/split-chunks-single/e1.css

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
body { content: "e1"; }

test/cases/split-chunks-single/e2.css

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
body { content: "e2"; }
+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import './a.css';
2+
import './e1.css';
3+
import './e2.css';
4+
import './f.css';
5+
import("./chunk1");
6+
import("./chunk2");
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import './b.css';
2+
import './e2.css';
3+
import './e1.css';
4+
import './g.css';
5+
import './h.css';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
body { content: "a"; }
2+
3+
body { content: "b"; }
4+
5+
body { content: "c"; }
6+
7+
body { content: "d"; }
8+
9+
body { content: "e1"; }
10+
11+
body { content: "e2"; }
12+
13+
body { content: "f"; }
14+
15+
body { content: "g"; }
16+
17+
body { content: "h"; }
18+

test/cases/split-chunks-single/f.css

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
body { content: "f"; }

test/cases/split-chunks-single/g.css

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
body { content: "g"; }

test/cases/split-chunks-single/h.css

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
body { content: "h"; }
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
body { background: red; }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
const Self = require('../../../');
2+
3+
module.exports = {
4+
entry: {
5+
entry1: './entry1.js',
6+
entry2: './entry2.js'
7+
},
8+
module: {
9+
rules: [
10+
{
11+
test: /\.css$/,
12+
use: [
13+
Self.loader,
14+
'css-loader',
15+
],
16+
},
17+
],
18+
},
19+
optimization: {
20+
splitChunks: {
21+
cacheGroups: {
22+
styles: {
23+
name: "styles",
24+
chunks: 'all',
25+
test: /\.css$/,
26+
enforce: true
27+
}
28+
}
29+
}
30+
},
31+
plugins: [
32+
new Self({
33+
filename: '[name].css',
34+
}),
35+
],
36+
};

0 commit comments

Comments
 (0)