concatenate.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const Template = require("../Template");
  7. /** @import { Identifier, Node } from "estree" */
  8. /** @import { Optimization } from "../../declarations/WebpackOptions" */
  9. /**
  10. * @import {
  11. * Scope,
  12. * Reference,
  13. * Variable
  14. * } from "../javascript/JavascriptModulesPlugin"
  15. */
  16. /** @import { Range } from "../javascript/JavascriptParser" */
  17. /** @typedef {Node & { start?: number, end?: number }} PositionedNode */
  18. /** @typedef {Exclude<keyof Node, "range" | "loc" | "leadingComments" | "trailingComments">} ChildKey */
  19. /** @typedef {Set<string>} UsedNames */
  20. const DEFAULT_EXPORT = "__WEBPACK_DEFAULT_EXPORT__";
  21. const NAMESPACE_OBJECT_EXPORT = "__WEBPACK_NAMESPACE_OBJECT__";
  22. /**
  23. * Whether CommonJS modules and require edges take part in concatenation.
  24. * @param {Optimization["concatenateModules"]} concatenateModules the optimization.concatenateModules option
  25. * @returns {boolean} true when CommonJS concatenation is enabled
  26. */
  27. const isCommonJsConcatenationEnabled = (concatenateModules) =>
  28. typeof concatenateModules === "object"
  29. ? concatenateModules.commonjs !== false
  30. : concatenateModules === true;
  31. /** @type {WeakMap<Scope, Map<Identifier, Variable>>} */
  32. const sharedInnerBindingsCache = new WeakMap();
  33. /**
  34. * The inner bindings sharing a declaring identifier with their own scope — a
  35. * class name binds twice. Indexed once per scope; per binding it is quadratic.
  36. * @param {Scope} scope the scope whose children to index
  37. * @returns {Map<Identifier, Variable>} the inner binding sharing each identifier
  38. */
  39. const getSharedInnerBindings = (scope) => {
  40. const cached = sharedInnerBindingsCache.get(scope);
  41. if (cached !== undefined) return cached;
  42. /** @type {Map<Identifier, Variable>} */
  43. const shared = new Map();
  44. /** @type {Set<Identifier>} */
  45. const declared = new Set();
  46. for (const variable of scope.variables) {
  47. for (const identifier of variable.identifiers) declared.add(identifier);
  48. }
  49. if (declared.size !== 0) {
  50. for (const child of scope.childScopes) {
  51. for (const innerVariable of child.variables) {
  52. for (const identifier of innerVariable.identifiers) {
  53. if (declared.has(identifier)) shared.set(identifier, innerVariable);
  54. }
  55. }
  56. }
  57. }
  58. sharedInnerBindingsCache.set(scope, shared);
  59. return shared;
  60. };
  61. /**
  62. * Gets all references.
  63. * @param {Variable} variable variable
  64. * @returns {Reference[]} references
  65. */
  66. const getAllReferences = (variable) => {
  67. // the inner binding of `class Foo { t() { Foo } }` holds references to the
  68. // same name, and renaming has to move them too
  69. const shared = getSharedInnerBindings(variable.scope);
  70. if (shared.size === 0) return variable.references;
  71. let set = variable.references;
  72. /** @type {Variable | undefined} */
  73. let last;
  74. for (const identifier of variable.identifiers) {
  75. const innerVariable = shared.get(identifier);
  76. if (innerVariable === undefined || innerVariable === last) continue;
  77. last = innerVariable;
  78. // copy-on-write to keep the common no-match case allocation-free
  79. if (set === variable.references) set = [...set];
  80. for (const reference of innerVariable.references) set.push(reference);
  81. }
  82. return set;
  83. };
  84. /**
  85. * Tests whether a node covers the searched range. `start`/`end` are read before
  86. * `range`, because webpack's parser serves `range` from a lazy getter that
  87. * allocates an array and transitions the node's shape on first access — reading
  88. * it while walking would pay that for most nodes of the ast.
  89. * @param {PositionedNode} node node
  90. * @param {number} start start of the searched range
  91. * @param {number} end end of the searched range
  92. * @returns {boolean} whether the node covers the range
  93. */
  94. const coversRange = (node, start, end) => {
  95. const nodeStart = node.start;
  96. if (typeof nodeStart === "number") {
  97. return nodeStart <= start && /** @type {number} */ (node.end) >= end;
  98. }
  99. const range = node.range;
  100. return range !== undefined && range[0] <= start && range[1] >= end;
  101. };
  102. /**
  103. * Returns the single sibling covering the searched range.
  104. * @param {Node[]} items sibling nodes
  105. * @param {number} start start of the searched range
  106. * @param {number} end end of the searched range
  107. * @returns {Node | undefined} covering sibling
  108. */
  109. const findCoveringItem = (items, start, end) => {
  110. // sibling ranges are ordered and disjoint; binary search the container
  111. let low = 0;
  112. let high = items.length - 1;
  113. while (low <= high) {
  114. const middle = (low + high) >> 1;
  115. const item = /** @type {PositionedNode} */ (items[middle]);
  116. /** @type {number | undefined} */
  117. let itemStart;
  118. /** @type {number | undefined} */
  119. let itemEnd;
  120. if (item) {
  121. itemStart = item.start;
  122. if (typeof itemStart === "number") {
  123. itemEnd = item.end;
  124. } else {
  125. const range = item.range;
  126. if (range === undefined) {
  127. itemStart = undefined;
  128. } else {
  129. itemStart = range[0];
  130. itemEnd = range[1];
  131. }
  132. }
  133. }
  134. if (itemStart === undefined) {
  135. // holes or range-less nodes: scan the remaining window linearly
  136. for (let i = low; i <= high; i++) {
  137. const candidate = items[i];
  138. if (candidate && coversRange(candidate, start, end)) return candidate;
  139. }
  140. return undefined;
  141. }
  142. if (itemStart > start) {
  143. high = middle - 1;
  144. } else if (start >= /** @type {number} */ (itemEnd)) {
  145. low = middle + 1;
  146. } else {
  147. return /** @type {number} */ (itemEnd) >= end ? item : undefined;
  148. }
  149. }
  150. return undefined;
  151. };
  152. /**
  153. * Collects the ancestors of `node` below `parent`, innermost first.
  154. * Keeps scanning after a covering child turns out not to contain `node`:
  155. * a shorthand `{ a }` holds two distinct identifiers with the same range.
  156. * @param {Node} parent node to search in
  157. * @param {number} start start of the searched range
  158. * @param {number} end end of the searched range
  159. * @param {Node} node node to find
  160. * @param {Node[]} path collected ancestors
  161. * @returns {boolean} whether the node was found
  162. */
  163. const collectPathInNode = (parent, start, end, node, path) => {
  164. for (const key in parent) {
  165. // sit in front of the child keys on every node and never hold one, so
  166. // skipping them by name is what keeps the per-level scan short
  167. if (key === "type" || key === "start" || key === "end") continue;
  168. const value = parent[/** @type {ChildKey} */ (key)];
  169. if (value === null || typeof value !== "object") continue;
  170. /** @type {Node | undefined} */
  171. let child;
  172. if (Array.isArray(value)) {
  173. // `range` is a number pair on parsers that own the property
  174. if (key === "range") continue;
  175. child = findCoveringItem(value, start, end);
  176. } else if (coversRange(value, start, end)) {
  177. child = value;
  178. }
  179. if (
  180. child !== undefined &&
  181. (child === node || collectPathInNode(child, start, end, node, path))
  182. ) {
  183. path.push(child);
  184. return true;
  185. }
  186. }
  187. return false;
  188. };
  189. /**
  190. * Returns the ancestors of `node` up to (but excluding) `ast`, innermost first.
  191. * @param {Node | Node[]} ast ast
  192. * @param {Node} node node
  193. * @returns {undefined | Node[]} result
  194. */
  195. const getPathInAst = (ast, node) => {
  196. if (ast === node) {
  197. return [];
  198. }
  199. const nodeRange = /** @type {Range} */ (node.range);
  200. const start = nodeRange[0];
  201. const end = nodeRange[1];
  202. /** @type {Node[]} */
  203. const path = [];
  204. if (Array.isArray(ast)) {
  205. const item = findCoveringItem(ast, start, end);
  206. if (item === undefined) return undefined;
  207. if (item !== node && !collectPathInNode(item, start, end, node, path)) {
  208. return undefined;
  209. }
  210. path.push(item);
  211. return path;
  212. }
  213. if (!ast || typeof ast !== "object") return undefined;
  214. return collectPathInNode(ast, start, end, node, path) ? path : undefined;
  215. };
  216. /** @type {Map<string, string[]>} */
  217. const splittedInfoCache = new Map();
  218. /**
  219. * Returns path segments of the cleaned extra info.
  220. * @param {string} extraInfo extra info
  221. * @returns {string[]} cleaned path segments
  222. */
  223. const getSplittedInfo = (extraInfo) => {
  224. let splittedInfo = splittedInfoCache.get(extraInfo);
  225. if (splittedInfo === undefined) {
  226. // bound the cache — extraInfo repeats for every renamed binding of a
  227. // module, but distinct values grow with project size
  228. if (splittedInfoCache.size >= 4096) splittedInfoCache.clear();
  229. // Remove uncool stuff
  230. splittedInfo = extraInfo
  231. .replace(
  232. /\.+\/|(?:\/index)?\.[a-zA-Z0-9]{1,4}(?:$|\s|\?)|\s*\+\s*\d+\s*modules/g,
  233. ""
  234. )
  235. .split("/");
  236. splittedInfoCache.set(extraInfo, splittedInfo);
  237. }
  238. return splittedInfo;
  239. };
  240. /**
  241. * Returns found new name.
  242. * @param {string} oldName old name
  243. * @param {UsedNames} usedNamed1 used named 1
  244. * @param {UsedNames} usedNamed2 used named 2
  245. * @param {string} extraInfo extra info
  246. * @returns {string} found new name
  247. */
  248. function findNewName(oldName, usedNamed1, usedNamed2, extraInfo) {
  249. let name = oldName;
  250. if (name === DEFAULT_EXPORT) {
  251. name = "";
  252. }
  253. if (name === NAMESPACE_OBJECT_EXPORT) {
  254. name = "namespaceObject";
  255. }
  256. const splittedInfo = getSplittedInfo(extraInfo);
  257. for (let i = splittedInfo.length - 1; i >= 0; i--) {
  258. name = splittedInfo[i] + (name ? `_${name}` : "");
  259. const nameIdent = Template.toIdentifier(name);
  260. if (
  261. !usedNamed1.has(nameIdent) &&
  262. (!usedNamed2 || !usedNamed2.has(nameIdent))
  263. ) {
  264. return nameIdent;
  265. }
  266. }
  267. // `_${i}` is identifier-safe, so escaping the base once is equivalent to
  268. // escaping every candidate — avoids two regexes per collision
  269. const nameIdent = Template.toIdentifier(name);
  270. let i = 0;
  271. let nameWithNumber = `${nameIdent}_${i}`;
  272. while (
  273. usedNamed1.has(nameWithNumber) ||
  274. // eslint-disable-next-line no-unmodified-loop-condition
  275. (usedNamed2 && usedNamed2.has(nameWithNumber))
  276. ) {
  277. i++;
  278. nameWithNumber = `${nameIdent}_${i}`;
  279. }
  280. return nameWithNumber;
  281. }
  282. /** @typedef {Set<Scope>} ScopeSet */
  283. /**
  284. * Adds scope symbols.
  285. * @param {Scope | null} s scope
  286. * @param {UsedNames} nameSet name set
  287. * @param {ScopeSet} scopeSet1 scope set 1
  288. * @param {ScopeSet} scopeSet2 scope set 2
  289. */
  290. const addScopeSymbols = (s, nameSet, scopeSet1, scopeSet2) => {
  291. let scope = s;
  292. while (scope) {
  293. if (scopeSet1.has(scope)) break;
  294. if (scopeSet2.has(scope)) break;
  295. scopeSet1.add(scope);
  296. for (const variable of scope.variables) {
  297. nameSet.add(variable.name);
  298. }
  299. scope = scope.upper;
  300. }
  301. };
  302. // Declared by the chunk bootstrap in the scope module code is hoisted into.
  303. // Hit when a webpack bundle is bundled again, since its output declares exactly
  304. // these: the `const` one then fails to parse, the `var` one silently clobbers
  305. // the module table. CompatibilityPlugin renames the other two runtime names.
  306. const CHUNK_RUNTIME_DECLARATIONS = new Set([
  307. "__webpack_modules__",
  308. "__webpack_module_cache__"
  309. ]);
  310. const RESERVED_NAMES = new Set(
  311. [
  312. // internal names (should always be renamed)
  313. DEFAULT_EXPORT,
  314. NAMESPACE_OBJECT_EXPORT,
  315. ...CHUNK_RUNTIME_DECLARATIONS,
  316. // keywords
  317. "abstract,arguments,async,await,boolean,break,byte,case,catch,char,class,const,continue",
  318. "debugger,default,delete,do,double,else,enum,eval,export,extends,false,final,finally,float",
  319. "for,function,goto,if,implements,import,in,instanceof,int,interface,let,long,native,new,null",
  320. "package,private,protected,public,return,short,static,super,switch,synchronized,this,throw",
  321. "throws,transient,true,try,typeof,var,void,volatile,while,with,yield",
  322. // commonjs/amd
  323. "module,__dirname,__filename,exports,require,define",
  324. // js globals
  325. "Array,Date,eval,function,hasOwnProperty,Infinity,isFinite,isNaN,isPrototypeOf,length,Math",
  326. "NaN,name,Number,Object,prototype,String,Symbol,toString,undefined,valueOf",
  327. // browser globals
  328. "alert,all,anchor,anchors,area,assign,blur,button,checkbox,clearInterval,clearTimeout",
  329. "clientInformation,close,closed,confirm,constructor,crypto,decodeURI,decodeURIComponent",
  330. "defaultStatus,document,element,elements,embed,embeds,encodeURI,encodeURIComponent,escape",
  331. "event,fileUpload,focus,form,forms,frame,innerHeight,innerWidth,layer,layers,link,location",
  332. "mimeTypes,navigate,navigator,frames,frameRate,hidden,history,image,images,offscreenBuffering",
  333. "open,opener,option,outerHeight,outerWidth,packages,pageXOffset,pageYOffset,parent,parseFloat",
  334. "parseInt,password,pkcs11,plugin,prompt,propertyIsEnum,radio,reset,screenX,screenY,scroll",
  335. "secure,select,self,setInterval,setTimeout,status,submit,taint,text,textarea,top,unescape",
  336. "untaint,window",
  337. // window events
  338. "onblur,onclick,onerror,onfocus,onkeydown,onkeypress,onkeyup,onmouseover,onload,onmouseup,onmousedown,onsubmit"
  339. ]
  340. .join(",")
  341. .split(",")
  342. );
  343. /** @typedef {{ usedNames: UsedNames, alreadyCheckedScopes: ScopeSet }} ScopeInfo */
  344. /** @typedef {Map<string, Map<string, ScopeInfo>>} UsedNamesInScopeInfo */
  345. /**
  346. * Gets used names in scope info.
  347. * @param {UsedNamesInScopeInfo} usedNamesInScopeInfo used names in scope info
  348. * @param {string} module module identifier
  349. * @param {string} id export id
  350. * @returns {ScopeInfo} info
  351. */
  352. const getUsedNamesInScopeInfo = (usedNamesInScopeInfo, module, id) => {
  353. // nested maps avoid building a `${module}-${id}` key string per lookup
  354. let byId = usedNamesInScopeInfo.get(module);
  355. if (byId === undefined) {
  356. byId = new Map();
  357. usedNamesInScopeInfo.set(module, byId);
  358. }
  359. let info = byId.get(id);
  360. if (info === undefined) {
  361. info = {
  362. usedNames: new Set(),
  363. alreadyCheckedScopes: new Set()
  364. };
  365. byId.set(id, info);
  366. }
  367. return info;
  368. };
  369. module.exports = {
  370. CHUNK_RUNTIME_DECLARATIONS,
  371. DEFAULT_EXPORT,
  372. NAMESPACE_OBJECT_EXPORT,
  373. RESERVED_NAMES,
  374. addScopeSymbols,
  375. findNewName,
  376. getAllReferences,
  377. getPathInAst,
  378. getUsedNamesInScopeInfo,
  379. isCommonJsConcatenationEnabled
  380. };