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 Options from '../common/options'
import clientWebpackConfig from './webpack/client.config'
import serverWebpackConfig from './webpack/server.config'
import ClientWebpackConfig from './webpack/client.config'
import ServerWebpackConfig from './webpack/server.config'
const debug = Debug('nuxt:build')
debug.color = 2 // Force green color
@ -147,39 +147,6 @@ export default class Builder {
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() {
this.spinner.start(`Generating nuxt files...`)
@ -452,13 +419,13 @@ export default class Builder {
const compilersOptions = []
// Client
const clientConfig = clientWebpackConfig.call(this)
const clientConfig = new ClientWebpackConfig(this).config()
compilersOptions.push(clientConfig)
// Server
let serverConfig = null
if (this.options.build.ssr) {
serverConfig = serverWebpackConfig.call(this)
serverConfig = new ServerWebpackConfig(this).config()
compilersOptions.push(serverConfig)
}

View File

@ -9,7 +9,7 @@ import VueLoader from 'vue-loader'
import { isUrl, urlJoin } from '../../common/utils'
import customLoaders from './loaders'
import styleLoaderWrapper from './style-loader'
import createStyleLoader from './style-loader'
import WarnFixPlugin from './plugins/warnfix'
import ProgressPlugin from './plugins/progress'
import StatsPlugin from './plugins/stats'
@ -22,149 +22,218 @@ import StatsPlugin from './plugins/stats'
| webpack config files
|--------------------------------------------------------------------------
*/
export default function webpackBaseConfig({ name, isServer }) {
// Prioritize nested node_modules in webpack search path (#2558)
const webpackModulesDir = ['node_modules'].concat(this.options.modulesDir)
export default class WebpackBaseConfig {
constructor(builder, options) {
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 = {}
const styleLoader = styleLoaderWrapper({ isServer })
getBabelOptions() {
const options = _.clone(this.options.build.babel)
// Used by vue-loader so we can use in templates
// with <img src="~/assets/nuxt.png"/>
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
)
if (typeof options.presets === 'function') {
options.presets = options.presets({ isServer: this.isServer })
}
const config = {
name,
mode: this.options.dev ? 'development' : 'production',
optimization: {},
output: {
if (!options.babelrc && !options.presets) {
options.presets = [
[
this.builder.nuxt.resolvePath('babel-preset-vue-app'),
{
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'),
filename: this.getFileName('app'),
chunkFilename: this.getFileName('chunk'),
publicPath: isUrl(this.options.build.publicPath)
? this.options.build.publicPath
: urlJoin(this.options.router.base, this.options.build.publicPath)
},
performance: {
maxEntrypointSize: 1000 * 1024,
hints: this.options.dev ? false : 'warning'
},
resolve: {
extensions: ['.js', '.json', '.vue', '.jsx'],
alias: Object.assign(
{
'~': path.join(this.options.srcDir),
'~~': path.join(this.options.rootDir),
'@': path.join(this.options.srcDir),
'@@': path.join(this.options.rootDir)
},
configAlias
}
}
alias() {
return {
'~': path.join(this.options.srcDir),
'~~': path.join(this.options.rootDir),
'@': path.join(this.options.srcDir),
'@@': path.join(this.options.rootDir),
[this.options.dir.assets]: path.join(
this.options.srcDir,
this.options.dir.assets
),
modules: webpackModulesDir
},
resolveLoader: {
alias: customLoaders,
modules: webpackModulesDir
},
module: {
noParse: /es6-promise\.js$/, // Avoid webpack shimming process
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: _.cloneDeep(this.options.build.vue)
},
{
test: /\.jsx?$/,
loader: 'babel-loader',
exclude: /node_modules/,
options: this.getBabelOptions({ isServer })
},
{ test: /\.css$/, use: styleLoader.call(this, 'css') },
{ test: /\.less$/, use: styleLoader.call(this, 'less', 'less-loader') },
{
test: /\.sass$/,
use: styleLoader.call(this, 'sass', {
loader: 'sass-loader',
options: { indentedSyntax: true }
})
},
{ test: /\.scss$/, use: styleLoader.call(this, 'scss', 'sass-loader') },
{
test: /\.styl(us)?$/,
use: styleLoader.call(this, 'stylus', 'stylus-loader')
},
{
test: /\.(png|jpe?g|gif|svg)$/,
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
name: 'fonts/[name].[hash:7].[ext]'
}
},
{
test: /\.(webm|mp4)$/,
loader: 'file-loader',
options: {
name: 'videos/[name].[hash:7].[ext]'
}
[this.options.dir.static]: path.join(
this.options.srcDir,
this.options.dir.static
)
}
}
rules() {
const styleLoader = createStyleLoader({
isServer: this.isServer
})
return [
{
test: /\.vue$/,
loader: 'vue-loader',
options: _.cloneDeep(this.options.build.vue)
},
{
test: /\.jsx?$/,
loader: 'babel-loader',
exclude: /node_modules/,
options: this.getBabelOptions()
},
{ test: /\.css$/, use: styleLoader.call(this.builder, 'css') },
{ test: /\.less$/, use: styleLoader.call(this.builder, 'less', 'less-loader') },
{
test: /\.sass$/,
use: styleLoader.call(this.builder, 'sass', {
loader: 'sass-loader',
options: { indentedSyntax: true }
})
},
{ test: /\.scss$/, use: styleLoader.call(this.builder, 'scss', 'sass-loader') },
{
test: /\.styl(us)?$/,
use: styleLoader.call(this.builder, 'stylus', 'stylus-loader')
},
{
test: /\.(png|jpe?g|gif|svg)$/,
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
name: 'fonts/[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
config.plugins.push(new StatsPlugin(this.options.build.stats))
// Hide warnings about plugins without a default export (#1179)
plugins.push(new WarnFixPlugin())
// Add friendly error plugin
config.plugins.push(
new FriendlyErrorsWebpackPlugin({
clearConsole: true,
logLevel: 'WARNING'
})
)
if (!this.options.test) {
// Build progress indicator
if (this.options.build.profile) {
plugins.push(new webpack.ProgressPlugin({ profile: true }))
} 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
return _.cloneDeep(config)
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 _ from 'lodash'
import webpack from 'webpack'
import HTMLPlugin from 'html-webpack-plugin'
@ -8,7 +7,7 @@ import BundleAnalyzer from 'webpack-bundle-analyzer'
import MiniCssExtractPlugin from 'mini-css-extract-plugin'
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 './plugins/vue/client'
@ -21,141 +20,140 @@ debug.color = 2 // Force green color
| Webpack Client Config
|--------------------------------------------------------------------------
*/
export default function webpackClientConfig() {
let config = base.call(this, { name: 'client', isServer: false })
export default class WebpackClientConfig extends BaseConfig {
constructor(builder) {
super(builder, { name: 'client', isServer: false })
}
// Entry points
config.entry = path.resolve(this.options.buildDir, 'client.js')
// Env object defined in nuxt.config.js
let env = {}
_.each(this.options.env, (value, key) => {
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'
env() {
return Object.assign(super.env(), {
'process.env.VUE_ENV': JSON.stringify('client'),
'process.browser': true,
'process.client': true,
'process.server': false
})
)
// 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
config.plugins.push(
new VueSSRClientPlugin({
filename: 'vue-ssr-client-manifest.json'
})
)
plugins() {
const plugins = super.plugins()
// Define Env
config.plugins.push(
new webpack.DefinePlugin(
Object.assign(env, {
'process.env.VUE_ENV': JSON.stringify('client'),
'process.mode': JSON.stringify(this.options.mode),
'process.browser': true,
'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
// Generate output HTML for SSR
if (this.options.build.ssr) {
plugins.push(
new HTMLPlugin({
filename: 'index.ssr.html',
template: 'lodash!' + this.options.appTemplatePath,
inject: false // Resources will be injected using bundleRenderer
})
)
}
// Webpack Bundle Analyzer
if (this.options.build.analyze) {
config.plugins.push(
new BundleAnalyzer.BundleAnalyzerPlugin(Object.assign({}, this.options.build.analyze))
)
plugins.push(
new HTMLPlugin({
filename: 'index.spa.html',
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
if (typeof this.options.build.extend === 'function') {
const isDev = this.options.dev
const extendedConfig = this.options.build.extend.call(this, config, {
isDev,
isClient: true
})
// Only overwrite config when something is returned for backwards compatibility
if (extendedConfig !== undefined) {
config = extendedConfig
// CSS extraction
const extractCSS = this.options.build.extractCSS
if (extractCSS) {
plugins.push(new MiniCssExtractPlugin(Object.assign({
filename: this.getFileName('css')
}, typeof extractCSS === 'object' ? extractCSS : {})))
}
return plugins
}
// CSS extraction
const extractCSS = this.options.build.extractCSS
if (extractCSS) {
config.plugins.push(new MiniCssExtractPlugin(Object.assign({
filename: this.getFileName('css')
}, typeof extractCSS === 'object' ? extractCSS : {})))
}
config() {
let config = super.config()
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 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 './plugins/vue/server'
@ -15,77 +14,79 @@ import VueSSRServerPlugin from './plugins/vue/server'
| Webpack Server Config
|--------------------------------------------------------------------------
*/
export default function webpackServerConfig() {
let config = base.call(this, { name: 'server', isServer: true })
export default class WebpackServerConfig extends BaseConfig {
constructor(builder) {
super(builder, { name: 'server', isServer: true })
}
// Env object defined in nuxt.config.js
let env = {}
_.each(this.options.env, (value, key) => {
env['process.env.' + key] =
['boolean', 'number'].indexOf(typeof value) !== -1
? value
: JSON.stringify(value)
})
env() {
return Object.assign(super.env(), {
'process.env.VUE_ENV': JSON.stringify('server'),
'process.browser': false,
'process.client': false,
'process.server': true
})
}
// Config devtool
config.devtool = this.options.dev ? 'cheap-source-map' : false
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([
plugins() {
const plugins = super.plugins()
plugins.push(
new VueSSRServerPlugin({
filename: 'server-bundle.json'
}),
new webpack.DefinePlugin(
Object.assign(env, {
'process.env.VUE_ENV': JSON.stringify('server'),
'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
}
new webpack.DefinePlugin(this.env())
)
return plugins
}
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
}
}