Skip to content

Commit 6caeeca

Browse files
fix(index.js): Improved HMR detection multi instance collisions (#104)
* fix(index.js): Improved HMR detection multi instance collisions addressed Merging a PR which improved upon my initial code which allowed the plugin to automatically hot reload. With the help of React Static, we are able to fix a bug that appeared when building out build toolchains. This bug causes the pugin to have two seperate context references within webpack and ends up passing incorrect references to the wrong instance. Deceptivally simple solution was naming the context key myself. * fix(nested-loaders-hmr): Fix n-depth nested loader usage where HMR fails (#103) HMR doesn't work when loaders are nested * fix(nested-loaders-hmr): Fix n-depth nested loader usage where HMR fails (#103) HMR doesn't work when loaders are nested * feat(HMR):Adding the ability to enable hot options. Like reload all, or css moduels CSS Modules option * use throw error, not console error * use throw error, not console error * fixing error thrown by empty options object * Fix HMR options being ignored (fixes #106) (#107) `options.reloadAll` had no effect, because they were incorrectly copied into the webpack's loader.options and could never reach `hotModuleReplacement.js`. This commit fixes the copy site, allowing for options to pass through. Fixes #106 * changed context string to match webpack conventions * version bump * perp for release
1 parent 7e15d9d commit 6caeeca

File tree

5 files changed

+130
-116
lines changed

5 files changed

+130
-116
lines changed

package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
{
22
"name": "extract-css-chunks-webpack-plugin",
3-
"version": "0.0.0-placeholder",
3+
"version": "3.1.2",
44
"author": "James Gillmore <[email protected]>",
55
"contributors": [
66
"Zack Jackson <[email protected]> (https://github.com/ScriptedAlchemy)"
77
],
8-
"description": "Extract CSS from chunks into stylesheets + HMR. Supports Webpack 4",
8+
"description": "Extract CSS from chunks into stylesheets + HMR. Supports Webpack 4 + SSR",
99
"engines": {
1010
"node": ">= 6.9.0 <7.0.0 || >= 8.9.0"
1111
},
@@ -24,7 +24,9 @@
2424
"hmr",
2525
"universal",
2626
"webpack",
27-
"webpack 4"
27+
"webpack 4",
28+
"css-hot-loader",
29+
"extract-css-chunks-webpack-plugin"
2830
],
2931
"license": "MIT",
3032
"scripts": {

src/hotLoader.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const loaderUtils = require('loader-utils');
33

44
const defaultOptions = {
55
fileMap: '{fileName}',
6+
cssModules: true,
67
};
78

89
module.exports = function (content) {
@@ -13,7 +14,7 @@ module.exports = function (content) {
1314
loaderUtils.getOptions(this),
1415
);
1516

16-
const accept = options.cssModule ? '' : 'module.hot.accept(undefined, cssReload);';
17+
const accept = options.cssModules ? '' : 'module.hot.accept(undefined, cssReload);';
1718
return content + `
1819
if(module.hot) {
1920
// ${Date.now()}

src/hotModuleReplacement.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,15 @@ function updateCss(el, url) {
5353
const newEl = el.cloneNode();
5454

5555
newEl.isLoaded = false;
56+
5657
newEl.addEventListener('load', function () {
5758
newEl.isLoaded = true;
58-
el.remove();
59+
el.parentNode.removeChild(el);
5960
});
61+
6062
newEl.addEventListener('error', function () {
6163
newEl.isLoaded = true;
62-
el.remove();
64+
el.parentNode.removeChild(el);
6365
});
6466

6567
newEl.href = url + '?' + Date.now();

src/index.js

Lines changed: 77 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import fs from 'fs';
21
import path from 'path';
32

43
import webpack from 'webpack';
@@ -7,9 +6,12 @@ import sources from 'webpack-sources';
76
const hotLoader = path.resolve(__dirname, './hotLoader.js');
87

98
const { ConcatSource, SourceMapSource, OriginalSource } = sources;
10-
const { Template, util: { createHash } } = webpack;
9+
const {
10+
Template,
11+
util: { createHash },
12+
} = webpack;
1113

12-
const NS = path.dirname(fs.realpathSync(__filename));
14+
const MODULE_TYPE = 'css/extract-chunks';
1315

1416
const pluginName = 'extract-css-chunks-webpack-plugin';
1517

@@ -58,7 +60,7 @@ class CssDependencyTemplate {
5860

5961
class CssModule extends webpack.Module {
6062
constructor(dependency) {
61-
super(NS, dependency.context);
63+
super(MODULE_TYPE, dependency.context);
6264
this._identifier = dependency.identifier;
6365
this._identifierIndex = dependency.identifierIndex;
6466
this.content = dependency.content;
@@ -114,35 +116,48 @@ class CssModule extends webpack.Module {
114116
}
115117

116118
class CssModuleFactory {
117-
create({ dependencies: [dependency] }, callback) {
119+
create(
120+
{
121+
dependencies: [dependency],
122+
},
123+
callback,
124+
) {
118125
callback(null, new CssModule(dependency));
119126
}
120127
}
121128

122129
class ExtractCssChunks {
123130
constructor(options) {
124-
this.options = Object.assign(
125-
{
126-
filename: '[name].css',
127-
},
128-
options,
129-
);
131+
this.options = Object.assign({ filename: '[name].css' }, options);
132+
const { cssModules, reloadAll } = this.options;
133+
130134
if (!this.options.chunkFilename) {
131135
const { filename } = this.options;
132136
const hasName = filename.includes('[name]');
133137
const hasId = filename.includes('[id]');
134138
const hasChunkHash = filename.includes('[chunkhash]');
135-
// Anything changing depending on chunk is fine
139+
140+
// Anything changing depending on chunk is fine
136141
if (hasChunkHash || hasName || hasId) {
137142
this.options.chunkFilename = filename;
138143
} else {
139-
// Elsewise prefix '[id].' in front of the basename to make it changing
140-
this.options.chunkFilename = filename.replace(
141-
/(^|\/)([^/]*(?:\?|$))/,
142-
'$1[id].$2',
143-
);
144+
// Elsewise prefix '[id].' in front of the basename to make it changing
145+
this.options.chunkFilename = filename.replace(/(^|\/)([^/]*(?:\?|$))/, '$1[id].$2');
144146
}
145147
}
148+
149+
this.hotLoaderObject = Object.assign({
150+
loader: hotLoader,
151+
options: {
152+
cssModules: false,
153+
reloadAll: false,
154+
},
155+
}, {
156+
options: {
157+
cssModules,
158+
reloadAll,
159+
},
160+
});
146161
}
147162

148163
apply(compiler) {
@@ -153,20 +168,16 @@ class ExtractCssChunks {
153168
compiler.options.module.rules = this.updateWebpackConfig(compiler.options.module.rules);
154169
}
155170
} catch (e) {
156-
console.error('Something went wrong: contact the author', JSON.stringify(e)); // eslint-disable-line no-console
171+
throw new Error(`Something went wrong: contact the author: ${JSON.stringify(e)}`);
157172
}
158173

159174
compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
160175
compilation.hooks.normalModuleLoader.tap(pluginName, (lc, m) => {
161176
const loaderContext = lc;
162177
const module = m;
163-
loaderContext[NS] = (content) => {
178+
loaderContext[MODULE_TYPE] = (content) => {
164179
if (!Array.isArray(content) && content != null) {
165-
throw new Error(
166-
`Exported value was not extracted as an array: ${JSON.stringify(
167-
content,
168-
)}`,
169-
);
180+
throw new Error(`Exported value was not extracted as an array: ${JSON.stringify(content)}`);
170181
}
171182
const identifierCountMap = new Map();
172183
for (const line of content) {
@@ -188,7 +199,7 @@ class ExtractCssChunks {
188199
pluginName,
189200
(result, { chunk }) => {
190201
const renderedModules = Array.from(chunk.modulesIterable).filter(
191-
module => module.type === NS,
202+
module => module.type === MODULE_TYPE,
192203
);
193204
if (renderedModules.length > 0) {
194205
result.push({
@@ -202,10 +213,10 @@ class ExtractCssChunks {
202213
filenameTemplate: this.options.filename,
203214
pathOptions: {
204215
chunk,
205-
contentHashType: NS,
216+
contentHashType: MODULE_TYPE,
206217
},
207218
identifier: `${pluginName}.${chunk.id}`,
208-
hash: chunk.contentHash[NS],
219+
hash: chunk.contentHash[MODULE_TYPE],
209220
});
210221
}
211222
},
@@ -214,7 +225,7 @@ class ExtractCssChunks {
214225
pluginName,
215226
(result, { chunk }) => {
216227
const renderedModules = Array.from(chunk.modulesIterable).filter(
217-
module => module.type === NS,
228+
module => module.type === MODULE_TYPE,
218229
);
219230
if (renderedModules.length > 0) {
220231
result.push({
@@ -228,10 +239,10 @@ class ExtractCssChunks {
228239
filenameTemplate: this.options.chunkFilename,
229240
pathOptions: {
230241
chunk,
231-
contentHashType: NS,
242+
contentHashType: MODULE_TYPE,
232243
},
233244
identifier: `${pluginName}.${chunk.id}`,
234-
hash: chunk.contentHash[NS],
245+
hash: chunk.contentHash[MODULE_TYPE],
235246
});
236247
}
237248
},
@@ -245,7 +256,7 @@ class ExtractCssChunks {
245256
}
246257
if (REGEXP_CONTENTHASH.test(chunkFilename)) {
247258
hash.update(
248-
JSON.stringify(chunk.getChunkMaps(true).contentHash[NS] || {}),
259+
JSON.stringify(chunk.getChunkMaps(true).contentHash[MODULE_TYPE] || {}),
249260
);
250261
}
251262
if (REGEXP_NAME.test(chunkFilename)) {
@@ -258,12 +269,12 @@ class ExtractCssChunks {
258269
const { hashFunction, hashDigest, hashDigestLength } = outputOptions;
259270
const hash = createHash(hashFunction);
260271
for (const m of chunk.modulesIterable) {
261-
if (m.type === NS) {
272+
if (m.type === MODULE_TYPE) {
262273
m.updateHash(hash);
263274
}
264275
}
265276
const { contentHash } = chunk;
266-
contentHash[NS] = hash
277+
contentHash[MODULE_TYPE] = hash
267278
.digest(hashDigest)
268279
.substring(0, hashDigestLength);
269280
});
@@ -276,9 +287,7 @@ class ExtractCssChunks {
276287
'',
277288
'// object to store loaded CSS chunks',
278289
'var installedCssChunks = {',
279-
Template.indent(
280-
chunk.ids.map(id => `${JSON.stringify(id)}: 0`).join(',\n'),
281-
),
290+
Template.indent(chunk.ids.map(id => `${JSON.stringify(id)}: 0`).join(',\n')),
282291
'}',
283292
]);
284293
}
@@ -313,14 +322,14 @@ class ExtractCssChunks {
313322
)}[chunkId] + "`;
314323
},
315324
contentHash: {
316-
[NS]: `" + ${JSON.stringify(
317-
chunkMaps.contentHash[NS],
325+
[MODULE_TYPE]: `" + ${JSON.stringify(
326+
chunkMaps.contentHash[MODULE_TYPE],
318327
)}[chunkId] + "`,
319328
},
320329
contentHashWithLength: {
321-
[NS]: (length) => {
330+
[MODULE_TYPE]: (length) => {
322331
const shortContentHashMap = {};
323-
const contentHash = chunkMaps.contentHash[NS];
332+
const contentHash = chunkMaps.contentHash[MODULE_TYPE];
324333
for (const chunkId of Object.keys(contentHash)) {
325334
if (typeof contentHash[chunkId] === 'string') {
326335
shortContentHashMap[chunkId] = contentHash[
@@ -337,7 +346,7 @@ class ExtractCssChunks {
337346
chunkMaps.name,
338347
)}[chunkId]||chunkId) + "`,
339348
},
340-
contentHashType: NS,
349+
contentHashType: MODULE_TYPE,
341350
},
342351
);
343352
return Template.asString([
@@ -397,24 +406,34 @@ class ExtractCssChunks {
397406
});
398407
}
399408

400-
updateWebpackConfig(rulez) {
401-
let isExtract = null;
402-
return rulez.reduce((rules, rule) => {
403-
if (rule.oneOf) {
404-
rule.oneOf = this.updateWebpackConfig(rule.oneOf);
405-
}
409+
traverseDepthFirst(root, visit) {
410+
let nodesToVisit = [root];
406411

407-
if (rule.use && Array.isArray(rule.use)) {
408-
isExtract = rule.use.some((l) => {
409-
const needle = l.loader || l;
410-
return needle.includes(pluginName);
411-
});
412+
while (nodesToVisit.length > 0) {
413+
const currentNode = nodesToVisit.shift();
412414

413-
if (isExtract) {
414-
rule.use.unshift(hotLoader);
415-
}
415+
if (currentNode !== null && typeof currentNode === 'object') {
416+
const children = Object.values(currentNode);
417+
nodesToVisit = [...children, ...nodesToVisit];
416418
}
417419

420+
visit(currentNode);
421+
}
422+
}
423+
424+
updateWebpackConfig(rulez) {
425+
return rulez.reduce((rules, rule) => {
426+
this.traverseDepthFirst(rule, (node) => {
427+
if (node && node.use && Array.isArray(node.use)) {
428+
const isMiniCss = node.use.some((l) => {
429+
const needle = l.loader || l;
430+
return needle.includes(pluginName);
431+
});
432+
if (isMiniCss) {
433+
node.use.unshift(this.hotLoaderObject);
434+
}
435+
}
436+
});
418437
rules.push(rule);
419438

420439
return rules;
@@ -425,7 +444,7 @@ class ExtractCssChunks {
425444
const obj = {};
426445
for (const chunk of mainChunk.getAllAsyncChunks()) {
427446
for (const module of chunk.modulesIterable) {
428-
if (module.type === NS) {
447+
if (module.type === MODULE_TYPE) {
429448
obj[chunk.id] = 1;
430449
break;
431450
}
@@ -476,17 +495,14 @@ class ExtractCssChunks {
476495
// get first module where dependencies are fulfilled
477496
for (const list of modulesByChunkGroup) {
478497
// skip and remove already added modules
479-
while (list.length > 0 && usedModules.has(list[list.length - 1])) {
480-
list.pop();
481-
}
498+
while (list.length > 0 && usedModules.has(list[list.length - 1])) { list.pop(); }
482499

483500
// skip empty lists
484501
if (list.length !== 0) {
485502
const module = list[list.length - 1];
486503
const deps = moduleDependencies.get(module);
487504
// determine dependencies that are not yet included
488-
const failedDeps = Array.from(deps)
489-
.filter(unusedModulesFilter);
505+
const failedDeps = Array.from(deps).filter(unusedModulesFilter);
490506

491507
// store best match for fallback behavior
492508
if (!bestMatchDeps || bestMatchDeps.length > failedDeps.length) {

0 commit comments

Comments
 (0)