/
githubmirror
/
webpack
Обзор
Документация
Войти
/
githubmirror
/
webpack
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
lib/optimize/InnerGraphPlugin.js
590 строк
20 KB
Alexander Akait
feat: derive parser locations from node offsets (#21451)
19 июл 2026, 21:49
Не верифицирован
19 июл 2026, 21:49
4968684
Код
Авторство
О чём код?
/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; const { JAVASCRIPT_MODULE_TYPE_AUTO, JAVASCRIPT_MODULE_TYPE_ESM } = require("../ModuleTypeConstants"); const RuntimeGlobals = require("../RuntimeGlobals"); const { harmonySpecifierTag } = require("../dependencies/HarmonyImportDependencyParserPlugin"); const HarmonyImportSideEffectDependency = require("../dependencies/HarmonyImportSideEffectDependency"); const PureExpressionDependency = require("../dependencies/PureExpressionDependency"); const { getInnerGraphUtils, topLevelSymbolTag } = require("./InnerGraph"); /** @typedef {import("estree").CallExpression} CallExpression */ /** @typedef {import("estree").ClassDeclaration} ClassDeclaration */ /** @typedef {import("estree").ClassExpression} ClassExpression */ /** @typedef {import("estree").Expression} Expression */ /** @typedef {import("estree").MaybeNamedClassDeclaration} MaybeNamedClassDeclaration */ /** @typedef {import("estree").MaybeNamedFunctionDeclaration} MaybeNamedFunctionDeclaration */ /** @typedef {import("estree").Node} Node */ /** @typedef {import("estree").VariableDeclarator} VariableDeclarator */ /** @typedef {import("../../declarations/WebpackOptions").JavascriptParserOptions} JavascriptParserOptions */ /** @typedef {import("../Compilation")} Compilation */ /** @typedef {import("../Compiler")} Compiler */ /** @typedef {import("../Dependency")} Dependency */ /** @typedef {import("../Dependency").DependencyLocation} DependencyLocation */ /** @typedef {import("../Module")} Module */ /** @typedef {import("../ModuleGraph")} ModuleGraph */ /** @typedef {import("../javascript/JavascriptParser")} JavascriptParser */ /** @typedef {import("../javascript/JavascriptParser").Range} Range */ /** @typedef {import("./InnerGraph").TopLevelSymbol} TopLevelSymbol */ const PLUGIN_NAME = "InnerGraphPlugin"; const impureVariableDeclarationKinds = new Set(["using", "await using"]); class InnerGraphPlugin { /** * Applies the plugin by registering its hooks on the compiler. * @param {Compiler} compiler the compiler instance * @returns {void} */ apply(compiler) { compiler.hooks.compilation.tap( PLUGIN_NAME, (compilation, { normalModuleFactory }) => { const logger = compilation.getLogger("webpack.InnerGraphPlugin"); const innerGraph = getInnerGraphUtils(compilation); compilation.dependencyTemplates.set( PureExpressionDependency, new PureExpressionDependency.Template() ); /** * Adds pure dependency added after parsing, with parents set so it survives persistent caching. * @param {Module} module module * @param {Dependency} dep pure dependency * @returns {void} */ const addPureDependency = (module, dep) => { compilation.moduleGraph.setParents(dep, module, module, -1); module.addDependency(dep); }; /** * Handles the hook callback for this code path. * @param {JavascriptParser} parser the parser * @param {JavascriptParserOptions} parserOptions options * @returns {void} */ const handler = (parser, parserOptions) => { /** * Processes the provided sup. * @param {Expression} sup sup */ const onUsageSuper = (sup) => { // the callback fires after parsing, when the parser can no // longer map offsets — take the location now const loc = parser.getLocation(sup); innerGraph.onUsage(parser.state, (usedByExports, module) => { switch (usedByExports) { case undefined: case true: return; default: { const dep = new PureExpressionDependency( /** @type {Range} */ (sup.range) ); dep.loc = loc; dep.usedByExports = usedByExports; addPureDependency(module, dep); break; } } }); }; parser.hooks.program.tap(PLUGIN_NAME, () => { innerGraph.enable(parser.state); statementWithTopLevelSymbol = new WeakMap(); statementPurePart = new WeakMap(); classWithTopLevelSymbol = new WeakMap(); declWithTopLevelSymbol = new WeakMap(); pureDeclarators = new WeakSet(); pureConditionByCallExpr = new WeakMap(); }); // During prewalking the following datastructures are filled with // nodes that have a TopLevelSymbol assigned and // variables are tagged with the assigned TopLevelSymbol // We differ 3 types of nodes: // 1. full statements (export default, function declaration) // 2. classes (class declaration, class expression) // 3. variable declarators (const x = ...) /** @type {WeakMap<Node | MaybeNamedFunctionDeclaration | MaybeNamedClassDeclaration, TopLevelSymbol>} */ let statementWithTopLevelSymbol = new WeakMap(); /** @type {WeakMap<Node | MaybeNamedFunctionDeclaration | MaybeNamedClassDeclaration, Node>} */ let statementPurePart = new WeakMap(); /** @type {WeakMap<ClassExpression | ClassDeclaration | MaybeNamedClassDeclaration, TopLevelSymbol>} */ let classWithTopLevelSymbol = new WeakMap(); /** @type {WeakMap<VariableDeclarator, TopLevelSymbol>} */ let declWithTopLevelSymbol = new WeakMap(); /** @type {WeakSet<VariableDeclarator>} */ let pureDeclarators = new WeakSet(); /** @type {WeakMap<CallExpression, (compilation: Compilation, module: Module) => boolean>} */ let pureConditionByCallExpr = new WeakMap(); parser.hooks.isPure.for("CallExpression").tap( { name: PLUGIN_NAME, stage: -10 }, (expression) => { const expr = /** @type {CallExpression} */ (expression); const callee = expr.callee; /** @type {Node} */ let root; /** @type {string[]} */ let chainMembers; if (callee.type === "Identifier") { root = callee; chainMembers = []; } else if (callee.type === "MemberExpression") { const chain = parser.extractMemberExpressionChain(callee); // optional chaining short-circuits and breaks straight purity if (chain.membersOptionals.some(Boolean)) return; root = /** @type {Node} */ (chain.object); // extractMemberExpressionChain returns members in reverse chainMembers = [...chain.members].reverse(); } else { return; } if (root.type !== "Identifier") return; const harmonySettings = /** @type {{ source: string, ids: string[] } | undefined} */ ( parser.getTagData(root.name, harmonySpecifierTag) ); if (!harmonySettings) return; const ids = [...harmonySettings.ids, ...chainMembers]; if (ids.length === 0) return; let pos = /** @type {Range} */ (callee.range)[1]; for (const arg of expr.arguments) { if (arg.type === "SpreadElement") return; if (!parser.isPure(arg, pos)) return; pos = /** @type {Range} */ (arg.range)[1]; } const source = harmonySettings.source; pureConditionByCallExpr.set(expr, (compilation, module) => { const moduleGraph = compilation.moduleGraph; for (const dep of module.dependencies) { if ( dep instanceof HarmonyImportSideEffectDependency && dep.request === source ) { const m = moduleGraph.getModule(dep); if (!m) return false; const exportInfo = moduleGraph .getExportsInfo(m) .getReadOnlyExportInfoRecursive(ids); if (!exportInfo) return false; const target = exportInfo.getTarget(moduleGraph); const final = target && target.export ? moduleGraph .getExportsInfo(target.module) .getReadOnlyExportInfoRecursive(target.export) : exportInfo; return final !== undefined && final.pureProvide === true; } } return false; }); } ); // The following hooks are used during prewalking: parser.hooks.preStatementByType .for("FunctionDeclaration") .tap(PLUGIN_NAME, (statement) => { if (!innerGraph.isEnabled(parser.state)) return; if ( parser.scope.topLevelScope === true && statement.type === "FunctionDeclaration" ) { const name = statement.id ? statement.id.name : "*default*"; const symbol = /** @type {TopLevelSymbol} */ ( innerGraph.tagTopLevelSymbol( parser, name, parser.isPure( statement, /** @type {Range} */ (statement.range)[0] ) ) ); statementWithTopLevelSymbol.set(statement, symbol); return true; } }); /** * @param {import("estree").Statement | import("estree").ModuleDeclaration | import("estree").MaybeNamedFunctionDeclaration | import("estree").MaybeNamedClassDeclaration} statement statement * @returns {boolean | void} true when handled */ const blockPreStatementHandler = (statement) => { if (!innerGraph.isEnabled(parser.state)) return; if (parser.scope.topLevelScope === true) { if (statement.type === "ClassDeclaration") { const name = statement.id ? statement.id.name : "*default*"; const pure = parser.isPure( statement, /** @type {Range} */ (statement.range)[0] ); const symbol = /** @type {TopLevelSymbol} */ ( innerGraph.tagTopLevelSymbol(parser, name, pure) ); classWithTopLevelSymbol.set(statement, symbol); return true; } if (statement.type === "ExportDefaultDeclaration") { const name = "*default*"; const decl = statement.declaration; /** @type {boolean | ((compilation: Compilation, module: Module) => boolean)} */ let pure = parser.isPure( decl, /** @type {Range} */ (statement.range)[0] ); if (!pure && decl.type === "CallExpression") { const deferred = pureConditionByCallExpr.get(decl); if (deferred) pure = deferred; } const symbol = /** @type {TopLevelSymbol} */ (innerGraph.tagTopLevelSymbol(parser, name, pure)); if ( decl.type === "ClassExpression" || decl.type === "ClassDeclaration" ) { classWithTopLevelSymbol.set( /** @type {ClassExpression | ClassDeclaration} */ (decl), symbol ); } else { statementWithTopLevelSymbol.set(statement, symbol); if ( pure && // body deferred to call-time, no eager read !decl.type.endsWith("FunctionExpression") && // only FunctionDeclaration here (classes routed above), body deferred !decl.type.endsWith("Declaration") && // literal references nothing decl.type !== "Literal" ) { statementPurePart.set( statement, /** @type {Expression} */ (decl) ); } } } } }; parser.hooks.blockPreStatementByType .for("ClassDeclaration") .tap(PLUGIN_NAME, blockPreStatementHandler); parser.hooks.blockPreStatementByType .for("ExportDefaultDeclaration") .tap(PLUGIN_NAME, blockPreStatementHandler); parser.hooks.preDeclarator.tap(PLUGIN_NAME, (decl, statement) => { if (!innerGraph.isEnabled(parser.state)) return; if (impureVariableDeclarationKinds.has(statement.kind)) return; if ( parser.scope.topLevelScope === true && decl.init && decl.id.type === "Identifier" ) { const name = decl.id.name; // Skip webpack runtime variables handled by CompatibilityPlugin if ( name === RuntimeGlobals.require || name === RuntimeGlobals.exports ) { return; } /** @type {boolean | ((compilation: Compilation, module: Module) => boolean)} */ let pure = parser.isPure( decl.init, /** @type {Range} */ (decl.id.range)[1] ); if (!pure && decl.init.type === "CallExpression") { const deferred = pureConditionByCallExpr.get(decl.init); if (deferred) pure = deferred; } if (decl.init.type === "ClassExpression") { const symbol = /** @type {TopLevelSymbol} */ (innerGraph.tagTopLevelSymbol(parser, name, pure)); classWithTopLevelSymbol.set(decl.init, symbol); } else { const symbol = /** @type {TopLevelSymbol} */ (innerGraph.tagTopLevelSymbol(parser, name, pure)); declWithTopLevelSymbol.set(decl, symbol); if ( pure && !decl.init.type.endsWith("FunctionExpression") && decl.init.type !== "Literal" ) { pureDeclarators.add(decl); } } } }); // During real walking we set the TopLevelSymbol state to the assigned // TopLevelSymbol by using the fill datastructures. // In addition to tracking TopLevelSymbols, we sometimes need to // add a PureExpressionDependency. This is needed to skip execution // of pure expressions, even when they are not dropped due to // minimizing. Otherwise symbols used there might not exist anymore // as they are removed as unused by this optimization // When we find a reference to a TopLevelSymbol, we register a // TopLevelSymbol dependency from TopLevelSymbol in state to the // referenced TopLevelSymbol. This way we get a graph of all // TopLevelSymbols. // The following hooks are called during walking: parser.hooks.statement.tap(PLUGIN_NAME, (statement) => { if (!innerGraph.isEnabled(parser.state)) return; if (parser.scope.topLevelScope === true) { innerGraph.setTopLevelSymbol(parser.state, undefined); const symbol = statementWithTopLevelSymbol.get(statement); if (symbol) { innerGraph.setTopLevelSymbol(parser.state, symbol); const purePart = statementPurePart.get(statement); if (purePart) { // the callback fires after parsing, when the parser can no // longer map offsets — take the location now const loc = parser.getLocation(statement); innerGraph.onUsage(parser.state, (usedByExports, module) => { switch (usedByExports) { case undefined: case true: return; default: { const dep = new PureExpressionDependency( /** @type {Range} */ (purePart.range) ); dep.loc = loc; dep.usedByExports = usedByExports; addPureDependency(module, dep); break; } } }); } } } }); parser.hooks.classExtendsExpression.tap( PLUGIN_NAME, (expr, statement) => { if (!innerGraph.isEnabled(parser.state)) return; if (parser.scope.topLevelScope === true) { const symbol = classWithTopLevelSymbol.get(statement); if ( symbol && parser.isPure( expr, statement.id ? /** @type {Range} */ (statement.id.range)[1] : /** @type {Range} */ (statement.range)[0] ) ) { innerGraph.setTopLevelSymbol(parser.state, symbol); onUsageSuper(expr); } } } ); parser.hooks.classBodyElement.tap( PLUGIN_NAME, (element, classDefinition) => { if (!innerGraph.isEnabled(parser.state)) return; if (parser.scope.topLevelScope === true) { const symbol = classWithTopLevelSymbol.get(classDefinition); if (symbol) { innerGraph.setTopLevelSymbol(parser.state, undefined); } } } ); parser.hooks.classBodyValue.tap( PLUGIN_NAME, (expression, element, classDefinition) => { if (!innerGraph.isEnabled(parser.state)) return; if (parser.scope.topLevelScope === true) { const symbol = classWithTopLevelSymbol.get(classDefinition); if (symbol) { if ( !element.static || parser.isPure( expression, element.key ? /** @type {Range} */ (element.key.range)[1] : /** @type {Range} */ (element.range)[0] ) ) { innerGraph.setTopLevelSymbol(parser.state, symbol); if (element.type !== "MethodDefinition" && element.static) { // the callback fires after parsing, when the parser can // no longer map offsets — take the location now const loc = parser.getLocation(expression); innerGraph.onUsage( parser.state, (usedByExports, module) => { switch (usedByExports) { case undefined: case true: return; default: { const dep = new PureExpressionDependency( /** @type {Range} */ (expression.range) ); dep.loc = loc; dep.usedByExports = usedByExports; addPureDependency(module, dep); break; } } } ); } } else { innerGraph.setTopLevelSymbol(parser.state, undefined); } } } } ); parser.hooks.declarator.tap(PLUGIN_NAME, (decl, _statement) => { if (!innerGraph.isEnabled(parser.state)) return; const symbol = declWithTopLevelSymbol.get(decl); if (symbol) { innerGraph.setTopLevelSymbol(parser.state, symbol); if (pureDeclarators.has(decl)) { if ( /** @type {ClassExpression} */ (decl.init).type === "ClassExpression" ) { if (decl.init.superClass) { onUsageSuper(decl.init.superClass); } } else { // the callback fires after parsing, when the parser can no // longer map offsets — take the location now const loc = parser.getLocation(decl); innerGraph.onUsage(parser.state, (usedByExports, module) => { switch (usedByExports) { case undefined: case true: return; default: { const dep = new PureExpressionDependency( /** @type {Range} */ ( /** @type {ClassExpression} */ (decl.init).range ) ); dep.loc = loc; dep.usedByExports = usedByExports; addPureDependency(module, dep); break; } } }); } } parser.walkExpression( /** @type {NonNullable<VariableDeclarator["init"]>} */ ( decl.init ) ); innerGraph.setTopLevelSymbol(parser.state, undefined); return true; } else if ( decl.id.type === "Identifier" && decl.init && decl.init.type === "ClassExpression" && classWithTopLevelSymbol.has(decl.init) ) { parser.walkExpression(decl.init); innerGraph.setTopLevelSymbol(parser.state, undefined); return true; } }); parser.hooks.expression .for(topLevelSymbolTag) .tap(PLUGIN_NAME, () => { const topLevelSymbol = /** @type {TopLevelSymbol} */ ( parser.currentTagData ); const currentTopLevelSymbol = innerGraph.getTopLevelSymbol( parser.state ); innerGraph.addUsage( parser.state, topLevelSymbol, currentTopLevelSymbol || true ); }); parser.hooks.assign .for(topLevelSymbolTag) .tap(PLUGIN_NAME, (expr) => { if (!innerGraph.isEnabled(parser.state)) return; if (expr.operator === "=") return true; }); }; normalModuleFactory.hooks.parser .for(JAVASCRIPT_MODULE_TYPE_AUTO) .tap(PLUGIN_NAME, handler); normalModuleFactory.hooks.parser .for(JAVASCRIPT_MODULE_TYPE_ESM) .tap(PLUGIN_NAME, handler); compilation.hooks.finishModules.tap(PLUGIN_NAME, (modules) => { logger.time("infer dependency usage"); for (const module of modules) { innerGraph.inferDependencyUsage(module); // state is dead after inference; release it to not retain ASTs via callbacks innerGraph.release(module); } logger.timeEnd("infer dependency usage"); }); } ); } } module.exports = InnerGraphPlugin;