IdHelpers.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const createHash = require("../util/createHash");
  7. const { makePathsRelative } = require("../util/identifier");
  8. const numberHash = require("../util/numberHash");
  9. /** @import Chunk from "../Chunk" */
  10. /** @import ChunkGraph from "../ChunkGraph" */
  11. /** @import Compilation from "../Compilation" */
  12. /** @import Module from "../Module" */
  13. /** @import { HashFunction } from "../util/Hash" */
  14. /** @import { AssociatedObjectForCache } from "../util/identifier" */
  15. /** @import { CssModuleBuildMeta } from "../css/CssModule" */
  16. // Numbers longer than this are written in exponential form ("...e+xx"), so a
  17. // longer string can't be a plain integer id.
  18. const MAX_NUMERIC_STRING_LENGTH = 21;
  19. // Char codes bounding the "looks like a plain number" fast check in `avoidNumber`.
  20. const CC_HYPHEN_MINUS = 45;
  21. const CC_DIGIT_ONE = 49;
  22. const CC_DIGIT_NINE = 57;
  23. // Long ids are truncated to this length and disambiguated with a short hash.
  24. const MAX_SHORTENED_STRING_LENGTH = 100;
  25. const SHORTENED_STRING_HASH_LENGTH = 6;
  26. /**
  27. * Returns hash.
  28. * @param {string} str string to hash
  29. * @param {number} len max length of the hash
  30. * @param {HashFunction} hashFunction hash function to use
  31. * @returns {string} hash
  32. */
  33. const getHash = (str, len, hashFunction) => {
  34. const hash = createHash(hashFunction);
  35. hash.update(str);
  36. const digest = hash.digest("hex");
  37. return digest.slice(0, len);
  38. };
  39. /**
  40. * Returns string prefixed by an underscore if it is a number.
  41. * @param {string} str the string
  42. * @returns {string} string prefixed by an underscore if it is a number
  43. */
  44. const avoidNumber = (str) => {
  45. if (str.length > MAX_NUMERIC_STRING_LENGTH) return str;
  46. const firstChar = str.charCodeAt(0);
  47. // Skip everything that doesn't start like a number ("-" or a digit).
  48. if (firstChar < CC_DIGIT_ONE) {
  49. if (firstChar !== CC_HYPHEN_MINUS) return str;
  50. } else if (firstChar > CC_DIGIT_NINE) {
  51. return str;
  52. }
  53. if (str === String(Number(str))) {
  54. return `_${str}`;
  55. }
  56. return str;
  57. };
  58. /**
  59. * Returns id representation.
  60. * @param {string} request the request
  61. * @returns {string} id representation
  62. */
  63. const requestToId = (request) =>
  64. request.replace(/^(\.\.?\/)+/, "").replace(/(^[.-]|[^a-z0-9_-])+/gi, "_");
  65. /**
  66. * Shorten long string.
  67. * @param {string} string the string
  68. * @param {string} delimiter separator for string and hash
  69. * @param {HashFunction} hashFunction hash function to use
  70. * @returns {string} string with limited max length to 100 chars
  71. */
  72. const shortenLongString = (string, delimiter, hashFunction) => {
  73. if (string.length < MAX_SHORTENED_STRING_LENGTH) return string;
  74. return (
  75. string.slice(
  76. 0,
  77. MAX_SHORTENED_STRING_LENGTH -
  78. SHORTENED_STRING_HASH_LENGTH -
  79. delimiter.length
  80. ) +
  81. delimiter +
  82. getHash(string, SHORTENED_STRING_HASH_LENGTH, hashFunction)
  83. );
  84. };
  85. /**
  86. * Gets short module name.
  87. * @param {Module} module the module
  88. * @param {string} context context directory
  89. * @param {AssociatedObjectForCache=} associatedObjectForCache an object to which the cache will be attached
  90. * @returns {string} short module name
  91. */
  92. const getShortModuleName = (module, context, associatedObjectForCache) => {
  93. const libIdent = module.libIdent({ context, associatedObjectForCache });
  94. if (libIdent) return avoidNumber(libIdent);
  95. const nameForCondition = module.nameForCondition();
  96. if (nameForCondition) {
  97. return avoidNumber(
  98. makePathsRelative(context, nameForCondition, associatedObjectForCache)
  99. );
  100. }
  101. return "";
  102. };
  103. /**
  104. * Gets long module name.
  105. * @param {string} shortName the short name
  106. * @param {Module} module the module
  107. * @param {string} context context directory
  108. * @param {HashFunction} hashFunction hash function to use
  109. * @param {AssociatedObjectForCache=} associatedObjectForCache an object to which the cache will be attached
  110. * @returns {string} long module name
  111. */
  112. const getLongModuleName = (
  113. shortName,
  114. module,
  115. context,
  116. hashFunction,
  117. associatedObjectForCache
  118. ) => {
  119. const fullName = getFullModuleName(module, context, associatedObjectForCache);
  120. return `${shortName}?${getHash(fullName, 4, hashFunction)}`;
  121. };
  122. /**
  123. * Gets full module name.
  124. * @param {Module} module the module
  125. * @param {string} context context directory
  126. * @param {AssociatedObjectForCache=} associatedObjectForCache an object to which the cache will be attached
  127. * @returns {string} full module name
  128. */
  129. const getFullModuleName = (module, context, associatedObjectForCache) =>
  130. makePathsRelative(context, module.identifier(), associatedObjectForCache);
  131. /**
  132. * Gets short chunk name.
  133. * @param {Chunk} chunk the chunk
  134. * @param {ChunkGraph} chunkGraph the chunk graph
  135. * @param {string} context context directory
  136. * @param {string} delimiter delimiter for names
  137. * @param {HashFunction} hashFunction hash function to use
  138. * @param {AssociatedObjectForCache=} associatedObjectForCache an object to which the cache will be attached
  139. * @returns {string} short chunk name
  140. */
  141. const getShortChunkName = (
  142. chunk,
  143. chunkGraph,
  144. context,
  145. delimiter,
  146. hashFunction,
  147. associatedObjectForCache
  148. ) => {
  149. const modules = chunkGraph.getChunkRootModules(chunk);
  150. const shortModuleNames = modules.map((m) =>
  151. requestToId(getShortModuleName(m, context, associatedObjectForCache))
  152. );
  153. chunk.idNameHints.sort();
  154. const chunkName = [...chunk.idNameHints, ...shortModuleNames]
  155. .filter(Boolean)
  156. .join(delimiter);
  157. return shortenLongString(chunkName, delimiter, hashFunction);
  158. };
  159. /**
  160. * Gets long chunk name.
  161. * @param {Chunk} chunk the chunk
  162. * @param {ChunkGraph} chunkGraph the chunk graph
  163. * @param {string} context context directory
  164. * @param {string} delimiter delimiter for names
  165. * @param {HashFunction} hashFunction hash function to use
  166. * @param {AssociatedObjectForCache=} associatedObjectForCache an object to which the cache will be attached
  167. * @returns {string} short chunk name
  168. */
  169. const getLongChunkName = (
  170. chunk,
  171. chunkGraph,
  172. context,
  173. delimiter,
  174. hashFunction,
  175. associatedObjectForCache
  176. ) => {
  177. const modules = chunkGraph.getChunkRootModules(chunk);
  178. const shortModuleNames = modules.map((m) =>
  179. requestToId(getShortModuleName(m, context, associatedObjectForCache))
  180. );
  181. const longModuleNames = modules.map((m) =>
  182. requestToId(
  183. getLongModuleName("", m, context, hashFunction, associatedObjectForCache)
  184. )
  185. );
  186. chunk.idNameHints.sort();
  187. const chunkName = [
  188. ...chunk.idNameHints,
  189. ...shortModuleNames,
  190. ...longModuleNames
  191. ]
  192. .filter(Boolean)
  193. .join(delimiter);
  194. return shortenLongString(chunkName, delimiter, hashFunction);
  195. };
  196. /**
  197. * Gets full chunk name.
  198. * @param {Chunk} chunk the chunk
  199. * @param {ChunkGraph} chunkGraph the chunk graph
  200. * @param {string} context context directory
  201. * @param {AssociatedObjectForCache=} associatedObjectForCache an object to which the cache will be attached
  202. * @returns {string} full chunk name
  203. */
  204. const getFullChunkName = (
  205. chunk,
  206. chunkGraph,
  207. context,
  208. associatedObjectForCache
  209. ) => {
  210. if (chunk.name) return chunk.name;
  211. const modules = chunkGraph.getChunkRootModules(chunk);
  212. const fullModuleNames = modules.map((m) =>
  213. makePathsRelative(context, m.identifier(), associatedObjectForCache)
  214. );
  215. return fullModuleNames.join();
  216. };
  217. /**
  218. * Adds to map of items.
  219. * @template K
  220. * @template V
  221. * @param {Map<K, V[]>} map a map from key to values
  222. * @param {K} key key
  223. * @param {V} value value
  224. * @returns {void}
  225. */
  226. const addToMapOfItems = (map, key, value) => {
  227. let array = map.get(key);
  228. if (array === undefined) {
  229. array = [];
  230. map.set(key, array);
  231. }
  232. array.push(value);
  233. };
  234. /** @typedef {Set<string>} UsedModuleIds */
  235. /**
  236. * Gets used module ids and modules.
  237. * @param {Compilation} compilation the compilation
  238. * @param {((module: Module) => boolean)=} filter filter modules
  239. * @returns {[UsedModuleIds, Module[]]} used module ids as strings and modules without id matching the filter
  240. */
  241. const getUsedModuleIdsAndModules = (compilation, filter) => {
  242. const chunkGraph = compilation.chunkGraph;
  243. /** @type {Module[]} */
  244. const modules = [];
  245. /** @type {UsedModuleIds} */
  246. const usedIds = new Set();
  247. if (compilation.usedModuleIds) {
  248. for (const id of compilation.usedModuleIds) {
  249. usedIds.add(String(id));
  250. }
  251. }
  252. for (const module of compilation.modules) {
  253. if (!module.needId) continue;
  254. const moduleId = chunkGraph.getModuleId(module);
  255. if (moduleId !== null) {
  256. usedIds.add(String(moduleId));
  257. } else if (
  258. (!filter || filter(module)) &&
  259. (chunkGraph.getNumberOfModuleChunks(module) !== 0 ||
  260. // CSS modules need IDs even when not in chunks, for generating CSS class names(i.e. [id]-[local])
  261. /** @type {CssModuleBuildMeta} */ (module.buildMeta).isCssModule ||
  262. /** @type {CssModuleBuildMeta} */ (module.buildMeta)
  263. .needIdInConcatenation)
  264. ) {
  265. modules.push(module);
  266. }
  267. }
  268. return [usedIds, modules];
  269. };
  270. /** @typedef {Set<string>} UsedChunkIds */
  271. /**
  272. * Gets used chunk ids.
  273. * @param {Compilation} compilation the compilation
  274. * @returns {UsedChunkIds} used chunk ids as strings
  275. */
  276. const getUsedChunkIds = (compilation) => {
  277. /** @type {UsedChunkIds} */
  278. const usedIds = new Set();
  279. if (compilation.usedChunkIds) {
  280. for (const id of compilation.usedChunkIds) {
  281. usedIds.add(String(id));
  282. }
  283. }
  284. for (const chunk of compilation.chunks) {
  285. const chunkId = chunk.id;
  286. if (chunkId !== null) {
  287. usedIds.add(String(chunkId));
  288. }
  289. }
  290. return usedIds;
  291. };
  292. /**
  293. * Returns list of items without a name.
  294. * @template T
  295. * @param {Iterable<T>} items list of items to be named
  296. * @param {(item: T) => string} getShortName get a short name for an item
  297. * @param {(item: T, name: string) => string} getLongName get a long name for an item
  298. * @param {(a: T, b: T) => -1 | 0 | 1} comparator order of items
  299. * @param {Set<string>} usedIds already used ids, will not be assigned
  300. * @param {(item: T, name: string) => void} assignName assign a name to an item
  301. * @returns {T[]} list of items without a name
  302. */
  303. const assignNames = (
  304. items,
  305. getShortName,
  306. getLongName,
  307. comparator,
  308. usedIds,
  309. assignName
  310. ) => {
  311. /**
  312. * Defines the map to item type used by this module.
  313. * @template T
  314. * @typedef {Map<string, T[]>} MapToItem
  315. */
  316. /** @type {MapToItem<T>} */
  317. const nameToItems = new Map();
  318. for (const item of items) {
  319. const name = getShortName(item);
  320. addToMapOfItems(nameToItems, name, item);
  321. }
  322. /** @type {MapToItem<T>} */
  323. const nameToItems2 = new Map();
  324. for (const [name, items] of nameToItems) {
  325. if (items.length > 1 || !name) {
  326. for (const item of items) {
  327. const longName = getLongName(item, name);
  328. addToMapOfItems(nameToItems2, longName, item);
  329. }
  330. } else {
  331. addToMapOfItems(nameToItems2, name, items[0]);
  332. }
  333. }
  334. /** @type {T[]} */
  335. const unnamedItems = [];
  336. for (const [name, items] of nameToItems2) {
  337. if (!name) {
  338. for (const item of items) {
  339. unnamedItems.push(item);
  340. }
  341. } else if (items.length === 1 && !usedIds.has(name)) {
  342. assignName(items[0], name);
  343. usedIds.add(name);
  344. } else {
  345. items.sort(comparator);
  346. let i = 0;
  347. for (const item of items) {
  348. while (usedIds.has(name + i)) i++;
  349. assignName(item, name + i);
  350. usedIds.add(name + i);
  351. i++;
  352. }
  353. }
  354. }
  355. unnamedItems.sort(comparator);
  356. return unnamedItems;
  357. };
  358. /**
  359. * Assign deterministic ids.
  360. * @template T
  361. * @param {T[]} items list of items to be named
  362. * @param {(item: T) => string} getName get a name for an item
  363. * @param {(a: T, n: T) => -1 | 0 | 1} comparator order of items
  364. * @param {(item: T, id: number) => boolean} assignId assign an id to an item
  365. * @param {number[]} ranges usable ranges for ids
  366. * @param {number} expandFactor factor to create more ranges
  367. * @param {number} extraSpace extra space to allocate, i. e. when some ids are already used
  368. * @param {number} salt salting number to initialize hashing
  369. * @returns {void}
  370. */
  371. const assignDeterministicIds = (
  372. items,
  373. getName,
  374. comparator,
  375. assignId,
  376. ranges = [10],
  377. expandFactor = 10,
  378. extraSpace = 0,
  379. salt = 0
  380. ) => {
  381. items.sort(comparator);
  382. // max 5% fill rate
  383. const optimalRange = Math.min(
  384. items.length * 20 + extraSpace,
  385. Number.MAX_SAFE_INTEGER
  386. );
  387. let i = 0;
  388. let range = ranges[i];
  389. while (range < optimalRange) {
  390. i++;
  391. if (i < ranges.length) {
  392. range = Math.min(ranges[i], Number.MAX_SAFE_INTEGER);
  393. } else if (expandFactor) {
  394. range = Math.min(range * expandFactor, Number.MAX_SAFE_INTEGER);
  395. } else {
  396. break;
  397. }
  398. }
  399. for (const item of items) {
  400. const ident = getName(item);
  401. /** @type {number} */
  402. let id;
  403. let i = salt;
  404. do {
  405. id = numberHash(ident + i++, range);
  406. } while (!assignId(item, id));
  407. }
  408. };
  409. /**
  410. * Assign ascending module ids.
  411. * @param {UsedModuleIds} usedIds used ids
  412. * @param {Iterable<Module>} modules the modules
  413. * @param {Compilation} compilation the compilation
  414. * @returns {void}
  415. */
  416. const assignAscendingModuleIds = (usedIds, modules, compilation) => {
  417. const chunkGraph = compilation.chunkGraph;
  418. let nextId = 0;
  419. /** @type {(mod: Module) => void} */
  420. let assignId;
  421. if (usedIds.size > 0) {
  422. /**
  423. * Processes the provided module.
  424. * @param {Module} module the module
  425. */
  426. assignId = (module) => {
  427. if (chunkGraph.getModuleId(module) === null) {
  428. while (usedIds.has(String(nextId))) nextId++;
  429. chunkGraph.setModuleId(module, nextId++);
  430. }
  431. };
  432. } else {
  433. /**
  434. * Processes the provided module.
  435. * @param {Module} module the module
  436. */
  437. assignId = (module) => {
  438. if (chunkGraph.getModuleId(module) === null) {
  439. chunkGraph.setModuleId(module, nextId++);
  440. }
  441. };
  442. }
  443. for (const module of modules) {
  444. assignId(module);
  445. }
  446. };
  447. /**
  448. * Assign ascending chunk ids.
  449. * @param {Iterable<Chunk>} chunks the chunks
  450. * @param {Compilation} compilation the compilation
  451. * @returns {void}
  452. */
  453. const assignAscendingChunkIds = (chunks, compilation) => {
  454. const usedIds = getUsedChunkIds(compilation);
  455. let nextId = 0;
  456. if (usedIds.size > 0) {
  457. for (const chunk of chunks) {
  458. if (chunk.id === null) {
  459. while (usedIds.has(String(nextId))) nextId++;
  460. chunk.id = nextId;
  461. chunk.ids = [nextId];
  462. nextId++;
  463. }
  464. }
  465. } else {
  466. for (const chunk of chunks) {
  467. if (chunk.id === null) {
  468. chunk.id = nextId;
  469. chunk.ids = [nextId];
  470. nextId++;
  471. }
  472. }
  473. }
  474. };
  475. module.exports.assignAscendingChunkIds = assignAscendingChunkIds;
  476. module.exports.assignAscendingModuleIds = assignAscendingModuleIds;
  477. module.exports.assignDeterministicIds = assignDeterministicIds;
  478. module.exports.assignNames = assignNames;
  479. module.exports.getFullChunkName = getFullChunkName;
  480. module.exports.getFullModuleName = getFullModuleName;
  481. module.exports.getLongChunkName = getLongChunkName;
  482. module.exports.getLongModuleName = getLongModuleName;
  483. module.exports.getShortChunkName = getShortChunkName;
  484. module.exports.getShortModuleName = getShortModuleName;
  485. module.exports.getUsedChunkIds = getUsedChunkIds;
  486. module.exports.getUsedModuleIdsAndModules = getUsedModuleIdsAndModules;
  487. module.exports.requestToId = requestToId;