SideEffectsFlagPlugin.js 13 KB

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