Skip to content

Commit 2e242d2

Browse files
Fix modules hmr (#101)
* fix(src): CSS Modules HMR [PATCH] Short term solution to get css modules to hot reload for everyone. fix #80, #77 * style: Linting and generalized updates basic updates to code, linting, new yarn lock and so on * placeholder changed at version * fix(order): use correct order when multiple chunk groups are merged
1 parent a364370 commit 2e242d2

File tree

6 files changed

+134
-50
lines changed

6 files changed

+134
-50
lines changed

.eslintrc.js

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ module.exports = {
1919
'no-restricted-syntax': 0,
2020
'prefer-arrow-callback': 0,
2121
'prefer-destructuring': 0,
22+
'array-callback-return': 0,
2223
'prefer-template': 0,
2324
'class-methods-use-this': 0
2425
}

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "extract-css-chunks-webpack-plugin",
3-
"version": "3.1.0-beta.1",
3+
"version": "0.0.0-placeholder",
44
"author": "James Gillmore <james@faceyspacey.com>",
55
"contributors": [
66
"Zack Jackson <zackary.l.jackson@gmail.com> (https://github.com/ScriptedAlchemy)"

src/hotLoader.js

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const loaderUtils = require('loader-utils');
44
const defaultOptions = {
55
fileMap: '{fileName}',
66
};
7+
78
module.exports = function (content) {
89
this.cacheable();
910
const options = Object.assign(
@@ -12,11 +13,13 @@ module.exports = function (content) {
1213
loaderUtils.getOptions(this),
1314
);
1415

16+
const accept = options.cssModule ? '' : 'module.hot.accept(undefined, cssReload);';
1517
return content + `
1618
if(module.hot) {
1719
// ${Date.now()}
1820
var cssReload = require(${loaderUtils.stringifyRequest(this, '!' + path.join(__dirname, 'hotModuleReplacement.js'))})(module.id, ${JSON.stringify(options)});
1921
module.hot.dispose(cssReload);
22+
${accept};
2023
}
2124
`;
2225
};

src/hotModuleReplacement.js

+12-14
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,17 @@ function updateCss(el, url) {
6666
el.parentNode.appendChild(newEl);
6767
}
6868

69+
function getReloadUrl(href, src) {
70+
href = normalizeUrl(href, { stripWWW: false });
71+
let ret;
72+
src.some(function (url) {
73+
if (href.indexOf(src) > -1) {
74+
ret = url;
75+
}
76+
});
77+
return ret;
78+
}
79+
6980
function reloadStyle(src) {
7081
const elements = document.querySelectorAll('link');
7182
let loaded = false;
@@ -83,17 +94,6 @@ function reloadStyle(src) {
8394
return loaded;
8495
}
8596

86-
function getReloadUrl(href, src) {
87-
href = normalizeUrl(href, { stripWWW: false });
88-
let ret;
89-
src.some(function (url) {
90-
if (href.indexOf(src) > -1) {
91-
ret = url;
92-
}
93-
});
94-
return ret;
95-
}
96-
9797
function reloadAll() {
9898
const elements = document.querySelectorAll('link');
9999
forEach.call(elements, function (el) {
@@ -103,13 +103,11 @@ function reloadAll() {
103103
}
104104

105105
module.exports = function (moduleId, options) {
106-
let getScriptSrc;
107-
108106
if (noDocument) {
109107
return noop;
110108
}
111109

112-
getScriptSrc = getCurrentScriptUrl(moduleId);
110+
const getScriptSrc = getCurrentScriptUrl(moduleId);
113111

114112
function update() {
115113
const src = getScriptSrc(options.fileMap);

src/index.js

+88-10
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ class ExtractCssChunks {
209209
result.push({
210210
render: () =>
211211
this.renderContentAsset(
212+
compilation,
212213
chunk,
213214
renderedModules,
214215
compilation.runtimeTemplate.requestShortener,
@@ -234,6 +235,7 @@ class ExtractCssChunks {
234235
result.push({
235236
render: () =>
236237
this.renderContentAsset(
238+
compilation,
237239
chunk,
238240
renderedModules,
239241
compilation.runtimeTemplate.requestShortener,
@@ -423,27 +425,103 @@ class ExtractCssChunks {
423425
return obj;
424426
}
425427

426-
renderContentAsset(chunk, modules, requestShortener) {
427-
// get first chunk group and take ordr from this one
428-
// When a chunk is shared between multiple chunk groups
429-
// with different order this can lead to wrong order
430-
// but it's not possible to create a correct order in
431-
// this case. Don't share chunks if you don't like it.
428+
renderContentAsset(compilation, chunk, modules, requestShortener) {
429+
let usedModules;
430+
432431
const [chunkGroup] = chunk.groupsIterable;
433432
if (typeof chunkGroup.getModuleIndex2 === 'function') {
434-
modules.sort(
435-
(a, b) => chunkGroup.getModuleIndex2(a) - chunkGroup.getModuleIndex2(b),
436-
);
433+
// Store dependencies for modules
434+
const moduleDependencies = new Map(modules.map(m => [m, new Set()]));
435+
436+
// Get ordered list of modules per chunk group
437+
// This loop also gathers dependencies from the ordered lists
438+
// Lists are in reverse order to allow to use Array.pop()
439+
const modulesByChunkGroup = Array.from(chunk.groupsIterable, (cg) => {
440+
const sortedModules = modules
441+
.map(m => ({
442+
module: m,
443+
index: cg.getModuleIndex2(m),
444+
}))
445+
.filter(item => item.index !== undefined)
446+
.sort((a, b) => b.index - a.index)
447+
.map(item => item.module);
448+
for (let i = 0; i < sortedModules.length; i++) {
449+
const set = moduleDependencies.get(sortedModules[i]);
450+
for (let j = i + 1; j < sortedModules.length; j++) {
451+
set.add(sortedModules[j]);
452+
}
453+
}
454+
455+
return sortedModules;
456+
});
457+
458+
// set with already included modules in correct order
459+
usedModules = new Set();
460+
461+
const unusedModulesFilter = m => !usedModules.has(m);
462+
463+
while (usedModules.size < modules.length) {
464+
let success = false;
465+
let bestMatch;
466+
let bestMatchDeps;
467+
// get first module where dependencies are fulfilled
468+
for (const list of modulesByChunkGroup) {
469+
// skip and remove already added modules
470+
while (list.length > 0 && usedModules.has(list[list.length - 1])) {
471+
list.pop();
472+
}
473+
474+
// skip empty lists
475+
if (list.length !== 0) {
476+
const module = list[list.length - 1];
477+
const deps = moduleDependencies.get(module);
478+
// determine dependencies that are not yet included
479+
const failedDeps = Array.from(deps)
480+
.filter(unusedModulesFilter);
481+
482+
// store best match for fallback behavior
483+
if (!bestMatchDeps || bestMatchDeps.length > failedDeps.length) {
484+
bestMatch = list;
485+
bestMatchDeps = failedDeps;
486+
}
487+
if (failedDeps.length === 0) {
488+
// use this module and remove it from list
489+
usedModules.add(list.pop());
490+
success = true;
491+
break;
492+
}
493+
}
494+
}
495+
496+
if (!success) {
497+
// no module found => there is a conflict
498+
// use list with fewest failed deps
499+
// and emit a warning
500+
const fallbackModule = bestMatch.pop();
501+
compilation.warnings.push(
502+
new Error(
503+
`chunk ${chunk.name || chunk.id} [mini-css-extract-plugin]\n` +
504+
'Conflicting order between:\n' +
505+
` * ${fallbackModule.readableIdentifier(requestShortener)}\n` +
506+
`${bestMatchDeps
507+
.map(m => ` * ${m.readableIdentifier(requestShortener)}`)
508+
.join('\n')}`,
509+
),
510+
);
511+
usedModules.add(fallbackModule);
512+
}
513+
}
437514
} else {
438515
// fallback for older webpack versions
439516
// (to avoid a breaking change)
440517
// TODO remove this in next mayor version
441518
// and increase minimum webpack version to 4.12.0
442519
modules.sort((a, b) => a.index2 - b.index2);
520+
usedModules = modules;
443521
}
444522
const source = new ConcatSource();
445523
const externalsSource = new ConcatSource();
446-
for (const m of modules) {
524+
for (const m of usedModules) {
447525
if (/^@import url/.test(m.content)) {
448526
// HACK for IE
449527
// http://stackoverflow.com/a/14676665/1458162

src/loader.js

+29-25
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ const pluginName = 'extract-css-chunks-webpack-plugin';
1414

1515
const exec = (loaderContext, code, filename) => {
1616
const module = new NativeModule(filename, loaderContext);
17-
module.paths = NativeModule._nodeModulePaths(loaderContext.context); // eslint-disable-line no-underscore-dangle, max-len
17+
18+
// eslint-disable-next-line no-underscore-dangle
19+
module.paths = NativeModule._nodeModulePaths(loaderContext.context);
1820
module.filename = filename;
1921
module._compile(code, filename); // eslint-disable-line no-underscore-dangle
2022
return module.exports;
@@ -34,10 +36,9 @@ export function pitch(request) {
3436
const loaders = this.loaders.slice(this.loaderIndex + 1);
3537
this.addDependency(this.resourcePath);
3638
const childFilename = '*'; // eslint-disable-line no-path-concat
37-
const publicPath =
38-
typeof query.publicPath === 'string'
39-
? query.publicPath
40-
: this._compilation.outputOptions.publicPath;
39+
const publicPath = typeof query.publicPath === 'string'
40+
? query.publicPath
41+
: this._compilation.outputOptions.publicPath;
4142
const outputOptions = {
4243
filename: childFilename,
4344
publicPath,
@@ -49,8 +50,11 @@ export function pitch(request) {
4950
new NodeTemplatePlugin(outputOptions).apply(childCompiler);
5051
new LibraryTemplatePlugin(null, 'commonjs2').apply(childCompiler);
5152
new NodeTargetPlugin().apply(childCompiler);
52-
new SingleEntryPlugin(this.context, `!!${request}`, pluginName).apply(
53-
childCompiler,
53+
new SingleEntryPlugin(
54+
this.context,
55+
`!!${request}`,
56+
pluginName
57+
).apply(childCompiler,
5458
);
5559
new LimitChunkCountPlugin({ maxChunks: 1 }).apply(childCompiler);
5660
// We set loaderContext[NS] = false to indicate we already in
@@ -63,32 +67,32 @@ export function pitch(request) {
6367
(loaderContext, module) => {
6468
loaderContext[NS] = false; // eslint-disable-line no-param-reassign
6569
if (module.request === request) {
66-
module.loaders = loaders.map(loader =>
67-
// eslint-disable-line no-param-reassign
68-
({
69-
loader: loader.path,
70-
options: loader.options,
71-
ident: loader.ident,
72-
}));
70+
module.loaders = loaders.map(loader => ({
71+
loader: loader.path,
72+
options: loader.options,
73+
ident: loader.ident,
74+
}));
7375
}
7476
},
7577
);
7678
},
7779
);
7880

7981
let source;
80-
childCompiler.hooks.afterCompile.tap(pluginName, (compilation) => {
81-
source =
82-
compilation.assets[childFilename] &&
83-
compilation.assets[childFilename].source();
82+
childCompiler.hooks.afterCompile.tap(
83+
pluginName,
84+
(compilation) => {
85+
source = compilation.assets[childFilename]
86+
&& compilation.assets[childFilename].source();
8487

85-
// Remove all chunk assets
86-
compilation.chunks.forEach((chunk) => {
87-
chunk.files.forEach((file) => {
88-
delete compilation.assets[file]; // eslint-disable-line no-param-reassign
88+
// Remove all chunk assets
89+
compilation.chunks.forEach((chunk) => {
90+
chunk.files.forEach((file) => {
91+
delete compilation.assets[file]; // eslint-disable-line no-param-reassign
92+
});
8993
});
90-
});
91-
});
94+
}
95+
);
9296

9397
const callback = this.async();
9498
childCompiler.runAsChild((err, entries, compilation) => {
@@ -104,7 +108,7 @@ export function pitch(request) {
104108
this.addContextDependency(dep);
105109
}, this);
106110
if (!source) {
107-
return callback(new Error("Didn't get a result from child compiler"));
111+
return callback(new Error('Didn\'t get a result from child compiler'));
108112
}
109113
let text;
110114
let locals;

0 commit comments

Comments
 (0)