feat: rewrite webpack config (#30)

This commit is contained in:
pooya parsa 2020-09-02 14:27:27 +02:00
parent dab1a831a6
commit d6ed1dfc2c
24 changed files with 885 additions and 1017 deletions

View File

@ -289,7 +289,7 @@ export default () => ({
runtimeChunk: 'single', runtimeChunk: 'single',
minimize: undefined as boolean | undefined, minimize: undefined as boolean | undefined,
minimizer: undefined, minimizer: undefined,
cssMinimizer: undefined, // cssMinimizer: undefined,
splitChunks: { splitChunks: {
chunks: 'all', chunks: 'all',
name: undefined, name: undefined,
@ -299,7 +299,9 @@ export default () => ({
} }
} }
} }
} as WebpackConfiguration['optimization'] & { cssMinimizer: undefined | boolean | Record<string, any> }, } as WebpackConfiguration['optimization'] & {
// cssMinimizer: undefined | boolean | Record<string, any>
},
/** /**
* Enable [thread-loader](https://github.com/webpack-contrib/thread-loader#thread-loader) in webpack building * Enable [thread-loader](https://github.com/webpack-contrib/thread-loader#thread-loader) in webpack building
* *

View File

@ -346,9 +346,9 @@ function normalizeConfig (_options: CliConfiguration) {
} }
// Enable cssMinimizer only when extractCSS is enabled // Enable cssMinimizer only when extractCSS is enabled
if (options.build.optimization.cssMinimizer === undefined) { // if (options.build.optimization.cssMinimizer === undefined) {
options.build.optimization.cssMinimizer = options.build.extractCSS ? {} : false // options.build.optimization.cssMinimizer = options.build.extractCSS ? {} : false
} // }
const { loaders } = options.build const { loaders } = options.build
// const vueLoader = loaders.vue // const vueLoader = loaders.vue

View File

@ -5,13 +5,11 @@ import Glob from 'glob'
import webpackDevMiddleware from 'webpack-dev-middleware' import webpackDevMiddleware from 'webpack-dev-middleware'
import webpackHotMiddleware from 'webpack-hot-middleware' import webpackHotMiddleware from 'webpack-hot-middleware'
import consola from 'consola' import consola from 'consola'
import { Nuxt } from 'src/core' import { Nuxt } from 'src/core'
import { TARGETS, parallel, sequence, wrapArray, isModernRequest } from 'src/utils' import { TARGETS, parallel, sequence, wrapArray, isModernRequest } from 'src/utils'
import { createMFS } from './utils/mfs' import { createMFS } from './utils/mfs'
import { client, server } from './configs'
import * as WebpackConfigs from './config' import { createWebpackConfigContext, applyPresets, getWebpackConfig } from './utils/config'
import PerfLoader from './utils/perf-loader'
const glob = pify(Glob) const glob = pify(Glob)
@ -40,27 +38,32 @@ export class WebpackBundler {
} }
getWebpackConfig (name) { getWebpackConfig (name) {
const Config = WebpackConfigs[name.toLowerCase()] // eslint-disable-line import/namespace const ctx = createWebpackConfigContext({ nuxt: this.nuxt })
if (!Config) {
if (name === 'client') {
applyPresets(ctx, client)
} else if (name === 'server') {
applyPresets(ctx, server)
} else {
throw new Error(`Unsupported webpack config ${name}`) throw new Error(`Unsupported webpack config ${name}`)
} }
const config = new Config(this)
return config.config() return getWebpackConfig(ctx)
} }
async build () { async build () {
const { options } = this.nuxt const { options } = this.nuxt
const webpackConfigs = [ const webpackConfigs = [
this.getWebpackConfig('Client') this.getWebpackConfig('client')
] ]
if (options.modern) { if (options.modern) {
webpackConfigs.push(this.getWebpackConfig('Modern')) webpackConfigs.push(this.getWebpackConfig('modern'))
} }
if (options.build.ssr) { if (options.build.ssr) {
webpackConfigs.push(this.getWebpackConfig('Server')) webpackConfigs.push(this.getWebpackConfig('server'))
} }
await this.nuxt.callHook('webpack:config', webpackConfigs) await this.nuxt.callHook('webpack:config', webpackConfigs)
@ -95,13 +98,6 @@ export class WebpackBundler {
return compiler return compiler
}) })
// Warm up perfLoader before build
if (options.build.parallel) {
consola.info('Warming up worker pools')
PerfLoader.warmupAll({ dev: options.dev })
consola.success('Worker pools ready')
}
// Start Builds // Start Builds
const runner = options.dev ? parallel : sequence const runner = options.dev ? parallel : sequence
@ -128,7 +124,7 @@ export class WebpackBundler {
// --- Dev Build --- // --- Dev Build ---
if (options.dev) { if (options.dev) {
// Client buiild // Client build
if (['client', 'modern'].includes(name)) { if (['client', 'modern'].includes(name)) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
compiler.hooks.done.tap('nuxt-dev', () => { resolve() }) compiler.hooks.done.tap('nuxt-dev', () => { resolve() })

View File

@ -1,521 +0,0 @@
import path from 'path'
import consola from 'consola'
import TimeFixPlugin from 'time-fix-plugin'
import cloneDeep from 'lodash/cloneDeep'
import escapeRegExp from 'lodash/escapeRegExp'
import VueLoaderPlugin from 'vue-loader/dist/pluginWebpack5'
// import ExtractCssChunksPlugin from 'extract-css-chunks-webpack-plugin'
import MiniCssExtractPlugin from 'mini-css-extract-plugin'
import TerserWebpackPlugin from 'terser-webpack-plugin'
import WebpackBar from 'webpackbar'
import env from 'std-env'
import semver from 'semver'
import type { NormalizedConfiguration } from 'src/config'
import { TARGETS, isUrl, urlJoin, getPKG } from 'src/utils'
import PerfLoader from '../utils/perf-loader'
import StyleLoader from '../utils/style-loader'
import WarningIgnorePlugin from '../plugins/warning-ignore'
import { reservedVueTags } from '../utils/reserved-tags'
export default class WebpackBaseConfig {
options: NormalizedConfiguration
constructor (builder) {
this.builder = builder
this.options = builder.nuxt.options
}
get colors () {
return {
client: 'green',
server: 'orange',
modern: 'blue'
}
}
get devtool () {
return false
}
get nuxtEnv () {
return {
isDev: this.dev,
isServer: this.isServer,
isClient: !this.isServer,
isModern: Boolean(this.isModern),
isLegacy: Boolean(!this.isModern)
}
}
get mode () {
return this.dev ? 'development' : 'production'
}
get target () {
return this.options.target
}
get dev () {
return this.options.dev
}
get loaders () {
if (!this._loaders) {
this._loaders = cloneDeep(this.options.build.loaders)
// sass-loader<8 support (#6460)
const sassLoaderPKG = getPKG('sass-loader')
if (sassLoaderPKG && semver.lt(sassLoaderPKG.version, '8.0.0')) {
const { sass } = this._loaders
sass.indentedSyntax = sass.sassOptions.indentedSyntax
delete sass.sassOptions.indentedSyntax
}
}
return this._loaders
}
get modulesToTranspile () {
return [
/\.vue\.js/i, // include SFCs in node_modules
/consola\/src/,
...this.normalizeTranspile({ pathNormalize: true })
]
}
normalizeTranspile ({ pathNormalize = false } = {}) {
const transpile = []
for (let pattern of this.options.build.transpile) {
if (typeof pattern === 'function') {
pattern = pattern(this.nuxtEnv)
}
if (pattern instanceof RegExp) {
transpile.push(pattern)
} else if (typeof pattern === 'string') {
const posixModule = pattern.replace(/\\/g, '/')
transpile.push(new RegExp(escapeRegExp(
pathNormalize ? path.normalize(posixModule) : posixModule
)))
}
}
return transpile
}
getBabelOptions () {
const envName = this.name
const options = {
...this.options.build.babel,
envName
}
if (options.configFile || options.babelrc) {
return options
}
if (typeof options.plugins === 'function') {
options.plugins = options.plugins(
{
envName,
...this.nuxtEnv
}
)
}
const defaultPreset = [require.resolve('../../babel-preset-app'), {}]
if (typeof options.presets === 'function') {
options.presets = options.presets(
{
envName,
...this.nuxtEnv
},
defaultPreset
)
}
if (!options.presets) {
options.presets = [defaultPreset]
}
return options
}
getFileName (key) {
let fileName = this.options.build.filenames[key]
if (typeof fileName === 'function') {
fileName = fileName(this.nuxtEnv)
}
if (typeof fileName === 'string' && this.dev) {
const hash = /\[(chunkhash|contenthash|hash)(?::(\d+))?]/.exec(fileName)
if (hash) {
consola.warn(`Notice: Please do not use ${hash[1]} in dev mode to prevent memory leak`)
}
}
return fileName
}
env () {
const env = {
'process.env.NODE_ENV': JSON.stringify(this.mode),
'process.mode': JSON.stringify(this.mode),
'process.dev': this.dev,
'process.static': this.target === TARGETS.static,
'process.target': JSON.stringify(this.target)
}
if (this.options.build.aggressiveCodeRemoval) {
env['typeof process'] = JSON.stringify(this.isServer ? 'object' : 'undefined')
env['typeof window'] = JSON.stringify(!this.isServer ? 'object' : 'undefined')
env['typeof document'] = JSON.stringify(!this.isServer ? 'object' : 'undefined')
}
Object.entries(this.options.env).forEach(([key, value]) => {
env['process.env.' + key] =
['boolean', 'number'].includes(typeof value)
? value
: JSON.stringify(value)
})
return env
}
output () {
const {
build: { publicPath },
buildDir,
router
} = this.options
return {
path: path.resolve(buildDir, 'dist', this.isServer ? 'server' : 'client'),
filename: this.getFileName('app'),
chunkFilename: this.getFileName('chunk'),
publicPath: isUrl(publicPath) ? publicPath : urlJoin(router.base, publicPath)
}
}
cache () {
if (!this.options.build.cache) {
return false
}
return {
type: 'filesystem',
cacheDirectory: path.resolve('node_modules/.cache/@nuxt/webpack/'),
buildDependencies: {
config: [...this.options._nuxtConfigFiles]
},
...this.options.build.cache,
name: this.name
}
}
optimization () {
const optimization = cloneDeep(this.options.build.optimization)
if (optimization.minimize && optimization.minimizer === undefined) {
optimization.minimizer = this.minimizer()
}
return optimization
}
resolve () {
// Prioritize nested node_modules in webpack search path (#2558)
const webpackModulesDir = ['node_modules'].concat(this.options.modulesDir)
return {
resolve: {
extensions: ['.wasm', '.mjs', '.js', '.ts', '.json', '.vue', '.jsx', '.tsx'],
alias: this.alias(),
modules: webpackModulesDir
},
resolveLoader: {
modules: webpackModulesDir
}
}
}
minimizer () {
const minimizer = []
const { terser, cache } = this.options.build
// https://github.com/webpack-contrib/terser-webpack-plugin
if (terser) {
minimizer.push(
new TerserWebpackPlugin(Object.assign({
cache,
extractComments: {
condition: 'some',
filename: 'LICENSES'
},
terserOptions: {
compress: {
ecma: this.isModern ? 6 : undefined
},
mangle: {
reserved: reservedVueTags
}
}
}, terser))
)
}
return minimizer
}
alias () {
return {
...this.options.alias,
app: this.options.appDir,
'nuxt-build': this.options.buildDir,
'vue-meta': require.resolve(`vue-meta${this.isServer ? '' : '/dist/vue-meta.esm.browser.js'}`)
}
}
rules () {
const perfLoader = new PerfLoader(this.name, this.options)
const styleLoader = new StyleLoader(
this.builder.nuxt,
{ isServer: this.isServer, perfLoader }
)
const babelLoader = {
loader: require.resolve('babel-loader'),
options: this.getBabelOptions()
}
return [
{
test: /\.vue$/i,
loader: 'vue-loader',
options: this.loaders.vue
},
{
test: /\.pug$/i,
oneOf: [
{
resourceQuery: /^\?vue/i,
use: [{
loader: 'pug-plain-loader',
options: this.loaders.pugPlain
}]
},
{
use: [
'raw-loader',
{
loader: 'pug-plain-loader',
options: this.loaders.pugPlain
}
]
}
]
},
{
test: /\.m?[jt]sx?$/i,
exclude: (file) => {
file = file.split('node_modules', 2)[1]
// not exclude files outside node_modules
if (!file) {
return false
}
// item in transpile can be string or regex object
return !this.modulesToTranspile.some(module => module.test(file))
},
use: perfLoader.js().concat(babelLoader)
},
{
test: /\.css$/i,
oneOf: styleLoader.apply('css')
},
{
test: /\.p(ost)?css$/i,
oneOf: styleLoader.apply('postcss')
},
{
test: /\.less$/i,
oneOf: styleLoader.apply('less', {
loader: 'less-loader',
options: this.loaders.less
})
},
{
test: /\.sass$/i,
oneOf: styleLoader.apply('sass', {
loader: 'sass-loader',
options: this.loaders.sass
})
},
{
test: /\.scss$/i,
oneOf: styleLoader.apply('scss', {
loader: 'sass-loader',
options: this.loaders.scss
})
},
{
test: /\.styl(us)?$/i,
oneOf: styleLoader.apply('stylus', {
loader: 'stylus-loader',
options: this.loaders.stylus
})
},
{
test: /\.(png|jpe?g|gif|svg|webp)$/i,
use: [{
loader: 'url-loader',
options: Object.assign(
this.loaders.imgUrl,
{ name: this.getFileName('img') }
)
}]
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i,
use: [{
loader: 'url-loader',
options: Object.assign(
this.loaders.fontUrl,
{ name: this.getFileName('font') }
)
}]
},
{
test: /\.(webm|mp4|ogv)$/i,
use: [{
loader: 'file-loader',
options: Object.assign(
this.loaders.file,
{ name: this.getFileName('video') }
)
}]
}
]
}
plugins () {
const plugins = []
const { nuxt } = this.builder
const { build: buildOptions } = this.options
// Add timefix-plugin before others plugins
if (this.dev) {
plugins.push(new TimeFixPlugin())
}
// CSS extraction)
if (buildOptions.extractCSS) {
plugins.push(new MiniCssExtractPlugin(Object.assign({
filename: this.getFileName('css'),
chunkFilename: this.getFileName('css')
}, buildOptions.extractCSS)))
}
plugins.push(new VueLoaderPlugin())
plugins.push(...(buildOptions.plugins || []))
plugins.push(new WarningIgnorePlugin(this.warningIgnoreFilter()))
// Build progress indicator
plugins.push(new WebpackBar({
name: this.name,
color: this.colors[this.name],
reporters: [
'basic',
'fancy',
'profile',
'stats'
],
basic: !buildOptions.quiet && env.minimalCLI,
fancy: !buildOptions.quiet && !env.minimalCLI,
profile: !buildOptions.quiet && buildOptions.profile,
stats: !buildOptions.quiet && !this.dev && buildOptions.stats,
reporter: {
change: (_, { shortPath }) => {
if (!this.isServer) {
nuxt.callHook('bundler:change', shortPath)
}
},
done: (stats) => {
if (stats.hasErrors) {
nuxt.callHook('bundler:error')
}
},
allDone: () => {
nuxt.callHook('bundler:done')
},
progress ({ statesArray }) {
nuxt.callHook('bundler:progress', statesArray)
}
}
}))
// CSS extraction
if (this.options.build.extractCSS) {
plugins.push(new MiniCssExtractPlugin(Object.assign({
filename: this.getFileName('css'),
chunkFilename: this.getFileName('css'),
// TODO: https://github.com/faceyspacey/extract-css-chunks-webpack-plugin/issues/132
reloadAll: true
}, this.options.build.extractCSS)))
}
return plugins
}
warningIgnoreFilter () {
const filters = [
// Hide warnings about plugins without a default export (#1179)
warn => warn.name === 'ModuleDependencyWarning' &&
warn.message.includes('export \'default\'') &&
warn.message.includes('nuxt_plugin_'),
...(this.options.build.warningIgnoreFilters || [])
]
return warn => !filters.some(ignoreFilter => ignoreFilter(warn))
}
extendConfig (config) {
const { extend } = this.options.build
if (typeof extend === 'function') {
const extendedConfig = extend.call(
this.builder, config, { loaders: this.loaders, ...this.nuxtEnv }
) || config
const pragma = /@|#/
const { devtool } = extendedConfig
if (typeof devtool === 'string' && pragma.test(devtool)) {
extendedConfig.devtool = devtool.replace(pragma, '')
consola.warn(`devtool has been normalized to ${extendedConfig.devtool} as webpack documented value`)
}
return extendedConfig
}
return config
}
config () {
const config = {
name: this.name,
mode: this.mode,
devtool: this.devtool,
cache: this.cache(),
optimization: this.optimization(),
output: this.output(),
performance: {
maxEntrypointSize: 1000 * 1024,
hints: this.dev ? false : 'warning'
},
module: {
rules: this.rules()
},
plugins: this.plugins(),
...this.resolve()
}
// Clone deep avoid leaking config between Client and Server
const extendedConfig = cloneDeep(this.extendConfig(config))
return extendedConfig
}
}

View File

@ -1,233 +0,0 @@
import path from 'path'
import querystring from 'querystring'
import webpack from 'webpack'
import HTMLPlugin from 'html-webpack-plugin'
import BundleAnalyzer from 'webpack-bundle-analyzer'
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'
import FriendlyErrorsWebpackPlugin from '@nuxt/friendly-errors-webpack-plugin'
import CorsPlugin from '../plugins/vue/cors'
import ModernModePlugin from '../plugins/vue/modern'
import VueSSRClientPlugin from '../plugins/vue/client'
import WebpackBaseConfig from './base'
export default class WebpackClientConfig extends WebpackBaseConfig {
constructor (builder) {
super(builder)
this.name = 'client'
this.isServer = false
this.isModern = false
}
get devtool () {
if (!this.dev) {
return false
}
const scriptPolicy = this.getCspScriptPolicy()
const noUnsafeEval = scriptPolicy && !scriptPolicy.includes('\'unsafe-eval\'')
return noUnsafeEval
? 'cheap-module-source-map'
: 'eval-cheap-module-source-map'
}
getCspScriptPolicy () {
const { csp } = this.options.render
if (typeof csp === 'object') {
const { policies = {} } = csp
return policies['script-src'] || policies['default-src'] || []
}
}
env () {
return Object.assign(
super.env(),
{
'process.env.VUE_ENV': JSON.stringify('client'),
'process.browser': true,
'process.client': true,
'process.server': false,
'process.modern': false
}
)
}
optimization () {
const optimization = super.optimization()
const { splitChunks } = optimization
const { cacheGroups } = splitChunks
// 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 &&
cacheGroups.commons === undefined
) {
cacheGroups.commons = {
test: /node_modules[\\/](vue|vue-loader|vue-router|vuex|vue-meta|core-js|@babel\/runtime|axios|webpack|setimmediate|timers-browserify|process|regenerator-runtime|cookie|js-cookie|is-buffer|dotprop|nuxt\.js)[\\/]/,
chunks: 'all',
priority: 10,
name: 'commons',
automaticNameDelimiter: '/'
}
}
if (!this.dev && cacheGroups.default && cacheGroups.default.name === undefined) {
cacheGroups.default.name = (_module, chunks) => {
// Use default name for single chunks
if (chunks.length === 1) {
return chunks[0].name || ''
}
// Use compact name for concatinated modules
return 'commons/' + chunks.filter(c => c.name).map(c =>
c.name.replace(/\//g, '.').replace(/_/g, '').replace('pages.', '')
).join('~')
}
}
return optimization
}
minimizer () {
const minimizer = super.minimizer()
const { cssMinimizer } = this.options.build.optimization
if (cssMinimizer) {
minimizer.push(new CssMinimizerPlugin(Object.assign({}, cssMinimizer)))
}
return minimizer
}
alias () {
const aliases = super.alias()
for (const p of this.builder.plugins) {
if (!aliases[p.name]) {
// Do not load server-side plugins on client-side
aliases[p.name] = p.mode === 'server' ? './empty.js' : p.src
}
}
return aliases
}
plugins () {
const plugins = super.plugins()
const { build: buildOptions, appTemplatePath, buildDir, modern, render } = this.options
// Generate output HTML for SSR
if (buildOptions.ssr) {
plugins.push(
new HTMLPlugin({
filename: '../server/index.ssr.html',
template: appTemplatePath,
minify: buildOptions.html.minify,
inject: false // Resources will be injected using bundleRenderer
})
)
}
plugins.push(
new HTMLPlugin({
filename: '../server/index.spa.html',
template: appTemplatePath,
minify: buildOptions.html.minify,
inject: true
}),
new VueSSRClientPlugin({
filename: `../server/${this.name}.manifest.json`
}),
new webpack.DefinePlugin(this.env())
)
if (this.dev) {
// TODO: webpackHotUpdate is not defined: https://github.com/webpack/webpack/issues/6693
plugins.push(new webpack.HotModuleReplacementPlugin())
}
// Webpack Bundle Analyzer
// https://github.com/webpack-contrib/webpack-bundle-analyzer
if (!this.dev && buildOptions.analyze) {
const statsDir = path.resolve(buildDir, 'stats')
plugins.push(new BundleAnalyzer.BundleAnalyzerPlugin(Object.assign({
analyzerMode: 'static',
defaultSizes: 'gzip',
generateStatsFile: true,
openAnalyzer: !buildOptions.quiet,
reportFilename: path.resolve(statsDir, `${this.name}.html`),
statsFilename: path.resolve(statsDir, `${this.name}.json`)
}, buildOptions.analyze)))
}
if (modern) {
const scriptPolicy = this.getCspScriptPolicy()
const noUnsafeInline = scriptPolicy && !scriptPolicy.includes('\'unsafe-inline\'')
plugins.push(new ModernModePlugin({
targetDir: path.resolve(buildDir, 'dist', 'client'),
isModernBuild: this.isModern,
noUnsafeInline
}))
}
if (render.crossorigin) {
plugins.push(new CorsPlugin({
crossorigin: render.crossorigin
}))
}
return plugins
}
config () {
const config = super.config()
const {
router,
buildDir,
build: { hotMiddleware, quiet, friendlyErrors }
} = this.options
const { client = {} } = hotMiddleware || {}
const { ansiColors, overlayStyles, ...options } = client
const hotMiddlewareClientOptions = {
reload: true,
timeout: 30000,
ansiColors: JSON.stringify(ansiColors),
overlayStyles: JSON.stringify(overlayStyles),
path: `${router.base}/__webpack_hmr/${this.name}`.replace(/\/\//g, '/'),
...options,
name: this.name
}
const hotMiddlewareClientOptionsStr = querystring.stringify(hotMiddlewareClientOptions)
// Entry points
config.entry = Object.assign({}, config.entry, {
app: [path.resolve(buildDir, 'entry.client.ts')]
})
// Add HMR support
if (this.dev) {
config.entry.app.unshift(
// https://github.com/webpack-contrib/webpack-hot-middleware/issues/53#issuecomment-162823945
'eventsource-polyfill',
// https://github.com/glenjamin/webpack-hot-middleware#config
`webpack-hot-middleware/client?${hotMiddlewareClientOptionsStr}`
)
}
// Add friendly error plugin
if (this.dev && !quiet && friendlyErrors) {
config.plugins.push(
new FriendlyErrorsWebpackPlugin({
clearConsole: false,
reporter: 'consola',
logLevel: 'WARNING'
})
)
}
return config
}
}

View File

@ -1,3 +0,0 @@
export { default as client } from './client'
export { default as modern } from './modern'
export { default as server } from './server'

View File

@ -1,15 +0,0 @@
import WebpackClientConfig from './client'
export default class WebpackModernConfig extends WebpackClientConfig {
constructor (...args) {
super(...args)
this.name = 'modern'
this.isModern = true
}
env () {
return Object.assign(super.env(), {
'process.modern': true
})
}
}

View File

@ -1,161 +0,0 @@
import path from 'path'
import fs from 'fs'
import { DefinePlugin, ProvidePlugin } from 'webpack'
import FriendlyErrorsWebpackPlugin from '@nuxt/friendly-errors-webpack-plugin'
// TODO: remove when webpack-node-externals support webpack5
// import nodeExternals from 'webpack-node-externals'
import nodeExternals from '../plugins/externals'
import VueSSRServerPlugin from '../plugins/vue/server'
import WebpackBaseConfig from './base'
const nativeFileExtensions = [
'.json',
'.js'
]
export default class WebpackServerConfig extends WebpackBaseConfig {
constructor (...args) {
super(...args)
this.name = 'server'
this.isServer = true
}
get devtool () {
return 'cheap-module-source-map'
}
get externalsWhitelist () {
return [
this.isNonNativeImport.bind(this),
...this.normalizeTranspile()
]
}
/**
* files *not* ending on js|json should be processed by webpack
*
* this might generate false-positives for imports like
* - "someFile.umd" (actually requiring someFile.umd.js)
* - "some.folder" (some.folder being a directory containing a package.json)
*/
isNonNativeImport (modulePath) {
const extname = path.extname(modulePath)
return extname !== '' && !nativeFileExtensions.includes(extname)
}
env () {
return Object.assign(
super.env(),
{
'process.env.VUE_ENV': JSON.stringify('server'),
'process.browser': false,
'process.client': false,
'process.server': true,
'process.modern': false
}
)
}
optimization () {
const { _minifyServer } = this.options.build
return {
splitChunks: false,
minimizer: _minifyServer ? this.minimizer() : []
}
}
resolve () {
const resolveConfig = super.resolve()
resolveConfig.resolve.mainFields = ['main', 'module']
return resolveConfig
}
alias () {
const aliases = super.alias()
for (const p of this.builder.plugins) {
if (!aliases[p.name]) {
// Do not load client-side plugins on server-side
aliases[p.name] = p.mode === 'client' ? './empty.js' : p.src
}
}
return aliases
}
plugins () {
const plugins = super.plugins()
plugins.push(
new VueSSRServerPlugin({ filename: `${this.name}.manifest.json` }),
new DefinePlugin(this.env())
)
const { serverURLPolyfill } = this.options.build
if (serverURLPolyfill) {
plugins.push(new ProvidePlugin({
URL: [serverURLPolyfill, 'URL'],
URLSearchParams: [serverURLPolyfill, 'URLSearchParams']
}))
}
return plugins
}
config () {
const config = super.config()
Object.assign(config, {
target: 'node',
node: false,
entry: Object.assign({}, config.entry, {
app: [path.resolve(this.options.buildDir, 'entry.server.ts')]
}),
output: Object.assign({}, config.output, {
filename: 'server.js',
chunkFilename: '[name].js',
libraryTarget: 'commonjs2'
}),
performance: {
hints: false,
maxEntrypointSize: Infinity,
maxAssetSize: Infinity
},
externals: [].concat(config.externals || [])
})
// https://webpack.js.org/configuration/externals/#externals
// https://github.com/liady/webpack-node-externals
// https://vue-loader.vuejs.org/migrating.html#ssr-externals
if (!this.options.build.standalone) {
this.options.modulesDir.forEach((dir) => {
if (fs.existsSync(dir)) {
config.externals.push(
nodeExternals({
whitelist: this.externalsWhitelist,
modulesDir: dir
})
)
}
})
}
// Add friendly error plugin
if (this.dev) {
config.plugins.push(
new FriendlyErrorsWebpackPlugin({
clearConsole: false,
reporter: 'consola',
logLevel: 'WARNING'
})
)
}
return config
}
}

View File

@ -0,0 +1,147 @@
import path from 'path'
import querystring from 'querystring'
import webpack from 'webpack'
import HTMLPlugin from 'html-webpack-plugin'
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'
import CorsPlugin from '../plugins/vue/cors'
import { applyPresets, WebpackConfigContext } from '../utils/config'
import { nuxt } from '../presets/nuxt'
export function client (ctx: WebpackConfigContext) {
ctx.name = 'client'
ctx.isClient = true
applyPresets(ctx, [
nuxt,
clientPlugins,
clientOptimization,
clientDevtool,
clientPerformance,
clientHMR,
clientHTML
])
}
function clientDevtool (ctx: WebpackConfigContext) {
if (!ctx.isDev) {
ctx.config.devtool = false
return
}
const scriptPolicy = getCspScriptPolicy(ctx)
const noUnsafeEval = scriptPolicy && !scriptPolicy.includes('\'unsafe-eval\'')
ctx.config.devtool = noUnsafeEval
? 'cheap-module-source-map'
: 'eval-cheap-module-source-map'
}
function clientPerformance (ctx: WebpackConfigContext) {
ctx.config.performance = {
maxEntrypointSize: 1000 * 1024,
hints: ctx.isDev ? false : 'warning',
...ctx.config.performance
}
}
function clientHMR (ctx: WebpackConfigContext) {
const { options, config } = ctx
if (!ctx.isDev) {
return
}
const clientOptions = options.build.hotMiddleware?.client || {}
const hotMiddlewareClientOptions = {
reload: true,
timeout: 30000,
ansiColors: JSON.stringify(clientOptions.ansiColors || {}),
overlayStyles: JSON.stringify(clientOptions.overlayStyles || {}),
path: `${options.router.base}/__webpack_hmr/${ctx.name}`.replace(/\/\//g, '/'),
...clientOptions,
name: ctx.name
}
const hotMiddlewareClientOptionsStr = querystring.stringify(hotMiddlewareClientOptions)
// Add HMR support
const app = (config.entry as any).app as any
app.unshift(
// https://github.com/webpack-contrib/webpack-hot-middleware/issues/53#issuecomment-162823945
'eventsource-polyfill',
// https://github.com/glenjamin/webpack-hot-middleware#config
`webpack-hot-middleware/client?${hotMiddlewareClientOptionsStr}`
)
// TODO: webpackHotUpdate is not defined: https://github.com/webpack/webpack/issues/6693
config.plugins.push(new webpack.HotModuleReplacementPlugin())
}
function clientOptimization (ctx: WebpackConfigContext) {
const { options, config } = ctx
config.optimization = {
...config.optimization,
...options.build.optimization as any
}
// TODO: Improve optimization.splitChunks.cacheGroups
}
function clientHTML (ctx: WebpackConfigContext) {
const { options, config } = ctx
// Generate output HTML for SSR
if (options.build.ssr) {
config.plugins.push(
new HTMLPlugin({
filename: '../server/index.ssr.html',
template: options.appTemplatePath,
minify: options.build.html.minify as any,
inject: false // Resources will be injected using bundleRenderer
})
)
}
config.plugins.push(
new HTMLPlugin({
filename: '../server/index.spa.html',
template: options.appTemplatePath,
minify: options.build.html.minify as any,
inject: true
})
)
}
function clientPlugins (ctx: WebpackConfigContext) {
const { options, config } = ctx
// Webpack Bundle Analyzer
// https://github.com/webpack-contrib/webpack-bundle-analyzer
if (!ctx.isDev && options.build.analyze) {
const statsDir = path.resolve(options.buildDir, 'stats')
config.plugins.push(new BundleAnalyzerPlugin({
analyzerMode: 'static',
defaultSizes: 'gzip',
generateStatsFile: true,
openAnalyzer: !options.build.quiet,
reportFilename: path.resolve(statsDir, `${ctx.name}.html`),
statsFilename: path.resolve(statsDir, `${ctx.name}.json`),
...options.build.analyze as any
}))
}
// CORS
if (ctx.options.render.crossorigin) {
ctx.config.plugins.push(new CorsPlugin({
crossorigin: ctx.options.render.crossorigin
}))
}
}
function getCspScriptPolicy (ctx: WebpackConfigContext) {
const { csp } = ctx.options.render
if (typeof csp === 'object') {
const { policies = {} } = csp
return policies['script-src'] || policies['default-src'] || []
}
}

View File

@ -0,0 +1,2 @@
export { client } from './client'
export { server } from './server'

View File

@ -0,0 +1,69 @@
import path from 'path'
import fs from 'fs'
import { ProvidePlugin } from 'webpack'
import nodeExternals from '../plugins/externals'
import { WebpackConfigContext, applyPresets, getWebpackConfig } from '../utils/config'
import { nuxt } from '../presets/nuxt'
import { node } from '../presets/node'
export function server (ctx: WebpackConfigContext) {
ctx.name = 'server'
ctx.isServer = true
applyPresets(ctx, [
nuxt,
node,
serverStandalone,
serverPreset,
serverPlugins
])
return getWebpackConfig(ctx)
}
function serverPreset (ctx: WebpackConfigContext) {
const { config } = ctx
config.output.filename = 'server.js'
config.devtool = 'cheap-module-source-map'
config.optimization = {
splitChunks: false,
minimize: false
}
}
function serverStandalone (ctx: WebpackConfigContext) {
const { options, config } = ctx
// https://webpack.js.org/configuration/externals/#externals
// https://github.com/liady/webpack-node-externals
// https://vue-loader.vuejs.org/migrating.html#ssr-externals
if (!options.build.standalone) {
options.modulesDir.forEach((dir) => {
if (fs.existsSync(dir)) {
(config.externals as any[]).push(
nodeExternals({
whitelist: [
modulePath => !['.js', '.json', ''].includes(path.extname(modulePath)),
ctx.transpile
],
modulesDir: dir
})
)
}
})
}
}
function serverPlugins (ctx: WebpackConfigContext) {
const { config, options } = ctx
// Server polyfills
if (options.build.serverURLPolyfill) {
config.plugins.push(new ProvidePlugin({
URL: [options.build.serverURLPolyfill, 'URL'],
URLSearchParams: [options.build.serverURLPolyfill, 'URLSearchParams']
}))
}
}

View File

@ -0,0 +1,38 @@
import { fileName, WebpackConfigContext } from '../utils/config'
export function assets (ctx: WebpackConfigContext) {
const { options } = ctx
ctx.config.module.rules.push(
{
test: /\.(png|jpe?g|gif|svg|webp)$/i,
use: [{
loader: 'url-loader',
options: {
...options.build.loaders.imgUrl,
name: fileName(ctx, 'img')
}
}]
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i,
use: [{
loader: 'url-loader',
options: {
...options.build.loaders.fontUrl,
name: fileName(ctx, 'font')
}
}]
},
{
test: /\.(webm|mp4|ogv)$/i,
use: [{
loader: 'file-loader',
options: {
...options.build.loaders.file,
name: fileName(ctx, 'video')
}
}]
}
)
}

View File

@ -0,0 +1,78 @@
import TerserWebpackPlugin from 'terser-webpack-plugin'
import { WebpackPluginInstance } from 'webpack'
import { reservedVueTags } from '../utils/reserved-tags'
import { WebpackConfigContext } from '../utils/config'
export function babel (ctx: WebpackConfigContext) {
const { config, options } = ctx
const babelLoader = {
loader: require.resolve('babel-loader'),
options: getBabelOptions(ctx)
}
config.module.rules.push({
test: /\.m?[jt]sx?$/i,
exclude: (file) => {
file = file.split('node_modules', 2)[1]
// not exclude files outside node_modules
if (!file) {
return false
}
// item in transpile can be string or regex object
return !ctx.transpile.some(module => module.test(file))
},
use: babelLoader
})
// https://github.com/webpack-contrib/terser-webpack-plugin
if (options.build.terser) {
const terser = new TerserWebpackPlugin({
// cache, TODO
extractComments: {
condition: 'some',
filename: 'LICENSES'
},
terserOptions: {
compress: {
ecma: ctx.isModern ? 6 : undefined
},
mangle: {
reserved: reservedVueTags
}
},
...options.build.terser as any
})
config.plugins.push(terser as WebpackPluginInstance)
}
}
function getBabelOptions (ctx: WebpackConfigContext) {
const { options } = ctx
const babelOptions: any = {
...options.build.babel,
envName: ctx.name
}
if (babelOptions.configFile || babelOptions.babelrc) {
return babelOptions
}
if (typeof babelOptions.plugins === 'function') {
babelOptions.plugins = babelOptions.plugins(ctx)
}
const defaultPreset = [require.resolve('../../babel-preset-app'), {}]
if (typeof babelOptions.presets === 'function') {
babelOptions.presets = babelOptions.presets(ctx, defaultPreset)
}
if (!babelOptions.presets) {
babelOptions.presets = [defaultPreset]
}
return babelOptions
}

View File

@ -0,0 +1,240 @@
import { resolve, normalize } from 'path'
import TimeFixPlugin from 'time-fix-plugin'
import WebpackBar from 'webpackbar'
import stdEnv from 'std-env'
import { DefinePlugin, Configuration } from 'webpack'
import FriendlyErrorsWebpackPlugin from '@nuxt/friendly-errors-webpack-plugin'
import { isUrl, urlJoin, TARGETS } from 'src/utils'
import escapeRegExp from 'lodash/escapeRegExp'
import WarningIgnorePlugin from '../plugins/warning-ignore'
import { WebpackConfigContext, applyPresets, fileName } from '../utils/config'
export function base (ctx: WebpackConfigContext) {
applyPresets(ctx, [
baseAlias,
baseConfig,
basePlugins,
baseResolve
])
}
function baseConfig (ctx: WebpackConfigContext) {
const { options } = ctx
ctx.config = {
name: ctx.name,
entry: { app: [resolve(options.buildDir, `entry.${ctx.name}.ts`)] },
module: { rules: [] },
plugins: [],
externals: [],
optimization: {
...options.build.optimization as any,
minimizer: []
},
mode: ctx.isDev ? 'development' : 'production',
cache: getCache(ctx),
output: getOutput(ctx),
...ctx.config
}
}
function basePlugins (ctx: WebpackConfigContext) {
const { config, options, nuxt } = ctx
// Add timefix-plugin before others plugins
if (options.dev) {
config.plugins.push(new TimeFixPlugin())
}
// User plugins
config.plugins.push(...(options.build.plugins || []))
// Ignore empty warnings
config.plugins.push(new WarningIgnorePlugin(getWarningIgnoreFilter(ctx)))
// Provide env via DefinePlugin
config.plugins.push(new DefinePlugin(getEnv(ctx)))
// Friendly errors
if (
ctx.isServer ||
(ctx.isDev && !options.build.quiet && options.build.friendlyErrors)
) {
ctx.config.plugins.push(
new FriendlyErrorsWebpackPlugin({
clearConsole: false,
reporter: 'consola',
logLevel: 'WARNING'
})
)
}
// Webpackbar
const colors = {
client: 'green',
server: 'orange',
modern: 'blue'
}
config.plugins.push(new WebpackBar({
name: ctx.name,
color: colors[ctx.name],
reporters: [
'basic',
'fancy',
'profile',
'stats'
],
basic: !options.build.quiet && stdEnv.minimalCLI,
fancy: !options.build.quiet && !stdEnv.minimalCLI,
profile: !options.build.quiet && options.build.profile,
stats: !options.build.quiet && !options.dev && options.build.stats,
reporter: {
change: (_, { shortPath }) => {
if (!ctx.isServer) {
nuxt.callHook('bundler:change', shortPath)
}
},
done: (stats) => {
if (stats.hasErrors) {
nuxt.callHook('bundler:error')
}
},
allDone: () => {
nuxt.callHook('bundler:done')
},
progress ({ statesArray }) {
nuxt.callHook('bundler:progress', statesArray)
}
}
}))
}
function baseAlias (ctx: WebpackConfigContext) {
const { options, isServer } = ctx
ctx.alias = {
app: options.appDir,
'nuxt-build': options.buildDir,
'vue-meta': require.resolve(`vue-meta${isServer ? '' : '/dist/vue-meta.esm.browser.js'}`),
...options.alias,
...ctx.alias
}
}
function baseResolve (ctx: WebpackConfigContext) {
const { options, config } = ctx
// Prioritize nested node_modules in webpack search path (#2558)
// TODO: this might be refactored as default modulesDir?
const webpackModulesDir = ['node_modules'].concat(options.modulesDir)
config.resolve = {
extensions: ['.wasm', '.mjs', '.js', '.ts', '.json', '.vue', '.jsx', '.tsx'],
alias: ctx.alias,
modules: webpackModulesDir,
...config.resolve
}
config.resolveLoader = {
modules: webpackModulesDir,
...config.resolveLoader
}
}
export function baseTranspile (ctx: WebpackConfigContext) {
const { options } = ctx
const transpile = [
/\.vue\.js/i, // include SFCs in node_modules
/consola\/src/
]
for (let pattern of options.build.transpile) {
if (typeof pattern === 'function') {
pattern = pattern(ctx)
}
if (typeof pattern === 'string') {
const posixModule = pattern.replace(/\\/g, '/')
// TODO: should only do for clientside? (hint: pathNormalize)
transpile.push(new RegExp(escapeRegExp(normalize(posixModule))))
} else if (pattern instanceof RegExp) {
transpile.push(pattern)
}
}
// TODO: unique
ctx.transpile = [...transpile, ...ctx.transpile]
}
function getCache (ctx: WebpackConfigContext): Configuration['cache'] {
const { options } = ctx
if (!options.build.cache) {
return false
}
return {
type: 'filesystem',
cacheDirectory: resolve('node_modules/.cache/@nuxt/webpack/'),
buildDependencies: {
config: [...options._nuxtConfigFiles]
},
...(options.build.cache as any),
name
}
}
function getOutput (ctx: WebpackConfigContext): Configuration['output'] {
const { options } = ctx
return {
path: resolve(options.buildDir, 'dist', ctx.isServer ? 'server' : 'client'),
filename: fileName(ctx, 'app'),
chunkFilename: fileName(ctx, 'chunk'),
publicPath: isUrl(options.build.publicPath) ? options.build.publicPath
: urlJoin(options.router.base, options.build.publicPath)
}
}
function getWarningIgnoreFilter (ctx: WebpackConfigContext) {
const { options } = ctx
const filters = [
// Hide warnings about plugins without a default export (#1179)
warn => warn.name === 'ModuleDependencyWarning' &&
warn.message.includes('export \'default\'') &&
warn.message.includes('nuxt_plugin_'),
...(options.build.warningIgnoreFilters || [])
]
return warn => !filters.some(ignoreFilter => ignoreFilter(warn))
}
function getEnv (ctx: WebpackConfigContext) {
const { options } = ctx
const _env = {
'process.env.NODE_ENV': JSON.stringify(ctx.config.mode),
'process.mode': JSON.stringify(ctx.config.mode),
'process.dev': options.dev,
'process.static': options.target === TARGETS.static,
'process.target': JSON.stringify(options.target),
'process.env.VUE_ENV': JSON.stringify(ctx.name),
'process.browser': ctx.isClient,
'process.client': ctx.isClient,
'process.server': ctx.isServer,
'process.modern': ctx.isModern
}
if (options.build.aggressiveCodeRemoval) {
_env['typeof process'] = JSON.stringify(ctx.isServer ? 'object' : 'undefined')
_env['typeof window'] = _env['typeof document'] = JSON.stringify(!ctx.isServer ? 'object' : 'undefined')
}
Object.entries(options.env).forEach(([key, value]) => {
const isNative = ['boolean', 'number'].includes(typeof value)
_env['process.env.' + key] = isNative ? value : JSON.stringify(value)
})
return _env
}

View File

@ -0,0 +1,40 @@
import { ESBuildPlugin, ESBuildMinifyPlugin } from 'esbuild-loader'
import { WebpackConfigContext } from '../utils/config'
export function esbuild (ctx: WebpackConfigContext) {
const { config } = ctx
config.optimization.minimizer.push(new ESBuildMinifyPlugin())
config.plugins.push(new ESBuildPlugin())
config.module.rules.push(
{
test: /\.[jt]sx?$/,
loader: 'esbuild-loader',
exclude: (file) => {
file = file.split('node_modules', 2)[1]
// Not exclude files outside node_modules
if (!file) {
return false
}
// Item in transpile can be string or regex object
return !ctx.transpile.some(module => module.test(file))
},
options: {
loader: 'ts',
target: 'es2015'
}
},
{
test: /\.tsx$/,
loader: 'esbuild-loader',
options: {
loader: 'tsx',
target: 'es2015'
}
}
)
}

View File

@ -0,0 +1,23 @@
import { WebpackConfigContext } from '../utils/config'
export function node (ctx: WebpackConfigContext) {
const { config } = ctx
config.target = 'node'
config.node = false
config.resolve.mainFields = ['main', 'module']
config.output = {
...config.output,
chunkFilename: '[name].js',
libraryTarget: 'commonjs2'
}
config.performance = {
...config.performance,
hints: false,
maxEntrypointSize: Infinity,
maxAssetSize: Infinity
}
}

View File

@ -0,0 +1,20 @@
import { WebpackConfigContext, applyPresets } from '../utils/config'
import { assets } from './assets'
import { base } from './base'
import { esbuild } from './esbuild'
import { pug } from './pug'
import { style } from './style'
import { vue } from './vue'
export function nuxt (ctx: WebpackConfigContext) {
applyPresets(ctx, [
base,
assets,
// babel,
esbuild,
pug,
style,
vue
])
}

View File

@ -0,0 +1,25 @@
import { WebpackConfigContext } from '../utils/config'
export function pug (ctx: WebpackConfigContext) {
ctx.config.module.rules.push({
test: /\.pug$/i,
oneOf: [
{
resourceQuery: /^\?vue/i,
use: [{
loader: 'pug-plain-loader',
options: ctx.options.build.loaders.pugPlain
}]
},
{
use: [
'raw-loader',
{
loader: 'pug-plain-loader',
options: ctx.options.build.loaders.pugPlain
}
]
}
]
})
}

View File

@ -0,0 +1,62 @@
// import MiniCssExtractPlugin from 'mini-css-extract-plugin'
// import OptimizeCSSAssetsPlugin from 'optimize-css-assets-webpack-plugin'
// import StyleLoader from '../utils/style-loader'
import { WebpackConfigContext } from '../utils/config'
export function style (_ctx: WebpackConfigContext) {
// // CSS extraction)
// if (options.build.extractCSS) {
// plugins.push(new MiniCssExtractPlugin(Object.assign({
// filename: fileName(ctx, 'css'),
// chunkFilename: fileName(ctx, 'css')
// }, options.build.extractCSS)))
// }
// CSS extraction
// if (options.build.extractCSS) {
// plugins.push(new MiniCssExtractPlugin(Object.assign({
// filename: fileName(ctx, 'css'),
// chunkFilename: fileName(ctx, 'css'),
// // TODO: https://github.com/faceyspacey/extract-css-chunks-webpack-plugin/issues/132
// reloadAll: true
// }, options.build.extractCSS)))
// }
return [
// {
// test: /\.css$/i,
// oneOf: styleLoader.apply('css')
// },
// {
// test: /\.p(ost)?css$/i,
// oneOf: styleLoader.apply('postcss')
// },
// {
// test: /\.less$/i,
// oneOf: styleLoader.apply('less', {
// loader: 'less-loader',
// options: loaders.less
// })
// },
// {
// test: /\.sass$/i,
// oneOf: styleLoader.apply('sass', {
// loader: 'sass-loader',
// options: loaders.sass
// })
// },
// {
// test: /\.scss$/i,
// oneOf: styleLoader.apply('scss', {
// loader: 'sass-loader',
// options: loaders.scss
// })
// },
// {
// test: /\.styl(us)?$/i,
// oneOf: styleLoader.apply('stylus', {
// loader: 'stylus-loader',
// options: loaders.stylus
// })
// }
]
}

View File

@ -0,0 +1,26 @@
import VueLoaderPlugin from 'vue-loader/dist/pluginWebpack5'
import VueSSRClientPlugin from '../plugins/vue/client'
import VueSSRServerPlugin from '../plugins/vue/server'
import { WebpackConfigContext } from '../utils/config'
export function vue (ctx: WebpackConfigContext) {
const { options, config } = ctx
config.plugins.push(new VueLoaderPlugin())
config.module.rules.push({
test: /\.vue$/i,
loader: 'vue-loader',
options: options.build.loaders.vue
})
if (ctx.isClient) {
config.plugins.push(new VueSSRClientPlugin({
filename: `../server/${ctx.name}.manifest.json`
}))
} else {
config.plugins.push(new VueSSRServerPlugin({
filename: `${ctx.name}.manifest.json`
}))
}
}

View File

@ -0,0 +1,88 @@
import consola from 'consola'
import cloneDeep from 'lodash/cloneDeep'
import { Configuration } from 'webpack'
import { Nuxt } from 'src/core'
export interface WebpackConfigContext extends ReturnType<typeof createWebpackConfigContext>{ }
type WebpackConfigPreset = (ctx: WebpackConfigContext, options?: object) => void
type WebpackConfigPresetItem = WebpackConfigPreset | [WebpackConfigPreset, any]
export function createWebpackConfigContext ({ nuxt }) {
return {
nuxt: nuxt as Nuxt,
options: nuxt.options as Nuxt['options'],
config: {} as Configuration,
name: 'base',
isDev: nuxt.options.dev,
isServer: false,
isClient: false,
isModern: undefined, // TODO
isLegacy: false,
alias: {} as Configuration['resolve']['alias'],
transpile: [] as any[]
}
}
export function applyPresets (ctx: WebpackConfigContext, presets: WebpackConfigPresetItem | WebpackConfigPresetItem[]) {
if (!Array.isArray(presets)) {
presets = [presets]
}
for (const preset of presets) {
if (Array.isArray(preset)) {
preset[0](ctx, preset[1])
} else {
preset(ctx)
}
}
}
export function fileName (ctx: WebpackConfigContext, key: string) {
const { options } = ctx
let fileName = options.build.filenames[key]
if (typeof fileName === 'function') {
fileName = fileName(ctx)
}
if (typeof fileName === 'string' && options.dev) {
const hash = /\[(chunkhash|contenthash|hash)(?::(\d+))?]/.exec(fileName)
if (hash) {
consola.warn(`Notice: Please do not use ${hash[1]} in dev mode to prevent memory leak`)
}
}
return fileName
}
export function getWebpackConfig (ctx: WebpackConfigContext): Configuration {
const { options, config } = ctx
// TODO
const builder = {}
const loaders = []
const { extend } = options.build
if (typeof extend === 'function') {
const extendedConfig = extend.call(
builder,
config,
{ loaders, ...ctx }
) || config
const pragma = /@|#/
const { devtool } = extendedConfig
if (typeof devtool === 'string' && pragma.test(devtool)) {
extendedConfig.devtool = devtool.replace(pragma, '')
consola.warn(`devtool has been normalized to ${extendedConfig.devtool} as webpack documented value`)
}
return extendedConfig
}
// Clone deep avoid leaking config between Client and Server
return cloneDeep(config)
}

View File

@ -1,3 +1,2 @@
export { default as PerfLoader } from './perf-loader'
export { default as StyleLoader } from './style-loader' export { default as StyleLoader } from './style-loader'
export { reservedVueTags } from './reserved-tags' export { reservedVueTags } from './reserved-tags'

View File

@ -1,53 +0,0 @@
import { warmup } from 'thread-loader'
// https://github.com/webpack-contrib/thread-loader
export default class PerfLoader {
constructor (name, options) {
this.name = name
this.options = options
this.workerPools = PerfLoader.defaultPools({ dev: options.dev })
return new Proxy(this, {
get (target, name) {
return target[name] ? target[name] : target.use.bind(target, name)
}
})
}
static defaultPools ({ dev }) {
const poolTimeout = dev ? Infinity : 2000
return {
js: { name: 'js', poolTimeout },
css: { name: 'css', poolTimeout }
}
}
static warmupAll ({ dev }) {
const pools = PerfLoader.defaultPools({ dev })
PerfLoader.warmup(pools.js, [
require.resolve('babel-loader'),
require.resolve('@babel/preset-env')
])
PerfLoader.warmup(pools.css, ['css-loader'])
}
static warmup (...args) {
warmup(...args)
}
use (poolName) {
const loaders = []
if (this.options.build.buildOptions) {
const pool = this.workerPools[poolName]
if (pool) {
loaders.push({
loader: 'thread-loader',
options: pool
})
}
}
return loaders
}
}

View File

@ -11,10 +11,9 @@ import PostcssConfig from './postcss'
export default class StyleLoader { export default class StyleLoader {
options: NormalizedConfiguration options: NormalizedConfiguration
constructor (nuxt: Nuxt, { isServer, perfLoader }) { constructor (nuxt: Nuxt, { isServer }) {
this.options = nuxt.options this.options = nuxt.options
this.isServer = isServer this.isServer = isServer
this.perfLoader = perfLoader
if (this.options.build.postcss) { if (this.options.build.postcss) {
this.postcssConfig = new PostcssConfig(nuxt) this.postcssConfig = new PostcssConfig(nuxt)
@ -121,17 +120,17 @@ export default class StyleLoader {
// This matches <style module> // This matches <style module>
{ {
resourceQuery: /module/, resourceQuery: /module/,
use: this.perfLoader.css().concat( use: [
this.cssModules(cssModules), this.cssModules(cssModules),
customLoaders customLoaders
) ]
}, },
// This matches plain <style> or <style scoped> // This matches plain <style> or <style scoped>
{ {
use: this.perfLoader.css().concat( use: [
this.css(css), this.css(css),
customLoaders customLoaders
) ]
} }
] ]
} }