StatsPrinter.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  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. /** @typedef {import("./DefaultStatsFactoryPlugin").StatsAsset} StatsAsset */
  8. /** @typedef {import("./DefaultStatsFactoryPlugin").StatsChunk} StatsChunk */
  9. /** @typedef {import("./DefaultStatsFactoryPlugin").StatsChunkGroup} StatsChunkGroup */
  10. /** @typedef {import("./DefaultStatsFactoryPlugin").StatsCompilation} StatsCompilation */
  11. /** @typedef {import("./DefaultStatsFactoryPlugin").StatsError} StatsError */
  12. /** @typedef {import("./DefaultStatsFactoryPlugin").StatsLogging} StatsLogging */
  13. /** @typedef {import("./DefaultStatsFactoryPlugin").StatsModule} StatsModule */
  14. /** @typedef {import("./DefaultStatsFactoryPlugin").StatsModuleIssuer} StatsModuleIssuer */
  15. /** @typedef {import("./DefaultStatsFactoryPlugin").StatsModuleReason} StatsModuleReason */
  16. /** @typedef {import("./DefaultStatsFactoryPlugin").StatsModuleTraceDependency} StatsModuleTraceDependency */
  17. /** @typedef {import("./DefaultStatsFactoryPlugin").StatsModuleTraceItem} StatsModuleTraceItem */
  18. /** @typedef {import("./DefaultStatsFactoryPlugin").StatsProfile} StatsProfile */
  19. /**
  20. * Defines the printed element type used by this module.
  21. * @typedef {object} PrintedElement
  22. * @property {string} element
  23. * @property {string | undefined} content
  24. */
  25. /**
  26. * Defines the known stats printer context type used by this module.
  27. * @typedef {object} KnownStatsPrinterContext
  28. * @property {string=} type
  29. * @property {StatsCompilation=} compilation
  30. * @property {StatsChunkGroup=} chunkGroup
  31. * @property {string=} chunkGroupKind
  32. * @property {StatsAsset=} asset
  33. * @property {StatsModule=} module
  34. * @property {StatsChunk=} chunk
  35. * @property {StatsModuleReason=} moduleReason
  36. * @property {StatsModuleIssuer=} moduleIssuer
  37. * @property {StatsError=} error
  38. * @property {StatsProfile=} profile
  39. * @property {StatsLogging=} logging
  40. * @property {StatsModuleTraceItem=} moduleTraceItem
  41. * @property {StatsModuleTraceDependency=} moduleTraceDependency
  42. */
  43. /** @typedef {(value: string | number) => string} ColorFunction */
  44. /**
  45. * Defines the known stats printer color functions type used by this module.
  46. * @typedef {object} KnownStatsPrinterColorFunctions
  47. * @property {ColorFunction=} bold
  48. * @property {ColorFunction=} yellow
  49. * @property {ColorFunction=} red
  50. * @property {ColorFunction=} green
  51. * @property {ColorFunction=} magenta
  52. * @property {ColorFunction=} cyan
  53. */
  54. /**
  55. * Defines the known stats printer formatters type used by this module.
  56. * @typedef {object} KnownStatsPrinterFormatters
  57. * @property {(file: string, oversize?: boolean) => string=} formatFilename
  58. * @property {(id: string | number) => string=} formatModuleId
  59. * @property {(id: string | number, direction?: "parent" | "child" | "sibling") => string=} formatChunkId
  60. * @property {(size: number) => string=} formatSize
  61. * @property {(size: string) => string=} formatLayer
  62. * @property {(dateTime: number) => string=} formatDateTime
  63. * @property {(flag: string) => string=} formatFlag
  64. * @property {(time: number, boldQuantity?: boolean) => string=} formatTime
  65. * @property {(message: string) => string=} formatError
  66. */
  67. /** @typedef {KnownStatsPrinterColorFunctions & KnownStatsPrinterFormatters & KnownStatsPrinterContext & Record<string, EXPECTED_ANY>} StatsPrinterContext */
  68. /** @typedef {StatsPrinterContext & Required<KnownStatsPrinterColorFunctions> & Required<KnownStatsPrinterFormatters> & { type: string }} StatsPrinterContextWithExtra */
  69. /** @typedef {EXPECTED_ANY} PrintObject */
  70. /**
  71. * Represents the stats printer runtime component.
  72. * @typedef {object} StatsPrintHooks
  73. * @property {HookMap<SyncBailHook<[string[], StatsPrinterContext], void>>} sortElements
  74. * @property {HookMap<SyncBailHook<[PrintedElement[], StatsPrinterContext], string | undefined | void>>} printElements
  75. * @property {HookMap<SyncBailHook<[PrintObject[], StatsPrinterContext], boolean | void>>} sortItems
  76. * @property {HookMap<SyncBailHook<[PrintObject, StatsPrinterContext], string | void>>} getItemName
  77. * @property {HookMap<SyncBailHook<[string[], StatsPrinterContext], string | undefined>>} printItems
  78. * @property {HookMap<SyncBailHook<[PrintObject, StatsPrinterContext], string | undefined | void>>} print
  79. * @property {HookMap<SyncWaterfallHook<[string, StatsPrinterContext]>>} result
  80. */
  81. class StatsPrinter {
  82. constructor() {
  83. /** @type {StatsPrintHooks} */
  84. this.hooks = Object.freeze({
  85. sortElements: new HookMap(
  86. () => new SyncBailHook(["elements", "context"])
  87. ),
  88. printElements: new HookMap(
  89. () => new SyncBailHook(["printedElements", "context"])
  90. ),
  91. sortItems: new HookMap(() => new SyncBailHook(["items", "context"])),
  92. getItemName: new HookMap(() => new SyncBailHook(["item", "context"])),
  93. printItems: new HookMap(
  94. () => new SyncBailHook(["printedItems", "context"])
  95. ),
  96. print: new HookMap(() => new SyncBailHook(["object", "context"])),
  97. result: new HookMap(() => new SyncWaterfallHook(["result", "context"]))
  98. });
  99. /** @type {Map<StatsPrintHooks[keyof StatsPrintHooks], Map<string, import("tapable").Hook<EXPECTED_ANY, EXPECTED_ANY>[]>>} */
  100. this._levelHookCache = new Map();
  101. this._inPrint = false;
  102. }
  103. /**
  104. * get all level hooks
  105. * @private
  106. * @template {StatsPrintHooks[keyof StatsPrintHooks]} HM
  107. * @template {HM extends HookMap<infer H> ? H : never} H
  108. * @param {HM} hookMap hook map
  109. * @param {string} type type
  110. * @returns {H[]} hooks
  111. */
  112. _getAllLevelHooks(hookMap, type) {
  113. let cache = this._levelHookCache.get(hookMap);
  114. if (cache === undefined) {
  115. cache = new Map();
  116. this._levelHookCache.set(hookMap, cache);
  117. }
  118. const cacheEntry = cache.get(type);
  119. if (cacheEntry !== undefined) {
  120. return /** @type {H[]} */ (cacheEntry);
  121. }
  122. /** @type {H[]} */
  123. const hooks = [];
  124. const typeParts = type.split(".");
  125. for (let i = 0; i < typeParts.length; i++) {
  126. const hook = /** @type {H} */ (hookMap.get(typeParts.slice(i).join(".")));
  127. if (hook) {
  128. hooks.push(hook);
  129. }
  130. }
  131. cache.set(type, hooks);
  132. return hooks;
  133. }
  134. /**
  135. * Run `fn` for each level
  136. * @private
  137. * @template {StatsPrintHooks[keyof StatsPrintHooks]} HM
  138. * @template {HM extends HookMap<infer H> ? H : never} H
  139. * @template {H extends import("tapable").Hook<EXPECTED_ANY, infer R> ? R : never} R
  140. * @param {HM} hookMap hook map
  141. * @param {string} type type
  142. * @param {(hooK: H) => R | undefined | void} fn fn
  143. * @returns {R | undefined} hook
  144. */
  145. _forEachLevel(hookMap, type, fn) {
  146. for (const hook of this._getAllLevelHooks(hookMap, type)) {
  147. const result = fn(/** @type {H} */ (hook));
  148. if (result !== undefined) return /** @type {R} */ (result);
  149. }
  150. }
  151. /**
  152. * Run `fn` for each level
  153. * @private
  154. * @template {StatsPrintHooks[keyof StatsPrintHooks]} HM
  155. * @template {HM extends HookMap<infer H> ? H : never} H
  156. * @param {HM} hookMap hook map
  157. * @param {string} type type
  158. * @param {string} data data
  159. * @param {(hook: H, data: string) => string} fn fn
  160. * @returns {string | undefined} result of `fn`
  161. */
  162. _forEachLevelWaterfall(hookMap, type, data, fn) {
  163. for (const hook of this._getAllLevelHooks(hookMap, type)) {
  164. data = fn(/** @type {H} */ (hook), data);
  165. }
  166. return data;
  167. }
  168. /**
  169. * Returns printed result.
  170. * @param {string} type The type
  171. * @param {PrintObject} object Object to print
  172. * @param {StatsPrinterContext=} baseContext The base context
  173. * @returns {string | undefined} printed result
  174. */
  175. print(type, object, baseContext) {
  176. if (this._inPrint) {
  177. return this._print(type, object, baseContext);
  178. }
  179. try {
  180. this._inPrint = true;
  181. return this._print(type, object, baseContext);
  182. } finally {
  183. this._levelHookCache.clear();
  184. this._inPrint = false;
  185. }
  186. }
  187. /**
  188. * Returns printed result.
  189. * @private
  190. * @param {string} type type
  191. * @param {PrintObject} object object
  192. * @param {StatsPrinterContext=} baseContext context
  193. * @returns {string | undefined} printed result
  194. */
  195. _print(type, object, baseContext) {
  196. /** @type {StatsPrinterContext} */
  197. const context = {
  198. ...baseContext,
  199. type,
  200. [type]: object
  201. };
  202. /** @type {string | undefined} */
  203. let printResult = this._forEachLevel(this.hooks.print, type, (hook) =>
  204. hook.call(object, context)
  205. );
  206. if (printResult === undefined) {
  207. if (Array.isArray(object)) {
  208. const sortedItems = [...object];
  209. this._forEachLevel(this.hooks.sortItems, type, (h) =>
  210. h.call(
  211. sortedItems,
  212. /** @type {StatsPrinterContextWithExtra} */
  213. (context)
  214. )
  215. );
  216. const printedItems = sortedItems.map((item, i) => {
  217. const itemContext =
  218. /** @type {StatsPrinterContextWithExtra} */
  219. ({
  220. ...context,
  221. _index: i
  222. });
  223. const itemName = this._forEachLevel(
  224. this.hooks.getItemName,
  225. `${type}[]`,
  226. (h) => h.call(item, itemContext)
  227. );
  228. if (itemName) itemContext[itemName] = item;
  229. return this.print(
  230. itemName ? `${type}[].${itemName}` : `${type}[]`,
  231. item,
  232. itemContext
  233. );
  234. });
  235. printResult = this._forEachLevel(this.hooks.printItems, type, (h) =>
  236. h.call(
  237. /** @type {string[]} */ (printedItems),
  238. /** @type {StatsPrinterContextWithExtra} */
  239. (context)
  240. )
  241. );
  242. if (printResult === undefined) {
  243. const result = printedItems.filter(Boolean);
  244. if (result.length > 0) printResult = result.join("\n");
  245. }
  246. } else if (object !== null && typeof object === "object") {
  247. const elements = Object.keys(object).filter(
  248. (key) => object[key] !== undefined
  249. );
  250. this._forEachLevel(this.hooks.sortElements, type, (h) =>
  251. h.call(
  252. elements,
  253. /** @type {StatsPrinterContextWithExtra} */
  254. (context)
  255. )
  256. );
  257. const printedElements = elements.map((element) => {
  258. const content = this.print(`${type}.${element}`, object[element], {
  259. ...context,
  260. _parent: object,
  261. _element: element,
  262. [element]: object[element]
  263. });
  264. return { element, content };
  265. });
  266. printResult = this._forEachLevel(this.hooks.printElements, type, (h) =>
  267. h.call(
  268. printedElements,
  269. /** @type {StatsPrinterContextWithExtra} */
  270. (context)
  271. )
  272. );
  273. if (printResult === undefined) {
  274. const result = printedElements.map((e) => e.content).filter(Boolean);
  275. if (result.length > 0) printResult = result.join("\n");
  276. }
  277. }
  278. }
  279. return this._forEachLevelWaterfall(
  280. this.hooks.result,
  281. type,
  282. /** @type {string} */
  283. (printResult),
  284. (h, r) => h.call(r, /** @type {StatsPrinterContextWithExtra} */ (context))
  285. );
  286. }
  287. }
  288. module.exports = StatsPrinter;