source: frontend/node_modules/webpack/lib/optimize/SideEffectsFlagPlugin.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 11 days ago

Fix frontend appearance

  • Property mode set to 100644
File size: 16.6 KB
Line 
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Tobias Koppers @sokra
4*/
5
6"use strict";
7
8const glob2regexp = require("glob-to-regexp");
9const {
10 JAVASCRIPT_MODULE_TYPE_AUTO,
11 JAVASCRIPT_MODULE_TYPE_DYNAMIC,
12 JAVASCRIPT_MODULE_TYPE_ESM
13} = require("../ModuleTypeConstants");
14const { STAGE_DEFAULT } = require("../OptimizationStages");
15const HarmonyExportImportedSpecifierDependency = require("../dependencies/HarmonyExportImportedSpecifierDependency");
16const HarmonyImportSpecifierDependency = require("../dependencies/HarmonyImportSpecifierDependency");
17const formatLocation = require("../util/formatLocation");
18const { CompilerHintNotationRegExp } = require("../util/magicComment");
19
20/** @typedef {import("estree").MaybeNamedClassDeclaration} MaybeNamedClassDeclaration */
21/** @typedef {import("estree").MaybeNamedFunctionDeclaration} MaybeNamedFunctionDeclaration */
22/** @typedef {import("estree").ModuleDeclaration} ModuleDeclaration */
23/** @typedef {import("estree").Statement} Statement */
24/** @typedef {import("estree").CallExpression} CallExpression */
25/** @typedef {import("../Compiler")} Compiler */
26/** @typedef {import("../Dependency").DependencyLocation} DependencyLocation */
27/** @typedef {import("../Module")} Module */
28/** @typedef {import("../Module").BuildMeta} BuildMeta */
29/** @typedef {import("../ModuleGraphConnection")} ModuleGraphConnection */
30/** @typedef {import("../javascript/JavascriptParser")} JavascriptParser */
31/** @typedef {import("../javascript/JavascriptParser").Range} Range */
32
33/**
34 * Defines the export in module type used by this module.
35 * @typedef {object} ExportInModule
36 * @property {Module} module the module
37 * @property {string} exportName the name of the export
38 * @property {boolean} checked if the export is conditional
39 */
40
41/** @typedef {string | boolean | string[] | undefined} SideEffectsFlagValue */
42
43/** @typedef {Map<string, RegExp>} CacheItem */
44
45/** @type {WeakMap<Compiler, CacheItem>} */
46const globToRegexpCache = new WeakMap();
47
48/**
49 * Returns a regular expression.
50 * @param {string} glob the pattern
51 * @param {CacheItem} cache the glob to RegExp cache
52 * @returns {RegExp} a regular expression
53 */
54const globToRegexp = (glob, cache) => {
55 const cacheEntry = cache.get(glob);
56 if (cacheEntry !== undefined) return cacheEntry;
57 if (!glob.includes("/")) {
58 glob = `**/${glob}`;
59 }
60 const baseRegexp = glob2regexp(glob, { globstar: true, extended: true });
61 const regexpSource = baseRegexp.source;
62 const regexp = new RegExp(`^(\\./)?${regexpSource.slice(1)}`);
63 cache.set(glob, regexp);
64 return regexp;
65};
66
67/**
68 * @param {JavascriptParser} parser parser
69 * @param {number} start start position
70 * @param {number} end end position
71 * @returns {boolean} if annotation is found in the range
72 */
73const hasNoSideEffectsNotation = (parser, start, end) => {
74 // Fast path
75 if (end - start < 18) return false;
76
77 const comments = parser.getComments([start, end]);
78 return comments.some(
79 (c) =>
80 c.type === "Block" &&
81 CompilerHintNotationRegExp.NoSideEffects.test(c.value)
82 );
83};
84
85const PLUGIN_NAME = "SideEffectsFlagPlugin";
86
87class SideEffectsFlagPlugin {
88 /**
89 * Creates an instance of SideEffectsFlagPlugin.
90 * @param {boolean} analyseSource analyse source code for side effects
91 */
92 constructor(analyseSource = true) {
93 /** @type {boolean} */
94 this._analyseSource = analyseSource;
95 }
96
97 /**
98 * Applies the plugin by registering its hooks on the compiler.
99 * @param {Compiler} compiler the compiler instance
100 * @returns {void}
101 */
102 apply(compiler) {
103 let cache = globToRegexpCache.get(compiler.root);
104 if (cache === undefined) {
105 cache = new Map();
106 globToRegexpCache.set(compiler.root, cache);
107 }
108 compiler.hooks.compilation.tap(
109 PLUGIN_NAME,
110 (compilation, { normalModuleFactory }) => {
111 const moduleGraph = compilation.moduleGraph;
112 normalModuleFactory.hooks.module.tap(PLUGIN_NAME, (module, data) => {
113 const resolveData = data.resourceResolveData;
114 if (
115 resolveData &&
116 resolveData.descriptionFileData &&
117 resolveData.relativePath
118 ) {
119 const sideEffects = resolveData.descriptionFileData.sideEffects;
120 if (sideEffects !== undefined) {
121 if (module.factoryMeta === undefined) {
122 module.factoryMeta = {};
123 }
124 const hasSideEffects = SideEffectsFlagPlugin.moduleHasSideEffects(
125 resolveData.relativePath,
126 /** @type {SideEffectsFlagValue} */ (sideEffects),
127 /** @type {CacheItem} */ (cache)
128 );
129 module.factoryMeta.sideEffectFree = !hasSideEffects;
130 }
131 }
132
133 return module;
134 });
135 normalModuleFactory.hooks.module.tap(PLUGIN_NAME, (module, data) => {
136 const settings = data.settings;
137 if (typeof settings.sideEffects === "boolean") {
138 if (module.factoryMeta === undefined) {
139 module.factoryMeta = {};
140 }
141 module.factoryMeta.sideEffectFree = !settings.sideEffects;
142 }
143 return module;
144 });
145 if (this._analyseSource) {
146 /**
147 * Processes the provided parser.
148 * @param {JavascriptParser} parser the parser
149 * @returns {void}
150 */
151 const applySideEffectsStmtHandler = (parser) => {
152 /** @type {undefined | Statement | ModuleDeclaration | MaybeNamedFunctionDeclaration | MaybeNamedClassDeclaration} */
153 let sideEffectsStatement;
154 parser.hooks.program.tap(PLUGIN_NAME, () => {
155 sideEffectsStatement = undefined;
156 });
157 parser.hooks.statement.tap(
158 { name: PLUGIN_NAME, stage: -100 },
159 (statement) => {
160 if (sideEffectsStatement) return;
161 if (parser.scope.topLevelScope !== true) return;
162 switch (statement.type) {
163 case "ExpressionStatement":
164 if (
165 !parser.isPure(
166 statement.expression,
167 /** @type {Range} */
168 (statement.range)[0]
169 )
170 ) {
171 sideEffectsStatement = statement;
172 }
173 break;
174 case "IfStatement":
175 case "WhileStatement":
176 case "DoWhileStatement":
177 if (
178 !parser.isPure(
179 statement.test,
180 /** @type {Range} */
181 (statement.range)[0]
182 )
183 ) {
184 sideEffectsStatement = statement;
185 }
186 // statement hook will be called for child statements too
187 break;
188 case "ForStatement":
189 if (
190 !parser.isPure(
191 statement.init,
192 /** @type {Range} */ (statement.range)[0]
193 ) ||
194 !parser.isPure(
195 statement.test,
196 statement.init
197 ? /** @type {Range} */ (statement.init.range)[1]
198 : /** @type {Range} */ (statement.range)[0]
199 ) ||
200 !parser.isPure(
201 statement.update,
202 statement.test
203 ? /** @type {Range} */ (statement.test.range)[1]
204 : statement.init
205 ? /** @type {Range} */ (statement.init.range)[1]
206 : /** @type {Range} */ (statement.range)[0]
207 )
208 ) {
209 sideEffectsStatement = statement;
210 }
211 // statement hook will be called for child statements too
212 break;
213 case "SwitchStatement":
214 if (
215 !parser.isPure(
216 statement.discriminant,
217 /** @type {Range} */
218 (statement.range)[0]
219 )
220 ) {
221 sideEffectsStatement = statement;
222 }
223 // statement hook will be called for child statements too
224 break;
225 case "VariableDeclaration":
226 case "ClassDeclaration":
227 case "FunctionDeclaration":
228 if (
229 !parser.isPure(
230 statement,
231 /** @type {Range} */ (statement.range)[0]
232 )
233 ) {
234 sideEffectsStatement = statement;
235 }
236 break;
237 case "ExportNamedDeclaration":
238 case "ExportDefaultDeclaration":
239 if (
240 !parser.isPure(
241 statement.declaration,
242 /** @type {Range} */
243 (statement.range)[0]
244 )
245 ) {
246 sideEffectsStatement = statement;
247 }
248 break;
249 case "LabeledStatement":
250 case "BlockStatement":
251 // statement hook will be called for child statements too
252 break;
253 case "EmptyStatement":
254 break;
255 case "ExportAllDeclaration":
256 case "ImportDeclaration":
257 // imports will be handled by the dependencies
258 break;
259 default:
260 sideEffectsStatement = statement;
261 break;
262 }
263 }
264 );
265 parser.hooks.finish.tap(PLUGIN_NAME, () => {
266 if (sideEffectsStatement === undefined) {
267 /** @type {BuildMeta} */
268 (parser.state.module.buildMeta).sideEffectFree = true;
269 } else {
270 const { loc, type } = sideEffectsStatement;
271 moduleGraph
272 .getOptimizationBailout(parser.state.module)
273 .push(
274 () =>
275 `Statement (${type}) with side effects in source code at ${formatLocation(
276 /** @type {DependencyLocation} */ (loc)
277 )}`
278 );
279 }
280 });
281 };
282
283 /**
284 * @param {JavascriptParser} parser the parser
285 * @returns {void}
286 */
287 const applyNoSideEffectsNotationHandler = (parser) => {
288 /** @type {Set<string>} */
289 let noSideEffectsFnNames;
290
291 parser.hooks.program.tap(PLUGIN_NAME, () => {
292 noSideEffectsFnNames = new Set();
293 });
294
295 // Detect on function declarations
296 // Covers:
297 // 1. function foo
298 // 2. export function foo
299 // 3. export default function foo
300 parser.hooks.preStatement.tap(PLUGIN_NAME, (statement) => {
301 if (parser.scope.topLevelScope !== true) return;
302 if (statement.type !== "FunctionDeclaration" || !statement.id) {
303 return;
304 }
305 const commentsStart = parser.prevStatement
306 ? /** @type {Range} */ (parser.prevStatement.range)[1]
307 : 0;
308 if (
309 hasNoSideEffectsNotation(
310 parser,
311 commentsStart,
312 /** @type {Range} */ (statement.range)[0]
313 )
314 ) {
315 noSideEffectsFnNames.add(statement.id.name);
316 }
317 });
318
319 // Detect on variable declarations with function init
320 parser.hooks.preDeclarator.tap(PLUGIN_NAME, (decl, statement) => {
321 if (parser.scope.topLevelScope !== true) return;
322 if (!decl.init || decl.id.type !== "Identifier") return;
323 if (!decl.init.type.endsWith("FunctionExpression")) return;
324
325 let hasAnnotation = false;
326 // Before the VariableDeclaration (only for const)
327 if (statement.kind === "const") {
328 const commentsStart = parser.prevStatement
329 ? /** @type {Range} */ (parser.prevStatement.range)[1]
330 : 0;
331 hasAnnotation = hasNoSideEffectsNotation(
332 parser,
333 commentsStart,
334 /** @type {Range} */ (statement.range)[0]
335 );
336 }
337
338 if (!hasAnnotation) {
339 hasAnnotation = hasNoSideEffectsNotation(
340 parser,
341 /** @type {Range} */ (decl.id.range)[1],
342 /** @type {Range} */ (decl.init.range)[0]
343 );
344 }
345 if (hasAnnotation) {
346 noSideEffectsFnNames.add(decl.id.name);
347 }
348 });
349
350 // Mark calls to annotated functions as pure
351 parser.hooks.isPure
352 .for("CallExpression")
353 .tap(PLUGIN_NAME, (expression, commentsStartPos) => {
354 const expr = /** @type {CallExpression} */ (expression);
355 if (expr.callee.type !== "Identifier") return;
356 if (!noSideEffectsFnNames.has(expr.callee.name)) return;
357 commentsStartPos = /** @type {Range} */ (expr.callee.range)[1];
358 for (const arg of expr.arguments) {
359 if (arg.type === "SpreadElement") return;
360 if (!parser.isPure(arg, commentsStartPos)) return;
361 commentsStartPos = /** @type {Range} */ (arg.range)[1];
362 }
363 return true;
364 });
365 };
366
367 for (const key of [
368 JAVASCRIPT_MODULE_TYPE_AUTO,
369 JAVASCRIPT_MODULE_TYPE_ESM,
370 JAVASCRIPT_MODULE_TYPE_DYNAMIC
371 ]) {
372 normalModuleFactory.hooks.parser
373 .for(key)
374 .tap(PLUGIN_NAME, (parser) => {
375 applyNoSideEffectsNotationHandler(parser);
376 applySideEffectsStmtHandler(parser);
377 });
378 }
379 }
380 compilation.hooks.optimizeDependencies.tap(
381 {
382 name: PLUGIN_NAME,
383 stage: STAGE_DEFAULT
384 },
385 (modules) => {
386 const logger = compilation.getLogger(
387 "webpack.SideEffectsFlagPlugin"
388 );
389
390 logger.time("update dependencies");
391
392 /** @type {Set<Module>} */
393 const optimizedModules = new Set();
394
395 /**
396 * Optimize incoming connections.
397 * @param {Module} module module
398 */
399 const optimizeIncomingConnections = (module) => {
400 if (optimizedModules.has(module)) return;
401 optimizedModules.add(module);
402 if (module.getSideEffectsConnectionState(moduleGraph) === false) {
403 const exportsInfo = moduleGraph.getExportsInfo(module);
404 for (const connection of moduleGraph.getIncomingConnections(
405 module
406 )) {
407 const dep = connection.dependency;
408 /** @type {boolean} */
409 let isReexport;
410 if (
411 (isReexport =
412 dep instanceof
413 HarmonyExportImportedSpecifierDependency) ||
414 (dep instanceof HarmonyImportSpecifierDependency &&
415 !dep.namespaceObjectAsContext)
416 ) {
417 if (connection.originModule !== null) {
418 optimizeIncomingConnections(connection.originModule);
419 }
420 // TODO improve for export *
421 if (isReexport && dep.name) {
422 const exportInfo = moduleGraph.getExportInfo(
423 /** @type {Module} */ (connection.originModule),
424 dep.name
425 );
426 exportInfo.moveTarget(
427 moduleGraph,
428 ({ module }) =>
429 module.getSideEffectsConnectionState(moduleGraph) ===
430 false,
431 ({
432 module: newModule,
433 export: exportName,
434 connection: targetConnection
435 }) => {
436 moduleGraph.updateModule(dep, newModule);
437 moduleGraph.updateParent(
438 dep,
439 targetConnection,
440 /** @type {Module} */ (connection.originModule)
441 );
442 moduleGraph.addExplanation(
443 dep,
444 "(skipped side-effect-free modules)"
445 );
446 const ids = dep.getIds(moduleGraph);
447 dep.setIds(
448 moduleGraph,
449 exportName
450 ? [...exportName, ...ids.slice(1)]
451 : ids.slice(1)
452 );
453 return /** @type {ModuleGraphConnection} */ (
454 moduleGraph.getConnection(dep)
455 );
456 }
457 );
458 continue;
459 }
460 // TODO improve for nested imports
461 const ids = dep.getIds(moduleGraph);
462 if (ids.length > 0) {
463 const exportInfo = exportsInfo.getExportInfo(ids[0]);
464 const target = exportInfo.getTarget(
465 moduleGraph,
466 ({ module }) =>
467 module.getSideEffectsConnectionState(moduleGraph) ===
468 false
469 );
470 if (!target) continue;
471
472 moduleGraph.updateModule(dep, target.module);
473 moduleGraph.updateParent(
474 dep,
475 /** @type {ModuleGraphConnection} */ (
476 target.connection
477 ),
478 /** @type {Module} */ (connection.originModule)
479 );
480 moduleGraph.addExplanation(
481 dep,
482 "(skipped side-effect-free modules)"
483 );
484 dep.setIds(
485 moduleGraph,
486 target.export
487 ? [...target.export, ...ids.slice(1)]
488 : ids.slice(1)
489 );
490 }
491 }
492 }
493 }
494 };
495
496 for (const module of modules) {
497 optimizeIncomingConnections(module);
498 }
499 moduleGraph.finishUpdateParent();
500 logger.timeEnd("update dependencies");
501 }
502 );
503 }
504 );
505 }
506
507 /**
508 * Module has side effects.
509 * @param {string} moduleName the module name
510 * @param {SideEffectsFlagValue} flagValue the flag value
511 * @param {CacheItem} cache cache for glob to regexp
512 * @returns {boolean | undefined} true, when the module has side effects, undefined or false when not
513 */
514 static moduleHasSideEffects(moduleName, flagValue, cache) {
515 switch (typeof flagValue) {
516 case "undefined":
517 return true;
518 case "boolean":
519 return flagValue;
520 case "string":
521 return globToRegexp(flagValue, cache).test(moduleName);
522 case "object":
523 return flagValue.some((glob) =>
524 SideEffectsFlagPlugin.moduleHasSideEffects(moduleName, glob, cache)
525 );
526 }
527 }
528}
529
530module.exports = SideEffectsFlagPlugin;
Note: See TracBrowser for help on using the repository browser.