From 6d14e0aa7bac1d8ba8e60b4d12f7cd33078763b7 Mon Sep 17 00:00:00 2001 From: Evilebot Tnawi Date: Fri, 24 Apr 2020 15:26:13 +0300 Subject: [PATCH] feat: hot module replacement for css modules --- src/index.js | 103 ++++++++++++++++++++--------- src/runtime/isEqualLocals.js | 23 +++++++ test/runtime/isEqualLocals.test.js | 47 +++++++++++++ 3 files changed, 142 insertions(+), 31 deletions(-) create mode 100644 src/runtime/isEqualLocals.js create mode 100644 test/runtime/isEqualLocals.test.js diff --git a/src/index.js b/src/index.js index 5907d16f..41e25cd8 100644 --- a/src/index.js +++ b/src/index.js @@ -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 = () => {}; @@ -86,7 +88,7 @@ var update = api(content, options); ${hmrCode} -${esModule ? `export default {}` : ''}`; +${esModule ? 'export default {}' : ''}`; } case 'lazyStyleTag': @@ -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(); } }); }` @@ -147,7 +176,7 @@ if (module.hot) { } var refs = 0; -var dispose; +var update; var options = ${JSON.stringify(options)}; options.insert = ${insert}; @@ -155,22 +184,18 @@ 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; } }; @@ -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}` @@ -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);` } } @@ -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')}` @@ -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 || {};`; } } }; diff --git a/src/runtime/isEqualLocals.js b/src/runtime/isEqualLocals.js new file mode 100644 index 00000000..1002ff1f --- /dev/null +++ b/src/runtime/isEqualLocals.js @@ -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; diff --git a/test/runtime/isEqualLocals.test.js b/test/runtime/isEqualLocals.test.js new file mode 100644 index 00000000..0a5c458b --- /dev/null +++ b/test/runtime/isEqualLocals.test.js @@ -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); + }); +});