SideEffectsFlagPlugin.js 12 KB

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