app.js 9.5 KB

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