SideEffectsFlagPlugin.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  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} */
  137. (statement.range)[0]
  138. )
  139. ) {
  140. sideEffectsStatement = statement;
  141. }
  142. break;
  143. case "IfStatement":
  144. case "WhileStatement":
  145. case "DoWhileStatement":
  146. if (
  147. !parser.isPure(
  148. statement.test,
  149. /** @type {Range} */
  150. (statement.range)[0]
  151. )
  152. ) {
  153. sideEffectsStatement = statement;
  154. }
  155. // statement hook will be called for child statements too
  156. break;
  157. case "ForStatement":
  158. if (
  159. !parser.isPure(
  160. statement.init,
  161. /** @type {Range} */ (statement.range)[0]
  162. ) ||
  163. !parser.isPure(
  164. statement.test,
  165. statement.init
  166. ? /** @type {Range} */ (statement.init.range)[1]
  167. : /** @type {Range} */ (statement.range)[0]
  168. ) ||
  169. !parser.isPure(
  170. statement.update,
  171. statement.test
  172. ? /** @type {Range} */ (statement.test.range)[1]
  173. : statement.init
  174. ? /** @type {Range} */ (statement.init.range)[1]
  175. : /** @type {Range} */ (statement.range)[0]
  176. )
  177. ) {
  178. sideEffectsStatement = statement;
  179. }
  180. // statement hook will be called for child statements too
  181. break;
  182. case "SwitchStatement":
  183. if (
  184. !parser.isPure(
  185. statement.discriminant,
  186. /** @type {Range} */
  187. (statement.range)[0]
  188. )
  189. ) {
  190. sideEffectsStatement = statement;
  191. }
  192. // statement hook will be called for child statements too
  193. break;
  194. case "VariableDeclaration":
  195. case "ClassDeclaration":
  196. case "FunctionDeclaration":
  197. if (
  198. !parser.isPure(
  199. statement,
  200. /** @type {Range} */ (statement.range)[0]
  201. )
  202. ) {
  203. sideEffectsStatement = statement;
  204. }
  205. break;
  206. case "ExportNamedDeclaration":
  207. case "ExportDefaultDeclaration":
  208. if (
  209. !parser.isPure(
  210. statement.declaration,
  211. /** @type {Range} */
  212. (statement.range)[0]
  213. )
  214. ) {
  215. sideEffectsStatement = statement;
  216. }
  217. break;
  218. case "LabeledStatement":
  219. case "BlockStatement":
  220. // statement hook will be called for child statements too
  221. break;
  222. case "EmptyStatement":
  223. break;
  224. case "ExportAllDeclaration":
  225. case "ImportDeclaration":
  226. // imports will be handled by the dependencies
  227. break;
  228. default:
  229. sideEffectsStatement = statement;
  230. break;
  231. }
  232. }
  233. );
  234. parser.hooks.finish.tap(PLUGIN_NAME, () => {
  235. if (sideEffectsStatement === undefined) {
  236. /** @type {BuildMeta} */
  237. (parser.state.module.buildMeta).sideEffectFree = true;
  238. } else {
  239. const { loc, type } = sideEffectsStatement;
  240. moduleGraph
  241. .getOptimizationBailout(parser.state.module)
  242. .push(
  243. () =>
  244. `Statement (${type}) with side effects in source code at ${formatLocation(
  245. /** @type {DependencyLocation} */ (loc)
  246. )}`
  247. );
  248. }
  249. });
  250. };
  251. for (const key of [
  252. JAVASCRIPT_MODULE_TYPE_AUTO,
  253. JAVASCRIPT_MODULE_TYPE_ESM,
  254. JAVASCRIPT_MODULE_TYPE_DYNAMIC
  255. ]) {
  256. normalModuleFactory.hooks.parser
  257. .for(key)
  258. .tap(PLUGIN_NAME, parserHandler);
  259. }
  260. }
  261. compilation.hooks.optimizeDependencies.tap(
  262. {
  263. name: PLUGIN_NAME,
  264. stage: STAGE_DEFAULT
  265. },
  266. modules => {
  267. const logger = compilation.getLogger(
  268. "webpack.SideEffectsFlagPlugin"
  269. );
  270. logger.time("update dependencies");
  271. const optimizedModules = new Set();
  272. /**
  273. * @param {Module} module module
  274. */
  275. const optimizeIncomingConnections = module => {
  276. if (optimizedModules.has(module)) return;
  277. optimizedModules.add(module);
  278. if (module.getSideEffectsConnectionState(moduleGraph) === false) {
  279. const exportsInfo = moduleGraph.getExportsInfo(module);
  280. for (const connection of moduleGraph.getIncomingConnections(
  281. module
  282. )) {
  283. const dep = connection.dependency;
  284. let isReexport;
  285. if (
  286. (isReexport =
  287. dep instanceof
  288. HarmonyExportImportedSpecifierDependency) ||
  289. (dep instanceof HarmonyImportSpecifierDependency &&
  290. !dep.namespaceObjectAsContext)
  291. ) {
  292. if (connection.originModule !== null) {
  293. optimizeIncomingConnections(connection.originModule);
  294. }
  295. // TODO improve for export *
  296. if (isReexport && dep.name) {
  297. const exportInfo = moduleGraph.getExportInfo(
  298. /** @type {Module} */ (connection.originModule),
  299. dep.name
  300. );
  301. exportInfo.moveTarget(
  302. moduleGraph,
  303. ({ module }) =>
  304. module.getSideEffectsConnectionState(moduleGraph) ===
  305. false,
  306. ({ module: newModule, export: exportName }) => {
  307. moduleGraph.updateModule(dep, newModule);
  308. moduleGraph.addExplanation(
  309. dep,
  310. "(skipped side-effect-free modules)"
  311. );
  312. const ids = dep.getIds(moduleGraph);
  313. dep.setIds(
  314. moduleGraph,
  315. exportName
  316. ? [...exportName, ...ids.slice(1)]
  317. : ids.slice(1)
  318. );
  319. return /** @type {ModuleGraphConnection} */ (
  320. moduleGraph.getConnection(dep)
  321. );
  322. }
  323. );
  324. continue;
  325. }
  326. // TODO improve for nested imports
  327. const ids = dep.getIds(moduleGraph);
  328. if (ids.length > 0) {
  329. const exportInfo = exportsInfo.getExportInfo(ids[0]);
  330. const target = exportInfo.getTarget(
  331. moduleGraph,
  332. ({ module }) =>
  333. module.getSideEffectsConnectionState(moduleGraph) ===
  334. false
  335. );
  336. if (!target) continue;
  337. moduleGraph.updateModule(dep, target.module);
  338. moduleGraph.addExplanation(
  339. dep,
  340. "(skipped side-effect-free modules)"
  341. );
  342. dep.setIds(
  343. moduleGraph,
  344. target.export
  345. ? [...target.export, ...ids.slice(1)]
  346. : ids.slice(1)
  347. );
  348. }
  349. }
  350. }
  351. }
  352. };
  353. for (const module of modules) {
  354. optimizeIncomingConnections(module);
  355. }
  356. logger.timeEnd("update dependencies");
  357. }
  358. );
  359. }
  360. );
  361. }
  362. /**
  363. * @param {string} moduleName the module name
  364. * @param {undefined | boolean | string | string[]} flagValue the flag value
  365. * @param {Map<string, RegExp>} cache cache for glob to regexp
  366. * @returns {boolean | undefined} true, when the module has side effects, undefined or false when not
  367. */
  368. static moduleHasSideEffects(moduleName, flagValue, cache) {
  369. switch (typeof flagValue) {
  370. case "undefined":
  371. return true;
  372. case "boolean":
  373. return flagValue;
  374. case "string":
  375. return globToRegexp(flagValue, cache).test(moduleName);
  376. case "object":
  377. return flagValue.some(glob =>
  378. SideEffectsFlagPlugin.moduleHasSideEffects(moduleName, glob, cache)
  379. );
  380. }
  381. }
  382. }
  383. module.exports = SideEffectsFlagPlugin;