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