SideEffectsFlagPlugin.js 10 KB

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