123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389 |
- /*
- MIT License http://www.opensource.org/licenses/mit-license.php
- Author Tobias Koppers @sokra
- */
- "use strict";
- const glob2regexp = require("glob-to-regexp");
- const {
- JAVASCRIPT_MODULE_TYPE_AUTO,
- JAVASCRIPT_MODULE_TYPE_ESM,
- JAVASCRIPT_MODULE_TYPE_DYNAMIC
- } = require("../ModuleTypeConstants");
- const { STAGE_DEFAULT } = require("../OptimizationStages");
- const HarmonyExportImportedSpecifierDependency = require("../dependencies/HarmonyExportImportedSpecifierDependency");
- const HarmonyImportSpecifierDependency = require("../dependencies/HarmonyImportSpecifierDependency");
- const formatLocation = require("../formatLocation");
- /** @typedef {import("estree").ModuleDeclaration} ModuleDeclaration */
- /** @typedef {import("estree").Statement} Statement */
- /** @typedef {import("../Compiler")} Compiler */
- /** @typedef {import("../Dependency")} Dependency */
- /** @typedef {import("../Dependency").DependencyLocation} DependencyLocation */
- /** @typedef {import("../Module")} Module */
- /** @typedef {import("../Module").BuildMeta} BuildMeta */
- /** @typedef {import("../javascript/JavascriptParser")} JavascriptParser */
- /** @typedef {import("../javascript/JavascriptParser").Range} Range */
- /**
- * @typedef {object} ExportInModule
- * @property {Module} module the module
- * @property {string} exportName the name of the export
- * @property {boolean} checked if the export is conditional
- */
- /**
- * @typedef {object} ReexportInfo
- * @property {Map<string, ExportInModule[]>} static
- * @property {Map<Module, Set<string>>} dynamic
- */
- /** @typedef {Map<string, RegExp>} CacheItem */
- /** @type {WeakMap<any, CacheItem>} */
- const globToRegexpCache = new WeakMap();
- /**
- * @param {string} glob the pattern
- * @param {Map<string, RegExp>} cache the glob to RegExp cache
- * @returns {RegExp} a regular expression
- */
- const globToRegexp = (glob, cache) => {
- const cacheEntry = cache.get(glob);
- if (cacheEntry !== undefined) return cacheEntry;
- if (!glob.includes("/")) {
- glob = `**/${glob}`;
- }
- const baseRegexp = glob2regexp(glob, { globstar: true, extended: true });
- const regexpSource = baseRegexp.source;
- const regexp = new RegExp("^(\\./)?" + regexpSource.slice(1));
- cache.set(glob, regexp);
- return regexp;
- };
- const PLUGIN_NAME = "SideEffectsFlagPlugin";
- class SideEffectsFlagPlugin {
- /**
- * @param {boolean} analyseSource analyse source code for side effects
- */
- constructor(analyseSource = true) {
- this._analyseSource = analyseSource;
- }
- /**
- * Apply the plugin
- * @param {Compiler} compiler the compiler instance
- * @returns {void}
- */
- apply(compiler) {
- let cache = globToRegexpCache.get(compiler.root);
- if (cache === undefined) {
- cache = new Map();
- globToRegexpCache.set(compiler.root, cache);
- }
- compiler.hooks.compilation.tap(
- PLUGIN_NAME,
- (compilation, { normalModuleFactory }) => {
- const moduleGraph = compilation.moduleGraph;
- normalModuleFactory.hooks.module.tap(PLUGIN_NAME, (module, data) => {
- const resolveData = data.resourceResolveData;
- if (
- resolveData &&
- resolveData.descriptionFileData &&
- resolveData.relativePath
- ) {
- const sideEffects = resolveData.descriptionFileData.sideEffects;
- if (sideEffects !== undefined) {
- if (module.factoryMeta === undefined) {
- module.factoryMeta = {};
- }
- const hasSideEffects = SideEffectsFlagPlugin.moduleHasSideEffects(
- resolveData.relativePath,
- sideEffects,
- /** @type {CacheItem} */ (cache)
- );
- module.factoryMeta.sideEffectFree = !hasSideEffects;
- }
- }
- return module;
- });
- normalModuleFactory.hooks.module.tap(PLUGIN_NAME, (module, data) => {
- if (typeof data.settings.sideEffects === "boolean") {
- if (module.factoryMeta === undefined) {
- module.factoryMeta = {};
- }
- module.factoryMeta.sideEffectFree = !data.settings.sideEffects;
- }
- return module;
- });
- if (this._analyseSource) {
- /**
- * @param {JavascriptParser} parser the parser
- * @returns {void}
- */
- const parserHandler = parser => {
- /** @type {undefined | Statement | ModuleDeclaration} */
- let sideEffectsStatement;
- parser.hooks.program.tap(PLUGIN_NAME, () => {
- sideEffectsStatement = undefined;
- });
- parser.hooks.statement.tap(
- { name: PLUGIN_NAME, stage: -100 },
- statement => {
- if (sideEffectsStatement) return;
- if (parser.scope.topLevelScope !== true) return;
- switch (statement.type) {
- case "ExpressionStatement":
- if (
- !parser.isPure(
- statement.expression,
- /** @type {Range} */ (statement.range)[0]
- )
- ) {
- sideEffectsStatement = statement;
- }
- break;
- case "IfStatement":
- case "WhileStatement":
- case "DoWhileStatement":
- if (
- !parser.isPure(
- statement.test,
- /** @type {Range} */ (statement.range)[0]
- )
- ) {
- sideEffectsStatement = statement;
- }
- // statement hook will be called for child statements too
- break;
- case "ForStatement":
- if (
- !parser.isPure(
- statement.init,
- /** @type {Range} */ (statement.range)[0]
- ) ||
- !parser.isPure(
- statement.test,
- statement.init
- ? /** @type {Range} */ (statement.init.range)[1]
- : /** @type {Range} */ (statement.range)[0]
- ) ||
- !parser.isPure(
- statement.update,
- statement.test
- ? /** @type {Range} */ (statement.test.range)[1]
- : statement.init
- ? /** @type {Range} */ (statement.init.range)[1]
- : /** @type {Range} */ (statement.range)[0]
- )
- ) {
- sideEffectsStatement = statement;
- }
- // statement hook will be called for child statements too
- break;
- case "SwitchStatement":
- if (
- !parser.isPure(
- statement.discriminant,
- /** @type {Range} */ (statement.range)[0]
- )
- ) {
- sideEffectsStatement = statement;
- }
- // statement hook will be called for child statements too
- break;
- case "VariableDeclaration":
- case "ClassDeclaration":
- case "FunctionDeclaration":
- if (
- !parser.isPure(
- statement,
- /** @type {Range} */ (statement.range)[0]
- )
- ) {
- sideEffectsStatement = statement;
- }
- break;
- case "ExportNamedDeclaration":
- case "ExportDefaultDeclaration":
- if (
- !parser.isPure(
- statement.declaration,
- /** @type {Range} */ (statement.range)[0]
- )
- ) {
- sideEffectsStatement = statement;
- }
- break;
- case "LabeledStatement":
- case "BlockStatement":
- // statement hook will be called for child statements too
- break;
- case "EmptyStatement":
- break;
- case "ExportAllDeclaration":
- case "ImportDeclaration":
- // imports will be handled by the dependencies
- break;
- default:
- sideEffectsStatement = statement;
- break;
- }
- }
- );
- parser.hooks.finish.tap(PLUGIN_NAME, () => {
- if (sideEffectsStatement === undefined) {
- /** @type {BuildMeta} */
- (parser.state.module.buildMeta).sideEffectFree = true;
- } else {
- const { loc, type } = sideEffectsStatement;
- moduleGraph
- .getOptimizationBailout(parser.state.module)
- .push(
- () =>
- `Statement (${type}) with side effects in source code at ${formatLocation(
- /** @type {DependencyLocation} */ (loc)
- )}`
- );
- }
- });
- };
- for (const key of [
- JAVASCRIPT_MODULE_TYPE_AUTO,
- JAVASCRIPT_MODULE_TYPE_ESM,
- JAVASCRIPT_MODULE_TYPE_DYNAMIC
- ]) {
- normalModuleFactory.hooks.parser
- .for(key)
- .tap(PLUGIN_NAME, parserHandler);
- }
- }
- compilation.hooks.optimizeDependencies.tap(
- {
- name: PLUGIN_NAME,
- stage: STAGE_DEFAULT
- },
- modules => {
- const logger = compilation.getLogger(
- "webpack.SideEffectsFlagPlugin"
- );
- logger.time("update dependencies");
- const optimizedModules = new Set();
- /**
- * @param {Module} module module
- */
- const optimizeIncomingConnections = module => {
- if (optimizedModules.has(module)) return;
- optimizedModules.add(module);
- if (module.getSideEffectsConnectionState(moduleGraph) === false) {
- const exportsInfo = moduleGraph.getExportsInfo(module);
- for (const connection of moduleGraph.getIncomingConnections(
- module
- )) {
- const dep = connection.dependency;
- let isReexport;
- if (
- (isReexport =
- dep instanceof
- HarmonyExportImportedSpecifierDependency) ||
- (dep instanceof HarmonyImportSpecifierDependency &&
- !dep.namespaceObjectAsContext)
- ) {
- if (connection.originModule !== null) {
- optimizeIncomingConnections(connection.originModule);
- }
- // TODO improve for export *
- if (isReexport && dep.name) {
- const exportInfo = moduleGraph.getExportInfo(
- /** @type {Module} */ (connection.originModule),
- dep.name
- );
- exportInfo.moveTarget(
- moduleGraph,
- ({ module }) =>
- module.getSideEffectsConnectionState(moduleGraph) ===
- false,
- ({ module: newModule, export: exportName }) => {
- moduleGraph.updateModule(dep, newModule);
- moduleGraph.addExplanation(
- dep,
- "(skipped side-effect-free modules)"
- );
- const ids = dep.getIds(moduleGraph);
- dep.setIds(
- moduleGraph,
- exportName
- ? [...exportName, ...ids.slice(1)]
- : ids.slice(1)
- );
- return moduleGraph.getConnection(dep);
- }
- );
- continue;
- }
- // TODO improve for nested imports
- const ids = dep.getIds(moduleGraph);
- if (ids.length > 0) {
- const exportInfo = exportsInfo.getExportInfo(ids[0]);
- const target = exportInfo.getTarget(
- moduleGraph,
- ({ module }) =>
- module.getSideEffectsConnectionState(moduleGraph) ===
- false
- );
- if (!target) continue;
- moduleGraph.updateModule(dep, target.module);
- moduleGraph.addExplanation(
- dep,
- "(skipped side-effect-free modules)"
- );
- dep.setIds(
- moduleGraph,
- target.export
- ? [...target.export, ...ids.slice(1)]
- : ids.slice(1)
- );
- }
- }
- }
- }
- };
- for (const module of modules) {
- optimizeIncomingConnections(module);
- }
- logger.timeEnd("update dependencies");
- }
- );
- }
- );
- }
- /**
- * @param {string} moduleName the module name
- * @param {undefined | boolean | string | string[]} flagValue the flag value
- * @param {Map<string, RegExp>} cache cache for glob to regexp
- * @returns {boolean | undefined} true, when the module has side effects, undefined or false when not
- */
- static moduleHasSideEffects(moduleName, flagValue, cache) {
- switch (typeof flagValue) {
- case "undefined":
- return true;
- case "boolean":
- return flagValue;
- case "string":
- return globToRegexp(flagValue, cache).test(moduleName);
- case "object":
- return flagValue.some(glob =>
- SideEffectsFlagPlugin.moduleHasSideEffects(moduleName, glob, cache)
- );
- }
- }
- }
- module.exports = SideEffectsFlagPlugin;
|