diff --git a/bin/nuxt-dev b/bin/nuxt-dev index df72c433c6..7f52e988f5 100755 --- a/bin/nuxt-dev +++ b/bin/nuxt-dev @@ -87,12 +87,10 @@ function listenOnConfigChanges(nuxt, server) { options.rootDir = rootDir nuxt.close() .then(() => { - nuxt.renderer = null debug('Rebuilding the app...') - return new Nuxt(options).build() - }) - .then((nuxt) => { + var nuxt = new Nuxt(options) server.nuxt = nuxt + return nuxt.ready() }) .catch((error) => { console.error('Error while rebuild the app:', error) // eslint-disable-line no-console diff --git a/lib/builder.js b/lib/builder.js index e2c17ab7e9..2f1eb98b35 100644 --- a/lib/builder.js +++ b/lib/builder.js @@ -7,11 +7,11 @@ import webpack from 'webpack' import serialize from 'serialize-javascript' import { join, resolve, basename, dirname } from 'path' import Tapable from 'tappable' -import chalk from 'chalk' +import MFS from 'memory-fs' +import { sequence, parallel } from "./utils" import { r, wp, createRoutes } from './utils' import clientWebpackConfig from './webpack/client.config.js' import serverWebpackConfig from './webpack/server.config.js' -import MFS from 'memory-fs' const debug = require('debug')('nuxt:build') debug.color = 2 // Force green color @@ -23,9 +23,6 @@ const writeFile = pify(fs.writeFile) const mkdirp = pify(fs.mkdirp) const glob = pify(require('glob')) -const host = process.env.HOST || process.env.npm_package_config_nuxt_host || 'localhost' -const port = process.env.PORT || process.env.npm_package_config_nuxt_port || '3000' - export default class Builder extends Tapable { constructor (nuxt) { super() @@ -50,7 +47,7 @@ export default class Builder extends Tapable { _.defaultsDeep(this.options.build, extraDefaults) // Mute stats on dev - this.webpackStats = this.options.dev ? '' : { + this.webpackStats = this.options.dev ? false : { chunks: false, children: false, modules: false, @@ -220,7 +217,7 @@ export default class Builder extends Tapable { })) // Interpret and move template files to .nuxt/ - return Promise.all(templatesFiles.map(async ({ src, dst, options, custom }) => { + await Promise.all(templatesFiles.map(async ({ src, dst, options, custom }) => { // Add template to watchers this.options.build.watch.push(src) // Render template to dst @@ -245,7 +242,7 @@ export default class Builder extends Tapable { // Write file await writeFile(path, content, 'utf8') // Fix webpack loop (https://github.com/webpack/watchpack/issues/25#issuecomment-287789288) - const dateFS = Date.now() / 1000 - 30 + const dateFS = Date.now() / 1000 - 1000 return utimes(path, dateFS, dateFS) })) } @@ -256,16 +253,23 @@ export default class Builder extends Tapable { // Client let clientConfig = clientWebpackConfig.call(this) - clientConfig.name = '$client' compilersOptions.push(clientConfig) // Server let serverConfig = serverWebpackConfig.call(this) - serverConfig.name = '$server' compilersOptions.push(serverConfig) - // Leverage webpack multi-compiler for faster builds - this.compiler = webpack(compilersOptions) + // Simulate webpack multi compiler interface + // Separate compilers are simpler, safer and faster + this.compiler = { cache: {}, compilers: [] } + compilersOptions.forEach(compilersOption => { + this.compiler.compilers.push(webpack(compilersOption)) + }) + this.compiler.plugin = (...args) => { + this.compiler.compilers.forEach(compiler => { + compiler.plugin(...args) + }) + } // Access to compilers with name this.compiler.compilers.forEach(compiler => { @@ -274,71 +278,84 @@ export default class Builder extends Tapable { } }) - // Add middleware for dev + // Add dev Stuff if (this.options.dev) { this.webpackDev() } - // Start build - return new Promise((resolve, reject) => { - const handler = (err, multiStats) => { + // Start Builds + return parallel(this.compiler.compilers, compiler => new Promise((resolve, reject) => { + let _resolved = false + const handler = (err, stats) => { + if (_resolved) return + _resolved = true if (err) { return reject(err) } - for (let _stats of multiStats.stats) { - console.log(_stats.toString(this.webpackStats)) // eslint-disable-line no-console - if (_stats.hasErrors()) { + if (!this.options.dev) { + // Show build stats for production + console.log(stats.toString(this.webpackStats)) // eslint-disable-line no-console + if (stats.hasErrors()) { return reject(new Error('Webpack build exited with errors')) } } - // Use watch handler instead of compiler.apply('done') to prevent duplicate emits - this.applyPlugins('reload', multiStats) resolve() } if (this.options.dev) { - this.compiler.watch(this.options.watchers.webpack, handler) + if (compiler.options.name === 'client') { + // Client watch is started by dev-middleware + resolve() + } else { + // Build and watch for changes + compiler.watch(this.options.watchers.webpack, handler) + } } else { - this.compiler.run(handler) + // Production build + compiler.run(handler) } - }) + })) } webpackDev () { - debug('Adding webpack middleware...') - - // Use MFS for faster builds + // Use shared MFS + Cache for faster builds let mfs = new MFS() this.compiler.compilers.forEach(compiler => { compiler.outputFileSystem = mfs + compiler.cache = this.compiler.cache }) - // Watch - this.plugin('reload', () => { - // Show open URL - let _host = host === '0.0.0.0' ? 'localhost' : host - // eslint-disable-next-line no-console - console.log(chalk.bold(chalk.bgCyan.black(' OPEN ') + chalk.cyan(` http://${_host}:${port}\n`))) - + // Run after each compile + this.compiler.plugin('done', stats => { + console.log(stats.toString(this.webpackStats)) // eslint-disable-line no-console // Reload renderer if available if (this.nuxt.renderer) { this.nuxt.renderer.loadResources(mfs) } }) - // Create webpack Dev/Hot middleware - this.webpackDevMiddleware = pify(require('webpack-dev-middleware')(this.compiler.$client, { + // Add dev Middleware + debug('Adding webpack middleware...') + + // Create webpack dev middleware + this.webpackDevMiddleware = pify(require('webpack-dev-middleware')(this.compiler.client, { publicPath: this.options.build.publicPath, stats: this.webpackStats, - quiet: true, noInfo: true, + quiet: true, watchOptions: this.options.watchers.webpack })) - this.webpackHotMiddleware = pify(require('webpack-hot-middleware')(this.compiler.$client, { + this.webpackHotMiddleware = pify(require('webpack-hot-middleware')(this.compiler.client, { log: false, heartbeat: 2500 })) + // Stop webpack middleware on nuxt.close() + this.nuxt.plugin('close', () => new Promise(resolve => { + this.webpackDevMiddleware.close(() => resolve()) + })) + + // Start watching files this.watchFiles() } @@ -360,13 +377,21 @@ export default class Builder extends Tapable { }) /* istanbul ignore next */ const refreshFiles = _.debounce(this.generateRoutesAndFiles, 200) - // Watch for internals - this.filesWatcher = chokidar.watch(patterns, options) + + // Watch for src Files + let filesWatcher = chokidar.watch(patterns, options) .on('add', refreshFiles) .on('unlink', refreshFiles) + // Watch for custom provided files - this.customFilesWatcher = chokidar.watch(_.uniq(this.options.build.watch), options) + let customFilesWatcher = chokidar.watch(_.uniq(this.options.build.watch), options) .on('change', refreshFiles) + + // Stop watching on nuxt.close() + this.nuxt.plugin('close', () => { + filesWatcher.close() + customFilesWatcher.close() + }) } } diff --git a/lib/nuxt.js b/lib/nuxt.js index 1a14ffd9d2..499d254343 100644 --- a/lib/nuxt.js +++ b/lib/nuxt.js @@ -1,11 +1,14 @@ import Tapable from 'tappable' -import Builder from './builder' +import chalk from 'chalk' import * as Utils from './utils' import Renderer from './renderer' import ModuleContainer from './module-container' import Server from './server' import defaults from './defaults' +const defaultHost = process.env.HOST || process.env.npm_package_config_nuxt_host || 'localhost' +const defaultPort = process.env.PORT || process.env.npm_package_config_nuxt_port || '3000' + export default class Nuxt extends Tapable { constructor (_options = {}) { super() @@ -57,7 +60,7 @@ export default class Nuxt extends Tapable { if (this._builder) { return this._builder } - // const Builder = require('./builder').default + const Builder = require('./builder').default this._builder = new Builder(this) return this._builder } @@ -90,36 +93,28 @@ export default class Nuxt extends Tapable { process.exit(1) } + serverReady ({ host = defaultHost, port = defaultPort } = {}) { + let _host = host === '0.0.0.0' ? 'localhost' : host + + // eslint-disable-next-line no-console + console.log('\n' + chalk.bold(chalk.bgBlue.black(' OPEN ') + chalk.blue(` http://${_host}:${port}\n`))) + + return this.applyPluginsAsync('serverReady').catch(this.errorHandler) + } + async close (callback) { - let promises = [] - /* istanbul ignore if */ - if (this.webpackDevMiddleware) { - const p = new Promise((resolve, reject) => { - this.webpackDevMiddleware.close(() => resolve()) - }) - promises.push(p) - } - /* istanbul ignore if */ - if (this.webpackServerWatcher) { - const p = new Promise((resolve, reject) => { - this.webpackServerWatcher.close(() => resolve()) - }) - promises.push(p) - } - /* istanbul ignore if */ - if (this.filesWatcher) { - this.filesWatcher.close() - } - /* istanbul ignore if */ - if (this.customFilesWatcher) { - this.customFilesWatcher.close() - } + // Call for close + await this.applyPluginsAsync('close') - promises.push(this.applyPluginsAsync('close')) + // Remove all references + delete this._generator + delete this._builder - return Promise.all(promises).then(() => { - if (typeof callback === 'function') callback() - }) + this.initialized = false + + if (typeof callback === 'function') { + callback() + } } } diff --git a/lib/renderer.js b/lib/renderer.js index c7deae435a..d9c311fbcb 100644 --- a/lib/renderer.js +++ b/lib/renderer.js @@ -10,6 +10,7 @@ import _ from 'lodash' import { resolve, join } from 'path' import fs from 'fs-extra' import { createBundleRenderer } from 'vue-server-renderer' +import chalk from 'chalk' import { getContext, setAnsiColors, encodeHtml } from './utils' const debug = require('debug')('nuxt:render') @@ -39,6 +40,7 @@ export default class Renderer extends Tapable { errorTemplate: parseTemplate(fs.readFileSync(resolve(__dirname, 'views', 'error.html'), 'utf8')) } + // Initialize if (nuxt.initialized) { // If nuxt already initialized this._ready = this.ready().catch(this.nuxt.errorHandler) @@ -71,16 +73,18 @@ export default class Renderer extends Tapable { // Load resources from fs if (!this.options.dev) { - return this.loadResources() + await this.loadResources() } + + return this } - async loadResources (_fs = fs, distPath) { - distPath = distPath || resolve(this.options.buildDir, 'dist') + async loadResources (_fs = fs, isServer) { + let distPath = resolve(this.options.buildDir, 'dist') const resourceMap = { clientManifest: { - path: join(distPath, 'client-manifest.json'), + path: join(distPath, 'vue-ssr-client-manifest.json'), transform: JSON.parse }, serverBundle: { @@ -116,7 +120,7 @@ export default class Renderer extends Tapable { }) if (updated.length > 0) { - // debug('Updated', updated.join(', ')) + // debug('Updated', updated.join(', '), isServer) this.createRenderer() } } @@ -136,6 +140,8 @@ export default class Renderer extends Tapable { // Promisify renderToString this.bundleRenderer.renderToString = pify(this.bundleRenderer.renderToString) + + this.nuxt.serverReady() } async render (req, res) { diff --git a/lib/server.js b/lib/server.js index 09bd816c09..f0082e8e7c 100644 --- a/lib/server.js +++ b/lib/server.js @@ -8,17 +8,33 @@ class Server { this.options = nuxt.options // Initialize + if (nuxt.initialized) { + // If nuxt already initialized + this._ready = this.ready().catch(this.nuxt.errorHandler) + } else { + // Wait for hook + this.nuxt.plugin('afterInit', () => { + this._ready = this.ready() + return this._ready + }) + } + } + + async ready() { + if (this._ready) { + return this._ready + } + this.app = connect() this.server = http.createServer(this.app) - this.nuxt.ready() - .then(() => { - // Add Middleware - this.options.serverMiddleware.forEach(m => { - this.useMiddleware(m) - }) - // Add default render middleware - this.useMiddleware(this.render.bind(this)) - }) + + // Add Middleware + this.options.serverMiddleware.forEach(m => { + this.useMiddleware(m) + }) + // Add default render middleware + this.useMiddleware(this.render.bind(this)) + return this } @@ -49,12 +65,13 @@ class Server { host = host || 'localhost' port = port || 3000 this.nuxt.ready() - .then(() => { - this.server.listen(port, host, () => { - let _host = host === '0.0.0.0' ? 'localhost' : host - console.log('Ready on http://%s:%s', _host, port) // eslint-disable-line no-console + .then(() => { + this.server.listen(port, host, () => { + // Renderer calls showURL when server is really ready + // this.nuxt.showURL(host, port) + }) }) - }) + .catch(this.nuxt.errorHandler) return this } diff --git a/lib/utils.js b/lib/utils.js index 2f2fbac7b8..fcfe7eba4b 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -63,6 +63,10 @@ export function sequence (tasks, fn) { return tasks.reduce((promise, task) => promise.then(() => fn(task)), Promise.resolve()) } +export function parallel (tasks, fn) { + return Promise.all(tasks.map(task => fn(task))) +} + export function chainFn (base, fn) { /* istanbul ignore if */ if (!(fn instanceof Function)) { diff --git a/lib/webpack/base.config.js b/lib/webpack/base.config.js index fdadcbce9b..fd15de3ac2 100644 --- a/lib/webpack/base.config.js +++ b/lib/webpack/base.config.js @@ -1,6 +1,8 @@ import ExtractTextPlugin from 'extract-text-webpack-plugin' import { defaults } from 'lodash' -import { join } from 'path' +import { join, resolve, } from 'path' +import webpack from 'webpack' +import { cloneDeep } from 'lodash' import { isUrl, urlJoin } from '../utils' import vueLoaderConfig from './vue-loader.config' import { styleLoader, extractStyles } from './helpers' @@ -15,12 +17,15 @@ import { styleLoader, extractStyles } from './helpers' */ export default function webpackBaseConfig ({ isClient, isServer }) { const nodeModulesDir = join(__dirname, '..', 'node_modules') - let config = { - devtool: (this.options.dev ? 'cheap-module-source-map' : false), + + const config = { + devtool: this.options.dev ? 'cheap-module-source-map' : false, entry: { vendor: ['vue', 'vue-router', 'vue-meta'] }, output: { + path: resolve(this.options.buildDir, 'dist'), + filename: this.options.build.filenames.app, publicPath: (isUrl(this.options.build.publicPath) ? this.options.build.publicPath : urlJoin(this.options.router.base, this.options.build.publicPath)) @@ -28,7 +33,7 @@ export default function webpackBaseConfig ({ isClient, isServer }) { performance: { maxEntrypointSize: 300000, maxAssetSize: 300000, - hints: (this.options.dev ? false : 'warning') + hints: this.options.dev ? false : 'warning' }, resolve: { extensions: ['.js', '.json', '.vue', '.ts'], @@ -57,6 +62,7 @@ export default function webpackBaseConfig ({ isClient, isServer }) { ] }, module: { + noParse: /es6-promise\.js$/, // avoid webpack shimming process rules: [ { test: /\.vue$/, @@ -98,12 +104,33 @@ export default function webpackBaseConfig ({ isClient, isServer }) { }, plugins: this.options.build.plugins } + // CSS extraction if (extractStyles.call(this)) { config.plugins.push( new ExtractTextPlugin({ filename: this.options.build.filenames.css }) ) } - // Return config - return config + + // -------------------------------------- + // Dev specific config + // -------------------------------------- + if (this.options.dev) { + // + } + + // -------------------------------------- + // Production specific config + // -------------------------------------- + if (!this.options.dev) { + // This is needed in webpack 2 for minify CSS + config.plugins.push( + new webpack.LoaderOptionsPlugin({ + minimize: true + }) + ) + } + + // Clone deep avoid leaking config between Client and Server + return cloneDeep(config) } diff --git a/lib/webpack/client.config.js b/lib/webpack/client.config.js index 5bb047b234..3b2e320b9a 100644 --- a/lib/webpack/client.config.js +++ b/lib/webpack/client.config.js @@ -22,89 +22,108 @@ import base from './base.config.js' export default function webpackClientConfig () { let config = base.call(this, { isClient: true }) + config.name = 'client' + // Entry config.entry.app = resolve(this.options.buildDir, 'client.js') // Add vendors - if (this.options.store) { - config.entry.vendor.push('vuex') + if (!this.options.dev) { + if (this.options.store) { + config.entry.vendor.push('vuex') + } + config.entry.vendor = config.entry.vendor.concat(this.options.build.vendor) + // Extract vendor chunks for better caching + config.plugins.push( + new webpack.optimize.CommonsChunkPlugin({ + name: 'vendor', + filename: this.options.build.filenames.vendor, + minChunks (module) { + // A module is extracted into the vendor chunk when... + return ( + // If it's inside node_modules + /node_modules/.test(module.context) && + // Do not externalize if the request is a CSS file + !/\.(css|less|scss|sass|styl|stylus)$/.test(module.request) + ) + } + }) + ) } - config.entry.vendor = config.entry.vendor.concat(this.options.build.vendor) - // Output - config.output.path = resolve(this.options.buildDir, 'dist') - config.output.filename = this.options.build.filenames.app - - // env object defined in nuxt.config.js + // Env object defined in nuxt.config.js let env = {} each(this.options.env, (value, key) => { env['process.env.' + key] = (typeof value === 'string' ? JSON.stringify(value) : value) }) - // Webpack plugins - config.plugins = (config.plugins || []).concat([ - // Strip comments in Vue code - new webpack.DefinePlugin(Object.assign(env, { - 'process.env.NODE_ENV': JSON.stringify(env.NODE_ENV || (this.options.dev ? 'development' : 'production')), - 'process.BROWSER_BUILD': true, - 'process.SERVER_BUILD': false, - 'process.browser': true, - 'process.server': true - })), - // Extract vendor chunks for better caching - new webpack.optimize.CommonsChunkPlugin({ - name: 'vendor', - filename: this.options.build.filenames.vendor, - minChunks (module) { - // A module is extracted into the vendor chunk when... - return ( - // If it's inside node_modules - /node_modules/.test(module.context) && - // Do not externalize if the request is a CSS file - !/\.(css|less|scss|sass|styl|stylus)$/.test(module.request) - ) - } - }), - // Extract webpack runtime & manifest + + // Webpack common plugins + if (!Array.isArray(config.plugins)) { + config.plugins = [] + } + + // Generate output HTML + config.plugins.push( + new HTMLPlugin({ + template: this.options.appTemplatePath, + inject: false + }) + ) + + // Generate vue-ssr-client-manifest + config.plugins.push( + new VueSSRClientPlugin({ + filename: 'vue-ssr-client-manifest.json' + }) + ) + + // Extract webpack runtime & manifest + config.plugins.push( new webpack.optimize.CommonsChunkPlugin({ name: 'manifest', minChunks: Infinity, filename: this.options.build.filenames.manifest - }), - // Generate output HTML - new HTMLPlugin({ - template: this.options.appTemplatePath, - inject: false // <- Resources will be injected using vue server renderer - }), - // Generate client manifest json - new VueSSRClientPlugin({ - filename: 'client-manifest.json' }) - ]) - // client bundle progress bar + ) + + + // Define Env + config.plugins.push( + new webpack.DefinePlugin(Object.assign(env, { + 'process.env.NODE_ENV': JSON.stringify(env.NODE_ENV || (this.options.dev ? 'development' : 'production')), + 'process.env.VUE_ENV': JSON.stringify('client'), + 'process.browser': true, + 'process.server': false + })) + ) + + // Build progress bar config.plugins.push( new ProgressBarPlugin() ) - // Add friendly error plugin + + // -------------------------------------- + // Dev specific config + // -------------------------------------- if (this.options.dev) { + // Add friendly error plugin config.plugins.push(new FriendlyErrorsWebpackPlugin()) - } - // Dev client build - if (this.options.dev) { + // Add HMR support - config.entry.app = flatten(['webpack-hot-middleware/client?name=$client&reload=true', config.entry.app]) + config.entry.app = ['webpack-hot-middleware/client?name=$client&reload=true', config.entry.app] + config.output.filename = '[name].js' config.plugins.push( new webpack.HotModuleReplacementPlugin(), new webpack.NoEmitOnErrorsPlugin() ) } - // Production client build + + // -------------------------------------- + // Production specific config + // -------------------------------------- if (!this.options.dev) { + // Minify JS config.plugins.push( - // This is needed in webpack 2 for minifying CSS - new webpack.LoaderOptionsPlugin({ - minimize: true - }), - // Minify JS new webpack.optimize.UglifyJsPlugin({ sourceMap: true, compress: { @@ -112,7 +131,15 @@ export default function webpackClientConfig () { } }) ) + + // Webpack Bundle Analyzer + if (this.options.build.analyze) { + config.plugins.push( + new BundleAnalyzerPlugin(Object.assign({}, this.options.build.analyze)) + ) + } } + // Extend config if (typeof this.options.build.extend === 'function') { this.options.build.extend.call(this, config, { @@ -120,22 +147,6 @@ export default function webpackClientConfig () { isClient: true }) } - // Offline-plugin integration - if (!this.options.dev && this.options.offline) { - const offlineOpts = typeof this.options.offline === 'object' ? this.options.offline : {} - config.plugins.push( - new OfflinePlugin(defaults(offlineOpts, {})) - ) - } - // Webpack Bundle Analyzer - if (!this.options.dev && this.options.build.analyze) { - let options = {} - if (typeof this.options.build.analyze === 'object') { - options = this.options.build.analyze - } - config.plugins.push( - new BundleAnalyzerPlugin(options) - ) - } + return config } diff --git a/lib/webpack/server.config.js b/lib/webpack/server.config.js index 62a2eb2b5e..185a19a0fe 100644 --- a/lib/webpack/server.config.js +++ b/lib/webpack/server.config.js @@ -13,6 +13,8 @@ import base from './base.config.js' export default function webpackServerConfig () { let config = base.call(this, { isServer: true }) + config.name = 'server' + // env object defined in nuxt.config.js let env = {} each(this.options.env, (value, key) => { @@ -24,7 +26,6 @@ export default function webpackServerConfig () { devtool: (this.options.dev ? 'source-map' : false), entry: resolve(this.options.buildDir, 'server.js'), output: Object.assign({}, config.output, { - path: resolve(this.options.buildDir, 'dist'), filename: 'server-bundle.js', libraryTarget: 'commonjs2' }), @@ -32,6 +33,8 @@ export default function webpackServerConfig () { hints: false }, externals: [ + // https://webpack.js.org/configuration/externals/#externals + // https://github.com/liady/webpack-node-externals nodeExternals({ // load non-javascript files with extensions, presumably via loaders whitelist: [/\.(?!(?:js|json)$).{1,5}$/i] @@ -42,21 +45,19 @@ export default function webpackServerConfig () { filename: 'server-bundle.json' }), new webpack.DefinePlugin(Object.assign(env, { - 'process.env.NODE_ENV': JSON.stringify(this.options.dev ? 'development' : 'production'), - 'process.BROWSER_BUILD': false, // deprecated - 'process.SERVER_BUILD': true, // deprecated + 'process.env.NODE_ENV': JSON.stringify(env.NODE_ENV || (this.options.dev ? 'development' : 'production')), + 'process.env.VUE_ENV': JSON.stringify('server'), 'process.browser': false, 'process.server': true })) ]) }) - // This is needed in webpack 2 for minifying CSS + + // -------------------------------------- + // Production specific config + // -------------------------------------- if (!this.options.dev) { - config.plugins.push( - new webpack.LoaderOptionsPlugin({ - minimize: true - }) - ) + } // Extend config @@ -66,5 +67,6 @@ export default function webpackServerConfig () { isServer: true }) } + return config } diff --git a/lib/webpack/vue-loader.config.js b/lib/webpack/vue-loader.config.js index fcb7a0c90a..d8f3a1171c 100644 --- a/lib/webpack/vue-loader.config.js +++ b/lib/webpack/vue-loader.config.js @@ -9,7 +9,7 @@ export default function ({ isClient }) { })) // https://github.com/vuejs/vue-loader/blob/master/docs/en/configurations - let config = { + const config = { postcss: this.options.build.postcss, loaders: { 'js': 'babel-loader?' + babelOptions, @@ -23,6 +23,7 @@ export default function ({ isClient }) { preserveWhitespace: false, extractCSS: extractStyles.call(this) } + // Return the config return config }