Skip to content

Commit 2712ccc

Browse files
committed
improve css modules hot-reload
1 parent e6daf96 commit 2712ccc

File tree

2 files changed

+59
-26
lines changed

2 files changed

+59
-26
lines changed

lib/component-normalizer.js

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ module.exports = function normalizeComponent (
4545
}
4646
// inject component styles
4747
if (injectStyles) {
48-
injectStyles(this, context)
48+
injectStyles.call(this, context)
4949
}
5050
// register component module identifier for async chunk inferrence
5151
if (context && context._registeredComponents) {
@@ -56,19 +56,7 @@ module.exports = function normalizeComponent (
5656
// never gets called
5757
options._ssrRegister = hook
5858
} else if (injectStyles) {
59-
var injected = false // only need to inject once on the client
60-
var cssModules
61-
hook = function () {
62-
if (!injected) {
63-
injected = true
64-
cssModules = injectStyles({})
65-
}
66-
if (cssModules) {
67-
for (var key in cssModules) {
68-
this[key] = cssModules[key]
69-
}
70-
}
71-
}
59+
hook = injectStyles
7260
}
7361

7462
if (hook) {

lib/loader.js

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -278,12 +278,23 @@ module.exports = function (content) {
278278
var output = ''
279279
var parts = parse(content, fileName, this.sourceMap)
280280
var hasScoped = parts.styles.some(function (s) { return s.scoped })
281+
var needsHotReload = (
282+
!isServer &&
283+
!isProduction &&
284+
(parts.script || parts.template)
285+
)
286+
287+
if (needsHotReload) {
288+
output += 'var disposed = false\n'
289+
}
281290

282291
// add requires for styles
283292
var cssModules
284293
if (parts.styles.length) {
285-
var styleInjectionCode =
286-
'function injectStyle (cssModuleTarget, ssrContext) {\n'
294+
var styleInjectionCode = 'function injectStyle (ssrContext) {\n'
295+
if (needsHotReload) {
296+
styleInjectionCode += ` if (disposed) return\n`
297+
}
287298
parts.styles.forEach(function (style, i) {
288299
// require style
289300
var requireString = style.src
@@ -301,7 +312,12 @@ module.exports = function (content) {
301312
var moduleName = (style.module === true) ? '$style' : style.module
302313
// setCssModule
303314
if (moduleName) {
304-
cssModules = cssModules || {}
315+
if (!cssModules) {
316+
cssModules = {}
317+
if (needsHotReload) {
318+
output += `var cssModules = {}\n`
319+
}
320+
}
305321
if (moduleName in cssModules) {
306322
loaderContext.emitError('CSS module name "' + moduleName + '" is not unique!')
307323
styleInjectionCode += invokeStyle(requireString)
@@ -315,15 +331,36 @@ module.exports = function (content) {
315331
requireString += '.locals'
316332
}
317333

318-
styleInjectionCode += invokeStyle('cssModuleTarget["' + moduleName + '"] = ' + requireString)
334+
if (!needsHotReload) {
335+
styleInjectionCode += invokeStyle('this["' + moduleName + '"] = ' + requireString)
336+
} else {
337+
// handle hot reload for CSS modules.
338+
// we store the exported locals in an object and proxy to it by
339+
// defining getters inside component instances' lifecycle hook.
340+
styleInjectionCode +=
341+
invokeStyle(`cssModules["${moduleName}"] = ${requireString}`) +
342+
`Object.defineProperty(this, "${moduleName}", { get: function () { return cssModules["${moduleName}"] }})\n`
343+
344+
var requirePath = getRequireString('styles', style, i, style.scoped)
345+
output +=
346+
`module.hot && module.hot.accept([${requirePath}], function () {\n` +
347+
// 1. check if style has been injected
348+
` var oldLocals = cssModules["${moduleName}"]\n` +
349+
` if (!oldLocals) return\n` +
350+
// 2. re-import (side effect: updates the <style>)
351+
` var newLocals = ${requireString}\n` +
352+
// 3. compare new and old locals to see if selectors changed
353+
` if (JSON.stringify(newLocals) === JSON.stringify(oldLocals)) return\n` +
354+
// 4. locals changed. Update and force re-render.
355+
` cssModules["${moduleName}"] = newLocals\n` +
356+
` require("${hotReloadAPIPath}").rerender("${moduleId}")\n` +
357+
`})\n`
358+
}
319359
}
320360
} else {
321361
styleInjectionCode += invokeStyle(requireString)
322362
}
323363
})
324-
if (cssModules) {
325-
styleInjectionCode += ' return cssModuleTarget\n'
326-
}
327364
styleInjectionCode += '}\n'
328365
output += styleInjectionCode
329366
}
@@ -435,11 +472,7 @@ module.exports = function (content) {
435472

436473
if (!query.inject) {
437474
// hot reload
438-
if (
439-
!isServer &&
440-
!isProduction &&
441-
(parts.script || parts.template)
442-
) {
475+
if (needsHotReload) {
443476
output +=
444477
'\n/* hot reload */\n' +
445478
'if (module.hot) {(function () {\n' +
@@ -452,9 +485,21 @@ module.exports = function (content) {
452485
' hotAPI.createRecord("' + moduleId + '", Component.options)\n' +
453486
' } else {\n'
454487
// update
488+
if (cssModules) {
489+
output +=
490+
' if (module.hot.data.cssModules && Object.keys(module.hot.data.cssModules) !== Object.keys(cssModules)) {\n' +
491+
' delete Component.options._Ctor\n' +
492+
' }\n'
493+
}
455494
output +=
456495
' hotAPI.reload("' + moduleId + '", Component.options)\n' +
457496
' }\n'
497+
// dispose
498+
output +=
499+
' module.hot.dispose(function (data) {\n' +
500+
(cssModules ? ' data.cssModules = cssModules\n' : '') +
501+
' disposed = true\n' +
502+
' })\n'
458503
output += '})()}\n'
459504
}
460505
// final export

0 commit comments

Comments
 (0)