feat(webpack, builder): allow extending loader options (#3799)

This commit is contained in:
Clark Du 2018-09-10 09:27:01 +01:00 committed by Pooya Parsa
parent 1e0219543a
commit c77fa479f6
7 changed files with 127 additions and 52 deletions

View File

@ -21,6 +21,7 @@ export default class WebpackBaseConfig {
this.isStatic = builder.isStatic this.isStatic = builder.isStatic
this.options = builder.options this.options = builder.options
this.spinner = builder.spinner this.spinner = builder.spinner
this.loaders = this.options.build.loaders
} }
get nuxtEnv() { get nuxtEnv() {
@ -129,25 +130,26 @@ export default class WebpackBaseConfig {
{ {
test: /\.vue$/, test: /\.vue$/,
loader: 'vue-loader', loader: 'vue-loader',
options: Object.assign({ options: this.loaders.vue
productionMode: !this.options.dev,
transformAssetUrls: {
video: 'src',
source: 'src',
object: 'src',
embed: 'src'
}
}, this.options.build.vueLoader)
}, },
{ {
test: /\.pug$/, test: /\.pug$/,
oneOf: [ oneOf: [
{ {
resourceQuery: /^\?vue/, 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$/, 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$/, test: /\.sass$/,
oneOf: perfLoader.poolOneOf('css', styleLoader.apply('sass', { oneOf: perfLoader.poolOneOf('css', styleLoader.apply('sass', {
loader: 'sass-loader', loader: 'sass-loader',
options: { indentedSyntax: true } options: this.loaders.sass
})) }))
}, },
{ {
test: /\.scss$/, 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)?$/, 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)$/, test: /\.(png|jpe?g|gif|svg|webp)$/,
use: perfLoader.pool('assets', { use: perfLoader.pool('assets', {
loader: 'url-loader', loader: 'url-loader',
options: { options: Object.assign(
limit: 1000, // 1KO this.loaders.imgUrl,
name: this.getFileName('img') { name: this.getFileName('img') }
} )
}) })
}, },
{ {
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
use: perfLoader.pool('assets', { use: perfLoader.pool('assets', {
loader: 'url-loader', loader: 'url-loader',
options: { options: Object.assign(
limit: 1000, // 1 KO this.loaders.fontUrl,
name: this.getFileName('font') { name: this.getFileName('font') }
} )
}) })
}, },
{ {
test: /\.(webm|mp4|ogv)$/, test: /\.(webm|mp4|ogv)$/,
use: perfLoader.pool('assets', { use: perfLoader.pool('assets', {
loader: 'file-loader', loader: 'file-loader',
options: { options: Object.assign(
name: this.getFileName('video') this.loaders.file,
} { name: this.getFileName('video') }
)
}) })
} }
] ]
@ -274,9 +286,11 @@ export default class WebpackBaseConfig {
return plugins return plugins
} }
customize(config) { extendConfig(config) {
if (typeof this.options.build.extend === 'function') { 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 // Only overwrite config when something is returned for backwards compatibility
if (extendedConfig !== undefined) { if (extendedConfig !== undefined) {
return extendedConfig return extendedConfig
@ -312,7 +326,9 @@ export default class WebpackBaseConfig {
plugins: this.plugins() plugins: this.plugins()
} }
const extendedConfig = this.extendConfig(config)
// Clone deep avoid leaking config between Client and Server // Clone deep avoid leaking config between Client and Server
return _.cloneDeep(config) return _.cloneDeep(extendedConfig)
} }
} }

View File

@ -96,8 +96,8 @@ export default class WebpackClientConfig extends WebpackBaseConfig {
return plugins return plugins
} }
customize() { extendConfig() {
const config = super.customize(...arguments) const config = super.extendConfig(...arguments)
if (!this.options.dev && !config.optimization.minimizer) { if (!this.options.dev && !config.optimization.minimizer) {
config.optimization.minimizer = [] config.optimization.minimizer = []
@ -158,6 +158,6 @@ export default class WebpackClientConfig extends WebpackBaseConfig {
) )
} }
return this.customize(config) return config
} }
} }

View File

@ -81,6 +81,6 @@ export default class WebpackServerConfig extends BaseConfig {
} }
}) })
return this.customize(config) return config
} }
} }

View File

@ -1,7 +1,5 @@
import path from 'path' import path from 'path'
import MiniCssExtractPlugin from 'mini-css-extract-plugin' import MiniCssExtractPlugin from 'mini-css-extract-plugin'
import PostcssConfig from './postcss' import PostcssConfig from './postcss'
export default class StyleLoader { export default class StyleLoader {
@ -11,6 +9,7 @@ export default class StyleLoader {
this.srcDir = options.srcDir this.srcDir = options.srcDir
this.assetsDir = options.dir.assets this.assetsDir = options.dir.assets
this.staticDir = options.dir.static this.staticDir = options.dir.static
this.loaders = options.build.loaders
this.extractCSS = options.build.extractCSS this.extractCSS = options.build.extractCSS
this.resources = options.build.styleResources this.resources = options.build.styleResources
this.sourceMap = Boolean(options.build.cssSourceMap) this.sourceMap = Boolean(options.build.cssSourceMap)
@ -22,10 +21,7 @@ export default class StyleLoader {
normalize(loaders) { normalize(loaders) {
loaders = Array.isArray(loaders) ? loaders : [loaders] loaders = Array.isArray(loaders) ? loaders : [loaders]
return loaders.map(loader => Object.assign( return loaders.map(loader => (typeof loader === 'string' ? { loader } : loader))
{ options: { sourceMap: this.sourceMap } },
typeof loader === 'string' ? { loader } : loader
))
} }
styleResource(ext) { styleResource(ext) {
@ -61,7 +57,7 @@ export default class StyleLoader {
} }
} }
css(importLoaders, options) { css(options) {
// css-loader // css-loader
// https://github.com/webpack-contrib/css-loader // https://github.com/webpack-contrib/css-loader
const cssLoaderAlias = { const cssLoaderAlias = {
@ -71,14 +67,17 @@ export default class StyleLoader {
return { return {
loader: (this.isServer && this.extractCSS) ? 'css-loader/locals' : 'css-loader', loader: (this.isServer && this.extractCSS) ? 'css-loader/locals' : 'css-loader',
options: Object.assign({ options: Object.assign(options, {
sourceMap: this.sourceMap,
importLoaders: importLoaders,
alias: cssLoaderAlias alias: cssLoaderAlias
}, options) })
} }
} }
cssModules(options) {
options.modules = true
return this.css(options)
}
extract() { extract() {
if (this.extractCSS && !this.isServer) { if (this.extractCSS && !this.isServer) {
return MiniCssExtractPlugin.loader return MiniCssExtractPlugin.loader
@ -89,7 +88,7 @@ export default class StyleLoader {
// https://github.com/vuejs/vue-style-loader // https://github.com/vuejs/vue-style-loader
return { return {
loader: 'vue-style-loader', loader: 'vue-style-loader',
options: { sourceMap: this.sourceMap } options: this.loaders.vueStyle
} }
} }
@ -100,6 +99,9 @@ export default class StyleLoader {
this.styleResource(ext) this.styleResource(ext)
).filter(Boolean) ).filter(Boolean)
const { css: cssOptions, cssModules: cssModulesOptions } = this.loaders
cssOptions.importLoaders = cssModulesOptions.importLoaders = customLoaders.length
const styleLoader = this.extract() || this.vueStyle() const styleLoader = this.extract() || this.vueStyle()
return [ return [
@ -108,10 +110,7 @@ export default class StyleLoader {
resourceQuery: /module/, resourceQuery: /module/,
use: [].concat( use: [].concat(
styleLoader, styleLoader,
this.css(customLoaders.length, { this.cssModules(cssModulesOptions),
modules: true,
localIdentName: '[local]_[hash:base64:5]'
}),
customLoaders customLoaders
) )
}, },
@ -119,7 +118,7 @@ export default class StyleLoader {
{ {
use: [].concat( use: [].concat(
styleLoader, styleLoader,
this.css(customLoaders.length), this.css(cssOptions),
customLoaders customLoaders
) )
} }

View File

@ -60,6 +60,31 @@ export default {
font: ({ isDev }) => isDev ? '[path][name].[ext]' : 'fonts/[hash:7].[ext]', font: ({ isDev }) => isDev ? '[path][name].[ext]' : 'fonts/[hash:7].[ext]',
video: ({ isDev }) => isDev ? '[path][name].[ext]' : 'videos/[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: {}, styleResources: {},
plugins: [], plugins: [],
optimization: { optimization: {
@ -81,7 +106,6 @@ export default {
cacheDirectory: undefined cacheDirectory: undefined
}, },
transpile: [], // Name of NPM packages to be transpiled transpile: [], // Name of NPM packages to be transpiled
vueLoader: {},
postcss: { postcss: {
preset: { preset: {
// https://cssdb.org/#staging-process // https://cssdb.org/#staging-process

View File

@ -209,6 +209,22 @@ Options.from = function (_options) {
options.build.extractCSS = false 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 // include SFCs in node_modules
options.build.transpile = [].concat(options.build.transpile || []) options.build.transpile = [].concat(options.build.transpile || [])
.map(module => module instanceof RegExp ? module : new RegExp(module)) .map(module => module instanceof RegExp ? module : new RegExp(module))

View File

@ -8,6 +8,8 @@ let nuxt = null
let builder = null let builder = null
let transpile = null let transpile = null
let output = null let output = null
let loadersOptions
let vueLoader
describe('basic dev', () => { describe('basic dev', () => {
beforeAll(async () => { beforeAll(async () => {
@ -26,11 +28,18 @@ describe('basic dev', () => {
'vue\\.test\\.js', 'vue\\.test\\.js',
/vue-test/ /vue-test/
], ],
extend({ module: { rules }, output: wpOutput }, { isClient }) { loaders: {
cssModules: {
localIdentName: '[hash:base64:6]'
}
},
extend({ module: { rules }, output: wpOutput }, { isClient, loaders }) {
if (isClient) { if (isClient) {
const babelLoader = rules.find(loader => loader.test.test('.jsx')) const babelLoader = rules.find(loader => loader.test.test('.jsx'))
transpile = file => !babelLoader.exclude(file) transpile = file => !babelLoader.exclude(file)
output = wpOutput 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 () => { test('/stateless', async () => {
const window = await nuxt.renderAndGetWindow(url('/stateless')) const window = await nuxt.renderAndGetWindow(url('/stateless'))
const html = window.document.body.innerHTML const html = window.document.body.innerHTML