From c77fa479f6af5cf6f1384ea59ddeb494d05a83b7 Mon Sep 17 00:00:00 2001 From: Clark Du Date: Mon, 10 Sep 2018 09:27:01 +0100 Subject: [PATCH] feat(webpack, builder): allow extending loader options (#3799) --- lib/builder/webpack/base.js | 74 ++++++++++++++--------- lib/builder/webpack/client.js | 6 +- lib/builder/webpack/server.js | 2 +- lib/builder/webpack/utils/style-loader.js | 33 +++++----- lib/common/nuxt.config.js | 26 +++++++- lib/common/options.js | 16 +++++ test/unit/basic.dev.test.js | 22 ++++++- 7 files changed, 127 insertions(+), 52 deletions(-) diff --git a/lib/builder/webpack/base.js b/lib/builder/webpack/base.js index 3106b30d65..34bfd12aa9 100644 --- a/lib/builder/webpack/base.js +++ b/lib/builder/webpack/base.js @@ -21,6 +21,7 @@ export default class WebpackBaseConfig { this.isStatic = builder.isStatic this.options = builder.options this.spinner = builder.spinner + this.loaders = this.options.build.loaders } get nuxtEnv() { @@ -129,25 +130,26 @@ export default class WebpackBaseConfig { { test: /\.vue$/, loader: 'vue-loader', - options: Object.assign({ - productionMode: !this.options.dev, - transformAssetUrls: { - video: 'src', - source: 'src', - object: 'src', - embed: 'src' - } - }, this.options.build.vueLoader) + options: this.loaders.vue }, { test: /\.pug$/, oneOf: [ { resourceQuery: /^\?vue/, - use: ['pug-plain-loader'] + use: [{ + loader: 'pug-plain-loader', + options: this.loaders.pugPlain + }] }, { - use: ['raw-loader', 'pug-plain-loader'] + use: [ + 'raw-loader', + { + loader: 'pug-plain-loader', + options: this.loaders.pugPlain + } + ] } ] }, @@ -176,50 +178,60 @@ export default class WebpackBaseConfig { }, { test: /\.less$/, - oneOf: perfLoader.poolOneOf('css', styleLoader.apply('less', 'less-loader')) + oneOf: perfLoader.poolOneOf('css', styleLoader.apply('less', { + loader: 'less-loader', + options: this.loaders.less + })) }, { test: /\.sass$/, oneOf: perfLoader.poolOneOf('css', styleLoader.apply('sass', { loader: 'sass-loader', - options: { indentedSyntax: true } + options: this.loaders.sass })) }, { test: /\.scss$/, - oneOf: perfLoader.poolOneOf('css', styleLoader.apply('scss', 'sass-loader')) + oneOf: perfLoader.poolOneOf('css', styleLoader.apply('scss', { + loader: 'sass-loader', + options: this.loaders.scss + })) }, { test: /\.styl(us)?$/, - oneOf: perfLoader.poolOneOf('css', styleLoader.apply('stylus', 'stylus-loader')) + oneOf: perfLoader.poolOneOf('css', styleLoader.apply('stylus', { + loader: 'stylus-loader', + options: this.loaders.stylus + })) }, { test: /\.(png|jpe?g|gif|svg|webp)$/, use: perfLoader.pool('assets', { loader: 'url-loader', - options: { - limit: 1000, // 1KO - name: this.getFileName('img') - } + options: Object.assign( + this.loaders.imgUrl, + { name: this.getFileName('img') } + ) }) }, { test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, use: perfLoader.pool('assets', { loader: 'url-loader', - options: { - limit: 1000, // 1 KO - name: this.getFileName('font') - } + options: Object.assign( + this.loaders.fontUrl, + { name: this.getFileName('font') } + ) }) }, { test: /\.(webm|mp4|ogv)$/, use: perfLoader.pool('assets', { loader: 'file-loader', - options: { - name: this.getFileName('video') - } + options: Object.assign( + this.loaders.file, + { name: this.getFileName('video') } + ) }) } ] @@ -274,9 +286,11 @@ export default class WebpackBaseConfig { return plugins } - customize(config) { + extendConfig(config) { if (typeof this.options.build.extend === 'function') { - const extendedConfig = this.options.build.extend.call(this.builder, config, this.nuxtEnv) + const extendedConfig = this.options.build.extend.call( + this.builder, config, { loaders: this.loaders, ...this.nuxtEnv } + ) // Only overwrite config when something is returned for backwards compatibility if (extendedConfig !== undefined) { return extendedConfig @@ -312,7 +326,9 @@ export default class WebpackBaseConfig { plugins: this.plugins() } + const extendedConfig = this.extendConfig(config) + // Clone deep avoid leaking config between Client and Server - return _.cloneDeep(config) + return _.cloneDeep(extendedConfig) } } diff --git a/lib/builder/webpack/client.js b/lib/builder/webpack/client.js index f025bece22..bd291a861d 100644 --- a/lib/builder/webpack/client.js +++ b/lib/builder/webpack/client.js @@ -96,8 +96,8 @@ export default class WebpackClientConfig extends WebpackBaseConfig { return plugins } - customize() { - const config = super.customize(...arguments) + extendConfig() { + const config = super.extendConfig(...arguments) if (!this.options.dev && !config.optimization.minimizer) { config.optimization.minimizer = [] @@ -158,6 +158,6 @@ export default class WebpackClientConfig extends WebpackBaseConfig { ) } - return this.customize(config) + return config } } diff --git a/lib/builder/webpack/server.js b/lib/builder/webpack/server.js index 96680be6ed..687d0327fd 100644 --- a/lib/builder/webpack/server.js +++ b/lib/builder/webpack/server.js @@ -81,6 +81,6 @@ export default class WebpackServerConfig extends BaseConfig { } }) - return this.customize(config) + return config } } diff --git a/lib/builder/webpack/utils/style-loader.js b/lib/builder/webpack/utils/style-loader.js index dd5ec90e12..7f72eb4ca6 100644 --- a/lib/builder/webpack/utils/style-loader.js +++ b/lib/builder/webpack/utils/style-loader.js @@ -1,7 +1,5 @@ import path from 'path' - import MiniCssExtractPlugin from 'mini-css-extract-plugin' - import PostcssConfig from './postcss' export default class StyleLoader { @@ -11,6 +9,7 @@ export default class StyleLoader { this.srcDir = options.srcDir this.assetsDir = options.dir.assets this.staticDir = options.dir.static + this.loaders = options.build.loaders this.extractCSS = options.build.extractCSS this.resources = options.build.styleResources this.sourceMap = Boolean(options.build.cssSourceMap) @@ -22,10 +21,7 @@ export default class StyleLoader { normalize(loaders) { loaders = Array.isArray(loaders) ? loaders : [loaders] - return loaders.map(loader => Object.assign( - { options: { sourceMap: this.sourceMap } }, - typeof loader === 'string' ? { loader } : loader - )) + return loaders.map(loader => (typeof loader === 'string' ? { loader } : loader)) } styleResource(ext) { @@ -61,7 +57,7 @@ export default class StyleLoader { } } - css(importLoaders, options) { + css(options) { // css-loader // https://github.com/webpack-contrib/css-loader const cssLoaderAlias = { @@ -71,14 +67,17 @@ export default class StyleLoader { return { loader: (this.isServer && this.extractCSS) ? 'css-loader/locals' : 'css-loader', - options: Object.assign({ - sourceMap: this.sourceMap, - importLoaders: importLoaders, + options: Object.assign(options, { alias: cssLoaderAlias - }, options) + }) } } + cssModules(options) { + options.modules = true + return this.css(options) + } + extract() { if (this.extractCSS && !this.isServer) { return MiniCssExtractPlugin.loader @@ -89,7 +88,7 @@ export default class StyleLoader { // https://github.com/vuejs/vue-style-loader return { loader: 'vue-style-loader', - options: { sourceMap: this.sourceMap } + options: this.loaders.vueStyle } } @@ -100,6 +99,9 @@ export default class StyleLoader { this.styleResource(ext) ).filter(Boolean) + const { css: cssOptions, cssModules: cssModulesOptions } = this.loaders + cssOptions.importLoaders = cssModulesOptions.importLoaders = customLoaders.length + const styleLoader = this.extract() || this.vueStyle() return [ @@ -108,10 +110,7 @@ export default class StyleLoader { resourceQuery: /module/, use: [].concat( styleLoader, - this.css(customLoaders.length, { - modules: true, - localIdentName: '[local]_[hash:base64:5]' - }), + this.cssModules(cssModulesOptions), customLoaders ) }, @@ -119,7 +118,7 @@ export default class StyleLoader { { use: [].concat( styleLoader, - this.css(customLoaders.length), + this.css(cssOptions), customLoaders ) } diff --git a/lib/common/nuxt.config.js b/lib/common/nuxt.config.js index 22e03328ec..6e296710f2 100644 --- a/lib/common/nuxt.config.js +++ b/lib/common/nuxt.config.js @@ -60,6 +60,31 @@ export default { font: ({ isDev }) => isDev ? '[path][name].[ext]' : 'fonts/[hash:7].[ext]', video: ({ isDev }) => isDev ? '[path][name].[ext]' : 'videos/[hash:7].[ext]' }, + loaders: { + file: {}, + fontUrl: { limit: 1000 }, + imgUrl: { limit: 1000 }, + pugPlain: {}, + vue: { + transformAssetUrls: { + video: 'src', + source: 'src', + object: 'src', + embed: 'src' + } + }, + css: {}, + cssModules: { + localIdentName: '[local]_[hash:base64:5]' + }, + less: {}, + sass: { + indentedSyntax: true + }, + scss: {}, + stylus: {}, + vueStyle: {} + }, styleResources: {}, plugins: [], optimization: { @@ -81,7 +106,6 @@ export default { cacheDirectory: undefined }, transpile: [], // Name of NPM packages to be transpiled - vueLoader: {}, postcss: { preset: { // https://cssdb.org/#staging-process diff --git a/lib/common/options.js b/lib/common/options.js index 281bb73eb8..fefb78518d 100644 --- a/lib/common/options.js +++ b/lib/common/options.js @@ -209,6 +209,22 @@ Options.from = function (_options) { options.build.extractCSS = false } + const loaders = options.build.loaders + const vueLoader = loaders.vue + if (vueLoader.productionMode === undefined) { + vueLoader.productionMode = !options.dev + } + const styleLoaders = [ + 'css', 'cssModules', 'less', + 'sass', 'scss', 'stylus', 'vueStyle' + ] + for (const name of styleLoaders) { + const loader = loaders[name] + if (loader && loader.sourceMap === undefined) { + loader.sourceMap = Boolean(options.build.cssSourceMap) + } + } + // include SFCs in node_modules options.build.transpile = [].concat(options.build.transpile || []) .map(module => module instanceof RegExp ? module : new RegExp(module)) diff --git a/test/unit/basic.dev.test.js b/test/unit/basic.dev.test.js index 5a6a181658..c756c03c60 100644 --- a/test/unit/basic.dev.test.js +++ b/test/unit/basic.dev.test.js @@ -8,6 +8,8 @@ let nuxt = null let builder = null let transpile = null let output = null +let loadersOptions +let vueLoader describe('basic dev', () => { beforeAll(async () => { @@ -26,11 +28,18 @@ describe('basic dev', () => { 'vue\\.test\\.js', /vue-test/ ], - extend({ module: { rules }, output: wpOutput }, { isClient }) { + loaders: { + cssModules: { + localIdentName: '[hash:base64:6]' + } + }, + extend({ module: { rules }, output: wpOutput }, { isClient, loaders }) { if (isClient) { const babelLoader = rules.find(loader => loader.test.test('.jsx')) transpile = file => !babelLoader.exclude(file) output = wpOutput + loadersOptions = loaders + vueLoader = rules.find(loader => loader.test.test('.vue')) } } } @@ -62,6 +71,17 @@ describe('basic dev', () => { ) }) + test('Config: build.loaders', () => { + expect(Object.keys(loadersOptions)).toHaveLength(12) + expect(loadersOptions).toHaveProperty( + 'file', 'fontUrl', 'imgUrl', 'pugPlain', 'vue', + 'css', 'cssModules', 'less', 'sass', 'scss', 'stylus', 'vueStyle' + ) + const { cssModules, vue } = loadersOptions + expect(cssModules.localIdentName).toBe('[hash:base64:6]') + expect(vueLoader.options).toBe(vue) + }) + test('/stateless', async () => { const window = await nuxt.renderAndGetWindow(url('/stateless')) const html = window.document.body.innerHTML