From 73ba30fb69b91a25ee595daa447f3d6988b4dc8c Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 25 Feb 2022 19:11:01 +0000 Subject: [PATCH] refactor(schema,webpack)!: split out webpack and postcss build options (#2812) * refactor: split out webpack and postcss build options * feat(schema): set nuxt3 builder in config * fix(schema): default postcss config file to false * chore: update lockfile * style: remove unused imports * refactor(webpack): remove (previously disabled) babel config * refactor: move shared vite options into schema * fix(schema): omit __NUXT_BASE__ from defaults * fix: move appDir-dependent options back to vite * refactor: split out virtual modules * refactor: extract compile/createDevMiddleware * refactor: further improvements * chore: remove `@nuxt/webpack-builder` dependency * chore: update lockfile * refactor: move `builder` option to top level * fix: bind close to watcher instance * chore: update lockfile * fix: create portal between postcss & build.postcss.postcssOptions * fix: remove duplicate * fix: revert * fix: use `postcss` directly * fix: import builder from rootDir * chore: dedupe webpack install * test: update fixture to use `builder` * fix: bind class in pify Co-authored-by: Pooya Parsa --- packages/kit/src/module/container.ts | 4 + packages/nuxt3/package.json | 1 - packages/nuxt3/src/core/builder.ts | 6 +- packages/schema/build.config.ts | 11 +- packages/schema/package.json | 1 + packages/schema/src/config/build.ts | 1163 +++++++++-------- packages/schema/src/config/index.ts | 22 +- packages/schema/src/config/postcss.ts | 53 + packages/schema/src/config/typescript.ts | 28 +- packages/schema/src/config/vite.ts | 83 ++ packages/schema/src/config/webpack.ts | 294 +++++ packages/schema/src/types/module.ts | 2 +- packages/vite/package.json | 1 - packages/vite/src/css.ts | 24 +- packages/vite/src/vite.ts | 46 +- packages/webpack/build.config.ts | 4 - packages/webpack/package.json | 5 - packages/webpack/src/configs/client.ts | 7 +- packages/webpack/src/configs/server.ts | 6 +- .../webpack/src/plugins/warning-ignore.ts | 13 +- packages/webpack/src/presets/assets.ts | 8 +- packages/webpack/src/presets/babel.ts | 84 -- packages/webpack/src/presets/base.ts | 87 +- packages/webpack/src/presets/esbuild.ts | 8 +- packages/webpack/src/presets/node.ts | 2 +- packages/webpack/src/presets/nuxt.ts | 1 - packages/webpack/src/presets/pug.ts | 4 +- packages/webpack/src/presets/style.ts | 57 +- packages/webpack/src/presets/vue.ts | 2 +- packages/webpack/src/utils/babel-preset.cjs | 202 --- packages/webpack/src/utils/config.ts | 12 +- packages/webpack/src/utils/mfs.ts | 4 +- packages/webpack/src/utils/postcss.ts | 180 +-- packages/webpack/src/utils/reserved-tags.ts | 23 - packages/webpack/src/virtual-modules.ts | 26 + packages/webpack/src/webpack.ts | 402 ++---- test/fixtures/basic/nuxt.config.ts | 2 +- yarn.lock | 61 +- 38 files changed, 1363 insertions(+), 1576 deletions(-) create mode 100644 packages/schema/src/config/postcss.ts create mode 100644 packages/schema/src/config/vite.ts create mode 100644 packages/schema/src/config/webpack.ts delete mode 100644 packages/webpack/src/presets/babel.ts delete mode 100644 packages/webpack/src/utils/babel-preset.cjs delete mode 100644 packages/webpack/src/utils/reserved-tags.ts create mode 100644 packages/webpack/src/virtual-modules.ts diff --git a/packages/kit/src/module/container.ts b/packages/kit/src/module/container.ts index b719d7fb2c..8a75b40c6f 100644 --- a/packages/kit/src/module/container.ts +++ b/packages/kit/src/module/container.ts @@ -83,6 +83,10 @@ export function useModuleContainer (nuxt: Nuxt = useNuxt()): ModuleContainer { extendBuild (fn) { // @ts-ignore nuxt.options.build.extend = chainFn(nuxt.options.build.extend, fn) + + if (!isNuxt2(nuxt)) { + console.warn('[kit] [compat] Using `extendBuild` in Nuxt 3 has no effect. Instead call extendWebpackConfig and extendViteConfig.') + } }, extendRoutes (fn) { diff --git a/packages/nuxt3/package.json b/packages/nuxt3/package.json index a39fbe8c6f..ce9def1d28 100644 --- a/packages/nuxt3/package.json +++ b/packages/nuxt3/package.json @@ -35,7 +35,6 @@ "@nuxt/nitro": "3.0.0", "@nuxt/schema": "3.0.0", "@nuxt/vite-builder": "3.0.0", - "@nuxt/webpack-builder": "3.0.0", "@vue/reactivity": "^3.2.31", "@vue/shared": "^3.2.31", "@vueuse/head": "^0.7.5", diff --git a/packages/nuxt3/src/core/builder.ts b/packages/nuxt3/src/core/builder.ts index 679e343e5f..75d5aa9674 100644 --- a/packages/nuxt3/src/core/builder.ts +++ b/packages/nuxt3/src/core/builder.ts @@ -1,5 +1,6 @@ import chokidar from 'chokidar' import type { Nuxt } from '@nuxt/schema' +import { tryImportModule } from '@nuxt/kit' import { createApp, generateApp } from './app' export async function build (nuxt: Nuxt) { @@ -48,8 +49,9 @@ function watch (nuxt: Nuxt) { } async function bundle (nuxt: Nuxt) { - const useVite = nuxt.options.vite !== false - const { bundle } = await (useVite ? import('@nuxt/vite-builder') : import('@nuxt/webpack-builder')) + const { bundle } = typeof nuxt.options.builder === 'string' + ? await tryImportModule(nuxt.options.builder, { paths: nuxt.options.rootDir }) + : nuxt.options.builder try { return bundle(nuxt) } catch (error) { diff --git a/packages/schema/build.config.ts b/packages/schema/build.config.ts index 5e329835b9..9a6b53d591 100644 --- a/packages/schema/build.config.ts +++ b/packages/schema/build.config.ts @@ -9,7 +9,10 @@ export default defineBuildConfig({ name: 'config', builder: 'untyped', defaults: { - rootDir: '//' + rootDir: '//', + vite: { + base: '/' + } } }, 'src/index' @@ -24,6 +27,12 @@ export default defineBuildConfig({ 'webpack-bundle-analyzer', 'rollup-plugin-visualizer', 'vite', + 'mini-css-extract-plugin', + 'terser-webpack-plugin', + 'css-minimizer-webpack-plugin', + 'webpack-dev-middleware', + 'webpack-hot-middleware', + 'postcss', 'consola', // Implicit '@vue/compiler-core', diff --git a/packages/schema/package.json b/packages/schema/package.json index 625b6ce6b2..99abcbd065 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -24,6 +24,7 @@ "defu": "^5.0.1", "jiti": "^1.13.0", "pathe": "^0.2.0", + "postcss-import-resolver": "^2.0.0", "scule": "^0.2.1", "std-env": "^3.0.1", "ufo": "^0.7.9" diff --git a/packages/schema/src/config/build.ts b/packages/schema/src/config/build.ts index 54df8d8f81..ba29cb03c7 100644 --- a/packages/schema/src/config/build.ts +++ b/packages/schema/src/config/build.ts @@ -1,622 +1,631 @@ +import defu from 'defu' import { join } from 'pathe' import { isCI, isTest } from 'std-env' import { normalizeURL, withTrailingSlash } from 'ufo' export default { /** + * The builder to use for bundling the Vue part of your application. + * + * @type {'vite' | 'webpack' | { bundle: (nuxt: typeof import('../src/types/nuxt').Nuxt) => Promise }} + */ + builder: { + $resolve: (val, get) => { + if (typeof val === 'object') { + return val + } + const map = { + vite: '@nuxt/vite-builder', + webpack: '@nuxt/webpack-builder', + } + return map[val] || (get('vite') === false ? map.webpack : map.vite) + }, + }, + + build: { + /** * Suppresses most of the build output log. * * It is enabled by default when a CI or test environment is detected. * * @see [std-env](https://github.com/unjs/std-env) - * @version 2 - */ - quiet: Boolean(isCI || isTest), - - /** - * Nuxt uses `webpack-bundle-analyzer` to visualize your bundles and how to optimize them. - * - * Set to `true` to enable bundle analysis, or pass an object with options: [for webpack](https://github.com/webpack-contrib/webpack-bundle-analyzer#options-for-plugin) or [for vite](https://github.com/btd/rollup-plugin-visualizer#options). - * - * @example - * ```js - * analyze: { - * analyzerMode: 'static' - * } - * ``` - * @type {boolean | typeof import('webpack-bundle-analyzer').BundleAnalyzerPlugin.Options | typeof import('rollup-plugin-visualizer').PluginVisualizerOptions} - */ - analyze: { - $resolve: (val, get) => { - if(val !== true) { - return val ?? false - } - const rootDir = get('rootDir') - return { - template: 'treemap', - projectRoot: rootDir, - filename: join(rootDir, '.nuxt/stats', '{name}.html') - } - } - }, - - /** - * Enable the profiler in webpackbar. - * - * It is normally enabled by CLI argument `--profile`. - * - * @see [webpackbar](https://github.com/unjs/webpackbar#profile) * @version 2 + * @version 3 */ - profile: process.argv.includes('--profile'), + quiet: Boolean(isCI || isTest), - /** - * Enables Common CSS Extraction using - * [Vue Server Renderer guidelines](https://ssr.vuejs.org/guide/css.html). - * - * Using [extract-css-chunks-webpack-plugin](https://github.com/faceyspacey/extract-css-chunks-webpack-plugin/) under the hood, your CSS will be extracted - * into separate files, usually one per component. This allows caching your CSS and - * JavaScript separately and is worth trying if you have a lot of global or shared CSS. - * - * @example - * ```js - * export default { - * build: { - * extractCSS: true, - * // or - * extractCSS: { - * ignoreOrder: true - * } - * } - * } - * ``` - * - * If you want to extract all your CSS to a single file, there is a workaround for this. - * However, note that it is not recommended to extract everything into a single file. - * Extracting into multiple CSS files is better for caching and preload isolation. It - * can also improve page performance by downloading and resolving only those resources - * that are needed. - * - * @example - * ```js - * export default { - * build: { - * extractCSS: true, - * optimization: { - * splitChunks: { - * cacheGroups: { - * styles: { - * name: 'styles', - * test: /\.(css|vue)$/, - * chunks: 'all', - * enforce: true - * } - * } - * } - * } - * } - * } - * ``` - * @version 2 - * @version 3 webpack only - */ - extractCSS: false, - - /** - * Enables CSS source map support (defaults to true in development) - * @version 2 - * @version 3 webpack only - */ - cssSourceMap: { - $resolve: (val, get) => val ?? get('dev') - }, - - /** - * Creates special webpack bundle for SSR renderer. It is normally not necessary to change this value. - * @version 2 - */ - ssr: undefined, - - /** - * Enable [thread-loader](https://github.com/webpack-contrib/thread-loader#thread-loader) when building app with webpack. - * - * @warning This is an unstable feature. - * @version 2 - */ - parallel: { - $resolve: (val, get) => get('build.extractCSS') ? false : Boolean(val) - }, - - /** - * Enable caching for [`terser-webpack-plugin`](https://github.com/webpack-contrib/terser-webpack-plugin#options) - * and [`cache-loader`](https://github.com/webpack-contrib/cache-loader#cache-loader) - * - * @warning This is an unstable feature. - * @version 2 - */ - cache: false, - - /** - * Inline server bundle dependencies - * - * This mode bundles `node_modules` that are normally preserved as externals in the server build. - * - * @warning Runtime dependencies (modules, `nuxt.config`, server middleware and the static directory) are not bundled. - * This feature only disables use of [webpack-externals](https://webpack.js.org/configuration/externals/) for server-bundle. - * - * @note You can enable standalone bundling by passing `--standalone` via the command line. - * - * @see [context](https://github.com/nuxt/nuxt.js/pull/4661) - * @version 2 - */ - standalone: false, - - /** - * If you are uploading your dist files to a CDN, you can set the publicPath to your CDN. - * - * @note This is only applied in production. - * - * The value of this property at runtime will override the configuration of an app that - * has already been built. - * - * @example - * ```js - * build: { - * publicPath: process.env.PUBLIC_PATH || 'https://cdn.nuxtjs.org' - * } - * ``` - * @version 2 - */ - publicPath: { - $resolve: (val, get) => val ? withTrailingSlash(normalizeURL(val)) : get('app.buildAssetsDir') - }, - - /** - * The polyfill library to load to provide URL and URLSearchParams. - * - * Defaults to `'url'` ([see package](https://www.npmjs.com/package/url)). - * @version 2 - * @version 3 webpack only - */ - serverURLPolyfill: 'url', - - /** - * Customize bundle filenames. - * - * To understand a bit more about the use of manifests, take a look at [this webpack documentation](https://webpack.js.org/guides/code-splitting/). - * - * @note Be careful when using non-hashed based filenames in production - * as most browsers will cache the asset and not detect the changes on first load. - * - * This example changes fancy chunk names to numerical ids: - * - * @example - * ```js - * filenames: { - * chunk: ({ isDev }) => (isDev ? '[name].js' : '[id].[contenthash].js') - * } - * ``` - * @version 2 - * @version 3 webpack only - */ - filenames: { - app: ({ isDev, isModern }) => isDev ? `[name]${isModern ? '.modern' : ''}.js` : `[contenthash:7]${isModern ? '.modern' : ''}.js`, - chunk: ({ isDev, isModern }) => isDev ? `[name]${isModern ? '.modern' : ''}.js` : `[contenthash:7]${isModern ? '.modern' : ''}.js`, - css: ({ isDev }) => isDev ? '[name].css' : 'css/[contenthash:7].css', - img: ({ isDev }) => isDev ? '[path][name].[ext]' : 'img/[name].[contenthash:7].[ext]', - font: ({ isDev }) => isDev ? '[path][name].[ext]' : 'fonts/[name].[contenthash:7].[ext]', - video: ({ isDev }) => isDev ? '[path][name].[ext]' : 'videos/[name].[contenthash:7].[ext]' - }, - - /** - * Customize the options of Nuxt's integrated webpack loaders. - * @version 2 - * @version 3 webpack only - */ - loaders: { - $resolve: (val, get) => { - const styleLoaders = [ - 'css', 'cssModules', 'less', - 'sass', 'scss', 'stylus', 'vueStyle' - ] - for (const name of styleLoaders) { - const loader = val[name] - if (loader && loader.sourceMap === undefined) { - loader.sourceMap = Boolean(get('build.cssSourceMap')) + /** + * Nuxt uses `webpack-bundle-analyzer` to visualize your bundles and how to optimize them. + * + * Set to `true` to enable bundle analysis, or pass an object with options: [for webpack](https://github.com/webpack-contrib/webpack-bundle-analyzer#options-for-plugin) or [for vite](https://github.com/btd/rollup-plugin-visualizer#options). + * + * @example + * ```js + * analyze: { + * analyzerMode: 'static' + * } + * ``` + * @type {boolean | typeof import('webpack-bundle-analyzer').BundleAnalyzerPlugin.Options | typeof import('rollup-plugin-visualizer').PluginVisualizerOptions} + */ + analyze: { + $resolve: (val, get) => { + if(val !== true) { + return val ?? false + } + const rootDir = get('rootDir') + return { + template: 'treemap', + projectRoot: rootDir, + filename: join(rootDir, '.nuxt/stats', '{name}.html') } } - return val }, - file: { esModule: false }, - fontUrl: { esModule: false, limit: 1000 }, - imgUrl: { esModule: false, limit: 1000 }, - pugPlain: {}, - vue: { - productionMode: { $resolve: (val, get) => val ?? !get('dev') }, - transformAssetUrls: { - video: 'src', - source: 'src', - object: 'src', - embed: 'src' - }, - compilerOptions: { $resolve: (val, get) => val ?? get('vue.compilerOptions') }, - }, - css: { - importLoaders: 0, - esModule: false - }, - cssModules: { - importLoaders: 0, - esModule: false, - modules: { - localIdentName: '[local]_[hash:base64:5]' - } - }, - less: {}, - sass: { - sassOptions: { - indentedSyntax: true - } - }, - scss: {}, - stylus: {}, - vueStyle: {} - }, - /** - * @deprecated Use [style-resources-module](https://github.com/nuxt-community/style-resources-module/) - * @version 2 - */ - styleResources: {}, - - /** - * Add webpack plugins. - * - * @example - * ```js - * import webpack from 'webpack' - * import { version } from './package.json' - * // ... - * plugins: [ - * new webpack.DefinePlugin({ - * 'process.VERSION': version - * }) - * ] - * ``` - * @version 2 - * @version 3 webpack only - */ - plugins: [], - - /** - * Terser plugin options. - * - * Set to false to disable this plugin, or pass an object of options. - * - * @see [terser-webpack-plugin documentation](https://github.com/webpack-contrib/terser-webpack-plugin) - * - * @note Enabling sourceMap will leave `//# sourceMappingURL` linking comment at - * the end of each output file if webpack `config.devtool` is set to `source-map`. - * @version 2 - * @version 3 webpack only - */ - terser: {}, - - /** - * Enables the [HardSourceWebpackPlugin](https://github.com/mzgoddard/hard-source-webpack-plugin) for improved caching. - * - * @warning unstable - * @version 2 - */ - hardSource: false, - - /** - * Hard-replaces `typeof process`, `typeof window` and `typeof document` to tree-shake bundle. - * @version 2 - * @version 3 webpack only - */ - aggressiveCodeRemoval: false, - - /** - * OptimizeCSSAssets plugin options. - * - * Defaults to true when `extractCSS` is enabled. - * - * @see [optimize-css-assets-webpack-plugin documentation](https://github.com/NMFR/optimize-css-assets-webpack-plugin). - * @version 2 - * @version 3 webpack only - */ - optimizeCSS: { - $resolve: (val, get) => val ?? (get('build.extractCSS') ? {} : false) - }, - - /** - * Configure [webpack optimization](https://webpack.js.org/configuration/optimization/). - * @version 2 - * @version 3 webpack only - */ - optimization: { - runtimeChunk: 'single', - /** Set minimize to false to disable all minimizers. (It is disabled in development by default) */ - minimize: { $resolve: (val, get) => val ?? !get('dev') }, - /** You can set minimizer to a customized array of plugins. */ - minimizer: undefined, - splitChunks: { - chunks: 'all', - automaticNameDelimiter: '/', - cacheGroups: {} - } - }, - - /** - * Whether to split code for `layout`, `pages` and `commons` chunks. - * - * Commons libs include `vue`, `vue-loader`, `vue-router`, `vuex`, etc. - * @version 2 - */ - splitChunks: { - layouts: false, - pages: true, - commons: true - }, - - /** - * Nuxt will automatically detect the current version of `core-js` in your project (`'auto'`), - * or you can specify which version you want to use (`2` or `3`). - * @version 2 - * @version 3 webpack only - */ - corejs: 'auto', - - /** - * Customize your Babel configuration. - * - * See [babel-loader options](https://github.com/babel/babel-loader#options) and - * [babel options](https://babeljs.io/docs/en/options). - * - * @note `.babelrc` is ignored by default. - * @version 2 - * @version 3 webpack only - */ - babel: { - configFile: false, - babelrc: false, /** - * An array of Babel plugins to load, or a function that takes webpack context and returns - * an array of Babel plugins. + * Enable the profiler in webpackbar. * - * For more information see [Babel plugins options](https://babeljs.io/docs/en/options#plugins) - * and [babel-loader options](https://github.com/babel/babel-loader#options). + * It is normally enabled by CLI argument `--profile`. + * + * @see [webpackbar](https://github.com/unjs/webpackbar#profile) + * @version 2 + */ + profile: process.argv.includes('--profile'), + + /** + * Enables Common CSS Extraction using + * [Vue Server Renderer guidelines](https://ssr.vuejs.org/guide/css.html). + * + * Using [extract-css-chunks-webpack-plugin](https://github.com/faceyspacey/extract-css-chunks-webpack-plugin/) under the hood, your CSS will be extracted + * into separate files, usually one per component. This allows caching your CSS and + * JavaScript separately and is worth trying if you have a lot of global or shared CSS. + * + * @example + * ```js + * export default { + * build: { + * extractCSS: true, + * // or + * extractCSS: { + * ignoreOrder: true + * } + * } + * } + * ``` + * + * If you want to extract all your CSS to a single file, there is a workaround for this. + * However, note that it is not recommended to extract everything into a single file. + * Extracting into multiple CSS files is better for caching and preload isolation. It + * can also improve page performance by downloading and resolving only those resources + * that are needed. + * + * @example + * ```js + * export default { + * build: { + * extractCSS: true, + * optimization: { + * splitChunks: { + * cacheGroups: { + * styles: { + * name: 'styles', + * test: /\.(css|vue)$/, + * chunks: 'all', + * enforce: true + * } + * } + * } + * } + * } + * } + * ``` + * @version 2 + */ + extractCSS: false, + + /** + * Enables CSS source map support (defaults to true in development) + * @version 2 + */ + cssSourceMap: { + $resolve: (val, get) => val ?? get('dev') + }, + + /** + * Creates special webpack bundle for SSR renderer. It is normally not necessary to change this value. + * @version 2 + */ + ssr: undefined, + + /** + * Enable [thread-loader](https://github.com/webpack-contrib/thread-loader#thread-loader) when building app with webpack. + * + * @warning This is an unstable feature. + * @version 2 + */ + parallel: { + $resolve: (val, get) => get('build.extractCSS') ? false : Boolean(val) + }, + + /** + * Enable caching for [`terser-webpack-plugin`](https://github.com/webpack-contrib/terser-webpack-plugin#options) + * and [`cache-loader`](https://github.com/webpack-contrib/cache-loader#cache-loader) + * + * @warning This is an unstable feature. + * @version 2 + */ + cache: false, + + /** + * Inline server bundle dependencies + * + * This mode bundles `node_modules` that are normally preserved as externals in the server build. + * + * @warning Runtime dependencies (modules, `nuxt.config`, server middleware and the static directory) are not bundled. + * This feature only disables use of [webpack-externals](https://webpack.js.org/configuration/externals/) for server-bundle. + * + * @note You can enable standalone bundling by passing `--standalone` via the command line. + * + * @see [context](https://github.com/nuxt/nuxt.js/pull/4661) + * @version 2 + */ + standalone: false, + + /** + * If you are uploading your dist files to a CDN, you can set the publicPath to your CDN. + * + * @note This is only applied in production. + * + * The value of this property at runtime will override the configuration of an app that + * has already been built. + * + * @example + * ```js + * build: { + * publicPath: process.env.PUBLIC_PATH || 'https://cdn.nuxtjs.org' + * } + * ``` + * @version 2 + */ + publicPath: { + $resolve: (val, get) => val ? withTrailingSlash(normalizeURL(val)) : get('app.buildAssetsDir') + }, + + /** + * The polyfill library to load to provide URL and URLSearchParams. + * + * Defaults to `'url'` ([see package](https://www.npmjs.com/package/url)). + * @version 2 + */ + serverURLPolyfill: 'url', + + /** + * Customize bundle filenames. + * + * To understand a bit more about the use of manifests, take a look at [this webpack documentation](https://webpack.js.org/guides/code-splitting/). + * + * @note Be careful when using non-hashed based filenames in production + * as most browsers will cache the asset and not detect the changes on first load. + * + * This example changes fancy chunk names to numerical ids: + * + * @example + * ```js + * filenames: { + * chunk: ({ isDev }) => (isDev ? '[name].js' : '[id].[contenthash].js') + * } + * ``` + * @version 2 + */ + filenames: { + app: ({ isDev, isModern }) => isDev ? `[name]${isModern ? '.modern' : ''}.js` : `[contenthash:7]${isModern ? '.modern' : ''}.js`, + chunk: ({ isDev, isModern }) => isDev ? `[name]${isModern ? '.modern' : ''}.js` : `[contenthash:7]${isModern ? '.modern' : ''}.js`, + css: ({ isDev }) => isDev ? '[name].css' : 'css/[contenthash:7].css', + img: ({ isDev }) => isDev ? '[path][name].[ext]' : 'img/[name].[contenthash:7].[ext]', + font: ({ isDev }) => isDev ? '[path][name].[ext]' : 'fonts/[name].[contenthash:7].[ext]', + video: ({ isDev }) => isDev ? '[path][name].[ext]' : 'videos/[name].[contenthash:7].[ext]' + }, + + /** + * Customize the options of Nuxt's integrated webpack loaders. + * @version 2 + */ + loaders: { + $resolve: (val, get) => { + const styleLoaders = [ + 'css', 'cssModules', 'less', + 'sass', 'scss', 'stylus', 'vueStyle' + ] + for (const name of styleLoaders) { + const loader = val[name] + if (loader && loader.sourceMap === undefined) { + loader.sourceMap = Boolean(get('build.cssSourceMap')) + } + } + return val + }, + file: { esModule: false }, + fontUrl: { esModule: false, limit: 1000 }, + imgUrl: { esModule: false, limit: 1000 }, + pugPlain: {}, + vue: { + productionMode: { $resolve: (val, get) => val ?? !get('dev') }, + transformAssetUrls: { + video: 'src', + source: 'src', + object: 'src', + embed: 'src' + }, + compilerOptions: { $resolve: (val, get) => val ?? get('vue.compilerOptions') }, + }, + css: { + importLoaders: 0, + esModule: false + }, + cssModules: { + importLoaders: 0, + esModule: false, + modules: { + localIdentName: '[local]_[hash:base64:5]' + } + }, + less: {}, + sass: { + sassOptions: { + indentedSyntax: true + } + }, + scss: {}, + stylus: {}, + vueStyle: {} + }, + + /** + * @deprecated Use [style-resources-module](https://github.com/nuxt-community/style-resources-module/) + * @version 2 + */ + styleResources: {}, + + /** + * Add webpack plugins. + * + * @example + * ```js + * import webpack from 'webpack' + * import { version } from './package.json' + * // ... + * plugins: [ + * new webpack.DefinePlugin({ + * 'process.VERSION': version + * }) + * ] + * ``` + * @version 2 */ plugins: [], + /** - * The Babel presets to be applied. + * Terser plugin options. * - * @note The presets configured here will be applied to both the client and the server - * build. The target will be set by Nuxt accordingly (client/server). If you want to configure - * the preset differently for the client or the server build, please use presets as a function. + * Set to false to disable this plugin, or pass an object of options. * - * @warning It is highly recommended to use the default preset instead customizing. + * @see [terser-webpack-plugin documentation](https://github.com/webpack-contrib/terser-webpack-plugin) * - * @example - * ```js - * export default { - * build: { - * babel: { - * presets({ isServer }, [ preset, options ]) { - * // change options directly - * options.targets = isServer ? '...' : '...' - * options.corejs = '...' - * // return nothing - * } - * } - * } - * } - * ``` - * - * @example - * ```js - * export default { - * build: { - * babel: { - * presets({ isServer }, [preset, options]) { - * return [ - * [ - * preset, - * { - * targets: isServer ? '...' : '...', - * ...options - * } - * ], - * [ - * // Other presets - * ] - * ] - * } - * } - * } - * } - * ``` + * @note Enabling sourceMap will leave `//# sourceMappingURL` linking comment at + * the end of each output file if webpack `config.devtool` is set to `source-map`. + * @version 2 */ - presets: {}, - cacheDirectory: { - $resolve: (val, get) => val ?? get('dev') - } - }, + terser: {}, - /** - * If you want to transpile specific dependencies with Babel, you can add them here. - * Each item in transpile can be a package name, a function, a string or regex object matching the - * dependency's file name. - * - * You can also use a function to conditionally transpile. The function will receive an object ({ isDev, isServer, isClient, isModern, isLegacy }). - * - * @example - * ```js - transpile: [({ isLegacy }) => isLegacy && 'ky'] - * ``` - * @version 2 - * @version 3 - * @type {Array} - */ - transpile: { - $resolve: val => [].concat(val).filter(Boolean) - }, + /** + * Enables the [HardSourceWebpackPlugin](https://github.com/mzgoddard/hard-source-webpack-plugin) for improved caching. + * + * @warning unstable + * @version 2 + */ + hardSource: false, - /** - * Customize PostCSS Loader plugins. - * Sames options as https://github.com/webpack-contrib/postcss-loader#options - * @version 2 - * @version 3 webpack only - */ - postcss: { - execute: undefined, - postcssOptions: { - config: undefined, - plugins: undefined + /** + * Hard-replaces `typeof process`, `typeof window` and `typeof document` to tree-shake bundle. + * @version 2 + */ + aggressiveCodeRemoval: false, + + /** + * OptimizeCSSAssets plugin options. + * + * Defaults to true when `extractCSS` is enabled. + * + * @see [optimize-css-assets-webpack-plugin documentation](https://github.com/NMFR/optimize-css-assets-webpack-plugin). + * @version 2 + */ + optimizeCSS: { + $resolve: (val, get) => val ?? (get('build.extractCSS') ? {} : false) }, - sourceMap: undefined, - implementation: undefined, - order: '' - }, - /** @version 2 */ - html: { /** - * Configuration for the html-minifier plugin used to minify HTML files created - * during the build process (will be applied for all modes). + * Configure [webpack optimization](https://webpack.js.org/configuration/optimization/). + * @version 2 + */ + optimization: { + runtimeChunk: 'single', + /** Set minimize to false to disable all minimizers. (It is disabled in development by default) */ + minimize: { $resolve: (val, get) => val ?? !get('dev') }, + /** You can set minimizer to a customized array of plugins. */ + minimizer: undefined, + splitChunks: { + chunks: 'all', + automaticNameDelimiter: '/', + cacheGroups: {} + } + }, + + /** + * Whether to split code for `layout`, `pages` and `commons` chunks. * - * @warning If you make changes, they won't be merged with the defaults! + * Commons libs include `vue`, `vue-loader`, `vue-router`, `vuex`, etc. + * @version 2 + */ + splitChunks: { + layouts: false, + pages: true, + commons: true + }, + + /** + * Nuxt will automatically detect the current version of `core-js` in your project (`'auto'`), + * or you can specify which version you want to use (`2` or `3`). + * @version 2 + */ + corejs: 'auto', + + /** + * Customize your Babel configuration. + * + * See [babel-loader options](https://github.com/babel/babel-loader#options) and + * [babel options](https://babeljs.io/docs/en/options). + * + * @note `.babelrc` is ignored by default. + * @version 2 + */ + babel: { + configFile: false, + babelrc: false, + /** + * An array of Babel plugins to load, or a function that takes webpack context and returns + * an array of Babel plugins. + * + * For more information see [Babel plugins options](https://babeljs.io/docs/en/options#plugins) + * and [babel-loader options](https://github.com/babel/babel-loader#options). + */ + plugins: [], + /** + * The Babel presets to be applied. + * + * @note The presets configured here will be applied to both the client and the server + * build. The target will be set by Nuxt accordingly (client/server). If you want to configure + * the preset differently for the client or the server build, please use presets as a function. + * + * @warning It is highly recommended to use the default preset instead customizing. + * + * @example + * ```js + * export default { + * build: { + * babel: { + * presets({ isServer }, [ preset, options ]) { + * // change options directly + * options.targets = isServer ? '...' : '...' + * options.corejs = '...' + * // return nothing + * } + * } + * } + * } + * ``` + * + * @example + * ```js + * export default { + * build: { + * babel: { + * presets({ isServer }, [preset, options]) { + * return [ + * [ + * preset, + * { + * targets: isServer ? '...' : '...', + * ...options + * } + * ], + * [ + * // Other presets + * ] + * ] + * } + * } + * } + * } + * ``` + */ + presets: {}, + cacheDirectory: { + $resolve: (val, get) => val ?? get('dev') + } + }, + + /** + * If you want to transpile specific dependencies with Babel, you can add them here. + * Each item in transpile can be a package name, a function, a string or regex object matching the + * dependency's file name. + * + * You can also use a function to conditionally transpile. The function will receive an object ({ isDev, isServer, isClient, isModern, isLegacy }). * * @example * ```js - * export default { - * html: { - * minify: { - * collapseBooleanAttributes: true, - * decodeEntities: true, - * minifyCSS: true, - * minifyJS: true, - * processConditionalComments: true, - * removeEmptyAttributes: true, - * removeRedundantAttributes: true, - * trimCustomFragments: true, - * useShortDoctype: true + transpile: [({ isLegacy }) => isLegacy && 'ky'] + * ``` + * @version 2 + * @version 3 + * @type {Array} + */ + transpile: { + $resolve: val => [].concat(val).filter(Boolean) + }, + + /** + * Customize PostCSS Loader plugins. + * Sames options as https://github.com/webpack-contrib/postcss-loader#options + * @version 2 + */ + postcss: { + execute: undefined, + postcssOptions: { + $resolve: (val, get) => { + // Ensure we return the same object in `build.postcss.postcssOptions as `postcss` + // so modules which modify the configuration continue to work. + const postcssOptions = get('postcss') + Object.assign(postcssOptions, defu(postcssOptions, val)) + return postcssOptions + } + }, + sourceMap: undefined, + implementation: undefined, + order: '' + }, + + /** @version 2 */ + html: { + /** + * Configuration for the html-minifier plugin used to minify HTML files created + * during the build process (will be applied for all modes). + * + * @warning If you make changes, they won't be merged with the defaults! + * + * @example + * ```js + * export default { + * html: { + * minify: { + * collapseBooleanAttributes: true, + * decodeEntities: true, + * minifyCSS: true, + * minifyJS: true, + * processConditionalComments: true, + * removeEmptyAttributes: true, + * removeRedundantAttributes: true, + * trimCustomFragments: true, + * useShortDoctype: true + * } + * } + * } + * ``` + */ + minify: { + collapseBooleanAttributes: true, + decodeEntities: true, + minifyCSS: true, + minifyJS: true, + processConditionalComments: true, + removeEmptyAttributes: true, + removeRedundantAttributes: true, + trimCustomFragments: true, + useShortDoctype: true + } + }, + + /** + * Allows setting a different app template (other than `@nuxt/vue-app`) + * @version 2 + */ + template: undefined, + /** + * You can provide your own templates which will be rendered based + * on Nuxt configuration. This feature is specially useful for using with modules. + * + * Templates are rendered using [`lodash.template`](https://lodash.com/docs/4.17.15#template). + * + * @example + * ```js + * templates: [ + * { + * src: '~/modules/support/plugin.js', // `src` can be absolute or relative + * dst: 'support.js', // `dst` is relative to project `.nuxt` dir + * options: { + * // Options are provided to template as `options` key + * live_chat: false * } * } - * } + * ] * ``` + * @version 2 + * @version 3 */ - minify: { - collapseBooleanAttributes: true, - decodeEntities: true, - minifyCSS: true, - minifyJS: true, - processConditionalComments: true, - removeEmptyAttributes: true, - removeRedundantAttributes: true, - trimCustomFragments: true, - useShortDoctype: true - } - }, + templates: [], - /** - * Allows setting a different app template (other than `@nuxt/vue-app`) - * @version 2 - */ - template: undefined, - /** - * You can provide your own templates which will be rendered based - * on Nuxt configuration. This feature is specially useful for using with modules. - * - * Templates are rendered using [`lodash.template`](https://lodash.com/docs/4.17.15#template). - * - * @example - * ```js - * templates: [ - * { - * src: '~/modules/support/plugin.js', // `src` can be absolute or relative - * dst: 'support.js', // `dst` is relative to project `.nuxt` dir - * options: { - * // Options are provided to template as `options` key - * live_chat: false - * } - * } - * ] - * ``` - * @version 2 - * @version 3 - */ - templates: [], + /** + * You can provide your custom files to watch and regenerate after changes. + * + * This feature is specially useful for using with modules. + * + * @example + * ```js + watch: ['~/.nuxt/support.js'] + * ``` + * @version 2 + */ + watch: [], + /** + * See [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware) for available options. + * @version 2 + */ + devMiddleware: { + stats: 'none' + }, + /** + * See [webpack-hot-middleware](https://github.com/webpack-contrib/webpack-hot-middleware) for available options. + * @version 2 + */ + hotMiddleware: {}, - /** - * You can provide your custom files to watch and regenerate after changes. - * - * This feature is specially useful for using with modules. - * - * @example - * ```js - watch: ['~/.nuxt/support.js'] - * ``` - * @version 2 - */ - watch: [], - /** - * See [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware) for available options. - * @version 2 - * @version 3 webpack only - */ - devMiddleware: { - stats: 'none' - }, - /** - * See [webpack-hot-middleware](https://github.com/webpack-contrib/webpack-hot-middleware) for available options. - * @version 2 - * @version 3 webpack only - */ - hotMiddleware: {}, + /** @version 2 */ + vendor: { + $meta: { + deprecated: 'vendor has been deprecated since nuxt 2' + } + }, - /** @version 2 */ - vendor: { - $meta: { - deprecated: 'vendor has been deprecated since nuxt 2' - } - }, + /** + * Set to `'none'` or `false` to disable stats printing out after a build. + * @version 2 + */ + stats: { + $resolve: (val, get) => (val === 'none' || get('build.quiet')) ? false : val, + excludeAssets: [ + /.map$/, + /index\..+\.html$/, + /vue-ssr-(client|modern)-manifest.json/ + ] + }, + /** + * Set to `false` to disable the overlay provided by [FriendlyErrorsWebpackPlugin](https://github.com/nuxt/friendly-errors-webpack-plugin) + * @version 2 + */ + friendlyErrors: true, + /** + * Additional extensions (beyond `['vue', 'js']` to support in `pages/`, `layouts/`, `middleware/`, etc.) + * @version 2 + */ + additionalExtensions: [], + /** + * Filters to hide build warnings. + * @version 2 + */ + warningIgnoreFilters: [], - /** - * Set to `'none'` or `false` to disable stats printing out after a build. - * @version 2 - */ - stats: { - $resolve: (val, get) => (val === 'none' || get('build.quiet')) ? false : val, - excludeAssets: [ - /.map$/, - /index\..+\.html$/, - /vue-ssr-(client|modern)-manifest.json/ - ] - }, - /** - * Set to `false` to disable the overlay provided by [FriendlyErrorsWebpackPlugin](https://github.com/nuxt/friendly-errors-webpack-plugin) - * @version 2 - * @version 3 webpack only - */ - friendlyErrors: true, - /** - * Additional extensions (beyond `['vue', 'js']` to support in `pages/`, `layouts/`, `middleware/`, etc.) - * @version 2 - */ - additionalExtensions: [], - /** - * Filters to hide build warnings. - * @version 2 - * @version 3 webpack only - */ - warningIgnoreFilters: [], - - /** - * Set to true to scan files within symlinks in the build (such as within `pages/`). - * @version 2 - */ - - followSymlinks: false + /** + * Set to true to scan files within symlinks in the build (such as within `pages/`). + * @version 2 + */ + followSymlinks: false + } } diff --git a/packages/schema/src/config/index.ts b/packages/schema/src/config/index.ts index bb868be66c..a3184b48ae 100644 --- a/packages/schema/src/config/index.ts +++ b/packages/schema/src/config/index.ts @@ -10,7 +10,10 @@ import router from './router' import server from './server' import cli from './cli' import generate from './generate' +import postcss from './postcss' import typescript from './typescript' +import vite from './vite' +import webpack from './webpack' import nitro from './nitro' import experimental from './experimental' @@ -37,25 +40,18 @@ export default { ..._app, ..._common, ..._internal, + ...postcss, + ...typescript, + ...vite, + ...webpack, ...nitro, - build, + // Legacy + ...build, messages, render, router, server, cli, generate, - typescript, experimental, - - /** - * Configuration that will be passed directly to Vite. - * - * See https://vitejs.dev/config for more information. - * Please note that not all vite options are supported in Nuxt. - * - * @type {boolean | typeof import('vite').InlineConfig} - * @version 3 - */ - vite: undefined, } diff --git a/packages/schema/src/config/postcss.ts b/packages/schema/src/config/postcss.ts new file mode 100644 index 0000000000..2ad0ff1535 --- /dev/null +++ b/packages/schema/src/config/postcss.ts @@ -0,0 +1,53 @@ +import defu from 'defu' +import createResolver from 'postcss-import-resolver' + +export default { + /** @version 3 */ + postcss: { + /** Path to postcss config file. */ + /** @type string | false */ + config: false, + /** + * Options for configuring PostCSS plugins. + * + * https://postcss.org/ + */ + plugins: { + /** + * https://github.com/postcss/postcss-import + */ + 'postcss-import': { + $resolve: (val, get) => val !== false ? defu(val || {}, { + resolve: createResolver({ + alias: { ...get('alias') }, + modules: [ + get('srcDir'), + get('rootDir'), + ...get('modulesDir') + ] + }) + }) : val, + }, + + /** + * https://github.com/postcss/postcss-url + */ + 'postcss-url': {}, + + /** + * https://github.com/postcss/autoprefixer + */ + autoprefixer: {}, + + cssnano: { + $resolve: (val, get) => val ?? (get('dev') && { + preset: ['default', { + // Keep quotes in font values to prevent from HEX conversion + // https://github.com/nuxt/nuxt.js/issues/6306 + minifyFontValues: { removeQuotes: false } + }] + }) + } + } + } +} diff --git a/packages/schema/src/config/typescript.ts b/packages/schema/src/config/typescript.ts index 007d4a7213..7d4c34c32f 100644 --- a/packages/schema/src/config/typescript.ts +++ b/packages/schema/src/config/typescript.ts @@ -1,21 +1,23 @@ export default { - /** + typescript: { + /** * TypeScript comes with certain checks to give you more safety and analysis of your program. * Once you’ve converted your codebase to TypeScript, you can start enabling these checks for greater safety. * [Read More](https://www.typescriptlang.org/docs/handbook/migrating-from-javascript.html#getting-stricter-checks) */ - strict: false, + strict: false, - /** - * You can extend generated `.nuxt/tsconfig.json` using this option - * @typedef {Awaited>} - */ - tsConfig: {}, + /** + * You can extend generated `.nuxt/tsconfig.json` using this option + * @typedef {Awaited>} + */ + tsConfig: {}, - /** - * Generate a `*.vue` shim. - * - * We recommend instead either enabling [**Take Over Mode**](https://github.com/johnsoncodehk/volar/discussions/471) or adding **TypeScript Vue Plugin (Volar)** 👉 [[Download](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.vscode-typescript-vue-plugin)]. - */ - shim: true + /** + * Generate a `*.vue` shim. + * + * We recommend instead either enabling [**Take Over Mode**](https://github.com/johnsoncodehk/volar/discussions/471) or adding **TypeScript Vue Plugin (Volar)** 👉 [[Download](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.vscode-typescript-vue-plugin)]. + */ + shim: true + } } diff --git a/packages/schema/src/config/vite.ts b/packages/schema/src/config/vite.ts new file mode 100644 index 0000000000..cbafc0e07f --- /dev/null +++ b/packages/schema/src/config/vite.ts @@ -0,0 +1,83 @@ +import { resolve } from 'pathe' +import { joinURL, withoutLeadingSlash } from 'ufo' + +export default { + /** + * Configuration that will be passed directly to Vite. + * + * See https://vitejs.dev/config for more information. + * Please note that not all vite options are supported in Nuxt. + * + * @type {typeof import('vite').InlineConfig} + * @version 3 + */ + vite: { + root: { + $resolve: (val, get) => val ?? get('srcDir'), + }, + mode: { + $resolve: (val, get) => val ?? get('dev') ? 'development' : 'production', + }, + logLevel: 'warn', + define: { + $resolve: (val, get) => ({ + 'process.dev': get('dev'), + ...val || {} + }) + }, + resolve: { + extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'], + }, + base: { + $resolve: (val, get) => val ?? get('dev') + ? joinURL(get('app').baseURL, get('app').buildAssetsDir) + : '/__NUXT_BASE__/', + }, + publicDir: { + $resolve: (val, get) => val ?? resolve(get('srcDir'), get('dir').public), + }, + vue: { + isProduction: { + $resolve: (val, get) => val ?? !get('dev'), + }, + template: { compilerOptions: { + $resolve: (val, get) => val ?? get('vue').compilerOptions } + }, + }, + optimizeDeps: { + exclude: { + $resolve: (val, get) => [ + ...val || [], + ...get('build.transpile').filter(i => typeof i === 'string'), + 'vue-demi' + ], + }, + }, + esbuild: { + jsxFactory: 'h', + jsxFragment: 'Fragment', + tsconfigRaw: '{}' + }, + clearScreen: false, + build: { + assetsDir: { + $resolve: (val, get) => val ?? get('dev') ? withoutLeadingSlash(get('app').buildAssetsDir) : '.', + }, + emptyOutDir: false, + }, + server: { + fs: { + strict: false, + allow: { + $resolve: (val, get) => [ + get('buildDir'), + get('srcDir'), + get('rootDir'), + ...get('modulesDir'), + ...val ?? [] + ] + } + } + } + } +} diff --git a/packages/schema/src/config/webpack.ts b/packages/schema/src/config/webpack.ts new file mode 100644 index 0000000000..e83f776c46 --- /dev/null +++ b/packages/schema/src/config/webpack.ts @@ -0,0 +1,294 @@ +import { join } from 'pathe' + +export default { + /** @version 3 */ + webpack: { + /** + * Nuxt uses `webpack-bundle-analyzer` to visualize your bundles and how to optimize them. + * + * Set to `true` to enable bundle analysis, or pass an object with options: [for webpack](https://github.com/webpack-contrib/webpack-bundle-analyzer#options-for-plugin) or [for vite](https://github.com/btd/rollup-plugin-visualizer#options). + * + * @example + * ```js + * analyze: { + * analyzerMode: 'static' + * } + * ``` + * @type {boolean | typeof import('webpack-bundle-analyzer').BundleAnalyzerPlugin.Options} + */ + analyze: { + $resolve: (val, get) => { + if(val !== true) { + return val ?? false + } + const rootDir = get('rootDir') + return { + template: 'treemap', + projectRoot: rootDir, + filename: join(rootDir, '.nuxt/stats', '{name}.html') + } + } + }, + + /** + * Enable the profiler in webpackbar. + * + * It is normally enabled by CLI argument `--profile`. + * + * @see [webpackbar](https://github.com/unjs/webpackbar#profile) + */ + profile: process.argv.includes('--profile'), + + /** + * Enables Common CSS Extraction using + * [Vue Server Renderer guidelines](https://ssr.vuejs.org/guide/css.html). + * + * Using [extract-css-chunks-webpack-plugin](https://github.com/faceyspacey/extract-css-chunks-webpack-plugin/) under the hood, your CSS will be extracted + * into separate files, usually one per component. This allows caching your CSS and + * JavaScript separately and is worth trying if you have a lot of global or shared CSS. + * + * @example + * ```js + * export default { + * build: { + * extractCSS: true, + * // or + * extractCSS: { + * ignoreOrder: true + * } + * } + * } + * ``` + * + * If you want to extract all your CSS to a single file, there is a workaround for this. + * However, note that it is not recommended to extract everything into a single file. + * Extracting into multiple CSS files is better for caching and preload isolation. It + * can also improve page performance by downloading and resolving only those resources + * that are needed. + * + * @example + * ```js + * export default { + * build: { + * extractCSS: true, + * optimization: { + * splitChunks: { + * cacheGroups: { + * styles: { + * name: 'styles', + * test: /\.(css|vue)$/, + * chunks: 'all', + * enforce: true + * } + * } + * } + * } + * } + * } + * ``` + * @type {false | typeof import('mini-css-extract-plugin').PluginOptions} + */ + extractCSS: false, + + /** + * Enables CSS source map support (defaults to true in development) + */ + cssSourceMap: { + $resolve: (val, get) => val ?? get('dev') + }, + + /** + * The polyfill library to load to provide URL and URLSearchParams. + * + * Defaults to `'url'` ([see package](https://www.npmjs.com/package/url)). + */ + serverURLPolyfill: 'url', + + /** + * Customize bundle filenames. + * + * To understand a bit more about the use of manifests, take a look at [this webpack documentation](https://webpack.js.org/guides/code-splitting/). + * + * @note Be careful when using non-hashed based filenames in production + * as most browsers will cache the asset and not detect the changes on first load. + * + * This example changes fancy chunk names to numerical ids: + * + * @example + * ```js + * filenames: { + * chunk: ({ isDev }) => (isDev ? '[name].js' : '[id].[contenthash].js') + * } + * ``` + */ + filenames: { + app: ({ isDev }) => isDev ? `[name].js` : `[contenthash:7].js`, + chunk: ({ isDev }) => isDev ? `[name].js` : `[contenthash:7].js`, + css: ({ isDev }) => isDev ? '[name].css' : 'css/[contenthash:7].css', + img: ({ isDev }) => isDev ? '[path][name].[ext]' : 'img/[name].[contenthash:7].[ext]', + font: ({ isDev }) => isDev ? '[path][name].[ext]' : 'fonts/[name].[contenthash:7].[ext]', + video: ({ isDev }) => isDev ? '[path][name].[ext]' : 'videos/[name].[contenthash:7].[ext]' + }, + + /** + * Customize the options of Nuxt's integrated webpack loaders. + */ + loaders: { + $resolve: (val, get) => { + const styleLoaders = [ + 'css', 'cssModules', 'less', + 'sass', 'scss', 'stylus', 'vueStyle' + ] + for (const name of styleLoaders) { + const loader = val[name] + if (loader && loader.sourceMap === undefined) { + loader.sourceMap = Boolean(get('build.cssSourceMap')) + } + } + return val + }, + file: { esModule: false }, + fontUrl: { esModule: false, limit: 1000 }, + imgUrl: { esModule: false, limit: 1000 }, + pugPlain: {}, + vue: { + productionMode: { $resolve: (val, get) => val ?? !get('dev') }, + transformAssetUrls: { + video: 'src', + source: 'src', + object: 'src', + embed: 'src' + }, + compilerOptions: { $resolve: (val, get) => val ?? get('vue.compilerOptions') }, + }, + css: { + importLoaders: 0, + esModule: false + }, + cssModules: { + importLoaders: 0, + esModule: false, + modules: { + localIdentName: '[local]_[hash:base64:5]' + } + }, + less: {}, + sass: { + sassOptions: { + indentedSyntax: true + } + }, + scss: {}, + stylus: {}, + vueStyle: {} + }, + + /** + * Add webpack plugins. + * + * @example + * ```js + * import webpack from 'webpack' + * import { version } from './package.json' + * // ... + * plugins: [ + * new webpack.DefinePlugin({ + * 'process.VERSION': version + * }) + * ] + * ``` + */ + plugins: [], + + /** + * Terser plugin options. + * + * Set to false to disable this plugin, or pass an object of options. + * + * @see [terser-webpack-plugin documentation](https://github.com/webpack-contrib/terser-webpack-plugin) + * + * @note Enabling sourceMap will leave `//# sourceMappingURL` linking comment at + * the end of each output file if webpack `config.devtool` is set to `source-map`. + * + * @type {false | typeof import('terser-webpack-plugin').BasePluginOptions & typeof import('terser-webpack-plugin').DefinedDefaultMinimizerAndOptions} + */ + terser: { + + }, + + /** + * Hard-replaces `typeof process`, `typeof window` and `typeof document` to tree-shake bundle. + */ + aggressiveCodeRemoval: false, + + /** + * OptimizeCSSAssets plugin options. + * + * Defaults to true when `extractCSS` is enabled. + * + * @see [css-minimizer-webpack-plugin documentation](https://github.com/webpack-contrib/css-minimizer-webpack-plugin). + * + * @type {false | typeof import('css-minimizer-webpack-plugin').BasePluginOptions & typeof import('css-minimizer-webpack-plugin').DefinedDefaultMinimizerAndOptions} + */ + optimizeCSS: { + $resolve: (val, get) => val ?? (get('build.extractCSS') ? {} : false) + }, + + /** + * Configure [webpack optimization](https://webpack.js.org/configuration/optimization/). + * @type {false | typeof import('webpack').Configuration['optimization']} + */ + optimization: { + runtimeChunk: 'single', + /** Set minimize to false to disable all minimizers. (It is disabled in development by default) */ + minimize: { $resolve: (val, get) => val ?? !get('dev') }, + /** You can set minimizer to a customized array of plugins. */ + minimizer: undefined, + splitChunks: { + chunks: 'all', + automaticNameDelimiter: '/', + cacheGroups: {} + } + }, + /** + * Customize PostCSS Loader. + * Same options as https://github.com/webpack-contrib/postcss-loader#options + */ + postcss: { + execute: undefined, + postcssOptions: { + config: { + $resolve: (val, get) => val ?? get('postcss.config') + }, + plugins: { + $resolve: (val, get) => val ?? get('postcss.plugins') + } + }, + sourceMap: undefined, + implementation: undefined, + order: '' + }, + + /** + * See [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware) for available options. + * @type {typeof import('webpack-dev-middleware').Options} + */ + devMiddleware: { + stats: 'none' + }, + /** + * See [webpack-hot-middleware](https://github.com/webpack-contrib/webpack-hot-middleware) for available options. + * @type {typeof import('webpack-hot-middleware').MiddlewareOptions & { client?: typeof import('webpack-hot-middleware').ClientOptions }} + */ + hotMiddleware: {}, + /** + * Set to `false` to disable the overlay provided by [FriendlyErrorsWebpackPlugin](https://github.com/nuxt/friendly-errors-webpack-plugin) + */ + friendlyErrors: true, + /** + * Filters to hide build warnings. + * @type {Array<(warn: typeof import('webpack').WebpackError) => boolean>} + */ + warningIgnoreFilters: [], + } +} diff --git a/packages/schema/src/types/module.ts b/packages/schema/src/types/module.ts index fb8a2ba761..dd2f855581 100644 --- a/packages/schema/src/types/module.ts +++ b/packages/schema/src/types/module.ts @@ -70,7 +70,7 @@ export interface ModuleContainer { /** Allows extending webpack build config by chaining `options.build.extend` function. */ extendBuild(fn): void - /** Allows extending routes by chaining `options.build.extendRoutes` function. */ + /** Allows extending routes by chaining `options.router.extendRoutes` function. */ extendRoutes(fn): void /** Registers a module */ diff --git a/packages/vite/package.json b/packages/vite/package.json index 0f922ad8f9..e34686a096 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -35,7 +35,6 @@ "p-debounce": "^4.0.0", "pathe": "^0.2.0", "postcss-import": "^14.0.2", - "postcss-import-resolver": "^2.0.0", "postcss-url": "^10.1.3", "rollup-plugin-visualizer": "^5.6.0", "ufo": "^0.7.9", diff --git a/packages/vite/src/css.ts b/packages/vite/src/css.ts index 93b4883992..5daf34d005 100644 --- a/packages/vite/src/css.ts +++ b/packages/vite/src/css.ts @@ -1,8 +1,6 @@ -import createResolver from 'postcss-import-resolver' -import defu from 'defu' import { requireModule } from '@nuxt/kit' import type { Nuxt } from '@nuxt/schema' -import { ViteOptions } from './vite' +import type { ViteOptions } from './vite' import { distDir } from './dirs' export function resolveCSSOptions (nuxt: Nuxt): ViteOptions['css'] { @@ -12,25 +10,7 @@ export function resolveCSSOptions (nuxt: Nuxt): ViteOptions['css'] { } } - const plugins = defu(nuxt.options.build.postcss.postcssOptions.plugins, { - // https://github.com/postcss/postcss-import - 'postcss-import': { - resolve: createResolver({ - alias: { ...nuxt.options.alias }, - modules: [ - nuxt.options.srcDir, - nuxt.options.rootDir, - ...nuxt.options.modulesDir - ] - }) - }, - - // https://github.com/postcss/postcss-url - 'postcss-url': {}, - - // https://github.com/postcss/autoprefixer - autoprefixer: {} - }) + const plugins = nuxt.options.postcss.plugins for (const name in plugins) { const opts = plugins[name] diff --git a/packages/vite/src/vite.ts b/packages/vite/src/vite.ts index f516630889..b37f988e67 100644 --- a/packages/vite/src/vite.ts +++ b/packages/vite/src/vite.ts @@ -5,7 +5,6 @@ import type { InlineConfig, SSROptions } from 'vite' import { logger } from '@nuxt/kit' import type { Options } from '@vitejs/plugin-vue' import { sanitizeFilePath } from 'mlly' -import { joinURL, withoutLeadingSlash } from 'ufo' import { getPort } from 'get-port-please' import { buildClient } from './client' import { buildServer } from './server' @@ -35,14 +34,7 @@ export async function bundle (nuxt: Nuxt) { nuxt, config: vite.mergeConfig( { - root: nuxt.options.srcDir, - mode: nuxt.options.dev ? 'development' : 'production', - logLevel: 'warn', - define: { - 'process.dev': nuxt.options.dev - }, resolve: { - extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'], alias: { ...nuxt.options.alias, '#app': nuxt.options.appDir, @@ -56,37 +48,16 @@ export async function bundle (nuxt: Nuxt) { 'abort-controller': 'unenv/runtime/mock/empty' } }, - base: nuxt.options.dev - ? joinURL(nuxt.options.app.baseURL, nuxt.options.app.buildAssetsDir) - : '/__NUXT_BASE__/', - publicDir: resolve(nuxt.options.srcDir, nuxt.options.dir.public), - // TODO: move to kit schema when it exists - vue: { - isProduction: !nuxt.options.dev, - template: { compilerOptions: nuxt.options.vue.compilerOptions } - }, - css: resolveCSSOptions(nuxt), optimizeDeps: { - exclude: [ - ...nuxt.options.build.transpile.filter(i => typeof i === 'string'), - 'vue-demi' - ], entries: [ resolve(nuxt.options.appDir, 'entry.ts') ] }, - esbuild: { - jsxFactory: 'h', - jsxFragment: 'Fragment', - tsconfigRaw: '{}' - }, - clearScreen: false, + css: resolveCSSOptions(nuxt), build: { - assetsDir: nuxt.options.dev ? withoutLeadingSlash(nuxt.options.app.buildAssetsDir) : '.', - emptyOutDir: false, rollupOptions: { - input: resolve(nuxt.options.appDir, 'entry'), - output: { sanitizeFileName: sanitizeFilePath } + output: { sanitizeFileName: sanitizeFilePath }, + input: resolve(nuxt.options.appDir, 'entry') } }, plugins: [ @@ -98,18 +69,13 @@ export async function bundle (nuxt: Nuxt) { port: hmrPort }, fs: { - strict: false, allow: [ - nuxt.options.buildDir, - nuxt.options.appDir, - nuxt.options.srcDir, - nuxt.options.rootDir, - ...nuxt.options.modulesDir + nuxt.options.appDir ] } } - } as ViteOptions, - nuxt.options.vite as any || {} + }, + nuxt.options.vite ) } diff --git a/packages/webpack/build.config.ts b/packages/webpack/build.config.ts index 37cccc3bc1..76f80a55c1 100644 --- a/packages/webpack/build.config.ts +++ b/packages/webpack/build.config.ts @@ -9,18 +9,14 @@ export default defineBuildConfig({ '@nuxt/kit', 'unplugin', 'webpack-virtual-modules', - '@vue/babel-preset-jsx', 'postcss', - 'postcss-import-resolver', 'postcss-loader', - 'babel-loader', 'vue-loader', 'css-loader', 'file-loader', 'style-resources-loader', 'url-loader', 'vue-style-loader', - '@babel/core', 'vue' ], externals: [ diff --git a/packages/webpack/package.json b/packages/webpack/package.json index 3959460980..a55cc8412c 100644 --- a/packages/webpack/package.json +++ b/packages/webpack/package.json @@ -19,9 +19,7 @@ "@babel/core": "^7.17.5", "@nuxt/friendly-errors-webpack-plugin": "^2.5.2", "@nuxt/kit": "3.0.0", - "@vue/babel-preset-jsx": "^1.2.4", "autoprefixer": "^10.4.2", - "babel-loader": "^8.2.3", "css-loader": "^6.6.0", "css-minimizer-webpack-plugin": "^3.4.1", "cssnano": "^5.0.17", @@ -29,7 +27,6 @@ "escape-string-regexp": "^5.0.0", "file-loader": "^6.2.0", "fs-extra": "^10.0.1", - "glob": "^7.2.0", "hash-sum": "^2.0.0", "lodash-es": "^4.17.21", "memfs": "^3.4.1", @@ -39,7 +36,6 @@ "pify": "^5.0.0", "postcss": "^8.4.7", "postcss-import": "^14.0.2", - "postcss-import-resolver": "^2.0.0", "postcss-loader": "^6.2.1", "postcss-url": "^10.1.3", "style-resources-loader": "^1.5.0", @@ -59,7 +55,6 @@ "devDependencies": { "@nuxt/schema": "3.0.0", "@types/pify": "^5.0.1", - "@types/terser-webpack-plugin": "^5.0.4", "@types/webpack-bundle-analyzer": "^4.4.1", "@types/webpack-dev-middleware": "^5.0.2", "@types/webpack-hot-middleware": "^2.25.6", diff --git a/packages/webpack/src/configs/client.ts b/packages/webpack/src/configs/client.ts index 5c72d2f93f..2fe26a12e3 100644 --- a/packages/webpack/src/configs/client.ts +++ b/packages/webpack/src/configs/client.ts @@ -3,7 +3,6 @@ import { resolve } from 'pathe' import webpack from 'webpack' import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer' -import type { ClientOptions } from 'webpack-hot-middleware' import { joinURL } from 'ufo' import { applyPresets, WebpackConfigContext } from '../utils/config' import { nuxt } from '../presets/nuxt' @@ -50,7 +49,7 @@ function clientHMR (ctx: WebpackConfigContext) { return } - const clientOptions = options.build.hotMiddleware?.client || {} as ClientOptions + const clientOptions = options.webpack.hotMiddleware?.client || {} const hotMiddlewareClientOptions = { reload: true, timeout: 30000, @@ -81,7 +80,7 @@ function clientPlugins (ctx: WebpackConfigContext) { // Webpack Bundle Analyzer // https://github.com/webpack-contrib/webpack-bundle-analyzer - if (!ctx.isDev && ctx.name === 'client' && options.build.analyze) { + if (!ctx.isDev && ctx.name === 'client' && options.webpack.analyze) { const statsDir = resolve(options.buildDir, 'stats') // @ts-ignore @@ -92,7 +91,7 @@ function clientPlugins (ctx: WebpackConfigContext) { openAnalyzer: !options.build.quiet, reportFilename: resolve(statsDir, `${ctx.name}.html`), statsFilename: resolve(statsDir, `${ctx.name}.json`), - ...options.build.analyze as any + ...options.webpack.analyze === true ? {} : options.webpack.analyze })) } } diff --git a/packages/webpack/src/configs/server.ts b/packages/webpack/src/configs/server.ts index ce83c70e5c..add9527ee5 100644 --- a/packages/webpack/src/configs/server.ts +++ b/packages/webpack/src/configs/server.ts @@ -71,10 +71,10 @@ function serverPlugins (ctx: WebpackConfigContext) { const { config, options } = ctx // Server polyfills - if (options.build.serverURLPolyfill) { + if (options.webpack.serverURLPolyfill) { config.plugins.push(new webpack.ProvidePlugin({ - URL: [options.build.serverURLPolyfill, 'URL'], - URLSearchParams: [options.build.serverURLPolyfill, 'URLSearchParams'] + URL: [options.webpack.serverURLPolyfill, 'URL'], + URLSearchParams: [options.webpack.serverURLPolyfill, 'URLSearchParams'] })) } } diff --git a/packages/webpack/src/plugins/warning-ignore.ts b/packages/webpack/src/plugins/warning-ignore.ts index ac48316443..f1432b7660 100644 --- a/packages/webpack/src/plugins/warning-ignore.ts +++ b/packages/webpack/src/plugins/warning-ignore.ts @@ -1,12 +1,15 @@ -import type { WebpackError } from 'webpack' -export default class WarningIgnorePlugin { - filter: (warn: WebpackError) => boolean +import type { Compiler, WebpackError } from 'webpack' - constructor (filter) { +export type WarningFilter = (warn: WebpackError) => boolean + +export default class WarningIgnorePlugin { + filter: WarningFilter + + constructor (filter: WarningFilter) { this.filter = filter } - apply (compiler) /* istanbul ignore next */ { + apply (compiler: Compiler) { compiler.hooks.done.tap('warnfix-plugin', (stats) => { stats.compilation.warnings = stats.compilation.warnings.filter(this.filter) }) diff --git a/packages/webpack/src/presets/assets.ts b/packages/webpack/src/presets/assets.ts index 0768263118..15a52ad7b2 100644 --- a/packages/webpack/src/presets/assets.ts +++ b/packages/webpack/src/presets/assets.ts @@ -1,15 +1,13 @@ 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, + ...ctx.options.webpack.loaders.imgUrl, name: fileName(ctx, 'img') } }] @@ -19,7 +17,7 @@ export function assets (ctx: WebpackConfigContext) { use: [{ loader: 'url-loader', options: { - ...options.build.loaders.fontUrl, + ...ctx.options.webpack.loaders.fontUrl, name: fileName(ctx, 'font') } }] @@ -29,7 +27,7 @@ export function assets (ctx: WebpackConfigContext) { use: [{ loader: 'file-loader', options: { - ...options.build.loaders.file, + ...ctx.options.webpack.loaders.file, name: fileName(ctx, 'video') } }] diff --git a/packages/webpack/src/presets/babel.ts b/packages/webpack/src/presets/babel.ts deleted file mode 100644 index bd1a1d28f9..0000000000 --- a/packages/webpack/src/presets/babel.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { createRequire } from 'module' -import { normalize } from 'pathe' -import TerserWebpackPlugin from 'terser-webpack-plugin' -import { reservedVueTags } from '../utils/reserved-tags' -import { WebpackConfigContext } from '../utils/config' - -const _require = createRequire(import.meta.url) - -export function babel (ctx: WebpackConfigContext) { - const { config, options } = ctx - - const babelLoader = { - loader: normalize(_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)) - }, - resolve: { - fullySpecified: false - }, - 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 any) - } -} - -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 = [normalize(_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/webpack/src/presets/base.ts b/packages/webpack/src/presets/base.ts index 8ed00530ef..fca55a6939 100644 --- a/packages/webpack/src/presets/base.ts +++ b/packages/webpack/src/presets/base.ts @@ -6,7 +6,7 @@ import { logger } from '@nuxt/kit' import FriendlyErrorsWebpackPlugin from '@nuxt/friendly-errors-webpack-plugin' import escapeRegExp from 'escape-string-regexp' import { joinURL } from 'ufo' -import WarningIgnorePlugin from '../plugins/warning-ignore' +import WarningIgnorePlugin, { WarningFilter } from '../plugins/warning-ignore' import { WebpackConfigContext, applyPresets, fileName } from '../utils/config' export function base (ctx: WebpackConfigContext) { @@ -28,7 +28,7 @@ function baseConfig (ctx: WebpackConfigContext) { plugins: [], externals: [], optimization: { - ...options.build.optimization as any, + ...options.webpack.optimization, minimizer: [] }, experiments: {}, @@ -43,13 +43,13 @@ function baseConfig (ctx: WebpackConfigContext) { function basePlugins (ctx: WebpackConfigContext) { const { config, options, nuxt } = ctx - // Add timefix-plugin before others plugins + // Add timefix-plugin before other plugins if (options.dev) { config.plugins.push(new TimeFixPlugin()) } // User plugins - config.plugins.push(...(options.build.plugins || [])) + config.plugins.push(...(options.webpack.plugins || [])) // Ignore empty warnings config.plugins.push(new WarningIgnorePlugin(getWarningIgnoreFilter(ctx))) @@ -60,7 +60,7 @@ function basePlugins (ctx: WebpackConfigContext) { // Friendly errors if ( ctx.isServer || - (ctx.isDev && !options.build.quiet && options.build.friendlyErrors) + (ctx.isDev && !options.build.quiet && options.webpack.friendlyErrors) ) { ctx.config.plugins.push( new FriendlyErrorsWebpackPlugin({ @@ -71,39 +71,41 @@ function basePlugins (ctx: WebpackConfigContext) { ) } - // Webpackbar - const colors = { - client: 'green', - server: 'orange', - modern: 'blue' - } - config.plugins.push(new WebpackBar({ - name: ctx.name, - color: colors[ctx.name], - reporters: ['stats'], - stats: !ctx.isDev, - reporter: { - // @ts-ignore - change: (_, { shortPath }) => { - if (!ctx.isServer) { - nuxt.callHook('bundler:change', shortPath) - } - }, - done: ({ state }) => { - if (state.hasErrors) { - nuxt.callHook('bundler:error') - } else { - logger.success(`${state.name} ${state.message}`) - } - }, - allDone: () => { - nuxt.callHook('bundler:done') - }, - progress ({ statesArray }) { - nuxt.callHook('bundler:progress', statesArray) - } + if (nuxt.options.webpack.profile) { + // Webpackbar + const colors = { + client: 'green', + server: 'orange', + modern: 'blue' } - })) + config.plugins.push(new WebpackBar({ + name: ctx.name, + color: colors[ctx.name], + reporters: ['stats'], + stats: !ctx.isDev, + reporter: { + // @ts-ignore + change: (_, { shortPath }) => { + if (!ctx.isServer) { + nuxt.callHook('bundler:change', shortPath) + } + }, + done: ({ state }) => { + if (state.hasErrors) { + nuxt.callHook('bundler:error') + } else { + logger.success(`${state.name} ${state.message}`) + } + }, + allDone: () => { + nuxt.callHook('bundler:done') + }, + progress ({ statesArray }) { + nuxt.callHook('bundler:progress', statesArray) + } + } + })) + } } function baseAlias (ctx: WebpackConfigContext) { @@ -198,15 +200,15 @@ function getOutput (ctx: WebpackConfigContext): webpack.Configuration['output'] } } -function getWarningIgnoreFilter (ctx: WebpackConfigContext) { +function getWarningIgnoreFilter (ctx: WebpackConfigContext): WarningFilter { const { options } = ctx - const filters = [ + const filters: WarningFilter[] = [ // 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 || []) + ...(options.webpack.warningIgnoreFilters || []) ] return warn => !filters.some(ignoreFilter => ignoreFilter(warn)) @@ -224,11 +226,10 @@ function getEnv (ctx: WebpackConfigContext) { 'process.env.VUE_ENV': JSON.stringify(ctx.name), 'process.browser': ctx.isClient, 'process.client': ctx.isClient, - 'process.server': ctx.isServer, - 'process.modern': ctx.isModern + 'process.server': ctx.isServer } - if (options.build.aggressiveCodeRemoval) { + if (options.webpack.aggressiveCodeRemoval) { _env['typeof process'] = JSON.stringify(ctx.isServer ? 'object' : 'undefined') _env['typeof window'] = _env['typeof document'] = JSON.stringify(!ctx.isServer ? 'object' : 'undefined') } diff --git a/packages/webpack/src/presets/esbuild.ts b/packages/webpack/src/presets/esbuild.ts index 0785b33088..494282144c 100644 --- a/packages/webpack/src/presets/esbuild.ts +++ b/packages/webpack/src/presets/esbuild.ts @@ -17,14 +17,10 @@ export function esbuild (ctx: WebpackConfigContext) { test: /\.m?[jt]s$/i, loader: 'esbuild-loader', exclude: (file) => { - file = file.split('node_modules', 2)[1] - // Not exclude files outside node_modules - if (!file) { - return false - } + file = file.split('node_modules', 2)[1] + if (!file) { return false } - // Item in transpile can be string or regex object return !ctx.transpile.some(module => module.test(file)) }, resolve: { diff --git a/packages/webpack/src/presets/node.ts b/packages/webpack/src/presets/node.ts index e98c987302..4f1fd4a9ce 100644 --- a/packages/webpack/src/presets/node.ts +++ b/packages/webpack/src/presets/node.ts @@ -1,4 +1,4 @@ -import { WebpackConfigContext } from '../utils/config' +import type { WebpackConfigContext } from '../utils/config' export function node (ctx: WebpackConfigContext) { const { config } = ctx diff --git a/packages/webpack/src/presets/nuxt.ts b/packages/webpack/src/presets/nuxt.ts index 216a5c6531..ffc19aaed3 100644 --- a/packages/webpack/src/presets/nuxt.ts +++ b/packages/webpack/src/presets/nuxt.ts @@ -11,7 +11,6 @@ export function nuxt (ctx: WebpackConfigContext) { applyPresets(ctx, [ base, assets, - // babel, esbuild, pug, style, diff --git a/packages/webpack/src/presets/pug.ts b/packages/webpack/src/presets/pug.ts index 3ff0939f8a..2cccf5867b 100644 --- a/packages/webpack/src/presets/pug.ts +++ b/packages/webpack/src/presets/pug.ts @@ -8,7 +8,7 @@ export function pug (ctx: WebpackConfigContext) { resourceQuery: /^\?vue/i, use: [{ loader: 'pug-plain-loader', - options: ctx.options.build.loaders.pugPlain + options: ctx.options.webpack.loaders.pugPlain }] }, { @@ -16,7 +16,7 @@ export function pug (ctx: WebpackConfigContext) { 'raw-loader', { loader: 'pug-plain-loader', - options: ctx.options.build.loaders.pugPlain + options: ctx.options.webpack.loaders.pugPlain } ] } diff --git a/packages/webpack/src/presets/style.ts b/packages/webpack/src/presets/style.ts index 01ba048d80..c2098e3a69 100644 --- a/packages/webpack/src/presets/style.ts +++ b/packages/webpack/src/presets/style.ts @@ -1,8 +1,7 @@ -import { resolve } from 'pathe' import MiniCssExtractPlugin from 'mini-css-extract-plugin' import CssMinimizerPlugin from 'css-minimizer-webpack-plugin' import { fileName, WebpackConfigContext, applyPresets } from '../utils/config' -import { PostcssConfig } from '../utils/postcss' +import { getPostcssConfig } from '../utils/postcss' export function style (ctx: WebpackConfigContext) { applyPresets(ctx, [ @@ -15,9 +14,9 @@ export function style (ctx: WebpackConfigContext) { function minimizer (ctx: WebpackConfigContext) { const { options, config } = ctx - if (options.build.optimizeCSS && Array.isArray(config.optimization.minimizer)) { + if (options.webpack.optimizeCSS && Array.isArray(config.optimization.minimizer)) { config.optimization.minimizer.push(new CssMinimizerPlugin({ - ...options.build.optimizeCSS as any + ...options.webpack.optimizeCSS })) } } @@ -26,11 +25,11 @@ function extractCSS (ctx: WebpackConfigContext) { const { options, config } = ctx // CSS extraction - if (options.build.extractCSS) { + if (options.webpack.extractCSS) { config.plugins.push(new MiniCssExtractPlugin({ filename: fileName(ctx, 'css'), chunkFilename: fileName(ctx, 'css'), - ...(options.build.extractCSS as any) + ...options.webpack.extractCSS })) } } @@ -45,18 +44,18 @@ function loaders (ctx: WebpackConfigContext) { config.module.rules.push(createdStyleRule('postcss', /\.p(ost)?css$/i, null, ctx)) // Less - const lessLoader = { loader: 'less-loader', options: options.build.loaders.less } + const lessLoader = { loader: 'less-loader', options: options.webpack.loaders.less } config.module.rules.push(createdStyleRule('less', /\.less$/i, lessLoader, ctx)) // Sass (TODO: optional dependency) - const sassLoader = { loader: 'sass-loader', options: options.build.loaders.sass } + const sassLoader = { loader: 'sass-loader', options: options.webpack.loaders.sass } config.module.rules.push(createdStyleRule('sass', /\.sass$/i, sassLoader, ctx)) - const scssLoader = { loader: 'sass-loader', options: options.build.loaders.scss } + const scssLoader = { loader: 'sass-loader', options: options.webpack.loaders.scss } config.module.rules.push(createdStyleRule('scss', /\.scss$/i, scssLoader, ctx)) // Stylus - const stylusLoader = { loader: 'stylus-loader', options: options.build.loaders.stylus } + const stylusLoader = { loader: 'stylus-loader', options: options.webpack.loaders.stylus } config.module.rules.push(createdStyleRule('stylus', /\.styl(us)?$/i, stylusLoader, ctx)) } @@ -65,16 +64,15 @@ function createdStyleRule (lang: string, test: RegExp, processorLoader, ctx: Web const styleLoaders = [ createPostcssLoadersRule(ctx), - processorLoader, - createStyleResourcesLoaderRule(lang, options.build.styleResources, options.rootDir) + processorLoader ].filter(Boolean) - options.build.loaders.css.importLoaders = - options.build.loaders.cssModules.importLoaders = + options.webpack.loaders.css.importLoaders = + options.webpack.loaders.cssModules.importLoaders = styleLoaders.length - const cssLoaders = createCssLoadersRule(ctx, options.build.loaders.css) - const cssModuleLoaders = createCssLoadersRule(ctx, options.build.loaders.cssModules) + const cssLoaders = createCssLoadersRule(ctx, options.webpack.loaders.css) + const cssModuleLoaders = createCssLoadersRule(ctx, options.webpack.loaders.cssModules) return { test, @@ -97,7 +95,7 @@ function createCssLoadersRule (ctx: WebpackConfigContext, cssLoaderOptions) { const cssLoader = { loader: 'css-loader', options: cssLoaderOptions } - if (options.build.extractCSS) { + if (options.webpack.extractCSS) { if (ctx.isServer) { return [cssLoader] } @@ -113,37 +111,18 @@ function createCssLoadersRule (ctx: WebpackConfigContext, cssLoaderOptions) { return [ { loader: 'vue-style-loader', - options: options.build.loaders.vueStyle + options: options.webpack.loaders.vueStyle }, cssLoader ] } -function createStyleResourcesLoaderRule (styleLang, styleResources, rootDir) { - // style-resources-loader - // https://github.com/yenshih/style-resources-loader - if (!styleResources[styleLang]) { - return - } - - return { - loader: 'style-resources-loader', - options: { - patterns: Array.from(styleResources[styleLang]).map(p => resolve(rootDir, p as string)), - ...styleResources.options - } - } -} - function createPostcssLoadersRule (ctx: WebpackConfigContext) { const { options, nuxt } = ctx - if (!options.build.postcss) { - return - } + if (!options.postcss) { return } - const postcssConfig = new PostcssConfig(nuxt) - const config = postcssConfig.config() + const config = getPostcssConfig(nuxt) if (!config) { return diff --git a/packages/webpack/src/presets/vue.ts b/packages/webpack/src/presets/vue.ts index 7355ca8f44..6b0c6544b9 100644 --- a/packages/webpack/src/presets/vue.ts +++ b/packages/webpack/src/presets/vue.ts @@ -14,7 +14,7 @@ export function vue (ctx: WebpackConfigContext) { config.module.rules.push({ test: /\.vue$/i, loader: 'vue-loader', - options: options.build.loaders.vue + options: options.webpack.loaders.vue }) if (ctx.isClient) { diff --git a/packages/webpack/src/utils/babel-preset.cjs b/packages/webpack/src/utils/babel-preset.cjs deleted file mode 100644 index 6986d40c69..0000000000 --- a/packages/webpack/src/utils/babel-preset.cjs +++ /dev/null @@ -1,202 +0,0 @@ -const coreJsMeta = { - 2: { - prefixes: { - es6: 'es6', - es7: 'es7' - }, - builtIns: '@babel/compat-data/corejs2-built-ins' - }, - 3: { - prefixes: { - es6: 'es', - es7: 'es' - }, - builtIns: 'core-js-compat/data' - } -} - -function getDefaultPolyfills (corejs) { - const { prefixes: { es6, es7 } } = coreJsMeta[corejs.version] - return [ - // Promise polyfill alone doesn't work in IE, - // Needs this as well. see: #1642 - `${es6}.array.iterator`, - // This is required for webpack code splitting, vuex etc. - `${es6}.promise`, - // this is needed for object rest spread support in templates - // as vue-template-es2015-compiler 1.8+ compiles it to Object.assign() calls. - `${es6}.object.assign`, - // #2012 es7.promise replaces native Promise in FF and causes missing finally - `${es7}.promise.finally` - ] -} - -function getPolyfills (targets, includes, { ignoreBrowserslistConfig, configPath, corejs }) { - const { default: getTargets, isRequired } = require('@babel/helper-compilation-targets') - const builtInsList = require(coreJsMeta[corejs.version].builtIns) - const builtInTargets = getTargets(targets, { - ignoreBrowserslistConfig, - configPath - }) - - return includes.filter(item => isRequired( - 'nuxt-polyfills', - builtInTargets, - { - compatData: { 'nuxt-polyfills': builtInsList[item] } - } - )) -} - -function isPackageHoisted (packageName) { - const path = require('pathe') - const installedPath = path.normalize(require.resolve(packageName)) - const relativePath = path.resolve(__dirname, '..', 'node_modules', packageName) - return installedPath !== relativePath -} - -module.exports = (api, options = {}) => { - const presets = [] - const plugins = [] - - const envName = api.env() - - const { - bugfixes, - polyfills: userPolyfills, - loose = false, - debug = false, - useBuiltIns = 'usage', - modules = false, - spec, - ignoreBrowserslistConfig = envName === 'modern', - configPath, - include, - exclude, - shippedProposals, - forceAllTransforms, - decoratorsBeforeExport, - decoratorsLegacy, - absoluteRuntime - } = options - - let { corejs = { version: 3 } } = options - - if (typeof corejs !== 'object') { - corejs = { version: Number(corejs) } - } - - const isCorejs3Hoisted = isPackageHoisted('core-js') - if ( - (corejs.version === 3 && !isCorejs3Hoisted) || - (corejs.version === 2 && isCorejs3Hoisted) - ) { - // eslint-disable-next-line no-console - (console.fatal || console.error)(`babel corejs option is ${corejs.version}, please directly install core-js@${corejs.version}.`) - } - - const defaultTargets = { - server: { node: 'current' }, - client: { ie: 9 }, - modern: { esmodules: true } - } - - let { targets = defaultTargets[envName] } = options - - // modern mode can only be { esmodules: true } - if (envName === 'modern') { - targets = defaultTargets.modern - } - - const polyfills = [] - - if (envName === 'client' && useBuiltIns === 'usage') { - polyfills.push( - ...getPolyfills( - targets, - userPolyfills || getDefaultPolyfills(corejs), - { - ignoreBrowserslistConfig, - configPath, - corejs - } - ) - ) - plugins.push([polyfillsPlugin, { polyfills }]) - } - - // Pass options along to babel-preset-env - presets.push([ - require('@babel/preset-env'), { - bugfixes, - spec, - loose, - debug, - modules, - targets, - useBuiltIns, - corejs, - ignoreBrowserslistConfig, - configPath, - include, - exclude: polyfills.concat(exclude || []), - shippedProposals, - forceAllTransforms - } - ], [ - require('@babel/preset-typescript') - ]) - - // JSX - if (options.jsx !== false) { - // presets.push([require('@vue/babel-preset-jsx'), Object.assign({}, options.jsx)]) - } - - plugins.push( - [require('@babel/plugin-proposal-decorators'), { - decoratorsBeforeExport, - legacy: decoratorsLegacy !== false - }], - [require('@babel/plugin-proposal-class-properties'), { loose: true }] - ) - - // Transform runtime, but only for helpers - plugins.push([require('@babel/plugin-transform-runtime'), { - regenerator: useBuiltIns !== 'usage', - corejs: false, - helpers: useBuiltIns === 'usage', - useESModules: envName !== 'server', - absoluteRuntime - }]) - - return { - sourceType: 'unambiguous', - presets, - plugins - } -} - -// Add polyfill imports to the first file encountered. -function polyfillsPlugin ({ _types }) { - let entryFile - return { - name: 'inject-polyfills', - visitor: { - Program (path, state) { - if (!entryFile) { - entryFile = state.filename - } else if (state.filename !== entryFile) { - return - } - - const { polyfills } = state.opts - const { createImport } = require('@babel/preset-env/lib/utils') - - // Imports are injected in reverse order - polyfills.slice().reverse().forEach((p) => { - createImport(path, p) - }) - } - } - } -} diff --git a/packages/webpack/src/utils/config.ts b/packages/webpack/src/utils/config.ts index 4214d9ee49..ac0415c889 100644 --- a/packages/webpack/src/utils/config.ts +++ b/packages/webpack/src/utils/config.ts @@ -8,21 +8,19 @@ export interface WebpackConfigContext extends ReturnType void type WebpackConfigPresetItem = WebpackConfigPreset | [WebpackConfigPreset, any] -export function createWebpackConfigContext ({ nuxt }) { +export function createWebpackConfigContext (nuxt: Nuxt) { return { - nuxt: nuxt as Nuxt, - options: nuxt.options as Nuxt['options'], + nuxt, + options: 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[] + transpile: [] as RegExp[] } } @@ -42,7 +40,7 @@ export function applyPresets (ctx: WebpackConfigContext, presets: WebpackConfigP export function fileName (ctx: WebpackConfigContext, key: string) { const { options } = ctx - let fileName = options.build.filenames[key] + let fileName = options.webpack.filenames[key] if (typeof fileName === 'function') { fileName = fileName(ctx) diff --git a/packages/webpack/src/utils/mfs.ts b/packages/webpack/src/utils/mfs.ts index af07fddd9c..4df4d4ecc9 100644 --- a/packages/webpack/src/utils/mfs.ts +++ b/packages/webpack/src/utils/mfs.ts @@ -9,7 +9,7 @@ export function createMFS () { const fs = createFsFromVolume(new Volume()) // Clone to extend - const _fs : Partial & { join?(...paths: string[]): string } = { ...fs } + const _fs: Partial & { join?(...paths: string[]): string } = { ...fs } // fs.join method is (still) expected by webpack-dev-middleware // There might be differences with https://github.com/webpack/memory-fs/blob/master/lib/join.js @@ -19,5 +19,5 @@ export function createMFS () { _fs.exists = p => Promise.resolve(_fs.existsSync(p)) _fs.readFile = pify(_fs.readFile) - return _fs + return _fs as IFs & { join?(...paths: string[]): string } } diff --git a/packages/webpack/src/utils/postcss.ts b/packages/webpack/src/utils/postcss.ts index 738722bace..ae8472cc64 100644 --- a/packages/webpack/src/utils/postcss.ts +++ b/packages/webpack/src/utils/postcss.ts @@ -1,136 +1,41 @@ -import fs from 'fs' -import { resolve } from 'pathe' -import { logger, requireModule } from '@nuxt/kit' import { createCommonJS } from 'mlly' import { defaults, merge, cloneDeep } from 'lodash-es' -import createResolver from 'postcss-import-resolver' +import { requireModule } from '@nuxt/kit' import type { Nuxt } from '@nuxt/schema' -const isPureObject = obj => obj !== null && !Array.isArray(obj) && typeof obj === 'object' +const isPureObject = (obj: unknown): obj is Object => obj !== null && !Array.isArray(obj) && typeof obj === 'object' export const orderPresets = { - cssnanoLast (names) { + cssnanoLast (names: string[]) { const nanoIndex = names.indexOf('cssnano') if (nanoIndex !== names.length - 1) { names.push(names.splice(nanoIndex, 1)[0]) } return names }, - autoprefixerLast (names) { + autoprefixerLast (names: string[]) { const nanoIndex = names.indexOf('autoprefixer') if (nanoIndex !== names.length - 1) { names.push(names.splice(nanoIndex, 1)[0]) } return names }, - autoprefixerAndCssnanoLast (names) { + autoprefixerAndCssnanoLast (names: string[]) { return orderPresets.cssnanoLast(orderPresets.autoprefixerLast(names)) } } -let _postcssConfigFileWarningShown -function postcssConfigFileWarning () { - if (_postcssConfigFileWarningShown) { - return - } - logger.warn('Please use `build.postcss` in your nuxt.config.js instead of an external config file. Support for such files will be removed in Nuxt 3 as they remove all defaults set by Nuxt and can cause severe problems with features like alias resolving inside your CSS.') - _postcssConfigFileWarningShown = true -} - -export class PostcssConfig { - nuxt: Nuxt - options: Nuxt['options'] - - constructor (nuxt) { - this.nuxt = nuxt - this.options = nuxt.options - } - - get postcssOptions () { - return this.options.build.postcss.postcssOptions - } - - get postcssImportAlias () { - const alias = { ...this.options.alias } - - for (const key in alias) { - if (key.startsWith('~')) { - continue - } - const newKey = '~' + key - if (!alias[newKey]) { - alias[newKey] = alias[key] - } - } - - return alias - } - - get defaultConfig () { - const { dev, srcDir, rootDir, modulesDir } = this.options +export const getPostcssConfig = (nuxt: Nuxt) => { + function defaultConfig () { return { - sourceMap: this.options.build.cssSourceMap, - plugins: { - // https://github.com/postcss/postcss-import - 'postcss-import': { - resolve: createResolver({ - alias: this.postcssImportAlias, - modules: [srcDir, rootDir, ...modulesDir] - }) - }, - - // https://github.com/postcss/postcss-url - 'postcss-url': {}, - - autoprefixer: {}, - - cssnano: dev - ? false - : { - preset: ['default', { - // Keep quotes in font values to prevent from HEX conversion - // https://github.com/nuxt/nuxt.js/issues/6306 - minifyFontValues: { removeQuotes: false } - }] - } - }, + sourceMap: nuxt.options.webpack.cssSourceMap, + plugins: nuxt.options.postcss.plugins, // Array, String or Function order: 'autoprefixerAndCssnanoLast' } } - searchConfigFile () { - // Search for postCSS config file and use it if exists - // https://github.com/michael-ciniawsky/postcss-load-config - // TODO: Remove in Nuxt 3 - const { srcDir, rootDir } = this.options - for (const dir of [srcDir, rootDir]) { - for (const file of [ - 'postcss.config.js', - '.postcssrc.js', - '.postcssrc', - '.postcssrc.json', - '.postcssrc.yaml' - ]) { - const configFile = resolve(dir, file) - if (fs.existsSync(configFile)) { - postcssConfigFileWarning() - return configFile - } - } - } - } - - getConfigFile () { - const loaderConfig = this.postcssOptions && this.postcssOptions.config - const postcssConfigFile = loaderConfig || this.searchConfigFile() - - if (typeof postcssConfigFile === 'string') { - return postcssConfigFile - } - } - - sortPlugins ({ plugins, order }) { + function sortPlugins ({ plugins, order }) { const names = Object.keys(plugins) if (typeof order === 'string') { order = orderPresets[order] @@ -138,11 +43,12 @@ export class PostcssConfig { return typeof order === 'function' ? order(names, orderPresets) : (order || names) } - loadPlugins (config) { + function loadPlugins (config) { if (!isPureObject(config.plugins)) { return } + // Map postcss plugins into instances on object mode once const cjs = createCommonJS(import.meta.url) - config.plugins = this.sortPlugins(config).map((pluginName) => { + config.plugins = sortPlugins(config).map((pluginName) => { const pluginFn = requireModule(pluginName, { paths: [cjs.__dirname] }) const pluginOptions = config.plugins[pluginName] if (!pluginOptions || typeof pluginFn !== 'function') { return null } @@ -150,41 +56,37 @@ export class PostcssConfig { }).filter(Boolean) } - config () { - /* istanbul ignore if */ - if (!this.options.build.postcss) { - return false + if (!nuxt.options.webpack.postcss || !nuxt.options.postcss) { + return false + } + + const configFile = nuxt.options.postcss?.config + if (configFile) { + return { + postcssOptions: { + config: configFile + }, + sourceMap: nuxt.options.webpack.cssSourceMap + } + } + + let postcssOptions = cloneDeep(nuxt.options.postcss) + // Apply default plugins + if (isPureObject(postcssOptions)) { + if (Array.isArray(postcssOptions.plugins)) { + defaults(postcssOptions, defaultConfig()) + } else { + // Keep the order of default plugins + postcssOptions = merge({}, defaultConfig(), postcssOptions) + loadPlugins(postcssOptions) } - const configFile = this.getConfigFile() - if (configFile) { - return { - postcssOptions: { - config: configFile - }, - sourceMap: this.options.build.cssSourceMap - } - } + delete nuxt.options.webpack.postcss.order - let postcssOptions = cloneDeep(this.postcssOptions) - - // Apply default plugins - if (isPureObject(postcssOptions)) { - if (Array.isArray(postcssOptions.plugins)) { - defaults(postcssOptions, this.defaultConfig) - } else { - // Keep the order of default plugins - postcssOptions = merge({}, this.defaultConfig, postcssOptions) - this.loadPlugins(postcssOptions) - } - - delete this.options.build.postcss.order - - return { - sourceMap: this.options.build.cssSourceMap, - ...this.options.build.postcss, - postcssOptions - } + return { + sourceMap: nuxt.options.webpack.cssSourceMap, + ...nuxt.options.webpack.postcss, + postcssOptions } } } diff --git a/packages/webpack/src/utils/reserved-tags.ts b/packages/webpack/src/utils/reserved-tags.ts deleted file mode 100644 index a29d1b9795..0000000000 --- a/packages/webpack/src/utils/reserved-tags.ts +++ /dev/null @@ -1,23 +0,0 @@ -export const reservedVueTags = [ - // HTML tags - 'html', 'body', 'base', 'head', 'link', 'meta', 'style', 'title', 'address', - 'article', 'aside', 'footer', 'header', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', - 'hgroup', 'nav', 'section', 'div', 'dd', 'dl', 'dt', 'figcaption', 'figure', - 'picture', 'hr', 'img', 'li', 'main', 'ol', 'p', 'pre', 'ul', 'a', 'b', 'abbr', - 'bdi', 'bdo', 'br', 'cite', 'code', 'data', 'dfn', 'em', 'i', 'kbd', 'mark', - 'q', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'small', 'span', 'strong', 'sub', - 'sup', 'time', 'u', 'var', 'wbr', 'area', 'audio', 'map', 'track', 'video', - 'embed', 'object', 'param', 'source', 'canvas', 'script', 'noscript', 'del', - 'ins', 'caption', 'col', 'colgroup', 'table', 'thead', 'tbody', 'td', 'th', 'tr', - 'button', 'datalist', 'fieldset', 'form', 'input', 'label', 'legend', 'meter', - 'optgroup', 'option', 'output', 'progress', 'select', 'textarea', 'details', - 'dialog', 'menu', 'menuitem', 'summary', 'content', 'element', 'shadow', 'template', - 'blockquote', 'iframe', 'tfoot', - // SVG tags - 'svg', 'animate', 'circle', 'clippath', 'cursor', 'defs', 'desc', 'ellipse', 'filter', - 'font-face', 'foreignObject', 'g', 'glyph', 'image', 'line', 'marker', 'mask', - 'missing-glyph', 'path', 'pattern', 'polygon', 'polyline', 'rect', 'switch', 'symbol', - 'text', 'textpath', 'tspan', 'use', 'view', - // Vue built-in tags - 'slot', 'component' -] diff --git a/packages/webpack/src/virtual-modules.ts b/packages/webpack/src/virtual-modules.ts new file mode 100644 index 0000000000..688690b2a3 --- /dev/null +++ b/packages/webpack/src/virtual-modules.ts @@ -0,0 +1,26 @@ +import { useNuxt } from '@nuxt/kit' +import VirtualModulesPlugin from 'webpack-virtual-modules' + +export function registerVirtualModules () { + const nuxt = useNuxt() + + // Initialize virtual modules instance + const virtualModules = new VirtualModulesPlugin(nuxt.vfs) + const writeFiles = () => { + for (const filePath in nuxt.vfs) { + virtualModules.writeModule(filePath, nuxt.vfs[filePath]) + } + } + + // Workaround to initialize virtual modules + nuxt.hook('build:compile', ({ compiler }) => { + if (compiler.name === 'server') { writeFiles() } + }) + // Update virtual modules when templates are updated + nuxt.hook('app:templatesGenerated', writeFiles) + + nuxt.hook('webpack:config', configs => configs.forEach((config) => { + // Support virtual modules (input) + config.plugins.push(virtualModules) + })) +} diff --git a/packages/webpack/src/webpack.ts b/packages/webpack/src/webpack.ts index 2d5c0685ce..154e9bd15c 100644 --- a/packages/webpack/src/webpack.ts +++ b/packages/webpack/src/webpack.ts @@ -1,302 +1,162 @@ import type { IncomingMessage, ServerResponse } from 'http' -import { resolve } from 'pathe' import pify from 'pify' import webpack from 'webpack' -import Glob from 'glob' -import webpackDevMiddleware from 'webpack-dev-middleware' +import webpackDevMiddleware, { API } from 'webpack-dev-middleware' import webpackHotMiddleware from 'webpack-hot-middleware' -import VirtualModulesPlugin from 'webpack-virtual-modules' -import { logger } from '@nuxt/kit' import type { Compiler, Watching } from 'webpack' -import type { Context as WebpackDevMiddlewareContext, Options as WebpackDevMiddlewareOptions } from 'webpack-dev-middleware' -import type { MiddlewareOptions as WebpackHotMiddlewareOptions } from 'webpack-hot-middleware' import type { Nuxt } from '@nuxt/schema' import { joinURL } from 'ufo' +import { logger, useNuxt } from '@nuxt/kit' import { DynamicBasePlugin } from '../../vite/src/plugins/dynamic-base' import { createMFS } from './utils/mfs' +import { registerVirtualModules } from './virtual-modules' import { client, server } from './configs' import { createWebpackConfigContext, applyPresets, getWebpackConfig } from './utils/config' -const glob = pify(Glob) -class WebpackBundler { - nuxt: Nuxt - plugins: Array - compilers: Array - compilersWatching: Array void }> - // TODO: change this when pify has better types https://github.com/sindresorhus/pify/pull/76 - devMiddleware: Record Promise, context?: WebpackDevMiddlewareContext }> - hotMiddleware: Record - virtualModules: VirtualModulesPlugin - mfs?: Compiler['outputFileSystem'] - __closed?: boolean +// TODO: Support plugins +// const plugins: string[] = [] - constructor (nuxt: Nuxt) { - this.nuxt = nuxt - // TODO: plugins - this.plugins = [] - - // Class fields - this.compilers = [] - this.compilersWatching = [] - this.devMiddleware = {} - this.hotMiddleware = {} - - // Bind middleware to self - this.middleware = this.middleware.bind(this) - - // Initialize shared MFS for dev - if (this.nuxt.options.dev) { - this.mfs = createMFS() as Compiler['outputFileSystem'] - } - - // Initialize virtual modules instance - this.virtualModules = new VirtualModulesPlugin(nuxt.vfs) - const writeFiles = () => { - for (const filePath in nuxt.vfs) { - this.virtualModules.writeModule(filePath, nuxt.vfs[filePath]) - } - } - // Workaround to initialize virtual modules - nuxt.hook('build:compile', ({ compiler }) => { - if (compiler.name === 'server') { writeFiles() } - }) - // Update virtual modules when templates are updated - nuxt.hook('app:templatesGenerated', writeFiles) - } - - getWebpackConfig (name) { - 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}`) - } +export async function bundle (nuxt: Nuxt) { + await registerVirtualModules() + const webpackConfigs = [client, ...nuxt.options.ssr ? [server] : []].map((preset) => { + const ctx = createWebpackConfigContext(nuxt) + applyPresets(ctx, preset) return getWebpackConfig(ctx) - } + }) - async build () { - const { options } = this.nuxt + await nuxt.callHook('webpack:config', webpackConfigs) - const webpackConfigs = [ - this.getWebpackConfig('client') - ] + // Initialize shared MFS for dev + const mfs = nuxt.options.dev ? createMFS() : null - if (options.ssr) { - webpackConfigs.push(this.getWebpackConfig('server')) + // Configure compilers + const compilers = webpackConfigs.map((config) => { + config.plugins.push(DynamicBasePlugin.webpack({ + env: nuxt.options.dev ? 'dev' : config.name as 'client', + devAppConfig: nuxt.options.app, + globalPublicPath: '__webpack_public_path__' + })) + + // Create compiler + const compiler = webpack(config) + + // In dev, write files in memory FS + if (nuxt.options.dev) { + compiler.outputFileSystem = mfs } - await this.nuxt.callHook('webpack:config', webpackConfigs) + return compiler + }) - // Check styleResource existence - const { styleResources } = this.nuxt.options.build - if (styleResources && Object.keys(styleResources).length) { - logger.warn( - 'Using styleResources without the @nuxtjs/style-resources is not suggested and can lead to severe performance issues.', - 'Please use https://github.com/nuxt-community/style-resources-module' - ) - for (const ext of Object.keys(styleResources)) { - await Promise.all(Array.from(styleResources[ext]).map(async (p) => { - const styleResourceFiles = await glob(resolve(this.nuxt.options.rootDir, p as string)) - - if (!styleResourceFiles || styleResourceFiles.length === 0) { - throw new Error(`Style Resource not found: ${p}`) - } - })) - } - } - - // Configure compilers - this.compilers = webpackConfigs.map((config) => { - // Support virtual modules (input) - config.plugins.push(this.virtualModules) - config.plugins.push(DynamicBasePlugin.webpack({ - env: this.nuxt.options.dev ? 'dev' : config.name as 'client', - devAppConfig: this.nuxt.options.app, - globalPublicPath: '__webpack_public_path__' - })) - - // Create compiler - const compiler = webpack(config) - - // In dev, write files in memory FS - if (options.dev) { - compiler.outputFileSystem = this.mfs! - } - - return compiler - }) - - // Start Builds - if (options.dev) { - return Promise.all(this.compilers.map(c => this.webpackCompile(c))) - } else { - for (const c of this.compilers) { - await this.webpackCompile(c) - } - } - } - - async webpackCompile (compiler) { - const { name } = compiler.options - const { options } = this.nuxt - - await this.nuxt.callHook('build:compile', { name, compiler }) - - // Load renderer resources after build - compiler.hooks.done.tap('load-resources', async (stats) => { - await this.nuxt.callHook('build:compiled', { - name, - compiler, - stats - }) - - // Reload renderer - await this.nuxt.callHook('build:resources', this.mfs) - }) - - // --- Dev Build --- - if (options.dev) { - // Client build - if (['client', 'modern'].includes(name)) { - return new Promise((resolve, reject) => { - compiler.hooks.done.tap('nuxt-dev', () => { resolve(null) }) - compiler.hooks.failed.tap('nuxt-errorlog', (err) => { reject(err) }) - // Start watch - this.webpackDev(compiler) - }) - } - - // Server, build and watch for changes - return new Promise((resolve, reject) => { - const watching = compiler.watch(options.watchers.webpack, (err) => { - if (err) { - return reject(err) - } - resolve(null) - }) - - watching.closeAsync = pify(watching.close) - this.compilersWatching.push(watching) - }) - } - - // --- Production Build --- - compiler.run = pify(compiler.run) - const stats = await compiler.run() - - if (stats.hasErrors()) { - // non-quiet mode: errors will be printed by webpack itself - const error = new Error('Nuxt build error') - if (options.build.quiet === true) { - error.stack = stats.toString('errors-only') - } - throw error - } - - // Await for renderer to load resources (programmatic, tests and generate) - await this.nuxt.callHook('build:resources') - } - - async webpackDev (compiler: Compiler) { - logger.debug('Creating webpack middleware...') - - const { name } = compiler.options - const buildOptions = this.nuxt.options.build - const { client, ...hotMiddlewareOptions } = buildOptions.hotMiddleware || {} - - // Create webpack dev middleware - this.devMiddleware[name] = pify( - // @ts-ignore - webpackDevMiddleware( - // @ts-ignore - compiler, - { - publicPath: joinURL(this.nuxt.options.app.baseURL, this.nuxt.options.app.buildAssetsDir), - outputFileSystem: this.mfs, - stats: 'none', - ...buildOptions.devMiddleware - } as WebpackDevMiddlewareOptions - ) - ) - - this.devMiddleware[name].close = pify(this.devMiddleware[name].close) - - // @ts-ignore - this.compilersWatching.push(this.devMiddleware[name].context.watching) - - this.hotMiddleware[name] = pify( - webpackHotMiddleware( - compiler as any, - { - log: false, - heartbeat: 10000, - path: joinURL(this.nuxt.options.app.baseURL, '__webpack_hmr', name), - ...hotMiddlewareOptions - } as WebpackHotMiddlewareOptions - ) - ) - - // Register devMiddleware on server - await this.nuxt.callHook('server:devMiddleware', this.middleware) - } - - async middleware (req: IncomingMessage, res: ServerResponse, next: () => any) { - if (this.devMiddleware && this.devMiddleware.client) { - await this.devMiddleware.client(req, res) - } - - if (this.hotMiddleware && this.hotMiddleware.client) { - await this.hotMiddleware.client(req, res) - } - - next() - } - - async unwatch () { - await Promise.all(this.compilersWatching.map(watching => watching.closeAsync())) - } - - async close () { - if (this.__closed) { - return - } - this.__closed = true - - // Unwatch - await this.unwatch() - - // Stop webpack middleware - for (const devMiddleware of Object.values(this.devMiddleware)) { - await devMiddleware.close() - } - - for (const compiler of this.compilers) { + nuxt.hook('close', async () => { + for (const compiler of compilers) { await new Promise(resolve => compiler.close(resolve)) } + }) - // Cleanup MFS - if (this.mfs) { - delete this.mfs + // Start Builds + if (nuxt.options.dev) { + return Promise.all(compilers.map(c => compile(c))) + } + + for (const c of compilers) { + await compile(c) + } +} + +async function createDevMiddleware (compiler: Compiler) { + const nuxt = useNuxt() + + logger.debug('Creating webpack middleware...') + + // Create webpack dev middleware + const devMiddleware = pify(webpackDevMiddleware(compiler, { + publicPath: joinURL(nuxt.options.app.baseURL, nuxt.options.app.buildAssetsDir), + outputFileSystem: compiler.outputFileSystem as any, + stats: 'none', + ...nuxt.options.webpack.devMiddleware + })) as API + + nuxt.hook('close', () => pify(devMiddleware.close.bind(devMiddleware))()) + + const { client: _client, ...hotMiddlewareOptions } = nuxt.options.webpack.hotMiddleware || {} + const hotMiddleware = pify(webpackHotMiddleware(compiler, { + log: false, + heartbeat: 10000, + path: joinURL(nuxt.options.app.baseURL, '__webpack_hmr', compiler.options.name), + ...hotMiddlewareOptions + })) + + // Register devMiddleware on server + await nuxt.callHook('server:devMiddleware', async (req, res, next) => { + for (const mw of [devMiddleware, hotMiddleware]) { + await mw?.(req, res) + } + next() + }) + + return devMiddleware +} + +async function compile (compiler: Compiler) { + const nuxt = useNuxt() + + const { name } = compiler.options + + await nuxt.callHook('build:compile', { name, compiler }) + + // Load renderer resources after build + compiler.hooks.done.tap('load-resources', async (stats) => { + await nuxt.callHook('build:compiled', { name, compiler, stats }) + // Reload renderer + await nuxt.callHook('build:resources', compiler.outputFileSystem) + }) + + // --- Dev Build --- + if (nuxt.options.dev) { + const compilersWatching: Watching[] = [] + + nuxt.hook('close', async () => { + await Promise.all(compilersWatching.map(watching => pify(watching.close.bind(watching))())) + }) + + // Client build + if (name === 'client') { + return new Promise((resolve, reject) => { + compiler.hooks.done.tap('nuxt-dev', () => { resolve(null) }) + compiler.hooks.failed.tap('nuxt-errorlog', (err) => { reject(err) }) + // Start watch + createDevMiddleware(compiler).then((devMiddleware) => { + compilersWatching.push(devMiddleware.context.watching) + }) + }) } - // Cleanup more resources - delete this.compilers - delete this.compilersWatching - delete this.devMiddleware - delete this.hotMiddleware + // Server, build and watch for changes + return new Promise((resolve, reject) => { + const watching = compiler.watch(nuxt.options.watchers.webpack, (err) => { + if (err) { return reject(err) } + resolve(null) + }) + + compilersWatching.push(watching) + }) } - forGenerate () { - this.nuxt.options.target = 'static' - } -} + // --- Production Build --- + const stats = await new Promise((resolve, reject) => compiler.run((err, stats) => err ? reject(err) : resolve(stats))) -export function bundle (nuxt: Nuxt) { - const bundler = new WebpackBundler(nuxt) - return bundler.build() + if (stats.hasErrors()) { + // non-quiet mode: errors will be printed by webpack itself + const error = new Error('Nuxt build error') + if (nuxt.options.build.quiet === true) { + error.stack = stats.toString('errors-only') + } + throw error + } + + // Await for renderer to load resources (programmatic, tests and generate) + await nuxt.callHook('build:resources') } diff --git a/test/fixtures/basic/nuxt.config.ts b/test/fixtures/basic/nuxt.config.ts index b7ec5f1063..059d1a7041 100644 --- a/test/fixtures/basic/nuxt.config.ts +++ b/test/fixtures/basic/nuxt.config.ts @@ -3,7 +3,7 @@ import { addComponent } from '@nuxt/kit' export default defineNuxtConfig({ buildDir: process.env.NITRO_BUILD_DIR, - vite: !process.env.TEST_WITH_WEBPACK, + builder: process.env.TEST_WITH_WEBPACK ? 'webpack' : 'vite', nitro: { output: { dir: process.env.NITRO_OUTPUT_DIR } }, diff --git a/yarn.lock b/yarn.lock index 73a3d35b74..971f01dd9e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3163,6 +3163,7 @@ __metadata: defu: ^5.0.1 jiti: ^1.13.0 pathe: ^0.2.0 + postcss-import-resolver: ^2.0.0 scule: ^0.2.1 std-env: ^3.0.1 ufo: ^0.7.9 @@ -3393,7 +3394,6 @@ __metadata: p-debounce: ^4.0.0 pathe: ^0.2.0 postcss-import: ^14.0.2 - postcss-import-resolver: ^2.0.0 postcss-url: ^10.1.3 rollup-plugin-visualizer: ^5.6.0 ufo: ^0.7.9 @@ -3480,7 +3480,7 @@ __metadata: languageName: node linkType: hard -"@nuxt/webpack-builder@3.0.0, @nuxt/webpack-builder@workspace:packages/webpack": +"@nuxt/webpack-builder@workspace:packages/webpack": version: 0.0.0-use.local resolution: "@nuxt/webpack-builder@workspace:packages/webpack" dependencies: @@ -3489,14 +3489,11 @@ __metadata: "@nuxt/kit": 3.0.0 "@nuxt/schema": 3.0.0 "@types/pify": ^5.0.1 - "@types/terser-webpack-plugin": ^5.0.4 "@types/webpack-bundle-analyzer": ^4.4.1 "@types/webpack-dev-middleware": ^5.0.2 "@types/webpack-hot-middleware": ^2.25.6 "@types/webpack-virtual-modules": ^0 - "@vue/babel-preset-jsx": ^1.2.4 autoprefixer: ^10.4.2 - babel-loader: ^8.2.3 css-loader: ^6.6.0 css-minimizer-webpack-plugin: ^3.4.1 cssnano: ^5.0.17 @@ -3504,7 +3501,6 @@ __metadata: escape-string-regexp: ^5.0.0 file-loader: ^6.2.0 fs-extra: ^10.0.1 - glob: ^7.2.0 hash-sum: ^2.0.0 lodash-es: ^4.17.21 memfs: ^3.4.1 @@ -3514,7 +3510,6 @@ __metadata: pify: ^5.0.0 postcss: ^8.4.7 postcss-import: ^14.0.2 - postcss-import-resolver: ^2.0.0 postcss-loader: ^6.2.1 postcss-url: ^10.1.3 style-resources-loader: ^1.5.0 @@ -4630,16 +4625,6 @@ __metadata: languageName: node linkType: hard -"@types/terser-webpack-plugin@npm:^5.0.4": - version: 5.0.4 - resolution: "@types/terser-webpack-plugin@npm:5.0.4" - dependencies: - terser: ^5.3.8 - webpack: ^5.1.0 - checksum: 7b3845526d4ed7b636f7b2f8f35b5d7e45211f7a55afc85e32a66655eaf65cd193e2fec195bcdb473c0d9a5b2828798b9c23196fdc86e9cd2e9420227012f438 - languageName: node - linkType: hard - "@types/throttle-debounce@npm:^2.1.0": version: 2.1.0 resolution: "@types/throttle-debounce@npm:2.1.0" @@ -15675,7 +15660,6 @@ __metadata: "@nuxt/nitro": 3.0.0 "@nuxt/schema": 3.0.0 "@nuxt/vite-builder": 3.0.0 - "@nuxt/webpack-builder": 3.0.0 "@types/fs-extra": ^9.0.13 "@types/hash-sum": ^1.0.0 "@vue/reactivity": ^3.2.31 @@ -20771,7 +20755,7 @@ __metadata: languageName: node linkType: hard -"terser@npm:^5.0.0, terser@npm:^5.3.4, terser@npm:^5.3.8, terser@npm:^5.7.2": +"terser@npm:^5.0.0, terser@npm:^5.3.4, terser@npm:^5.7.2": version: 5.10.0 resolution: "terser@npm:5.10.0" dependencies: @@ -22553,44 +22537,7 @@ __metadata: languageName: node linkType: hard -"webpack@npm:^5, webpack@npm:^5.1.0, webpack@npm:^5.38.1": - version: 5.69.0 - resolution: "webpack@npm:5.69.0" - dependencies: - "@types/eslint-scope": ^3.7.3 - "@types/estree": ^0.0.51 - "@webassemblyjs/ast": 1.11.1 - "@webassemblyjs/wasm-edit": 1.11.1 - "@webassemblyjs/wasm-parser": 1.11.1 - acorn: ^8.4.1 - acorn-import-assertions: ^1.7.6 - browserslist: ^4.14.5 - chrome-trace-event: ^1.0.2 - enhanced-resolve: ^5.9.0 - es-module-lexer: ^0.9.0 - eslint-scope: 5.1.1 - events: ^3.2.0 - glob-to-regexp: ^0.4.1 - graceful-fs: ^4.2.9 - json-parse-better-errors: ^1.0.2 - loader-runner: ^4.2.0 - mime-types: ^2.1.27 - neo-async: ^2.6.2 - schema-utils: ^3.1.0 - tapable: ^2.1.1 - terser-webpack-plugin: ^5.1.3 - watchpack: ^2.3.1 - webpack-sources: ^3.2.3 - peerDependenciesMeta: - webpack-cli: - optional: true - bin: - webpack: bin/webpack.js - checksum: 0b0fc2ffcb926fc7506ec56901ae9a69f1ae969a6afc63395a3ed56f938d385c1d16d1dea39fd56dbb9362189c05da49174219904e5a337040aed5a46080e926 - languageName: node - linkType: hard - -"webpack@npm:^5.69.1": +"webpack@npm:^5, webpack@npm:^5.38.1, webpack@npm:^5.69.1": version: 5.69.1 resolution: "webpack@npm:5.69.1" dependencies: