Skip to content

Commit

Permalink
feat: add option to enable/disable HMR (options.hmr) (#264)
Browse files Browse the repository at this point in the history
  • Loading branch information
drawyan authored and evilebottnawi committed Sep 14, 2017
1 parent 67120f8 commit 378e906
Show file tree
Hide file tree
Showing 12 changed files with 266 additions and 49 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ Styles are not added on `import/require()`, but instead on call to `use`/`ref`.

|Name|Type|Default|Description|
|:--:|:--:|:-----:|:----------|
|**`hmr`**|`{Boolean}`|`true`|Enable/disable Hot Module Replacement (HMR), if disabled no HMR Code will be added (good for non local development/production)|
|**`base`** |`{Number}`|`true`|Set module ID base (DLLPlugin)|
|**`attrs`**|`{Object}`|`{}`|Add custom attrs to `<style></style>`|
|**`transform`** |`{Function}`|`false`|Transform/Conditionally load CSS by passing a transform/condition function|
Expand All @@ -142,6 +143,21 @@ Styles are not added on `import/require()`, but instead on call to `use`/`ref`.
|**`sourceMap`**|`{Boolean}`|`false`|Enable/Disable Sourcemaps|
|**`convertToAbsoluteUrls`**|`{Boolean}`|`false`|Converts relative URLs to absolute urls, when source maps are enabled|

### `hmr`

Enable/disable Hot Module Replacement (HMR), if disabled no HMR Code will be added.
This could be used for non local development and production.

**webpack.config.js**
```js
{
loader: 'style-loader'
options: {
hmr: false
}
}
```

### `base`

This setting is primarily used as a workaround for [css clashes](https://github.com/webpack-contrib/style-loader/issues/163) when using one or more [DllPlugin](https://robertknight.github.io/posts/webpack-dll-plugins/)'s. `base` allows you to prevent either the *app*'s css (or *DllPlugin2*'s css) from overwriting *DllPlugin1*'s css by specifying a css module id base which is greater than the range used by *DllPlugin1* e.g.:
Expand Down
32 changes: 19 additions & 13 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,24 @@ module.exports.pitch = function (request) {

validateOptions(require('./options.json'), options, 'Style Loader')

options.hmr = typeof options.hmr === 'undefined' ? true : options.hmr;

var hmrCode = [
"// Hot Module Replacement",
"if(module.hot) {",
" // When the styles change, update the <style> tags",
" if(!content.locals) {",
" module.hot.accept(" + loaderUtils.stringifyRequest(this, "!!" + request) + ", function() {",
" var newContent = require(" + loaderUtils.stringifyRequest(this, "!!" + request) + ");",
" if(typeof newContent === 'string') newContent = [[module.id, newContent, '']];",
" update(newContent);",
" });",
" }",
" // When the module is disposed, remove the <style> tags",
" module.hot.dispose(function() { update(); });",
"}"
].join("\n");

return [
"// style-loader: Adds some css to the DOM by adding a <style> tag",
"",
Expand All @@ -31,18 +49,6 @@ module.exports.pitch = function (request) {
"// add the styles to the DOM",
"var update = require(" + loaderUtils.stringifyRequest(this, "!" + path.join(__dirname, "lib", "addStyles.js")) + ")(content, options);",
"if(content.locals) module.exports = content.locals;",
"// Hot Module Replacement",
"if(module.hot) {",
" // When the styles change, update the <style> tags",
" if(!content.locals) {",
" module.hot.accept(" + loaderUtils.stringifyRequest(this, "!!" + request) + ", function() {",
" var newContent = require(" + loaderUtils.stringifyRequest(this, "!!" + request) + ");",
" if(typeof newContent === 'string') newContent = [[module.id, newContent, '']];",
" update(newContent);",
" });",
" }",
" // When the module is disposed, remove the <style> tags",
" module.hot.dispose(function() { update(); });",
"}"
options.hmr ? hmrCode : ""
].join("\n");
};
6 changes: 4 additions & 2 deletions lib/addStyleUrl.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ module.exports = function addStyleUrl (url, options) {

options.attrs = typeof options.attrs === "object" ? options.attrs : {};

options.hmr = typeof options.hmr === 'undefined' ? true : options.hmr;

var link = document.createElement("link");

link.rel = "stylesheet";
Expand All @@ -30,7 +32,7 @@ module.exports = function addStyleUrl (url, options) {

head.appendChild(link);

if (module.hot) {
if (options.hmr && module.hot) {
return function(url) {
if(typeof url === "string") {
link.href = url;
Expand All @@ -39,4 +41,4 @@ module.exports = function addStyleUrl (url, options) {
}
};
}
}
}
3 changes: 3 additions & 0 deletions options.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
{
"type": "object",
"properties": {
"hmr": {
"type": "boolean"
},
"base": {
"type": "number"
},
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"jsdom": "^9.12.0",
"memory-fs": "^0.4.1",
"mocha": "^3.4.2",
"sinon": "^2.4.1",
"standard-version": "^4.0.0",
"webpack": "^2.6.1"
},
Expand Down
44 changes: 34 additions & 10 deletions test/basicTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ describe("basic tests", function() {
var path = require("path");

var utils = require("./utils"),
runCompilerTest = utils.runCompilerTest;
runCompilerTest = utils.runCompilerTest,
runSourceTest = utils.runSourceTest;

var fs;

Expand Down Expand Up @@ -60,6 +61,18 @@ describe("basic tests", function() {
}
};

var setupWebpackConfig = function() {
fs = utils.setup(webpackConfig, jsdomHtml);

// Create a tiny file system. rootDir is used because loaders are referring to absolute paths.
fs.mkdirpSync(rootDir);
fs.writeFileSync(rootDir + "main.js", "var css = require('./style.css');");
fs.writeFileSync(rootDir + "style.css", requiredCss);
fs.writeFileSync(rootDir + "styleTwo.css", requiredCssTwo);
fs.writeFileSync(rootDir + "localScoped.css", localScopedCss);
fs.writeFileSync(rootDir + "localComposing.css", localComposingCss);
};

beforeEach(function() {
// Reset all style-loader options
for (var member in styleLoaderOptions) {
Expand All @@ -70,15 +83,7 @@ describe("basic tests", function() {
cssRule[member] = defaultCssRule[member];
}

fs = utils.setup(webpackConfig, jsdomHtml);

// Create a tiny file system. rootDir is used because loaders are refering to absolute paths.
fs.mkdirpSync(rootDir);
fs.writeFileSync(rootDir + "main.js", "var css = require('./style.css');");
fs.writeFileSync(rootDir + "style.css", requiredCss);
fs.writeFileSync(rootDir + "styleTwo.css", requiredCssTwo);
fs.writeFileSync(rootDir + "localScoped.css", localScopedCss);
fs.writeFileSync(rootDir + "localComposing.css", localComposingCss);
setupWebpackConfig();
}); // before each

it("insert at bottom", function(done) {
Expand Down Expand Up @@ -405,6 +410,25 @@ describe("basic tests", function() {

runCompilerTest(expected, done);
});
});

describe("hmr option", function() {

it("should output HMR code block by default", function(done) {
runSourceTest(/Hot Module Replacement/g, null, done);
});

it("should output HMR code block when options.hmr is true", function(done) {
styleLoaderOptions.hmr = true;
setupWebpackConfig();
runSourceTest(/Hot Module Replacement/g, null, done);
});

it("should not output HMR code block when options.hmr is false", function(done) {
styleLoaderOptions.hmr = false;
setupWebpackConfig();
runSourceTest(null, /Hot Module Replacement/g, done);
});

});

Expand Down
37 changes: 37 additions & 0 deletions test/urlTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Node v4 requires "use strict" to allow block scoped let & const
"use strict";

var assert = require("assert");
var sinon = require('sinon');
var loaderUtils = require('loader-utils');

var url = require("../url");

describe("url tests", function () {
var sandbox = sinon.sandbox.create();
var getOptions;

beforeEach(() => {
// Mock loaderUtils to override options
getOptions = sandbox.stub(loaderUtils, 'getOptions');
});

afterEach(() => {
sandbox.restore();
});

it("should output HMR code by default", function () {
assert.equal(/Hot Module Replacement/g.test(url.pitch()), true);
});

it("should NOT output HMR code when options.hmr is false", function () {
getOptions.returns({hmr: false});
assert.equal(/Hot Module Replacement/g.test(url.pitch()), false);
});

it("should output HMR code when options.hmr is true", function () {
getOptions.returns({hmr: true});
assert.equal(/Hot Module Replacement/g.test(url.pitch()), true);
});

});
37 changes: 37 additions & 0 deletions test/useableTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Node v4 requires "use strict" to allow block scoped let & const
"use strict";

var assert = require("assert");
var sinon = require('sinon');
var loaderUtils = require('loader-utils');

var useable = require("../useable");

describe("useable tests", function () {
var sandbox = sinon.sandbox.create();
var getOptions;

beforeEach(() => {
// Mock loaderUtils to override options
getOptions = sandbox.stub(loaderUtils, 'getOptions');
});

afterEach(() => {
sandbox.restore();
});

it("should output HMR code by default", function () {
assert.equal(/Hot Module Replacement/g.test(useable.pitch()), true);
});

it("should NOT output HMR code when options.hmr is false", function () {
getOptions.returns({hmr: false});
assert.equal(/Hot Module Replacement/g.test(useable.pitch()), false);
});

it("should output HMR code when options.hmr is true", function () {
getOptions.returns({hmr: true});
assert.equal(/Hot Module Replacement/g.test(useable.pitch()), true);
});

});
27 changes: 26 additions & 1 deletion test/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ module.exports = {
virtualConsole: jsdom.createVirtualConsole().sendTo(console),
done: function(err, window) {
if (typeof actual === 'function') {
assert.equal(actual.apply(window), expected);
assert.equal(actual.apply(window), expected);
} else {
assert.equal(window.document.querySelector(selector).innerHTML.trim(), expected);
}
Expand All @@ -83,5 +83,30 @@ module.exports = {
}
});
});
},

/*
* Runs the test against Webpack compiled source code.
* @param {regex} regexToMatch - regex to match the source code
* @param {regex} regexToNotMatch - regex to NOT match the source code
* @param {function} done - Async callback from Mocha.
*/
runSourceTest: function(regexToMatch, regexToNotMatch, done) {
compiler.run(function(err, stats) {
if (stats.compilation.errors.length) {
throw new Error(stats.compilation.errors);
}

const bundleJs = stats.compilation.assets["bundle.js"].source();
if (regexToMatch) {
assert.equal(regexToMatch.test(bundleJs), true);
}

if (regexToNotMatch) {
assert.equal(regexToNotMatch.test(bundleJs), false);
}

done();
});
}
};
16 changes: 11 additions & 5 deletions url.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,9 @@ module.exports.pitch = function (request) {

validateOptions(require('./options.json'), options, 'Style Loader (URL)');

return [
"// style-loader: Adds some reference to a css file to the DOM by adding a <link> tag",
"var update = require(" + loaderUtils.stringifyRequest(this, "!" + path.join(__dirname, "lib", "addStyleUrl.js")) + ")(",
"\trequire(" + loaderUtils.stringifyRequest(this, "!!" + request) + ")",
", " + JSON.stringify(options) + ");",
options.hmr = typeof options.hmr === 'undefined' ? true : options.hmr;

var hmrCode = [
"// Hot Module Replacement",
"if(module.hot) {",
"\tmodule.hot.accept(" + loaderUtils.stringifyRequest(this, "!!" + request) + ", function() {",
Expand All @@ -29,4 +27,12 @@ module.exports.pitch = function (request) {
"\tmodule.hot.dispose(function() { update(); });",
"}"
].join("\n");

return [
"// style-loader: Adds some reference to a css file to the DOM by adding a <link> tag",
"var update = require(" + loaderUtils.stringifyRequest(this, "!" + path.join(__dirname, "lib", "addStyleUrl.js")) + ")(",
"\trequire(" + loaderUtils.stringifyRequest(this, "!!" + request) + ")",
", " + JSON.stringify(options) + ");",
options.hmr ? hmrCode : ""
].join("\n");
};
43 changes: 25 additions & 18 deletions useable.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,10 @@ module.exports.pitch = function (request) {

validateOptions(require('./options.json'), options, 'Style Loader (Useable)');

return [
"var refs = 0;",
"var dispose;",
"var content = require(" + loaderUtils.stringifyRequest(this, "!!" + request) + ");",
"if(typeof content === 'string') content = [[module.id, content, '']];",
"if(content.locals) exports.locals = content.locals;",
"exports.use = exports.ref = function() {",
" if(!(refs++)) {",
" dispose = require(" + loaderUtils.stringifyRequest(this, "!" + path.join(__dirname, "lib", "addStyles.js")) + ")(content, " + JSON.stringify(options) + ");",
" }",
" return exports;",
"};",
"exports.unuse = exports.unref = function() {",
" if(refs > 0 && !(--refs)) {",
" dispose();",
" dispose = null;",
" }",
"};",
options.hmr = typeof options.hmr === 'undefined' ? true : options.hmr;

var hmrCode = [
"// Hot Module Replacement",
"if(module.hot) {",
" var lastRefs = module.hot.data && module.hot.data.refs || 0;",
" if(lastRefs) {",
Expand All @@ -53,4 +39,25 @@ module.exports.pitch = function (request) {
" });",
"}"
].join("\n");

return [
"var refs = 0;",
"var dispose;",
"var content = require(" + loaderUtils.stringifyRequest(this, "!!" + request) + ");",
"if(typeof content === 'string') content = [[module.id, content, '']];",
"if(content.locals) exports.locals = content.locals;",
"exports.use = exports.ref = function() {",
" if(!(refs++)) {",
" dispose = require(" + loaderUtils.stringifyRequest(this, "!" + path.join(__dirname, "lib", "addStyles.js")) + ")(content, " + JSON.stringify(options) + ");",
" }",
" return exports;",
"};",
"exports.unuse = exports.unref = function() {",
" if(refs > 0 && !(--refs)) {",
" dispose();",
" dispose = null;",
" }",
"};",
options.hmr ? hmrCode : ""
].join("\n");
};
Loading

0 comments on commit 378e906

Please sign in to comment.