SideEffectsFlagPlugin.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const mm = require("micromatch");
  7. const HarmonyExportImportedSpecifierDependency = require("../dependencies/HarmonyExportImportedSpecifierDependency");
  8. const HarmonyImportSideEffectDependency = require("../dependencies/HarmonyImportSideEffectDependency");
  9. const HarmonyImportSpecifierDependency = require("../dependencies/HarmonyImportSpecifierDependency");
  10. /** @typedef {import("../Module")} Module */
  11. /** @typedef {import("../Dependency")} Dependency */
  12. /**
  13. * @typedef {Object} ExportInModule
  14. * @property {Module} module the module
  15. * @property {string} exportName the name of the export
  16. * @property {boolean} checked if the export is conditional
  17. */
  18. /**
  19. * @typedef {Object} ReexportInfo
  20. * @property {Map<string, ExportInModule[]>} static
  21. * @property {Map<Module, Set<string>>} dynamic
  22. */
  23. /**
  24. * @param {ReexportInfo} info info object
  25. * @param {string} exportName name of export
  26. * @returns {ExportInModule | undefined} static export
  27. */
  28. const getMappingFromInfo = (info, exportName) => {
  29. const staticMappings = info.static.get(exportName);
  30. if (staticMappings !== undefined) {
  31. if (staticMappings.length === 1) return staticMappings[0];
  32. return undefined;
  33. }
  34. const dynamicMappings = Array.from(info.dynamic).filter(
  35. ([_, ignored]) => !ignored.has(exportName)
  36. );
  37. if (dynamicMappings.length === 1) {
  38. return {
  39. module: dynamicMappings[0][0],
  40. exportName,
  41. checked: true
  42. };
  43. }
  44. return undefined;
  45. };
  46. /**
  47. * @param {ReexportInfo} info info object
  48. * @param {string} exportName name of export of source module
  49. * @param {Module} module the target module
  50. * @param {string} innerExportName name of export of target module
  51. * @param {boolean} checked true, if existence of target module is checked
  52. */
  53. const addStaticReexport = (
  54. info,
  55. exportName,
  56. module,
  57. innerExportName,
  58. checked
  59. ) => {
  60. let mappings = info.static.get(exportName);
  61. if (mappings !== undefined) {
  62. for (const mapping of mappings) {
  63. if (mapping.module === module && mapping.exportName === innerExportName) {
  64. mapping.checked = mapping.checked && checked;
  65. return;
  66. }
  67. }
  68. } else {
  69. mappings = [];
  70. info.static.set(exportName, mappings);
  71. }
  72. mappings.push({
  73. module,
  74. exportName: innerExportName,
  75. checked
  76. });
  77. };
  78. /**
  79. * @param {ReexportInfo} info info object
  80. * @param {Module} module the reexport module
  81. * @param {Set<string>} ignored ignore list
  82. * @returns {void}
  83. */
  84. const addDynamicReexport = (info, module, ignored) => {
  85. const existingList = info.dynamic.get(module);
  86. if (existingList !== undefined) {
  87. for (const key of existingList) {
  88. if (!ignored.has(key)) existingList.delete(key);
  89. }
  90. } else {
  91. info.dynamic.set(module, new Set(ignored));
  92. }
  93. };
  94. class SideEffectsFlagPlugin {
  95. apply(compiler) {
  96. compiler.hooks.normalModuleFactory.tap("SideEffectsFlagPlugin", nmf => {
  97. nmf.hooks.module.tap("SideEffectsFlagPlugin", (module, data) => {
  98. const resolveData = data.resourceResolveData;
  99. if (
  100. resolveData &&
  101. resolveData.descriptionFileData &&
  102. resolveData.relativePath
  103. ) {
  104. const sideEffects = resolveData.descriptionFileData.sideEffects;
  105. const hasSideEffects = SideEffectsFlagPlugin.moduleHasSideEffects(
  106. resolveData.relativePath,
  107. sideEffects
  108. );
  109. if (!hasSideEffects) {
  110. module.factoryMeta.sideEffectFree = true;
  111. }
  112. }
  113. return module;
  114. });
  115. nmf.hooks.module.tap("SideEffectsFlagPlugin", (module, data) => {
  116. if (data.settings.sideEffects === false) {
  117. module.factoryMeta.sideEffectFree = true;
  118. } else if (data.settings.sideEffects === true) {
  119. module.factoryMeta.sideEffectFree = false;
  120. }
  121. });
  122. });
  123. compiler.hooks.compilation.tap("SideEffectsFlagPlugin", compilation => {
  124. compilation.hooks.optimizeDependencies.tap(
  125. "SideEffectsFlagPlugin",
  126. modules => {
  127. /** @type {Map<Module, ReexportInfo>} */
  128. const reexportMaps = new Map();
  129. // Capture reexports of sideEffectFree modules
  130. for (const module of modules) {
  131. /** @type {Dependency[]} */
  132. const removeDependencies = [];
  133. for (const dep of module.dependencies) {
  134. if (dep instanceof HarmonyImportSideEffectDependency) {
  135. if (dep.module && dep.module.factoryMeta.sideEffectFree) {
  136. removeDependencies.push(dep);
  137. }
  138. } else if (
  139. dep instanceof HarmonyExportImportedSpecifierDependency
  140. ) {
  141. if (module.factoryMeta.sideEffectFree) {
  142. const mode = dep.getMode(true);
  143. if (
  144. mode.type === "safe-reexport" ||
  145. mode.type === "checked-reexport" ||
  146. mode.type === "dynamic-reexport" ||
  147. mode.type === "reexport-non-harmony-default" ||
  148. mode.type === "reexport-non-harmony-default-strict" ||
  149. mode.type === "reexport-named-default"
  150. ) {
  151. let info = reexportMaps.get(module);
  152. if (!info) {
  153. reexportMaps.set(
  154. module,
  155. (info = {
  156. static: new Map(),
  157. dynamic: new Map()
  158. })
  159. );
  160. }
  161. const targetModule = dep._module;
  162. switch (mode.type) {
  163. case "safe-reexport":
  164. for (const [key, id] of mode.map) {
  165. if (id) {
  166. addStaticReexport(
  167. info,
  168. key,
  169. targetModule,
  170. id,
  171. false
  172. );
  173. }
  174. }
  175. break;
  176. case "checked-reexport":
  177. for (const [key, id] of mode.map) {
  178. if (id) {
  179. addStaticReexport(
  180. info,
  181. key,
  182. targetModule,
  183. id,
  184. true
  185. );
  186. }
  187. }
  188. break;
  189. case "dynamic-reexport":
  190. addDynamicReexport(info, targetModule, mode.ignored);
  191. break;
  192. case "reexport-non-harmony-default":
  193. case "reexport-non-harmony-default-strict":
  194. case "reexport-named-default":
  195. addStaticReexport(
  196. info,
  197. mode.name,
  198. targetModule,
  199. "default",
  200. false
  201. );
  202. break;
  203. }
  204. }
  205. }
  206. }
  207. }
  208. }
  209. // Flatten reexports
  210. for (const info of reexportMaps.values()) {
  211. const dynamicReexports = info.dynamic;
  212. info.dynamic = new Map();
  213. for (const reexport of dynamicReexports) {
  214. let [targetModule, ignored] = reexport;
  215. for (;;) {
  216. const innerInfo = reexportMaps.get(targetModule);
  217. if (!innerInfo) break;
  218. for (const [key, reexports] of innerInfo.static) {
  219. if (ignored.has(key)) continue;
  220. for (const { module, exportName, checked } of reexports) {
  221. addStaticReexport(info, key, module, exportName, checked);
  222. }
  223. }
  224. // Follow dynamic reexport if there is only one
  225. if (innerInfo.dynamic.size !== 1) {
  226. // When there are more then one, we don't know which one
  227. break;
  228. }
  229. ignored = new Set(ignored);
  230. for (const [innerModule, innerIgnored] of innerInfo.dynamic) {
  231. for (const key of innerIgnored) {
  232. if (ignored.has(key)) continue;
  233. // This reexports ends here
  234. addStaticReexport(info, key, targetModule, key, true);
  235. ignored.add(key);
  236. }
  237. targetModule = innerModule;
  238. }
  239. }
  240. // Update reexport as all other cases has been handled
  241. addDynamicReexport(info, targetModule, ignored);
  242. }
  243. }
  244. for (const info of reexportMaps.values()) {
  245. const staticReexports = info.static;
  246. info.static = new Map();
  247. for (const [key, reexports] of staticReexports) {
  248. for (let mapping of reexports) {
  249. for (;;) {
  250. const innerInfo = reexportMaps.get(mapping.module);
  251. if (!innerInfo) break;
  252. const newMapping = getMappingFromInfo(
  253. innerInfo,
  254. mapping.exportName
  255. );
  256. if (!newMapping) break;
  257. mapping = newMapping;
  258. }
  259. addStaticReexport(
  260. info,
  261. key,
  262. mapping.module,
  263. mapping.exportName,
  264. mapping.checked
  265. );
  266. }
  267. }
  268. }
  269. // Update imports along the reexports from sideEffectFree modules
  270. for (const pair of reexportMaps) {
  271. const module = pair[0];
  272. const info = pair[1];
  273. let newReasons = undefined;
  274. for (let i = 0; i < module.reasons.length; i++) {
  275. const reason = module.reasons[i];
  276. const dep = reason.dependency;
  277. if (
  278. (dep instanceof HarmonyExportImportedSpecifierDependency ||
  279. (dep instanceof HarmonyImportSpecifierDependency &&
  280. !dep.namespaceObjectAsContext)) &&
  281. dep._id
  282. ) {
  283. const mapping = getMappingFromInfo(info, dep._id);
  284. if (mapping) {
  285. dep.redirectedModule = mapping.module;
  286. dep.redirectedId = mapping.exportName;
  287. mapping.module.addReason(
  288. reason.module,
  289. dep,
  290. reason.explanation
  291. ? reason.explanation +
  292. " (skipped side-effect-free modules)"
  293. : "(skipped side-effect-free modules)"
  294. );
  295. // removing the currect reason, by not adding it to the newReasons array
  296. // lazily create the newReasons array
  297. if (newReasons === undefined) {
  298. newReasons = i === 0 ? [] : module.reasons.slice(0, i);
  299. }
  300. continue;
  301. }
  302. }
  303. if (newReasons !== undefined) newReasons.push(reason);
  304. }
  305. if (newReasons !== undefined) {
  306. module.reasons = newReasons;
  307. }
  308. }
  309. }
  310. );
  311. });
  312. }
  313. static moduleHasSideEffects(moduleName, flagValue) {
  314. switch (typeof flagValue) {
  315. case "undefined":
  316. return true;
  317. case "boolean":
  318. return flagValue;
  319. case "string":
  320. if (process.platform === "win32") {
  321. flagValue = flagValue.replace(/\\/g, "/");
  322. }
  323. return mm.isMatch(moduleName, flagValue, {
  324. matchBase: true
  325. });
  326. case "object":
  327. return flagValue.some(glob =>
  328. SideEffectsFlagPlugin.moduleHasSideEffects(moduleName, glob)
  329. );
  330. }
  331. }
  332. }
  333. module.exports = SideEffectsFlagPlugin;