diff --git a/README.md b/README.md index 75663ea..23d46ae 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/lib/loader.js b/lib/loader.js index ce99d72..17de12d 100644 --- a/lib/loader.js +++ b/lib/loader.js @@ -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) { @@ -58,6 +87,7 @@ module.exports = async function (content, sourceMap) { const options = { match: [], attrsMatch: [], + registerStylesSSR: false, ...loaderUtils.getOptions(this) } @@ -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)) } diff --git a/lib/matcher/generator.js b/lib/matcher/generator.js index 9a38f3c..771a954 100755 --- a/lib/matcher/generator.js +++ b/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) } @@ -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 } diff --git a/lib/matcher/tag.js b/lib/matcher/tag.js index a7fec68..e60c2d3 100644 --- a/lib/matcher/tag.js +++ b/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)] } } diff --git a/lib/plugin.js b/lib/plugin.js index ae04b05..6dc22c2 100644 --- a/lib/plugin.js +++ b/lib/plugin.js @@ -135,7 +135,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 } }, ...rule.use diff --git a/package.json b/package.json index 1cfaa0a..9fef416 100755 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/yarn.lock b/yarn.lock index e2d08bd..c5f1602 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -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"