FlagDependencyExportsPlugin.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const asyncLib = require("neo-async");
  7. const Queue = require("./util/Queue");
  8. const { compareModulesByIdentifier } = require("./util/comparators");
  9. const createHash = require("./util/createHash");
  10. /** @import Compiler from "./Compiler" */
  11. /** @import DependenciesBlock from "./DependenciesBlock" */
  12. /**
  13. * @import Dependency, {
  14. * ExportSpec,
  15. * ExportsSpec,
  16. * ExportInfoName
  17. * } from "./Dependency"
  18. */
  19. /** @import ExportsInfo, { RestoreProvidedData } from "./ExportsInfo" */
  20. /** @import { HashFunction } from "../declarations/WebpackOptions" */
  21. /** @import LazyBarrelController from "./LazyBarrel" */
  22. /** @import Module, { BuildInfo } from "./Module" */
  23. const PLUGIN_NAME = "FlagDependencyExportsPlugin";
  24. const PLUGIN_LOGGER_NAME = `webpack.${PLUGIN_NAME}`;
  25. /** @typedef {{ etag: string, data: InstanceType<RestoreProvidedData> }} MemCacheEntry */
  26. /**
  27. * @param {Module} module the module
  28. * @param {LazyBarrelController} lazyBarrelController the compilation's lazy barrel controller
  29. * @param {HashFunction} hashFunction hash function for long keys
  30. * @returns {string} the hash
  31. */
  32. const getLazyRequestsHash = (module, lazyBarrelController, hashFunction) => {
  33. const lazyKeys = lazyBarrelController.getLazyRequests(module);
  34. if (!lazyKeys) return "";
  35. const joined = [...lazyKeys].join("|");
  36. if (joined.length < 100) return joined;
  37. const hash = createHash(hashFunction);
  38. hash.update(joined);
  39. return /** @type {string} */ (hash.digest("hex"));
  40. };
  41. class FlagDependencyExportsPlugin {
  42. /**
  43. * Applies the plugin by registering its hooks on the compiler.
  44. * @param {Compiler} compiler the compiler instance
  45. * @returns {void}
  46. */
  47. apply(compiler) {
  48. compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
  49. const moduleGraph = compilation.moduleGraph;
  50. const cache = compilation.getCache(PLUGIN_NAME);
  51. const lazyBarrelController = compilation._lazyBarrelController;
  52. const hashFunction = compilation.outputOptions.hashFunction;
  53. compilation.hooks.finishModules.tapAsync(
  54. PLUGIN_NAME,
  55. (modules, callback) => {
  56. const logger = compilation.getLogger(PLUGIN_LOGGER_NAME);
  57. let statRestoredFromMemCache = 0;
  58. let statRestoredFromCache = 0;
  59. let statNoExports = 0;
  60. let statFlaggedUncached = 0;
  61. let statNotCached = 0;
  62. let statQueueItemsProcessed = 0;
  63. const { moduleMemCaches } = compilation;
  64. /** @type {Queue<Module>} */
  65. const queue = new Queue();
  66. /** @type {Set<Module>} */
  67. const modulesToAnalyse = new Set();
  68. // Step 1: Try to restore cached provided export info from cache
  69. logger.time("restore cached provided exports");
  70. asyncLib.each(
  71. /** @type {import("neo-async").IterableCollection<Module>} */ (
  72. /** @type {unknown} */ (modules)
  73. ),
  74. (module, callback) => {
  75. const exportsInfo = moduleGraph.getExportsInfo(module);
  76. // If the module doesn't have an exportsType, it's a module
  77. // without declared exports.
  78. if (
  79. (!module.buildMeta || !module.buildMeta.exportsType) &&
  80. exportsInfo.otherExportsInfo.provided !== null
  81. ) {
  82. // It's a module without declared exports
  83. statNoExports++;
  84. exportsInfo.setHasProvideInfo();
  85. exportsInfo.setUnknownExportsProvided();
  86. return callback();
  87. }
  88. // If the module has no hash, it's uncacheable
  89. if (
  90. typeof (/** @type {BuildInfo} */ (module.buildInfo).hash) !==
  91. "string"
  92. ) {
  93. statFlaggedUncached++;
  94. // Determined below, in a stable order
  95. modulesToAnalyse.add(module);
  96. exportsInfo.setHasProvideInfo();
  97. return callback();
  98. }
  99. const memCache = moduleMemCaches && moduleMemCaches.get(module);
  100. /** @type {MemCacheEntry | undefined} */
  101. const memCacheEntry = memCache && memCache.get(this);
  102. const hash = getLazyRequestsHash(
  103. module,
  104. lazyBarrelController,
  105. hashFunction
  106. );
  107. // validated by the still-lazy request keys: reference-change
  108. // detection cannot invalidate the mem cache when the un-lazied
  109. // connections are unsafe-cached (they are skipped when comparing
  110. // references)
  111. if (memCacheEntry !== undefined && memCacheEntry.etag === hash) {
  112. statRestoredFromMemCache++;
  113. exportsInfo.restoreProvided(memCacheEntry.data);
  114. return callback();
  115. }
  116. const buildHash = /** @type {string} */ (
  117. /** @type {BuildInfo} */ (module.buildInfo).hash
  118. );
  119. cache.get(
  120. module.identifier(),
  121. hash ? `${buildHash}|${hash}` : buildHash,
  122. (err, result) => {
  123. if (err) return callback(err);
  124. if (result !== undefined) {
  125. statRestoredFromCache++;
  126. exportsInfo.restoreProvided(result);
  127. } else {
  128. statNotCached++;
  129. // Without cached info determine the exports below
  130. modulesToAnalyse.add(module);
  131. exportsInfo.setHasProvideInfo();
  132. }
  133. callback();
  134. }
  135. );
  136. },
  137. (err) => {
  138. logger.timeEnd("restore cached provided exports");
  139. if (err) return callback(err);
  140. // Seed the queue with targets before the modules that read
  141. // them. Enqueueing at the two branches above ordered the work by
  142. // how fast each cache lookup answered — one is synchronous, the
  143. // other a `cache.get` callback — so the persistent cache decided
  144. // it. A module analysed before the module it re-exports from
  145. // reads exports that are not determined yet and over-estimates,
  146. // and this loop can add exports info but never retract it, so
  147. // the over-estimate survives the pass that corrects it. The
  148. // roots are sorted so the walk is stable, and post-order visits
  149. // each module after everything it points at.
  150. /** @type {Set<Module>} */
  151. const visited = new Set();
  152. /** @type {Module[]} */
  153. const stack = [];
  154. /** @type {Set<Module>} */
  155. const expanded = new Set();
  156. for (const root of [...modulesToAnalyse].sort(
  157. compareModulesByIdentifier
  158. )) {
  159. if (visited.has(root)) continue;
  160. stack.push(root);
  161. while (stack.length > 0) {
  162. const current = stack[stack.length - 1];
  163. if (expanded.has(current)) {
  164. stack.pop();
  165. if (!visited.has(current)) {
  166. visited.add(current);
  167. if (modulesToAnalyse.has(current)) {
  168. queue.enqueue(current);
  169. }
  170. }
  171. continue;
  172. }
  173. expanded.add(current);
  174. for (const connection of moduleGraph.getOutgoingConnections(
  175. current
  176. )) {
  177. const target = connection.module;
  178. // A module restored from cache is already final, so
  179. // nothing has to be ordered before it and the walk stops
  180. // there — on an incremental build that is most of them.
  181. if (
  182. target !== undefined &&
  183. target !== null &&
  184. modulesToAnalyse.has(target) &&
  185. !expanded.has(target)
  186. ) {
  187. stack.push(target);
  188. }
  189. }
  190. }
  191. }
  192. /** @type {Set<Module>} */
  193. const modulesToStore = new Set();
  194. /** @type {Map<Module, Set<Module>>} */
  195. const dependencies = new Map();
  196. /** @type {Module} */
  197. let module;
  198. /** @type {ExportsInfo} */
  199. let exportsInfo;
  200. /** @type {Map<Dependency, ExportsSpec>} */
  201. const exportsSpecsFromDependencies = new Map();
  202. let cacheable = true;
  203. let changed = false;
  204. /**
  205. * Process dependencies block.
  206. * @param {DependenciesBlock} depBlock the dependencies block
  207. * @returns {void}
  208. */
  209. const processDependenciesBlock = (depBlock) => {
  210. for (const dep of depBlock.dependencies) {
  211. processDependency(dep);
  212. }
  213. for (const block of depBlock.blocks) {
  214. processDependenciesBlock(block);
  215. }
  216. };
  217. /**
  218. * Process dependency.
  219. * @param {Dependency} dep the dependency
  220. * @returns {void}
  221. */
  222. const processDependency = (dep) => {
  223. const exportDesc = dep.getExports(moduleGraph);
  224. if (!exportDesc) return;
  225. exportsSpecsFromDependencies.set(dep, exportDesc);
  226. };
  227. /**
  228. * Process exports spec.
  229. * @param {Dependency} dep dependency
  230. * @param {ExportsSpec} exportDesc info
  231. * @returns {void}
  232. */
  233. const processExportsSpec = (dep, exportDesc) => {
  234. const exports = exportDesc.exports;
  235. const globalCanMangle = exportDesc.canMangle;
  236. const globalFrom = exportDesc.from;
  237. const globalPriority = exportDesc.priority;
  238. const globalTerminalBinding =
  239. exportDesc.terminalBinding || false;
  240. const globalPure = exportDesc.isPure || false;
  241. const exportDeps = exportDesc.dependencies;
  242. if (exportDesc.hideExports) {
  243. for (const name of exportDesc.hideExports) {
  244. const exportInfo = exportsInfo.getExportInfo(name);
  245. exportInfo.unsetTarget(dep);
  246. }
  247. }
  248. if (exports === true) {
  249. // unknown exports
  250. if (
  251. exportsInfo.setUnknownExportsProvided(
  252. globalCanMangle,
  253. exportDesc.excludeExports,
  254. globalFrom && dep,
  255. globalFrom,
  256. globalPriority
  257. )
  258. ) {
  259. changed = true;
  260. }
  261. } else if (Array.isArray(exports)) {
  262. /**
  263. * merge in new exports
  264. * @param {ExportsInfo} exportsInfo own exports info
  265. * @param {(ExportSpec | string)[]} exports list of exports
  266. */
  267. const mergeExports = (exportsInfo, exports) => {
  268. for (const exportNameOrSpec of exports) {
  269. /** @type {ExportInfoName} */
  270. let name;
  271. let canMangle = globalCanMangle;
  272. let terminalBinding = globalTerminalBinding;
  273. let pure = globalPure;
  274. /** @type {ExportSpec["exports"]} */
  275. let exports;
  276. let from = globalFrom;
  277. /** @type {ExportSpec["export"]} */
  278. let fromExport;
  279. let priority = globalPriority;
  280. let hidden = false;
  281. /** @type {ExportSpec["inlined"]} */
  282. let inlined;
  283. if (typeof exportNameOrSpec === "string") {
  284. name = exportNameOrSpec;
  285. } else {
  286. name = exportNameOrSpec.name;
  287. if (exportNameOrSpec.canMangle !== undefined) {
  288. canMangle = exportNameOrSpec.canMangle;
  289. }
  290. if (exportNameOrSpec.export !== undefined) {
  291. fromExport = exportNameOrSpec.export;
  292. }
  293. if (exportNameOrSpec.exports !== undefined) {
  294. exports = exportNameOrSpec.exports;
  295. }
  296. if (exportNameOrSpec.from !== undefined) {
  297. from = exportNameOrSpec.from;
  298. }
  299. if (exportNameOrSpec.priority !== undefined) {
  300. priority = exportNameOrSpec.priority;
  301. }
  302. if (exportNameOrSpec.terminalBinding !== undefined) {
  303. terminalBinding = exportNameOrSpec.terminalBinding;
  304. }
  305. if (exportNameOrSpec.isPure !== undefined) {
  306. pure = exportNameOrSpec.isPure;
  307. }
  308. if (exportNameOrSpec.hidden !== undefined) {
  309. hidden = exportNameOrSpec.hidden;
  310. }
  311. if (exportNameOrSpec.inlined !== undefined) {
  312. inlined = exportNameOrSpec.inlined;
  313. }
  314. }
  315. const exportInfo = exportsInfo.getExportInfo(name);
  316. if (
  317. exportInfo.provided === false ||
  318. exportInfo.provided === null
  319. ) {
  320. exportInfo.provided = true;
  321. changed = true;
  322. }
  323. if (
  324. exportInfo.canMangleProvide !== false &&
  325. canMangle === false
  326. ) {
  327. exportInfo.canMangleProvide = false;
  328. changed = true;
  329. }
  330. if (
  331. inlined !== undefined &&
  332. exportInfo.canInlineProvide === undefined
  333. ) {
  334. exportInfo.canInlineProvide = inlined;
  335. changed = true;
  336. }
  337. if (terminalBinding && !exportInfo.terminalBinding) {
  338. exportInfo.terminalBinding = true;
  339. changed = true;
  340. }
  341. if (pure && exportInfo.pureProvide !== true) {
  342. exportInfo.pureProvide = true;
  343. changed = true;
  344. }
  345. if (exports) {
  346. const nestedExportsInfo =
  347. exportInfo.createNestedExportsInfo();
  348. mergeExports(
  349. /** @type {ExportsInfo} */ (nestedExportsInfo),
  350. exports
  351. );
  352. }
  353. if (
  354. from &&
  355. (hidden
  356. ? exportInfo.unsetTarget(dep)
  357. : exportInfo.setTarget(
  358. dep,
  359. from,
  360. fromExport === undefined ? [name] : fromExport,
  361. priority
  362. ))
  363. ) {
  364. changed = true;
  365. }
  366. // Recalculate target exportsInfo
  367. const target = exportInfo.getTarget(moduleGraph);
  368. /** @type {undefined | ExportsInfo} */
  369. let targetExportsInfo;
  370. if (target) {
  371. const targetModuleExportsInfo =
  372. moduleGraph.getExportsInfo(target.module);
  373. targetExportsInfo =
  374. targetModuleExportsInfo.getNestedExportsInfo(
  375. target.export
  376. );
  377. // add dependency for this module
  378. const set = dependencies.get(target.module);
  379. if (set === undefined) {
  380. dependencies.set(target.module, new Set([module]));
  381. } else {
  382. set.add(module);
  383. }
  384. }
  385. if (exportInfo.exportsInfoOwned) {
  386. if (
  387. /** @type {ExportsInfo} */
  388. (exportInfo.exportsInfo).setRedirectNamedTo(
  389. targetExportsInfo
  390. )
  391. ) {
  392. changed = true;
  393. }
  394. } else if (exportInfo.exportsInfo !== targetExportsInfo) {
  395. exportInfo.exportsInfo = targetExportsInfo;
  396. changed = true;
  397. }
  398. }
  399. };
  400. mergeExports(exportsInfo, exports);
  401. }
  402. // store dependencies
  403. if (exportDeps) {
  404. cacheable = false;
  405. for (const exportDependency of exportDeps) {
  406. // add dependency for this module
  407. const set = dependencies.get(exportDependency);
  408. if (set === undefined) {
  409. dependencies.set(exportDependency, new Set([module]));
  410. } else {
  411. set.add(module);
  412. }
  413. }
  414. }
  415. };
  416. const notifyDependencies = () => {
  417. const deps = dependencies.get(module);
  418. if (deps !== undefined) {
  419. for (const dep of deps) {
  420. queue.enqueue(dep);
  421. }
  422. }
  423. };
  424. logger.time("figure out provided exports");
  425. while (queue.length > 0) {
  426. module = /** @type {Module} */ (queue.dequeue());
  427. statQueueItemsProcessed++;
  428. exportsInfo = moduleGraph.getExportsInfo(module);
  429. cacheable = true;
  430. changed = false;
  431. exportsSpecsFromDependencies.clear();
  432. moduleGraph.freeze();
  433. processDependenciesBlock(module);
  434. moduleGraph.unfreeze();
  435. for (const [dep, exportsSpec] of exportsSpecsFromDependencies) {
  436. processExportsSpec(dep, exportsSpec);
  437. }
  438. if (cacheable) {
  439. modulesToStore.add(module);
  440. }
  441. if (changed) {
  442. notifyDependencies();
  443. }
  444. }
  445. logger.timeEnd("figure out provided exports");
  446. logger.log(
  447. `${Math.round(
  448. (100 * (statFlaggedUncached + statNotCached)) /
  449. (statRestoredFromMemCache +
  450. statRestoredFromCache +
  451. statNotCached +
  452. statFlaggedUncached +
  453. statNoExports)
  454. )}% of exports of modules have been determined (${statNoExports} no declared exports, ${statNotCached} not cached, ${statFlaggedUncached} flagged uncacheable, ${statRestoredFromCache} from cache, ${statRestoredFromMemCache} from mem cache, ${
  455. statQueueItemsProcessed - statNotCached - statFlaggedUncached
  456. } additional calculations due to dependencies)`
  457. );
  458. logger.time("store provided exports into cache");
  459. asyncLib.each(
  460. modulesToStore,
  461. (module, callback) => {
  462. if (
  463. typeof (
  464. /** @type {BuildInfo} */
  465. (module.buildInfo).hash
  466. ) !== "string"
  467. ) {
  468. // not cacheable
  469. return callback();
  470. }
  471. const cachedData = moduleGraph
  472. .getExportsInfo(module)
  473. .getRestoreProvidedData();
  474. const memCache =
  475. moduleMemCaches && moduleMemCaches.get(module);
  476. const hash = getLazyRequestsHash(
  477. module,
  478. lazyBarrelController,
  479. hashFunction
  480. );
  481. if (memCache) {
  482. /** @type {MemCacheEntry} */
  483. const entry = {
  484. etag: hash,
  485. data: cachedData
  486. };
  487. memCache.set(this, entry);
  488. }
  489. const buildHash = /** @type {string} */ (
  490. /** @type {BuildInfo} */ (module.buildInfo).hash
  491. );
  492. cache.store(
  493. module.identifier(),
  494. hash ? `${buildHash}|${hash}` : buildHash,
  495. cachedData,
  496. callback
  497. );
  498. },
  499. (err) => {
  500. logger.timeEnd("store provided exports into cache");
  501. callback(err);
  502. }
  503. );
  504. }
  505. );
  506. }
  507. );
  508. /** @type {WeakMap<Module, InstanceType<RestoreProvidedData>>} */
  509. const providedExportsCache = new WeakMap();
  510. compilation.hooks.rebuildModule.tap(PLUGIN_NAME, (module) => {
  511. providedExportsCache.set(
  512. module,
  513. moduleGraph.getExportsInfo(module).getRestoreProvidedData()
  514. );
  515. });
  516. compilation.hooks.finishRebuildingModule.tap(PLUGIN_NAME, (module) => {
  517. moduleGraph.getExportsInfo(module).restoreProvided(
  518. /** @type {InstanceType<RestoreProvidedData>} */
  519. (providedExportsCache.get(module))
  520. );
  521. });
  522. });
  523. }
  524. }
  525. module.exports = FlagDependencyExportsPlugin;