// config that are specific to --target app const fs = require('fs') const path = require('path') // ensure the filename passed to html-webpack-plugin is a relative path // because it cannot correctly handle absolute paths function ensureRelative (outputDir, _path) { if (path.isAbsolute(_path)) { return path.relative(outputDir, _path) } else { return _path } } module.exports = (api, options) => { api.chainWebpack(webpackConfig => { // only apply when there's no alternative target if (process.env.VUE_CLI_BUILD_TARGET && process.env.VUE_CLI_BUILD_TARGET !== 'app') { return } const isProd = process.env.NODE_ENV === 'production' const isLegacyBundle = process.env.VUE_CLI_MODERN_MODE && !process.env.VUE_CLI_MODERN_BUILD const outputDir = api.resolve(options.outputDir) const getAssetPath = require('../util/getAssetPath') const outputFilename = getAssetPath( options, `js/[name]${isLegacyBundle ? `-legacy` : ``}${isProd && options.filenameHashing ? '.[contenthash:8]' : ''}.js` ) webpackConfig .output .filename(outputFilename) .chunkFilename(outputFilename) // code splitting if (process.env.NODE_ENV !== 'test') { webpackConfig .optimization.splitChunks({ cacheGroups: { vendors: { name: `chunk-vendors`, test: /[\\/]node_modules[\\/]/, priority: -10, chunks: 'initial' }, common: { name: `chunk-common`, minChunks: 2, priority: -20, chunks: 'initial', reuseExistingChunk: true } } }) } // HTML plugin const resolveClientEnv = require('../util/resolveClientEnv') // #1669 html-webpack-plugin's default sort uses toposort which cannot // handle cyclic deps in certain cases. Monkey patch it to handle the case // before we can upgrade to its 4.0 version (incompatible with preload atm) const chunkSorters = require('html-webpack-plugin/lib/chunksorter') const depSort = chunkSorters.dependency chunkSorters.auto = chunkSorters.dependency = (chunks, ...args) => { try { return depSort(chunks, ...args) } catch (e) { // fallback to a manual sort if that happens... return chunks.sort((a, b) => { // make sure user entry is loaded last so user CSS can override // vendor CSS if (a.id === 'app') { return 1 } else if (b.id === 'app') { return -1 } else if (a.entry !== b.entry) { return b.entry ? -1 : 1 } return 0 }) } } const htmlOptions = { title: api.service.pkg.name, templateParameters: (compilation, assets, pluginOptions) => { // enhance html-webpack-plugin's built in template params let stats return Object.assign({ // make stats lazy as it is expensive get webpack () { return stats || (stats = compilation.getStats().toJson()) }, compilation: compilation, webpackConfig: compilation.options, htmlWebpackPlugin: { files: assets, options: pluginOptions } }, resolveClientEnv(options, true /* raw */)) } } // handle indexPath if (options.indexPath !== 'index.html') { // why not set filename for html-webpack-plugin? // 1. It cannot handle absolute paths // 2. Relative paths causes incorrect SW manifest to be generated (#2007) webpackConfig .plugin('move-index') .use(require('../webpack/MovePlugin'), [ path.resolve(outputDir, 'index.html'), path.resolve(outputDir, options.indexPath) ]) } if (isProd) { Object.assign(htmlOptions, { minify: { removeComments: true, collapseWhitespace: true, removeAttributeQuotes: true, collapseBooleanAttributes: true, removeScriptTypeAttributes: true // more options: // https://github.com/kangax/html-minifier#options-quick-reference } }) // keep chunk ids stable so async chunks have consistent hash (#1916) webpackConfig .plugin('named-chunks') .use(require('webpack/lib/NamedChunksPlugin'), [chunk => { if (chunk.name) { return chunk.name } const hash = require('hash-sum') const joinedHash = hash( Array.from(chunk.modulesIterable, m => m.id).join('_') ) return `chunk-` + joinedHash }]) } // resolve HTML file(s) const HTMLPlugin = require('html-webpack-plugin') const PreloadPlugin = require('@vue/preload-webpack-plugin') const multiPageConfig = options.pages const htmlPath = api.resolve('public/index.html') const defaultHtmlPath = path.resolve(__dirname, 'index-default.html') const publicCopyIgnore = ['.DS_Store'] if (!multiPageConfig) { // default, single page setup. htmlOptions.template = fs.existsSync(htmlPath) ? htmlPath : defaultHtmlPath publicCopyIgnore.push({ glob: path.relative(api.resolve('public'), api.resolve(htmlOptions.template)), matchBase: false }) webpackConfig .plugin('html') .use(HTMLPlugin, [htmlOptions]) if (!isLegacyBundle) { // inject preload/prefetch to HTML webpackConfig .plugin('preload') .use(PreloadPlugin, [{ rel: 'preload', include: 'initial', fileBlacklist: [/\.map$/, /hot-update\.js$/] }]) webpackConfig .plugin('prefetch') .use(PreloadPlugin, [{ rel: 'prefetch', include: 'asyncChunks' }]) } } else { // multi-page setup webpackConfig.entryPoints.clear() const pages = Object.keys(multiPageConfig) const normalizePageConfig = c => typeof c === 'string' ? { entry: c } : c pages.forEach(name => { const pageConfig = normalizePageConfig(multiPageConfig[name]) const { entry, template = `public/${name}.html`, filename = `${name}.html`, chunks = ['chunk-vendors', 'chunk-common', name] } = pageConfig // Currently Cypress v3.1.0 comes with a very old version of Node, // which does not support object rest syntax. // (https://github.com/cypress-io/cypress/issues/2253) // So here we have to extract the customHtmlOptions manually. const customHtmlOptions = {} for (const key in pageConfig) { if ( !['entry', 'template', 'filename', 'chunks'].includes(key) ) { customHtmlOptions[key] = pageConfig[key] } } // inject entry const entries = Array.isArray(entry) ? entry : [entry] webpackConfig.entry(name).merge(entries.map(e => api.resolve(e))) // resolve page index template const hasDedicatedTemplate = fs.existsSync(api.resolve(template)) const templatePath = hasDedicatedTemplate ? template : fs.existsSync(htmlPath) ? htmlPath : defaultHtmlPath publicCopyIgnore.push({ glob: path.relative(api.resolve('public'), api.resolve(templatePath)), matchBase: false }) // inject html plugin for the page const pageHtmlOptions = Object.assign( {}, htmlOptions, { chunks, template: templatePath, filename: ensureRelative(outputDir, filename) }, customHtmlOptions ) webpackConfig .plugin(`html-${name}`) .use(HTMLPlugin, [pageHtmlOptions]) }) if (!isLegacyBundle) { pages.forEach(name => { const filename = ensureRelative( outputDir, normalizePageConfig(multiPageConfig[name]).filename || `${name}.html` ) webpackConfig .plugin(`preload-${name}`) .use(PreloadPlugin, [{ rel: 'preload', includeHtmlNames: [filename], include: { type: 'initial', entries: [name] }, fileBlacklist: [/\.map$/, /hot-update\.js$/] }]) webpackConfig .plugin(`prefetch-${name}`) .use(PreloadPlugin, [{ rel: 'prefetch', includeHtmlNames: [filename], include: { type: 'asyncChunks', entries: [name] } }]) }) } } // CORS and Subresource Integrity if (options.crossorigin != null || options.integrity) { webpackConfig .plugin('cors') .use(require('../webpack/CorsPlugin'), [{ crossorigin: options.crossorigin, integrity: options.integrity, publicPath: options.publicPath }]) } // copy static assets in public/ const publicDir = api.resolve('public') if (!isLegacyBundle && fs.existsSync(publicDir)) { webpackConfig .plugin('copy') .use(require('copy-webpack-plugin'), [[{ from: publicDir, to: outputDir, toType: 'dir', ignore: publicCopyIgnore }]]) } }) }