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 for css modules #466

Merged
merged 3 commits into from
Apr 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
});
});