diff --git a/lib/webpack/RemoveEmptyChunkPlugin.js b/lib/webpack/RemoveEmptyChunkPlugin.js new file mode 100644 index 0000000000..d2102c9022 --- /dev/null +++ b/lib/webpack/RemoveEmptyChunkPlugin.js @@ -0,0 +1,12 @@ +// https://github.com/webpack-contrib/mini-css-extract-plugin/issues/85 +module.exports = class Plugin { + apply (compiler) { + compiler.hooks.emit.tap('vuepress-remove-empty-chunk', compilation => { + Object.keys(compilation.assets).forEach(name => { + if (/_assets\/js\/styles\.\w{8}\.js$/.test(name)) { + delete compilation.assets[name] + } + }) + }) + } +} diff --git a/lib/webpack/baseConfig.js b/lib/webpack/baseConfig.js index ae6aaa608f..8bb028adff 100644 --- a/lib/webpack/baseConfig.js +++ b/lib/webpack/baseConfig.js @@ -7,7 +7,7 @@ module.exports = function createBaseConfig ({ publicPath, themePath, notFoundPath -}, { debug } = {}) { +}, { debug } = {}, isServer) { const markdown = require('../markdown')(siteConfig) const Config = require('webpack-chain') const { VueLoaderPlugin } = require('vue-loader') @@ -142,10 +142,12 @@ module.exports = function createBaseConfig ({ applyLoaders(normalRule, false) function applyLoaders (rule, modules) { - if (isProd) { - rule.use('extract-css-loader').loader(CSSExtractPlugin.loader) - } else { - rule.use('vue-style-loader').loader('vue-style-loader') + if (!isServer) { + if (isProd) { + rule.use('extract-css-loader').loader(CSSExtractPlugin.loader) + } else { + rule.use('vue-style-loader').loader('vue-style-loader') + } } rule.use('css-loader').loader('css-loader').options({ @@ -174,10 +176,39 @@ module.exports = function createBaseConfig ({ .plugin('vue-loader') .use(VueLoaderPlugin) - if (isProd) { + if (isProd && !isServer) { config .plugin('extract-css') - .use(CSSExtractPlugin, [{ filename: '_assets/css/styles.[hash:8].css' }]) + .use(CSSExtractPlugin, [{ + filename: '_assets/css/styles.[chunkhash:8].css' + }]) + + // ensure all css are extracted together. + // since most of the CSS will be from the theme and very little + // CSS will be from async chunks + config + .set('optimization', { + splitChunks: { + cacheGroups: { + chunks: 'all', + styles: { + name: 'styles', + // necessary for extraction to include md files as well + test: m => /css-extract/.test(m.type), + chunks: 'all', + enforce: true + } + } + } + }) + + // enforcing all styles extraction leaves an empty styles chunk. + // prevent it from being emitted. + // this is a bug in mini-css-extract-plugin + // https://github.com/webpack-contrib/mini-css-extract-plugin/issues/85 + config + .plugin('remove-empty-chunk') + .use(require('./RemoveEmptyChunkPlugin')) } return config diff --git a/lib/webpack/clientConfig.js b/lib/webpack/clientConfig.js index 9a1f5c550c..57ec9d8945 100644 --- a/lib/webpack/clientConfig.js +++ b/lib/webpack/clientConfig.js @@ -29,7 +29,13 @@ module.exports = function createClientConfig (options, cliOptions) { // generate client manifest only during build if (process.env.NODE_ENV === 'production') { - const VueSSRClientPlugin = require('vue-server-renderer/client-plugin') + // TODO this is a temp build of vue-server-renderer/client-plugin. + // Switch back after problems are resolved. + // Fixes two things: + // 1. Include CSS in preload files + // 2. filter out useless styles.xxxxx.js chunk from mini-css-extract-plugin + // https://github.com/webpack-contrib/mini-css-extract-plugin/issues/85 + const VueSSRClientPlugin = require('./clientPlugin') config .plugin('ssr-client') .use(VueSSRClientPlugin, [{ diff --git a/lib/webpack/clientPlugin.js b/lib/webpack/clientPlugin.js new file mode 100644 index 0000000000..0198f5d5f4 --- /dev/null +++ b/lib/webpack/clientPlugin.js @@ -0,0 +1,99 @@ +// Temporarily copied from a dev build + +'use strict' + +/* */ + +var isJS = function (file) { return /\.js(\?[^.]+)?$/.test(file) } + +var isCSS = function (file) { return /\.css(\?[^.]+)?$/.test(file) } + +var onEmit = function (compiler, name, hook) { + if (compiler.hooks) { + // Webpack >= 4.0.0 + compiler.hooks.emit.tapAsync(name, hook) + } else { + // Webpack < 4.0.0 + compiler.plugin('emit', hook) + } +} + +var hash = require('hash-sum') +var uniq = require('lodash.uniq') +var VueSSRClientPlugin = function VueSSRClientPlugin (options) { + if (options === void 0) options = {} + + this.options = Object.assign({ + filename: 'vue-ssr-client-manifest.json' + }, options) +} + +VueSSRClientPlugin.prototype.apply = function apply (compiler) { + var this$1 = this + + onEmit(compiler, 'vue-client-plugin', function (compilation, cb) { + var stats = compilation.getStats().toJson() + + var allFiles = uniq(stats.assets + .map(function (a) { return a.name })) + .filter(file => { + return !/styles\.\w{8}\.js$/.test(file) + }) + + var initialFiles = uniq(Object.keys(stats.entrypoints) + .map(function (name) { return stats.entrypoints[name].assets }) + .reduce(function (assets, all) { return all.concat(assets) }, []) + .filter(function (file) { return isJS(file) || isCSS(file) })) + .filter(file => { + return !/styles\.\w{8}\.js$/.test(file) + }) + + var asyncFiles = allFiles + .filter(function (file) { return isJS(file) || isCSS(file) }) + .filter(function (file) { return initialFiles.indexOf(file) < 0 }) + + var manifest = { + publicPath: stats.publicPath, + all: allFiles, + initial: initialFiles, + async: asyncFiles, + modules: { /* [identifier: string]: Array */ } + } + + var assetModules = stats.modules.filter(function (m) { return m.assets.length }) + var fileToIndex = function (file) { return manifest.all.indexOf(file) } + stats.modules.forEach(function (m) { + // ignore modules duplicated in multiple chunks + if (m.chunks.length === 1) { + var cid = m.chunks[0] + var chunk = stats.chunks.find(function (c) { return c.id === cid }) + if (!chunk || !chunk.files) { + return + } + var id = m.identifier.replace(/\s\w+$/, '') // remove appended hash + var files = manifest.modules[hash(id)] = chunk.files.map(fileToIndex) + // find all asset modules associated with the same chunk + assetModules.forEach(function (m) { + if (m.chunks.some(function (id) { return id === cid })) { + files.push.apply(files, m.assets.map(fileToIndex)) + } + }) + } + }) + + // const debug = (file, obj) => { + // require('fs').writeFileSync(__dirname + '/' + file, JSON.stringify(obj, null, 2)) + // } + // debug('stats.json', stats) + // debug('client-manifest.json', manifest) + + var json = JSON.stringify(manifest, null, 2) + compilation.assets[this$1.options.filename] = { + source: function () { return json }, + size: function () { return json.length } + } + cb() + }) +} + +module.exports = VueSSRClientPlugin diff --git a/lib/webpack/serverConfig.js b/lib/webpack/serverConfig.js index 6e63a3085f..85c4106687 100644 --- a/lib/webpack/serverConfig.js +++ b/lib/webpack/serverConfig.js @@ -6,7 +6,7 @@ module.exports = function createServerConfig (options, cliOptions) { const VueSSRServerPlugin = require('vue-server-renderer/server-plugin') const CopyPlugin = require('copy-webpack-plugin') - const config = createBaseConfig(options, cliOptions) + const config = createBaseConfig(options, cliOptions, true /* isServer */) const { sourceDir, outDir } = options config