/*
	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 { 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 { getAllReferences, getPathInAst } = require("../util/mergeScope");
const propertyAccess = require("../util/propertyAccess");
const { propertyName } = require("../util/propertyName");
const {
	filterRuntime,
	intersectRuntime,
	mergeRuntimeCondition,
	mergeRuntimeConditionNonFalse,
	runtimeConditionToString,
	subtractRuntimeCondition
} = require("../util/runtime");

/** @typedef {import("eslint-scope").Reference} Reference */
/** @typedef {import("eslint-scope").Scope} Scope */
/** @typedef {import("eslint-scope").Variable} Variable */
/** @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 */
/** @typedef {import("../Module").BuildInfo} BuildInfo */
/** @typedef {import("../Module").BuildMeta} BuildMeta */
/** @typedef {import("../Module").CodeGenerationContext} CodeGenerationContext */
/** @typedef {import("../Module").CodeGenerationResult} CodeGenerationResult */
/** @typedef {import("../Module").LibIdentOptions} LibIdentOptions */
/** @typedef {import("../Module").ReadOnlyRuntimeRequirements} ReadOnlyRuntimeRequirements */
/** @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("../javascript/JavascriptParser").Program} Program */
/** @typedef {import("../javascript/JavascriptParser").Range} Range */
/** @typedef {import("../serialization/ObjectMiddleware").ObjectDeserializerContext} ObjectDeserializerContext */
/** @typedef {import("../util/Hash")} Hash */
/** @typedef {typeof import("../util/Hash")} HashConstructor */
/** @typedef {import("../util/fs").InputFileSystem} InputFileSystem */
/** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */

/**
 * @template T
 * @typedef {import("../InitFragment")<T>} InitFragment
 */

/**
 * @template T
 * @typedef {import("../util/comparators").Comparator<T>} Comparator
 */

// fix eslint-scope to support class properties correctly
// cspell:word Referencer
const ReferencerClass = /** @type {any} */ (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 {Program | undefined} ast
 * @property {Source} internalSource
 * @property {ReplaceSource} source
 * @property {InitFragment<ChunkRenderContext>[]=} chunkInitFragments
 * @property {ReadOnlyRuntimeRequirements} runtimeRequirements
 * @property {Scope} globalScope
 * @property {Scope} moduleScope
 * @property {Map<string, string>} internalNames
 * @property {Map<string, string> | undefined} exportMap
 * @property {Map<string, string> | undefined} rawExportMap
 * @property {string=} namespaceExportSymbol
 * @property {string | undefined} namespaceObjectName
 * @property {boolean} interopNamespaceObjectUsed
 * @property {string | undefined} interopNamespaceObjectName
 * @property {boolean} interopNamespaceObject2Used
 * @property {string | undefined} interopNamespaceObject2Name
 * @property {boolean} interopDefaultAccessUsed
 * @property {string | undefined} 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
 */

/** @typedef {Set<string>} UsedNames */

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]);

/**
 * @param {number} a a
 * @param {number} b b
 * @returns {0 | 1 | -1} result
 */
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);

/**
 * @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 {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<Module, ModuleInfo>} moduleToInfoMap moduleToInfoMap
 * @param {RuntimeSpec} runtime for which runtime
 * @param {RequestShortener} requestShortener the request shortener
 * @param {RuntimeTemplate} runtimeTemplate the runtime template
 * @param {Set<ConcatenatedModuleInfo>} neededNamespaceObjects modules for which a namespace object should be generated
 * @param {boolean} asCall asCall
 * @param {boolean | undefined} strictHarmonyModule strictHarmonyModule
 * @param {boolean | undefined} asiSafe asiSafe
 * @param {Set<ExportInfo>} 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: /** @type {string} */ (info.interopNamespaceObject2Name),
					ids: exportName,
					exportName
				};
			case "default-with-named":
				info.interopNamespaceObjectUsed = true;
				return {
					info,
					rawName: /** @type {string} */ (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: /** @type {string} */ (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: /** @type {string} */ (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,
					/** @type {ModuleInfo} */ (refInfo),
					reexport.export
						? [...reexport.export, ...exportName.slice(1)]
						: exportName.slice(1),
					moduleToInfoMap,
					runtime,
					requestShortener,
					runtimeTemplate,
					neededNamespaceObjects,
					asCall,
					/** @type {BuildMeta} */
					(info.module.buildMeta).strictHarmonyModule,
					asiSafe,
					alreadyVisited
				);
			}
			if (info.namespaceExportSymbol) {
				const usedName = /** @type {string[]} */ (
					exportsInfo.getUsedName(exportName, runtime)
				);
				return {
					info,
					rawName: /** @type {string} */ (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<Module, ModuleInfo>} moduleToInfoMap moduleToInfoMap
 * @param {RuntimeSpec} runtime for which runtime
 * @param {RequestShortener} requestShortener the request shortener
 * @param {RuntimeTemplate} runtimeTemplate the runtime template
 * @param {Set<ConcatenatedModuleInfo>} neededNamespaceObjects modules for which a namespace object should be generated
 * @param {boolean} asCall asCall
 * @param {boolean | undefined} callContext callContext
 * @param {boolean | undefined} 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;
	}
};

/**
 * @param {Scope | null} s scope
 * @param {UsedNames} nameSet name set
 * @param {TODO} scopeSet1 scope set 1
 * @param {TODO} scopeSet2 scope set 2
 */
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 TYPES = new Set(["javascript"]);

/**
 * @typedef {object} ConcatenateModuleHooks
 * @property {SyncBailHook<[Record<string, string>]>} exportsDefinitions
 */

/** @type {WeakMap<Compilation, ConcatenateModuleHooks>} */
const compilationHooksMap = new WeakMap();

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 {object=} associatedObjectForCache object for caching
	 * @param {string | HashConstructor=} hashFunction hash function to use
	 * @returns {ConcatenatedModule} the module
	 */
	static create(
		rootModule,
		modules,
		runtime,
		compilation,
		associatedObjectForCache,
		hashFunction = "md4"
	) {
		const identifier = ConcatenatedModule._createIdentifier(
			rootModule,
			modules,
			associatedObjectForCache,
			hashFunction
		);
		return new ConcatenatedModule({
			identifier,
			rootModule,
			modules,
			runtime,
			compilation
		});
	}

	/**
	 * @param {Compilation} compilation the compilation
	 * @returns {ConcatenateModuleHooks} the attached hooks
	 */
	static getCompilationHooks(compilation) {
		let hooks = compilationHooksMap.get(compilation);
		if (hooks === undefined) {
			hooks = {
				exportsDefinitions: new SyncBailHook(["definitions"])
			};
			compilationHooksMap.set(compilation, hooks);
		}
		return hooks;
	}

	/**
	 * @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);

		// Info from Factory
		/** @type {string} */
		this._identifier = identifier;
		/** @type {Module} */
		this.rootModule = rootModule;
		/** @type {Set<Module>} */
		this._modules = modules;
		this._runtime = runtime;
		this.factoryMeta = rootModule && rootModule.factoryMeta;
		/** @type {Compilation | undefined} */
		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 {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;
		const { moduleArgument, exportsArgument } =
			/** @type {BuildInfo} */
			(rootModule.buildInfo);
		this.buildInfo = {
			strict: true,
			cacheable: true,
			moduleArgument,
			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 (!(/** @type {BuildInfo} */ (m.buildInfo).cacheable)) {
				this.buildInfo.cacheable = false;
			}

			// populate dependencies
			for (const d of m.dependencies.filter(
				dep =>
					!(dep instanceof HarmonyImportDependency) ||
					!this._modules.has(
						/** @type {Module} */ (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);
				}
			}

			const { assets, assetsInfo, topLevelDeclarations } =
				/** @type {BuildInfo} */ (m.buildInfo);

			// populate topLevelDeclarations
			if (topLevelDeclarations) {
				const topLevelDeclarations = this.buildInfo.topLevelDeclarations;
				if (topLevelDeclarations !== undefined) {
					for (const decl of topLevelDeclarations) {
						topLevelDeclarations.add(decl);
					}
				}
			} else {
				this.buildInfo.topLevelDeclarations = undefined;
			}

			// populate assets
			if (assets) {
				if (this.buildInfo.assets === undefined) {
					this.buildInfo.assets = Object.create(null);
				}
				Object.assign(/** @type {BuildInfo} */ (this.buildInfo).assets, assets);
			}
			if (assetsInfo) {
				if (this.buildInfo.assetsInfo === undefined) {
					this.buildInfo.assetsInfo = new Map();
				}
				for (const [key, value] of 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<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) {
		/** @type {ConcatenationEntry[]} */
		const list = [];
		/** @type {Map<Module, RuntimeSpec | true>} */
		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<Module, { connection: ModuleGraphConnection, runtimeCondition: RuntimeSpec | true }>} */
			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<Module>} 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(
			/** @type {string} */ (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<string>} fileDependencies set where file dependencies are added to
	 * @param {LazySet<string>} contextDependencies set where context dependencies are added to
	 * @param {LazySet<string>} missingDependencies set where missing dependencies are added to
	 * @param {LazySet<string>} 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<string>} */
		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<ConcatenatedModuleInfo>} */
		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<string, { usedNames: UsedNames, alreadyCheckedScopes: Set<TODO> }>} */
		const usedNamesInScopeInfo = new Map();
		/**
		 * @param {string} module module identifier
		 * @param {string} id export id
		 * @returns {{ usedNames: UsedNames, alreadyCheckedScopes: Set<TODO> }} 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();
				/**
				 * @param {Scope} scope scope
				 * @returns {TODO} result
				 */
				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,
								/** @type {BuildMeta} */
								(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] <=
										/** @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);
						}
					}
				}
			}
		}

		// 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 = /** @type {Range} */ (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 =
						/** @type {string} */
						(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;
				}
			}
			const buildMeta = /** @type {BuildMeta} */ (info.module.buildMeta);
			if (buildMeta.exportsType !== "namespace") {
				const externalNameInterop = this.findNewName(
					"namespaceObject",
					allUsedNames,
					namespaceObjectUsedNames,
					info.module.readableIdentifier(requestShortener)
				);
				allUsedNames.add(externalNameInterop);
				info.interopNamespaceObjectName = externalNameInterop;
				topLevelDeclarations.add(externalNameInterop);
			}
			if (
				buildMeta.exportsType === "default" &&
				buildMeta.defaultObject !== "redirect"
			) {
				const externalNameInterop = this.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 = 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,
							/** @type {BuildMeta} */
							(info.module.buildMeta).strictHarmonyModule,
							match.asiSafe
						);
						const r = /** @type {Range} */ (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<string, function(RequestShortener): string>} */
		const exportsMap = new Map();

		// Set with all root exposed unused exports
		/** @type {Set<string>} */
		const unusedExports = 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;
			}
			exportsMap.set(used, requestShortener => {
				try {
					const finalName = getFinalName(
						moduleGraph,
						rootInfo,
						[name],
						moduleToInfoMap,
						runtime,
						requestShortener,
						runtimeTemplate,
						neededNamespaceObjects,
						false,
						false,
						strictHarmonyModule,
						true
					);
					exportsFinalName[used] = finalName;
					return `/* ${
						exportInfo.isReexport() ? "reexport" : "binding"
					} */ ${finalName}`;
				} catch (e) {
					/** @type {Error} */
					(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)
		let shouldAddHarmonyFlag = false;
		if (
			moduleGraph.getExportsInfo(this).otherExportsInfo.getUsed(runtime) !==
			UsageState.Unused
		) {
			shouldAddHarmonyFlag = true;
		}

		// define exports
		if (exportsMap.size > 0) {
			const { exportsDefinitions } = ConcatenatedModule.getCompilationHooks(
				this.compilation
			);

			const definitions = [];
			for (const [key, value] of exportsMap) {
				definitions.push(
					`\n  ${propertyName(key)}: ${runtimeTemplate.returningFunction(
						value(requestShortener)
					)}`
				);
			}
			const shouldSkipRenderDefinitions =
				exportsDefinitions.call(exportsFinalName);

			if (!shouldSkipRenderDefinitions) {
				runtimeRequirements.add(RuntimeGlobals.exports);
				runtimeRequirements.add(RuntimeGlobals.definePropertyGetters);

				if (shouldAddHarmonyFlag) {
					result.add(`// ESM COMPAT FLAG\n`);
					result.add(
						runtimeTemplate.defineEsModuleFlagStatement({
							exportsArgument: this.exportsArgument,
							runtimeRequirements
						})
					);
				}

				result.add(`\n// EXPORTS\n`);
				result.add(
					`${RuntimeGlobals.definePropertyGetters}(${
						this.exportsArgument
					}, {${definitions.join(",")}\n});\n`
				);
			} else {
				this.buildMeta.exportsFinalName = exportsFinalName;
			}
		}

		// 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,
						/** @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);
		}

		// 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<Module, ModuleInfo>} 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 = /** @type {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 = /** @type {Scope} */ (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) {
				/** @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[], Map<Module, ModuleInfo>]} module info items
	 */
	_getModulesWithInfo(moduleGraph, runtime) {
		const orderedConcatenationList = this._createConcatenationList(
			this.rootModule,
			this._modules,
			runtime,
			moduleGraph
		);
		/** @type {Map<Module, ModuleInfo>} */
		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(
					/** @type {ModuleInfo} */ (item).module,
					/** @type {ModuleInfo} */ (item)
				);
				return item;
			} else {
				/** @type {ReferenceToModuleInfo} */
				const ref = {
					type: "reference",
					runtimeCondition: info.runtimeCondition,
					target: item
				};
				return ref;
			}
		});
		return [list, map];
	}

	/**
	 * @param {string} oldName old name
	 * @param {UsedNames} usedNamed1 used named 1
	 * @param {UsedNames} usedNamed2 used named 2
	 * @param {string} extraInfo extra info
	 * @returns {string} found new name
	 */
	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);
	}

	/**
	 * @param {ObjectDeserializerContext} context context
	 * @returns {ConcatenatedModule} ConcatenatedModule
	 */
	static deserialize(context) {
		const obj = new ConcatenatedModule({
			identifier: undefined,
			rootModule: undefined,
			modules: undefined,
			runtime: undefined,
			compilation: undefined
		});
		obj.deserialize(context);
		return obj;
	}
}

makeSerializable(ConcatenatedModule, "webpack/lib/optimize/ConcatenatedModule");

module.exports = ConcatenatedModule;
