Skip to content

Commit 234d48b

Browse files
committed
feat: dynamic style injection
1 parent 3a16ce5 commit 234d48b

File tree

5 files changed

+135
-19
lines changed

5 files changed

+135
-19
lines changed

Diff for: lib/customBlocks.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = function genCustomBlocksCode () {
2+
3+
}

Diff for: lib/index.js

+19-15
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const plugin = require('./plugin')
66
const selectBlock = require('./select')
77
const loaderUtils = require('loader-utils')
88
const { genHotReloadCode } = require('./hotReload')
9+
const genStyleInjectionCode = require('./styleInjection')
910
const componentNormalizerPath = require.resolve('./runtime/componentNormalizer')
1011

1112
module.exports = function (source) {
@@ -22,9 +23,12 @@ module.exports = function (source) {
2223
resourceQuery
2324
} = loaderContext
2425

26+
const incomingQuery = qs.parse(resourceQuery.slice(1))
27+
const options = loaderUtils.getOptions(loaderContext) || {}
28+
2529
const isServer = target === 'node'
30+
const isShadow = incomingQuery.shadow != null
2631
const isProduction = minimize || process.env.NODE_ENV === 'production'
27-
const options = loaderUtils.getOptions(loaderContext) || {}
2832
const fileName = path.basename(resourcePath)
2933
const context = rootContext || process.cwd()
3034
const sourceRoot = path.dirname(path.relative(context, resourcePath))
@@ -39,7 +43,6 @@ module.exports = function (source) {
3943
// if the query has a type field, this is a language block request
4044
// e.g. foo.vue?type=template&id=xxxxx
4145
// and we will return early
42-
const incomingQuery = qs.parse(resourceQuery.slice(1))
4346
if (incomingQuery.type) {
4447
return selectBlock(descriptor, loaderContext, incomingQuery)
4548
}
@@ -93,33 +96,34 @@ module.exports = function (source) {
9396
}
9497

9598
// styles
96-
// TODO construct an injection function instead
97-
let styleImports = ``
99+
let styleInjectionCode = ``
98100
if (descriptor.styles.length) {
99-
styleImports = descriptor.styles.map((style, i) => {
100-
const src = style.src || resourcePath
101-
const langQuery = getLangQuery(style, 'css')
102-
const scopedQuery = style.scoped ? `&scoped&id=${id}` : ``
103-
const query = `?vue&type=style&index=${i}${langQuery}${scopedQuery}`
104-
const request = stringifyRequest(src + query)
105-
return `import style${i} from ${request}`
106-
}).join('\n')
101+
styleInjectionCode = genStyleInjectionCode(
102+
loaderContext,
103+
descriptor.styles,
104+
id,
105+
resourcePath,
106+
stringifyRequest,
107+
getLangQuery,
108+
needsHotReload,
109+
isServer || isShadow // needs explicit injection?
110+
)
107111
}
108112

109113
let code = `
110114
${templateImport}
111115
${scriptImport}
112-
${styleImports}
116+
${styleInjectionCode}
113117
import normalizer from ${stringifyRequest(`!${componentNormalizerPath}`)}
114118
var component = normalizer(
115119
script,
116120
render,
117121
staticRenderFns,
118122
${hasFunctional ? `true` : `false`},
119-
null, // TODO style injection
123+
${/injectStyles/.test(styleInjectionCode) ? `injectStyles` : `null`},
120124
${hasScoped ? JSON.stringify(id) : `null`},
121125
${isServer ? JSON.stringify(hash(request)) : `null`}
122-
${incomingQuery.shadow ? `,true` : ``}
126+
${isShadow ? `,true` : ``}
123127
)
124128
`.trim()
125129

Diff for: lib/styleInjection.js

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
const hotReloadAPIPath = require.resolve('vue-hot-reload-api')
2+
3+
module.exports = function genStyleInjectionCode (
4+
loaderContext,
5+
styles,
6+
id,
7+
resourcePath,
8+
stringifyRequest,
9+
getLangQuery,
10+
needsHotReload,
11+
needsExplicitInjection
12+
) {
13+
let styleImportsCode = ``
14+
let styleInjectionCode = ``
15+
let cssModulesHotReloadCode = ``
16+
17+
const hasCSSModules = false
18+
const cssModuleNames = new Map()
19+
20+
function genStyleRequest (style, i) {
21+
const src = style.src || resourcePath
22+
const langQuery = getLangQuery(style, 'css')
23+
const scopedQuery = style.scoped ? `&scoped&id=${id}` : ``
24+
const query = `?vue&type=style&index=${i}${langQuery}${scopedQuery}`
25+
return stringifyRequest(src + query)
26+
}
27+
28+
function genCSSModulesCode (style, request, i) {
29+
const moduleName = style.module === true ? '$style' : style.module
30+
if (cssModuleNames.has(moduleName)) {
31+
loaderContext.emitError(`CSS module name ${moduleName} is not unique!`)
32+
}
33+
cssModuleNames.set(moduleName, true)
34+
35+
// `(vue-)style-loader` exports the name-to-hash map directly
36+
// `css-loader` exports it in `.locals`
37+
const locals = `(style${i}.locals || style${i})`
38+
const name = JSON.stringify(moduleName)
39+
40+
if (!needsHotReload) {
41+
styleInjectionCode += `this[${name}] = ${locals}`
42+
} else {
43+
styleInjectionCode += `
44+
cssModules[${name}] = ${locals}
45+
Object.defineProperty(this, ${name}, {
46+
get: function () {
47+
return cssModules[${name}]
48+
}
49+
})
50+
`
51+
cssModulesHotReloadCode += `
52+
module.hot && module.hot.accept([${request}], function () {
53+
var oldLocals = cssModules[${name}]
54+
if (oldLocals) {
55+
var newLocals = require(${request})
56+
if (JSON.stringify(newLocals) !== JSON.stringify(oldLocals)) {
57+
cssModules[${name}] = newLocals
58+
require("${hotReloadAPIPath}").rerender("${id}")
59+
}
60+
}
61+
})
62+
`
63+
}
64+
}
65+
66+
// explicit injection is needed in SSR (for critical CSS collection)
67+
// or in Shadow Mode (for injection into shadow root)
68+
// In these modes, vue-style-loader exports objects with the __inject__
69+
// method; otherwise we simply import the styles.
70+
if (!needsExplicitInjection) {
71+
styles.forEach((style, i) => {
72+
const request = genStyleRequest(style, i)
73+
styleImportsCode += `import style${i} from ${request}\n`
74+
if (style.module) genCSSModulesCode(style, request, i)
75+
})
76+
} else {
77+
styles.forEach((style, i) => {
78+
const request = genStyleRequest(style, i)
79+
styleInjectionCode += (
80+
`var style${i} = require(${request})\n` +
81+
`if (style${i}.__inject__) style${i}.__inject__(context)\n`
82+
)
83+
if (style.module) genCSSModulesCode(style, request, i)
84+
})
85+
}
86+
87+
if (!needsExplicitInjection && !hasCSSModules) {
88+
return styleImportsCode
89+
}
90+
91+
return `
92+
${styleImportsCode}
93+
${hasCSSModules && needsHotReload ? `var cssModules = {}` : ``}
94+
${needsHotReload ? `var disposed = false` : ``}
95+
96+
function injectStyles (context) {
97+
${needsHotReload ? `if (disposed) return` : ``}
98+
${styleInjectionCode}
99+
}
100+
101+
${needsHotReload ? `
102+
module.hot && module.hot.dispose(function (data) {
103+
disposed = true
104+
})
105+
` : ``}
106+
107+
${cssModulesHotReloadCode}
108+
`.trim()
109+
}

Diff for: package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@
7474
"source-map": "^0.5.6",
7575
"vue-component-compiler": "vuejs/vue-component-compiler#master",
7676
"vue-hot-reload-api": "^2.3.0",
77-
"vue-style-loader": "^4.0.2",
77+
"vue-style-loader": "^4.1.0",
7878
"vue-template-es2015-compiler": "^1.6.0"
7979
}
8080
}

Diff for: yarn.lock

+3-3
Original file line numberDiff line numberDiff line change
@@ -8026,9 +8026,9 @@ vue-server-renderer@^2.5.16:
80268026
serialize-javascript "^1.3.0"
80278027
source-map "0.5.6"
80288028

8029-
vue-style-loader@^4.0.2:
8030-
version "4.0.2"
8031-
resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-4.0.2.tgz#e89aa4702a0c6b9630d8de70b1cbddb06b9ad254"
8029+
vue-style-loader@^4.1.0:
8030+
version "4.1.0"
8031+
resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-4.1.0.tgz#7588bd778e2c9f8d87bfc3c5a4a039638da7a863"
80328032
dependencies:
80338033
hash-sum "^1.0.2"
80348034
loader-utils "^1.0.2"

0 commit comments

Comments
 (0)