57f7a6fd9b224d3d4edeac512e8779e2c45b9341e141e16b9dfb3dec8ff42f3ff724ffdd729f73d20ce0df047048887fc5192cc0cbc3d50a38f44ccc1c0a09 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. const fs = require('fs');
  2. const _ = require('lodash');
  3. const acorn = require('acorn');
  4. const walk = require('acorn-walk');
  5. module.exports = {
  6. parseBundle
  7. };
  8. function parseBundle(bundlePath) {
  9. const content = fs.readFileSync(bundlePath, 'utf8');
  10. const ast = acorn.parse(content, {
  11. sourceType: 'script',
  12. // I believe in a bright future of ECMAScript!
  13. // Actually, it's set to `2050` to support the latest ECMAScript version that currently exists.
  14. // Seems like `acorn` supports such weird option value.
  15. ecmaVersion: 2050
  16. });
  17. const walkState = {
  18. locations: null
  19. };
  20. walk.recursive(
  21. ast,
  22. walkState,
  23. {
  24. AssignmentExpression(node, state) {
  25. if (state.locations) return;
  26. // Modules are stored in exports.modules:
  27. // exports.modules = {};
  28. const {left, right} = node;
  29. if (
  30. left &&
  31. left.object && left.object.name === 'exports' &&
  32. left.property && left.property.name === 'modules' &&
  33. isModulesHash(right)
  34. ) {
  35. state.locations = getModulesLocations(right);
  36. }
  37. },
  38. CallExpression(node, state, c) {
  39. if (state.locations) return;
  40. const args = node.arguments;
  41. // Main chunk with webpack loader.
  42. // Modules are stored in first argument:
  43. // (function (...) {...})(<modules>)
  44. if (
  45. node.callee.type === 'FunctionExpression' &&
  46. !node.callee.id &&
  47. args.length === 1 &&
  48. isSimpleModulesList(args[0])
  49. ) {
  50. state.locations = getModulesLocations(args[0]);
  51. return;
  52. }
  53. // Async Webpack < v4 chunk without webpack loader.
  54. // webpackJsonp([<chunks>], <modules>, ...)
  55. // As function name may be changed with `output.jsonpFunction` option we can't rely on it's default name.
  56. if (
  57. node.callee.type === 'Identifier' &&
  58. mayBeAsyncChunkArguments(args) &&
  59. isModulesList(args[1])
  60. ) {
  61. state.locations = getModulesLocations(args[1]);
  62. return;
  63. }
  64. // Async Webpack v4 chunk without webpack loader.
  65. // (window.webpackJsonp=window.webpackJsonp||[]).push([[<chunks>], <modules>, ...]);
  66. // As function name may be changed with `output.jsonpFunction` option we can't rely on it's default name.
  67. if (isAsyncChunkPushExpression(node)) {
  68. state.locations = getModulesLocations(args[0].elements[1]);
  69. return;
  70. }
  71. // Webpack v4 WebWorkerChunkTemplatePlugin
  72. // globalObject.chunkCallbackName([<chunks>],<modules>, ...);
  73. // Both globalObject and chunkCallbackName can be changed through the config, so we can't check them.
  74. if (isAsyncWebWorkerChunkExpression(node)) {
  75. state.locations = getModulesLocations(args[1]);
  76. return;
  77. }
  78. // Walking into arguments because some of plugins (e.g. `DedupePlugin`) or some Webpack
  79. // features (e.g. `umd` library output) can wrap modules list into additional IIFE.
  80. _.each(args, arg => c(arg, state));
  81. }
  82. }
  83. );
  84. let modules;
  85. if (walkState.locations) {
  86. modules = _.mapValues(walkState.locations,
  87. loc => content.slice(loc.start, loc.end)
  88. );
  89. } else {
  90. modules = {};
  91. }
  92. return {
  93. src: content,
  94. modules
  95. };
  96. }
  97. function isModulesList(node) {
  98. return (
  99. isSimpleModulesList(node) ||
  100. // Modules are contained in expression `Array([minimum ID]).concat([<module>, <module>, ...])`
  101. isOptimizedModulesArray(node)
  102. );
  103. }
  104. function isSimpleModulesList(node) {
  105. return (
  106. // Modules are contained in hash. Keys are module ids.
  107. isModulesHash(node) ||
  108. // Modules are contained in array. Indexes are module ids.
  109. isModulesArray(node)
  110. );
  111. }
  112. function isModulesHash(node) {
  113. return (
  114. node.type === 'ObjectExpression' &&
  115. _(node.properties)
  116. .map('value')
  117. .every(isModuleWrapper)
  118. );
  119. }
  120. function isModulesArray(node) {
  121. return (
  122. node.type === 'ArrayExpression' &&
  123. _.every(node.elements, elem =>
  124. // Some of array items may be skipped because there is no module with such id
  125. !elem ||
  126. isModuleWrapper(elem)
  127. )
  128. );
  129. }
  130. function isOptimizedModulesArray(node) {
  131. // Checking whether modules are contained in `Array(<minimum ID>).concat(...modules)` array:
  132. // https://github.com/webpack/webpack/blob/v1.14.0/lib/Template.js#L91
  133. // The `<minimum ID>` + array indexes are module ids
  134. return (
  135. node.type === 'CallExpression' &&
  136. node.callee.type === 'MemberExpression' &&
  137. // Make sure the object called is `Array(<some number>)`
  138. node.callee.object.type === 'CallExpression' &&
  139. node.callee.object.callee.type === 'Identifier' &&
  140. node.callee.object.callee.name === 'Array' &&
  141. node.callee.object.arguments.length === 1 &&
  142. isNumericId(node.callee.object.arguments[0]) &&
  143. // Make sure the property X called for `Array(<some number>).X` is `concat`
  144. node.callee.property.type === 'Identifier' &&
  145. node.callee.property.name === 'concat' &&
  146. // Make sure exactly one array is passed in to `concat`
  147. node.arguments.length === 1 &&
  148. isModulesArray(node.arguments[0])
  149. );
  150. }
  151. function isModuleWrapper(node) {
  152. return (
  153. // It's an anonymous function expression that wraps module
  154. ((node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') && !node.id) ||
  155. // If `DedupePlugin` is used it can be an ID of duplicated module...
  156. isModuleId(node) ||
  157. // or an array of shape [<module_id>, ...args]
  158. (node.type === 'ArrayExpression' && node.elements.length > 1 && isModuleId(node.elements[0]))
  159. );
  160. }
  161. function isModuleId(node) {
  162. return (node.type === 'Literal' && (isNumericId(node) || typeof node.value === 'string'));
  163. }
  164. function isNumericId(node) {
  165. return (node.type === 'Literal' && Number.isInteger(node.value) && node.value >= 0);
  166. }
  167. function isChunkIds(node) {
  168. // Array of numeric or string ids. Chunk IDs are strings when NamedChunksPlugin is used
  169. return (
  170. node.type === 'ArrayExpression' &&
  171. _.every(node.elements, isModuleId)
  172. );
  173. }
  174. function isAsyncChunkPushExpression(node) {
  175. const {
  176. callee,
  177. arguments: args
  178. } = node;
  179. return (
  180. callee.type === 'MemberExpression' &&
  181. callee.property.name === 'push' &&
  182. callee.object.type === 'AssignmentExpression' &&
  183. args.length === 1 &&
  184. args[0].type === 'ArrayExpression' &&
  185. mayBeAsyncChunkArguments(args[0].elements) &&
  186. isModulesList(args[0].elements[1])
  187. );
  188. }
  189. function mayBeAsyncChunkArguments(args) {
  190. return (
  191. args.length >= 2 &&
  192. isChunkIds(args[0])
  193. );
  194. }
  195. function isAsyncWebWorkerChunkExpression(node) {
  196. const {callee, type, arguments: args} = node;
  197. return (
  198. type === 'CallExpression' &&
  199. callee.type === 'MemberExpression' &&
  200. args.length === 2 &&
  201. isChunkIds(args[0]) &&
  202. isModulesList(args[1])
  203. );
  204. }
  205. function getModulesLocations(node) {
  206. if (node.type === 'ObjectExpression') {
  207. // Modules hash
  208. const modulesNodes = node.properties;
  209. return _.transform(modulesNodes, (result, moduleNode) => {
  210. const moduleId = moduleNode.key.name || moduleNode.key.value;
  211. result[moduleId] = getModuleLocation(moduleNode.value);
  212. }, {});
  213. }
  214. const isOptimizedArray = (node.type === 'CallExpression');
  215. if (node.type === 'ArrayExpression' || isOptimizedArray) {
  216. // Modules array or optimized array
  217. const minId = isOptimizedArray ?
  218. // Get the [minId] value from the Array() call first argument literal value
  219. node.callee.object.arguments[0].value :
  220. // `0` for simple array
  221. 0;
  222. const modulesNodes = isOptimizedArray ?
  223. // The modules reside in the `concat()` function call arguments
  224. node.arguments[0].elements :
  225. node.elements;
  226. return _.transform(modulesNodes, (result, moduleNode, i) => {
  227. if (!moduleNode) return;
  228. result[i + minId] = getModuleLocation(moduleNode);
  229. }, {});
  230. }
  231. return {};
  232. }
  233. function getModuleLocation(node) {
  234. return {
  235. start: node.start,
  236. end: node.end
  237. };
  238. }