app.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. // config that are specific to --target app
  2. const fs = require('fs')
  3. const path = require('path')
  4. // ensure the filename passed to html-webpack-plugin is a relative path
  5. // because it cannot correctly handle absolute paths
  6. function ensureRelative (outputDir, _path) {
  7. if (path.isAbsolute(_path)) {
  8. return path.relative(outputDir, _path)
  9. } else {
  10. return _path
  11. }
  12. }
  13. module.exports = (api, options) => {
  14. api.chainWebpack(webpackConfig => {
  15. // only apply when there's no alternative target
  16. if (process.env.VUE_CLI_BUILD_TARGET && process.env.VUE_CLI_BUILD_TARGET !== 'app') {
  17. return
  18. }
  19. const isProd = process.env.NODE_ENV === 'production'
  20. const isLegacyBundle = process.env.VUE_CLI_MODERN_MODE && !process.env.VUE_CLI_MODERN_BUILD
  21. const outputDir = api.resolve(options.outputDir)
  22. // code splitting
  23. if (isProd && !process.env.CYPRESS_ENV) {
  24. webpackConfig
  25. .optimization.splitChunks({
  26. cacheGroups: {
  27. vendors: {
  28. name: `chunk-vendors`,
  29. test: /[\\/]node_modules[\\/]/,
  30. priority: -10,
  31. chunks: 'initial'
  32. },
  33. common: {
  34. name: `chunk-common`,
  35. minChunks: 2,
  36. priority: -20,
  37. chunks: 'initial',
  38. reuseExistingChunk: true
  39. }
  40. }
  41. })
  42. }
  43. // HTML plugin
  44. const resolveClientEnv = require('../util/resolveClientEnv')
  45. // #1669 html-webpack-plugin's default sort uses toposort which cannot
  46. // handle cyclic deps in certain cases. Monkey patch it to handle the case
  47. // before we can upgrade to its 4.0 version (incompatible with preload atm)
  48. const chunkSorters = require('html-webpack-plugin/lib/chunksorter')
  49. const depSort = chunkSorters.dependency
  50. chunkSorters.auto = chunkSorters.dependency = (chunks, ...args) => {
  51. try {
  52. return depSort(chunks, ...args)
  53. } catch (e) {
  54. // fallback to a manual sort if that happens...
  55. return chunks.sort((a, b) => {
  56. // make sure user entry is loaded last so user CSS can override
  57. // vendor CSS
  58. if (a.id === 'app') {
  59. return 1
  60. } else if (b.id === 'app') {
  61. return -1
  62. } else if (a.entry !== b.entry) {
  63. return b.entry ? -1 : 1
  64. }
  65. return 0
  66. })
  67. }
  68. }
  69. const htmlOptions = {
  70. templateParameters: (compilation, assets, pluginOptions) => {
  71. // enhance html-webpack-plugin's built in template params
  72. let stats
  73. return Object.assign({
  74. // make stats lazy as it is expensive
  75. get webpack () {
  76. return stats || (stats = compilation.getStats().toJson())
  77. },
  78. compilation: compilation,
  79. webpackConfig: compilation.options,
  80. htmlWebpackPlugin: {
  81. files: assets,
  82. options: pluginOptions
  83. }
  84. }, resolveClientEnv(options, true /* raw */))
  85. }
  86. }
  87. // handle indexPath
  88. if (options.indexPath !== 'index.html') {
  89. // why not set filename for html-webpack-plugin?
  90. // 1. It cannot handle absolute paths
  91. // 2. Relative paths causes incorrect SW manifest to be generated (#2007)
  92. webpackConfig
  93. .plugin('move-index')
  94. .use(require('../webpack/MovePlugin'), [
  95. path.resolve(outputDir, 'index.html'),
  96. path.resolve(outputDir, options.indexPath)
  97. ])
  98. }
  99. if (isProd) {
  100. Object.assign(htmlOptions, {
  101. minify: {
  102. removeComments: true,
  103. collapseWhitespace: true,
  104. removeAttributeQuotes: true,
  105. collapseBooleanAttributes: true,
  106. removeScriptTypeAttributes: true
  107. // more options:
  108. // https://github.com/kangax/html-minifier#options-quick-reference
  109. }
  110. })
  111. // keep chunk ids stable so async chunks have consistent hash (#1916)
  112. webpackConfig
  113. .plugin('named-chunks')
  114. .use(require('webpack/lib/NamedChunksPlugin'), [chunk => {
  115. if (chunk.name) {
  116. return chunk.name
  117. }
  118. const hash = require('hash-sum')
  119. const joinedHash = hash(
  120. Array.from(chunk.modulesIterable, m => m.id).join('_')
  121. )
  122. return `chunk-` + joinedHash
  123. }])
  124. }
  125. // resolve HTML file(s)
  126. const HTMLPlugin = require('html-webpack-plugin')
  127. const PreloadPlugin = require('@vue/preload-webpack-plugin')
  128. const multiPageConfig = options.pages
  129. const htmlPath = api.resolve('public/index.html')
  130. const defaultHtmlPath = path.resolve(__dirname, 'index-default.html')
  131. const publicCopyIgnore = ['.DS_Store']
  132. if (!multiPageConfig) {
  133. // default, single page setup.
  134. htmlOptions.template = fs.existsSync(htmlPath)
  135. ? htmlPath
  136. : defaultHtmlPath
  137. publicCopyIgnore.push({
  138. glob: path.relative(api.resolve('public'), api.resolve(htmlOptions.template)),
  139. matchBase: false
  140. })
  141. webpackConfig
  142. .plugin('html')
  143. .use(HTMLPlugin, [htmlOptions])
  144. if (!isLegacyBundle) {
  145. // inject preload/prefetch to HTML
  146. webpackConfig
  147. .plugin('preload')
  148. .use(PreloadPlugin, [{
  149. rel: 'preload',
  150. include: 'initial',
  151. fileBlacklist: [/\.map$/, /hot-update\.js$/]
  152. }])
  153. webpackConfig
  154. .plugin('prefetch')
  155. .use(PreloadPlugin, [{
  156. rel: 'prefetch',
  157. include: 'asyncChunks'
  158. }])
  159. }
  160. } else {
  161. // multi-page setup
  162. webpackConfig.entryPoints.clear()
  163. const pages = Object.keys(multiPageConfig)
  164. const normalizePageConfig = c => typeof c === 'string' ? { entry: c } : c
  165. pages.forEach(name => {
  166. const pageConfig = normalizePageConfig(multiPageConfig[name])
  167. const {
  168. entry,
  169. template = `public/${name}.html`,
  170. filename = `${name}.html`,
  171. chunks = ['chunk-vendors', 'chunk-common', name]
  172. } = pageConfig
  173. // Currently Cypress v3.1.0 comes with a very old version of Node,
  174. // which does not support object rest syntax.
  175. // (https://github.com/cypress-io/cypress/issues/2253)
  176. // So here we have to extract the customHtmlOptions manually.
  177. const customHtmlOptions = {}
  178. for (const key in pageConfig) {
  179. if (
  180. !['entry', 'template', 'filename', 'chunks'].includes(key)
  181. ) {
  182. customHtmlOptions[key] = pageConfig[key]
  183. }
  184. }
  185. // inject entry
  186. const entries = Array.isArray(entry) ? entry : [entry]
  187. webpackConfig.entry(name).merge(entries.map(e => api.resolve(e)))
  188. // resolve page index template
  189. const hasDedicatedTemplate = fs.existsSync(api.resolve(template))
  190. const templatePath = hasDedicatedTemplate
  191. ? template
  192. : fs.existsSync(htmlPath)
  193. ? htmlPath
  194. : defaultHtmlPath
  195. publicCopyIgnore.push({
  196. glob: path.relative(api.resolve('public'), api.resolve(templatePath)),
  197. matchBase: false
  198. })
  199. // inject html plugin for the page
  200. const pageHtmlOptions = Object.assign(
  201. {},
  202. htmlOptions,
  203. {
  204. chunks,
  205. template: templatePath,
  206. filename: ensureRelative(outputDir, filename)
  207. },
  208. customHtmlOptions
  209. )
  210. webpackConfig
  211. .plugin(`html-${name}`)
  212. .use(HTMLPlugin, [pageHtmlOptions])
  213. })
  214. if (!isLegacyBundle) {
  215. pages.forEach(name => {
  216. const filename = ensureRelative(
  217. outputDir,
  218. normalizePageConfig(multiPageConfig[name]).filename || `${name}.html`
  219. )
  220. webpackConfig
  221. .plugin(`preload-${name}`)
  222. .use(PreloadPlugin, [{
  223. rel: 'preload',
  224. includeHtmlNames: [filename],
  225. include: {
  226. type: 'initial',
  227. entries: [name]
  228. },
  229. fileBlacklist: [/\.map$/, /hot-update\.js$/]
  230. }])
  231. webpackConfig
  232. .plugin(`prefetch-${name}`)
  233. .use(PreloadPlugin, [{
  234. rel: 'prefetch',
  235. includeHtmlNames: [filename],
  236. include: {
  237. type: 'asyncChunks',
  238. entries: [name]
  239. }
  240. }])
  241. })
  242. }
  243. }
  244. // CORS and Subresource Integrity
  245. if (options.crossorigin != null || options.integrity) {
  246. webpackConfig
  247. .plugin('cors')
  248. .use(require('../webpack/CorsPlugin'), [{
  249. crossorigin: options.crossorigin,
  250. integrity: options.integrity,
  251. publicPath: options.publicPath
  252. }])
  253. }
  254. // copy static assets in public/
  255. const publicDir = api.resolve('public')
  256. if (!isLegacyBundle && fs.existsSync(publicDir)) {
  257. webpackConfig
  258. .plugin('copy')
  259. .use(require('copy-webpack-plugin'), [[{
  260. from: publicDir,
  261. to: outputDir,
  262. toType: 'dir',
  263. ignore: publicCopyIgnore
  264. }]])
  265. }
  266. })
  267. }