LazyBarrel.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Haijie Xie @hai-x
  4. */
  5. "use strict";
  6. const Dependency = require("./Dependency");
  7. /** @import Compilation, { DependencyConstructor } from "./Compilation" */
  8. /** @import Module from "./Module" */
  9. /** @import ModuleFactory from "./ModuleFactory" */
  10. /**
  11. * Defines the deferred request group type used by this module.
  12. * @typedef {object} DependencyGroup
  13. * @property {ModuleFactory} factory factory for the request
  14. * @property {Dependency[]} dependencies deferred dependencies of the request
  15. * @property {string | undefined} context request context
  16. */
  17. /**
  18. * Defines the lazy barrel state type used by this module.
  19. * @typedef {object} LazyBarrelState
  20. * @property {Set<string> | true} forwardedIds export names requested so far, true for all
  21. * @property {LazyBarrelInfo | undefined} lazyBarrelInfo deferred dependencies, undefined until classified
  22. */
  23. /**
  24. * Defines the lazy barrel unlazy item type used by this module.
  25. * @typedef {object} UnlazyDependencyInfo
  26. * @property {ModuleFactory} factory factory for the request
  27. * @property {Dependency[]} dependencies deferred dependencies of the request
  28. * @property {string | undefined} context request context
  29. * @property {Module} originModule the lazy barrel module
  30. */
  31. /**
  32. * Collects the export names a dependency group requests from its target.
  33. * @param {Dependency[]} dependencies dependency group resolving to one module
  34. * @returns {Set<string> | true} requested export names, true for all
  35. */
  36. function getForwardedIds(dependencies) {
  37. /** @type {Set<string>} */
  38. const ids = new Set();
  39. for (const dependency of dependencies) {
  40. // TODO remove in webpack 6
  41. // It may be missing on custom dependency types not extending the base Dependency
  42. if (!("getForwardId" in dependency)) continue;
  43. const id = dependency.getForwardId();
  44. if (id === true) return true;
  45. if (id !== null) ids.add(id);
  46. }
  47. return ids;
  48. }
  49. /**
  50. * Tracks the deferred (not yet factorized/built) dependencies of a
  51. * side-effect-free barrel module and which export names resolve to them.
  52. */
  53. class LazyBarrelInfo {
  54. constructor() {
  55. /** @type {Map<string, string>} forward id -> request key */
  56. this._forwardIdToRequest = new Map();
  57. /** @type {Map<string, DependencyGroup>} request key -> still-deferred group */
  58. this._requestToDepGroup = new Map();
  59. /** @type {Set<string> | undefined} locally provided export names */
  60. this._terminalIds = undefined;
  61. /** @type {Set<string> | undefined} requests of star re-exports */
  62. this._fallbackRequests = undefined;
  63. }
  64. /**
  65. * Records a deferred dependency under its request key.
  66. * @param {string} requestKey request key
  67. * @param {Dependency} dependency dependency to defer
  68. * @param {ModuleFactory} factory factory for the request
  69. * @param {string | undefined} context request context
  70. */
  71. addLazy(requestKey, dependency, factory, context) {
  72. let group = this._requestToDepGroup.get(requestKey);
  73. if (group === undefined) {
  74. group = { factory, dependencies: [], context };
  75. this._requestToDepGroup.set(requestKey, group);
  76. }
  77. group.dependencies.push(dependency);
  78. }
  79. /**
  80. * Maps an export name to the request providing it.
  81. * @param {string} id export name
  82. * @param {string} requestKey request key
  83. */
  84. addForwardId(id, requestKey) {
  85. this._forwardIdToRequest.set(id, requestKey);
  86. }
  87. /**
  88. * Registers a locally provided export name.
  89. * @param {string} id export name
  90. */
  91. addTerminal(id) {
  92. if (this._terminalIds === undefined) this._terminalIds = new Set();
  93. this._terminalIds.add(id);
  94. }
  95. /**
  96. * Registers a star re-export request key.
  97. * @param {string} requestKey request key
  98. */
  99. addFallback(requestKey) {
  100. if (this._fallbackRequests === undefined) {
  101. this._fallbackRequests = new Set();
  102. }
  103. this._fallbackRequests.add(requestKey);
  104. }
  105. /**
  106. * Returns whether nothing is deferred.
  107. * @returns {boolean} true, when no dependency is deferred
  108. */
  109. isEmpty() {
  110. return this._requestToDepGroup.size === 0;
  111. }
  112. /**
  113. * Returns the requests of the still-lazy groups.
  114. * @returns {Iterable<string>} still-lazy requests
  115. */
  116. getLazyRequests() {
  117. return this._requestToDepGroup.keys();
  118. }
  119. /**
  120. * Drops the export-name lookups once no group stays deferred; they only serve
  121. * to find still-deferred groups, so keeping them per module wastes memory.
  122. */
  123. _release() {
  124. this._forwardIdToRequest.clear();
  125. this._terminalIds = undefined;
  126. this._fallbackRequests = undefined;
  127. }
  128. /**
  129. * Removes and returns the groups needed to provide the requested export names.
  130. * @param {Set<string> | true} forwardedIds requested export names, true for all
  131. * @returns {DependencyGroup[]} groups that must be processed now
  132. */
  133. request(forwardedIds) {
  134. /** @type {DependencyGroup[]} */
  135. const result = [];
  136. /**
  137. * @param {DependencyGroup} group taken group
  138. */
  139. const unlazy = (group) => {
  140. for (const dependency of group.dependencies) {
  141. dependency.setLazy(false);
  142. }
  143. result.push(group);
  144. };
  145. if (forwardedIds === true) {
  146. for (const group of this._requestToDepGroup.values()) unlazy(group);
  147. this._requestToDepGroup.clear();
  148. this._release();
  149. return result;
  150. }
  151. /**
  152. * @param {string} requestKey request key to take
  153. */
  154. const take = (requestKey) => {
  155. const group = this._requestToDepGroup.get(requestKey);
  156. if (group === undefined) return;
  157. this._requestToDepGroup.delete(requestKey);
  158. unlazy(group);
  159. };
  160. for (const id of forwardedIds) {
  161. if (this._terminalIds !== undefined && this._terminalIds.has(id)) {
  162. continue;
  163. }
  164. const requestKey = this._forwardIdToRequest.get(id);
  165. if (requestKey !== undefined) {
  166. take(requestKey);
  167. } else if (this._fallbackRequests !== undefined) {
  168. // unknown name: any star re-export may provide it
  169. for (const fallbackKey of this._fallbackRequests) take(fallbackKey);
  170. }
  171. }
  172. if (this._requestToDepGroup.size === 0) this._release();
  173. return result;
  174. }
  175. }
  176. const esmDependencyCategory = "esm";
  177. /**
  178. * Owns the per side-effect-free module lazy barrel state for a `Compilation`,
  179. * keeping the deferral bookkeeping out of `Compilation` itself.
  180. */
  181. class LazyBarrelController {
  182. /**
  183. * @param {Compilation} compilation the owning compilation
  184. */
  185. constructor(compilation) {
  186. /** @type {Compilation} */
  187. this._compilation = compilation;
  188. /** @type {WeakMap<Module, LazyBarrelState>} */
  189. this._modules = new WeakMap();
  190. }
  191. /**
  192. * Releases all per-module deferral state. The last consumer is
  193. * `FlagDependencyExportsPlugin`'s cache key during `finishModules`, so the
  194. * bookkeeping is dead weight once sealing starts.
  195. */
  196. clear() {
  197. this._modules = new WeakMap();
  198. }
  199. /**
  200. * requests of the re-export targets `module` still keeps lazy.
  201. * `FlagDependencyExportsPlugin` folds them into its provided-exports cache
  202. * key, so a barrel whose built targets differ from a cached run does not
  203. * reuse that run's stale exports.
  204. * @param {Module} module the module
  205. * @returns {Iterable<string> | undefined} still-lazy requests, if any
  206. */
  207. getLazyRequests(module) {
  208. const factoryMeta = module.factoryMeta;
  209. if (factoryMeta === undefined || !factoryMeta.sideEffectFree) return;
  210. const state = this._modules.get(module);
  211. if (state === undefined) return;
  212. const info = state.lazyBarrelInfo;
  213. if (info === undefined || info.isEmpty()) return;
  214. return info.getLazyRequests();
  215. }
  216. /**
  217. * Classifies the re-export dependencies of a side-effect-free module (lazy barrel)
  218. * into deferrable groups and marks the deferred ones.
  219. * @param {Module} module the module whose dependencies are processed
  220. * @returns {boolean} true, when some dependencies were deferred
  221. */
  222. classify(module) {
  223. const modules = this._modules;
  224. const factoryMeta = module.factoryMeta;
  225. if (factoryMeta === undefined || !factoryMeta.sideEffectFree) {
  226. return false;
  227. }
  228. const dependencies = module.dependencies;
  229. const dependencyFactories = this._compilation.dependencyFactories;
  230. /** @type {LazyBarrelInfo | undefined} */
  231. let info;
  232. let hasFallback = false;
  233. for (const dep of dependencies) {
  234. // TODO remove in webpack 6
  235. // It may be missing on custom dependency types not extending the base Dependency
  236. if (!("getLazyUntil" in dep)) continue;
  237. const until = dep.getLazyUntil();
  238. // null (eager) and LAZY_UNTIL_LOCAL (terminal) defer nothing; terminals are
  239. // only consulted alongside a star re-export, so they are recorded below
  240. if (until === null || until === Dependency.LAZY_UNTIL_LOCAL) continue;
  241. // deferrable: setLazy is needed to toggle its deferred state
  242. if (!("setLazy" in dep)) continue;
  243. const resourceIdent = dep.getResourceIdentifier();
  244. if (resourceIdent === null) continue;
  245. const factory = dependencyFactories.get(
  246. /** @type {DependencyConstructor} */ (dep.constructor)
  247. );
  248. if (factory === undefined) continue;
  249. const category = dep.category;
  250. // Match the request grouping key of `processDependencyForResolving`
  251. const requestKey =
  252. category === esmDependencyCategory
  253. ? resourceIdent
  254. : `${category}${resourceIdent}`;
  255. if (info === undefined) info = new LazyBarrelInfo();
  256. if (dep.isLazy()) {
  257. info.addLazy(requestKey, dep, factory, dep.getContext());
  258. }
  259. if (until === Dependency.LAZY_UNTIL_ID) {
  260. info.addForwardId(
  261. /** @type {string} */ (dep.getLazyName()),
  262. requestKey
  263. );
  264. } else if (until === Dependency.LAZY_UNTIL_FALLBACK) {
  265. info.addFallback(requestKey);
  266. hasFallback = true;
  267. }
  268. }
  269. const state = modules.get(module);
  270. // info is only created once a deferrable target exists, so it is never empty here
  271. if (info === undefined) {
  272. if (state !== undefined) modules.delete(module);
  273. return false;
  274. }
  275. // terminals only matter together with a star re-export (to avoid building it
  276. // for a locally-provided name); skip the extra pass otherwise
  277. if (hasFallback) {
  278. for (const dep of dependencies) {
  279. if (
  280. "getLazyUntil" in dep &&
  281. dep.getLazyUntil() === Dependency.LAZY_UNTIL_LOCAL
  282. ) {
  283. info.addTerminal(/** @type {string} */ (dep.getLazyName()));
  284. }
  285. }
  286. }
  287. if (state !== undefined) {
  288. // barrel classified after its targets were already requested: replay the
  289. // recorded ids so processDependency un-lazies and builds those targets now
  290. info.request(state.forwardedIds);
  291. // stay lazy only while some targets remain deferred
  292. if (info.isEmpty()) return false;
  293. state.lazyBarrelInfo = info;
  294. return true;
  295. }
  296. modules.set(module, {
  297. forwardedIds: new Set(),
  298. lazyBarrelInfo: info
  299. });
  300. return true;
  301. }
  302. /**
  303. * Requests the export names that `dependencies` need from a lazy barrel and
  304. * returns the deferred re-export targets that must be built now. Records the
  305. * request when the barrel's dependencies were not classified yet.
  306. * @param {Module} module the resolved module
  307. * @param {Dependency[]} dependencies the dependencies that resolved to the module
  308. * @returns {UnlazyDependencyInfo[] | undefined} items to process, if any
  309. */
  310. request(module, dependencies) {
  311. // state only exists for side-effect-free modules, so a non-side-effect-free
  312. // module never has state — skip the WeakMap lookup entirely for it
  313. const factoryMeta = module.factoryMeta;
  314. if (factoryMeta === undefined || !factoryMeta.sideEffectFree) return;
  315. const modules = this._modules;
  316. const state = modules.get(module);
  317. if (state === undefined) {
  318. const forwardedIds = getForwardedIds(dependencies);
  319. if (forwardedIds !== true && forwardedIds.size === 0) return;
  320. modules.set(module, {
  321. forwardedIds,
  322. lazyBarrelInfo: undefined
  323. });
  324. return;
  325. }
  326. const forwardedIds = getForwardedIds(dependencies);
  327. if (forwardedIds !== true && forwardedIds.size === 0) return;
  328. if (state.forwardedIds !== true) {
  329. if (forwardedIds === true) state.forwardedIds = true;
  330. else for (const id of forwardedIds) state.forwardedIds.add(id);
  331. }
  332. const info = state.lazyBarrelInfo;
  333. // Pending requests will be replayed during `classify` later
  334. if (info === undefined) return;
  335. const groups = info.request(forwardedIds);
  336. // drop the per-module state once every deferred target has been built
  337. if (info.isEmpty()) state.lazyBarrelInfo = undefined;
  338. if (groups.length === 0) return;
  339. return groups.map((group) => ({
  340. factory: group.factory,
  341. dependencies: group.dependencies,
  342. context: group.context,
  343. originModule: module
  344. }));
  345. }
  346. /**
  347. * Queues the deferred dependency groups of a lazy barrel requested by a single dependency.
  348. * @param {Module} module the resolved module of the dependency
  349. * @param {Dependency} dependency the requesting dependency
  350. * @param {{ factory: ModuleFactory, dependencies: Dependency[], context: string | undefined, originModule: Module | null }[]} sortedDependencies item list to append to
  351. */
  352. unlazyForDependency(module, dependency, sortedDependencies) {
  353. const unlazyItems = this.request(module, [dependency]);
  354. if (unlazyItems === undefined) return;
  355. for (const item of unlazyItems) sortedDependencies.push(item);
  356. }
  357. }
  358. module.exports = LazyBarrelController;