analyzeBundle.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. // From https://github.com/webpack-contrib/webpack-bundle-analyzer/blob/4abac503c789bac94118e5bbfc410686fb5112c7/src/parseUtils.js
  2. // Modified by Guillaume Chau (Akryum)
  3. const acorn = require('acorn')
  4. const walk = require('acorn-walk')
  5. const mapValues = require('lodash.mapvalues')
  6. const zlib = require('zlib')
  7. const { warn } = require('@vue/cli-shared-utils')
  8. exports.analyzeBundle = function analyzeBundle (bundleStats, assetSources) {
  9. // Picking only `*.js` assets from bundle that has non-empty `chunks` array
  10. const jsAssets = []
  11. const otherAssets = []
  12. // Separate JS assets
  13. bundleStats.assets.forEach(asset => {
  14. if (asset.name.endsWith('.js') && asset.chunks && asset.chunks.length) {
  15. jsAssets.push(asset)
  16. } else {
  17. otherAssets.push(asset)
  18. }
  19. })
  20. // Trying to parse bundle assets and get real module sizes
  21. let bundlesSources = null
  22. let parsedModules = null
  23. bundlesSources = {}
  24. parsedModules = {}
  25. for (const asset of jsAssets) {
  26. const source = assetSources.get(asset.name)
  27. let bundleInfo
  28. try {
  29. bundleInfo = parseBundle(source)
  30. } catch (err) {
  31. const msg = (err.code === 'ENOENT') ? 'no such file' : err.message
  32. warn(`Error parsing bundle asset "${asset.fullPath}": ${msg}`)
  33. continue
  34. }
  35. bundlesSources[asset.name] = bundleInfo.src
  36. Object.assign(parsedModules, bundleInfo.modules)
  37. }
  38. if (!Object.keys(bundlesSources).length) {
  39. bundlesSources = null
  40. parsedModules = null
  41. warn('\nNo bundles were parsed. Analyzer will show only original module sizes from stats file.\n')
  42. }
  43. // Update sizes
  44. bundleStats.modules.forEach(module => {
  45. const parsedSrc = parsedModules && parsedModules[module.id]
  46. module.size = {
  47. stats: module.size
  48. }
  49. if (parsedSrc) {
  50. module.size.parsed = parsedSrc.length
  51. module.size.gzip = getGzipSize(parsedSrc)
  52. } else {
  53. module.size.parsed = module.size.stats
  54. module.size.gzip = 0
  55. }
  56. })
  57. jsAssets.forEach(asset => {
  58. const src = bundlesSources && bundlesSources[asset.name]
  59. asset.size = {
  60. stats: asset.size
  61. }
  62. if (src) {
  63. asset.size.parsed = src.length
  64. asset.size.gzip = getGzipSize(src)
  65. } else {
  66. asset.size.parsed = asset.size.stats
  67. asset.size.gzip = 0
  68. }
  69. }, {})
  70. otherAssets.forEach(asset => {
  71. const src = assetSources.get(asset.name)
  72. asset.size = {
  73. stats: asset.size,
  74. parsed: asset.size
  75. }
  76. if (src) {
  77. asset.size.gzip = getGzipSize(src)
  78. } else {
  79. asset.size.gzip = 0
  80. }
  81. })
  82. }
  83. function parseBundle (bundleContent) {
  84. const ast = acorn.parse(bundleContent, {
  85. sourceType: 'script',
  86. // I believe in a bright future of ECMAScript!
  87. // Actually, it's set to `2050` to support the latest ECMAScript version that currently exists.
  88. // Seems like `acorn` supports such weird option value.
  89. ecmaVersion: 2050
  90. })
  91. const walkState = {
  92. locations: null,
  93. expressionStatementDepth: 0
  94. }
  95. walk.recursive(
  96. ast,
  97. walkState,
  98. {
  99. ExpressionStatement (node, state, c) {
  100. if (state.locations) return
  101. state.expressionStatementDepth++
  102. if (
  103. // Webpack 5 stores modules in the the top-level IIFE
  104. state.expressionStatementDepth === 1 &&
  105. ast.body.includes(node) &&
  106. isIIFE(node)
  107. ) {
  108. const fn = getIIFECallExpression(node)
  109. if (
  110. // It should not contain neither arguments
  111. fn.arguments.length === 0 &&
  112. // ...nor parameters
  113. fn.callee.params.length === 0
  114. ) {
  115. // Modules are stored in the very first variable declaration as hash
  116. const firstVariableDeclaration = fn.callee.body.body.find(n => n.type === 'VariableDeclaration')
  117. if (firstVariableDeclaration) {
  118. for (const declaration of firstVariableDeclaration.declarations) {
  119. if (declaration.init) {
  120. state.locations = getModulesLocations(declaration.init)
  121. if (state.locations) {
  122. break
  123. }
  124. }
  125. }
  126. }
  127. }
  128. }
  129. if (!state.locations) {
  130. c(node.expression, state)
  131. }
  132. state.expressionStatementDepth--
  133. },
  134. AssignmentExpression (node, state) {
  135. if (state.locations) return
  136. // Modules are stored in exports.modules:
  137. // exports.modules = {};
  138. const { left, right } = node
  139. if (
  140. left &&
  141. left.object && left.object.name === 'exports' &&
  142. left.property && left.property.name === 'modules' &&
  143. isModulesHash(right)
  144. ) {
  145. state.locations = getModulesLocations(right)
  146. }
  147. },
  148. CallExpression (node, state, c) {
  149. if (state.locations) return
  150. const args = node.arguments
  151. // Main chunk with webpack loader.
  152. // Modules are stored in first argument:
  153. // (function (...) {...})(<modules>)
  154. if (
  155. node.callee.type === 'FunctionExpression' &&
  156. !node.callee.id &&
  157. args.length === 1 &&
  158. isSimpleModulesList(args[0])
  159. ) {
  160. state.locations = getModulesLocations(args[0])
  161. return
  162. }
  163. // Async Webpack < v4 chunk without webpack loader.
  164. // webpackJsonp([<chunks>], <modules>, ...)
  165. // As function name may be changed with `output.jsonpFunction` option we can't rely on it's default name.
  166. if (
  167. node.callee.type === 'Identifier' &&
  168. mayBeAsyncChunkArguments(args) &&
  169. isModulesList(args[1])
  170. ) {
  171. state.locations = getModulesLocations(args[1])
  172. return
  173. }
  174. // Async Webpack v4 chunk without webpack loader.
  175. // (window.webpackJsonp=window.webpackJsonp||[]).push([[<chunks>], <modules>, ...]);
  176. // As function name may be changed with `output.jsonpFunction` option we can't rely on it's default name.
  177. if (isAsyncChunkPushExpression(node)) {
  178. state.locations = getModulesLocations(args[0].elements[1])
  179. return
  180. }
  181. // Webpack v4 WebWorkerChunkTemplatePlugin
  182. // globalObject.chunkCallbackName([<chunks>],<modules>, ...);
  183. // Both globalObject and chunkCallbackName can be changed through the config, so we can't check them.
  184. if (isAsyncWebWorkerChunkExpression(node)) {
  185. state.locations = getModulesLocations(args[1])
  186. return
  187. }
  188. // Walking into arguments because some of plugins (e.g. `DedupePlugin`) or some Webpack
  189. // features (e.g. `umd` library output) can wrap modules list into additional IIFE.
  190. args.forEach(arg => c(arg, state))
  191. }
  192. }
  193. )
  194. let modules
  195. if (walkState.locations) {
  196. modules = mapValues(walkState.locations,
  197. loc => bundleContent.slice(loc.start, loc.end)
  198. )
  199. } else {
  200. modules = {}
  201. }
  202. return {
  203. modules: modules,
  204. src: bundleContent,
  205. runtimeSrc: getBundleRuntime(bundleContent, walkState.locations)
  206. }
  207. }
  208. function getGzipSize (buffer) {
  209. return zlib.gzipSync(buffer).length
  210. }
  211. /**
  212. * Returns bundle source except modules
  213. */
  214. function getBundleRuntime (content, modulesLocations) {
  215. const sortedLocations = Object.values(modulesLocations || {})
  216. .sort((a, b) => a.start - b.start)
  217. let result = ''
  218. let lastIndex = 0
  219. for (const { start, end } of sortedLocations) {
  220. result += content.slice(lastIndex, start)
  221. lastIndex = end
  222. }
  223. return result + content.slice(lastIndex, content.length)
  224. }
  225. function isIIFE (node) {
  226. return (
  227. node.type === 'ExpressionStatement' &&
  228. (
  229. node.expression.type === 'CallExpression' ||
  230. (node.expression.type === 'UnaryExpression' && node.expression.argument.type === 'CallExpression')
  231. )
  232. )
  233. }
  234. function getIIFECallExpression (node) {
  235. if (node.expression.type === 'UnaryExpression') {
  236. return node.expression.argument
  237. } else {
  238. return node.expression
  239. }
  240. }
  241. function isModulesList (node) {
  242. return (
  243. isSimpleModulesList(node) ||
  244. // Modules are contained in expression `Array([minimum ID]).concat([<module>, <module>, ...])`
  245. isOptimizedModulesArray(node)
  246. )
  247. }
  248. function isSimpleModulesList (node) {
  249. return (
  250. // Modules are contained in hash. Keys are module ids.
  251. isModulesHash(node) ||
  252. // Modules are contained in array. Indexes are module ids.
  253. isModulesArray(node)
  254. )
  255. }
  256. function isModulesHash (node) {
  257. return (
  258. node.type === 'ObjectExpression' &&
  259. node.properties
  260. .map(p => p.value)
  261. .every(isModuleWrapper)
  262. )
  263. }
  264. function isModulesArray (node) {
  265. return (
  266. node.type === 'ArrayExpression' &&
  267. node.elements.every(elem =>
  268. // Some of array items may be skipped because there is no module with such id
  269. !elem ||
  270. isModuleWrapper(elem)
  271. )
  272. )
  273. }
  274. function isOptimizedModulesArray (node) {
  275. // Checking whether modules are contained in `Array(<minimum ID>).concat(...modules)` array:
  276. // https://github.com/webpack/webpack/blob/v1.14.0/lib/Template.js#L91
  277. // The `<minimum ID>` + array indexes are module ids
  278. return (
  279. node.type === 'CallExpression' &&
  280. node.callee.type === 'MemberExpression' &&
  281. // Make sure the object called is `Array(<some number>)`
  282. node.callee.object.type === 'CallExpression' &&
  283. node.callee.object.callee.type === 'Identifier' &&
  284. node.callee.object.callee.name === 'Array' &&
  285. node.callee.object.arguments.length === 1 &&
  286. isNumericId(node.callee.object.arguments[0]) &&
  287. // Make sure the property X called for `Array(<some number>).X` is `concat`
  288. node.callee.property.type === 'Identifier' &&
  289. node.callee.property.name === 'concat' &&
  290. // Make sure exactly one array is passed in to `concat`
  291. node.arguments.length === 1 &&
  292. isModulesArray(node.arguments[0])
  293. )
  294. }
  295. function isModuleWrapper (node) {
  296. return (
  297. // It's an anonymous function expression that wraps module
  298. ((node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') && !node.id) ||
  299. // If `DedupePlugin` is used it can be an ID of duplicated module...
  300. isModuleId(node) ||
  301. // or an array of shape [<module_id>, ...args]
  302. (node.type === 'ArrayExpression' && node.elements.length > 1 && isModuleId(node.elements[0]))
  303. )
  304. }
  305. function isModuleId (node) {
  306. return (node.type === 'Literal' && (isNumericId(node) || typeof node.value === 'string'))
  307. }
  308. function isNumericId (node) {
  309. return (node.type === 'Literal' && Number.isInteger(node.value) && node.value >= 0)
  310. }
  311. function isChunkIds (node) {
  312. // Array of numeric or string ids. Chunk IDs are strings when NamedChunksPlugin is used
  313. return (
  314. node.type === 'ArrayExpression' &&
  315. node.elements.every(isModuleId)
  316. )
  317. }
  318. function isAsyncChunkPushExpression (node) {
  319. const {
  320. callee,
  321. arguments: args
  322. } = node
  323. return (
  324. callee.type === 'MemberExpression' &&
  325. callee.property.name === 'push' &&
  326. callee.object.type === 'AssignmentExpression' &&
  327. args.length === 1 &&
  328. args[0].type === 'ArrayExpression' &&
  329. mayBeAsyncChunkArguments(args[0].elements) &&
  330. isModulesList(args[0].elements[1])
  331. )
  332. }
  333. function mayBeAsyncChunkArguments (args) {
  334. return (
  335. args.length >= 2 &&
  336. isChunkIds(args[0])
  337. )
  338. }
  339. function isAsyncWebWorkerChunkExpression (node) {
  340. const { callee, type, arguments: args } = node
  341. return (
  342. type === 'CallExpression' &&
  343. callee.type === 'MemberExpression' &&
  344. args.length === 2 &&
  345. isChunkIds(args[0]) &&
  346. isModulesList(args[1])
  347. )
  348. }
  349. function getModulesLocations (node) {
  350. if (node.type === 'ObjectExpression') {
  351. // Modules hash
  352. const modulesNodes = node.properties
  353. return modulesNodes.reduce((result, moduleNode) => {
  354. const moduleId = moduleNode.key.name || moduleNode.key.value
  355. result[moduleId] = getModuleLocation(moduleNode.value)
  356. return result
  357. }, {})
  358. }
  359. const isOptimizedArray = (node.type === 'CallExpression')
  360. if (node.type === 'ArrayExpression' || isOptimizedArray) {
  361. // Modules array or optimized array
  362. const minId = isOptimizedArray
  363. // Get the [minId] value from the Array() call first argument literal value
  364. ? node.callee.object.arguments[0].value
  365. // `0` for simple array
  366. : 0
  367. const modulesNodes = isOptimizedArray
  368. // The modules reside in the `concat()` function call arguments
  369. ? node.arguments[0].elements
  370. : node.elements
  371. return modulesNodes.reduce((result, moduleNode, i) => {
  372. if (moduleNode) {
  373. result[i + minId] = getModuleLocation(moduleNode)
  374. }
  375. return result
  376. }, {})
  377. }
  378. return {}
  379. }
  380. function getModuleLocation (node) {
  381. return { start: node.start, end: node.end }
  382. }