SideEffectsFlagPlugin.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  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_DYNAMIC,
  10. JAVASCRIPT_MODULE_TYPE_ESM
  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").DependencyLocation} DependencyLocation */
  22. /** @typedef {import("../Module")} Module */
  23. /** @typedef {import("../Module").BuildMeta} BuildMeta */
  24. /** @typedef {import("../ModuleGraphConnection")} ModuleGraphConnection */
  25. /** @typedef {import("../NormalModuleFactory").ModuleSettings} ModuleSettings */
  26. /** @typedef {import("../javascript/JavascriptParser")} JavascriptParser */
  27. /** @typedef {import("../javascript/JavascriptParser").Range} Range */
  28. /**
  29. * Defines the export in module type used by this module.
  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. /** @typedef {string | boolean | string[] | undefined} SideEffectsFlagValue */
  36. /** @typedef {Map<string, RegExp>} CacheItem */
  37. /** @type {WeakMap<Compiler, CacheItem>} */
  38. const globToRegexpCache = new WeakMap();
  39. /**
  40. * Returns a regular expression.
  41. * @param {string} glob the pattern
  42. * @param {CacheItem} cache the glob to RegExp cache
  43. * @returns {RegExp} a regular expression
  44. */
  45. const globToRegexp = (glob, cache) => {
  46. const cacheEntry = cache.get(glob);
  47. if (cacheEntry !== undefined) return cacheEntry;
  48. if (!glob.includes("/")) {
  49. glob = `**/${glob}`;
  50. }
  51. const baseRegexp = glob2regexp(glob, { globstar: true, extended: true });
  52. const regexpSource = baseRegexp.source;
  53. const regexp = new RegExp(`^(\\./)?${regexpSource.slice(1)}`);
  54. cache.set(glob, regexp);
  55. return regexp;
  56. };
  57. const PLUGIN_NAME = "SideEffectsFlagPlugin";
  58. class SideEffectsFlagPlugin {
  59. /**
  60. * Creates an instance of SideEffectsFlagPlugin.
  61. * @param {boolean} analyseSource analyse source code for side effects
  62. */
  63. constructor(analyseSource = true) {
  64. /** @type {boolean} */
  65. this._analyseSource = analyseSource;
  66. }
  67. /**
  68. * Applies the plugin by registering its hooks on the compiler.
  69. * @param {Compiler} compiler the compiler instance
  70. * @returns {void}
  71. */
  72. apply(compiler) {
  73. let cache = globToRegexpCache.get(compiler.root);
  74. if (cache === undefined) {
  75. cache = new Map();
  76. globToRegexpCache.set(compiler.root, cache);
  77. }
  78. compiler.hooks.compilation.tap(
  79. PLUGIN_NAME,
  80. (compilation, { normalModuleFactory }) => {
  81. const moduleGraph = compilation.moduleGraph;
  82. normalModuleFactory.hooks.module.tap(PLUGIN_NAME, (module, data) => {
  83. const resolveData = data.resourceResolveData;
  84. if (
  85. resolveData &&
  86. resolveData.descriptionFileData &&
  87. resolveData.relativePath
  88. ) {
  89. const sideEffects = resolveData.descriptionFileData.sideEffects;
  90. if (sideEffects !== undefined) {
  91. if (module.factoryMeta === undefined) {
  92. module.factoryMeta = {};
  93. }
  94. const hasSideEffects = SideEffectsFlagPlugin.moduleHasSideEffects(
  95. resolveData.relativePath,
  96. /** @type {SideEffectsFlagValue} */ (sideEffects),
  97. /** @type {CacheItem} */ (cache)
  98. );
  99. module.factoryMeta.sideEffectFree = !hasSideEffects;
  100. }
  101. }
  102. return module;
  103. });
  104. normalModuleFactory.hooks.module.tap(PLUGIN_NAME, (module, data) => {
  105. const settings = /** @type {ModuleSettings} */ (data.settings);
  106. if (typeof settings.sideEffects === "boolean") {
  107. if (module.factoryMeta === undefined) {
  108. module.factoryMeta = {};
  109. }
  110. module.factoryMeta.sideEffectFree = !settings.sideEffects;
  111. }
  112. return module;
  113. });
  114. if (this._analyseSource) {
  115. /**
  116. * Processes the provided parser.
  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. /** @type {Set<Module>} */
  272. const optimizedModules = new Set();
  273. /**
  274. * Optimize incoming connections.
  275. * @param {Module} module module
  276. */
  277. const optimizeIncomingConnections = (module) => {
  278. if (optimizedModules.has(module)) return;
  279. optimizedModules.add(module);
  280. if (module.getSideEffectsConnectionState(moduleGraph) === false) {
  281. const exportsInfo = moduleGraph.getExportsInfo(module);
  282. for (const connection of moduleGraph.getIncomingConnections(
  283. module
  284. )) {
  285. const dep = connection.dependency;
  286. /** @type {boolean} */
  287. let isReexport;
  288. if (
  289. (isReexport =
  290. dep instanceof
  291. HarmonyExportImportedSpecifierDependency) ||
  292. (dep instanceof HarmonyImportSpecifierDependency &&
  293. !dep.namespaceObjectAsContext)
  294. ) {
  295. if (connection.originModule !== null) {
  296. optimizeIncomingConnections(connection.originModule);
  297. }
  298. // TODO improve for export *
  299. if (isReexport && dep.name) {
  300. const exportInfo = moduleGraph.getExportInfo(
  301. /** @type {Module} */ (connection.originModule),
  302. dep.name
  303. );
  304. exportInfo.moveTarget(
  305. moduleGraph,
  306. ({ module }) =>
  307. module.getSideEffectsConnectionState(moduleGraph) ===
  308. false,
  309. ({
  310. module: newModule,
  311. export: exportName,
  312. connection: targetConnection
  313. }) => {
  314. moduleGraph.updateModule(dep, newModule);
  315. moduleGraph.updateParent(
  316. dep,
  317. targetConnection,
  318. /** @type {Module} */ (connection.originModule)
  319. );
  320. moduleGraph.addExplanation(
  321. dep,
  322. "(skipped side-effect-free modules)"
  323. );
  324. const ids = dep.getIds(moduleGraph);
  325. dep.setIds(
  326. moduleGraph,
  327. exportName
  328. ? [...exportName, ...ids.slice(1)]
  329. : ids.slice(1)
  330. );
  331. return /** @type {ModuleGraphConnection} */ (
  332. moduleGraph.getConnection(dep)
  333. );
  334. }
  335. );
  336. continue;
  337. }
  338. // TODO improve for nested imports
  339. const ids = dep.getIds(moduleGraph);
  340. if (ids.length > 0) {
  341. const exportInfo = exportsInfo.getExportInfo(ids[0]);
  342. const target = exportInfo.getTarget(
  343. moduleGraph,
  344. ({ module }) =>
  345. module.getSideEffectsConnectionState(moduleGraph) ===
  346. false
  347. );
  348. if (!target) continue;
  349. moduleGraph.updateModule(dep, target.module);
  350. moduleGraph.updateParent(
  351. dep,
  352. /** @type {ModuleGraphConnection} */ (
  353. target.connection
  354. ),
  355. /** @type {Module} */ (connection.originModule)
  356. );
  357. moduleGraph.addExplanation(
  358. dep,
  359. "(skipped side-effect-free modules)"
  360. );
  361. dep.setIds(
  362. moduleGraph,
  363. target.export
  364. ? [...target.export, ...ids.slice(1)]
  365. : ids.slice(1)
  366. );
  367. }
  368. }
  369. }
  370. }
  371. };
  372. for (const module of modules) {
  373. optimizeIncomingConnections(module);
  374. }
  375. moduleGraph.finishUpdateParent();
  376. logger.timeEnd("update dependencies");
  377. }
  378. );
  379. }
  380. );
  381. }
  382. /**
  383. * Module has side effects.
  384. * @param {string} moduleName the module name
  385. * @param {SideEffectsFlagValue} flagValue the flag value
  386. * @param {CacheItem} cache cache for glob to regexp
  387. * @returns {boolean | undefined} true, when the module has side effects, undefined or false when not
  388. */
  389. static moduleHasSideEffects(moduleName, flagValue, cache) {
  390. switch (typeof flagValue) {
  391. case "undefined":
  392. return true;
  393. case "boolean":
  394. return flagValue;
  395. case "string":
  396. return globToRegexp(flagValue, cache).test(moduleName);
  397. case "object":
  398. return flagValue.some((glob) =>
  399. SideEffectsFlagPlugin.moduleHasSideEffects(moduleName, glob, cache)
  400. );
  401. }
  402. }
  403. }
  404. module.exports = SideEffectsFlagPlugin;