Skip to content

Commit

Permalink
fix(index.js): Improved HMR detection multi instance collisions (#104)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ScriptedAlchemy committed Oct 2, 2018
1 parent 7e15d9d commit 6caeeca
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 116 deletions.
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"name": "extract-css-chunks-webpack-plugin",
"version": "0.0.0-placeholder",
"version": "3.1.2",
"author": "James Gillmore <james@faceyspacey.com>",
"contributors": [
"Zack Jackson <zack@ScriptedAlchemy.com> (https://github.com/ScriptedAlchemy)"
],
"description": "Extract CSS from chunks into stylesheets + HMR. Supports Webpack 4",
"description": "Extract CSS from chunks into stylesheets + HMR. Supports Webpack 4 + SSR",
"engines": {
"node": ">= 6.9.0 <7.0.0 || >= 8.9.0"
},
Expand All @@ -24,7 +24,9 @@
"hmr",
"universal",
"webpack",
"webpack 4"
"webpack 4",
"css-hot-loader",
"extract-css-chunks-webpack-plugin"
],
"license": "MIT",
"scripts": {
Expand Down
3 changes: 2 additions & 1 deletion src/hotLoader.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const loaderUtils = require('loader-utils');

const defaultOptions = {
fileMap: '{fileName}',
cssModules: true,
};

module.exports = function (content) {
Expand All @@ -13,7 +14,7 @@ module.exports = function (content) {
loaderUtils.getOptions(this),
);

const accept = options.cssModule ? '' : 'module.hot.accept(undefined, cssReload);';
const accept = options.cssModules ? '' : 'module.hot.accept(undefined, cssReload);';
return content + `
if(module.hot) {
// ${Date.now()}
Expand Down
6 changes: 4 additions & 2 deletions src/hotModuleReplacement.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,15 @@ function updateCss(el, url) {
const newEl = el.cloneNode();

newEl.isLoaded = false;

newEl.addEventListener('load', function () {
newEl.isLoaded = true;
el.remove();
el.parentNode.removeChild(el);
});

newEl.addEventListener('error', function () {
newEl.isLoaded = true;
el.remove();
el.parentNode.removeChild(el);
});

newEl.href = url + '?' + Date.now();
Expand Down
138 changes: 77 additions & 61 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import fs from 'fs';
import path from 'path';

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

const { ConcatSource, SourceMapSource, OriginalSource } = sources;
const { Template, util: { createHash } } = webpack;
const {
Template,
util: { createHash },
} = webpack;

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

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

Expand Down Expand Up @@ -58,7 +60,7 @@ class CssDependencyTemplate {

class CssModule extends webpack.Module {
constructor(dependency) {
super(NS, dependency.context);
super(MODULE_TYPE, dependency.context);
this._identifier = dependency.identifier;
this._identifierIndex = dependency.identifierIndex;
this.content = dependency.content;
Expand Down Expand Up @@ -114,35 +116,48 @@ class CssModule extends webpack.Module {
}

class CssModuleFactory {
create({ dependencies: [dependency] }, callback) {
create(
{
dependencies: [dependency],
},
callback,
) {
callback(null, new CssModule(dependency));
}
}

class ExtractCssChunks {
constructor(options) {
this.options = Object.assign(
{
filename: '[name].css',
},
options,
);
this.options = Object.assign({ filename: '[name].css' }, options);
const { cssModules, reloadAll } = this.options;

if (!this.options.chunkFilename) {
const { filename } = this.options;
const hasName = filename.includes('[name]');
const hasId = filename.includes('[id]');
const hasChunkHash = filename.includes('[chunkhash]');
// Anything changing depending on chunk is fine

// Anything changing depending on chunk is fine
if (hasChunkHash || hasName || hasId) {
this.options.chunkFilename = filename;
} else {
// Elsewise prefix '[id].' in front of the basename to make it changing
this.options.chunkFilename = filename.replace(
/(^|\/)([^/]*(?:\?|$))/,
'$1[id].$2',
);
// Elsewise prefix '[id].' in front of the basename to make it changing
this.options.chunkFilename = filename.replace(/(^|\/)([^/]*(?:\?|$))/, '$1[id].$2');
}
}

this.hotLoaderObject = Object.assign({
loader: hotLoader,
options: {
cssModules: false,
reloadAll: false,
},
}, {
options: {
cssModules,
reloadAll,
},
});
}

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

compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
compilation.hooks.normalModuleLoader.tap(pluginName, (lc, m) => {
const loaderContext = lc;
const module = m;
loaderContext[NS] = (content) => {
loaderContext[MODULE_TYPE] = (content) => {
if (!Array.isArray(content) && content != null) {
throw new Error(
`Exported value was not extracted as an array: ${JSON.stringify(
content,
)}`,
);
throw new Error(`Exported value was not extracted as an array: ${JSON.stringify(content)}`);
}
const identifierCountMap = new Map();
for (const line of content) {
Expand All @@ -188,7 +199,7 @@ class ExtractCssChunks {
pluginName,
(result, { chunk }) => {
const renderedModules = Array.from(chunk.modulesIterable).filter(
module => module.type === NS,
module => module.type === MODULE_TYPE,
);
if (renderedModules.length > 0) {
result.push({
Expand All @@ -202,10 +213,10 @@ class ExtractCssChunks {
filenameTemplate: this.options.filename,
pathOptions: {
chunk,
contentHashType: NS,
contentHashType: MODULE_TYPE,
},
identifier: `${pluginName}.${chunk.id}`,
hash: chunk.contentHash[NS],
hash: chunk.contentHash[MODULE_TYPE],
});
}
},
Expand All @@ -214,7 +225,7 @@ class ExtractCssChunks {
pluginName,
(result, { chunk }) => {
const renderedModules = Array.from(chunk.modulesIterable).filter(
module => module.type === NS,
module => module.type === MODULE_TYPE,
);
if (renderedModules.length > 0) {
result.push({
Expand All @@ -228,10 +239,10 @@ class ExtractCssChunks {
filenameTemplate: this.options.chunkFilename,
pathOptions: {
chunk,
contentHashType: NS,
contentHashType: MODULE_TYPE,
},
identifier: `${pluginName}.${chunk.id}`,
hash: chunk.contentHash[NS],
hash: chunk.contentHash[MODULE_TYPE],
});
}
},
Expand All @@ -245,7 +256,7 @@ class ExtractCssChunks {
}
if (REGEXP_CONTENTHASH.test(chunkFilename)) {
hash.update(
JSON.stringify(chunk.getChunkMaps(true).contentHash[NS] || {}),
JSON.stringify(chunk.getChunkMaps(true).contentHash[MODULE_TYPE] || {}),
);
}
if (REGEXP_NAME.test(chunkFilename)) {
Expand All @@ -258,12 +269,12 @@ class ExtractCssChunks {
const { hashFunction, hashDigest, hashDigestLength } = outputOptions;
const hash = createHash(hashFunction);
for (const m of chunk.modulesIterable) {
if (m.type === NS) {
if (m.type === MODULE_TYPE) {
m.updateHash(hash);
}
}
const { contentHash } = chunk;
contentHash[NS] = hash
contentHash[MODULE_TYPE] = hash
.digest(hashDigest)
.substring(0, hashDigestLength);
});
Expand All @@ -276,9 +287,7 @@ class ExtractCssChunks {
'',
'// object to store loaded CSS chunks',
'var installedCssChunks = {',
Template.indent(
chunk.ids.map(id => `${JSON.stringify(id)}: 0`).join(',\n'),
),
Template.indent(chunk.ids.map(id => `${JSON.stringify(id)}: 0`).join(',\n')),
'}',
]);
}
Expand Down Expand Up @@ -313,14 +322,14 @@ class ExtractCssChunks {
)}[chunkId] + "`;
},
contentHash: {
[NS]: `" + ${JSON.stringify(
chunkMaps.contentHash[NS],
[MODULE_TYPE]: `" + ${JSON.stringify(
chunkMaps.contentHash[MODULE_TYPE],
)}[chunkId] + "`,
},
contentHashWithLength: {
[NS]: (length) => {
[MODULE_TYPE]: (length) => {
const shortContentHashMap = {};
const contentHash = chunkMaps.contentHash[NS];
const contentHash = chunkMaps.contentHash[MODULE_TYPE];
for (const chunkId of Object.keys(contentHash)) {
if (typeof contentHash[chunkId] === 'string') {
shortContentHashMap[chunkId] = contentHash[
Expand All @@ -337,7 +346,7 @@ class ExtractCssChunks {
chunkMaps.name,
)}[chunkId]||chunkId) + "`,
},
contentHashType: NS,
contentHashType: MODULE_TYPE,
},
);
return Template.asString([
Expand Down Expand Up @@ -397,24 +406,34 @@ class ExtractCssChunks {
});
}

updateWebpackConfig(rulez) {
let isExtract = null;
return rulez.reduce((rules, rule) => {
if (rule.oneOf) {
rule.oneOf = this.updateWebpackConfig(rule.oneOf);
}
traverseDepthFirst(root, visit) {
let nodesToVisit = [root];

if (rule.use && Array.isArray(rule.use)) {
isExtract = rule.use.some((l) => {
const needle = l.loader || l;
return needle.includes(pluginName);
});
while (nodesToVisit.length > 0) {
const currentNode = nodesToVisit.shift();

if (isExtract) {
rule.use.unshift(hotLoader);
}
if (currentNode !== null && typeof currentNode === 'object') {
const children = Object.values(currentNode);
nodesToVisit = [...children, ...nodesToVisit];
}

visit(currentNode);
}
}

updateWebpackConfig(rulez) {
return rulez.reduce((rules, rule) => {
this.traverseDepthFirst(rule, (node) => {
if (node && node.use && Array.isArray(node.use)) {
const isMiniCss = node.use.some((l) => {
const needle = l.loader || l;
return needle.includes(pluginName);
});
if (isMiniCss) {
node.use.unshift(this.hotLoaderObject);
}
}
});
rules.push(rule);

return rules;
Expand All @@ -425,7 +444,7 @@ class ExtractCssChunks {
const obj = {};
for (const chunk of mainChunk.getAllAsyncChunks()) {
for (const module of chunk.modulesIterable) {
if (module.type === NS) {
if (module.type === MODULE_TYPE) {
obj[chunk.id] = 1;
break;
}
Expand Down Expand Up @@ -476,17 +495,14 @@ class ExtractCssChunks {
// get first module where dependencies are fulfilled
for (const list of modulesByChunkGroup) {
// skip and remove already added modules
while (list.length > 0 && usedModules.has(list[list.length - 1])) {
list.pop();
}
while (list.length > 0 && usedModules.has(list[list.length - 1])) { list.pop(); }

// skip empty lists
if (list.length !== 0) {
const module = list[list.length - 1];
const deps = moduleDependencies.get(module);
// determine dependencies that are not yet included
const failedDeps = Array.from(deps)
.filter(unusedModulesFilter);
const failedDeps = Array.from(deps).filter(unusedModulesFilter);

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

0 comments on commit 6caeeca

Please sign in to comment.