SideEffectsFlagPlugin.js 11 KB

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