Skip to content

Commit dd38100

Browse files
authored
feat: add support for SSR style injection (#126)
1 parent 0dafb0a commit dd38100

File tree

7 files changed

+87
-9
lines changed

7 files changed

+87
-9
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,3 +200,15 @@ Type: `Boolean`
200200
Default: `false`
201201

202202
Use GraphicsMagic instead of ImageMagick
203+
204+
##### `registerStylesSSR`
205+
206+
Type: `Boolean`
207+
Default: `false`
208+
209+
Register Vuetify styles in [vue-style-loader](https://github.com/vuejs/vue-style-loader).
210+
211+
This fixes styles not being loaded when doing SSR (for example when using [@nuxtjs/vuetify](https://github.com/nuxt-community/vuetify-module)).
212+
As Vuetify imports styles with JS, without this option, they do not get picked up by SSR.
213+
214+
⚠️ This option requires having `manualInject` set to `true` in [`vue-style-loader`](https://github.com/vuejs/vue-style-loader#options) config.

lib/loader.js

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,42 @@ function getMatches (type, items, matches, component) {
3232
return imports
3333
}
3434

35-
function install (install, content, imports) {
35+
function injectStylesSSR (imports) {
36+
const styles = imports.map(componentImport => (componentImport[2] || [])).reduce((acc, styles) => {
37+
styles && styles.forEach(style => acc.add(style))
38+
return acc
39+
}, new Set())
40+
41+
if (styles.size) {
42+
return `
43+
if (process.env.VUE_ENV === 'server') {
44+
const options = typeof component.exports === 'function'
45+
? component.exports.extendOptions
46+
: component.options
47+
const existing = options.beforeCreate
48+
const hook = function () {
49+
${[...styles].map((style) => ` require('vuetify/${style}').__inject__(this.$ssrContext)`).join('\n')}
50+
}
51+
options.beforeCreate = existing
52+
? [].concat(existing, hook)
53+
: [hook]
54+
}
55+
`
56+
}
57+
return ""
58+
}
59+
60+
function install (install, content, imports, options = {}) {
3661
if (imports.length) {
3762
let newContent = '/* vuetify-loader */\n'
3863
newContent += `import ${install} from ${loaderUtils.stringifyRequest(this, '!' + runtimePaths[install])}\n`
3964
newContent += imports.map(i => i[1]).join('\n') + '\n'
4065
newContent += `${install}(component, {${imports.map(i => i[0]).join(',')}})\n`
4166

67+
if (options.registerStylesSSR) {
68+
newContent += injectStylesSSR(imports, newContent)
69+
}
70+
4271
// Insert our modification before the HMR code
4372
const hotReload = content.indexOf('/* hot reload */')
4473
if (hotReload > -1) {
@@ -58,6 +87,7 @@ module.exports = async function (content, sourceMap) {
5887
const options = {
5988
match: [],
6089
attrsMatch: [],
90+
registerStylesSSR: false,
6191
...loaderUtils.getOptions(this)
6292
}
6393

@@ -110,7 +140,7 @@ module.exports = async function (content, sourceMap) {
110140
})
111141
}
112142

113-
content = install.call(this, 'installComponents', content, getMatches.call(this, 'Tag', tags, options.match, component))
143+
content = install.call(this, 'installComponents', content, getMatches.call(this, 'Tag', tags, options.match, component), options)
114144
content = install.call(this, 'installDirectives', content, getMatches.call(this, 'Attr', attrs, options.attrsMatch, component))
115145
}
116146

lib/matcher/generator.js

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
const Module = require('module')
2+
const decache = require('decache')
23
const originalLoader = Module._load
34
const { readdirSync, statSync } = require('fs')
4-
const { dirname, join } = require('path')
5+
const { dirname, join, relative } = require('path')
6+
7+
let groupStyleDependencies = new Set()
8+
const vuetifyRootPath = join(require.resolve('vuetify/es5/components'), '../../..')
59

610
Module._load = function _load (request, parent) {
711
if (request.endsWith('.styl')) return
812
if (request.endsWith('.scss')) return
9-
if (request.endsWith('.sass')) return
13+
if (request.endsWith('.sass')) {
14+
groupStyleDependencies.add(relative(vuetifyRootPath, join(dirname(parent.filename), request)))
15+
}
1016
else return originalLoader(request, parent)
1117
}
1218

@@ -16,21 +22,37 @@ const directives = Object.keys(require('vuetify/es5/directives'))
1622
const dir = dirname(require.resolve('vuetify/es5/components'))
1723

1824
const components = new Map()
25+
const styles = new Map()
1926
readdirSync(dir).forEach(group => {
2027
if (!statSync(join(dir, group)).isDirectory()) return
2128

29+
groupStyleDependencies = new Set()
2230
const component = require(`vuetify/es5/components/${group}`).default
2331
if (component.hasOwnProperty('$_vuetify_subcomponents')) {
2432
Object.keys(component.$_vuetify_subcomponents)
25-
.forEach(name => components.set(name, group))
33+
.forEach(name => {
34+
components.set(name, group)
35+
styles.set(name, groupStyleDependencies)
36+
})
2637
} else {
2738
components.set(group, group)
39+
styles.set(group, groupStyleDependencies)
2840
}
41+
// This is required so that groups picks up dependencies they have to other groups.
42+
// For example VTabs depends on the style from VSlideGroup (VSlideGroup.sass).
43+
// As VSlideGroup will be loaded before (alphabetically), `Module._load` wouldn't be called for it when processing VTabs (as it would be already in the require cache).
44+
// By busting the require cache for each groups we unsure that when loading VTabs we do call `Module._load` for `VSlideGroup.sass` and it gets added to the dependencies.
45+
decache(`vuetify/es5/components/${group}`)
2946
})
3047

48+
// This makes sure Vuetify main styles will be injected.
49+
// Using VApp as it's must be present for Vuetify to work, and it must only be there once.
50+
styles.get('VApp').add('src/styles/main.sass')
51+
3152
Module._load = originalLoader
3253

3354
module.exports = {
3455
directives,
35-
components
56+
components,
57+
styles
3658
}

lib/matcher/tag.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
const { components } = require('./generator')
1+
const { components, styles } = require('./generator')
22

33
module.exports = function match (_, { kebabTag, camelTag: tag }) {
44
if (!kebabTag.startsWith('v-')) return
55

66
if (components.has(tag)) {
7-
return [tag, `import { ${tag} } from 'vuetify/lib/components/${components.get(tag)}';`]
7+
return [tag, `import { ${tag} } from 'vuetify/lib/components/${components.get(tag)}';`, styles.get(tag)]
88
}
99
}

lib/plugin.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,8 @@ class VuetifyLoaderPlugin {
135135
loader: require.resolve('./loader'),
136136
options: {
137137
match: this.options.match || [],
138-
attrsMatch: this.options.attrsMatch || []
138+
attrsMatch: this.options.attrsMatch || [],
139+
registerStylesSSR: this.options.registerStylesSSR || false
139140
}
140141
},
141142
...rule.use

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
},
1818
"homepage": "https://github.com/vuetifyjs/vuetify-loader#readme",
1919
"dependencies": {
20+
"decache": "^4.5.1",
2021
"file-loader": "^4.0.0",
2122
"loader-utils": "^1.2.0"
2223
},

yarn.lock

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,11 @@ cache-base@^1.0.1:
490490
union-value "^1.0.0"
491491
unset-value "^1.0.0"
492492

493+
callsite@^1.0.0:
494+
version "1.0.0"
495+
resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20"
496+
integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA=
497+
493498
chokidar@^2.0.2:
494499
version "2.0.4"
495500
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.4.tgz#356ff4e2b0e8e43e322d18a372460bbcf3accd26"
@@ -712,6 +717,13 @@ debug@^3.1.0:
712717
dependencies:
713718
ms "^2.1.1"
714719

720+
decache@^4.5.1:
721+
version "4.5.1"
722+
resolved "https://registry.yarnpkg.com/decache/-/decache-4.5.1.tgz#94a977a88a4188672c96550ec4889582ceecdf49"
723+
integrity sha512-5J37nATc6FmOTLbcsr9qx7Nm28qQyg1SK4xyEHqM0IBkNhWFp0Sm+vKoWYHD8wq+OUEb9jLyaKFfzzd1A9hcoA==
724+
dependencies:
725+
callsite "^1.0.0"
726+
715727
decode-uri-component@^0.2.0:
716728
version "0.2.0"
717729
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"

0 commit comments

Comments
 (0)