/
githubmirror
/
webpack
Обзор
Документация
Войти
/
githubmirror
/
webpack
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
lib/optimize/ConcatenatedModule.js
3 306 строк
107 KB
Alexander Akait
perf: load webpack's own modules only when a build needs them (#21652)
10 авг 2026, 07:32
Не верифицирован
10 авг 2026, 07:32
8e28763
Код
Авторство
О чём код?
/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; const eslintScope = require("eslint-scope"); const Referencer = require("eslint-scope/lib/referencer"); const { SyncBailHook } = require("tapable"); const { CachedSource, ConcatSource, ReplaceSource } = require("webpack-sources"); const ConcatenationScope = require("../ConcatenationScope"); const Dependency = require("../Dependency"); const { UsageState } = require("../ExportsInfo"); const ExternalModule = require("../ExternalModule"); const Module = require("../Module"); const { JAVASCRIPT_TYPE, JAVASCRIPT_TYPES } = require("../ModuleSourceTypeConstants"); const { JAVASCRIPT_MODULE_TYPE_ESM } = require("../ModuleTypeConstants"); const NormalModule = require("../NormalModule"); const RuntimeGlobals = require("../RuntimeGlobals"); const Template = require("../Template"); const { DEFAULTS } = require("../config/defaults"); const CommonJsExportRequireDependency = require("../dependencies/CommonJsExportRequireDependency"); const CommonJsFullRequireDependency = require("../dependencies/CommonJsFullRequireDependency"); const CommonJsRequireDependency = require("../dependencies/CommonJsRequireDependency"); const { ImportPhaseUtils } = require("../dependencies/ImportPhase"); const JavascriptParser = require("../javascript/JavascriptParser"); const { getDeferredCycleModuleIds, getDeferredCycleModules, getMakeDeferredNamespaceModeFromExportsType, getOptimizedDeferredModule } = require("../runtime/MakeDeferredNamespaceObjectRuntime"); const { equals } = require("../util/ArrayHelpers"); const LazySet = require("../util/LazySet"); const { compareModulesByFullName, concatComparators } = require("../util/comparators"); const { RESERVED_NAMES, addScopeSymbols, findNewName, getAllReferences, getPathInAst, getUsedNamesInScopeInfo, isCommonJsConcatenationEnabled } = require("../util/concatenate"); const createHash = require("../util/createHash"); const createHooksRegistry = require("../util/createHooksRegistry"); const { makePathsRelative } = require("../util/identifier"); const makeSerializable = require("../util/makeSerializable"); const { propertyAccess, propertyName } = require("../util/property"); const { filterRuntime, intersectRuntime, mergeRuntimeCondition, mergeRuntimeConditionNonFalse, runtimeConditionToString, subtractRuntimeCondition } = require("../util/runtime"); const { InlinedUsedName } = require("./InlineExports"); /** @typedef {import("webpack-sources").Source} Source */ /** @typedef {import("../config/defaults").WebpackOptionsNormalizedWithDefaults} WebpackOptions */ /** @typedef {import("../ChunkGraph")} ChunkGraph */ /** @typedef {import("../CodeGenerationResults")} CodeGenerationResults */ /** @typedef {import("../Compilation")} Compilation */ /** @typedef {import("../Dependency").UpdateHashContext} UpdateHashContext */ /** @typedef {import("../dependencies/ModuleDependency")} ModuleDependency */ /** @typedef {import("../dependencies/HarmonyImportDependency")} HarmonyImportDependency */ /** @typedef {import("../DependencyTemplates")} DependencyTemplates */ /** @typedef {import("../ExportsInfo").ExportInfo} ExportInfo */ /** @typedef {import("../Module").BuildCallback} BuildCallback */ /** @typedef {import("../Module").BuildInfo} BuildInfo */ /** @typedef {import("../Module").FileSystemDependencies} FileSystemDependencies */ /** @typedef {import("../Module").BuildMeta} BuildMeta */ /** @typedef {import("../Module").ExportsType} ExportsType */ /** @typedef {import("../Module").CodeGenerationContext} CodeGenerationContext */ /** @typedef {import("../Module").CodeGenerationResultData} CodeGenerationResultData */ /** @typedef {import("../Module").CodeGenerationResult} CodeGenerationResult */ /** @typedef {import("../Module").LibIdentOptions} LibIdentOptions */ /** @typedef {import("../Module").LibIdent} LibIdent */ /** @typedef {import("../Module").NameForCondition} NameForCondition */ /** @typedef {import("../Module").ReadOnlyRuntimeRequirements} ReadOnlyRuntimeRequirements */ /** @typedef {import("../Module").RuntimeRequirements} RuntimeRequirements */ /** @typedef {import("../Module").SourceTypes} SourceTypes */ /** @typedef {import("../ModuleGraph")} ModuleGraph */ /** @typedef {import("../ModuleGraphConnection")} ModuleGraphConnection */ /** @typedef {import("../ModuleGraphConnection").ConnectionState} ConnectionState */ /** @typedef {import("../RequestShortener")} RequestShortener */ /** @typedef {import("../ResolverFactory").ResolverWithOptions} ResolverWithOptions */ /** @typedef {import("../RuntimeTemplate")} RuntimeTemplate */ /** @typedef {import("../javascript/JavascriptModule").JavascriptModuleBuildInfo} JavascriptModuleBuildInfo */ /** @typedef {import("../javascript/JavascriptModule").JavascriptModuleBuildMeta} JavascriptModuleBuildMeta */ /** @typedef {import("../javascript/JavascriptModulesPlugin").ChunkRenderContext} ChunkRenderContext */ /** @typedef {import("../javascript/JavascriptModulesPlugin").Scope} Scope */ /** @typedef {import("../javascript/JavascriptModulesPlugin").Reference} Reference */ /** @typedef {import("../javascript/JavascriptModulesPlugin").Variable} Variable */ /** @typedef {import("../javascript/JavascriptParser").Program} Program */ /** @typedef {import("../javascript/JavascriptParser").Range} Range */ /** @typedef {import("estree").Identifier} Identifier */ /** @typedef {import("../serialization/ObjectMiddleware").ObjectDeserializerContext} ObjectDeserializerContext */ /** @typedef {import("../util/Hash")} Hash */ /** @typedef {import("../util/Hash").HashFunction} HashFunction */ /** @typedef {import("../util/concatenate").UsedNames} UsedNames */ /** @typedef {import("../util/concatenate").UsedNamesInScopeInfo} UsedNamesInScopeInfo */ /** @typedef {import("../util/fs").InputFileSystem} InputFileSystem */ /** @typedef {import("../util/identifier").AssociatedObjectForCache} AssociatedObjectForCache */ /** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */ /** * @template T * @typedef {import("../InitFragment")<T>} InitFragment */ /** * @template T * @typedef {import("../util/comparators").Comparator<T>} Comparator */ const IDENTIFIER_SCAN_REGEXP = /[$A-Za-z_][\w$]*/g; const IDENTIFIER_SCAN_WHITESPACE = new Set([" ", "\t", "\n", "\r"]); /** * Finds every identifier a generated source could resolve against the enclosing * concatenation scope. Property names are skipped; string and comment contents * are not, which only over-reserves. * @param {Source} source generated source * @returns {ReadonlySet<string>} identifier names */ const findIdentifiers = (source) => { const code = source.source().toString(); /** @type {Set<string>} */ const result = new Set(); IDENTIFIER_SCAN_REGEXP.lastIndex = 0; let match = IDENTIFIER_SCAN_REGEXP.exec(code); while (match !== null) { let i = match.index - 1; while (i >= 0 && IDENTIFIER_SCAN_WHITESPACE.has(code[i])) i--; // `a.b` can't bind to the outer scope, but the `b` of `...b` can if (!(i >= 0 && code[i] === "." && code[i - 1] !== ".")) { result.add(match[0]); } match = IDENTIFIER_SCAN_REGEXP.exec(code); } return result; }; // fix eslint-scope to support class properties correctly // cspell:word Referencer const ReferencerClass = Referencer; if (!ReferencerClass.prototype.PropertyDefinition) { ReferencerClass.prototype.PropertyDefinition = ReferencerClass.prototype.Property; } /** @typedef {RawBinding | SymbolBinding} Binding */ /** @typedef {string[]} ExportName */ /** * @typedef {object} RawBinding * @property {ModuleInfo} info * @property {string} rawName * @property {string=} comment * @property {ExportName} ids * @property {ExportName} exportName */ /** * @typedef {object} SymbolBinding * @property {ConcatenatedModuleInfo} info * @property {string} name * @property {string=} comment * @property {ExportName} ids * @property {ExportName} exportName */ /** @typedef {ConcatenatedModuleInfo | ExternalModuleInfo} ModuleInfo */ /** @typedef {ConcatenatedModuleInfo | ExternalModuleInfo | ReferenceToModuleInfo} ModuleInfoOrReference */ /** @typedef {Map<string, string>} ExportMap */ /** * @typedef {object} ConcatenatedModuleInfo * @property {"concatenated"} type * @property {boolean} wrapped the body renders inside the lazy CJS wrapper with real module/exports instead of being scope-hoisted into the shared scope * @property {boolean} eager a wrapped body reached by an eager import edge, so its accessor is called at its own slot in evaluation order * @property {Module} module * @property {number} index * @property {Program | undefined} ast * @property {Source | undefined} internalSource * @property {ReplaceSource | undefined} source * @property {InitFragment<ChunkRenderContext>[]=} chunkInitFragments * @property {ReadOnlyRuntimeRequirements | undefined} runtimeRequirements * @property {Scope | undefined} globalScope * @property {Scope | undefined} moduleScope * @property {Map<string, string>} internalNames * @property {ExportMap | undefined} exportMap * @property {ExportMap | undefined} rawExportMap * @property {string=} namespaceExportSymbol * @property {string | undefined} namespaceFnName name of the lazy wrapper accessor function (calling it evaluates the module and returns its exports) * @property {string | undefined} namespaceObjectName * @property {string | undefined} escapeNamespaceObjectName decoupled namespace object that keeps original export names when the exports are mangled * @property {ConcatenationScope | undefined} concatenationScope * @property {boolean} interopNamespaceObjectUsed "default-with-named" namespace * @property {string | undefined} interopNamespaceObjectName "default-with-named" namespace * @property {boolean} interopNamespaceObject2Used "default-only" namespace * @property {string | undefined} interopNamespaceObject2Name "default-only" namespace * @property {boolean} interopDefaultAccessUsed runtime namespace object that detects "__esModule" * @property {string | undefined} interopDefaultAccessName runtime namespace object that detects "__esModule" * @property {ExportsType=} exportsTypeStrict memoized getExportsType(strict=true) * @property {ExportsType=} exportsTypeNonStrict memoized getExportsType(strict=false) */ /** * @typedef {object} ExternalModuleInfo * @property {"external"} type * @property {boolean} wrapped the module's `require()` call is deferred behind an accessor instead of running at its own slot; unlike a wrapped concatenated member there is no body and no CJS wrapper, so any external may be wrapped * @property {boolean} eager a wrapped module reached by an eager import edge, so its accessor is called at its own slot in evaluation order * @property {Module} module * @property {RuntimeSpec | boolean} runtimeCondition * @property {NonDeferAccess} nonDeferAccess * @property {number} index * @property {string | undefined} name module.exports / harmony namespace object * @property {string | undefined} namespaceFnName lazy module accessor * @property {string | undefined} escapeNamespaceObjectName decoupled namespace object that keeps original export names when the exports are mangled * @property {string | undefined} deferredName deferred module.exports / harmony namespace object * @property {boolean} deferred the module is deferred at least once * @property {boolean} deferredNamespaceObjectUsed deferred namespace object that being used in a not-analyzable way so it must be materialized * @property {string | undefined} deferredNamespaceObjectName deferred namespace object that being used in a not-analyzable way so it must be materialized * @property {boolean} interopNamespaceObjectUsed "default-with-named" namespace * @property {string | undefined} interopNamespaceObjectName "default-with-named" namespace * @property {boolean} interopNamespaceObject2Used "default-only" namespace * @property {string | undefined} interopNamespaceObject2Name "default-only" namespace * @property {boolean} interopDefaultAccessUsed runtime namespace object that detects "__esModule" * @property {string | undefined} interopDefaultAccessName runtime namespace object that detects "__esModule" * @property {ExportsType=} exportsTypeStrict memoized getExportsType(strict=true) * @property {ExportsType=} exportsTypeNonStrict memoized getExportsType(strict=false) */ /** * @typedef {object} ReferenceToModuleInfo * @property {"reference"} type * @property {RuntimeSpec | boolean} runtimeCondition * @property {NonDeferAccess} nonDeferAccess * @property {ModuleInfo} target */ /** * @template T * @param {string} property property * @param {(a: T[keyof T], b: T[keyof T]) => 0 | 1 | -1} comparator comparator * @returns {Comparator<T>} comparator */ const createComparator = (property, comparator) => (a, b) => comparator( a[/** @type {keyof T} */ (property)], b[/** @type {keyof T} */ (property)] ); /** * @param {number} a a * @param {number} b b * @returns {0 | 1 | -1} result */ const compareNumbers = (a, b) => { if (Number.isNaN(a)) { if (!Number.isNaN(b)) { return 1; } } else { if (Number.isNaN(b)) { return -1; } if (a !== b) { return a < b ? -1 : 1; } } return 0; }; const bySourceOrder = createComparator("sourceOrder", compareNumbers); const byRangeStart = createComparator("rangeStart", compareNumbers); /** * @param {Iterable<string>} iterable iterable object * @returns {string} joined iterable object */ const joinIterableWithComma = (iterable) => { // This is more performant than Array.from().join(", ") // as it doesn't create an array let str = ""; let first = true; for (const item of iterable) { if (first) { first = false; } else { str += ", "; } str += item; } return str; }; /** @typedef {boolean} NonDeferAccess */ /** * @param {NonDeferAccess} a a * @param {NonDeferAccess} b b * @returns {NonDeferAccess} merged */ const mergeNonDeferAccess = (a, b) => a || b; /** * @param {NonDeferAccess} a first * @param {NonDeferAccess} b second * @returns {NonDeferAccess} first - second */ const subtractNonDeferAccess = (a, b) => a && !b; /** * @typedef {object} ConcatenationEntry * @property {"concatenated" | "external"} type * @property {boolean} wrapped evaluation is deferred behind an accessor: a "concatenated" entry renders its body inside the lazy CJS wrapper instead of being scope-hoisted, an "external" entry only defers its `require()` call * @property {Module} module * @property {RuntimeSpec | boolean} runtimeCondition * @property {NonDeferAccess} nonDeferAccess */ /** * Whether a member runs inside the lazy CJS wrapper or is scope-hoisted. Only a * NormalModule can carry a CJS body; an ESM member starts out hoisted and is * downgraded by {@link ConcatenatedModule._createConcatenationList}. * @param {Module} module a module taking part in the concatenation * @returns {boolean} true, when the module renders wrapped */ const needWrapped = (module) => module instanceof NormalModule && module.type.startsWith("javascript/") && module.buildMeta !== undefined && module.buildMeta.exportsType !== "namespace"; /** * Whether a hoisted member can move into the lazy wrapper instead. * @param {ConcatenationEntry} entry a member of the concatenation * @returns {boolean} true, when it may be wrapped */ const entryCanBeWrapped = (entry) => (entry.module instanceof NormalModule && !entry.module.type.startsWith("css/")) || (entry.module instanceof ExternalModule && entry.module.canBeWrappedInConcatenation()); /** @typedef {Set<ConcatenatedModuleInfo>} NeededNamespaceObjects */ /** @typedef {Map<Module, ModuleInfo>} ModuleToInfoMap */ /** * getExportsType memoized on the info, which lives for one codeGeneration. * The "dynamic" case walks the module graph, and is queried once per reference. * @param {ModuleGraph} moduleGraph the module graph * @param {ModuleInfo} info module info * @param {boolean | undefined} strict strict harmony module (undefined is treated as non-strict) * @returns {ExportsType} the exports type */ const getExportsType = (moduleGraph, info, strict) => { if (strict) { if (info.exportsTypeStrict === undefined) { info.exportsTypeStrict = info.module.getExportsType(moduleGraph, true); } return info.exportsTypeStrict; } if (info.exportsTypeNonStrict === undefined) { info.exportsTypeNonStrict = info.module.getExportsType(moduleGraph, false); } return info.exportsTypeNonStrict; }; /** * @param {ExternalModuleInfo} info module info * @returns {string} module exports expression */ const getExternalModuleExpr = (info) => info.wrapped ? `${ /** @type {NonNullable<ExternalModuleInfo["namespaceFnName"]>} */ (info.namespaceFnName) }()` : /** @type {NonNullable<ExternalModuleInfo["name"]>} */ (info.name); /** * A wrapped module's interop object is declared as a lazy accessor, so reading * it is a call. See `declareInterop`. * @param {ModuleInfo} info module info * @param {string | undefined} interopName interop variable name * @returns {string} interop object expression */ const getInteropExpr = (info, interopName) => info.wrapped ? `${interopName}()` : /** @type {string} */ (interopName); /** * Expression for an export that is not used. A hoisted body runs at its own * slot, but a wrapped one only runs when its accessor is called, so dropping * the reference would drop the module's side effects with it. * @param {ConcatenatedModuleInfo} info module info * @returns {string} expression evaluating to undefined */ const getUnusedExportExpr = (info) => info.wrapped ? `/* unused export */ (${info.namespaceObjectName}, undefined)` : "/* unused export */ undefined"; /** * @param {ModuleGraph} moduleGraph the module graph * @param {ModuleInfo} info module info * @param {ExportName} exportName exportName * @param {ModuleToInfoMap} moduleToInfoMap moduleToInfoMap * @param {RuntimeSpec} runtime for which runtime * @param {RequestShortener} requestShortener the request shortener * @param {RuntimeTemplate} runtimeTemplate the runtime template * @param {NeededNamespaceObjects} neededNamespaceObjects modules for which a namespace object should be generated * @param {boolean} asCall asCall * @param {boolean} depDeferred the dependency is deferred * @param {boolean | undefined} strictHarmonyModule strictHarmonyModule * @param {boolean | undefined} asiSafe asiSafe * @param {boolean=} moduleExportsAccess resolve to the module's own exports value with plain property access, skipping ESM interop * @param {Set<ExportInfo>=} alreadyVisited alreadyVisited * @returns {Binding} the final variable */ const getFinalBinding = ( moduleGraph, info, exportName, moduleToInfoMap, runtime, requestShortener, runtimeTemplate, neededNamespaceObjects, asCall, depDeferred, strictHarmonyModule, asiSafe, moduleExportsAccess, alreadyVisited ) => { const exportsType = getExportsType(moduleGraph, info, strictHarmonyModule); const moduleDeferred = info.type === "external" && info.deferred && !(/** @type {BuildMeta} */ (info.module.buildMeta).async); const deferred = depDeferred && moduleDeferred; if (moduleExportsAccess && info.type === "concatenated") { neededNamespaceObjects.add(info); // ids arrive as declared names and the rendered exports object is keyed by // used ones, so resolve here — the same way `case "external"` does below. // Resolving in the caller instead would mangle twice. const usedName = exportName.length === 0 ? exportName : moduleGraph .getExportsInfo(info.module) .getUsedName(exportName, runtime); if (!usedName) { return { info, rawName: getUnusedExportExpr(info), ids: [], exportName }; } return { info, rawName: /** @type {NonNullable<ConcatenatedModuleInfo["namespaceObjectName"]>} */ (info.namespaceObjectName), ids: /** @type {ExportName} */ (usedName), exportName }; } if (exportName.length === 0) { switch (exportsType) { case "default-only": if (deferred) info.deferredNamespaceObjectUsed = true; else info.interopNamespaceObject2Used = true; return { info, rawName: deferred ? /** @type {string} */ (info.deferredNamespaceObjectName) : getInteropExpr(info, info.interopNamespaceObject2Name), ids: exportName, exportName }; case "default-with-named": if (deferred) info.deferredNamespaceObjectUsed = true; else info.interopNamespaceObjectUsed = true; return { info, rawName: deferred ? /** @type {string} */ (info.deferredNamespaceObjectName) : getInteropExpr(info, info.interopNamespaceObjectName), ids: exportName, exportName }; case "namespace": case "dynamic": break; default: throw new Error(`Unexpected exportsType ${exportsType}`); } } else { switch (exportsType) { case "namespace": break; case "default-with-named": switch (exportName[0]) { case "default": exportName = exportName.slice(1); if (deferred) { // `ns.default` for a deferred default-with-named external // module must read through the optimized `.a` getter // (which lazily evaluates the module and returns its // exports), not the proxy namespace itself — otherwise // `typeof ns.default` / `ns.default instanceof X` // observe the proxy instead of the actual default. return { info, rawName: `${info.deferredName}.a`, ids: exportName, exportName }; } break; case "__esModule": return { info, rawName: "/* __esModule */true", ids: exportName.slice(1), exportName }; } break; case "default-only": { const exportId = exportName[0]; if (exportId === "__esModule") { return { info, rawName: "/* __esModule */true", ids: exportName.slice(1), exportName }; } exportName = exportName.slice(1); if (exportId !== "default") { return { info, rawName: "/* non-default import from default-exporting module */undefined", ids: exportName, exportName }; } if (deferred) { // As with default-with-named above, `ns.default` for a // deferred default-only external must read through the // optimized `.a` getter so that `typeof` / `instanceof` // observe the actual default value rather than the proxy. return { info, rawName: `${info.deferredName}.a`, ids: exportName, exportName }; } break; } case "dynamic": switch (exportName[0]) { case "default": { exportName = exportName.slice(1); if (deferred) { return { info, rawName: `${info.deferredName}.a`, ids: exportName, exportName }; } if (moduleDeferred) { return { info, rawName: getExternalModuleExpr(info), ids: exportName, exportName }; } info.interopDefaultAccessUsed = true; const accessor = getInteropExpr( info, info.interopDefaultAccessName ); const defaultExport = asCall ? `${accessor}()` : asiSafe ? `(${accessor}())` : asiSafe === false ? `;(${accessor}())` : `${accessor}.a`; return { info, rawName: defaultExport, ids: exportName, exportName }; } case "__esModule": return { info, rawName: "/* __esModule */true", ids: exportName.slice(1), exportName }; } break; default: throw new Error(`Unexpected exportsType ${exportsType}`); } } if (exportName.length === 0) { switch (info.type) { case "concatenated": neededNamespaceObjects.add(info); return { info, rawName: /** @type {NonNullable<ConcatenatedModuleInfo["namespaceObjectName"]>} */ (info.namespaceObjectName), ids: exportName, exportName }; case "external": if (deferred) { info.deferredNamespaceObjectUsed = true; return { info, rawName: /** @type {string} */ (info.deferredNamespaceObjectName), ids: exportName, exportName }; } return { info, rawName: getExternalModuleExpr(info), ids: exportName, exportName }; } } const exportsInfo = moduleGraph.getExportsInfo(info.module); const exportInfo = exportsInfo.getExportInfo(exportName[0]); // Lazily allocate: only the reexport-following recursion below needs it, // most calls return before reaching this point. if (alreadyVisited === undefined) alreadyVisited = new Set(); if (alreadyVisited.has(exportInfo)) { return { info, rawName: "/* circular reexport */ Object(function x() { x() }())", ids: [], exportName }; } alreadyVisited.add(exportInfo); switch (info.type) { case "concatenated": { const exportId = exportName[0]; if (exportInfo.provided === false) { // It's not provided, but it could be on the prototype neededNamespaceObjects.add(info); return { info, rawName: /** @type {string} */ (info.namespaceObjectName), ids: exportName, exportName }; } const directExport = info.exportMap && info.exportMap.get(exportId); if (directExport) { const usedName = exportsInfo.getUsedName(exportName, runtime); if (!usedName) { return { info, rawName: "/* unused export */ undefined", ids: exportName.slice(1), exportName }; } if (usedName instanceof InlinedUsedName) { return { info, // Render the inlined literal only (e.g. `"str"`), not its property // suffix: that suffix is returned in `ids` and appended once by // getFinalName. Using `usedName.render()` would emit it here too, // duplicating the access (e.g. `"str".a.a`). rawName: usedName.render( Template.toNormalComment( `inlined export ${propertyAccess(exportName)}` ) ), ids: usedName.suffix, exportName }; } return { info, name: directExport, ids: /** @type {ExportName} */ (usedName).slice(1), exportName }; } const rawExport = info.rawExportMap && info.rawExportMap.get(exportId); if (rawExport) { return { info, rawName: rawExport, ids: exportName.slice(1), exportName }; } const reexport = exportInfo.findTarget(moduleGraph, (module) => moduleToInfoMap.has(module) ); if (reexport === false) { // Source module was removed because all its exports were inlined; // we render the inlined value here instead of binding to the now-absent module. const target = exportInfo.getTarget(moduleGraph); if (target && target.export) { const usedName = moduleGraph .getExportsInfo(target.module) .getUsedName([...target.export, ...exportName.slice(1)], runtime); if (usedName instanceof InlinedUsedName) { return { info, // Literal only; suffix is appended once via `ids` (see directExport branch). rawName: usedName.render( Template.toNormalComment( `inlined export ${propertyAccess(exportName)}` ) ), ids: usedName.suffix, exportName }; } } throw new Error( `Target module of reexport from '${info.module.readableIdentifier( requestShortener )}' is not part of the concatenation (export '${exportId}')\nModules in the concatenation:\n${Array.from( moduleToInfoMap, ([m, info]) => ` * ${info.type} ${m.readableIdentifier(requestShortener)}` ).join("\n")}` ); } if (reexport) { const refInfo = moduleToInfoMap.get(reexport.module); return getFinalBinding( moduleGraph, /** @type {ModuleInfo} */ (refInfo), reexport.export ? [...reexport.export, ...exportName.slice(1)] : exportName.slice(1), moduleToInfoMap, runtime, requestShortener, runtimeTemplate, neededNamespaceObjects, asCall, reexport.deferred, /** @type {BuildMeta} */ (info.module.buildMeta).strictHarmonyModule, asiSafe, false, alreadyVisited ); } // A wrapped module resolves every export through its live exports // alias (namespaceObjectName), same as namespaceExportSymbol modules. if (info.namespaceExportSymbol || info.wrapped) { const usedName = exportsInfo.getUsedName(exportName, runtime); if (!usedName) { return { info, rawName: "/* unused export */ undefined", ids: exportName.slice(1), exportName }; } // an inlined export never reaches the exports object, so render the // literal instead of reading the namespace if (usedName instanceof InlinedUsedName) { return { info, // Literal only; suffix is appended once via `ids` (see directExport branch). rawName: usedName.render( Template.toNormalComment( `inlined export ${propertyAccess(exportName)}` ) ), ids: usedName.suffix, exportName }; } return { info, rawName: /** @type {string} */ (info.namespaceObjectName), ids: /** @type {ExportName} */ (usedName), exportName }; } throw new Error( `Cannot get final name for export '${exportName.join( "." )}' of ${info.module.readableIdentifier(requestShortener)}` ); } case "external": { const used = exportsInfo.getUsedName(exportName, runtime); if (!used) { return { info, rawName: "/* unused export */ undefined", ids: exportName.slice(1), exportName }; } if (used instanceof InlinedUsedName) { return { info, // Literal only; suffix is appended once via `ids` (see directExport branch). rawName: used.render( Template.toNormalComment( `inlined export ${propertyAccess(exportName)}` ) ), ids: used.suffix, exportName }; } const usedName = /** @type {ExportName} */ (used); const comment = equals(usedName, exportName) ? "" : Template.toNormalComment(`${exportName.join(".")}`); return { info, rawName: (deferred ? `${info.deferredName}.a` : getExternalModuleExpr(info)) + comment, ids: usedName, exportName }; } } }; /** * @param {ModuleGraph} moduleGraph the module graph * @param {ModuleInfo} info module info * @param {ExportName} exportName exportName * @param {ModuleToInfoMap} moduleToInfoMap moduleToInfoMap * @param {RuntimeSpec} runtime for which runtime * @param {RequestShortener} requestShortener the request shortener * @param {RuntimeTemplate} runtimeTemplate the runtime template * @param {NeededNamespaceObjects} neededNamespaceObjects modules for which a namespace object should be generated * @param {boolean} asCall asCall * @param {boolean} depDeferred the dependency is deferred * @param {boolean | undefined} callContext callContext * @param {boolean | undefined} strictHarmonyModule strictHarmonyModule * @param {boolean | undefined} asiSafe asiSafe * @param {boolean=} moduleExportsAccess resolve to the module's own exports value with plain property access, skipping ESM interop * @returns {string} the final name */ const getFinalName = ( moduleGraph, info, exportName, moduleToInfoMap, runtime, requestShortener, runtimeTemplate, neededNamespaceObjects, asCall, depDeferred, callContext, strictHarmonyModule, asiSafe, moduleExportsAccess ) => { const binding = getFinalBinding( moduleGraph, info, exportName, moduleToInfoMap, runtime, requestShortener, runtimeTemplate, neededNamespaceObjects, asCall, depDeferred, strictHarmonyModule, asiSafe, moduleExportsAccess ); { const { ids, comment } = binding; /** @type {string} */ let reference; /** @type {boolean} */ let isPropertyAccess; if ("rawName" in binding) { reference = `${binding.rawName}${comment || ""}${propertyAccess(ids)}`; isPropertyAccess = ids.length > 0; } else { const { info, name: exportId } = binding; const name = info.internalNames.get(exportId); if (!name) { throw new Error( `The export "${exportId}" in "${info.module.readableIdentifier( requestShortener )}" has no internal name (existing names: ${ Array.from( info.internalNames, ([name, symbol]) => `${name}: ${symbol}` ).join(", ") || "none" })` ); } reference = `${name}${comment || ""}${propertyAccess(ids)}`; isPropertyAccess = ids.length > 1; } if (isPropertyAccess) { if (asCall && callContext === false) { return asiSafe ? `(0,${reference})` : asiSafe === false ? `;(0,${reference})` : `/*#__PURE__*/Object(${reference})`; } else if (binding.info.wrapped) { return asiSafe === false ? `;(${reference})` : `(${reference})`; } } return reference; } }; /** * @typedef {object} ConcatenateModuleHooks * @property {SyncBailHook<[ConcatenatedModule, RuntimeSpec[], string, Record<string, string>], boolean>} onDemandExportsGeneration * @property {SyncBailHook<[Partial<ConcatenatedModuleInfo>, ConcatenatedModuleInfo], boolean | void>} concatenatedModuleInfo */ /** @typedef {BuildInfo["topLevelDeclarations"]} TopLevelDeclarations */ /** * Defines the build info properties specific to concatenated modules. * @typedef {object} KnownConcatenatedModuleBuildInfo * @property {FileSystemDependencies=} fileDependencies * @property {FileSystemDependencies=} contextDependencies * @property {FileSystemDependencies=} missingDependencies * @property {boolean=} needCreateRequire collected from the inner modules * @property {boolean=} inlineExports taken over from the root module */ /** @typedef {BuildInfo & KnownConcatenatedModuleBuildInfo} ConcatenatedModuleBuildInfo */ class ConcatenatedModule extends Module { /** * @param {Module} rootModule the root module of the concatenation * @param {Set<Module>} modules all modules in the concatenation (including the root module) * @param {RuntimeSpec} runtime the runtime * @param {Compilation} compilation the compilation * @param {AssociatedObjectForCache=} associatedObjectForCache object for caching * @param {HashFunction=} hashFunction hash function to use * @returns {ConcatenatedModule} the module */ static create( rootModule, modules, runtime, compilation, associatedObjectForCache, hashFunction = DEFAULTS.HASH_FUNCTION ) { const identifier = ConcatenatedModule._createIdentifier( rootModule, modules, associatedObjectForCache, hashFunction ); return new ConcatenatedModule({ identifier, rootModule, modules, runtime, compilation }); } /** * @param {object} options options * @param {string} options.identifier the identifier of the module * @param {Module} options.rootModule the root module of the concatenation * @param {RuntimeSpec} options.runtime the selected runtime * @param {Set<Module>} options.modules all concatenated modules * @param {Compilation} options.compilation the compilation */ constructor({ identifier, rootModule, modules, runtime, compilation }) { super(JAVASCRIPT_MODULE_TYPE_ESM, null, rootModule && rootModule.layer); // Redeclared with the concatenated module specific shape /** @type {ConcatenatedModuleBuildInfo | undefined} */ this.buildInfo = undefined; /** @type {JavascriptModuleBuildMeta | undefined} */ this.buildMeta = undefined; // Info from Factory /** @type {string} */ this._identifier = identifier; /** @type {Module} */ this.rootModule = rootModule; /** @type {Set<Module>} */ this._modules = modules; /** @type {RuntimeSpec} */ this._runtime = runtime; this.factoryMeta = rootModule && rootModule.factoryMeta; /** @type {Compilation} */ this.compilation = compilation; } /** * Assuming this module is in the cache. Update the (cached) module with * the fresh module from the factory. Usually updates internal references * and properties. * @param {Module} module fresh module * @returns {void} */ updateCacheModule(module) { throw new Error("Must not be called"); } /** * Returns the source types this module can generate. * @returns {SourceTypes} types available (do not mutate) */ getSourceTypes() { return JAVASCRIPT_TYPES; } get modules() { return [...this._modules]; } /** * Returns the unique identifier used to reference this module. * @returns {string} a unique identifier of the module */ identifier() { return this._identifier; } /** * Returns a human-readable identifier for this module. * @param {RequestShortener} requestShortener the request shortener * @returns {string} a user readable identifier of the module */ readableIdentifier(requestShortener) { return `${this.rootModule.readableIdentifier(requestShortener)} + ${ this._modules.size - 1 } modules`; } /** * Gets the library identifier. * @param {LibIdentOptions} options options * @returns {LibIdent | null} an identifier for library inclusion */ libIdent(options) { return this.rootModule.libIdent(options); } /** * Returns the path used when matching this module against rule conditions. * @returns {NameForCondition | null} absolute path which should be used for condition matching (usually the resource path) */ nameForCondition() { return this.rootModule.nameForCondition(); } /** * Gets side effects connection state. * @param {ModuleGraph} moduleGraph the module graph * @returns {ConnectionState} how this module should be connected to referencing modules when consumed for side-effects only */ getSideEffectsConnectionState(moduleGraph) { return this.rootModule.getSideEffectsConnectionState(moduleGraph); } /** * Builds the module using the provided compilation context. * @param {WebpackOptions} options webpack options * @param {Compilation} compilation the compilation * @param {ResolverWithOptions} resolver the resolver * @param {InputFileSystem} fs the file system * @param {BuildCallback} callback callback function * @returns {void} */ build(options, compilation, resolver, fs, callback) { const { rootModule } = this; const concatenateCommonJsModules = isCommonJsConcatenationEnabled( options.optimization.concatenateModules ); const { moduleArgument, exportsArgument } = /** @type {BuildInfo} */ (rootModule.buildInfo); /** @type {ConcatenatedModuleBuildInfo} */ this.buildInfo = { strict: true, cacheable: true, moduleArgument, exportsArgument, fileDependencies: new LazySet(), contextDependencies: new LazySet(), missingDependencies: new LazySet(), topLevelDeclarations: new Set(), assets: undefined, inlineExports: /** @type {JavascriptModuleBuildInfo} */ ( rootModule.buildInfo ).inlineExports }; this.buildMeta = rootModule.buildMeta; this.clearDependenciesAndBlocks(); this.clearWarningsAndErrors(); for (const m of this._modules) { // populate cacheable const { cacheable, notCacheableReasons } = /** @type {BuildInfo} */ ( m.buildInfo ); if (!cacheable) { this.buildInfo.cacheable = false; if (notCacheableReasons) { const reasons = this.buildInfo.notCacheableReasons || (this.buildInfo.notCacheableReasons = []); for (const reason of notCacheableReasons) { if (!reasons.includes(reason)) reasons.push(reason); } } } // populate dependencies — keep only deps that leave the concat set for (const d of m.dependencies) { if ( !Dependency.canConcatenate(d, concatenateCommonJsModules) || !this._modules.has( /** @type {Module} */ (compilation.moduleGraph.getModule(d)) ) ) { this.dependencies.push(d); } } // populate codeGenerationDependencies — the inner modules' // templates are applied during ConcatenatedModule.codeGeneration, // so the referenced modules must have been code-generated by then. // Skip references that point back into the concat set itself. if (m.codeGenerationDependencies !== undefined) { for (const d of m.codeGenerationDependencies) { const referenced = /** @type {Module} */ (compilation.moduleGraph.getModule(d)); if (!this._modules.has(referenced)) { this.addCodeGenerationDependency(d); } } } // populate blocks for (const d of m.blocks) { this.blocks.push(d); } // populate warnings const warnings = m.getWarnings(); if (warnings !== undefined) { for (const warning of warnings) { this.addWarning(warning); } } // populate errors const errors = m.getErrors(); if (errors !== undefined) { for (const error of errors) { this.addError(error); } } const { assets, assetsInfo, topLevelDeclarations, needCreateRequire } = /** @type {JavascriptModuleBuildInfo} */ (m.buildInfo); const buildInfo = this.buildInfo; // populate topLevelDeclarations if (topLevelDeclarations) { const mergedTopLevelDeclarations = buildInfo.topLevelDeclarations; if (mergedTopLevelDeclarations !== undefined) { for (const decl of topLevelDeclarations) { mergedTopLevelDeclarations.add(decl); } } } else { buildInfo.topLevelDeclarations = undefined; } // populate needCreateRequire if (needCreateRequire) { this.buildInfo.needCreateRequire = true; } // populate assets if (assets) { if (buildInfo.assets === undefined) { buildInfo.assets = Object.create(null); } Object.assign( /** @type {NonNullable<BuildInfo["assets"]>} */ (buildInfo.assets), assets ); } if (assetsInfo) { if (buildInfo.assetsInfo === undefined) { buildInfo.assetsInfo = new Map(); } for (const [key, value] of assetsInfo) { buildInfo.assetsInfo.set(key, value); } } } callback(); } /** * Returns the estimated size for the requested source type. * @param {string=} type the source type for which the size should be estimated * @returns {number} the estimated size of the module (must be non-zero) */ size(type) { // Guess size from embedded modules let size = 0; for (const module of this._modules) { size += module.size(type); } return size; } /** * @private * @param {Module} rootModule the root of the concatenation * @param {Set<Module>} modulesSet a set of modules which should be concatenated * @param {RuntimeSpec} runtime for this runtime * @param {ModuleGraph} moduleGraph the module graph * @returns {ConcatenationEntry[]} concatenation list */ _createConcatenationList(rootModule, modulesSet, runtime, moduleGraph) { const concatenateCommonJsModules = isCommonJsConcatenationEnabled( this.compilation.options.optimization.concatenateModules ); /** @type {ConcatenationEntry[]} */ const list = []; /** @type {Map<Module, { runtimeCondition: RuntimeSpec | true, nonDeferAccess: NonDeferAccess }>} */ const existingEntries = new Map(); /** @type {Map<Module, Module[]>} */ const importConnections = new Map(); /** @type {Set<Module>} */ const wrappedModules = new Set(); /** * @param {Module} module a module * @returns {Iterable<{ connection: ModuleGraphConnection, runtimeCondition: RuntimeSpec | true, nonDeferAccess: NonDeferAccess }>} imported modules in order */ const getConcatenatedImports = (module) => { const connections = [...moduleGraph.getOutgoingConnections(module)]; if (module === rootModule) { for (const c of moduleGraph.getOutgoingConnections(this)) { connections.push(c); } } /** * @type {{ connection: ModuleGraphConnection, sourceOrder: number, rangeStart: number | undefined, defer?: boolean }[]} */ const references = connections .filter((connection) => { if ( !connection.dependency || !Dependency.canConcatenate( connection.dependency, concatenateCommonJsModules ) ) { return false; } if ( !Module.getSourceBasicTypes(connection.module).has(JAVASCRIPT_TYPE) ) { return false; } return ( connection && connection.resolvedOriginModule === module && connection.module && connection.isTargetActive(runtime) ); }) .map((connection) => { const dep = /** @type {HarmonyImportDependency} */ (connection.dependency); return { connection, // A require() edge carries no sourceOrder; Infinity sorts it last, // matching the low-priority sentinel. NaN must not be used here: // compareNumbers returns 0 against every number, which makes the // comparator non-transitive and the order depend on insertion order. sourceOrder: typeof dep.sourceOrder === "number" ? dep.sourceOrder : Infinity, rangeStart: dep.range && dep.range[0], defer: ImportPhaseUtils.isDefer(dep.phase) }; }); /** * bySourceOrder * @example * import a from "a"; // sourceOrder=1 * import b from "b"; // sourceOrder=2 * * byRangeStart * @example * import {a, b} from "a"; // sourceOrder=1 * a.a(); // first range * b.b(); // second range * * If there is no reexport, we have the same source. * If there is reexport, but module has side effects, this will lead to reexport module only. * If there is side-effects-free reexport, we can get simple deterministic result with range start comparison. */ references.sort(concatComparators(bySourceOrder, byRangeStart)); /** @type {Map<Module, { connection: ModuleGraphConnection, runtimeCondition: RuntimeSpec | true, nonDeferAccess: NonDeferAccess }>} */ const referencesMap = new Map(); /** @type {Module[] | undefined} */ let targets; for (const { connection, defer } of references) { const runtimeCondition = filterRuntime(runtime, (r) => connection.isTargetActive(r) ); if (runtimeCondition === false) continue; const nonDeferAccess = !defer; const module = connection.module; if (targets === undefined) targets = [module]; else if (!targets.includes(module)) targets.push(module); if ( [ CommonJsRequireDependency, CommonJsFullRequireDependency, CommonJsExportRequireDependency ].some((Dep) => connection.dependency instanceof Dep) ) { wrappedModules.add(module); } const entry = referencesMap.get(module); if (entry === undefined) { referencesMap.set(module, { connection, runtimeCondition, nonDeferAccess }); continue; } entry.runtimeCondition = mergeRuntimeConditionNonFalse( entry.runtimeCondition, runtimeCondition, runtime ); entry.nonDeferAccess = mergeNonDeferAccess( entry.nonDeferAccess, nonDeferAccess ); } if (targets !== undefined) importConnections.set(module, targets); return referencesMap.values(); }; /** * @param {ModuleGraphConnection} connection graph connection * @param {RuntimeSpec | true} runtimeCondition runtime condition * @param {NonDeferAccess} nonDeferAccess non-defer access * @returns {void} */ const enterModule = (connection, runtimeCondition, nonDeferAccess) => { const module = connection.module; if (!module) return; const existingEntry = existingEntries.get(module); if ( existingEntry && existingEntry.runtimeCondition === true && existingEntry.nonDeferAccess === true ) { return; } if (modulesSet.has(module)) { existingEntries.set(module, { runtimeCondition: true, nonDeferAccess: true }); if (runtimeCondition !== true) { throw new Error( `Cannot runtime-conditional concatenate a module (${module.identifier()} in ${this.rootModule.identifier()}, ${runtimeConditionToString( runtimeCondition )}). This should not happen.` ); } if (nonDeferAccess !== true) { throw new Error( `Cannot deferred concatenate a module (${module.identifier()} in ${this.rootModule.identifier()}. This should not happen.` ); } const imports = getConcatenatedImports(module); for (const { connection, runtimeCondition, nonDeferAccess } of imports) { enterModule(connection, runtimeCondition, nonDeferAccess); } list.push({ type: "concatenated", wrapped: needWrapped(module), module: connection.module, runtimeCondition, nonDeferAccess }); } else { /** @type {RuntimeSpec | boolean} */ let reducedRuntimeCondition; /** @type {NonDeferAccess} */ let reducedNonDeferAccess; if (existingEntry !== undefined) { reducedRuntimeCondition = subtractRuntimeCondition( runtimeCondition, existingEntry.runtimeCondition, runtime ); reducedNonDeferAccess = subtractNonDeferAccess( nonDeferAccess, existingEntry.nonDeferAccess ); if ( reducedRuntimeCondition === false && reducedNonDeferAccess === false ) { return; } if (reducedRuntimeCondition !== false) { existingEntry.runtimeCondition = mergeRuntimeConditionNonFalse( existingEntry.runtimeCondition, reducedRuntimeCondition, runtime ); } if (reducedNonDeferAccess !== false) { existingEntry.nonDeferAccess = mergeNonDeferAccess( existingEntry.nonDeferAccess, reducedNonDeferAccess ); } } else { reducedRuntimeCondition = runtimeCondition; reducedNonDeferAccess = nonDeferAccess; existingEntries.set(connection.module, { runtimeCondition, nonDeferAccess }); } if (list.length > 0) { const lastItem = list[list.length - 1]; if ( lastItem.type === "external" && lastItem.module === connection.module ) { lastItem.runtimeCondition = mergeRuntimeCondition( lastItem.runtimeCondition, reducedRuntimeCondition, runtime ); lastItem.nonDeferAccess = mergeNonDeferAccess( lastItem.nonDeferAccess, reducedNonDeferAccess ); return; } } list.push({ type: "external", wrapped: wrappedModules.has(connection.module), get module() { // We need to use a getter here, because the module in the dependency // could be replaced by some other process (i. e. also replaced with a // concatenated module) return connection.module; }, runtimeCondition: reducedRuntimeCondition, nonDeferAccess: reducedNonDeferAccess }); } }; existingEntries.set(rootModule, { runtimeCondition: true, nonDeferAccess: true }); const imports = getConcatenatedImports(rootModule); for (const { connection, runtimeCondition, nonDeferAccess } of imports) { enterModule(connection, runtimeCondition, nonDeferAccess); } list.push({ type: "concatenated", wrapped: needWrapped(rootModule), module: rootModule, runtimeCondition: true, nonDeferAccess: true }); // A lazily evaluated member may only depend on lazily evaluated members, // so wrapping propagates forward from every `require()` target along all // import edges, ESM included. /** @type {Set<Module>} */ const modules = new Set(wrappedModules); for (const entry of list) { if (entry.wrapped) modules.add(entry.module); } const queue = [...modules]; for (let i = 0; i < queue.length; i++) { const targets = importConnections.get(queue[i]); if (targets === undefined) continue; for (const target of targets) { if (modules.has(target)) continue; modules.add(target); queue.push(target); } } for (const entry of list) { if ( !entry.wrapped && modules.has(entry.module) && entryCanBeWrapped(entry) ) { entry.wrapped = true; } } return list; } /** * @param {Module} rootModule the root module of the concatenation * @param {Set<Module>} modules all modules in the concatenation (including the root module) * @param {AssociatedObjectForCache=} associatedObjectForCache object for caching * @param {HashFunction=} hashFunction hash function to use * @returns {string} the identifier */ static _createIdentifier( rootModule, modules, associatedObjectForCache, hashFunction = DEFAULTS.HASH_FUNCTION ) { const cachedMakePathsRelative = makePathsRelative.bindContextCache( /** @type {string} */ (rootModule.context), associatedObjectForCache ); /** @type {string[]} */ const identifiers = []; for (const module of modules) { identifiers.push(cachedMakePathsRelative(module.identifier())); } identifiers.sort(); const hash = createHash(hashFunction); hash.update(identifiers.join(" ")); return `${rootModule.identifier()}|${hash.digest("hex")}`; } /** * Adds the provided file dependencies to the module. * @param {FileSystemDependencies} fileDependencies set where file dependencies are added to * @param {FileSystemDependencies} contextDependencies set where context dependencies are added to * @param {FileSystemDependencies} missingDependencies set where missing dependencies are added to * @param {FileSystemDependencies} buildDependencies set where build dependencies are added to */ addCacheDependencies( fileDependencies, contextDependencies, missingDependencies, buildDependencies ) { for (const module of this._modules) { module.addCacheDependencies( fileDependencies, contextDependencies, missingDependencies, buildDependencies ); } } /* * How the inner modules become one flat source. Stages [1]-[5] only mutate the * per-module info records (names, ReplaceSource patches); stages [6]-[9] append * to the single output ConcatSource, in exactly the order shown. * * _getModulesWithInfo() — ordered concatenation list + one info per module * | (concatenated [+wrapped] | external | reference) * v * [1] ANALYSE — inner codeGeneration() per module, into a ConcatenationScope * | plain -> ReplaceSource + ast + eslint-scope scopes * | wrapped -> ReplaceSource only, body kept verbatim * | external -> nothing yet, rendered as a require() below * [2] RESERVE — free globals of the analysed bodies enter allUsedNames, so * | renaming in [3] can't shadow them (a wrapped body is not analysed; * | the reserved `__webpack_` prefix protects it) * [3] RENAME — module-scope variables renamed inside the ReplaceSource, then * | the generated names allocated: wrapper accessor, namespace object, * | interop helpers, external import variable * [4] LINK — __WEBPACK_MODULE_REFERENCE__ tokens replaced by their final * | binding; through the ast for analysed bodies, textually for wrapped * [5] COLLECT EXPORTS — root exports split into used / unused / inlined * v * ---- from here on everything is appended to the result ConcatSource ---- * | * [6] EMIT EXPORTS — __esModule flag + exports getters, UNUSED/INLINED lists * [7] BUILD NS — namespace object sources built as strings (not emitted yet) * [8] DEFINE — everything that must exist before any body runs: namespace * | objects, wrapper accessors (wrapped modules and wrapped externals), * | deferred external loaders * [9] EVALUATE — bodies in concatenation order: plain bodies inline, externals * | as require(); a wrapped body stays behind its accessor * v * [10] RESULT — CodeGenerationResult { sources, data, runtimeRequirements } */ /** * Generates code and runtime requirements for this module. * @param {CodeGenerationContext} context context for code generation * @returns {CodeGenerationResult} result */ codeGeneration({ dependencyTemplates, runtimeTemplate, moduleGraph, chunkGraph, runtime: generationRuntime, runtimes, codeGenerationResults }) { const { concatenatedModuleInfo } = ConcatenatedModule.getCompilationHooks( this.compilation ); /** @type {RuntimeRequirements} */ const runtimeRequirements = new Set(); const runtime = intersectRuntime(generationRuntime, this._runtime); const requestShortener = runtimeTemplate.requestShortener; // Meta info for each module, in evaluation order (repeats become references) const [modulesWithInfo, moduleToInfoMap] = this._getModulesWithInfo( moduleGraph, runtime ); // This module's body is the root's body, so a wrapped root still evaluates // at its slot. No import edge inside the concatenation expresses that. const rootModuleInfo = moduleToInfoMap.get(this.rootModule); if (rootModuleInfo !== undefined) rootModuleInfo.eager = true; // State shared by all stages below, in the order they are filled // List of all used names to avoid conflicts const allUsedNames = new Set(RESERVED_NAMES); // Updated Top level declarations are created by renaming /** @type {TopLevelDeclarations} */ const topLevelDeclarations = new Set(); // Free names remaining in the rendered source (runtime globals are // tracked by runtimeRequirements instead) /** @type {Set<string>} */ const freeNames = new Set(); // List of additional names in scope for module references /** @type {UsedNamesInScopeInfo} */ const usedNamesInScopeInfo = new Map(); // Set of already checked scopes /** @type {Set<Scope>} */ const ignoredScopes = new Set(); // Set with modules that need a generated namespace object /** @type {NeededNamespaceObjects} */ const neededNamespaceObjects = new Set(); // Set with modules whose mangled namespace escapes as a whole value and // therefore needs a decoupled namespace object keyed by the original names /** @type {Set<ConcatenatedModuleInfo | ExternalModuleInfo>} */ const neededEscapeNamespaceObjects = new Set(); // #region [1] ANALYSE: run each inner module's own codeGeneration; a // non-wrapped body is parsed and scope-analysed so [3] can rename its locals for (const info of moduleToInfoMap.values()) { this._analyseModule( moduleToInfoMap, info, dependencyTemplates, runtimeTemplate, moduleGraph, chunkGraph, runtime, runtimes, /** @type {CodeGenerationResults} */ (codeGenerationResults), allUsedNames ); } // #endregion /** * Lazily allocates a decoupled namespace object for a concatenated module * whose exports are mangled, so an escaping whole-namespace value still * exposes the original export names. Returns undefined when no export is * mangled (the regular namespace object can be used as-is). * @param {ConcatenatedModuleInfo | ExternalModuleInfo} info module info * @returns {string | undefined} the escape namespace object name */ const getEscapeNamespaceObjectName = (info) => { // Only a hoisted member's namespaceExportSymbol is a real variable // holding the original names; a wrapped one reuses the field for its // `X_namespaceFn()` call, which exposes the mangled keys if ( info.type === "concatenated" && !info.wrapped && info.namespaceExportSymbol ) { return undefined; } // Deferred external modules keep their special deferred namespace object // and are rendered through a different path that wouldn't emit ours. if (info.type === "external" && info.deferred) { return undefined; } // Only real ES module namespaces are decoupled; non-harmony modules use // their interop/fake namespace object as before. const buildMeta = /** @type {BuildMeta} */ (info.module.buildMeta); if (!buildMeta || buildMeta.exportsType !== "namespace") return undefined; if (info.escapeNamespaceObjectName !== undefined) { return info.escapeNamespaceObjectName; } const exportsInfo = moduleGraph.getExportsInfo(info.module); let mangled = false; for (const exportInfo of exportsInfo.orderedExports) { if (exportInfo.provided === false) continue; const usedName = exportInfo.getUsedName(undefined, runtime); if (!usedName || usedName instanceof InlinedUsedName) continue; if (usedName[usedName.length - 1] !== exportInfo.name) { mangled = true; break; } } if (!mangled) return undefined; const name = findNewName( "namespaceObject", allUsedNames, /** @type {UsedNames} */ (new Set()), info.module.readableIdentifier(requestShortener) ); allUsedNames.add(name); topLevelDeclarations.add(name); info.escapeNamespaceObjectName = name; neededEscapeNamespaceObjects.add(info); return name; }; // #region [2] RESERVE: the free globals of every analysed body. Claiming // them up front is what lets [3] rename without shadowing any of them for (const info of modulesWithInfo) { if (info.type === "concatenated") { if (info.wrapped) { // The body is emitted verbatim and never renamed, so every identifier // in it is claimed: its declarations must not shadow a name [3] // generates, and its free globals must not be captured by one. for (const name of findIdentifiers( /** @type {Source} */ (info.internalSource) )) { allUsedNames.add(name); } // A wrapped body has no scope analysis, so its references are found // textually. They still have to be resolved here: binding one is what // flags the interop objects it needs, and [3] allocates their names. /** @type {Set<string>} */ const visitedReferences = new Set(); for (const reference of ConcatenationScope.findModuleReferences( /** @type {Source} */ (info.internalSource) )) { if (visitedReferences.has(reference.name)) continue; visitedReferences.add(reference.name); const match = ConcatenationScope.matchModuleReference( reference.name ); if (!match) continue; const referencedInfo = modulesWithInfo[match.index]; if (referencedInfo.type === "reference") { throw new Error("Module reference can't point to a reference"); } if ( match.mangleableNamespace && match.ids.length === 0 && getEscapeNamespaceObjectName(referencedInfo) !== undefined ) { continue; } getFinalBinding( moduleGraph, referencedInfo, match.ids, moduleToInfoMap, runtime, requestShortener, runtimeTemplate, neededNamespaceObjects, match.call, match.deferredImport, /** @type {BuildMeta} */ (info.module.buildMeta).strictHarmonyModule, match.asiSafe, match.moduleExportsAccess ); } } else { // ignore symbols from moduleScope if (info.moduleScope) { ignoredScopes.add(info.moduleScope); } // The super class expression in class scopes behaves weird // We get ranges of all super class expressions to make // renaming to work correctly /** @typedef {{ range: Range, variables: Variable[] }} ClassInfo */ /** @type {WeakMap<Scope, ClassInfo[]>} */ const superClassCache = new WeakMap(); /** * @param {Scope} scope scope * @returns {ClassInfo[]} result */ const getSuperClassExpressions = (scope) => { const cacheEntry = superClassCache.get(scope); if (cacheEntry !== undefined) return cacheEntry; /** @type {ClassInfo[]} */ const superClassExpressions = []; for (const childScope of scope.childScopes) { if (childScope.type !== "class") continue; const block = childScope.block; if ( (block.type === "ClassDeclaration" || block.type === "ClassExpression") && block.superClass ) { superClassExpressions.push({ range: /** @type {Range} */ (block.superClass.range), variables: childScope.variables }); } } superClassCache.set(scope, superClassExpressions); return superClassExpressions; }; // add global symbols if (info.globalScope) { for (const reference of info.globalScope.through) { const name = reference.identifier.name; if (ConcatenationScope.isModuleReference(name)) { const match = ConcatenationScope.matchModuleReference(name); if (!match) continue; const referencedInfo = modulesWithInfo[match.index]; if (referencedInfo.type === "reference") { throw new Error( "Module reference can't point to a reference" ); } // An escaping mangled namespace resolves to its own decoupled // namespace object (a unique top-level name), so it neither needs // the regular namespace object nor super-class scope handling. if ( match.mangleableNamespace && match.ids.length === 0 && getEscapeNamespaceObjectName(referencedInfo) !== undefined ) { continue; } const binding = getFinalBinding( moduleGraph, referencedInfo, match.ids, moduleToInfoMap, runtime, requestShortener, runtimeTemplate, neededNamespaceObjects, false, match.deferredImport, /** @type {BuildMeta} */ (info.module.buildMeta).strictHarmonyModule, true, match.moduleExportsAccess ); if (!binding.ids) continue; const { usedNames, alreadyCheckedScopes } = getUsedNamesInScopeInfo( usedNamesInScopeInfo, binding.info.module.identifier(), "name" in binding ? binding.name : "" ); for (const expr of getSuperClassExpressions(reference.from)) { if ( expr.range[0] <= /** @type {Range} */ (reference.identifier.range)[0] && expr.range[1] >= /** @type {Range} */ (reference.identifier.range)[1] ) { for (const variable of expr.variables) { usedNames.add(variable.name); } } } addScopeSymbols( reference.from, usedNames, alreadyCheckedScopes, ignoredScopes ); } else { allUsedNames.add(name); freeNames.add(name); } } } } } } // #endregion /** * @param {string} name the name to find a new name for * @param {ConcatenatedModuleInfo} info the info of the module * @param {Reference[]} references the references to the name * @returns {string | undefined} the new name or undefined if the name is not found */ const _findNewName = (name, info, references) => { const { usedNames, alreadyCheckedScopes } = getUsedNamesInScopeInfo( usedNamesInScopeInfo, info.module.identifier(), name ); if (allUsedNames.has(name) || usedNames.has(name)) { for (const ref of references) { addScopeSymbols( ref.from, usedNames, alreadyCheckedScopes, ignoredScopes ); } const newName = findNewName( name, allUsedNames, usedNames, info.module.readableIdentifier(requestShortener) ); allUsedNames.add(newName); info.internalNames.set(name, newName); topLevelDeclarations.add(newName); return newName; } }; /** * @param {string} name the name to find a new name for * @param {ConcatenatedModuleInfo} info the info of the module * @param {Reference[]} references the references to the name * @returns {string | undefined} the new name or undefined if the name is not found */ const _findNewNameForSpecifier = (name, info, references) => { const { usedNames: moduleUsedNames, alreadyCheckedScopes } = getUsedNamesInScopeInfo( usedNamesInScopeInfo, info.module.identifier(), name ); /** @type {UsedNames} */ const referencesUsedNames = new Set(); for (const ref of references) { addScopeSymbols( ref.from, referencesUsedNames, alreadyCheckedScopes, ignoredScopes ); } if (moduleUsedNames.has(name) || referencesUsedNames.has(name)) { const newName = findNewName( name, allUsedNames, new Set([...moduleUsedNames, ...referencesUsedNames]), info.module.readableIdentifier(requestShortener) ); allUsedNames.add(newName); topLevelDeclarations.add(newName); return newName; } }; // #region [3] RENAME: rename every module-scope variable inside its // ReplaceSource, then allocate the generated names (wrapper accessor, // namespace object, interop helpers, external import variable) for (const info of moduleToInfoMap.values()) { const { usedNames: namespaceObjectUsedNames } = getUsedNamesInScopeInfo( usedNamesInScopeInfo, info.module.identifier(), "" ); switch (info.type) { case "concatenated": { if (info.wrapped) { // A wrapped module declares exactly one shared-scope name: the // lazy wrapper accessor holding its body. [2] claimed every // identifier of that body, so findNewName avoids the ones that // would shadow the accessor from inside it. const namespaceFnName = findNewName( "namespaceFn", allUsedNames, namespaceObjectUsedNames, info.module.readableIdentifier(requestShortener) ); allUsedNames.add(namespaceFnName); topLevelDeclarations.add(namespaceFnName); info.namespaceFnName = namespaceFnName; // Every reference — `require()` and ESM import alike — reads the // live exports object through the accessor, so the namespace is // the call itself, not a variable. Not a declared name. info.namespaceObjectName = `${namespaceFnName}()`; } else { const variables = /** @type {Scope} */ (info.moduleScope).variables; for (const variable of variables) { const name = variable.name; const references = getAllReferences(variable); const newName = _findNewName(name, info, references); if (newName) { const source = /** @type {ReplaceSource} */ (info.source); /** @type {Set<Identifier>} */ const allIdentifiers = new Set(); for (const r of references) allIdentifiers.add(r.identifier); for (const identifier of variable.identifiers) { allIdentifiers.add(identifier); } for (const identifier of allIdentifiers) { const r = /** @type {Range} */ (identifier.range); const path = getPathInAst( /** @type {NonNullable<ConcatenatedModuleInfo["ast"]>} */ (info.ast), identifier ); if (path && path.length > 1) { const maybeProperty = path[1].type === "AssignmentPattern" && path[1].left === path[0] ? path[2] : path[1]; if ( maybeProperty.type === "Property" && maybeProperty.shorthand ) { source.insert(r[1], `: ${newName}`); continue; } } source.replace(r[0], r[1] - 1, newName); } } else { allUsedNames.add(name); info.internalNames.set(name, name); topLevelDeclarations.add(name); } } /** @type {string} */ let namespaceObjectName; if (info.namespaceExportSymbol) { namespaceObjectName = /** @type {string} */ (info.internalNames.get(info.namespaceExportSymbol)); } else { namespaceObjectName = findNewName( "namespaceObject", allUsedNames, namespaceObjectUsedNames, info.module.readableIdentifier(requestShortener) ); allUsedNames.add(namespaceObjectName); } info.namespaceObjectName = namespaceObjectName; topLevelDeclarations.add(namespaceObjectName); } break; } case "external": { const externalName = findNewName( "", allUsedNames, namespaceObjectUsedNames, info.module.readableIdentifier(requestShortener) ); allUsedNames.add(externalName); info.name = externalName; topLevelDeclarations.add(externalName); if (info.wrapped) { const namespaceFnName = findNewName( "namespaceFn", allUsedNames, namespaceObjectUsedNames, info.module.readableIdentifier(requestShortener) ); allUsedNames.add(namespaceFnName); info.namespaceFnName = namespaceFnName; topLevelDeclarations.add(namespaceFnName); } if (info.deferred) { const externalName = findNewName( "deferred", allUsedNames, namespaceObjectUsedNames, info.module.readableIdentifier(requestShortener) ); allUsedNames.add(externalName); info.deferredName = externalName; topLevelDeclarations.add(externalName); const externalNameInterop = findNewName( "deferredNamespaceObject", allUsedNames, namespaceObjectUsedNames, info.module.readableIdentifier(requestShortener) ); allUsedNames.add(externalNameInterop); info.deferredNamespaceObjectName = externalNameInterop; topLevelDeclarations.add(externalNameInterop); } break; } } const buildMeta = /** @type {BuildMeta} */ (info.module.buildMeta); if (buildMeta.exportsType !== "namespace") { const externalNameInterop = findNewName( "namespaceObject", allUsedNames, namespaceObjectUsedNames, info.module.readableIdentifier(requestShortener) ); allUsedNames.add(externalNameInterop); info.interopNamespaceObjectName = externalNameInterop; topLevelDeclarations.add(externalNameInterop); } if ( buildMeta.exportsType === "default" && buildMeta.defaultObject !== "redirect" && info.interopNamespaceObject2Used ) { const externalNameInterop = findNewName( "namespaceObject2", allUsedNames, namespaceObjectUsedNames, info.module.readableIdentifier(requestShortener) ); allUsedNames.add(externalNameInterop); info.interopNamespaceObject2Name = externalNameInterop; topLevelDeclarations.add(externalNameInterop); } if (buildMeta.exportsType === "dynamic" || !buildMeta.exportsType) { const externalNameInterop = findNewName( "default", allUsedNames, namespaceObjectUsedNames, info.module.readableIdentifier(requestShortener) ); allUsedNames.add(externalNameInterop); info.interopDefaultAccessName = externalNameInterop; topLevelDeclarations.add(externalNameInterop); } } // #endregion // #region [4] LINK: resolve each cross-module reference token to the final // name picked in [3] and patch it in. Must run after [3] — a reference can // only be resolved once its target has been named for (const info of moduleToInfoMap.values()) { if (info.type !== "concatenated") continue; if (!info.wrapped) { const globalScope = /** @type {Scope} */ (info.globalScope); // group references by name /** @type {Map<string, Reference[]>} */ const referencesByName = new Map(); for (const reference of globalScope.through) { const name = reference.identifier.name; let references = referencesByName.get(name); if (references === undefined) { referencesByName.set(name, (references = [])); } references.push(reference); } for (const [name, references] of referencesByName) { const match = ConcatenationScope.matchModuleReference(name); if (match) { const referencedInfo = modulesWithInfo[match.index]; if (referencedInfo.type === "reference") { throw new Error("Module reference can't point to a reference"); } const concatenationScope = /** @type {ConcatenatedModuleInfo} */ ( referencedInfo ).concatenationScope; const exportId = match.ids[0]; const specifier = concatenationScope && concatenationScope.getRawExport(exportId); if (specifier) { const newName = _findNewNameForSpecifier( specifier, info, references ); const initFragmentChanged = newName && concatenatedModuleInfo.call( { rawExportMap: new Map([ [exportId, /** @type {string} */ (newName)] ]) }, /** @type {ConcatenatedModuleInfo} */ (referencedInfo) ); if (initFragmentChanged) { concatenationScope.setRawExportMap(exportId, newName); } } // A whole-namespace value that escapes: when the module's exports // are mangled, point it at a decoupled namespace object that keeps // the original names so untracked access keeps working. const escapeName = match.mangleableNamespace && match.ids.length === 0 ? getEscapeNamespaceObjectName(referencedInfo) : undefined; const finalName = escapeName !== undefined ? escapeName : getFinalName( moduleGraph, referencedInfo, match.ids, moduleToInfoMap, runtime, requestShortener, runtimeTemplate, neededNamespaceObjects, match.call, match.deferredImport, !match.directImport, /** @type {BuildMeta} */ (info.module.buildMeta).strictHarmonyModule, match.asiSafe, match.moduleExportsAccess ); for (const reference of references) { const r = /** @type {Range} */ (reference.identifier.range); const source = /** @type {ReplaceSource} */ (info.source); // range is extended by 2 chars to cover the appended "._" source.replace(r[0], r[1] + 1, finalName); } } } } else { // Wrapped bodies are not re-parsed: find their reference tokens // textually. [2] scanned the same source, so this reuses its result. /** @type {Map<string, string>} */ const finalNames = new Map(); const source = /** @type {ReplaceSource} */ (info.source); for (const reference of ConcatenationScope.findModuleReferences( /** @type {Source} */ (info.internalSource) )) { let finalName = finalNames.get(reference.name); if (finalName === undefined) { const match = ConcatenationScope.matchModuleReference( reference.name ); if (!match) continue; const referencedInfo = modulesWithInfo[match.index]; if (referencedInfo.type === "reference") { throw new Error("Module reference can't point to a reference"); } // Same escaping whole-namespace rule as the hoisted branch above: a // wrapped target resolves to its `X_namespaceFn()` exports object, // whose keys are the mangled ones. const escapeName = match.mangleableNamespace && match.ids.length === 0 ? getEscapeNamespaceObjectName(referencedInfo) : undefined; finalName = escapeName !== undefined ? escapeName : getFinalName( moduleGraph, referencedInfo, match.ids, moduleToInfoMap, runtime, requestShortener, runtimeTemplate, neededNamespaceObjects, match.call, match.deferredImport, !match.directImport, /** @type {BuildMeta} */ (info.module.buildMeta).strictHarmonyModule, match.asiSafe, match.moduleExportsAccess ); finalNames.set(reference.name, finalName); } // token end is extended by 2 chars to cover the appended "._" source.replace(reference.start, reference.end + 1, finalName); } } } // #endregion // #region [5] COLLECT EXPORTS: split the root module's exports into used / // unused / inlined. Used ones stay thunks — resolving one can still // register a namespace object, which [7] must see // Map with all root exposed used exports /** @type {Map<string, (requestShortener: RequestShortener) => string>} */ const exportsMap = new Map(); // Set with all root exposed unused exports /** @type {Set<string>} */ const unusedExports = new Set(); // Set with all root exposed exports that were substituted with inlined literals /** @type {Set<string>} */ const inlinedExports = new Set(); const rootInfo = /** @type {ConcatenatedModuleInfo} */ (moduleToInfoMap.get(this.rootModule)); const strictHarmonyModule = /** @type {BuildMeta} */ (rootInfo.module.buildMeta).strictHarmonyModule; const exportsInfo = moduleGraph.getExportsInfo(rootInfo.module); /** @type {Record<string, string>} */ const exportsFinalName = {}; for (const exportInfo of exportsInfo.orderedExports) { const name = exportInfo.name; if (exportInfo.provided === false) continue; const used = exportInfo.getUsedName(undefined, runtime); if (!used) { unusedExports.add(name); continue; } if (used instanceof InlinedUsedName) { inlinedExports.add(name); continue; } exportsMap.set(used, (requestShortener) => { try { const finalName = getFinalName( moduleGraph, rootInfo, [name], moduleToInfoMap, runtime, requestShortener, runtimeTemplate, neededNamespaceObjects, false, false, false, strictHarmonyModule, true ); exportsFinalName[used] = finalName; return `/* ${ exportInfo.isReexport() ? "reexport" : "binding" } */ ${finalName}`; } catch (err) { /** @type {Error} */ (err).message += `\nwhile generating the root export '${name}' (used name: '${used}')`; throw err; } }); } // #endregion // #region [6] EMIT EXPORTS: first thing in the output — the exports getters // must exist before any body runs, so circular importers see them const result = new ConcatSource(); // add harmony compatibility flag (must be first because of possible circular dependencies) let shouldAddHarmonyFlag = false; const rootExportsInfo = moduleGraph.getExportsInfo(this); if ( rootExportsInfo.otherExportsInfo.getUsed(runtime) !== UsageState.Unused || rootExportsInfo.getReadOnlyExportInfo("__esModule").getUsed(runtime) !== UsageState.Unused ) { shouldAddHarmonyFlag = true; } // define exports if (exportsMap.size > 0) { /** @type {string[]} */ const definitions = []; for (const [key, value] of exportsMap) { definitions.push( `\n ${propertyName(key)}: ${runtimeTemplate.returningFunction( value(requestShortener) )}` ); } runtimeRequirements.add(RuntimeGlobals.exports); runtimeRequirements.add(RuntimeGlobals.definePropertyGetters); if (shouldAddHarmonyFlag) { result.add("// ESM COMPAT FLAG\n"); result.add( runtimeTemplate.defineEsModuleFlagStatement({ exportsArgument: this.exportsArgument, runtimeRequirements }) ); } const exportsSource = "\n// EXPORTS\n" + `${RuntimeGlobals.definePropertyGetters}(${ this.exportsArgument }, {${definitions.join(",")}\n});\n`; const { onDemandExportsGeneration } = ConcatenatedModule.getCompilationHooks(this.compilation); if ( !onDemandExportsGeneration.call( this, runtimes, exportsSource, exportsFinalName ) ) { result.add(exportsSource); } } // list unused exports if (unusedExports.size > 0) { result.add( `\n// UNUSED EXPORTS: ${joinIterableWithComma(unusedExports)}\n` ); } // list inlined exports if (inlinedExports.size > 0) { result.add( `\n// INLINED EXPORTS: ${joinIterableWithComma(inlinedExports)}\n` ); } // #endregion // #region [7] BUILD NS: namespace object sources are built as strings here // and emitted by [8]; an external's is emitted next to its import variable // or accessor instead, which is defined later. // // generate decoupled namespace objects for escaping mangled namespaces: // same getters as the regular namespace object, but keyed by the original // export names so untracked access by name keeps working. Done before the // regular namespace objects so any nested namespace they pull in is built. /** @type {Map<ConcatenatedModuleInfo | ExternalModuleInfo, string>} */ const escapeNamespaceObjectSources = new Map(); for (const info of neededEscapeNamespaceObjects) { /** @type {string[]} */ const nsObj = []; const exportsInfo = moduleGraph.getExportsInfo(info.module); for (const exportInfo of exportsInfo.orderedExports) { if (exportInfo.provided === false) continue; const usedName = exportInfo.getUsedName(undefined, runtime); if (!usedName) continue; if (usedName instanceof InlinedUsedName) { nsObj.push( `\n ${propertyName( exportInfo.name )}: ${runtimeTemplate.returningFunction( usedName.render( Template.toNormalComment( `inlined export ${propertyAccess([exportInfo.name])}` ) ) )}` ); } else { // External (non-concatenated) modules are accessed through their // import variable; concatenated ones resolve to an internal binding. const finalName = info.type === "external" ? `${getExternalModuleExpr(info)}${propertyAccess([usedName])}` : getFinalName( moduleGraph, info, [exportInfo.name], moduleToInfoMap, runtime, requestShortener, runtimeTemplate, neededNamespaceObjects, false, false, undefined, /** @type {BuildMeta} */ (info.module.buildMeta).strictHarmonyModule, true ); nsObj.push( `\n ${propertyName( exportInfo.name )}: ${runtimeTemplate.returningFunction(finalName)}` ); } } const name = /** @type {string} */ (info.escapeNamespaceObjectName); const defineGetters = nsObj.length > 0 ? `${RuntimeGlobals.definePropertyGetters}(${name}, {${nsObj.join( "," )}\n});\n` : ""; if (nsObj.length > 0) { runtimeRequirements.add(RuntimeGlobals.definePropertyGetters); } escapeNamespaceObjectSources.set( info, ` // NAMESPACE OBJECT (decoupled): ${info.module.readableIdentifier( requestShortener )} var ${name} = {}; ${RuntimeGlobals.makeNamespaceObject}(${name}); ${defineGetters}` ); runtimeRequirements.add(RuntimeGlobals.makeNamespaceObject); } // generate namespace objects /** @type {Map<ConcatenatedModuleInfo, string>} */ const namespaceObjectSources = new Map(); for (const info of neededNamespaceObjects) { if ( info.namespaceExportSymbol || // Skip constructing namespace object generated for wrapped module info.wrapped ) { continue; } /** @type {string[]} */ const nsObj = []; const exportsInfo = moduleGraph.getExportsInfo(info.module); for (const exportInfo of exportsInfo.orderedExports) { if (exportInfo.provided === false) continue; const usedName = exportInfo.getUsedName(undefined, runtime); if (usedName) { // TODO: Replace with the inlined value directly at the call site if (usedName instanceof InlinedUsedName) { nsObj.push( `\n ${propertyName( exportInfo.name )}: ${runtimeTemplate.returningFunction( usedName.render( Template.toNormalComment( `inlined export ${propertyAccess([exportInfo.name])}` ) ) )}` ); } else { const finalName = getFinalName( moduleGraph, info, [exportInfo.name], moduleToInfoMap, runtime, requestShortener, runtimeTemplate, neededNamespaceObjects, false, false, undefined, /** @type {BuildMeta} */ (info.module.buildMeta).strictHarmonyModule, true ); nsObj.push( `\n ${propertyName( usedName )}: ${runtimeTemplate.returningFunction(finalName)}` ); } } } const name = info.namespaceObjectName; const defineGetters = nsObj.length > 0 ? `${RuntimeGlobals.definePropertyGetters}(${name}, {${nsObj.join( "," )}\n});\n` : ""; if (nsObj.length > 0) { runtimeRequirements.add(RuntimeGlobals.definePropertyGetters); } namespaceObjectSources.set( info, ` // NAMESPACE OBJECT: ${info.module.readableIdentifier(requestShortener)} var ${name} = {}; ${RuntimeGlobals.makeNamespaceObject}(${name}); ${defineGetters}` ); runtimeRequirements.add(RuntimeGlobals.makeNamespaceObject); } // #endregion // #region [8] DEFINE: define required namespace objects and wrapper // (must be before evaluation modules — a cycle member calls another's // accessor before that module's own position in the order) // wrapper accessors are only called later, so their definitions may be // emitted in a stable order of their own instead of concatenation order /** @type {(ConcatenatedModuleInfo | ExternalModuleInfo)[]} */ const wrappedInfos = []; for (const info of modulesWithInfo) { if ( (info.type === "concatenated" || info.type === "external") && info.wrapped ) { wrappedInfos.push(info); } } const compareWrappedModules = compareModulesByFullName( runtimeTemplate.compilation.compiler ); wrappedInfos.sort((a, b) => compareWrappedModules(a.module, b.module)); for (const info of wrappedInfos) { if (info.type === "concatenated") { runtimeRequirements.add(RuntimeGlobals.concatenationWrap); result.add( `\n// MODULE: ${info.module.readableIdentifier( requestShortener )}\nvar ${info.namespaceFnName} = /*#__PURE__*/${ RuntimeGlobals.concatenationWrap }(function(${info.module.moduleArgument}, ${ info.module.exportsArgument }) {\n` ); result.add(/** @type {ReplaceSource} */ (info.source)); // A generated body (json, asset, html) binds its value to a local // instead of writing exports; hand that local to the accessor. The // name is verbatim: a wrapped body is not scope-analysed, so it keeps // the reserved symbol the generator registered. result.add( info.namespaceExportSymbol ? `\n${info.module.moduleArgument}.exports = ${info.namespaceExportSymbol};\n});\n` : "\n});\n" ); // its getters call the accessor lazily, so this may sit right after it const escapeSource = escapeNamespaceObjectSources.get(info); if (escapeSource) result.add(escapeSource); } else if (info.type === "external") { runtimeRequirements.add(RuntimeGlobals.require); const { runtimeCondition } = info; const condition = runtimeTemplate.runtimeConditionExpression({ chunkGraph, runtimeCondition, runtime, runtimeRequirements }); const moduleId = JSON.stringify(chunkGraph.getModuleId(info.module)); const body = condition === "true" ? `return ${RuntimeGlobals.require}(${moduleId});` : `if (${condition}) return ${RuntimeGlobals.require}(${moduleId});`; result.add( `\n// EXTERNAL MODULE: ${info.module.readableIdentifier( requestShortener )}\nvar ${info.namespaceFnName} = ${runtimeTemplate.basicFunction( "", body )};\n` ); const escapeSource = escapeNamespaceObjectSources.get(info); if (escapeSource) result.add(escapeSource); } } for (const info of modulesWithInfo) { if (info.type === "concatenated") { if (!info.wrapped) { const source = namespaceObjectSources.get(info); if (source) result.add(source); const escapeSource = escapeNamespaceObjectSources.get( /** @type {ConcatenatedModuleInfo} */ (info) ); if (escapeSource) result.add(escapeSource); } } else if (info.type === "external" && info.deferred) { const moduleId = JSON.stringify(chunkGraph.getModuleId(info.module)); const loader = getOptimizedDeferredModule( moduleId, getExportsType( moduleGraph, info, /** @type {BuildMeta} */ (this.rootModule.buildMeta).strictHarmonyModule ), // an async module will opt-out of the concat module optimization. [], // A closure member absorbed into this concatenation shares this // module's runtime id (and thus its `evaluating` flag); resolve it // to that id instead of the member's now-absent standalone id. getDeferredCycleModuleIds( getDeferredCycleModules(moduleGraph, info.module), (mod) => moduleToInfoMap.has(mod) ? chunkGraph.getModuleId(this) : chunkGraph.getModuleId(mod) ), runtimeRequirements ); runtimeRequirements.add(RuntimeGlobals.require); result.add( `\n// DEFERRED EXTERNAL MODULE: ${info.module.readableIdentifier( requestShortener )}\nvar ${info.deferredName} = ${loader};` ); if (info.deferredNamespaceObjectUsed) { runtimeRequirements.add(RuntimeGlobals.makeDeferredNamespaceObject); result.add( `\nvar ${info.deferredNamespaceObjectName} = /*#__PURE__*/${ RuntimeGlobals.makeDeferredNamespaceObject }(${JSON.stringify( chunkGraph.getModuleId(info.module) )}, ${getMakeDeferredNamespaceModeFromExportsType( getExportsType(moduleGraph, info, strictHarmonyModule) )});` ); } } } // #endregion // #region [9] EVALUATE: emit the bodies in concatenation order and collect // what they need — runtime requirements, chunk init fragments, interop /** @type {InitFragment<ChunkRenderContext>[]} */ const chunkInitFragments = []; for (const rawInfo of modulesWithInfo) { /** @type {undefined | string} */ let name; let isConditional = false; const info = rawInfo.type === "reference" ? rawInfo.target : rawInfo; switch (info.type) { case "concatenated": { name = info.namespaceObjectName; // wrapped bodies are emitted above, evaluated at their call sites if (info.wrapped && info.eager) { // This slot is the module's place in evaluation order; calling from // the importing body would run it too late. result.add( `\n;// ${info.module.readableIdentifier( requestShortener )}\n${info.namespaceFnName}();\n` ); } else if (!info.wrapped) { result.add( `\n;// ${info.module.readableIdentifier(requestShortener)}\n` ); result.add(/** @type {ReplaceSource} */ (info.source)); } if (info.chunkInitFragments) { for (const f of info.chunkInitFragments) chunkInitFragments.push(f); } if (info.runtimeRequirements) { for (const r of info.runtimeRequirements) { // Wrapped module supplies module/exports/this itself if ( info.wrapped && (r === RuntimeGlobals.module || r === RuntimeGlobals.exports || r === RuntimeGlobals.thisAsExports) ) { continue; } runtimeRequirements.add(r); } } break; } case "external": { name = getExternalModuleExpr(info); // deferred and wrapped cases are handled in the "const info of modulesWithInfo" loop above if (!info.deferred && !info.wrapped) { result.add( `\n// EXTERNAL MODULE: ${info.module.readableIdentifier( requestShortener )}\n` ); runtimeRequirements.add(RuntimeGlobals.require); const { runtimeCondition } = /** @type {ExternalModuleInfo | ReferenceToModuleInfo} */ (rawInfo); const condition = runtimeTemplate.runtimeConditionExpression({ chunkGraph, runtimeCondition, runtime, runtimeRequirements }); if (condition !== "true") { isConditional = true; result.add(`if (${condition}) {\n`); } const moduleId = JSON.stringify( chunkGraph.getModuleId(info.module) ); // External module bindings may be wrapped in // runtime-condition `if` blocks but referenced outside, // so they must remain function-scoped (`var`). result.add(`var ${info.name} = __webpack_require__(${moduleId});`); // Decoupled namespace object for an escaping mangled external // module must come after its import variable is defined. const escapeSource = escapeNamespaceObjectSources.get(info); if (escapeSource) result.add(escapeSource); } else if (info.wrapped && info.eager) { result.add( `\n;// EXTERNAL MODULE: ${info.module.readableIdentifier( requestShortener )}\n${info.namespaceFnName}();\n` ); } // If a module is deferred in other places, but used as non-deferred here, // the module itself will be emitted as mod_deferred (in the case "external"), // we need to emit an extra import declaration to evaluate it in order. const { nonDeferAccess } = /** @type {ExternalModuleInfo | ReferenceToModuleInfo} */ (rawInfo); if (info.deferred && nonDeferAccess && !info.wrapped) { result.add( `\n// non-deferred import to a deferred module (${info.module.readableIdentifier( requestShortener )})\nvar ${info.name} = ${info.deferredName}.a;` ); } break; } default: // @ts-expect-error never is expected here throw new Error(`Unsupported concatenation entry type ${info.type}`); } // Building a wrapped module's interop object here would evaluate its body // at the top level and break require() cycles, so defer it behind a // hoisted function. The memo lives on the function object: it keeps the // identity stable without claiming a second top-level name. /** * @param {string} interopName interop variable name * @param {string} expr interop object expression * @returns {string} the declaration */ const declareInterop = (interopName, expr) => info.wrapped ? `\nfunction ${interopName}() { return ${interopName}.c || (${interopName}.c = ${expr}); }` : `\nvar ${interopName} = /*#__PURE__*/${expr};`; if (info.interopNamespaceObjectUsed) { runtimeRequirements.add(RuntimeGlobals.createFakeNamespaceObject); result.add( declareInterop( /** @type {string} */ (info.interopNamespaceObjectName), `${RuntimeGlobals.createFakeNamespaceObject}(${name}, 2)` ) ); } if (info.interopNamespaceObject2Used) { runtimeRequirements.add(RuntimeGlobals.createFakeNamespaceObject); result.add( declareInterop( /** @type {string} */ (info.interopNamespaceObject2Name), `${RuntimeGlobals.createFakeNamespaceObject}(${name})` ) ); } if (info.interopDefaultAccessUsed) { runtimeRequirements.add(RuntimeGlobals.compatGetDefaultExport); result.add( declareInterop( /** @type {string} */ (info.interopDefaultAccessName), `${RuntimeGlobals.compatGetDefaultExport}(${name})` ) ); } if (isConditional) { result.add("\n}"); } } // #endregion // #region [10] RESULT: topLevelDeclarations and freeNames let consumers // (mangling, an outer concatenation) reason about this source unparsed /** @type {CodeGenerationResultData} */ const data = new Map(); if (chunkInitFragments.length > 0) { data.set("chunkInitFragments", chunkInitFragments); } data.set("topLevelDeclarations", topLevelDeclarations); data.set("freeNames", freeNames); /** @type {CodeGenerationResult} */ const resultEntry = { sources: new Map([[JAVASCRIPT_TYPE, new CachedSource(result)]]), data, runtimeRequirements }; // #endregion return resultEntry; } /** * @param {ModuleToInfoMap} modulesMap modulesMap * @param {ModuleInfo} info info * @param {DependencyTemplates} dependencyTemplates dependencyTemplates * @param {RuntimeTemplate} runtimeTemplate runtimeTemplate * @param {ModuleGraph} moduleGraph moduleGraph * @param {ChunkGraph} chunkGraph chunkGraph * @param {RuntimeSpec} runtime runtime * @param {RuntimeSpec[]} runtimes runtimes * @param {CodeGenerationResults} codeGenerationResults codeGenerationResults * @param {UsedNames} usedNames used names */ _analyseModule( modulesMap, info, dependencyTemplates, runtimeTemplate, moduleGraph, chunkGraph, runtime, runtimes, codeGenerationResults, usedNames ) { if (info.type === "concatenated") { const m = info.module; if (info.wrapped) { // the wrapper isolates the body's top-level declarations, so no // renaming is needed -- only a concatenation scope, to rewrite in-scope // `require()` targets to accessor references. [2] claims its identifiers // textually in place of the scope analysis skipped here. const concatenationScope = new ConcatenationScope( modulesMap, info, usedNames ); info.concatenationScope = concatenationScope; const codeGenResult = m.codeGeneration({ dependencyTemplates, runtimeTemplate, moduleGraph, chunkGraph, runtime, runtimes, concatenationScope, codeGenerationResults, sourceTypes: JAVASCRIPT_TYPES }); const source = /** @type {Source} */ (codeGenResult.sources.get(JAVASCRIPT_TYPE)); const data = codeGenResult.data; info.runtimeRequirements = /** @type {ReadOnlyRuntimeRequirements} */ (codeGenResult.runtimeRequirements); info.internalSource = source; info.source = new ReplaceSource(source); info.chunkInitFragments = data && data.get("chunkInitFragments"); } else { try { // Create a concatenation scope to track and capture information const concatenationScope = new ConcatenationScope( modulesMap, info, usedNames ); // Not cached: Compilation memoizes the outer codeGeneration per // (module, runtime), so inner generation never recomputes usefully. const codeGenResult = m.codeGeneration({ dependencyTemplates, runtimeTemplate, moduleGraph, chunkGraph, runtime, runtimes, concatenationScope, codeGenerationResults, sourceTypes: JAVASCRIPT_TYPES }); const source = /** @type {Source} */ (codeGenResult.sources.get(JAVASCRIPT_TYPE)); const data = codeGenResult.data; const chunkInitFragments = data && data.get("chunkInitFragments"); const code = source.source().toString(); /** @type {Program} */ let ast; try { const { experiments } = this.compilation.options; ({ ast } = JavascriptParser._parse( code, { sourceType: "module", ranges: true, // generated code contains phase imports when the experiments are on importPhases: Boolean( experiments.deferImport || experiments.sourceImport ) }, JavascriptParser._getModuleParseFunction(this.compilation, m) )); } catch (_err) { const err = /** @type {Error & { loc?: { line: number, column: number } }} */ (_err); if ( err.loc && typeof err.loc === "object" && typeof err.loc.line === "number" ) { const lineNumber = err.loc.line; const lines = code.split("\n"); err.message += `\n| ${lines .slice(Math.max(0, lineNumber - 3), lineNumber + 2) .join("\n| ")}`; } throw err; } const scopeManager = eslintScope.analyze(ast, { ecmaVersion: 6, sourceType: "module", optimistic: true, ignoreEval: true, impliedStrict: true }); const globalScope = /** @type {Scope} */ (scopeManager.acquire(ast)); const moduleScope = globalScope.childScopes[0]; const resultSource = new ReplaceSource(source); info.runtimeRequirements = /** @type {ReadOnlyRuntimeRequirements} */ (codeGenResult.runtimeRequirements); info.ast = ast; info.internalSource = source; info.source = resultSource; info.chunkInitFragments = chunkInitFragments; info.globalScope = globalScope; info.moduleScope = moduleScope; info.concatenationScope = concatenationScope; } catch (err) { /** @type {Error} */ (err).message += `\nwhile analyzing module ${m.identifier()} for concatenation`; throw err; } } } } /** * @param {ModuleGraph} moduleGraph the module graph * @param {RuntimeSpec} runtime the runtime * @returns {[ModuleInfoOrReference[], ModuleToInfoMap]} module info items */ _getModulesWithInfo(moduleGraph, runtime) { const orderedConcatenationList = this._createConcatenationList( this.rootModule, this._modules, runtime, moduleGraph ); /** @type {ModuleToInfoMap} */ const map = new Map(); const list = orderedConcatenationList.map((info, index) => { let item = map.get(info.module); if (item === undefined) { switch (info.type) { case "concatenated": item = { type: "concatenated", wrapped: info.wrapped, eager: false, module: info.module, index, namespaceFnName: undefined, ast: undefined, chunkInitFragments: undefined, internalSource: undefined, runtimeRequirements: undefined, source: undefined, globalScope: undefined, moduleScope: undefined, internalNames: new Map(), exportMap: undefined, rawExportMap: undefined, namespaceExportSymbol: undefined, namespaceObjectName: undefined, escapeNamespaceObjectName: undefined, interopNamespaceObjectUsed: false, interopNamespaceObjectName: undefined, interopNamespaceObject2Used: false, interopNamespaceObject2Name: undefined, interopDefaultAccessUsed: false, interopDefaultAccessName: undefined, concatenationScope: undefined, exportsTypeStrict: undefined, exportsTypeNonStrict: undefined }; break; case "external": item = { type: "external", wrapped: info.wrapped, eager: false, module: info.module, runtimeCondition: info.runtimeCondition, nonDeferAccess: info.nonDeferAccess, index, name: undefined, namespaceFnName: undefined, escapeNamespaceObjectName: undefined, deferredName: undefined, interopNamespaceObjectUsed: false, interopNamespaceObjectName: undefined, interopNamespaceObject2Used: false, interopNamespaceObject2Name: undefined, interopDefaultAccessUsed: false, interopDefaultAccessName: undefined, deferred: this.compilation.options.experiments.deferImport ? moduleGraph.isDeferred(info.module) : false, deferredNamespaceObjectName: undefined, deferredNamespaceObjectUsed: false, exportsTypeStrict: undefined, exportsTypeNonStrict: undefined }; break; default: throw new Error( `Unsupported concatenation entry type ${info.type}` ); } map.set( /** @type {ModuleInfo} */ (item).module, /** @type {ModuleInfo} */ (item) ); return /** @type {ModuleInfo} */ (item); } /** @type {ReferenceToModuleInfo} */ const ref = { type: "reference", runtimeCondition: info.runtimeCondition, nonDeferAccess: info.nonDeferAccess, target: item }; return ref; }); return [list, map]; } /** * Updates the hash with the data contributed by this instance. * @param {Hash} hash the hash used to track dependencies * @param {UpdateHashContext} context context * @returns {void} */ updateHash(hash, context) { const { chunkGraph, runtime } = context; for (const info of this._createConcatenationList( this.rootModule, this._modules, intersectRuntime(runtime, this._runtime), chunkGraph.moduleGraph )) { switch (info.type) { case "concatenated": info.module.updateHash(hash, context); break; case "external": hash.update( `${chunkGraph.getModuleId(info.module)}|${runtimeConditionToString( info.runtimeCondition )}|${info.nonDeferAccess ? "1" : "0"}|${ this.compilation.options.experiments.deferImport && chunkGraph.moduleGraph.isDeferred(info.module) ? "1" : "0" }` ); break; } } super.updateHash(hash, context); } /** * @param {ObjectDeserializerContext} context context * @returns {ConcatenatedModule} ConcatenatedModule */ static deserialize(context) { const obj = new ConcatenatedModule({ identifier: /** @type {EXPECTED_ANY} */ (undefined), rootModule: /** @type {EXPECTED_ANY} */ (undefined), modules: /** @type {EXPECTED_ANY} */ (undefined), runtime: undefined, compilation: /** @type {EXPECTED_ANY} */ (undefined) }); obj.deserialize(context); return obj; } } ConcatenatedModule.getCompilationHooks = createHooksRegistry( () => /** @type {ConcatenateModuleHooks} */ ({ onDemandExportsGeneration: new SyncBailHook([ "module", "runtimes", "exportsFinalName", "exportsSource" ]), concatenatedModuleInfo: new SyncBailHook([ "updatedInfo", "concatenatedModuleInfo" ]) }) ); makeSerializable(ConcatenatedModule, "webpack/lib/optimize/ConcatenatedModule"); module.exports = ConcatenatedModule;