Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(hmr): adding hot module reloading #334

Merged
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
0830301
feat(hmr): adding hot module reloading
evilebottnawi Dec 13, 2018
875f166
Merge remote-tracking branch 'origin/hot-module-reloading' into hot-m…
ScriptedAlchemy Jan 6, 2019
7f99f5f
fix: remove unused dependency
ScriptedAlchemy Jan 6, 2019
4e72531
refactor: refactors based on PR comments
ScriptedAlchemy Jan 13, 2019
50b3fd8
refactor: move hot loader into base loader
ScriptedAlchemy Jan 13, 2019
c2207af
refactor: get hmr working with new context
ScriptedAlchemy Jan 24, 2019
1700774
test: added hmr test case
ScriptedAlchemy Jan 27, 2019
3f975cd
revert: roll back package upgrades
ScriptedAlchemy Jan 27, 2019
4c0c742
fix: updating dependencies
ScriptedAlchemy Jan 27, 2019
edf19e1
fix: remove console log
ScriptedAlchemy Jan 27, 2019
bac3930
test: fixing tests
ScriptedAlchemy Jan 27, 2019
86134d0
fix: use older version of normalize
ScriptedAlchemy Jan 27, 2019
8e6f14c
fix: remove vulnerabilities in depencencies
ScriptedAlchemy Jan 27, 2019
3979add
test: adding snapshot for hmr
ScriptedAlchemy Jan 28, 2019
3157c84
fix: moved es checker to dev dependencies
ScriptedAlchemy Jan 30, 2019
01126f8
docs: updated readme to detail hmr
ScriptedAlchemy Mar 3, 2019
2cf31af
fix: hmr on global styles
ScriptedAlchemy Mar 13, 2019
e5a5afb
fix: lock webpack version to avoid npm acorn issue
ScriptedAlchemy Mar 15, 2019
9ed9a85
fix: patching issue with global css missing source
ScriptedAlchemy Mar 26, 2019
2f94eed
fix: renaming hot to hmr
ScriptedAlchemy Mar 27, 2019
969514f
fix: remove unused method and using locals for css modules
ScriptedAlchemy Mar 27, 2019
b06d817
test: updating tests and snapshots
ScriptedAlchemy Mar 27, 2019
0aa328c
fix: using older version of normalize for older node support
ScriptedAlchemy Mar 27, 2019
be929ae
fix: removing reloadAll option
ScriptedAlchemy Mar 27, 2019
06e7789
fix: auto detect locals and reloadAll
ScriptedAlchemy Apr 4, 2019
a6bdafd
feat: adding order warning option
ScriptedAlchemy Apr 4, 2019
150f858
fix: added default option for orderWarning
ScriptedAlchemy Apr 4, 2019
a91857c
test: updating test expectations
ScriptedAlchemy Apr 4, 2019
55b6158
docs: updating readme about order warning flags
ScriptedAlchemy Apr 4, 2019
d9ebdc5
docs: updating docs about filtering warnings warningsFilter
ScriptedAlchemy Apr 4, 2019
6e6642a
feat(hmr): adding hot module reloading
evilebottnawi Dec 13, 2018
5a9fc85
fix: remove unused dependency
ScriptedAlchemy Jan 6, 2019
50144a7
refactor: refactors based on PR comments
ScriptedAlchemy Jan 13, 2019
39dd711
refactor: move hot loader into base loader
ScriptedAlchemy Jan 13, 2019
6126e7e
refactor: get hmr working with new context
ScriptedAlchemy Jan 24, 2019
cd6df8e
test: added hmr test case
ScriptedAlchemy Jan 27, 2019
bf0f745
revert: roll back package upgrades
ScriptedAlchemy Jan 27, 2019
d1cbf81
fix: updating dependencies
ScriptedAlchemy Jan 27, 2019
31bfa71
fix: remove console log
ScriptedAlchemy Jan 27, 2019
a51d862
test: fixing tests
ScriptedAlchemy Jan 27, 2019
2bd6661
fix: use older version of normalize
ScriptedAlchemy Jan 27, 2019
9803db3
fix: remove vulnerabilities in depencencies
ScriptedAlchemy Jan 27, 2019
c190436
test: adding snapshot for hmr
ScriptedAlchemy Jan 28, 2019
0c93cf0
fix: moved es checker to dev dependencies
ScriptedAlchemy Jan 30, 2019
c111e9b
docs: updated readme to detail hmr
ScriptedAlchemy Mar 3, 2019
936b8d3
fix: lock webpack version to avoid npm acorn issue
ScriptedAlchemy Mar 15, 2019
b08a3ce
fix: renaming hot to hmr
ScriptedAlchemy Mar 27, 2019
9f16d99
fix: remove unused method and using locals for css modules
ScriptedAlchemy Mar 27, 2019
79001b8
test: updating tests and snapshots
ScriptedAlchemy Mar 27, 2019
59797cc
fix: using older version of normalize for older node support
ScriptedAlchemy Mar 27, 2019
c149cc2
fix: removing reloadAll option
ScriptedAlchemy Mar 27, 2019
33109c4
fix: auto detect locals and reloadAll
ScriptedAlchemy Apr 4, 2019
6981838
feat: adding order warning option
ScriptedAlchemy Apr 4, 2019
2708a67
fix: added default option for orderWarning
ScriptedAlchemy Apr 4, 2019
4d1c75d
test: updating test expectations
ScriptedAlchemy Apr 4, 2019
15dd658
docs: updating readme about order warning flags
ScriptedAlchemy Apr 4, 2019
7f4159e
docs: updating docs about filtering warnings warningsFilter
ScriptedAlchemy Apr 4, 2019
641bdce
Merge branch 'master' into hot-module-reloading
ScriptedAlchemy Apr 8, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -14,3 +14,4 @@ Thumbs.db
.vscode
*.sublime-project
*.sublime-workspace
.idea
677 changes: 501 additions & 176 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions package.json
Expand Up @@ -43,8 +43,10 @@
"webpack": "^4.4.0"
},
"dependencies": {
"schema-utils": "^1.0.0",
"es-check": "^5.0.0",
ScriptedAlchemy marked this conversation as resolved.
Show resolved Hide resolved
"loader-utils": "^1.1.0",
"normalize-url": "^3.0.0",
"schema-utils": "^1.0.0",
"webpack-sources": "^1.1.0"
},
"devDependencies": {
Expand Down Expand Up @@ -75,7 +77,7 @@
"webpack": "^4.14.0",
"webpack-cli": "^2.0.13",
"webpack-defaults": "^2.3.0",
"webpack-dev-server": "^3.1.1"
"webpack-dev-server": "^3.1.14"
},
"keywords": [
"webpack"
Expand Down
165 changes: 165 additions & 0 deletions src/hmr/hotModuleReplacement.js
@@ -0,0 +1,165 @@
/* global document, window */
/* eslint func-names: 0 */
/* eslint no-var: 0 */
/* eslint vars-on-top: 0 */
/* eslint prefer-arrow-func: 0 */
/* eslint prefer-rest-params: 0 */
/* eslint prefer-arrow-callback: 0 */
/* eslint prefer-template: 0 */

var normalizeUrl = require('normalize-url');
ScriptedAlchemy marked this conversation as resolved.
Show resolved Hide resolved

var srcByModuleId = Object.create(null);

var noDocument = typeof document === 'undefined';

var forEach = Array.prototype.forEach;

function debounce(fn, time) {
var timeout = 0;

// eslint-disable-next-line func-names
return function() {
var self = this;
var args = arguments;

// eslint-disable-next-line prefer-rest-params
var functionCall = function functionCall() {
return fn.apply(self, args);
};

clearTimeout(timeout);
timeout = setTimeout(functionCall, time);
};
}

function noop() {}

function getCurrentScriptUrl(moduleId) {
var src = srcByModuleId[moduleId];

if (!src) {
if (document.currentScript) {
src = document.currentScript.src;
} else {
var scripts = document.getElementsByTagName('script');
var lastScriptTag = scripts[scripts.length - 1];

if (lastScriptTag) {
src = lastScriptTag.src;
}
}
srcByModuleId[moduleId] = src;
}

return function(fileMap) {
if (!src) {
return null;
}
var splitResult = src.split(/([^\\/]+)\.js$/);
var filename = splitResult && splitResult[1];
if (!filename) {
return [src.replace('.js', '.css')];
}
if (!fileMap) {
return [src.replace('.js', '.css')];
}
return fileMap.split(',').map(function(mapRule) {
var reg = new RegExp(filename + '\\.js$', 'g');
return normalizeUrl(
src.replace(reg, mapRule.replace(/{fileName}/g, filename) + '.css'),
{ stripWWW: false }
);
});
};
}

function updateCss(el, url) {
if (!url) {
url = el.href.split('?')[0];
}
if (el.isLoaded === false) {
// We seem to be about to replace a css link that hasn't loaded yet.
// We're probably changing the same file more than once.
return;
}
if (!url || !(url.indexOf('.css') > -1)) return;

el.visited = true;
var newEl = el.cloneNode(); // eslint-disable-line vars-on-top

newEl.isLoaded = false;

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

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

newEl.href = url + '?' + Date.now();
el.parentNode.appendChild(newEl);
}

function getReloadUrl(href, src) {
var ret;
href = normalizeUrl(href, { stripWWW: false });
// eslint-disable-next-line array-callback-return
src.some(function(url) {
if (href.indexOf(src) > -1) {
ret = url;
}
});
return ret;
}

function reloadStyle(src) {
var elements = document.querySelectorAll('link');
var loaded = false;

forEach.call(elements, function(el) {
var url = getReloadUrl(el.href, src);

if (el.visited === true) return;

if (url) {
updateCss(el, url);
loaded = true;
}
});

return loaded;
}

function reloadAll() {
var elements = document.querySelectorAll('link');
forEach.call(elements, function(el) {
if (el.visited === true) return;
updateCss(el);
});
}

module.exports = function(moduleId, options) {
if (noDocument) {
return noop;
ScriptedAlchemy marked this conversation as resolved.
Show resolved Hide resolved
}

// eslint-disable-next-line vars-on-top
var getScriptSrc = getCurrentScriptUrl(moduleId);

function update() {
var src = getScriptSrc(options.filename);
var reloaded = reloadStyle(src);
if (reloaded && !options.reloadAll) {
console.log('[HMR] css reload %s', src.join(' '));
} else {
console.log('[HMR] Reload all css');
reloadAll();
}
}

return debounce(update, 10);
};
16 changes: 16 additions & 0 deletions src/index.js
Expand Up @@ -116,6 +116,7 @@ class MiniCssExtractPlugin {
},
options
);

if (!this.options.chunkFilename) {
const { filename } = this.options;
const hasName = filename.includes('[name]');
Expand Down Expand Up @@ -393,6 +394,21 @@ class MiniCssExtractPlugin {
});
}

traverseDepthFirst(root, visit) {
ScriptedAlchemy marked this conversation as resolved.
Show resolved Hide resolved
let nodesToVisit = [root];

while (nodesToVisit.length > 0) {
const currentNode = nodesToVisit.shift();

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

visit(currentNode);
}
}

getCssChunkObject(mainChunk) {
const obj = {};
for (const chunk of mainChunk.getAllAsyncChunks()) {
Expand Down
27 changes: 26 additions & 1 deletion src/loader.js
@@ -1,5 +1,7 @@
import NativeModule from 'module';

import path from 'path';

import loaderUtils from 'loader-utils';
import NodeTemplatePlugin from 'webpack/lib/node/NodeTemplatePlugin';
import NodeTargetPlugin from 'webpack/lib/node/NodeTargetPlugin';
Expand All @@ -10,6 +12,24 @@ import LimitChunkCountPlugin from 'webpack/lib/optimize/LimitChunkCountPlugin';
const MODULE_TYPE = 'css/mini-extract';
const pluginName = 'mini-css-extract-plugin';

function hotLoader(content, context) {
const accept = context.modules
? ''
: 'module.hot.accept(undefined, cssReload);';
const result = `${content}
if(module.hot) {
// ${Date.now()}
var cssReload = require(${loaderUtils.stringifyRequest(
context.context,
path.join(__dirname, 'hmr/hotModuleReplacement.js')
)})(module.id, ${JSON.stringify(context.query)});
module.hot.dispose(cssReload);
${accept}
}
`;
return result;
}

const exec = (loaderContext, code, filename) => {
const module = new NativeModule(filename, loaderContext);
module.paths = NativeModule._nodeModulePaths(loaderContext.context); // eslint-disable-line no-underscore-dangle
Expand All @@ -29,6 +49,7 @@ const findModuleById = (modules, id) => {

export function pitch(request) {
const query = loaderUtils.getOptions(this) || {};

const loaders = this.loaders.slice(this.loaderIndex + 1);
this.addDependency(this.resourcePath);
const childFilename = '*'; // eslint-disable-line no-path-concat
Expand Down Expand Up @@ -130,10 +151,14 @@ export function pitch(request) {
}
let resultSource = `// extracted by ${pluginName}`;
if (locals && typeof resultSource !== 'undefined') {
resultSource += `\nmodule.exports = ${JSON.stringify(locals)};`;
const result = `\nmodule.exports = ${JSON.stringify(locals)};`;
resultSource += query.hot
? hotLoader(result, { context: this.context, query })
: '';
}

return callback(null, resultSource);
});
}

export default function() {}
22 changes: 22 additions & 0 deletions test/TestCases.test.js
@@ -1,5 +1,6 @@
import fs from 'fs';
import path from 'path';
import { exec } from 'child_process';
ScriptedAlchemy marked this conversation as resolved.
Show resolved Hide resolved

import webpack from 'webpack';

Expand Down Expand Up @@ -77,3 +78,24 @@ describe('TestCases', () => {
}
}
});

describe('HMR', () => {
it('matches snapshot', () => {
const hmr = fs
.readFileSync(path.join(__dirname, '../src/hmr/hotModuleReplacement.js'))
.toString();

expect(hmr).toMatchSnapshot();
});

it('is es5 only', () => {
exec(
'./node_modules/.bin/es-check es5 src/hmr/hotModuleReplacement.js',
(error, stdout, stderr) => {
expect(
stderr.indexOf('there were no ES version matching errors!') > -1
).toBe(true);
}
);
});
});