SideEffectsFlagPlugin.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const glob2regexp = require("glob-to-regexp");
  7. const {
  8. JAVASCRIPT_MODULE_TYPE_AUTO,
  9. JAVASCRIPT_MODULE_TYPE_ESM,
  10. JAVASCRIPT_MODULE_TYPE_DYNAMIC
  11. } = require("../ModuleTypeConstants");
  12. const { STAGE_DEFAULT } = require("../OptimizationStages");
  13. const HarmonyExportImportedSpecifierDependency = require("../dependencies/HarmonyExportImportedSpecifierDependency");
  14. const HarmonyImportSpecifierDependency = require("../dependencies/HarmonyImportSpecifierDependency");
  15. const formatLocation = require("../formatLocation");
  16. /** @typedef {import("estree").ModuleDeclaration} ModuleDeclaration */
  17. /** @typedef {import("estree").Statement} Statement */
  18. /** @typedef {import("../Compiler")} Compiler */
  19. /** @typedef {import("../Dependency")} Dependency */
  20. /** @typedef {import("../Dependency").DependencyLocation} DependencyLocation */
  21. /** @typedef {import("../Module")} Module */
  22. /** @typedef {import("../Module").BuildMeta} BuildMeta */
  23. /** @typedef {import("../javascript/JavascriptParser")} JavascriptParser */
  24. /** @typedef {import("../javascript/JavascriptParser").Range} Range */
  25. /**
  26. * @typedef {object} ExportInModule
  27. * @property {Module} module the module
  28. * @property {string} exportName the name of the export
  29. * @property {boolean} checked if the export is conditional
  30. */
  31. /**
  32. * @typedef {object} ReexportInfo
  33. * @property {Map<string, ExportInModule[]>} static
  34. * @property {Map<Module, Set<string>>} dynamic
  35. */
  36. /** @typedef {Map<string, RegExp>} CacheItem */
  37. /** @type {WeakMap<any, CacheItem>} */
  38. const globToRegexpCache = new WeakMap();
  39. /**
  40. * @param {string} glob the pattern
  41. * @param {Map<string, RegExp>} cache the glob to RegExp cache
  42. * @returns {RegExp} a regular expression
  43. */
  44. const globToRegexp = (glob, cache) => {
  45. const cacheEntry = cache.get(glob);
  46. if (cacheEntry !== undefined) return cacheEntry;
  47. if (!glob.includes("/")) {
  48. glob = `**/${glob}`;
  49. }
  50. const baseRegexp = glob2regexp(glob, { globstar: true, extended: true });
  51. const regexpSource = baseRegexp.source;
  52. const regexp = new RegExp("^(\\./)?" + regexpSource.slice(1));
  53. cache.set(glob, regexp);
  54. return regexp;
  55. };
  56. const PLUGIN_NAME = "SideEffectsFlagPlugin";
  57. class SideEffectsFlagPlugin {
  58. /**
  59. * @param {boolean} analyseSource analyse source code for side effects
  60. */
  61. constructor(analyseSource = true) {
  62. this._analyseSource = analyseSource;
  63. }
  64. /**
  65. * Apply the plugin
  66. * @param {Compiler} compiler the compiler instance
  67. * @returns {void}
  68. */
  69. apply(compiler) {
  70. let cache = globToRegexpCache.get(compiler.root);
  71. if (cache === undefined) {
  72. cache = new Map();
  73. globToRegexpCache.set(compiler.root, cache);
  74. }
  75. compiler.hooks.compilation.tap(
  76. PLUGIN_NAME,
  77. (compilation, { normalModuleFactory }) => {
  78. const moduleGraph = compilation.moduleGraph;
  79. normalModuleFactory.hooks.module.tap(PLUGIN_NAME, (module, data) => {
  80. const resolveData = data.resourceResolveData;
  81. if (
  82. resolveData &&
  83. resolveData.descriptionFileData &&
  84. resolveData.relativePath
  85. ) {
  86. const sideEffects = resolveData.descriptionFileData.sideEffects;
  87. if (sideEffects !== undefined) {
  88. if (module.factoryMeta === undefined) {
  89. module.factoryMeta = {};
  90. }
  91. const hasSideEffects = SideEffectsFlagPlugin.moduleHasSideEffects(
  92. resolveData.relativePath,
  93. sideEffects,
  94. /** @type {CacheItem} */ (cache)
  95. );
  96. module.factoryMeta.sideEffectFree = !hasSideEffects;
  97. }
  98. }
  99. return module;
  100. });
  101. normalModuleFactory.hooks.module.tap(PLUGIN_NAME, (module, data) => {
  102. if (typeof data.settings.sideEffects === "boolean") {
  103. if (module.factoryMeta === undefined) {
  104. module.factoryMeta = {};
  105. }
  106. module.factoryMeta.sideEffectFree = !data.settings.sideEffects;
  107. }
  108. return module;
  109. });
  110. if (this._analyseSource) {
  111. /**
  112. * @param {JavascriptParser} parser the parser
  113. * @returns {void}
  114. */
  115. const parserHandler = parser => {
  116. /** @type {undefined | Statement | ModuleDeclaration} */
  117. let sideEffectsStatement;
  118. parser.hooks.program.tap(PLUGIN_NAME, () => {
  119. sideEffectsStatement = undefined;
  120. });
  121. parser.hooks.statement.tap(
  122. { name: PLUGIN_NAME, stage: -100 },
  123. statement => {
  124. if (sideEffectsStatement) return;
  125. if (parser.scope.topLevelScope !== true) return;
  126. switch (statement.type) {
  127. case "ExpressionStatement":
  128. if (
  129. !parser.isPure(
  130. statement.expression,
  131. /** @type {Range} */ (statement.range)[0]
  132. )
  133. ) {
  134. sideEffectsStatement = statement;
  135. }
  136. break;
  137. case "IfStatement":
  138. case "WhileStatement":
  139. case "DoWhileStatement":
  140. if (
  141. !parser.isPure(
  142. statement.test,
  143. /** @type {Range} */ (statement.range)[0]
  144. )
  145. ) {
  146. sideEffectsStatement = statement;
  147. }
  148. // statement hook will be called for child statements too
  149. break;
  150. case "ForStatement":
  151. if (
  152. !parser.isPure(
  153. statement.init,
  154. /** @type {Range} */ (statement.range)[0]
  155. ) ||
  156. !parser.isPure(
  157. statement.test,
  158. statement.init
  159. ? /** @type {Range} */ (statement.init.range)[1]
  160. : /** @type {Range} */ (statement.range)[0]
  161. ) ||
  162. !parser.isPure(
  163. statement.update,
  164. statement.test
  165. ? /** @type {Range} */ (statement.test.range)[1]
  166. : statement.init
  167. ? /** @type {Range} */ (statement.init.range)[1]
  168. : /** @type {Range} */ (statement.range)[0]
  169. )
  170. ) {
  171. sideEffectsStatement = statement;
  172. }
  173. // statement hook will be called for child statements too
  174. break;
  175. case "SwitchStatement":
  176. if (
  177. !parser.isPure(
  178. statement.discriminant,
  179. /** @type {Range} */ (statement.range)[0]
  180. )
  181. ) {
  182. sideEffectsStatement = statement;
  183. }
  184. // statement hook will be called for child statements too
  185. break;
  186. case "VariableDeclaration":
  187. case "ClassDeclaration":
  188. case "FunctionDeclaration":
  189. if (
  190. !parser.isPure(
  191. statement,
  192. /** @type {Range} */ (statement.range)[0]
  193. )
  194. ) {
  195. sideEffectsStatement = statement;
  196. }
  197. break;
  198. case "ExportNamedDeclaration":
  199. case "ExportDefaultDeclaration":
  200. if (
  201. !parser.isPure(
  202. statement.declaration,
  203. /** @type {Range} */ (statement.range)[0]
  204. )
  205. ) {
  206. sideEffectsStatement = statement;
  207. }
  208. break;
  209. case "LabeledStatement":
  210. case "BlockStatement":
  211. // statement hook will be called for child statements too
  212. break;
  213. case "EmptyStatement":
  214. break;
  215. case "ExportAllDeclaration":
  216. case "ImportDeclaration":
  217. // imports will be handled by the dependencies
  218. break;
  219. default:
  220. sideEffectsStatement = statement;
  221. break;
  222. }
  223. }
  224. );
  225. parser.hooks.finish.tap(PLUGIN_NAME, () => {
  226. if (sideEffectsStatement === undefined) {
  227. /** @type {BuildMeta} */
  228. (parser.state.module.buildMeta).sideEffectFree = true;
  229. } else {
  230. const { loc, type } = sideEffectsStatement;
  231. moduleGraph
  232. .getOptimizationBailout(parser.state.module)
  233. .push(
  234. () =>
  235. `Statement (${type}) with side effects in source code at ${formatLocation(
  236. /** @type {DependencyLocation} */ (loc)
  237. )}`
  238. );
  239. }
  240. });
  241. };
  242. for (const key of [
  243. JAVASCRIPT_MODULE_TYPE_AUTO,
  244. JAVASCRIPT_MODULE_TYPE_ESM,
  245. JAVASCRIPT_MODULE_TYPE_DYNAMIC
  246. ]) {
  247. normalModuleFactory.hooks.parser
  248. .for(key)
  249. .tap(PLUGIN_NAME, parserHandler);
  250. }
  251. }
  252. compilation.hooks.optimizeDependencies.tap(
  253. {
  254. name: PLUGIN_NAME,
  255. stage: STAGE_DEFAULT
  256. },
  257. modules => {
  258. const logger = compilation.getLogger(
  259. "webpack.SideEffectsFlagPlugin"
  260. );
  261. logger.time("update dependencies");
  262. const optimizedModules = new Set();
  263. /**
  264. * @param {Module} module module
  265. */
  266. const optimizeIncomingConnections = module => {
  267. if (optimizedModules.has(module)) return;
  268. optimizedModules.add(module);
  269. if (module.getSideEffectsConnectionState(moduleGraph) === false) {
  270. const exportsInfo = moduleGraph.getExportsInfo(module);
  271. for (const connection of moduleGraph.getIncomingConnections(
  272. module
  273. )) {
  274. const dep = connection.dependency;
  275. let isReexport;
  276. if (
  277. (isReexport =
  278. dep instanceof
  279. HarmonyExportImportedSpecifierDependency) ||
  280. (dep instanceof HarmonyImportSpecifierDependency &&
  281. !dep.namespaceObjectAsContext)
  282. ) {
  283. if (connection.originModule !== null) {
  284. optimizeIncomingConnections(connection.originModule);
  285. }
  286. // TODO improve for export *
  287. if (isReexport && dep.name) {
  288. const exportInfo = moduleGraph.getExportInfo(
  289. /** @type {Module} */ (connection.originModule),
  290. dep.name
  291. );
  292. exportInfo.moveTarget(
  293. moduleGraph,
  294. ({ module }) =>
  295. module.getSideEffectsConnectionState(moduleGraph) ===
  296. false,
  297. ({ module: newModule, export: exportName }) => {
  298. moduleGraph.updateModule(dep, newModule);
  299. moduleGraph.addExplanation(
  300. dep,
  301. "(skipped side-effect-free modules)"
  302. );
  303. const ids = dep.getIds(moduleGraph);
  304. dep.setIds(
  305. moduleGraph,
  306. exportName
  307. ? [...exportName, ...ids.slice(1)]
  308. : ids.slice(1)
  309. );
  310. return moduleGraph.getConnection(dep);
  311. }
  312. );
  313. continue;
  314. }
  315. // TODO improve for nested imports
  316. const ids = dep.getIds(moduleGraph);
  317. if (ids.length > 0) {
  318. const exportInfo = exportsInfo.getExportInfo(ids[0]);
  319. const target = exportInfo.getTarget(
  320. moduleGraph,
  321. ({ module }) =>
  322. module.getSideEffectsConnectionState(moduleGraph) ===
  323. false
  324. );
  325. if (!target) continue;
  326. moduleGraph.updateModule(dep, target.module);
  327. moduleGraph.addExplanation(
  328. dep,
  329. "(skipped side-effect-free modules)"
  330. );
  331. dep.setIds(
  332. moduleGraph,
  333. target.export
  334. ? [...target.export, ...ids.slice(1)]
  335. : ids.slice(1)
  336. );
  337. }
  338. }
  339. }
  340. }
  341. };
  342. for (const module of modules) {
  343. optimizeIncomingConnections(module);
  344. }
  345. logger.timeEnd("update dependencies");
  346. }
  347. );
  348. }
  349. );
  350. }
  351. /**
  352. * @param {string} moduleName the module name
  353. * @param {undefined | boolean | string | string[]} flagValue the flag value
  354. * @param {Map<string, RegExp>} cache cache for glob to regexp
  355. * @returns {boolean | undefined} true, when the module has side effects, undefined or false when not
  356. */
  357. static moduleHasSideEffects(moduleName, flagValue, cache) {
  358. switch (typeof flagValue) {
  359. case "undefined":
  360. return true;
  361. case "boolean":
  362. return flagValue;
  363. case "string":
  364. return globToRegexp(flagValue, cache).test(moduleName);
  365. case "object":
  366. return flagValue.some(glob =>
  367. SideEffectsFlagPlugin.moduleHasSideEffects(moduleName, glob, cache)
  368. );
  369. }
  370. }
  371. }
  372. module.exports = SideEffectsFlagPlugin;