/* 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 { CachedSource, ConcatSource, ReplaceSource } = require("webpack-sources"); const ConcatenationScope = require("../ConcatenationScope"); const { UsageState } = require("../ExportsInfo"); const Module = require("../Module"); const { JAVASCRIPT_MODULE_TYPE_ESM } = require("../ModuleTypeConstants"); const RuntimeGlobals = require("../RuntimeGlobals"); const Template = require("../Template"); const HarmonyImportDependency = require("../dependencies/HarmonyImportDependency"); const JavascriptParser = require("../javascript/JavascriptParser"); const { equals } = require("../util/ArrayHelpers"); const LazySet = require("../util/LazySet"); const { concatComparators } = require("../util/comparators"); const createHash = require("../util/createHash"); const { makePathsRelative } = require("../util/identifier"); const makeSerializable = require("../util/makeSerializable"); const propertyAccess = require("../util/propertyAccess"); const { propertyName } = require("../util/propertyName"); const { filterRuntime, intersectRuntime, mergeRuntimeCondition, mergeRuntimeConditionNonFalse, runtimeConditionToString, subtractRuntimeCondition } = require("../util/runtime"); /** @typedef {import("eslint-scope").Scope} Scope */ /** @typedef {import("webpack-sources").Source} Source */ /** @typedef {import("../../declarations/WebpackOptions").WebpackOptionsNormalized} WebpackOptions */ /** @typedef {import("../ChunkGraph")} ChunkGraph */ /** @typedef {import("../CodeGenerationResults")} CodeGenerationResults */ /** @typedef {import("../Compilation")} Compilation */ /** @typedef {import("../Dependency")} Dependency */ /** @typedef {import("../Dependency").UpdateHashContext} UpdateHashContext */ /** @typedef {import("../DependencyTemplate").DependencyTemplateContext} DependencyTemplateContext */ /** @typedef {import("../DependencyTemplates")} DependencyTemplates */ /** @typedef {import("../ExportsInfo").ExportInfo} ExportInfo */ /** @template T @typedef {import("../InitFragment")} InitFragment */ /** @typedef {import("../Module").CodeGenerationContext} CodeGenerationContext */ /** @typedef {import("../Module").CodeGenerationResult} CodeGenerationResult */ /** @typedef {import("../Module").LibIdentOptions} LibIdentOptions */ /** @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("../WebpackError")} WebpackError */ /** @typedef {import("../javascript/JavascriptModulesPlugin").ChunkRenderContext} ChunkRenderContext */ /** @typedef {import("../util/Hash")} Hash */ /** @typedef {typeof import("../util/Hash")} HashConstructor */ /** @typedef {import("../util/fs").InputFileSystem} InputFileSystem */ /** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */ // 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 {Object} ReexportInfo * @property {Module} module * @property {string[]} export */ /** @typedef {RawBinding | SymbolBinding} Binding */ /** * @typedef {Object} RawBinding * @property {ModuleInfo} info * @property {string} rawName * @property {string=} comment * @property {string[]} ids * @property {string[]} exportName */ /** * @typedef {Object} SymbolBinding * @property {ConcatenatedModuleInfo} info * @property {string} name * @property {string=} comment * @property {string[]} ids * @property {string[]} exportName */ /** @typedef {ConcatenatedModuleInfo | ExternalModuleInfo } ModuleInfo */ /** @typedef {ConcatenatedModuleInfo | ExternalModuleInfo | ReferenceToModuleInfo } ModuleInfoOrReference */ /** * @typedef {Object} ConcatenatedModuleInfo * @property {"concatenated"} type * @property {Module} module * @property {number} index * @property {Object} ast * @property {Source} internalSource * @property {ReplaceSource} source * @property {InitFragment[]=} chunkInitFragments * @property {Iterable} runtimeRequirements * @property {Scope} globalScope * @property {Scope} moduleScope * @property {Map} internalNames * @property {Map} exportMap * @property {Map} rawExportMap * @property {string=} namespaceExportSymbol * @property {string} namespaceObjectName * @property {boolean} interopNamespaceObjectUsed * @property {string} interopNamespaceObjectName * @property {boolean} interopNamespaceObject2Used * @property {string} interopNamespaceObject2Name * @property {boolean} interopDefaultAccessUsed * @property {string} interopDefaultAccessName */ /** * @typedef {Object} ExternalModuleInfo * @property {"external"} type * @property {Module} module * @property {RuntimeSpec | boolean} runtimeCondition * @property {number} index * @property {string} name * @property {boolean} interopNamespaceObjectUsed * @property {string} interopNamespaceObjectName * @property {boolean} interopNamespaceObject2Used * @property {string} interopNamespaceObject2Name * @property {boolean} interopDefaultAccessUsed * @property {string} interopDefaultAccessName */ /** * @typedef {Object} ReferenceToModuleInfo * @property {"reference"} type * @property {RuntimeSpec | boolean} runtimeCondition * @property {ConcatenatedModuleInfo | ExternalModuleInfo} target */ const RESERVED_NAMES = new Set( [ // internal names (should always be renamed) ConcatenationScope.DEFAULT_EXPORT, ConcatenationScope.NAMESPACE_OBJECT_EXPORT, // keywords "abstract,arguments,async,await,boolean,break,byte,case,catch,char,class,const,continue", "debugger,default,delete,do,double,else,enum,eval,export,extends,false,final,finally,float", "for,function,goto,if,implements,import,in,instanceof,int,interface,let,long,native,new,null", "package,private,protected,public,return,short,static,super,switch,synchronized,this,throw", "throws,transient,true,try,typeof,var,void,volatile,while,with,yield", // commonjs/amd "module,__dirname,__filename,exports,require,define", // js globals "Array,Date,eval,function,hasOwnProperty,Infinity,isFinite,isNaN,isPrototypeOf,length,Math", "NaN,name,Number,Object,prototype,String,toString,undefined,valueOf", // browser globals "alert,all,anchor,anchors,area,assign,blur,button,checkbox,clearInterval,clearTimeout", "clientInformation,close,closed,confirm,constructor,crypto,decodeURI,decodeURIComponent", "defaultStatus,document,element,elements,embed,embeds,encodeURI,encodeURIComponent,escape", "event,fileUpload,focus,form,forms,frame,innerHeight,innerWidth,layer,layers,link,location", "mimeTypes,navigate,navigator,frames,frameRate,hidden,history,image,images,offscreenBuffering", "open,opener,option,outerHeight,outerWidth,packages,pageXOffset,pageYOffset,parent,parseFloat", "parseInt,password,pkcs11,plugin,prompt,propertyIsEnum,radio,reset,screenX,screenY,scroll", "secure,select,self,setInterval,setTimeout,status,submit,taint,text,textarea,top,unescape", "untaint,window", // window events "onblur,onclick,onerror,onfocus,onkeydown,onkeypress,onkeyup,onmouseover,onload,onmouseup,onmousedown,onsubmit" ] .join(",") .split(",") ); const createComparator = (property, comparator) => (a, b) => comparator(a[property], b[property]); const compareNumbers = (a, b) => { if (isNaN(a)) { if (!isNaN(b)) { return 1; } } else { if (isNaN(b)) { return -1; } if (a !== b) { return a < b ? -1 : 1; } } return 0; }; const bySourceOrder = createComparator("sourceOrder", compareNumbers); const byRangeStart = createComparator("rangeStart", compareNumbers); 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 {Object} ConcatenationEntry * @property {"concatenated" | "external"} type * @property {Module} module * @property {RuntimeSpec | boolean} runtimeCondition */ /** * @param {ModuleGraph} moduleGraph the module graph * @param {ModuleInfo} info module info * @param {string[]} exportName exportName * @param {Map} moduleToInfoMap moduleToInfoMap * @param {RuntimeSpec} runtime for which runtime * @param {RequestShortener} requestShortener the request shortener * @param {RuntimeTemplate} runtimeTemplate the runtime template * @param {Set} neededNamespaceObjects modules for which a namespace object should be generated * @param {boolean} asCall asCall * @param {boolean} strictHarmonyModule strictHarmonyModule * @param {boolean | undefined} asiSafe asiSafe * @param {Set} alreadyVisited alreadyVisited * @returns {Binding} the final variable */ const getFinalBinding = ( moduleGraph, info, exportName, moduleToInfoMap, runtime, requestShortener, runtimeTemplate, neededNamespaceObjects, asCall, strictHarmonyModule, asiSafe, alreadyVisited = new Set() ) => { const exportsType = info.module.getExportsType( moduleGraph, strictHarmonyModule ); if (exportName.length === 0) { switch (exportsType) { case "default-only": info.interopNamespaceObject2Used = true; return { info, rawName: info.interopNamespaceObject2Name, ids: exportName, exportName }; case "default-with-named": info.interopNamespaceObjectUsed = true; return { info, rawName: 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); 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 }; } break; } case "dynamic": switch (exportName[0]) { case "default": { exportName = exportName.slice(1); info.interopDefaultAccessUsed = true; const defaultExport = asCall ? `${info.interopDefaultAccessName}()` : asiSafe ? `(${info.interopDefaultAccessName}())` : asiSafe === false ? `;(${info.interopDefaultAccessName}())` : `${info.interopDefaultAccessName}.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: info.namespaceObjectName, ids: exportName, exportName }; case "external": return { info, rawName: info.name, ids: exportName, exportName }; } } const exportsInfo = moduleGraph.getExportsInfo(info.module); const exportInfo = exportsInfo.getExportInfo(exportName[0]); 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: info.namespaceObjectName, ids: exportName, exportName }; } const directExport = info.exportMap && info.exportMap.get(exportId); if (directExport) { const usedName = /** @type {string[]} */ ( exportsInfo.getUsedName(exportName, runtime) ); if (!usedName) { return { info, rawName: "/* unused export */ undefined", ids: exportName.slice(1), exportName }; } return { info, name: directExport, ids: 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) { 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, refInfo, reexport.export ? [...reexport.export, ...exportName.slice(1)] : exportName.slice(1), moduleToInfoMap, runtime, requestShortener, runtimeTemplate, neededNamespaceObjects, asCall, info.module.buildMeta.strictHarmonyModule, asiSafe, alreadyVisited ); } if (info.namespaceExportSymbol) { const usedName = /** @type {string[]} */ ( exportsInfo.getUsedName(exportName, runtime) ); return { info, rawName: info.namespaceObjectName, ids: usedName, exportName }; } throw new Error( `Cannot get final name for export '${exportName.join( "." )}' of ${info.module.readableIdentifier(requestShortener)}` ); } case "external": { const used = /** @type {string[]} */ ( exportsInfo.getUsedName(exportName, runtime) ); if (!used) { return { info, rawName: "/* unused export */ undefined", ids: exportName.slice(1), exportName }; } const comment = equals(used, exportName) ? "" : Template.toNormalComment(`${exportName.join(".")}`); return { info, rawName: info.name + comment, ids: used, exportName }; } } }; /** * @param {ModuleGraph} moduleGraph the module graph * @param {ModuleInfo} info module info * @param {string[]} exportName exportName * @param {Map} moduleToInfoMap moduleToInfoMap * @param {RuntimeSpec} runtime for which runtime * @param {RequestShortener} requestShortener the request shortener * @param {RuntimeTemplate} runtimeTemplate the runtime template * @param {Set} neededNamespaceObjects modules for which a namespace object should be generated * @param {boolean} asCall asCall * @param {boolean} callContext callContext * @param {boolean} strictHarmonyModule strictHarmonyModule * @param {boolean | undefined} asiSafe asiSafe * @returns {string} the final name */ const getFinalName = ( moduleGraph, info, exportName, moduleToInfoMap, runtime, requestShortener, runtimeTemplate, neededNamespaceObjects, asCall, callContext, strictHarmonyModule, asiSafe ) => { const binding = getFinalBinding( moduleGraph, info, exportName, moduleToInfoMap, runtime, requestShortener, runtimeTemplate, neededNamespaceObjects, asCall, strictHarmonyModule, asiSafe ); { const { ids, comment } = binding; let reference; 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 && asCall && callContext === false) { return asiSafe ? `(0,${reference})` : asiSafe === false ? `;(0,${reference})` : `/*#__PURE__*/Object(${reference})`; } return reference; } }; const addScopeSymbols = (s, nameSet, scopeSet1, scopeSet2) => { let scope = s; while (scope) { if (scopeSet1.has(scope)) break; if (scopeSet2.has(scope)) break; scopeSet1.add(scope); for (const variable of scope.variables) { nameSet.add(variable.name); } scope = scope.upper; } }; const getAllReferences = variable => { let set = variable.references; // Look for inner scope variables too (like in class Foo { t() { Foo } }) const identifiers = new Set(variable.identifiers); for (const scope of variable.scope.childScopes) { for (const innerVar of scope.variables) { if (innerVar.identifiers.some(id => identifiers.has(id))) { set = set.concat(innerVar.references); break; } } } return set; }; const getPathInAst = (ast, node) => { if (ast === node) { return []; } const nr = node.range; const enterNode = n => { if (!n) return undefined; const r = n.range; if (r) { if (r[0] <= nr[0] && r[1] >= nr[1]) { const path = getPathInAst(n, node); if (path) { path.push(n); return path; } } } return undefined; }; if (Array.isArray(ast)) { for (let i = 0; i < ast.length; i++) { const enterResult = enterNode(ast[i]); if (enterResult !== undefined) return enterResult; } } else if (ast && typeof ast === "object") { const keys = Object.keys(ast); for (let i = 0; i < keys.length; i++) { const value = ast[keys[i]]; if (Array.isArray(value)) { const pathResult = getPathInAst(value, node); if (pathResult !== undefined) return pathResult; } else if (value && typeof value === "object") { const enterResult = enterNode(value); if (enterResult !== undefined) return enterResult; } } } }; const TYPES = new Set(["javascript"]); class ConcatenatedModule extends Module { /** * @param {Module} rootModule the root module of the concatenation * @param {Set} modules all modules in the concatenation (including the root module) * @param {RuntimeSpec} runtime the runtime * @param {Object=} associatedObjectForCache object for caching * @param {string | HashConstructor=} hashFunction hash function to use * @returns {ConcatenatedModule} the module */ static create( rootModule, modules, runtime, associatedObjectForCache, hashFunction = "md4" ) { const identifier = ConcatenatedModule._createIdentifier( rootModule, modules, associatedObjectForCache, hashFunction ); return new ConcatenatedModule({ identifier, rootModule, modules, runtime }); } /** * @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=} options.modules all concatenated modules */ constructor({ identifier, rootModule, modules, runtime }) { super(JAVASCRIPT_MODULE_TYPE_ESM, null, rootModule && rootModule.layer); // Info from Factory /** @type {string} */ this._identifier = identifier; /** @type {Module} */ this.rootModule = rootModule; /** @type {Set} */ this._modules = modules; this._runtime = runtime; this.factoryMeta = rootModule && rootModule.factoryMeta; } /** * 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 {SourceTypes} types available (do not mutate) */ getSourceTypes() { return TYPES; } get modules() { return Array.from(this._modules); } /** * @returns {string} a unique identifier of the module */ identifier() { return this._identifier; } /** * @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` ); } /** * @param {LibIdentOptions} options options * @returns {string | null} an identifier for library inclusion */ libIdent(options) { return this.rootModule.libIdent(options); } /** * @returns {string | null} absolute path which should be used for condition matching (usually the resource path) */ nameForCondition() { return this.rootModule.nameForCondition(); } /** * @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); } /** * @param {WebpackOptions} options webpack options * @param {Compilation} compilation the compilation * @param {ResolverWithOptions} resolver the resolver * @param {InputFileSystem} fs the file system * @param {function(WebpackError=): void} callback callback function * @returns {void} */ build(options, compilation, resolver, fs, callback) { const { rootModule } = this; this.buildInfo = { strict: true, cacheable: true, moduleArgument: rootModule.buildInfo.moduleArgument, exportsArgument: rootModule.buildInfo.exportsArgument, fileDependencies: new LazySet(), contextDependencies: new LazySet(), missingDependencies: new LazySet(), topLevelDeclarations: new Set(), assets: undefined }; this.buildMeta = rootModule.buildMeta; this.clearDependenciesAndBlocks(); this.clearWarningsAndErrors(); for (const m of this._modules) { // populate cacheable if (!m.buildInfo.cacheable) { this.buildInfo.cacheable = false; } // populate dependencies for (const d of m.dependencies.filter( dep => !(dep instanceof HarmonyImportDependency) || !this._modules.has(compilation.moduleGraph.getModule(dep)) )) { this.dependencies.push(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); } } // populate topLevelDeclarations if (m.buildInfo.topLevelDeclarations) { const topLevelDeclarations = this.buildInfo.topLevelDeclarations; if (topLevelDeclarations !== undefined) { for (const decl of m.buildInfo.topLevelDeclarations) { topLevelDeclarations.add(decl); } } } else { this.buildInfo.topLevelDeclarations = undefined; } // populate assets if (m.buildInfo.assets) { if (this.buildInfo.assets === undefined) { this.buildInfo.assets = Object.create(null); } Object.assign(this.buildInfo.assets, m.buildInfo.assets); } if (m.buildInfo.assetsInfo) { if (this.buildInfo.assetsInfo === undefined) { this.buildInfo.assetsInfo = new Map(); } for (const [key, value] of m.buildInfo.assetsInfo) { this.buildInfo.assetsInfo.set(key, value); } } } callback(); } /** * @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} 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) { /** @type {ConcatenationEntry[]} */ const list = []; /** @type {Map} */ const existingEntries = new Map(); /** * @param {Module} module a module * @returns {Iterable<{ connection: ModuleGraphConnection, runtimeCondition: RuntimeSpec | true }>} imported modules in order */ const getConcatenatedImports = module => { let connections = Array.from(moduleGraph.getOutgoingConnections(module)); if (module === rootModule) { for (const c of moduleGraph.getOutgoingConnections(this)) connections.push(c); } /** * @type {Array<{ connection: ModuleGraphConnection, sourceOrder: number, rangeStart: number }>} */ const references = connections .filter(connection => { if (!(connection.dependency instanceof HarmonyImportDependency)) return false; return ( connection && connection.resolvedOriginModule === module && connection.module && connection.isTargetActive(runtime) ); }) .map(connection => { const dep = /** @type {HarmonyImportDependency} */ ( connection.dependency ); return { connection, sourceOrder: dep.sourceOrder, rangeStart: dep.range && dep.range[0] }; }); /** * 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} */ const referencesMap = new Map(); for (const { connection } of references) { const runtimeCondition = filterRuntime(runtime, r => connection.isTargetActive(r) ); if (runtimeCondition === false) continue; const module = connection.module; const entry = referencesMap.get(module); if (entry === undefined) { referencesMap.set(module, { connection, runtimeCondition }); continue; } entry.runtimeCondition = mergeRuntimeConditionNonFalse( entry.runtimeCondition, runtimeCondition, runtime ); } return referencesMap.values(); }; /** * @param {ModuleGraphConnection} connection graph connection * @param {RuntimeSpec | true} runtimeCondition runtime condition * @returns {void} */ const enterModule = (connection, runtimeCondition) => { const module = connection.module; if (!module) return; const existingEntry = existingEntries.get(module); if (existingEntry === true) { return; } if (modulesSet.has(module)) { existingEntries.set(module, 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.` ); } const imports = getConcatenatedImports(module); for (const { connection, runtimeCondition } of imports) enterModule(connection, runtimeCondition); list.push({ type: "concatenated", module: connection.module, runtimeCondition }); } else { if (existingEntry !== undefined) { const reducedRuntimeCondition = subtractRuntimeCondition( runtimeCondition, existingEntry, runtime ); if (reducedRuntimeCondition === false) return; runtimeCondition = reducedRuntimeCondition; existingEntries.set( connection.module, mergeRuntimeConditionNonFalse( existingEntry, runtimeCondition, runtime ) ); } else { existingEntries.set(connection.module, runtimeCondition); } if (list.length > 0) { const lastItem = list[list.length - 1]; if ( lastItem.type === "external" && lastItem.module === connection.module ) { lastItem.runtimeCondition = mergeRuntimeCondition( lastItem.runtimeCondition, runtimeCondition, runtime ); return; } } list.push({ type: "external", 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 }); } }; existingEntries.set(rootModule, true); const imports = getConcatenatedImports(rootModule); for (const { connection, runtimeCondition } of imports) enterModule(connection, runtimeCondition); list.push({ type: "concatenated", module: rootModule, runtimeCondition: true }); return list; } /** * @param {Module} rootModule the root module of the concatenation * @param {Set} modules all modules in the concatenation (including the root module) * @param {Object=} associatedObjectForCache object for caching * @param {string | HashConstructor=} hashFunction hash function to use * @returns {string} the identifier */ static _createIdentifier( rootModule, modules, associatedObjectForCache, hashFunction = "md4" ) { const cachedMakePathsRelative = makePathsRelative.bindContextCache( rootModule.context, associatedObjectForCache ); let 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"); } /** * @param {LazySet} fileDependencies set where file dependencies are added to * @param {LazySet} contextDependencies set where context dependencies are added to * @param {LazySet} missingDependencies set where missing dependencies are added to * @param {LazySet} 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 ); } } /** * @param {CodeGenerationContext} context context for code generation * @returns {CodeGenerationResult} result */ codeGeneration({ dependencyTemplates, runtimeTemplate, moduleGraph, chunkGraph, runtime: generationRuntime, codeGenerationResults }) { /** @type {Set} */ const runtimeRequirements = new Set(); const runtime = intersectRuntime(generationRuntime, this._runtime); const requestShortener = runtimeTemplate.requestShortener; // Meta info for each module const [modulesWithInfo, moduleToInfoMap] = this._getModulesWithInfo( moduleGraph, runtime ); // Set with modules that need a generated namespace object /** @type {Set} */ const neededNamespaceObjects = new Set(); // Generate source code and analyse scopes // Prepare a ReplaceSource for the final source for (const info of moduleToInfoMap.values()) { this._analyseModule( moduleToInfoMap, info, dependencyTemplates, runtimeTemplate, moduleGraph, chunkGraph, runtime, codeGenerationResults ); } // List of all used names to avoid conflicts const allUsedNames = new Set(RESERVED_NAMES); // Updated Top level declarations are created by renaming const topLevelDeclarations = new Set(); // List of additional names in scope for module references /** @type {Map, alreadyCheckedScopes: Set }>} */ const usedNamesInScopeInfo = new Map(); /** * @param {string} module module identifier * @param {string} id export id * @returns {{ usedNames: Set, alreadyCheckedScopes: Set }} info */ const getUsedNamesInScopeInfo = (module, id) => { const key = `${module}-${id}`; let info = usedNamesInScopeInfo.get(key); if (info === undefined) { info = { usedNames: new Set(), alreadyCheckedScopes: new Set() }; usedNamesInScopeInfo.set(key, info); } return info; }; // Set of already checked scopes const ignoredScopes = new Set(); // get all global names for (const info of modulesWithInfo) { if (info.type === "concatenated") { // 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 const superClassCache = new WeakMap(); const getSuperClassExpressions = scope => { const cacheEntry = superClassCache.get(scope); if (cacheEntry !== undefined) return cacheEntry; 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: 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"); const binding = getFinalBinding( moduleGraph, referencedInfo, match.ids, moduleToInfoMap, runtime, requestShortener, runtimeTemplate, neededNamespaceObjects, false, info.module.buildMeta.strictHarmonyModule, true ); if (!binding.ids) continue; const { usedNames, alreadyCheckedScopes } = getUsedNamesInScopeInfo( binding.info.module.identifier(), "name" in binding ? binding.name : "" ); for (const expr of getSuperClassExpressions(reference.from)) { if ( expr.range[0] <= reference.identifier.range[0] && expr.range[1] >= reference.identifier.range[1] ) { for (const variable of expr.variables) { usedNames.add(variable.name); } } } addScopeSymbols( reference.from, usedNames, alreadyCheckedScopes, ignoredScopes ); } else { allUsedNames.add(name); } } } } } // generate names for symbols for (const info of moduleToInfoMap.values()) { const { usedNames: namespaceObjectUsedNames } = getUsedNamesInScopeInfo( info.module.identifier(), "" ); switch (info.type) { case "concatenated": { for (const variable of info.moduleScope.variables) { const name = variable.name; const { usedNames, alreadyCheckedScopes } = getUsedNamesInScopeInfo( info.module.identifier(), name ); if (allUsedNames.has(name) || usedNames.has(name)) { const references = getAllReferences(variable); for (const ref of references) { addScopeSymbols( ref.from, usedNames, alreadyCheckedScopes, ignoredScopes ); } const newName = this.findNewName( name, allUsedNames, usedNames, info.module.readableIdentifier(requestShortener) ); allUsedNames.add(newName); info.internalNames.set(name, newName); topLevelDeclarations.add(newName); const source = info.source; const allIdentifiers = new Set( references.map(r => r.identifier).concat(variable.identifiers) ); for (const identifier of allIdentifiers) { const r = identifier.range; const path = getPathInAst(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); } } let namespaceObjectName; if (info.namespaceExportSymbol) { namespaceObjectName = info.internalNames.get( info.namespaceExportSymbol ); } else { namespaceObjectName = this.findNewName( "namespaceObject", allUsedNames, namespaceObjectUsedNames, info.module.readableIdentifier(requestShortener) ); allUsedNames.add(namespaceObjectName); } info.namespaceObjectName = namespaceObjectName; topLevelDeclarations.add(namespaceObjectName); break; } case "external": { const externalName = this.findNewName( "", allUsedNames, namespaceObjectUsedNames, info.module.readableIdentifier(requestShortener) ); allUsedNames.add(externalName); info.name = externalName; topLevelDeclarations.add(externalName); break; } } if (info.module.buildMeta.exportsType !== "namespace") { const externalNameInterop = this.findNewName( "namespaceObject", allUsedNames, namespaceObjectUsedNames, info.module.readableIdentifier(requestShortener) ); allUsedNames.add(externalNameInterop); info.interopNamespaceObjectName = externalNameInterop; topLevelDeclarations.add(externalNameInterop); } if ( info.module.buildMeta.exportsType === "default" && info.module.buildMeta.defaultObject !== "redirect" ) { const externalNameInterop = this.findNewName( "namespaceObject2", allUsedNames, namespaceObjectUsedNames, info.module.readableIdentifier(requestShortener) ); allUsedNames.add(externalNameInterop); info.interopNamespaceObject2Name = externalNameInterop; topLevelDeclarations.add(externalNameInterop); } if ( info.module.buildMeta.exportsType === "dynamic" || !info.module.buildMeta.exportsType ) { const externalNameInterop = this.findNewName( "default", allUsedNames, namespaceObjectUsedNames, info.module.readableIdentifier(requestShortener) ); allUsedNames.add(externalNameInterop); info.interopDefaultAccessName = externalNameInterop; topLevelDeclarations.add(externalNameInterop); } } // Find and replace references to modules for (const info of moduleToInfoMap.values()) { if (info.type === "concatenated") { for (const reference of info.globalScope.through) { const name = reference.identifier.name; 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 finalName = getFinalName( moduleGraph, referencedInfo, match.ids, moduleToInfoMap, runtime, requestShortener, runtimeTemplate, neededNamespaceObjects, match.call, !match.directImport, info.module.buildMeta.strictHarmonyModule, match.asiSafe ); const r = reference.identifier.range; const source = info.source; // range is extended by 2 chars to cover the appended "._" source.replace(r[0], r[1] + 1, finalName); } } } } // Map with all root exposed used exports /** @type {Map} */ const exportsMap = new Map(); // Set with all root exposed unused exports /** @type {Set} */ const unusedExports = new Set(); const rootInfo = /** @type {ConcatenatedModuleInfo} */ ( moduleToInfoMap.get(this.rootModule) ); const strictHarmonyModule = rootInfo.module.buildMeta.strictHarmonyModule; const exportsInfo = moduleGraph.getExportsInfo(rootInfo.module); 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; } exportsMap.set(used, requestShortener => { try { const finalName = getFinalName( moduleGraph, rootInfo, [name], moduleToInfoMap, runtime, requestShortener, runtimeTemplate, neededNamespaceObjects, false, false, strictHarmonyModule, true ); return `/* ${ exportInfo.isReexport() ? "reexport" : "binding" } */ ${finalName}`; } catch (e) { e.message += `\nwhile generating the root export '${name}' (used name: '${used}')`; throw e; } }); } const result = new ConcatSource(); // add harmony compatibility flag (must be first because of possible circular dependencies) if ( moduleGraph.getExportsInfo(this).otherExportsInfo.getUsed(runtime) !== UsageState.Unused ) { result.add(`// ESM COMPAT FLAG\n`); result.add( runtimeTemplate.defineEsModuleFlagStatement({ exportsArgument: this.exportsArgument, runtimeRequirements }) ); } // define exports if (exportsMap.size > 0) { runtimeRequirements.add(RuntimeGlobals.exports); runtimeRequirements.add(RuntimeGlobals.definePropertyGetters); const definitions = []; for (const [key, value] of exportsMap) { definitions.push( `\n ${propertyName(key)}: ${runtimeTemplate.returningFunction( value(requestShortener) )}` ); } result.add(`\n// EXPORTS\n`); result.add( `${RuntimeGlobals.definePropertyGetters}(${ this.exportsArgument }, {${definitions.join(",")}\n});\n` ); } // list unused exports if (unusedExports.size > 0) { result.add( `\n// UNUSED EXPORTS: ${joinIterableWithComma(unusedExports)}\n` ); } // generate namespace objects const namespaceObjectSources = new Map(); for (const info of neededNamespaceObjects) { if (info.namespaceExportSymbol) continue; 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) { const finalName = getFinalName( moduleGraph, info, [exportInfo.name], moduleToInfoMap, runtime, requestShortener, runtimeTemplate, neededNamespaceObjects, false, undefined, 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); } // define required namespace objects (must be before evaluation modules) for (const info of modulesWithInfo) { if (info.type === "concatenated") { const source = namespaceObjectSources.get(info); if (!source) continue; result.add(source); } } const chunkInitFragments = []; // evaluate modules in order for (const rawInfo of modulesWithInfo) { let name; let isConditional = false; const info = rawInfo.type === "reference" ? rawInfo.target : rawInfo; switch (info.type) { case "concatenated": { result.add( `\n;// CONCATENATED MODULE: ${info.module.readableIdentifier( requestShortener )}\n` ); result.add(info.source); if (info.chunkInitFragments) { for (const f of info.chunkInitFragments) chunkInitFragments.push(f); } if (info.runtimeRequirements) { for (const r of info.runtimeRequirements) { runtimeRequirements.add(r); } } name = info.namespaceObjectName; break; } case "external": { 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`); } result.add( `var ${info.name} = ${RuntimeGlobals.require}(${JSON.stringify( chunkGraph.getModuleId(info.module) )});` ); name = info.name; break; } default: // @ts-expect-error never is expected here throw new Error(`Unsupported concatenation entry type ${info.type}`); } if (info.interopNamespaceObjectUsed) { runtimeRequirements.add(RuntimeGlobals.createFakeNamespaceObject); result.add( `\nvar ${info.interopNamespaceObjectName} = /*#__PURE__*/${RuntimeGlobals.createFakeNamespaceObject}(${name}, 2);` ); } if (info.interopNamespaceObject2Used) { runtimeRequirements.add(RuntimeGlobals.createFakeNamespaceObject); result.add( `\nvar ${info.interopNamespaceObject2Name} = /*#__PURE__*/${RuntimeGlobals.createFakeNamespaceObject}(${name});` ); } if (info.interopDefaultAccessUsed) { runtimeRequirements.add(RuntimeGlobals.compatGetDefaultExport); result.add( `\nvar ${info.interopDefaultAccessName} = /*#__PURE__*/${RuntimeGlobals.compatGetDefaultExport}(${name});` ); } if (isConditional) { result.add("\n}"); } } const data = new Map(); if (chunkInitFragments.length > 0) data.set("chunkInitFragments", chunkInitFragments); data.set("topLevelDeclarations", topLevelDeclarations); /** @type {CodeGenerationResult} */ const resultEntry = { sources: new Map([["javascript", new CachedSource(result)]]), data, runtimeRequirements }; return resultEntry; } /** * @param {Map} 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 {CodeGenerationResults} codeGenerationResults codeGenerationResults */ _analyseModule( modulesMap, info, dependencyTemplates, runtimeTemplate, moduleGraph, chunkGraph, runtime, codeGenerationResults ) { if (info.type === "concatenated") { const m = info.module; try { // Create a concatenation scope to track and capture information const concatenationScope = new ConcatenationScope(modulesMap, info); // TODO cache codeGeneration results const codeGenResult = m.codeGeneration({ dependencyTemplates, runtimeTemplate, moduleGraph, chunkGraph, runtime, concatenationScope, codeGenerationResults, sourceTypes: TYPES }); const source = codeGenResult.sources.get("javascript"); const data = codeGenResult.data; const chunkInitFragments = data && data.get("chunkInitFragments"); const code = source.source().toString(); let ast; try { ast = JavascriptParser._parse(code, { sourceType: "module" }); } catch (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 = scopeManager.acquire(ast); const moduleScope = globalScope.childScopes[0]; const resultSource = new ReplaceSource(source); info.runtimeRequirements = codeGenResult.runtimeRequirements; info.ast = ast; info.internalSource = source; info.source = resultSource; info.chunkInitFragments = chunkInitFragments; info.globalScope = globalScope; info.moduleScope = moduleScope; } catch (err) { err.message += `\nwhile analyzing module ${m.identifier()} for concatenation`; throw err; } } } /** * @param {ModuleGraph} moduleGraph the module graph * @param {RuntimeSpec} runtime the runtime * @returns {[ModuleInfoOrReference[], Map]} module info items */ _getModulesWithInfo(moduleGraph, runtime) { const orderedConcatenationList = this._createConcatenationList( this.rootModule, this._modules, runtime, moduleGraph ); /** @type {Map} */ 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", module: info.module, index, ast: undefined, internalSource: undefined, runtimeRequirements: undefined, source: undefined, globalScope: undefined, moduleScope: undefined, internalNames: new Map(), exportMap: undefined, rawExportMap: undefined, namespaceExportSymbol: undefined, namespaceObjectName: undefined, interopNamespaceObjectUsed: false, interopNamespaceObjectName: undefined, interopNamespaceObject2Used: false, interopNamespaceObject2Name: undefined, interopDefaultAccessUsed: false, interopDefaultAccessName: undefined }; break; case "external": item = { type: "external", module: info.module, runtimeCondition: info.runtimeCondition, index, name: undefined, interopNamespaceObjectUsed: false, interopNamespaceObjectName: undefined, interopNamespaceObject2Used: false, interopNamespaceObject2Name: undefined, interopDefaultAccessUsed: false, interopDefaultAccessName: undefined }; break; default: throw new Error( `Unsupported concatenation entry type ${info.type}` ); } map.set(item.module, item); return item; } else { /** @type {ReferenceToModuleInfo} */ const ref = { type: "reference", runtimeCondition: info.runtimeCondition, target: item }; return ref; } }); return [list, map]; } findNewName(oldName, usedNamed1, usedNamed2, extraInfo) { let name = oldName; if (name === ConcatenationScope.DEFAULT_EXPORT) { name = ""; } if (name === ConcatenationScope.NAMESPACE_OBJECT_EXPORT) { name = "namespaceObject"; } // Remove uncool stuff extraInfo = extraInfo.replace( /\.+\/|(\/index)?\.([a-zA-Z0-9]{1,4})($|\s|\?)|\s*\+\s*\d+\s*modules/g, "" ); const splittedInfo = extraInfo.split("/"); while (splittedInfo.length) { name = splittedInfo.pop() + (name ? "_" + name : ""); const nameIdent = Template.toIdentifier(name); if ( !usedNamed1.has(nameIdent) && (!usedNamed2 || !usedNamed2.has(nameIdent)) ) return nameIdent; } let i = 0; let nameWithNumber = Template.toIdentifier(`${name}_${i}`); while ( usedNamed1.has(nameWithNumber) || (usedNamed2 && usedNamed2.has(nameWithNumber)) ) { i++; nameWithNumber = Template.toIdentifier(`${name}_${i}`); } return nameWithNumber; } /** * @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)}`); // TODO runtimeCondition break; } } super.updateHash(hash, context); } static deserialize(context) { const obj = new ConcatenatedModule({ identifier: undefined, rootModule: undefined, modules: undefined, runtime: undefined }); obj.deserialize(context); return obj; } } makeSerializable(ConcatenatedModule, "webpack/lib/optimize/ConcatenatedModule"); module.exports = ConcatenatedModule;