SideEffectsFlagPlugin.js 12 KB

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