refactor: webpack build config

This commit is contained in:
Clark Du 2018-03-22 22:06:54 +08:00 committed by Pooya Parsa
parent 46f7a0bc70
commit 818e982eca
4 changed files with 393 additions and 358 deletions

View File

@ -18,8 +18,8 @@ import upath from 'upath'
import { r, wp, wChunk, createRoutes, parallel, relativeTo, waitFor, createSpinner } from '../common/utils' import { r, wp, wChunk, createRoutes, parallel, relativeTo, waitFor, createSpinner } from '../common/utils'
import Options from '../common/options' import Options from '../common/options'
import clientWebpackConfig from './webpack/client.config' import ClientWebpackConfig from './webpack/client.config'
import serverWebpackConfig from './webpack/server.config' import ServerWebpackConfig from './webpack/server.config'
const debug = Debug('nuxt:build') const debug = Debug('nuxt:build')
debug.color = 2 // Force green color debug.color = 2 // Force green color
@ -147,39 +147,6 @@ export default class Builder {
return this return this
} }
getBabelOptions({ isServer }) {
const options = _.clone(this.options.build.babel)
if (typeof options.presets === 'function') {
options.presets = options.presets({ isServer })
}
if (!options.babelrc && !options.presets) {
options.presets = [
[
this.nuxt.resolvePath('babel-preset-vue-app'),
{
targets: isServer ? { node: '8.0.0' } : { ie: 9, uglify: true }
}
]
]
}
return options
}
getFileName(name) {
let fileName = this.options.build.filenames[name]
// Don't use hashes when watching
// https://github.com/webpack/webpack/issues/1914#issuecomment-174171709
if (this.options.dev) {
fileName = fileName.replace(/\[(chunkhash|contenthash|hash)\]\./g, '')
}
return fileName
}
async generateRoutesAndFiles() { async generateRoutesAndFiles() {
this.spinner.start(`Generating nuxt files...`) this.spinner.start(`Generating nuxt files...`)
@ -452,13 +419,13 @@ export default class Builder {
const compilersOptions = [] const compilersOptions = []
// Client // Client
const clientConfig = clientWebpackConfig.call(this) const clientConfig = new ClientWebpackConfig(this).config()
compilersOptions.push(clientConfig) compilersOptions.push(clientConfig)
// Server // Server
let serverConfig = null let serverConfig = null
if (this.options.build.ssr) { if (this.options.build.ssr) {
serverConfig = serverWebpackConfig.call(this) serverConfig = new ServerWebpackConfig(this).config()
compilersOptions.push(serverConfig) compilersOptions.push(serverConfig)
} }

View File

@ -9,7 +9,7 @@ import VueLoader from 'vue-loader'
import { isUrl, urlJoin } from '../../common/utils' import { isUrl, urlJoin } from '../../common/utils'
import customLoaders from './loaders' import customLoaders from './loaders'
import styleLoaderWrapper from './style-loader' import createStyleLoader from './style-loader'
import WarnFixPlugin from './plugins/warnfix' import WarnFixPlugin from './plugins/warnfix'
import ProgressPlugin from './plugins/progress' import ProgressPlugin from './plugins/progress'
import StatsPlugin from './plugins/stats' import StatsPlugin from './plugins/stats'
@ -22,149 +22,218 @@ import StatsPlugin from './plugins/stats'
| webpack config files | webpack config files
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
*/ */
export default function webpackBaseConfig({ name, isServer }) { export default class WebpackBaseConfig {
// Prioritize nested node_modules in webpack search path (#2558) constructor(builder, options) {
const webpackModulesDir = ['node_modules'].concat(this.options.modulesDir) this.name = options.name
this.isServer = options.isServer
this.builder = builder
this.isStatic = builder.isStatic
this.options = builder.options
this.spinner = builder.spinner
}
const configAlias = {} getBabelOptions() {
const styleLoader = styleLoaderWrapper({ isServer }) const options = _.clone(this.options.build.babel)
// Used by vue-loader so we can use in templates if (typeof options.presets === 'function') {
// with <img src="~/assets/nuxt.png"/> options.presets = options.presets({ isServer: this.isServer })
configAlias[this.options.dir.assets] = path.join( }
this.options.srcDir,
this.options.dir.assets
)
configAlias[this.options.dir.static] = path.join(
this.options.srcDir,
this.options.dir.static
)
const config = { if (!options.babelrc && !options.presets) {
name, options.presets = [
mode: this.options.dev ? 'development' : 'production', [
optimization: {}, this.builder.nuxt.resolvePath('babel-preset-vue-app'),
output: { {
targets: this.isServer ? { node: '8.0.0' } : { ie: 9, uglify: true }
}
]
]
}
return options
}
getFileName(name) {
let fileName = this.options.build.filenames[name]
// Don't use hashes when watching
// https://github.com/webpack/webpack/issues/1914#issuecomment-174171709
if (this.options.dev) {
fileName = fileName.replace(/\[(chunkhash|contenthash|hash)\]\./g, '')
}
return fileName
}
env() {
const env = {
'process.mode': JSON.stringify(this.options.mode),
'process.static': this.isStatic
}
_.each(this.options.env, (value, key) => {
env['process.env.' + key] =
['boolean', 'number'].indexOf(typeof value) !== -1
? value
: JSON.stringify(value)
})
return env
}
output() {
return {
path: path.resolve(this.options.buildDir, 'dist'), path: path.resolve(this.options.buildDir, 'dist'),
filename: this.getFileName('app'), filename: this.getFileName('app'),
chunkFilename: this.getFileName('chunk'), chunkFilename: this.getFileName('chunk'),
publicPath: isUrl(this.options.build.publicPath) publicPath: isUrl(this.options.build.publicPath)
? this.options.build.publicPath ? this.options.build.publicPath
: urlJoin(this.options.router.base, this.options.build.publicPath) : urlJoin(this.options.router.base, this.options.build.publicPath)
}, }
performance: { }
maxEntrypointSize: 1000 * 1024,
hints: this.options.dev ? false : 'warning' alias() {
}, return {
resolve: { '~': path.join(this.options.srcDir),
extensions: ['.js', '.json', '.vue', '.jsx'], '~~': path.join(this.options.rootDir),
alias: Object.assign( '@': path.join(this.options.srcDir),
{ '@@': path.join(this.options.rootDir),
'~': path.join(this.options.srcDir), [this.options.dir.assets]: path.join(
'~~': path.join(this.options.rootDir), this.options.srcDir,
'@': path.join(this.options.srcDir), this.options.dir.assets
'@@': path.join(this.options.rootDir)
},
configAlias
), ),
modules: webpackModulesDir [this.options.dir.static]: path.join(
}, this.options.srcDir,
resolveLoader: { this.options.dir.static
alias: customLoaders, )
modules: webpackModulesDir }
}, }
module: {
noParse: /es6-promise\.js$/, // Avoid webpack shimming process rules() {
rules: [ const styleLoader = createStyleLoader({
{ isServer: this.isServer
test: /\.vue$/, })
loader: 'vue-loader', return [
options: _.cloneDeep(this.options.build.vue) {
}, test: /\.vue$/,
{ loader: 'vue-loader',
test: /\.jsx?$/, options: _.cloneDeep(this.options.build.vue)
loader: 'babel-loader', },
exclude: /node_modules/, {
options: this.getBabelOptions({ isServer }) test: /\.jsx?$/,
}, loader: 'babel-loader',
{ test: /\.css$/, use: styleLoader.call(this, 'css') }, exclude: /node_modules/,
{ test: /\.less$/, use: styleLoader.call(this, 'less', 'less-loader') }, options: this.getBabelOptions()
{ },
test: /\.sass$/, { test: /\.css$/, use: styleLoader.call(this.builder, 'css') },
use: styleLoader.call(this, 'sass', { { test: /\.less$/, use: styleLoader.call(this.builder, 'less', 'less-loader') },
loader: 'sass-loader', {
options: { indentedSyntax: true } test: /\.sass$/,
}) use: styleLoader.call(this.builder, 'sass', {
}, loader: 'sass-loader',
{ test: /\.scss$/, use: styleLoader.call(this, 'scss', 'sass-loader') }, options: { indentedSyntax: true }
{ })
test: /\.styl(us)?$/, },
use: styleLoader.call(this, 'stylus', 'stylus-loader') { test: /\.scss$/, use: styleLoader.call(this.builder, 'scss', 'sass-loader') },
}, {
{ test: /\.styl(us)?$/,
test: /\.(png|jpe?g|gif|svg)$/, use: styleLoader.call(this.builder, 'stylus', 'stylus-loader')
loader: 'url-loader', },
options: { {
limit: 1000, // 1KO test: /\.(png|jpe?g|gif|svg)$/,
name: 'img/[name].[hash:7].[ext]' loader: 'url-loader',
} options: {
}, limit: 1000, // 1KO
{ name: 'img/[name].[hash:7].[ext]'
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, }
loader: 'url-loader', },
options: { {
limit: 1000, // 1 KO test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
name: 'fonts/[name].[hash:7].[ext]' loader: 'url-loader',
} options: {
}, limit: 1000, // 1 KO
{ name: 'fonts/[name].[hash:7].[ext]'
test: /\.(webm|mp4)$/, }
loader: 'file-loader', },
options: { {
name: 'videos/[name].[hash:7].[ext]' test: /\.(webm|mp4)$/,
} loader: 'file-loader',
options: {
name: 'videos/[name].[hash:7].[ext]'
} }
]
},
plugins: [
new VueLoader.VueLoaderPlugin()
].concat(this.options.build.plugins || [])
}
// Add timefix-plugin before others plugins
if (this.options.dev) {
config.plugins.unshift(new TimeFixPlugin())
}
// Hide warnings about plugins without a default export (#1179)
config.plugins.push(new WarnFixPlugin())
if (!this.options.test) {
// Build progress indicator
if (this.options.build.profile) {
config.plugins.push(new webpack.ProgressPlugin({ profile: true }))
} else {
if (!(this.options.minimalCLI)) {
config.plugins.push(new ProgressPlugin({
spinner: this.spinner,
name: isServer ? 'server' : 'client',
color: isServer ? 'green' : 'darkgreen'
}))
} }
]
}
plugins() {
const plugins = [ new VueLoader.VueLoaderPlugin() ]
Array.prototype.push.apply(plugins, this.options.build.plugins || [])
// Add timefix-plugin before others plugins
if (this.options.dev) {
plugins.unshift(new TimeFixPlugin())
} }
// Add stats plugin // Hide warnings about plugins without a default export (#1179)
config.plugins.push(new StatsPlugin(this.options.build.stats)) plugins.push(new WarnFixPlugin())
// Add friendly error plugin if (!this.options.test) {
config.plugins.push( // Build progress indicator
new FriendlyErrorsWebpackPlugin({ if (this.options.build.profile) {
clearConsole: true, plugins.push(new webpack.ProgressPlugin({ profile: true }))
logLevel: 'WARNING' } else {
}) if (!(this.options.minimalCLI)) {
) plugins.push(new ProgressPlugin({
spinner: this.spinner,
name: this.isServer ? 'server' : 'client',
color: this.isServer ? 'green' : 'darkgreen'
}))
}
}
// Add stats plugin
plugins.push(new StatsPlugin(this.options.build.stats))
// Add friendly error plugin
plugins.push(
new FriendlyErrorsWebpackPlugin({
clearConsole: true,
logLevel: 'WARNING'
})
)
}
return plugins
} }
// Clone deep avoid leaking config between Client and Server config() {
return _.cloneDeep(config) // Prioritize nested node_modules in webpack search path (#2558)
const webpackModulesDir = ['node_modules'].concat(this.options.modulesDir)
const config = {
name: this.name,
mode: this.options.dev ? 'development' : 'production',
optimization: {},
output: this.output(),
performance: {
maxEntrypointSize: 1000 * 1024,
hints: this.options.dev ? false : 'warning'
},
resolve: {
extensions: ['.js', '.json', '.vue', '.jsx'],
alias: this.alias(),
modules: webpackModulesDir
},
resolveLoader: {
alias: customLoaders,
modules: webpackModulesDir
},
module: {
noParse: /es6-promise\.js$/, // Avoid webpack shimming process
rules: this.rules()
},
plugins: this.plugins()
}
// Clone deep avoid leaking config between Client and Server
return _.cloneDeep(config)
}
} }

View File

@ -1,6 +1,5 @@
import path from 'path' import path from 'path'
import _ from 'lodash'
import webpack from 'webpack' import webpack from 'webpack'
import HTMLPlugin from 'html-webpack-plugin' import HTMLPlugin from 'html-webpack-plugin'
@ -8,7 +7,7 @@ import BundleAnalyzer from 'webpack-bundle-analyzer'
import MiniCssExtractPlugin from 'mini-css-extract-plugin' import MiniCssExtractPlugin from 'mini-css-extract-plugin'
import Debug from 'debug' import Debug from 'debug'
import base from './base.config' import BaseConfig from './base.config'
// import VueSSRClientPlugin from 'vue-server-renderer/client-plugin' // import VueSSRClientPlugin from 'vue-server-renderer/client-plugin'
import VueSSRClientPlugin from './plugins/vue/client' import VueSSRClientPlugin from './plugins/vue/client'
@ -21,141 +20,140 @@ debug.color = 2 // Force green color
| Webpack Client Config | Webpack Client Config
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
*/ */
export default function webpackClientConfig() { export default class WebpackClientConfig extends BaseConfig {
let config = base.call(this, { name: 'client', isServer: false }) constructor(builder) {
super(builder, { name: 'client', isServer: false })
}
// Entry points env() {
config.entry = path.resolve(this.options.buildDir, 'client.js') return Object.assign(super.env(), {
'process.env.VUE_ENV': JSON.stringify('client'),
// Env object defined in nuxt.config.js 'process.browser': true,
let env = {} 'process.client': true,
_.each(this.options.env, (value, key) => { 'process.server': false
env['process.env.' + key] =
['boolean', 'number'].indexOf(typeof value) !== -1
? value
: JSON.stringify(value)
})
// Generate output HTML for SPA
config.plugins.push(
new HTMLPlugin({
filename: 'index.spa.html',
template: 'lodash!' + this.options.appTemplatePath,
inject: true,
chunksSortMode: 'dependency'
}) })
)
// Generate output HTML for SSR
if (this.options.build.ssr) {
config.plugins.push(
new HTMLPlugin({
filename: 'index.ssr.html',
template: 'lodash!' + this.options.appTemplatePath,
inject: false // Resources will be injected using bundleRenderer
})
)
} }
// Generate vue-ssr-client-manifest plugins() {
config.plugins.push( const plugins = super.plugins()
new VueSSRClientPlugin({
filename: 'vue-ssr-client-manifest.json'
})
)
// Define Env // Generate output HTML for SSR
config.plugins.push( if (this.options.build.ssr) {
new webpack.DefinePlugin( plugins.push(
Object.assign(env, { new HTMLPlugin({
'process.env.VUE_ENV': JSON.stringify('client'), filename: 'index.ssr.html',
'process.mode': JSON.stringify(this.options.mode), template: 'lodash!' + this.options.appTemplatePath,
'process.browser': true, inject: false // Resources will be injected using bundleRenderer
'process.client': true,
'process.server': false,
'process.static': this.isStatic
})
)
)
// -- Optimization --
config.optimization = this.options.build.optimization
// Small, known and common modules which are usually used project-wise
// Sum of them may not be more than 244 KiB
if (
this.options.build.splitChunks.commons === true &&
config.optimization.splitChunks.cacheGroups.commons === undefined
) {
config.optimization.splitChunks.cacheGroups.commons = {
test: /node_modules\/(vue|vue-loader|vue-router|vuex|vue-meta|core-js|babel-runtime|es6-promise|axios|webpack|setimediate|timers-browserify|process|regenerator-runtime|cookie|js-cookie|is-buffer|dotprop|nuxt\.js)\//,
chunks: 'all',
priority: 10,
name: 'commons'
}
}
// --------------------------------------
// Dev specific config
// --------------------------------------
if (this.options.dev) {
// Add HMR support
config.entry = [
// https://github.com/glenjamin/webpack-hot-middleware#config
`webpack-hot-middleware/client?name=client&reload=true&timeout=30000&path=${
this.options.router.base
}/__webpack_hmr`.replace(/\/\//g, '/'),
config.entry
]
// HMR
config.plugins.push(new webpack.HotModuleReplacementPlugin())
}
// --------------------------------------
// Production specific config
// --------------------------------------
if (!this.options.dev) {
// Chunks size limit
// https://webpack.js.org/plugins/aggressive-splitting-plugin/
if (this.options.build.maxChunkSize) {
config.plugins.push(
new webpack.optimize.AggressiveSplittingPlugin({
minSize: this.options.build.maxChunkSize,
maxSize: this.options.build.maxChunkSize
}) })
) )
} }
// Webpack Bundle Analyzer plugins.push(
if (this.options.build.analyze) { new HTMLPlugin({
config.plugins.push( filename: 'index.spa.html',
new BundleAnalyzer.BundleAnalyzerPlugin(Object.assign({}, this.options.build.analyze)) template: 'lodash!' + this.options.appTemplatePath,
) inject: true,
chunksSortMode: 'dependency'
}),
new VueSSRClientPlugin({
filename: 'vue-ssr-client-manifest.json'
}),
new webpack.DefinePlugin(this.env())
)
if (this.options.dev) {
// --------------------------------------
// Dev specific config
// --------------------------------------
// HMR
plugins.push(new webpack.HotModuleReplacementPlugin())
} else {
// --------------------------------------
// Production specific config
// --------------------------------------
// Chunks size limit
// https://webpack.js.org/plugins/aggressive-splitting-plugin/
if (this.options.build.maxChunkSize) {
plugins.push(
new webpack.optimize.AggressiveSplittingPlugin({
minSize: this.options.build.maxChunkSize,
maxSize: this.options.build.maxChunkSize
})
)
}
// Webpack Bundle Analyzer
if (this.options.build.analyze) {
plugins.push(
new BundleAnalyzer.BundleAnalyzerPlugin(Object.assign({}, this.options.build.analyze))
)
}
} }
}
// Extend config // CSS extraction
if (typeof this.options.build.extend === 'function') { const extractCSS = this.options.build.extractCSS
const isDev = this.options.dev if (extractCSS) {
const extendedConfig = this.options.build.extend.call(this, config, { plugins.push(new MiniCssExtractPlugin(Object.assign({
isDev, filename: this.getFileName('css')
isClient: true }, typeof extractCSS === 'object' ? extractCSS : {})))
})
// Only overwrite config when something is returned for backwards compatibility
if (extendedConfig !== undefined) {
config = extendedConfig
} }
return plugins
} }
// CSS extraction config() {
const extractCSS = this.options.build.extractCSS let config = super.config()
if (extractCSS) {
config.plugins.push(new MiniCssExtractPlugin(Object.assign({
filename: this.getFileName('css')
}, typeof extractCSS === 'object' ? extractCSS : {})))
}
return config // Entry points
config.entry = path.resolve(this.options.buildDir, 'client.js')
// -- Optimization --
config.optimization = this.options.build.optimization
// Small, known and common modules which are usually used project-wise
// Sum of them may not be more than 244 KiB
if (
this.options.build.splitChunks.commons === true &&
config.optimization.splitChunks.cacheGroups.commons === undefined
) {
config.optimization.splitChunks.cacheGroups.commons = {
test: /node_modules\/(vue|vue-loader|vue-router|vuex|vue-meta|core-js|babel-runtime|es6-promise|axios|webpack|setimediate|timers-browserify|process|regenerator-runtime|cookie|js-cookie|is-buffer|dotprop|nuxt\.js)\//,
chunks: 'all',
priority: 10,
name: 'commons'
}
}
// --------------------------------------
// Dev specific config
// --------------------------------------
if (this.options.dev) {
// Add HMR support
config.entry = [
// https://github.com/glenjamin/webpack-hot-middleware#config
`webpack-hot-middleware/client?name=client&reload=true&timeout=30000&path=${
this.options.router.base
}/__webpack_hmr`.replace(/\/\//g, '/'),
config.entry
]
}
// Extend config
if (typeof this.options.build.extend === 'function') {
const isDev = this.options.dev
const extendedConfig = this.options.build.extend.call(this.builder, config, {
isDev,
isClient: true
})
// Only overwrite config when something is returned for backwards compatibility
if (extendedConfig !== undefined) {
config = extendedConfig
}
}
return config
}
} }

View File

@ -3,9 +3,8 @@ import fs from 'fs'
import webpack from 'webpack' import webpack from 'webpack'
import nodeExternals from 'webpack-node-externals' import nodeExternals from 'webpack-node-externals'
import _ from 'lodash'
import base from './base.config' import BaseConfig from './base.config'
// import VueSSRServerPlugin from 'vue-server-renderer/server-plugin' // import VueSSRServerPlugin from 'vue-server-renderer/server-plugin'
import VueSSRServerPlugin from './plugins/vue/server' import VueSSRServerPlugin from './plugins/vue/server'
@ -15,77 +14,79 @@ import VueSSRServerPlugin from './plugins/vue/server'
| Webpack Server Config | Webpack Server Config
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
*/ */
export default function webpackServerConfig() { export default class WebpackServerConfig extends BaseConfig {
let config = base.call(this, { name: 'server', isServer: true }) constructor(builder) {
super(builder, { name: 'server', isServer: true })
}
// Env object defined in nuxt.config.js env() {
let env = {} return Object.assign(super.env(), {
_.each(this.options.env, (value, key) => { 'process.env.VUE_ENV': JSON.stringify('server'),
env['process.env.' + key] = 'process.browser': false,
['boolean', 'number'].indexOf(typeof value) !== -1 'process.client': false,
? value 'process.server': true
: JSON.stringify(value) })
}) }
// Config devtool plugins() {
config.devtool = this.options.dev ? 'cheap-source-map' : false const plugins = super.plugins()
plugins.push(
config = Object.assign(config, {
target: 'node',
node: false,
entry: path.resolve(this.options.buildDir, 'server.js'),
output: Object.assign({}, config.output, {
filename: 'server-bundle.js',
libraryTarget: 'commonjs2'
}),
performance: {
hints: false,
maxAssetSize: Infinity
},
externals: [],
plugins: (config.plugins || []).concat([
new VueSSRServerPlugin({ new VueSSRServerPlugin({
filename: 'server-bundle.json' filename: 'server-bundle.json'
}), }),
new webpack.DefinePlugin( new webpack.DefinePlugin(this.env())
Object.assign(env, { )
'process.env.VUE_ENV': JSON.stringify('server'), return plugins
'process.mode': JSON.stringify(this.options.mode),
'process.browser': false,
'process.client': false,
'process.server': true,
'process.static': this.isStatic
})
)
])
})
// https://webpack.js.org/configuration/externals/#externals
// https://github.com/liady/webpack-node-externals
this.options.modulesDir.forEach(dir => {
if (fs.existsSync(dir)) {
config.externals.push(
nodeExternals({
// load non-javascript files with extensions, presumably via loaders
whitelist: [/es6-promise|\.(?!(?:js|json)$).{1,5}$/i],
modulesDir: dir
})
)
}
})
// Extend config
if (typeof this.options.build.extend === 'function') {
const isDev = this.options.dev
const extendedConfig = this.options.build.extend.call(this, config, {
isDev,
isServer: true
})
// Only overwrite config when something is returned for backwards compatibility
if (extendedConfig !== undefined) {
config = extendedConfig
}
} }
return config config() {
let config = super.config()
// Config devtool
config.devtool = this.options.dev ? 'cheap-source-map' : false
Object.assign(config, {
target: 'node',
node: false,
entry: path.resolve(this.options.buildDir, 'server.js'),
output: Object.assign({}, config.output, {
filename: 'server-bundle.js',
libraryTarget: 'commonjs2'
}),
performance: {
hints: false,
maxAssetSize: Infinity
},
externals: []
})
// https://webpack.js.org/configuration/externals/#externals
// https://github.com/liady/webpack-node-externals
this.options.modulesDir.forEach(dir => {
if (fs.existsSync(dir)) {
config.externals.push(
nodeExternals({
// load non-javascript files with extensions, presumably via loaders
whitelist: [/es6-promise|\.(?!(?:js|json)$).{1,5}$/i],
modulesDir: dir
})
)
}
})
// Extend config
if (typeof this.options.build.extend === 'function') {
const isDev = this.options.dev
const extendedConfig = this.options.build.extend.call(this.builder, config, {
isDev,
isServer: true
})
// Only overwrite config when something is returned for backwards compatibility
if (extendedConfig !== undefined) {
config = extendedConfig
}
}
return config
}
} }