UNPKG

@nuxt/webpack-edge

Version:
1,646 lines (1,628 loc) • 60.1 kB
/*! * @nuxt/webpack-edge v2.18.2-28661769.e265ef3 (c) 2016-2024 * Released under the MIT License * Repository: https://github.com/nuxt/nuxt.js * Website: https://nuxtjs.org */ 'use strict'; const upath = require('upath'); const pify = require('pify'); const webpack = require('webpack'); const Glob = require('glob'); const webpackDevMiddleware = require('webpack-dev-middleware'); const webpackHotMiddleware = require('webpack-hot-middleware'); const consola = require('consola'); const utilsEdge = require('@nuxt/utils-edge'); const path = require('path'); const mkdirp = require('mkdirp'); const memfs = require('memfs'); const querystring = require('querystring'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const BundleAnalyzer = require('webpack-bundle-analyzer'); const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); const FriendlyErrorsWebpackPlugin = require('@nuxt/friendly-errors-webpack-plugin'); const EventEmitter = require('events'); const hash = require('hash-sum'); const lodash = require('lodash'); const TimeFixPlugin = require('time-fix-plugin'); const VueLoader = require('vue-loader'); const ExtractCssChunksPlugin = require('extract-css-chunks-webpack-plugin'); const PnpWebpackPlugin = require('pnp-webpack-plugin'); const HardSourcePlugin = require('hard-source-webpack-plugin'); const TerserWebpackPlugin = require('terser-webpack-plugin'); const WebpackBar = require('webpackbar'); const stdEnv = require('std-env'); const semver = require('semver'); const ufo = require('ufo'); const threadLoader = require('thread-loader'); const fs = require('fs'); const createResolver = require('postcss-import-resolver'); const Watchpack = require('watchpack'); const objectToMap = require('webpack/lib/util/objectToMap'); const nodeExternals = require('webpack-node-externals'); function _interopNamespaceDefault(e) { const n = Object.create(null); if (e) { for (const k in e) { if (k !== 'default') { const d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } } } n.default = e; return Object.freeze(n); } const PnpWebpackPlugin__namespace = /*#__PURE__*/_interopNamespaceDefault(PnpWebpackPlugin); const syncRegex = /Sync$/; function createFS() { const volume = new memfs.Volume(); const fs = memfs.createFsFromVolume(volume); fs.join = path.join.bind(path); fs.mkdirp = mkdirp.bind(mkdirp); const propsToPromisify = Object.getOwnPropertyNames(fs).filter((n) => syncRegex.test(n)); const writeFileSync = fs.writeFileSync; function ensureDirSync(...args) { if (typeof args[0] === "string") { const dir = upath.dirname(args[0]); fs.mkdirSync(dir, { recursive: true }); } } fs.writeFileSync = function(...args) { ensureDirSync(...args); return writeFileSync.call(fs, ...args); }; for (const prop of propsToPromisify) { const asyncProp = prop.replace(syncRegex, ""); const origAsync = fs[asyncProp]; fs[asyncProp] = function(...args) { if (asyncProp === "writeFile") { ensureDirSync(...args); } if (origAsync && args.length && typeof args[args.length - 1] === "function") { return origAsync.call(fs, ...args); } try { return Promise.resolve(fs[prop](...args)); } catch (error) { return Promise.reject(error); } }; } return fs; } class CorsPlugin { constructor({ crossorigin }) { this.crossorigin = crossorigin; } apply(compiler) { const ID = "vue-cors-plugin"; compiler.hooks.compilation.tap(ID, (compilation) => { HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tap(ID, (data) => { if (!this.crossorigin) { return; } [...data.headTags, ...data.bodyTags].forEach((tag) => { if (["script", "link"].includes(tag.tagName)) { if (tag.attributes) { tag.attributes.crossorigin = this.crossorigin; } } }); }); }); } } const legacyTemplateTags = {}; const legacyTemplateWatcher = new EventEmitter(); class ModernModePlugin { constructor({ targetDir, isModernBuild, noUnsafeInline }) { this.targetDir = targetDir; this.isModernBuild = isModernBuild; this.noUnsafeInline = noUnsafeInline; } apply(compiler) { if (!this.isModernBuild) { this.applyLegacy(compiler); } else { this.applyModern(compiler); } } getLegacyTemplateTags(name) { return new Promise((resolve) => { const tags = legacyTemplateTags[name]; if (tags) { return resolve(tags); } return legacyTemplateWatcher.once(name, () => { const tags2 = legacyTemplateTags[name]; return tags2 && resolve(tags2); }); }); } applyLegacy(compiler) { const ID = "nuxt-legacy-bundle"; compiler.hooks.compilation.tap(ID, (compilation) => { HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tap(ID, (data) => { HtmlWebpackPlugin.getHooks(compilation).afterEmit.tap(ID, ({ outputName }) => { legacyTemplateTags[data.plugin.options.filename] = data.bodyTags; legacyTemplateWatcher.emit(outputName); }); return data; }); }); } applyModern(compiler) { const ID = "nuxt-modern-bundle"; compiler.hooks.compilation.tap(ID, (compilation) => { HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapPromise(ID, async (data) => { data.bodyTags.forEach((tag) => { if (tag.tagName === "script" && tag.attributes) { tag.attributes.type = "module"; } }); data.headTags.forEach((tag) => { if (tag.tagName === "link" && tag.attributes.rel === "preload" && tag.attributes.as === "script") { tag.attributes.rel = "modulepreload"; } }); const fileName = data.plugin.options.filename; const legacyScriptTags = (await this.getLegacyTemplateTags(fileName)).filter((a) => a.tagName === "script" && a.attributes); for (const a of legacyScriptTags) { a.attributes.nomodule = true; data.bodyTags.push(a); } if (this.noUnsafeInline) { const safariFixFilename = "safari-nomodule-fix.js"; const safariFixPath = legacyScriptTags[0].attributes.src.split("/").slice(0, -1).concat([safariFixFilename]).join("/"); compilation.assets[safariFixFilename] = { source: () => Buffer.from(utilsEdge.safariNoModuleFix), size: () => Buffer.byteLength(utilsEdge.safariNoModuleFix) }; data.bodyTags.push({ tagName: "script", closeTag: true, attributes: { src: safariFixPath } }); } else { data.bodyTags.push({ tagName: "script", closeTag: true, innerHTML: utilsEdge.safariNoModuleFix }); } delete legacyTemplateTags[fileName]; return data; }); }); } } const validate = (compiler) => { if (compiler.options.target !== "node") { consola.warn('webpack config `target` should be "node".'); } if (compiler.options.output && compiler.options.output.libraryTarget !== "commonjs2") { consola.warn('webpack config `output.libraryTarget` should be "commonjs2".'); } if (!compiler.options.externals) { consola.info( "It is recommended to externalize dependencies in the server build for better build performance." ); } }; const isJSRegExp = /\.js(\?[^.]+)?$/; const isJS = (file) => isJSRegExp.test(file); const extractQueryPartJS = (file) => isJSRegExp.exec(file)[1]; const isCSS = (file) => /\.css(\?[^.]+)?$/.test(file); class VueSSRClientPlugin { constructor(options = {}) { this.options = Object.assign({ filename: null }, options); } apply(compiler) { compiler.hooks.emit.tapAsync("vue-client-plugin", (compilation, cb) => { const stats = compilation.getStats().toJson(); const allFiles = lodash.uniq(stats.assets.map((a) => a.name)); const initialFiles = lodash.uniq(Object.keys(stats.entrypoints).map((name) => stats.entrypoints[name].assets).reduce((assets, all) => all.concat(assets), []).filter((file) => isJS(file) || isCSS(file))); const asyncFiles = allFiles.filter((file) => isJS(file) || isCSS(file)).filter((file) => !initialFiles.includes(file)); const assetsMapping = {}; stats.assets.filter(({ name }) => isJS(name)).forEach(({ name, chunkNames }) => { const componentHash = hash(chunkNames.join("|")); if (!assetsMapping[componentHash]) { assetsMapping[componentHash] = []; } assetsMapping[componentHash].push(name); }); const manifest = { publicPath: stats.publicPath, all: allFiles, initial: initialFiles, async: asyncFiles, modules: { /* [identifier: string]: Array<index: number> */ }, assetsMapping }; const { entrypoints, namedChunkGroups } = stats; const assetModules = stats.modules.filter((m) => m.assets.length); const fileToIndex = (file) => manifest.all.indexOf(file); stats.modules.forEach((m) => { if (m.chunks.length === 1) { const [cid] = m.chunks; const chunk = stats.chunks.find((c) => c.id === cid); if (!chunk || !chunk.files) { return; } const id = m.identifier.replace(/\s\w+$/, ""); const filesSet = new Set(chunk.files.map(fileToIndex)); for (const chunkName of chunk.names) { if (!entrypoints[chunkName]) { const chunkGroup = namedChunkGroups[chunkName]; if (chunkGroup) { for (const asset of chunkGroup.assets) { filesSet.add(fileToIndex(asset)); } } } } const files = Array.from(filesSet); manifest.modules[hash(id)] = files; if (Array.isArray(m.modules)) { for (const concatenatedModule of m.modules) { const id2 = hash(concatenatedModule.identifier.replace(/\s\w+$/, "")); if (!manifest.modules[id2]) { manifest.modules[id2] = files; } } } assetModules.forEach((m2) => { if (m2.chunks.includes(cid)) { files.push.apply(files, m2.assets.map(fileToIndex)); } }); } }); const src = JSON.stringify(manifest, null, 2); compilation.assets[this.options.filename] = { source: () => src, size: () => src.length }; cb(); }); } } class PerfLoader { constructor(name, buildContext, { resolveModule }) { this.name = name; this.buildContext = buildContext; this.workerPools = PerfLoader.defaultPools({ dev: buildContext.options.dev }); this.resolveModule = resolveModule; return new Proxy(this, { get(target, name2) { return target[name2] ? target[name2] : target.use.bind(target, name2); } }); } static defaultPools({ dev }) { const poolTimeout = dev ? Infinity : 2e3; return { js: { name: "js", poolTimeout }, css: { name: "css", poolTimeout } }; } static warmupAll({ dev, resolveModule }) { const pools = PerfLoader.defaultPools({ dev }); PerfLoader.warmup(pools.js, [ resolveModule("babel-loader"), resolveModule("@babel/preset-env") ]); PerfLoader.warmup(pools.css, [resolveModule("css-loader")]); } static warmup(...args) { threadLoader.warmup(...args); } use(poolName) { const loaders = []; if (this.buildContext.buildOptions.cache) { loaders.push({ loader: this.resolveModule("cache-loader"), options: { cacheDirectory: path.resolve(`node_modules/.cache/cache-loader/${this.name}`) } }); } if (this.buildContext.buildOptions.parallel) { const pool = this.workerPools[poolName]; if (pool) { loaders.push({ loader: this.resolveModule("thread-loader"), options: pool }); } } return loaders; } } const orderPresets = { cssnanoLast(names) { const nanoIndex = names.indexOf("cssnano"); if (nanoIndex !== names.length - 1) { names.push(names.splice(nanoIndex, 1)[0]); } return names; }, presetEnvLast(names) { const nanoIndex = names.indexOf("postcss-preset-env"); if (nanoIndex !== names.length - 1) { names.push(names.splice(nanoIndex, 1)[0]); } return names; }, presetEnvAndCssnanoLast(names) { return orderPresets.cssnanoLast(orderPresets.presetEnvLast(names)); } }; function postcssConfigFileWarning() { if (postcssConfigFileWarning.executed) { return; } consola.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."); postcssConfigFileWarning.executed = true; } class PostcssConfig { constructor(buildContext) { this.buildContext = buildContext; } get cssSourceMap() { return this.buildContext.buildOptions.cssSourceMap; } get postcssLoaderOptions() { return this.buildContext.buildOptions.postcss; } get postcssOptions() { return this.buildContext.buildOptions.postcss.postcssOptions; } get postcssImportAlias() { const alias = { ...this.buildContext.options.alias }; for (const key in alias) { if (key.startsWith("~")) { continue; } const newKey = "~" + key; if (!alias[newKey]) { alias[newKey] = alias[key]; } } return alias; } /** * Returns the default PostCSS options used by Nuxt. * @returns {{ plugins: {"postcss-import": {resolve: Function}, "postcss-preset-env": {}, "postcss-url": {}, cssnano: (boolean|{preset: [string,{minifyFontValues: {removeQuotes: boolean}}]})}, order: string}} */ get defaultPostcssOptions() { const { dev, srcDir, rootDir, modulesDir } = this.buildContext.options; return { 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": {}, // https://github.com/csstools/postcss-plugins/tree/main/plugin-packs/postcss-preset-env "postcss-preset-env": {}, 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 } }] } }, // Array, String or Function order: "presetEnvAndCssnanoLast" }; } searchConfigFile() { const { srcDir, rootDir } = this.buildContext.options; for (const dir of [srcDir, rootDir]) { for (const file of [ "postcss.config.js", ".postcssrc.js", ".postcssrc", ".postcssrc.json", ".postcssrc.yaml" ]) { const configFile = path.resolve(dir, file); if (fs.existsSync(configFile)) { postcssConfigFileWarning(); return configFile; } } } } configFromFile() { const loaderConfig = this.postcssOptions && this.postcssOptions.config || {}; if (loaderConfig.path) { consola.warn("`postcss-loader` has been removed `config.path` option, please use `config` instead."); return { config: loaderConfig.path }; } const postcssConfigFile = this.searchConfigFile(); if (postcssConfigFile) { return { config: postcssConfigFile }; } } /** * Converts the old syntax to the one expected by Nuxt ^2.16. * - `Array` to `{ plugins: [] }` * - `Boolean` to `{ plugins: {} }` (default plugins) or `{ plugins: false }` (no plugins) * - Moves the `preset`, `order` and `plugins` to the inner `postcssOptions` object * - Does not convert an Array of plugins to an object * @param postcssOptions * @returns {{ postcssOptions: { plugins?: unknown, order?: string, preset?: any} }} */ normalize(postcssOptions, warnAboutTopLevelDeprecation = true) { if (Array.isArray(postcssOptions)) { consola.warn("Using an Array as `build.postcss` will be deprecated in Nuxt 3. Please switch to the object declaration"); return { postcssOptions: { plugins: postcssOptions } }; } else if (typeof postcssOptions === "boolean") { consola.warn("Using a Boolean as `build.postcss` will be deprecated in Nuxt 3. Please switch to the object declaration"); return { postcssOptions: { plugins: postcssOptions ? {} : false } }; } else if (!utilsEdge.isPureObject(postcssOptions)) { return { postcssOptions: {} }; } if (postcssOptions.postcssOptions && typeof postcssOptions.postcssOptions === "function") { const postcssOptionsFn = postcssOptions.postcssOptions; return { postcssOptions: (loaderContext) => { const result = this.normalize(postcssOptionsFn(loaderContext), false); if (result) { return result.postcssOptions; } } }; } if (!("postcssOptions" in postcssOptions)) { if (Object.keys(postcssOptions).length > 0 && warnAboutTopLevelDeprecation) { consola.warn("Using the top-level properties in `build.postcss` will be deprecated in Nuxt 3. Please move the settings to `postcss.postcssOptions`"); } postcssOptions = { postcssOptions }; } if (postcssOptions.plugins) { postcssOptions.postcssOptions.plugins = lodash.merge( postcssOptions.postcssOptions.plugins || {}, postcssOptions.plugins ); delete postcssOptions.plugins; } if (postcssOptions.preset) { postcssOptions.postcssOptions.preset = lodash.merge( postcssOptions.preset, postcssOptions.postcssOptions.preset || {} ); delete postcssOptions.preset; } if (postcssOptions.order) { postcssOptions.postcssOptions.order = postcssOptions.postcssOptions.order || postcssOptions.order; delete postcssOptions.order; } return postcssOptions; } sortPlugins({ plugins, order }) { const names = Object.keys(plugins); if (typeof order === "string") { order = orderPresets[order]; } return typeof order === "function" ? order(names, orderPresets) : order || names; } /** * Load plugins from postcssOptions * @param {{ postcssOptions: {plugins?: unknown, order?: string | function }}} postcssOptions */ loadPlugins(postcssOptions) { const { plugins, order } = postcssOptions.postcssOptions; if (utilsEdge.isPureObject(plugins)) { postcssOptions.postcssOptions.plugins = this.sortPlugins({ plugins, order }).map((p) => { const plugin = this.buildContext.nuxt.resolver.requireModule(p, { paths: [__dirname] }); const opts = plugins[p]; if (opts === false) { return false; } return plugin(opts); }).filter(Boolean); } } config() { if (!this.postcssLoaderOptions) { return false; } const postcssOptionsFromFile = this.configFromFile(); if (postcssOptionsFromFile) { return { postcssOptions: postcssOptionsFromFile, sourceMap: this.cssSourceMap }; } const postcssOptions = this.normalize(lodash.cloneDeep(this.postcssLoaderOptions)); if (Array.isArray(postcssOptions.postcssOptions.plugins)) { lodash.defaults(postcssOptions.postcssOptions.plugins, this.defaultPostcssOptions.plugins); } else if (typeof postcssOptions.postcssOptions !== "function") { if (postcssOptions.postcssOptions.preset) { if (!postcssOptions.postcssOptions.plugins) { postcssOptions.postcssOptions.plugins = {}; } postcssOptions.postcssOptions.plugins["postcss-preset-env"] = lodash.defaults( postcssOptions.postcssOptions.preset, this.defaultPostcssOptions.plugins["postcss-preset-env"] ); delete postcssOptions.postcssOptions.preset; } postcssOptions.postcssOptions = lodash.merge({}, this.defaultPostcssOptions, postcssOptions.postcssOptions); this.loadPlugins(postcssOptions); } delete postcssOptions.postcssOptions.order; return { sourceMap: this.cssSourceMap, ...postcssOptions }; } } class StyleLoader { constructor(buildContext, { isServer, perfLoader, resolveModule }) { this.buildContext = buildContext; this.isServer = isServer; this.perfLoader = perfLoader; this.resolveModule = resolveModule; const { postcss } = buildContext.options.build; if (postcss) { this.postcssConfig = new PostcssConfig(buildContext); } } get extractCSS() { return this.buildContext.buildOptions.extractCSS; } get exportOnlyLocals() { return Boolean(this.isServer && this.extractCSS); } isUrlResolvingEnabled(url, resourcePath) { return !url.startsWith("/"); } normalize(loaders) { loaders = utilsEdge.wrapArray(loaders); return loaders.map((loader) => typeof loader === "string" ? { loader } : loader); } styleResource(ext) { const { buildOptions: { styleResources }, options: { rootDir } } = this.buildContext; const extResource = styleResources[ext]; if (!extResource) { return; } const patterns = utilsEdge.wrapArray(extResource).map((p) => path.resolve(rootDir, p)); return { loader: this.resolveModule("style-resources-loader"), options: Object.assign( { patterns }, styleResources.options || {} ) }; } postcss() { if (!this.postcssConfig) { return; } const config = this.postcssConfig.config(); if (!config) { return; } return { loader: this.resolveModule("postcss-loader"), options: Object.assign({ sourceMap: this.buildContext.buildOptions.cssSourceMap }, config) }; } css(options) { const cssLoader = { loader: this.resolveModule("css-loader"), options }; if (!options.url) { options.url = this.isUrlResolvingEnabled; } if (this.exportOnlyLocals) { options.modules = { ...options.modules, exportOnlyLocals: true }; return [cssLoader]; } return [this.styleLoader(), cssLoader]; } cssModules(options) { return this.css(options); } extract() { if (this.extractCSS) { const isDev = this.buildContext.options.dev; return { loader: ExtractCssChunksPlugin.loader, options: { // TODO: https://github.com/faceyspacey/extract-css-chunks-webpack-plugin/issues/132 // https://github.com/faceyspacey/extract-css-chunks-webpack-plugin/issues/161#issuecomment-500162574 reloadAll: isDev, hmr: isDev } }; } } styleLoader() { return this.extract() || { loader: this.resolveModule("vue-style-loader"), options: this.buildContext.buildOptions.loaders.vueStyle }; } apply(ext, loaders = []) { const { css, cssModules } = this.buildContext.buildOptions.loaders; const customLoaders = [].concat( this.postcss(), this.normalize(loaders), this.styleResource(ext) ).filter(Boolean); css.importLoaders = cssModules.importLoaders = customLoaders.length; return [ // This matches <style module> { resourceQuery: /module/, use: this.perfLoader.css().concat( this.cssModules(cssModules), customLoaders ) }, // This matches plain <style> or <style scoped> { use: this.perfLoader.css().concat( this.css(css), customLoaders ) } ]; } } class WarningIgnorePlugin { constructor(filter) { this.filter = filter; } apply(compiler) { compiler.hooks.done.tap("warnfix-plugin", (stats) => { stats.compilation.warnings = stats.compilation.warnings.filter(this.filter); }); } } class Watchpack2Plugin { apply(compiler) { if (compiler.watchFileSystem && compiler.watchFileSystem.watcher) { compiler.watchFileSystem.watcher.close(); } compiler.watchFileSystem = new NodeWatchFileSystem( compiler.inputFileSystem ); } } class NodeWatchFileSystem { constructor(inputFileSystem) { this.inputFileSystem = inputFileSystem; this.watcherOptions = { aggregateTimeout: 0 }; this.watcher = new Watchpack(this.watcherOptions); } watch(files, dirs, missing, startTime, options, callback, callbackUndelayed) { if (!Array.isArray(files)) { throw new TypeError("Invalid arguments: 'files'"); } if (!Array.isArray(dirs)) { throw new TypeError("Invalid arguments: 'dirs'"); } if (!Array.isArray(missing)) { throw new TypeError("Invalid arguments: 'missing'"); } if (typeof callback !== "function") { throw new TypeError("Invalid arguments: 'callback'"); } if (typeof startTime !== "number" && startTime) { throw new Error("Invalid arguments: 'startTime'"); } if (typeof options !== "object") { throw new TypeError("Invalid arguments: 'options'"); } if (typeof callbackUndelayed !== "function" && callbackUndelayed) { throw new Error("Invalid arguments: 'callbackUndelayed'"); } const oldWatcher = this.watcher; this.watcher = new Watchpack(options); if (callbackUndelayed) { this.watcher.once("change", callbackUndelayed); } const cachedFiles = files; const cachedDirs = dirs; this.watcher.once("aggregated", (_changes, _removals) => { const removals = Array.from(_removals); const changes = Array.from(_changes).concat(removals); if (this.inputFileSystem && this.inputFileSystem.purge) { this.inputFileSystem.purge(changes); } const times = objectToMap(this.watcher.getTimes()); files = new Set(files); dirs = new Set(dirs); missing = new Set(missing); callback( null, changes.filter((file) => files.has(file)).sort(), changes.filter((file) => dirs.has(file)).sort(), changes.filter((file) => missing.has(file)).sort(), times, times, new Set(removals.filter((file) => files.has(file))) ); }); this.watcher.watch( cachedFiles.concat(missing), cachedDirs.concat(missing), startTime ); if (oldWatcher) { oldWatcher.close(); } return { close: () => { if (this.watcher) { this.watcher.close(); this.watcher = null; } }, pause: () => { if (this.watcher) { this.watcher.pause(); } }, getFileTimestamps: () => { if (this.watcher) { return objectToMap(this.watcher.getTimes()); } else { return /* @__PURE__ */ new Map(); } }, getContextTimestamps: () => { if (this.watcher) { return objectToMap(this.watcher.getTimes()); } else { return /* @__PURE__ */ new Map(); } } }; } } 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" ]; class WebpackBaseConfig { constructor(builder) { this.builder = builder; this.buildContext = builder.buildContext; this.resolveModule = (id) => utilsEdge.tryResolve(id, [this.buildContext.options.rootDir, __dirname]) || id; } get colors() { return { client: "green", server: "orange", modern: "blue" }; } get nuxtEnv() { return { isDev: this.dev, isServer: this.isServer, isClient: !this.isServer, isModern: Boolean(this.isModern), isLegacy: Boolean(!this.isModern) }; } get mode() { return this.dev ? "development" : "production"; } get target() { return this.buildContext.target; } get dev() { return this.buildContext.options.dev; } get loaders() { if (!this._loaders) { this._loaders = lodash.cloneDeep(this.buildContext.buildOptions.loaders); const sassLoaderPKG = utilsEdge.getPKG("sass-loader"); if (sassLoaderPKG && semver.lt(sassLoaderPKG.version, "8.0.0")) { const { sass } = this._loaders; sass.indentedSyntax = sass.sassOptions.indentedSyntax; delete sass.sassOptions.indentedSyntax; } } return this._loaders; } get modulesToTranspile() { return [ /\.vue\.js/i, // include SFCs in node_modules /consola\/src/, /ufo/, // exports modern syntax for browser field ...this.normalizeTranspile({ pathNormalize: true }) ]; } normalizeTranspile({ pathNormalize = false } = {}) { const transpile = []; for (let pattern of this.buildContext.buildOptions.transpile) { if (typeof pattern === "function") { pattern = pattern(this.nuxtEnv); } if (pattern instanceof RegExp) { transpile.push(pattern); } else if (typeof pattern === "string") { const posixModule = pattern.replace(/\\/g, "/"); transpile.push(new RegExp(lodash.escapeRegExp( pathNormalize ? path.normalize(posixModule) : posixModule ))); } } return transpile; } getBabelOptions() { const envName = this.name; const { buildOptions: { corejs }, options: { rootDir } } = this.buildContext; const options = { ...this.buildContext.buildOptions.babel, envName }; if (options.configFile || options.babelrc) { return options; } if (typeof options.plugins === "function") { options.plugins = options.plugins( { envName, ...this.nuxtEnv } ); } let corejsVersion = corejs; if (corejsVersion === "auto") { try { corejsVersion = Number.parseInt(utilsEdge.requireModule("core-js/package.json", rootDir).version.split(".")[0]); } catch (_err) { corejsVersion = 3; } } else { corejsVersion = Number.parseInt(corejsVersion); } if (![2, 3].includes(corejsVersion)) { consola.warn(`Invalid corejs version ${corejsVersion}! Please set "build.corejs" to either "auto", 2 or 3.`); corejsVersion = 3; } const defaultPreset = [this.resolveModule("@nuxt/babel-preset-app-edge"), { corejs: { version: corejsVersion } }]; if (typeof options.presets === "function") { options.presets = options.presets( { envName, ...this.nuxtEnv }, defaultPreset ); } if (!options.presets) { options.presets = [defaultPreset]; } return options; } getFileName(key) { let fileName = this.buildContext.buildOptions.filenames[key]; if (typeof fileName === "function") { fileName = fileName(this.nuxtEnv); } if (this.dev) { const hash = /\[(chunkhash|contenthash|hash)(?::(\d+))?]/.exec(fileName); if (hash) { consola.warn(`Notice: Please do not use ${hash[1]} in dev mode to prevent memory leak`); } } if (this.buildContext.buildOptions.analyze && !fileName.includes("[name]")) { fileName = "[name]." + fileName; } return fileName; } env() { const env = { "process.env.NODE_ENV": JSON.stringify(this.mode), "process.mode": JSON.stringify(this.mode), "process.dev": this.dev, "process.static": this.target === utilsEdge.TARGETS.static, "process.target": JSON.stringify(this.target) }; if (this.buildContext.buildOptions.aggressiveCodeRemoval) { env["typeof process"] = JSON.stringify(this.isServer ? "object" : "undefined"); env["typeof window"] = JSON.stringify(!this.isServer ? "object" : "undefined"); env["typeof document"] = JSON.stringify(!this.isServer ? "object" : "undefined"); } Object.entries(this.buildContext.options.env).forEach(([key, value]) => { env["process.env." + key] = ["boolean", "number"].includes(typeof value) ? value : JSON.stringify(value); }); return env; } output() { const { options: { buildDir, router }, buildOptions: { publicPath } } = this.buildContext; return { path: path.resolve(buildDir, "dist", this.isServer ? "server" : "client"), filename: this.getFileName("app"), futureEmitAssets: true, // TODO: Remove when using webpack 5 chunkFilename: this.getFileName("chunk"), publicPath: utilsEdge.isUrl(publicPath) ? publicPath : ufo.isRelative(publicPath) ? publicPath.replace(/^\.+\//, "/") : utilsEdge.urlJoin(router.base, publicPath) }; } optimization() { const optimization = lodash.cloneDeep(this.buildContext.buildOptions.optimization); if (optimization.minimize && optimization.minimizer === void 0) { optimization.minimizer = this.minimizer(); } return optimization; } resolve() { const webpackModulesDir = ["node_modules"].concat(this.buildContext.options.modulesDir); const resolvePath = [ ...global.__NUXT_PREPATHS__ || [], this.buildContext.options.rootDir, __dirname, ...global.__NUXT_PATHS__ || [], utilsEdge.resolveModule("@nuxt/vue-app-edge"), utilsEdge.resolveModule("@nuxt/babel-preset-app-edge") ]; const resolvePlugins = [PnpWebpackPlugin__namespace].concat(resolvePath.map((p) => PnpWebpackPlugin__namespace.moduleLoader(p))); return { resolve: { extensions: [".mjs", ".cjs", ".js", ".json", ".vue", ".jsx", ".wasm"], alias: this.alias(), modules: webpackModulesDir, plugins: resolvePlugins }, resolveLoader: { modules: [ path.resolve(__dirname, "../node_modules"), ...webpackModulesDir ], plugins: resolvePlugins } }; } minimizer() { const minimizer = []; const { terser, cache } = this.buildContext.buildOptions; if (terser) { minimizer.push( new TerserWebpackPlugin(Object.assign({ cache, extractComments: { condition: "some", filename: "LICENSES" }, terserOptions: { compress: { ecma: this.isModern ? 2015 : void 0 }, mangle: { reserved: reservedVueTags } } }, terser)) ); } return minimizer; } alias() { return { ...this.buildContext.options.alias, "vue-meta": this.resolveModule(`vue-meta${this.isServer ? "" : "/dist/vue-meta.esm.browser.js"}`) }; } rules() { const perfLoader = new PerfLoader(this.name, this.buildContext, { resolveModule: this.resolveModule }); const styleLoader = new StyleLoader( this.buildContext, { isServer: this.isServer, perfLoader, resolveModule: this.resolveModule } ); const babelLoader = { loader: this.resolveModule("babel-loader"), options: this.getBabelOptions() }; return [ { test: /\.vue$/i, loader: this.resolveModule("vue-loader"), options: this.loaders.vue }, { test: /\.pug$/i, oneOf: [ { resourceQuery: /^\?vue/i, use: [{ loader: this.resolveModule("pug-plain-loader"), options: this.loaders.pugPlain }] }, { use: [ this.resolveModule("raw-loader"), { loader: this.resolveModule("pug-plain-loader"), options: this.loaders.pugPlain } ] } ] }, { test: /\.(c|m)?jsx?$/i, type: "javascript/auto", exclude: (file) => { file = file.split(/node_modules(.*)/)[1]; if (!file) { return false; } return !this.modulesToTranspile.some((module) => module.test(file)); }, use: perfLoader.js().concat(babelLoader) }, { test: /\.css$/i, oneOf: styleLoader.apply("css") }, { test: /\.p(ost)?css$/i, oneOf: styleLoader.apply("postcss") }, { test: /\.less$/i, oneOf: styleLoader.apply("less", { loader: this.resolveModule("less-loader"), options: this.loaders.less }) }, { test: /\.sass$/i, oneOf: styleLoader.apply("sass", { loader: this.resolveModule("sass-loader"), options: this.loaders.sass }) }, { test: /\.scss$/i, oneOf: styleLoader.apply("scss", { loader: this.resolveModule("sass-loader"), options: this.loaders.scss }) }, { test: /\.styl(us)?$/i, oneOf: styleLoader.apply("stylus", { loader: this.resolveModule("stylus-loader"), options: this.loaders.stylus }) }, { test: /\.(png|jpe?g|gif|svg|webp|avif)$/i, use: [{ loader: this.resolveModule("url-loader"), options: Object.assign( this.loaders.imgUrl, { name: this.getFileName("img") } ) }] }, { test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i, use: [{ loader: this.resolveModule("url-loader"), options: Object.assign( this.loaders.fontUrl, { name: this.getFileName("font") } ) }] }, { test: /\.(webm|mp4|ogv)$/i, use: [{ loader: this.resolveModule("file-loader"), options: Object.assign( this.loaders.file, { name: this.getFileName("video") } ) }] } ]; } plugins() { const plugins = []; const { nuxt, buildOptions } = this.buildContext; if (this.dev) { plugins.push(new TimeFixPlugin()); } if (buildOptions.extractCSS) { plugins.push(new ExtractCssChunksPlugin(Object.assign({ filename: this.getFileName("css"), chunkFilename: this.getFileName("css") }, buildOptions.extractCSS))); } plugins.push(new VueLoader.VueLoaderPlugin()); plugins.push(...buildOptions.plugins || []); plugins.push(new WarningIgnorePlugin(this.warningIgnoreFilter())); plugins.push(new WebpackBar({ name: this.name, color: this.colors[this.name], reporters: [ "basic", "fancy", "profile", "stats" ], basic: !buildOptions.quiet && stdEnv.isMinimal, fancy: !buildOptions.quiet && !stdEnv.isMinimal, profile: !buildOptions.quiet && buildOptions.profile, stats: !buildOptions.quiet && !this.dev && buildOptions.stats, reporter: { change: (_, { shortPath }) => { if (!this.isServer) { nuxt.callHook("bundler:change", shortPath); } }, done: (buildContext) => { if (buildContext.hasErrors) { nuxt.callHook("bundler:error"); } }, allDone: () => { nuxt.callHook("bundler:done"); }, progress({ statesArray }) { nuxt.callHook("bundler:progress", statesArray); } } })); if (buildOptions.hardSource) { plugins.push(new HardSourcePlugin({ info: { level: "warn" }, ...buildOptions.hardSource })); } plugins.push(new Watchpack2Plugin()); return plugins; } warningIgnoreFilter() { const filters = [ // Hide warnings about plugins without a default export (#1179) (warn) => warn.name === "ModuleDependencyWarning" && warn.message.includes("export 'default'") && warn.message.includes("nuxt_plugin_"), ...this.buildContext.buildOptions.warningIgnoreFilters || [] ]; return (warn) => !filters.some((ignoreFilter) => ignoreFilter(warn)); } extendConfig(config) { const { extend } = this.buildContext.buildOptions; if (typeof extend === "function") { const extendedConfig = extend.call( this.builder, config, { loaders: this.loaders, ...this.nuxtEnv } ) || config; const pragma = /@|#/; const { devtool } = extendedConfig; if (typeof devtool === "string" && pragma.test(devtool)) { extendedConfig.devtool = devtool.replace(pragma, ""); consola.warn(`devtool has been normalized to ${extendedConfig.devtool} as webpack documented value`); } return extendedConfig; } return config; } config() { const config = { name: this.name, mode: this.mode, devtool: this.devtool, optimization: this.optimization(), output: this.output(), performance: { maxEntrypointSize: 1e3 * 1024, hints: this.dev ? false : "warning" }, module: { rules: this.rules() }, plugins: this.plugins(), ...this.resolve() }; const extendedConfig = lodash.cloneDeep(this.extendConfig(config)); return extendedConfig; } } class WebpackClientConfig extends WebpackBaseConfig { constructor(builder) { super(builder); this.name = "client"; this.isServer = false; this.isModern = false; } get devtool() { if (!this.dev) { return false; } const scriptPolicy = this.getCspScriptPolicy(); const noUnsafeEval = scriptPolicy && !scriptPolicy.includes("'unsafe-eval'"); return noUnsafeEval ? "cheap-module-source-map" : "cheap-module-eval-source-map"; } getCspScriptPolicy() { const { csp } = this.buildContext.options.render; if (csp) { const { policies = {} } = csp; return policies["script-src"] || policies["default-src"] || []; } } env() { return Object.assign( super.env(), { "process.env.VUE_ENV": JSON.stringify("client"), "process.browser": true, "process.client": true, "process.server": false, "process.modern": false } ); } optimization() { const optimization = super.optimization(); const { splitChunks } = optimization; const { cacheGroups } = splitChunks; if (this.buildContext.buildOptions.splitChunks.commons === true && cacheGroups.commons === void 0) { cacheGroups.commons = { test: /node_modules[\\/](vue|vue-loader|vue-router|vuex|vue-meta|core-js|@babel\/runtime|axios|webpack|setimmediate|timers-browserify|process|regenerator-runtime|cookie|js-cookie|is-buffer|dotprop|url-polyfill|@nuxt[\\/]ufo|ufo|nuxt\.js)[\\/]/, chunks: "all", name: true, priority: 10 }; } return optimization; } minimizer() { const minimizer = super.minimizer(); const { optimizeCSS } = this.buildContext.buildOptions; if (optimizeCSS) { minimizer.push(new OptimizeCSSAssetsPlugin(Object.assign({}, optimizeCSS))); } return minimizer; } alias() { const aliases = super.alias(); for (const p of this.buildContext.plugins) { if (!aliases[p.name]) { aliases[p.name] = p.mode === "server" ? "./empty.js" : p.src; } } return aliases; } plugins() { const plugins = super.plugins(); const { buildOptions, options: { appTemplatePath, buildDir, modern, render } } = this.buildContext; if (buildOptions.ssr) { plugins.push( new HtmlWebpackPlugin({ filename: "../server/index.ssr.html", template: appTemplatePath, minify: buildOptions.html.minify, inject: false // Resources will be injected using bundleRenderer }) ); } plugins.push( new HtmlWebpackPlugin({ filename: "../server/index.spa.html", template: appTemplatePath, minify: buildOptions.html.minify, inject: true }), new VueSSRClientPlugin({ filename: `../server/${this.name}.manifest.json` }), new webpack.DefinePlugin(this.env()) ); if (this.dev) { plugins.push(new webpack.HotModuleReplacementPlugin()); } if (!this.dev && buildOptions.analyze) { const statsDir = path.resolve(buildDir, "stats"); plugins.push(new BundleAnalyzer.BundleAnalyzerPlugin(Object.assign({ analyzerMode: "static", defaultSizes: "gzip", generateStatsFile: true, openAnalyzer: !buildOptions.quiet, reportFilename: path.resolve(statsDir, `${this.name}.html`), statsFilename: path.resolve(statsDir, `${this.name}.json`) }, buildOptions.analyze))); } if (modern) { const scriptPolicy = this.getCspScriptPolicy(); const noUnsafeInline = scriptPolicy && !scriptPolicy.includes("'unsafe-inline'"); plugins.push(new ModernModePlugin({ targetDir: path.resolve(buildDir, "dist", "client"), isModernBuild: this.isModern, noUnsafeInline })); } if (render.crossorigin) { plugins.push(new CorsPlugin({ crossorigin: render.crossorigin })); } return plugins; } config() { const config = super.config(); const { options: { router, buildDir }, buildOptions: { hotMiddleware, quiet, friendlyErrors } } = this.buildContext; const { client = {} } = hotMiddleware || {}; const { ansiColors, overlayStyles, ...options } = client; const hotMiddlewareClientOptions = { reload: true, timeout: 3e4, ansiColors: JSON.stringify(ansiColors), overlayStyles: JSON.stringify(overlayStyles), path: `${router.base}/__webpack_hmr/${this.name}`.replace(/\/\//g, "/"), ...options, name: this.name }; const hotMiddlewareClientOptionsStr = querystring.stringify(hotMiddlewareClientOptions); config.entry = Object.assign({}, config.entry, { app: [path.resolve(buildDir, "client.js")] }); if (this.dev) { config.entry.app.unshift( // https://github.com/webpack-contrib/webpack-hot-middleware/issues/53#issuecomment-162823945 this.resolveModule("eventsource-polyfill"), // https://github.com/glenjamin/webpack-hot-middleware#config `${this.resolveModule("webpack-hot-middleware/client")}?${hotMiddlewareClientOptionsStr}` ); } if (this.dev && !quiet && friendlyErrors) { config.plugins.push( new FriendlyErrorsWebpackPlugin({ clearConsole: false, reporter: "consola", logLevel: "WARNING" }) ); } return config; } } class WebpackModernConfig extends WebpackClientConfig { constructor(...args) { super(...args); this.name = "modern"; this.isModern = true; } env() { return Object.assign(super.env(), { "process.modern": true }); } } class VueSSRServerPlugin { constructor(options = {}) { this.options = Object.assign({ filename: null }, options); } apply(compiler) { validate(compiler); compiler.hooks.emit.tapAsync("vue-server-plugin", (compilation, cb) => { const stats = compilation.getStats().toJson(); const [entryName] = Object.keys(stats.entrypoints); const entryInfo = stats.entrypoints[entryName]; if (!entryInfo) { return cb(); } const entryAssets = entryInfo.assets.filter(isJS); if (entryAssets.length > 1) { throw new Error( "Server-side bundle should have one single entry file. Avoid using CommonsChunkPlugin in the server config." ); } const [entry] = entryAssets; if (!entry || typeof entry !== "string") { throw new Error( `Entry "${entryName}" not found. Did you specify the correct entry option?` ); } const bundle = { entry, files: {}, maps: {} }; stats.assets.forEach((asset) => { if (is