StatsFactory.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const { HookMap, SyncBailHook, SyncWaterfallHook } = require("tapable");
  7. const smartGrouping = require("../util/smartGrouping");
  8. /** @import Chunk from "../Chunk" */
  9. /** @import { OriginRecord } from "../ChunkGroup" */
  10. /** @import Compilation, { Asset } from "../Compilation" */
  11. /** @import Dependency from "../Dependency" */
  12. /** @import Module from "../Module" */
  13. /** @import ModuleGraphConnection from "../ModuleGraphConnection" */
  14. /** @typedef {import("../util/comparators").Comparator<EXPECTED_ANY>} Comparator */
  15. /** @import ModuleProfile from "../ModuleProfile" */
  16. /** @import { RuntimeSpec } from "../util/runtime" */
  17. /**
  18. * Defines the group config type used by this module.
  19. * @template T, R
  20. * @typedef {import("../util/smartGrouping").GroupConfig<T, R>} GroupConfig
  21. */
  22. /**
  23. * @import {
  24. * ChunkGroupInfoWithName,
  25. * ModuleTrace,
  26. * StatsAsset,
  27. * StatsChunk,
  28. * StatsChunkGroup,
  29. * StatsChunkOrigin,
  30. * StatsCompilation,
  31. * StatsError,
  32. * StatsModule,
  33. * StatsModuleReason,
  34. * StatsModuleTraceDependency,
  35. * StatsModuleTraceItem,
  36. * StatsProfile
  37. * } from "./DefaultStatsFactoryPlugin"
  38. */
  39. /**
  40. * Defines the known stats factory context type used by this module.
  41. * @typedef {object} KnownStatsFactoryContext
  42. * @property {string} type
  43. * @property {Compilation} compilation
  44. * @property {(path: string) => string} makePathsRelative
  45. * @property {Set<Module>} rootModules
  46. * @property {Map<string, Chunk[]>} compilationFileToChunks
  47. * @property {Map<string, Chunk[]>} compilationAuxiliaryFileToChunks
  48. * @property {RuntimeSpec} runtime
  49. * @property {(compilation: Compilation) => Error[]} cachedGetErrors
  50. * @property {(compilation: Compilation) => Error[]} cachedGetWarnings
  51. */
  52. /** @typedef {KnownStatsFactoryContext & Record<string, EXPECTED_ANY>} StatsFactoryContext */
  53. // StatsLogging StatsLoggingEntry
  54. /**
  55. * Defines the stats object type used by this module.
  56. * @template T
  57. * @template F
  58. * @typedef {T extends Compilation ? StatsCompilation : T extends ChunkGroupInfoWithName ? StatsChunkGroup : T extends Chunk ? StatsChunk : T extends OriginRecord ? StatsChunkOrigin : T extends Module ? StatsModule : T extends ModuleGraphConnection ? StatsModuleReason : T extends Asset ? StatsAsset : T extends ModuleTrace ? StatsModuleTraceItem : T extends Dependency ? StatsModuleTraceDependency : T extends Error ? StatsError : T extends ModuleProfile ? StatsProfile : F} StatsObject
  59. */
  60. /**
  61. * Defines the created object type used by this module.
  62. * @template T
  63. * @template F
  64. * @typedef {T extends ChunkGroupInfoWithName[] ? Record<string, StatsObject<ChunkGroupInfoWithName, F>> : T extends (infer V)[] ? StatsObject<V, F>[] : StatsObject<T, F>} CreatedObject
  65. */
  66. /** @typedef {EXPECTED_ANY} ObjectForExtract */
  67. /** @typedef {EXPECTED_ANY} FactoryData */
  68. /** @typedef {EXPECTED_ANY} FactoryDataItem */
  69. /** @typedef {EXPECTED_ANY} Result */
  70. /**
  71. * Defines the stats factory hooks type used by this module.
  72. * @typedef {object} StatsFactoryHooks
  73. * @property {HookMap<SyncBailHook<[ObjectForExtract, FactoryData, StatsFactoryContext], void>>} extract
  74. * @property {HookMap<SyncBailHook<[FactoryDataItem, StatsFactoryContext, number, number], boolean | void>>} filter
  75. * @property {HookMap<SyncBailHook<[Comparator[], StatsFactoryContext], void>>} sort
  76. * @property {HookMap<SyncBailHook<[FactoryDataItem, StatsFactoryContext, number, number], boolean | void>>} filterSorted
  77. * @property {HookMap<SyncBailHook<[GroupConfig<EXPECTED_ANY, EXPECTED_ANY>[], StatsFactoryContext], void>>} groupResults
  78. * @property {HookMap<SyncBailHook<[Comparator[], StatsFactoryContext], void>>} sortResults
  79. * @property {HookMap<SyncBailHook<[FactoryDataItem, StatsFactoryContext, number, number], boolean | void>>} filterResults
  80. * @property {HookMap<SyncBailHook<[FactoryDataItem[], StatsFactoryContext], Result | void>>} merge
  81. * @property {HookMap<SyncBailHook<[Result, StatsFactoryContext], Result>>} result
  82. * @property {HookMap<SyncBailHook<[FactoryDataItem, StatsFactoryContext], string | void>>} getItemName
  83. * @property {HookMap<SyncBailHook<[FactoryDataItem, StatsFactoryContext], StatsFactory | void>>} getItemFactory
  84. */
  85. /**
  86. * Represents the stats factory runtime component.
  87. * @template T
  88. * @typedef {Map<string, T[]>} Caches
  89. */
  90. class StatsFactory {
  91. constructor() {
  92. /** @type {StatsFactoryHooks} */
  93. this.hooks = Object.freeze({
  94. extract: new HookMap(
  95. () => new SyncBailHook(["object", "data", "context"])
  96. ),
  97. filter: new HookMap(
  98. () => new SyncBailHook(["item", "context", "index", "unfilteredIndex"])
  99. ),
  100. sort: new HookMap(() => new SyncBailHook(["comparators", "context"])),
  101. filterSorted: new HookMap(
  102. () => new SyncBailHook(["item", "context", "index", "unfilteredIndex"])
  103. ),
  104. groupResults: new HookMap(
  105. () => new SyncBailHook(["groupConfigs", "context"])
  106. ),
  107. sortResults: new HookMap(
  108. () => new SyncBailHook(["comparators", "context"])
  109. ),
  110. filterResults: new HookMap(
  111. () => new SyncBailHook(["item", "context", "index", "unfilteredIndex"])
  112. ),
  113. merge: new HookMap(() => new SyncBailHook(["items", "context"])),
  114. result: new HookMap(() => new SyncWaterfallHook(["result", "context"])),
  115. getItemName: new HookMap(() => new SyncBailHook(["item", "context"])),
  116. getItemFactory: new HookMap(() => new SyncBailHook(["item", "context"]))
  117. });
  118. const hooks = this.hooks;
  119. this._caches =
  120. /** @type {{ [Key in keyof StatsFactoryHooks]: StatsFactoryHooks[Key] extends HookMap<infer H> ? Map<string, H[]> : never }} */ ({});
  121. for (const key of Object.keys(hooks)) {
  122. this._caches[/** @type {keyof StatsFactoryHooks} */ (key)] = new Map();
  123. }
  124. /** @type {boolean} */
  125. this._inCreate = false;
  126. }
  127. /**
  128. * Get all level hooks.
  129. * @template {StatsFactoryHooks[keyof StatsFactoryHooks]} HM
  130. * @template {HM extends HookMap<infer H> ? H : never} H
  131. * @param {HM} hookMap hook map
  132. * @param {Caches<H>} cache cache
  133. * @param {string} type type
  134. * @returns {H[]} hooks
  135. * @private
  136. */
  137. _getAllLevelHooks(hookMap, cache, type) {
  138. const cacheEntry = cache.get(type);
  139. if (cacheEntry !== undefined) {
  140. return cacheEntry;
  141. }
  142. const hooks = /** @type {H[]} */ ([]);
  143. const typeParts = type.split(".");
  144. for (let i = 0; i < typeParts.length; i++) {
  145. const hook = /** @type {H} */ (hookMap.get(typeParts.slice(i).join(".")));
  146. if (hook) {
  147. hooks.push(hook);
  148. }
  149. }
  150. cache.set(type, hooks);
  151. return hooks;
  152. }
  153. /**
  154. * Returns hook.
  155. * @template {StatsFactoryHooks[keyof StatsFactoryHooks]} HM
  156. * @template {HM extends HookMap<infer H> ? H : never} H
  157. * @template {H extends import("tapable").Hook<infer A, infer R> ? R : never} R
  158. * @param {HM} hookMap hook map
  159. * @param {Caches<H>} cache cache
  160. * @param {string} type type
  161. * @param {(hook: H) => R | void} fn fn
  162. * @returns {R | void} hook
  163. * @private
  164. */
  165. _forEachLevel(hookMap, cache, type, fn) {
  166. for (const hook of this._getAllLevelHooks(hookMap, cache, type)) {
  167. const result = fn(/** @type {H} */ (hook));
  168. if (result !== undefined) return result;
  169. }
  170. }
  171. /**
  172. * For each level waterfall.
  173. * @template {StatsFactoryHooks[keyof StatsFactoryHooks]} HM
  174. * @template {HM extends HookMap<infer H> ? H : never} H
  175. * @template [D=EXPECTED_ANY]
  176. * @param {HM} hookMap hook map
  177. * @param {Caches<H>} cache cache
  178. * @param {string} type type
  179. * @param {D} data data
  180. * @param {(hook: H, data: D) => D} fn fn
  181. * @returns {D} data
  182. * @private
  183. */
  184. _forEachLevelWaterfall(hookMap, cache, type, data, fn) {
  185. for (const hook of this._getAllLevelHooks(hookMap, cache, type)) {
  186. data = fn(/** @type {H} */ (hook), data);
  187. }
  188. return data;
  189. }
  190. /**
  191. * For each level filter.
  192. * @template {StatsFactoryHooks[keyof StatsFactoryHooks]} T
  193. * @template {T extends HookMap<infer H> ? H : never} H
  194. * @template [D=EXPECTED_ANY]
  195. * @param {T} hookMap hook map
  196. * @param {Caches<H>} cache cache
  197. * @param {string} type type
  198. * @param {D[]} items items
  199. * @param {(hook: H, item: D, idx: number, i: number) => boolean | void} fn fn
  200. * @param {boolean} forceClone force clone
  201. * @returns {D[]} result for each level
  202. * @private
  203. */
  204. _forEachLevelFilter(hookMap, cache, type, items, fn, forceClone) {
  205. const hooks = this._getAllLevelHooks(hookMap, cache, type);
  206. if (hooks.length === 0) return forceClone ? [...items] : items;
  207. let i = 0;
  208. return items.filter((item, idx) => {
  209. for (const hook of hooks) {
  210. const r = fn(/** @type {H} */ (hook), item, idx, i);
  211. if (r !== undefined) {
  212. if (r) i++;
  213. return r;
  214. }
  215. }
  216. i++;
  217. return true;
  218. });
  219. }
  220. /**
  221. * Returns created object.
  222. * @template FactoryData
  223. * @template FallbackCreatedObject
  224. * @param {string} type type
  225. * @param {FactoryData} data factory data
  226. * @param {Omit<StatsFactoryContext, "type">} baseContext context used as base
  227. * @returns {CreatedObject<FactoryData, FallbackCreatedObject>} created object
  228. */
  229. create(type, data, baseContext) {
  230. if (this._inCreate) {
  231. return this._create(type, data, baseContext);
  232. }
  233. try {
  234. this._inCreate = true;
  235. return this._create(type, data, baseContext);
  236. } finally {
  237. for (const key of Object.keys(this._caches)) {
  238. this._caches[/** @type {keyof StatsFactoryHooks} */ (key)].clear();
  239. }
  240. this._inCreate = false;
  241. }
  242. }
  243. /**
  244. * Returns created object.
  245. * @private
  246. * @template FactoryData
  247. * @template FallbackCreatedObject
  248. * @param {string} type type
  249. * @param {FactoryData} data factory data
  250. * @param {Omit<StatsFactoryContext, "type">} baseContext context used as base
  251. * @returns {CreatedObject<FactoryData, FallbackCreatedObject>} created object
  252. */
  253. _create(type, data, baseContext) {
  254. const context = /** @type {StatsFactoryContext} */ ({
  255. ...baseContext,
  256. type,
  257. [type]: data
  258. });
  259. if (Array.isArray(data)) {
  260. // run filter on unsorted items
  261. const items = this._forEachLevelFilter(
  262. this.hooks.filter,
  263. this._caches.filter,
  264. type,
  265. data,
  266. (h, r, idx, i) => h.call(r, context, idx, i),
  267. true
  268. );
  269. // sort items
  270. /** @type {Comparator[]} */
  271. const comparators = [];
  272. this._forEachLevel(this.hooks.sort, this._caches.sort, type, (h) =>
  273. h.call(comparators, context)
  274. );
  275. if (comparators.length > 0) {
  276. sortWithOriginalOrder(items, comparators);
  277. }
  278. // run filter on sorted items
  279. const items2 = this._forEachLevelFilter(
  280. this.hooks.filterSorted,
  281. this._caches.filterSorted,
  282. type,
  283. items,
  284. (h, r, idx, i) => h.call(r, context, idx, i),
  285. false
  286. );
  287. // reuse one item context; create() spreads it synchronously, so mutating
  288. // `_index`/the name key between items is safe and skips a per-item spread
  289. /** @type {StatsFactoryContext} */
  290. const itemContext = { ...context };
  291. const itemNameType = `${type}[]`;
  292. /** @type {string | void} */
  293. let prevItemName;
  294. let resultItems = items2.map((item, i) => {
  295. itemContext._index = i;
  296. // run getItemName
  297. const itemName = this._forEachLevel(
  298. this.hooks.getItemName,
  299. this._caches.getItemName,
  300. itemNameType,
  301. (h) => h.call(item, itemContext)
  302. );
  303. // drop a previous item's name key before adding this one's
  304. if (prevItemName !== undefined && prevItemName !== itemName) {
  305. delete itemContext[prevItemName];
  306. }
  307. if (itemName) itemContext[itemName] = item;
  308. prevItemName = itemName;
  309. const innerType = itemName ? `${type}[].${itemName}` : itemNameType;
  310. // run getItemFactory
  311. const itemFactory =
  312. this._forEachLevel(
  313. this.hooks.getItemFactory,
  314. this._caches.getItemFactory,
  315. innerType,
  316. (h) => h.call(item, itemContext)
  317. ) || this;
  318. // run item factory
  319. return itemFactory.create(innerType, item, itemContext);
  320. });
  321. // sort result items
  322. /** @type {Comparator[]} */
  323. const comparators2 = [];
  324. this._forEachLevel(
  325. this.hooks.sortResults,
  326. this._caches.sortResults,
  327. type,
  328. (h) => h.call(comparators2, context)
  329. );
  330. if (comparators2.length > 0) {
  331. sortWithOriginalOrder(resultItems, comparators2);
  332. }
  333. // group result items
  334. /** @type {GroupConfig<EXPECTED_ANY, EXPECTED_ANY>[]} */
  335. const groupConfigs = [];
  336. this._forEachLevel(
  337. this.hooks.groupResults,
  338. this._caches.groupResults,
  339. type,
  340. (h) => h.call(groupConfigs, context)
  341. );
  342. if (groupConfigs.length > 0) {
  343. resultItems = smartGrouping(resultItems, groupConfigs);
  344. }
  345. // run filter on sorted result items
  346. const finalResultItems = this._forEachLevelFilter(
  347. this.hooks.filterResults,
  348. this._caches.filterResults,
  349. type,
  350. resultItems,
  351. (h, r, idx, i) => h.call(r, context, idx, i),
  352. false
  353. );
  354. // run merge on mapped items
  355. let result = this._forEachLevel(
  356. this.hooks.merge,
  357. this._caches.merge,
  358. type,
  359. (h) => h.call(finalResultItems, context)
  360. );
  361. if (result === undefined) result = finalResultItems;
  362. // run result on merged items
  363. return this._forEachLevelWaterfall(
  364. this.hooks.result,
  365. this._caches.result,
  366. type,
  367. result,
  368. (h, r) => h.call(r, context)
  369. );
  370. }
  371. /** @type {ObjectForExtract} */
  372. const object = {};
  373. // run extract on value
  374. this._forEachLevel(this.hooks.extract, this._caches.extract, type, (h) =>
  375. h.call(object, data, context)
  376. );
  377. // run result on extracted object
  378. return this._forEachLevelWaterfall(
  379. this.hooks.result,
  380. this._caches.result,
  381. type,
  382. object,
  383. (h, r) => h.call(r, context)
  384. );
  385. }
  386. }
  387. /**
  388. * Stable in-place sort applying comparators in order, keeping the original order
  389. * for equal items. Inlined instead of `concatComparators(...c, keepOriginalOrder())`
  390. * because that combination is single-use per sort and only thrashes the comparator
  391. * caches (a fresh tiebreaker closure every call allocates a new cache entry).
  392. * @param {EXPECTED_ANY[]} items items to sort in place
  393. * @param {Comparator[]} comparators comparators applied in order
  394. * @returns {void}
  395. */
  396. const sortWithOriginalOrder = (items, comparators) => {
  397. // original-index tiebreaker keeps the sort stable on engines without a stable Array.sort
  398. /** @type {Map<EXPECTED_ANY, number>} */
  399. const originalOrder = new Map();
  400. for (let i = 0; i < items.length; i++) {
  401. originalOrder.set(items[i], i);
  402. }
  403. const count = comparators.length;
  404. items.sort((a, b) => {
  405. for (let i = 0; i < count; i++) {
  406. const res = comparators[i](a, b);
  407. if (res !== 0) return res;
  408. }
  409. return (
  410. /** @type {number} */ (originalOrder.get(a)) -
  411. /** @type {number} */ (originalOrder.get(b))
  412. );
  413. });
  414. };
  415. module.exports = StatsFactory;