From d6ed1dfc2c3ed7bdfa7481d3e4974b12701b3fc6 Mon Sep 17 00:00:00 2001 From: pooya parsa Date: Wed, 2 Sep 2020 14:27:27 +0200 Subject: [PATCH] feat: rewrite webpack config (#30) --- packages/nuxt3/src/config/config/build.ts | 6 +- packages/nuxt3/src/config/options.ts | 6 +- packages/nuxt3/src/webpack/builder.ts | 34 +- packages/nuxt3/src/webpack/config/base.ts | 521 ------------------ packages/nuxt3/src/webpack/config/client.ts | 233 -------- packages/nuxt3/src/webpack/config/index.ts | 3 - packages/nuxt3/src/webpack/config/modern.ts | 15 - packages/nuxt3/src/webpack/config/server.ts | 161 ------ packages/nuxt3/src/webpack/configs/client.ts | 147 +++++ packages/nuxt3/src/webpack/configs/index.ts | 2 + packages/nuxt3/src/webpack/configs/server.ts | 69 +++ packages/nuxt3/src/webpack/presets/assets.ts | 38 ++ packages/nuxt3/src/webpack/presets/babel.ts | 78 +++ packages/nuxt3/src/webpack/presets/base.ts | 240 ++++++++ packages/nuxt3/src/webpack/presets/esbuild.ts | 40 ++ packages/nuxt3/src/webpack/presets/node.ts | 23 + packages/nuxt3/src/webpack/presets/nuxt.ts | 20 + packages/nuxt3/src/webpack/presets/pug.ts | 25 + packages/nuxt3/src/webpack/presets/style.ts | 62 +++ packages/nuxt3/src/webpack/presets/vue.ts | 26 + packages/nuxt3/src/webpack/utils/config.ts | 88 +++ packages/nuxt3/src/webpack/utils/index.ts | 1 - .../nuxt3/src/webpack/utils/perf-loader.ts | 53 -- .../nuxt3/src/webpack/utils/style-loader.ts | 11 +- 24 files changed, 885 insertions(+), 1017 deletions(-) delete mode 100644 packages/nuxt3/src/webpack/config/base.ts delete mode 100644 packages/nuxt3/src/webpack/config/client.ts delete mode 100644 packages/nuxt3/src/webpack/config/index.ts delete mode 100644 packages/nuxt3/src/webpack/config/modern.ts delete mode 100644 packages/nuxt3/src/webpack/config/server.ts create mode 100644 packages/nuxt3/src/webpack/configs/client.ts create mode 100644 packages/nuxt3/src/webpack/configs/index.ts create mode 100644 packages/nuxt3/src/webpack/configs/server.ts create mode 100644 packages/nuxt3/src/webpack/presets/assets.ts create mode 100644 packages/nuxt3/src/webpack/presets/babel.ts create mode 100644 packages/nuxt3/src/webpack/presets/base.ts create mode 100644 packages/nuxt3/src/webpack/presets/esbuild.ts create mode 100644 packages/nuxt3/src/webpack/presets/node.ts create mode 100644 packages/nuxt3/src/webpack/presets/nuxt.ts create mode 100644 packages/nuxt3/src/webpack/presets/pug.ts create mode 100644 packages/nuxt3/src/webpack/presets/style.ts create mode 100644 packages/nuxt3/src/webpack/presets/vue.ts create mode 100644 packages/nuxt3/src/webpack/utils/config.ts delete mode 100644 packages/nuxt3/src/webpack/utils/perf-loader.ts diff --git a/packages/nuxt3/src/config/config/build.ts b/packages/nuxt3/src/config/config/build.ts index fe2861cf57..67269af042 100644 --- a/packages/nuxt3/src/config/config/build.ts +++ b/packages/nuxt3/src/config/config/build.ts @@ -289,7 +289,7 @@ export default () => ({ runtimeChunk: 'single', minimize: undefined as boolean | undefined, minimizer: undefined, - cssMinimizer: undefined, + // cssMinimizer: undefined, splitChunks: { chunks: 'all', name: undefined, @@ -299,7 +299,9 @@ export default () => ({ } } } - } as WebpackConfiguration['optimization'] & { cssMinimizer: undefined | boolean | Record }, + } as WebpackConfiguration['optimization'] & { + // cssMinimizer: undefined | boolean | Record + }, /** * Enable [thread-loader](https://github.com/webpack-contrib/thread-loader#thread-loader) in webpack building * diff --git a/packages/nuxt3/src/config/options.ts b/packages/nuxt3/src/config/options.ts index 03650d94d3..19575e25d1 100644 --- a/packages/nuxt3/src/config/options.ts +++ b/packages/nuxt3/src/config/options.ts @@ -346,9 +346,9 @@ function normalizeConfig (_options: CliConfiguration) { } // Enable cssMinimizer only when extractCSS is enabled - if (options.build.optimization.cssMinimizer === undefined) { - options.build.optimization.cssMinimizer = options.build.extractCSS ? {} : false - } + // if (options.build.optimization.cssMinimizer === undefined) { + // options.build.optimization.cssMinimizer = options.build.extractCSS ? {} : false + // } const { loaders } = options.build // const vueLoader = loaders.vue diff --git a/packages/nuxt3/src/webpack/builder.ts b/packages/nuxt3/src/webpack/builder.ts index 0d636587f3..08b1897c7e 100644 --- a/packages/nuxt3/src/webpack/builder.ts +++ b/packages/nuxt3/src/webpack/builder.ts @@ -5,13 +5,11 @@ import Glob from 'glob' import webpackDevMiddleware from 'webpack-dev-middleware' import webpackHotMiddleware from 'webpack-hot-middleware' import consola from 'consola' - import { Nuxt } from 'src/core' import { TARGETS, parallel, sequence, wrapArray, isModernRequest } from 'src/utils' import { createMFS } from './utils/mfs' - -import * as WebpackConfigs from './config' -import PerfLoader from './utils/perf-loader' +import { client, server } from './configs' +import { createWebpackConfigContext, applyPresets, getWebpackConfig } from './utils/config' const glob = pify(Glob) @@ -40,27 +38,32 @@ export class WebpackBundler { } getWebpackConfig (name) { - const Config = WebpackConfigs[name.toLowerCase()] // eslint-disable-line import/namespace - if (!Config) { + const ctx = createWebpackConfigContext({ nuxt: this.nuxt }) + + if (name === 'client') { + applyPresets(ctx, client) + } else if (name === 'server') { + applyPresets(ctx, server) + } else { throw new Error(`Unsupported webpack config ${name}`) } - const config = new Config(this) - return config.config() + + return getWebpackConfig(ctx) } async build () { const { options } = this.nuxt const webpackConfigs = [ - this.getWebpackConfig('Client') + this.getWebpackConfig('client') ] if (options.modern) { - webpackConfigs.push(this.getWebpackConfig('Modern')) + webpackConfigs.push(this.getWebpackConfig('modern')) } if (options.build.ssr) { - webpackConfigs.push(this.getWebpackConfig('Server')) + webpackConfigs.push(this.getWebpackConfig('server')) } await this.nuxt.callHook('webpack:config', webpackConfigs) @@ -95,13 +98,6 @@ export class WebpackBundler { 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 const runner = options.dev ? parallel : sequence @@ -128,7 +124,7 @@ export class WebpackBundler { // --- Dev Build --- if (options.dev) { - // Client buiild + // Client build if (['client', 'modern'].includes(name)) { return new Promise((resolve, reject) => { compiler.hooks.done.tap('nuxt-dev', () => { resolve() }) diff --git a/packages/nuxt3/src/webpack/config/base.ts b/packages/nuxt3/src/webpack/config/base.ts deleted file mode 100644 index 286a467da3..0000000000 --- a/packages/nuxt3/src/webpack/config/base.ts +++ /dev/null @@ -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 - } -} diff --git a/packages/nuxt3/src/webpack/config/client.ts b/packages/nuxt3/src/webpack/config/client.ts deleted file mode 100644 index ad25f9f9cc..0000000000 --- a/packages/nuxt3/src/webpack/config/client.ts +++ /dev/null @@ -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 - } -} diff --git a/packages/nuxt3/src/webpack/config/index.ts b/packages/nuxt3/src/webpack/config/index.ts deleted file mode 100644 index e22da970a9..0000000000 --- a/packages/nuxt3/src/webpack/config/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { default as client } from './client' -export { default as modern } from './modern' -export { default as server } from './server' diff --git a/packages/nuxt3/src/webpack/config/modern.ts b/packages/nuxt3/src/webpack/config/modern.ts deleted file mode 100644 index 4c97013439..0000000000 --- a/packages/nuxt3/src/webpack/config/modern.ts +++ /dev/null @@ -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 - }) - } -} diff --git a/packages/nuxt3/src/webpack/config/server.ts b/packages/nuxt3/src/webpack/config/server.ts deleted file mode 100644 index f8c6de7faa..0000000000 --- a/packages/nuxt3/src/webpack/config/server.ts +++ /dev/null @@ -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 - } -} diff --git a/packages/nuxt3/src/webpack/configs/client.ts b/packages/nuxt3/src/webpack/configs/client.ts new file mode 100644 index 0000000000..a257948102 --- /dev/null +++ b/packages/nuxt3/src/webpack/configs/client.ts @@ -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'] || [] + } +} diff --git a/packages/nuxt3/src/webpack/configs/index.ts b/packages/nuxt3/src/webpack/configs/index.ts new file mode 100644 index 0000000000..ec76385105 --- /dev/null +++ b/packages/nuxt3/src/webpack/configs/index.ts @@ -0,0 +1,2 @@ +export { client } from './client' +export { server } from './server' diff --git a/packages/nuxt3/src/webpack/configs/server.ts b/packages/nuxt3/src/webpack/configs/server.ts new file mode 100644 index 0000000000..c09cdbec11 --- /dev/null +++ b/packages/nuxt3/src/webpack/configs/server.ts @@ -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'] + })) + } +} diff --git a/packages/nuxt3/src/webpack/presets/assets.ts b/packages/nuxt3/src/webpack/presets/assets.ts new file mode 100644 index 0000000000..0768263118 --- /dev/null +++ b/packages/nuxt3/src/webpack/presets/assets.ts @@ -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') + } + }] + } + ) +} diff --git a/packages/nuxt3/src/webpack/presets/babel.ts b/packages/nuxt3/src/webpack/presets/babel.ts new file mode 100644 index 0000000000..ea9d334e30 --- /dev/null +++ b/packages/nuxt3/src/webpack/presets/babel.ts @@ -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 +} diff --git a/packages/nuxt3/src/webpack/presets/base.ts b/packages/nuxt3/src/webpack/presets/base.ts new file mode 100644 index 0000000000..3ad47cdc6a --- /dev/null +++ b/packages/nuxt3/src/webpack/presets/base.ts @@ -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 +} diff --git a/packages/nuxt3/src/webpack/presets/esbuild.ts b/packages/nuxt3/src/webpack/presets/esbuild.ts new file mode 100644 index 0000000000..7e9ef393a6 --- /dev/null +++ b/packages/nuxt3/src/webpack/presets/esbuild.ts @@ -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' + } + } + ) +} diff --git a/packages/nuxt3/src/webpack/presets/node.ts b/packages/nuxt3/src/webpack/presets/node.ts new file mode 100644 index 0000000000..2a188edf3b --- /dev/null +++ b/packages/nuxt3/src/webpack/presets/node.ts @@ -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 + } +} diff --git a/packages/nuxt3/src/webpack/presets/nuxt.ts b/packages/nuxt3/src/webpack/presets/nuxt.ts new file mode 100644 index 0000000000..216a5c6531 --- /dev/null +++ b/packages/nuxt3/src/webpack/presets/nuxt.ts @@ -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 + ]) +} diff --git a/packages/nuxt3/src/webpack/presets/pug.ts b/packages/nuxt3/src/webpack/presets/pug.ts new file mode 100644 index 0000000000..3ff0939f8a --- /dev/null +++ b/packages/nuxt3/src/webpack/presets/pug.ts @@ -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 + } + ] + } + ] + }) +} diff --git a/packages/nuxt3/src/webpack/presets/style.ts b/packages/nuxt3/src/webpack/presets/style.ts new file mode 100644 index 0000000000..47e1d940d4 --- /dev/null +++ b/packages/nuxt3/src/webpack/presets/style.ts @@ -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 + // }) + // } + ] +} diff --git a/packages/nuxt3/src/webpack/presets/vue.ts b/packages/nuxt3/src/webpack/presets/vue.ts new file mode 100644 index 0000000000..891fe89379 --- /dev/null +++ b/packages/nuxt3/src/webpack/presets/vue.ts @@ -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` + })) + } +} diff --git a/packages/nuxt3/src/webpack/utils/config.ts b/packages/nuxt3/src/webpack/utils/config.ts new file mode 100644 index 0000000000..06270da5c4 --- /dev/null +++ b/packages/nuxt3/src/webpack/utils/config.ts @@ -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{ } + +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) +} diff --git a/packages/nuxt3/src/webpack/utils/index.ts b/packages/nuxt3/src/webpack/utils/index.ts index 1a98c709f2..2daa4f2283 100644 --- a/packages/nuxt3/src/webpack/utils/index.ts +++ b/packages/nuxt3/src/webpack/utils/index.ts @@ -1,3 +1,2 @@ -export { default as PerfLoader } from './perf-loader' export { default as StyleLoader } from './style-loader' export { reservedVueTags } from './reserved-tags' diff --git a/packages/nuxt3/src/webpack/utils/perf-loader.ts b/packages/nuxt3/src/webpack/utils/perf-loader.ts deleted file mode 100644 index 171482dd77..0000000000 --- a/packages/nuxt3/src/webpack/utils/perf-loader.ts +++ /dev/null @@ -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 - } -} diff --git a/packages/nuxt3/src/webpack/utils/style-loader.ts b/packages/nuxt3/src/webpack/utils/style-loader.ts index d1d3dfff94..2ba6e8051a 100644 --- a/packages/nuxt3/src/webpack/utils/style-loader.ts +++ b/packages/nuxt3/src/webpack/utils/style-loader.ts @@ -11,10 +11,9 @@ import PostcssConfig from './postcss' export default class StyleLoader { options: NormalizedConfiguration - constructor (nuxt: Nuxt, { isServer, perfLoader }) { + constructor (nuxt: Nuxt, { isServer }) { this.options = nuxt.options this.isServer = isServer - this.perfLoader = perfLoader if (this.options.build.postcss) { this.postcssConfig = new PostcssConfig(nuxt) @@ -121,17 +120,17 @@ export default class StyleLoader { // This matches