Skip to content

Commit

Permalink
feat: add support for SSR style injection
Browse files Browse the repository at this point in the history
  • Loading branch information
adrienbaron committed Jul 5, 2020
1 parent 1f7b93b commit c064e52
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 9 deletions.
12 changes: 12 additions & 0 deletions README.md
Expand Up @@ -200,3 +200,15 @@ Type: `Boolean`
Default: `false`

Use GraphicsMagic instead of ImageMagick

##### `registerStylesSSR`

Type: `Boolean`
Default: `false`

Register Vuetify styles in [vue-style-loader](https://github.com/vuejs/vue-style-loader).

This fixes styles not being loaded when doing SSR (for example when using [@nuxtjs/vuetify](https://github.com/nuxt-community/vuetify-module)).
As Vuetify imports styles with JS, without this option, they do not get picked up by SSR.

鈿狅笍 This option requires having `manualInject` set to `true` in [`vue-style-loader`](https://github.com/vuejs/vue-style-loader#options) config.
34 changes: 32 additions & 2 deletions lib/loader.js
Expand Up @@ -32,13 +32,42 @@ function getMatches (type, items, matches, component) {
return imports
}

function install (install, content, imports) {
function injectStylesSSR (imports) {
const styles = imports.map(componentImport => (componentImport[2] || [])).reduce((acc, styles) => {
styles && styles.forEach(style => acc.add(style))
return acc
}, new Set())

if (styles.size) {
return `
if (process.server) {
const options = typeof component.exports === 'function'
? component.exports.extendOptions
: component.options
const existing = options.beforeCreate
const hook = function () {
${[...styles].map((style) => ` require('vuetify/${style}').__inject__(this.$ssrContext)`).join('\n')}
}
options.beforeCreate = existing
? [].concat(existing, hook)
: [hook]
}
`
}
return ""
}

function install (install, content, imports, options = {}) {
if (imports.length) {
let newContent = '/* vuetify-loader */\n'
newContent += `import ${install} from ${loaderUtils.stringifyRequest(this, '!' + runtimePaths[install])}\n`
newContent += imports.map(i => i[1]).join('\n') + '\n'
newContent += `${install}(component, {${imports.map(i => i[0]).join(',')}})\n`

if (options.registerStylesSSR) {
newContent += injectStylesSSR(imports, newContent)
}

// Insert our modification before the HMR code
const hotReload = content.indexOf('/* hot reload */')
if (hotReload > -1) {
Expand All @@ -58,6 +87,7 @@ module.exports = async function (content, sourceMap) {
const options = {
match: [],
attrsMatch: [],
registerStylesSSR: false,
...loaderUtils.getOptions(this)
}

Expand Down Expand Up @@ -110,7 +140,7 @@ module.exports = async function (content, sourceMap) {
})
}

content = install.call(this, 'installComponents', content, getMatches.call(this, 'Tag', tags, options.match, component))
content = install.call(this, 'installComponents', content, getMatches.call(this, 'Tag', tags, options.match, component), {registerStylesSSR: options.registerStylesSSR})
content = install.call(this, 'installDirectives', content, getMatches.call(this, 'Attr', attrs, options.attrsMatch, component))
}

Expand Down
30 changes: 26 additions & 4 deletions lib/matcher/generator.js
@@ -1,12 +1,18 @@
const Module = require('module')
const decache = require('decache')
const originalLoader = Module._load
const { readdirSync, statSync } = require('fs')
const { dirname, join } = require('path')
const { dirname, join, relative } = require('path')

let groupStyleDependencies = new Set()
const vuetifyRootPath = join(require.resolve('vuetify/es5/components'), '../../..')

Module._load = function _load (request, parent) {
if (request.endsWith('.styl')) return
if (request.endsWith('.scss')) return
if (request.endsWith('.sass')) return
if (request.endsWith('.sass')) {
groupStyleDependencies.add(relative(vuetifyRootPath, join(dirname(parent.filename), request)))
}
else return originalLoader(request, parent)
}

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

const components = new Map()
const styles = new Map()
readdirSync(dir).forEach(group => {
if (!statSync(join(dir, group)).isDirectory()) return

groupStyleDependencies = new Set()
const component = require(`vuetify/es5/components/${group}`).default
if (component.hasOwnProperty('$_vuetify_subcomponents')) {
Object.keys(component.$_vuetify_subcomponents)
.forEach(name => components.set(name, group))
.forEach(name => {
components.set(name, group)
styles.set(name, groupStyleDependencies)
})
} else {
components.set(group, group)
styles.set(group, groupStyleDependencies)
}
// This is required so that groups picks up dependencies they have to other groups.
// For example VTabs depends on the style from VSlideGroup (VSlideGroup.sass).
// 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).
// 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.
decache(`vuetify/es5/components/${group}`)
})

// This makes sure Vuetify main styles will be injected.
// Using VApp as it's must be present for Vuetify to work, and it must only be there once.
styles.get('VApp').add('src/styles/main.sass')

Module._load = originalLoader

module.exports = {
directives,
components
components,
styles
}
4 changes: 2 additions & 2 deletions lib/matcher/tag.js
@@ -1,9 +1,9 @@
const { components } = require('./generator')
const { components, styles } = require('./generator')

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

if (components.has(tag)) {
return [tag, `import { ${tag} } from 'vuetify/lib/components/${components.get(tag)}';`]
return [tag, `import { ${tag} } from 'vuetify/lib/components/${components.get(tag)}';`, styles.get(tag)]
}
}
3 changes: 2 additions & 1 deletion lib/plugin.js
Expand Up @@ -36,7 +36,8 @@ class VuetifyLoaderPlugin {
loader: require.resolve('./loader'),
options: {
match: this.options.match || [],
attrsMatch: this.options.attrsMatch || []
attrsMatch: this.options.attrsMatch || [],
registerStylesSSR: this.options.registerStylesSSR || false
}
})

Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -17,6 +17,7 @@
},
"homepage": "https://github.com/vuetifyjs/vuetify-loader#readme",
"dependencies": {
"decache": "^4.5.1",
"file-loader": "^4.0.0",
"loader-utils": "^1.2.0"
},
Expand Down
12 changes: 12 additions & 0 deletions yarn.lock
Expand Up @@ -490,6 +490,11 @@ cache-base@^1.0.1:
union-value "^1.0.0"
unset-value "^1.0.0"

callsite@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20"
integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA=

chokidar@^2.0.2:
version "2.0.4"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.4.tgz#356ff4e2b0e8e43e322d18a372460bbcf3accd26"
Expand Down Expand Up @@ -712,6 +717,13 @@ debug@^3.1.0:
dependencies:
ms "^2.1.1"

decache@^4.5.1:
version "4.5.1"
resolved "https://registry.yarnpkg.com/decache/-/decache-4.5.1.tgz#94a977a88a4188672c96550ec4889582ceecdf49"
integrity sha512-5J37nATc6FmOTLbcsr9qx7Nm28qQyg1SK4xyEHqM0IBkNhWFp0Sm+vKoWYHD8wq+OUEb9jLyaKFfzzd1A9hcoA==
dependencies:
callsite "^1.0.0"

decode-uri-component@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
Expand Down

0 comments on commit c064e52

Please sign in to comment.