Skip to content

Commit

Permalink
Fix modules hmr (#101)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ScriptedAlchemy committed Sep 1, 2018
1 parent a364370 commit 2e242d2
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 50 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ module.exports = {
'no-restricted-syntax': 0,
'prefer-arrow-callback': 0,
'prefer-destructuring': 0,
'array-callback-return': 0,
'prefer-template': 0,
'class-methods-use-this': 0
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "extract-css-chunks-webpack-plugin",
"version": "3.1.0-beta.1",
"version": "0.0.0-placeholder",
"author": "James Gillmore <james@faceyspacey.com>",
"contributors": [
"Zack Jackson <zackary.l.jackson@gmail.com> (https://github.com/ScriptedAlchemy)"
Expand Down
3 changes: 3 additions & 0 deletions src/hotLoader.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const loaderUtils = require('loader-utils');
const defaultOptions = {
fileMap: '{fileName}',
};

module.exports = function (content) {
this.cacheable();
const options = Object.assign(
Expand All @@ -12,11 +13,13 @@ module.exports = function (content) {
loaderUtils.getOptions(this),
);

const accept = options.cssModule ? '' : 'module.hot.accept(undefined, cssReload);';
return content + `
if(module.hot) {
// ${Date.now()}
var cssReload = require(${loaderUtils.stringifyRequest(this, '!' + path.join(__dirname, 'hotModuleReplacement.js'))})(module.id, ${JSON.stringify(options)});
module.hot.dispose(cssReload);
${accept};
}
`;
};
26 changes: 12 additions & 14 deletions src/hotModuleReplacement.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,17 @@ function updateCss(el, url) {
el.parentNode.appendChild(newEl);
}

function getReloadUrl(href, src) {
href = normalizeUrl(href, { stripWWW: false });
let ret;
src.some(function (url) {
if (href.indexOf(src) > -1) {
ret = url;
}
});
return ret;
}

function reloadStyle(src) {
const elements = document.querySelectorAll('link');
let loaded = false;
Expand All @@ -83,17 +94,6 @@ function reloadStyle(src) {
return loaded;
}

function getReloadUrl(href, src) {
href = normalizeUrl(href, { stripWWW: false });
let ret;
src.some(function (url) {
if (href.indexOf(src) > -1) {
ret = url;
}
});
return ret;
}

function reloadAll() {
const elements = document.querySelectorAll('link');
forEach.call(elements, function (el) {
Expand All @@ -103,13 +103,11 @@ function reloadAll() {
}

module.exports = function (moduleId, options) {
let getScriptSrc;

if (noDocument) {
return noop;
}

getScriptSrc = getCurrentScriptUrl(moduleId);
const getScriptSrc = getCurrentScriptUrl(moduleId);

function update() {
const src = getScriptSrc(options.fileMap);
Expand Down
98 changes: 88 additions & 10 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ class ExtractCssChunks {
result.push({
render: () =>
this.renderContentAsset(
compilation,
chunk,
renderedModules,
compilation.runtimeTemplate.requestShortener,
Expand All @@ -234,6 +235,7 @@ class ExtractCssChunks {
result.push({
render: () =>
this.renderContentAsset(
compilation,
chunk,
renderedModules,
compilation.runtimeTemplate.requestShortener,
Expand Down Expand Up @@ -423,27 +425,103 @@ class ExtractCssChunks {
return obj;
}

renderContentAsset(chunk, modules, requestShortener) {
// get first chunk group and take ordr from this one
// When a chunk is shared between multiple chunk groups
// with different order this can lead to wrong order
// but it's not possible to create a correct order in
// this case. Don't share chunks if you don't like it.
renderContentAsset(compilation, chunk, modules, requestShortener) {
let usedModules;

const [chunkGroup] = chunk.groupsIterable;
if (typeof chunkGroup.getModuleIndex2 === 'function') {
modules.sort(
(a, b) => chunkGroup.getModuleIndex2(a) - chunkGroup.getModuleIndex2(b),
);
// Store dependencies for modules
const moduleDependencies = new Map(modules.map(m => [m, new Set()]));

// Get ordered list of modules per chunk group
// This loop also gathers dependencies from the ordered lists
// Lists are in reverse order to allow to use Array.pop()
const modulesByChunkGroup = Array.from(chunk.groupsIterable, (cg) => {
const sortedModules = modules
.map(m => ({
module: m,
index: cg.getModuleIndex2(m),
}))
.filter(item => item.index !== undefined)
.sort((a, b) => b.index - a.index)
.map(item => item.module);
for (let i = 0; i < sortedModules.length; i++) {
const set = moduleDependencies.get(sortedModules[i]);
for (let j = i + 1; j < sortedModules.length; j++) {
set.add(sortedModules[j]);
}
}

return sortedModules;
});

// set with already included modules in correct order
usedModules = new Set();

const unusedModulesFilter = m => !usedModules.has(m);

while (usedModules.size < modules.length) {
let success = false;
let bestMatch;
let bestMatchDeps;
// 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();
}

// 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);

// store best match for fallback behavior
if (!bestMatchDeps || bestMatchDeps.length > failedDeps.length) {
bestMatch = list;
bestMatchDeps = failedDeps;
}
if (failedDeps.length === 0) {
// use this module and remove it from list
usedModules.add(list.pop());
success = true;
break;
}
}
}

if (!success) {
// no module found => there is a conflict
// use list with fewest failed deps
// and emit a warning
const fallbackModule = bestMatch.pop();
compilation.warnings.push(
new Error(
`chunk ${chunk.name || chunk.id} [mini-css-extract-plugin]\n` +
'Conflicting order between:\n' +
` * ${fallbackModule.readableIdentifier(requestShortener)}\n` +
`${bestMatchDeps
.map(m => ` * ${m.readableIdentifier(requestShortener)}`)
.join('\n')}`,
),
);
usedModules.add(fallbackModule);
}
}
} else {
// fallback for older webpack versions
// (to avoid a breaking change)
// TODO remove this in next mayor version
// and increase minimum webpack version to 4.12.0
modules.sort((a, b) => a.index2 - b.index2);
usedModules = modules;
}
const source = new ConcatSource();
const externalsSource = new ConcatSource();
for (const m of modules) {
for (const m of usedModules) {
if (/^@import url/.test(m.content)) {
// HACK for IE
// http://stackoverflow.com/a/14676665/1458162
Expand Down
54 changes: 29 additions & 25 deletions src/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ const pluginName = 'extract-css-chunks-webpack-plugin';

const exec = (loaderContext, code, filename) => {
const module = new NativeModule(filename, loaderContext);
module.paths = NativeModule._nodeModulePaths(loaderContext.context); // eslint-disable-line no-underscore-dangle, max-len

// eslint-disable-next-line no-underscore-dangle
module.paths = NativeModule._nodeModulePaths(loaderContext.context);
module.filename = filename;
module._compile(code, filename); // eslint-disable-line no-underscore-dangle
return module.exports;
Expand All @@ -34,10 +36,9 @@ export function pitch(request) {
const loaders = this.loaders.slice(this.loaderIndex + 1);
this.addDependency(this.resourcePath);
const childFilename = '*'; // eslint-disable-line no-path-concat
const publicPath =
typeof query.publicPath === 'string'
? query.publicPath
: this._compilation.outputOptions.publicPath;
const publicPath = typeof query.publicPath === 'string'
? query.publicPath
: this._compilation.outputOptions.publicPath;
const outputOptions = {
filename: childFilename,
publicPath,
Expand All @@ -49,8 +50,11 @@ export function pitch(request) {
new NodeTemplatePlugin(outputOptions).apply(childCompiler);
new LibraryTemplatePlugin(null, 'commonjs2').apply(childCompiler);
new NodeTargetPlugin().apply(childCompiler);
new SingleEntryPlugin(this.context, `!!${request}`, pluginName).apply(
childCompiler,
new SingleEntryPlugin(
this.context,
`!!${request}`,
pluginName
).apply(childCompiler,
);
new LimitChunkCountPlugin({ maxChunks: 1 }).apply(childCompiler);
// We set loaderContext[NS] = false to indicate we already in
Expand All @@ -63,32 +67,32 @@ export function pitch(request) {
(loaderContext, module) => {
loaderContext[NS] = false; // eslint-disable-line no-param-reassign
if (module.request === request) {
module.loaders = loaders.map(loader =>
// eslint-disable-line no-param-reassign
({
loader: loader.path,
options: loader.options,
ident: loader.ident,
}));
module.loaders = loaders.map(loader => ({
loader: loader.path,
options: loader.options,
ident: loader.ident,
}));
}
},
);
},
);

let source;
childCompiler.hooks.afterCompile.tap(pluginName, (compilation) => {
source =
compilation.assets[childFilename] &&
compilation.assets[childFilename].source();
childCompiler.hooks.afterCompile.tap(
pluginName,
(compilation) => {
source = compilation.assets[childFilename]
&& compilation.assets[childFilename].source();

// Remove all chunk assets
compilation.chunks.forEach((chunk) => {
chunk.files.forEach((file) => {
delete compilation.assets[file]; // eslint-disable-line no-param-reassign
// Remove all chunk assets
compilation.chunks.forEach((chunk) => {
chunk.files.forEach((file) => {
delete compilation.assets[file]; // eslint-disable-line no-param-reassign
});
});
});
});
}
);

const callback = this.async();
childCompiler.runAsChild((err, entries, compilation) => {
Expand All @@ -104,7 +108,7 @@ export function pitch(request) {
this.addContextDependency(dep);
}, this);
if (!source) {
return callback(new Error("Didn't get a result from child compiler"));
return callback(new Error('Didn\'t get a result from child compiler'));
}
let text;
let locals;
Expand Down

0 comments on commit 2e242d2

Please sign in to comment.