Skip to content

Commit

Permalink
feat: hot module replacement for css modules
Browse files Browse the repository at this point in the history
  • Loading branch information
evilebottnawi authored Apr 24, 2020
1 parent 26bfaa5 commit 6d14e0a
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 31 deletions.
103 changes: 72 additions & 31 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import path from 'path';
import loaderUtils from 'loader-utils';
import validateOptions from 'schema-utils';

import isEqualLocals from './runtime/isEqualLocals';

import schema from './options.json';

const loaderApi = () => {};
Expand Down Expand Up @@ -86,7 +88,7 @@ var update = api(content, options);
${hmrCode}
${esModule ? `export default {}` : ''}`;
${esModule ? 'export default {}' : ''}`;
}

case 'lazyStyleTag':
Expand All @@ -96,25 +98,52 @@ ${esModule ? `export default {}` : ''}`;
const hmrCode = this.hot
? `
if (module.hot) {
var lastRefs = module.hot.data && module.hot.data.refs || 0;
if (!content.locals || module.hot.invalidate) {
var isEqualLocals = ${isEqualLocals.toString()};
var oldLocals = content.locals;
if (lastRefs) {
exported.use();
module.hot.accept(
${loaderUtils.stringifyRequest(this, `!!${request}`)},
function () {
${
esModule
? `if (!isEqualLocals(oldLocals, content.locals)) {
module.hot.invalidate();
if (!content.locals) {
refs = lastRefs;
}
}
return;
}
if (!content.locals) {
module.hot.accept();
}
oldLocals = content.locals;
if (update && refs > 0) {
update(content);
}`
: `var newContent = require(${loaderUtils.stringifyRequest(
this,
`!!${request}`
)});
newContent = newContent.__esModule ? newContent.default : newContent;
if (!isEqualLocals(oldLocals, newContent.locals)) {
module.hot.invalidate();
return;
}
oldLocals = newContent.locals;
module.hot.dispose(function(data) {
data.refs = content.locals ? 0 : refs;
if (update && refs > 0) {
update(newContent);
}`
}
}
)
}
if (dispose) {
dispose();
module.hot.dispose(function() {
if (update) {
update();
}
});
}`
Expand Down Expand Up @@ -147,30 +176,26 @@ if (module.hot) {
}
var refs = 0;
var dispose;
var update;
var options = ${JSON.stringify(options)};
options.insert = ${insert};
options.singleton = ${isSingleton};
var exported = {};
if (content.locals) {
exported.locals = content.locals;
}
exported.locals = content.locals || {};
exported.use = function() {
if (!(refs++)) {
dispose = api(content, options);
update = api(content, options);
}
return exported;
};
exported.unuse = function() {
if (refs > 0 && !--refs) {
dispose();
dispose = null;
update();
update = null;
}
};
Expand All @@ -187,13 +212,24 @@ ${esModule ? 'export default' : 'module.exports ='} exported;`;
const hmrCode = this.hot
? `
if (module.hot) {
if (!content.locals) {
if (!content.locals || module.hot.invalidate) {
var isEqualLocals = ${isEqualLocals.toString()};
var oldLocals = content.locals;
module.hot.accept(
${loaderUtils.stringifyRequest(this, `!!${request}`)},
function () {
${
esModule
? `update(content);`
? `if (!isEqualLocals(oldLocals, content.locals)) {
module.hot.invalidate();
return;
}
oldLocals = content.locals;
update(content);`
: `var newContent = require(${loaderUtils.stringifyRequest(
this,
`!!${request}`
Expand All @@ -205,6 +241,14 @@ if (module.hot) {
newContent = [[module.id, newContent, '']];
}
if (!isEqualLocals(oldLocals, newContent.locals)) {
module.hot.invalidate();
return;
}
oldLocals = newContent.locals;
update(newContent);`
}
}
Expand All @@ -226,8 +270,7 @@ if (module.hot) {
import content from ${loaderUtils.stringifyRequest(
this,
`!!${request}`
)};
var clonedContent = content;`
)};`
: `var api = require(${loaderUtils.stringifyRequest(
this,
`!${path.join(__dirname, 'runtime/injectStylesIntoStyleTag.js')}`
Expand All @@ -251,11 +294,9 @@ options.singleton = ${isSingleton};
var update = api(content, options);
var exported = content.locals ? content.locals : {};
${hmrCode}
${esModule ? 'export default' : 'module.exports ='} exported;`;
${esModule ? 'export default' : 'module.exports ='} content.locals || {};`;
}
}
};
Expand Down
23 changes: 23 additions & 0 deletions src/runtime/isEqualLocals.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
function isEqualLocals(a, b) {
if ((!a && b) || (a && !b)) {
return false;
}

let p;

for (p in a) {
if (a[p] !== b[p]) {
return false;
}
}

for (p in b) {
if (!a[p]) {
return false;
}
}

return true;
}

module.exports = isEqualLocals;
47 changes: 47 additions & 0 deletions test/runtime/isEqualLocals.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/* eslint-env browser */

import isEqualLocals from '../../src/runtime/isEqualLocals';

describe('isEqualLocals', () => {
it('should work', () => {
expect(isEqualLocals()).toBe(true);
expect(isEqualLocals({}, {})).toBe(true);
// eslint-disable-next-line no-undefined
expect(isEqualLocals(undefined, undefined)).toBe(true);
expect(isEqualLocals({ foo: 'bar' }, { foo: 'bar' })).toBe(true);
expect(
isEqualLocals({ foo: 'bar', bar: 'baz' }, { foo: 'bar', bar: 'baz' })
).toBe(true);
expect(
isEqualLocals({ foo: 'bar', bar: 'baz' }, { bar: 'baz', foo: 'bar' })
).toBe(true);
expect(
isEqualLocals({ bar: 'baz', foo: 'bar' }, { foo: 'bar', bar: 'baz' })
).toBe(true);

// eslint-disable-next-line no-undefined
expect(isEqualLocals(undefined, { foo: 'bar' })).toBe(false);
// eslint-disable-next-line no-undefined
expect(isEqualLocals({ foo: 'bar' }, undefined)).toBe(false);

expect(isEqualLocals({ foo: 'bar' }, { foo: 'baz' })).toBe(false);

expect(isEqualLocals({ foo: 'bar' }, { bar: 'bar' })).toBe(false);
expect(isEqualLocals({ bar: 'bar' }, { foo: 'bar' })).toBe(false);

expect(isEqualLocals({ foo: 'bar' }, { foo: 'bar', bar: 'baz' })).toBe(
false
);
expect(isEqualLocals({ foo: 'bar', bar: 'baz' }, { foo: 'bar' })).toBe(
false
);

// Should never happen, but let's test it
expect(isEqualLocals({ foo: 'bar' }, { foo: true })).toBe(false);
expect(isEqualLocals({ foo: true }, { foo: 'bar' })).toBe(false);
// eslint-disable-next-line no-undefined
expect(isEqualLocals({ foo: 'bar' }, { foo: undefined })).toBe(false);
expect(isEqualLocals({ foo: undefined }, { foo: 'bar' })).toBe(false);
expect(isEqualLocals({ foo: { foo: 'bar' } }, { foo: 'bar' })).toBe(false);
});
});

0 comments on commit 6d14e0a

Please sign in to comment.