/*
	MIT License http://www.opensource.org/licenses/mit-license.php
	Author Tobias Koppers @sokra
*/

"use strict";

const { SyncWaterfallHook, SyncHook } = require("tapable");
const {
	ConcatSource,
	OriginalSource,
	PrefixSource,
	RawSource
} = require("webpack-sources");
const Compilation = require("../Compilation");
const { tryRunOrWebpackError } = require("../HookWebpackError");
const HotUpdateChunk = require("../HotUpdateChunk");
const RuntimeGlobals = require("../RuntimeGlobals");
const Template = require("../Template");
const { compareModulesByIdentifier } = require("../util/comparators");
const createHash = require("../util/createHash");
const JavascriptGenerator = require("./JavascriptGenerator");
const JavascriptParser = require("./JavascriptParser");

/** @typedef {import("webpack-sources").Source} Source */
/** @typedef {import("../Chunk")} Chunk */
/** @typedef {import("../ChunkGraph")} ChunkGraph */
/** @typedef {import("../Compilation").ChunkHashContext} ChunkHashContext */
/** @typedef {import("../Compiler")} Compiler */
/** @typedef {import("../DependencyTemplates")} DependencyTemplates */
/** @typedef {import("../Module")} Module */
/** @typedef {import("../Module").CodeGenerationResult} CodeGenerationResult */
/** @typedef {import("../ModuleGraph")} ModuleGraph */
/** @typedef {import("../RuntimeTemplate")} RuntimeTemplate */
/** @typedef {import("../util/Hash")} Hash */

/**
 * @template T
 * @param {Iterable<T>} iterable iterable
 * @param {function(T): boolean} filter predicate
 * @returns {boolean} true, if some items match the filter predicate
 */
const someInIterable = (iterable, filter) => {
	for (const item of iterable) {
		if (filter(item)) return true;
	}
	return false;
};

/**
 * @param {Chunk} chunk a chunk
 * @param {ChunkGraph} chunkGraph the chunk graph
 * @returns {boolean} true, when a JS file is needed for this chunk
 */
const chunkHasJs = (chunk, chunkGraph) => {
	if (chunkGraph.getNumberOfEntryModules(chunk) > 0) return true;

	for (const module of chunkGraph.getChunkModulesIterable(chunk)) {
		if (module.getSourceTypes().has("javascript")) {
			return true;
		}
	}
	return false;
};

/**
 * @typedef {Object} RenderContext
 * @property {Chunk} chunk the chunk
 * @property {DependencyTemplates} dependencyTemplates the dependency templates
 * @property {RuntimeTemplate} runtimeTemplate the runtime template
 * @property {ModuleGraph} moduleGraph the module graph
 * @property {ChunkGraph} chunkGraph the chunk graph
 * @property {Map<Module, CodeGenerationResult>} codeGenerationResults results of code generation
 */

/**
 * @typedef {Object} MainRenderContext
 * @property {Chunk} chunk the chunk
 * @property {DependencyTemplates} dependencyTemplates the dependency templates
 * @property {RuntimeTemplate} runtimeTemplate the runtime template
 * @property {ModuleGraph} moduleGraph the module graph
 * @property {ChunkGraph} chunkGraph the chunk graph
 * @property {Map<Module, CodeGenerationResult>} codeGenerationResults results of code generation
 * @property {string} hash hash to be used for render call
 */

/**
 * @typedef {Object} RenderBootstrapContext
 * @property {Chunk} chunk the chunk
 * @property {RuntimeTemplate} runtimeTemplate the runtime template
 * @property {ModuleGraph} moduleGraph the module graph
 * @property {ChunkGraph} chunkGraph the chunk graph
 * @property {string} hash hash to be used for render call
 */

/**
 * @typedef {Object} CompilationHooks
 * @property {SyncWaterfallHook<[Source, Module, RenderContext]>} renderModuleContent
 * @property {SyncWaterfallHook<[Source, Module, RenderContext]>} renderModuleContainer
 * @property {SyncWaterfallHook<[Source, Module, RenderContext]>} renderModulePackage
 * @property {SyncWaterfallHook<[Source, RenderContext]>} renderChunk
 * @property {SyncWaterfallHook<[Source, RenderContext]>} renderMain
 * @property {SyncWaterfallHook<[Source, RenderContext]>} render
 * @property {SyncWaterfallHook<[string, RenderBootstrapContext]>} renderRequire
 * @property {SyncHook<[Chunk, Hash, ChunkHashContext]>} chunkHash
 */

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

class JavascriptModulesPlugin {
	/**
	 * @param {Compilation} compilation the compilation
	 * @returns {CompilationHooks} the attached hooks
	 */
	static getCompilationHooks(compilation) {
		if (!(compilation instanceof Compilation)) {
			throw new TypeError(
				"The 'compilation' argument must be an instance of Compilation"
			);
		}
		let hooks = compilationHooksMap.get(compilation);
		if (hooks === undefined) {
			hooks = {
				renderModuleContent: new SyncWaterfallHook([
					"source",
					"module",
					"renderContext"
				]),
				renderModuleContainer: new SyncWaterfallHook([
					"source",
					"module",
					"renderContext"
				]),
				renderModulePackage: new SyncWaterfallHook([
					"source",
					"module",
					"renderContext"
				]),
				render: new SyncWaterfallHook(["source", "renderContext"]),
				renderChunk: new SyncWaterfallHook(["source", "renderContext"]),
				renderMain: new SyncWaterfallHook(["source", "renderContext"]),
				renderRequire: new SyncWaterfallHook(["code", "renderContext"]),
				chunkHash: new SyncHook(["chunk", "hash", "context"])
			};
			compilationHooksMap.set(compilation, hooks);
		}
		return hooks;
	}

	constructor(options = {}) {
		this.options = options;
	}

	/**
	 * @param {Compiler} compiler webpack compiler
	 * @returns {void}
	 */
	apply(compiler) {
		compiler.hooks.compilation.tap(
			"JavascriptModulesPlugin",
			(compilation, { normalModuleFactory }) => {
				const hooks = JavascriptModulesPlugin.getCompilationHooks(compilation);
				normalModuleFactory.hooks.createParser
					.for("javascript/auto")
					.tap("JavascriptModulesPlugin", options => {
						return new JavascriptParser(options, "auto");
					});
				normalModuleFactory.hooks.createParser
					.for("javascript/dynamic")
					.tap("JavascriptModulesPlugin", options => {
						return new JavascriptParser(options, "script");
					});
				normalModuleFactory.hooks.createParser
					.for("javascript/esm")
					.tap("JavascriptModulesPlugin", options => {
						return new JavascriptParser(options, "module");
					});
				normalModuleFactory.hooks.createGenerator
					.for("javascript/auto")
					.tap("JavascriptModulesPlugin", () => {
						return new JavascriptGenerator();
					});
				normalModuleFactory.hooks.createGenerator
					.for("javascript/dynamic")
					.tap("JavascriptModulesPlugin", () => {
						return new JavascriptGenerator();
					});
				normalModuleFactory.hooks.createGenerator
					.for("javascript/esm")
					.tap("JavascriptModulesPlugin", () => {
						return new JavascriptGenerator();
					});
				compilation.hooks.renderManifest.tap(
					"JavascriptModulesPlugin",
					(result, options) => {
						const {
							hash,
							chunk,
							chunkGraph,
							moduleGraph,
							runtimeTemplate,
							dependencyTemplates,
							outputOptions,
							codeGenerationResults
						} = options;

						const hotUpdateChunk =
							chunk instanceof HotUpdateChunk ? chunk : null;

						let render;
						let filenameTemplate;
						if (hotUpdateChunk) {
							filenameTemplate = outputOptions.hotUpdateChunkFilename;

							render = () =>
								this.renderChunk(
									{
										chunk,
										dependencyTemplates,
										runtimeTemplate,
										moduleGraph,
										chunkGraph,
										codeGenerationResults
									},
									hooks
								);
						} else if (chunk.hasRuntime()) {
							filenameTemplate =
								chunk.filenameTemplate || outputOptions.filename;

							render = () =>
								this.renderMain(
									{
										hash,
										chunk,
										dependencyTemplates,
										runtimeTemplate,
										moduleGraph,
										chunkGraph,
										codeGenerationResults
									},
									hooks
								);
						} else {
							if (!chunkHasJs(chunk, chunkGraph)) {
								return result;
							}

							if (chunk.filenameTemplate) {
								filenameTemplate = chunk.filenameTemplate;
							} else if (chunk.isOnlyInitial()) {
								filenameTemplate = outputOptions.filename;
							} else {
								filenameTemplate = outputOptions.chunkFilename;
							}

							render = () =>
								this.renderChunk(
									{
										chunk,
										dependencyTemplates,
										runtimeTemplate,
										moduleGraph,
										chunkGraph,
										codeGenerationResults
									},
									hooks
								);
						}

						result.push({
							render,
							filenameTemplate,
							pathOptions: {
								hash,
								chunk,
								contentHashType: "javascript"
							},
							identifier: hotUpdateChunk
								? `hotupdatechunk${chunk.id}`
								: `chunk${chunk.id}`,
							hash: chunk.hash
						});

						return result;
					}
				);
				compilation.hooks.chunkHash.tap(
					"JavascriptModulesPlugin",
					(chunk, hash, context) => {
						hooks.chunkHash.call(chunk, hash, context);
						if (chunk.hasRuntime()) {
							const bootstrap = this.renderBootstrap(
								{
									hash: "0000",
									chunk,
									chunkGraph: context.chunkGraph,
									moduleGraph: context.moduleGraph,
									runtimeTemplate: context.runtimeTemplate
								},
								hooks
							);
							for (const key of Object.keys(bootstrap)) {
								hash.update(key);
								if (Array.isArray(bootstrap[key])) {
									for (const line of bootstrap[key]) {
										hash.update(line);
									}
								} else {
									hash.update(JSON.stringify(bootstrap[key]));
								}
							}
						}
					}
				);
				compilation.hooks.contentHash.tap("JavascriptModulesPlugin", chunk => {
					const {
						chunkGraph,
						moduleGraph,
						runtimeTemplate,
						outputOptions: {
							hashSalt,
							hashDigest,
							hashDigestLength,
							hashFunction
						}
					} = compilation;
					const hotUpdateChunk = chunk instanceof HotUpdateChunk ? chunk : null;
					const hash = createHash(hashFunction);
					if (hashSalt) hash.update(hashSalt);
					hash.update(`${chunk.id} `);
					hash.update(chunk.ids ? chunk.ids.join(",") : "");
					hooks.chunkHash.call(chunk, hash, {
						chunkGraph,
						moduleGraph,
						runtimeTemplate
					});
					for (const m of chunkGraph.getOrderedChunkModulesIterable(
						chunk,
						compareModulesByIdentifier
					)) {
						if (m.getSourceTypes().has("javascript")) {
							hash.update(chunkGraph.getModuleHash(m));
						}
					}
					if (hotUpdateChunk) {
						hash.update(JSON.stringify(hotUpdateChunk.removedModules));
					}
					const digest = /** @type {string} */ (hash.digest(hashDigest));
					chunk.contentHash.javascript = digest.substr(0, hashDigestLength);
				});
			}
		);
	}

	/**
	 * @param {Module} module the rendered module
	 * @param {RenderContext} renderContext options object
	 * @param {CompilationHooks} hooks hooks
	 * @param {boolean | "strict"} factory true: renders as factory method, "strict": renders as factory method already in strict scope, false: pure module content
	 * @returns {Source} the newly generated source from rendering
	 */
	renderModule(module, renderContext, hooks, factory) {
		const {
			chunkGraph,
			runtimeTemplate,
			codeGenerationResults
		} = renderContext;
		try {
			const moduleSource = codeGenerationResults
				.get(module)
				.sources.get("javascript");
			if (!moduleSource) return null;
			const moduleSourcePostContent = tryRunOrWebpackError(
				() =>
					hooks.renderModuleContent.call(moduleSource, module, renderContext),
				"JavascriptModulesPlugin.getCompilationHooks().renderModuleContent"
			);
			let moduleSourcePostContainer;
			if (factory) {
				const source = new ConcatSource();
				const args = [];
				const runtimeRequirements = chunkGraph.getModuleRuntimeRequirements(
					module
				);
				const needModule = runtimeRequirements.has(RuntimeGlobals.module);
				const needExports = runtimeRequirements.has(RuntimeGlobals.exports);
				const needRequire =
					runtimeRequirements.has(RuntimeGlobals.require) ||
					runtimeRequirements.has(RuntimeGlobals.requireScope);
				const needThisAsExports = runtimeRequirements.has(
					RuntimeGlobals.thisAsExports
				);
				if (needExports || needRequire || needModule)
					args.push(
						needModule
							? module.moduleArgument
							: "__unused_webpack_" + module.moduleArgument
					);
				if (needExports || needRequire)
					args.push(
						needExports
							? module.exportsArgument
							: "__unused_webpack_" + module.exportsArgument
					);
				if (needRequire) args.push("__webpack_require__");
				if (!needThisAsExports && runtimeTemplate.supportsArrowFunction()) {
					source.add("/***/ ((" + args.join(", ") + ") => {\n\n");
				} else {
					source.add("/***/ (function(" + args.join(", ") + ") {\n\n");
				}
				if (module.buildInfo.strict && factory !== "strict") {
					source.add('"use strict";\n');
				}
				source.add(moduleSourcePostContent);
				source.add("\n\n/***/ })");
				moduleSourcePostContainer = tryRunOrWebpackError(
					() => hooks.renderModuleContainer.call(source, module, renderContext),
					"JavascriptModulesPlugin.getCompilationHooks().renderModuleContainer"
				);
			} else {
				moduleSourcePostContainer = moduleSourcePostContent;
			}
			return tryRunOrWebpackError(
				() =>
					hooks.renderModulePackage.call(
						moduleSourcePostContainer,
						module,
						renderContext
					),
				"JavascriptModulesPlugin.getCompilationHooks().renderModulePackage"
			);
		} catch (e) {
			e.module = module;
			throw e;
		}
	}

	/**
	 * @param {RenderContext} renderContext the render context
	 * @param {CompilationHooks} hooks hooks
	 * @returns {Source} the rendered source
	 */
	renderChunk(renderContext, hooks) {
		const { chunk, chunkGraph } = renderContext;
		const modules = chunkGraph
			.getOrderedChunkModules(chunk, compareModulesByIdentifier)
			.filter(m => m.getSourceTypes().has("javascript"));
		const moduleSources =
			Template.renderChunkModules(renderContext, modules, module =>
				this.renderModule(module, renderContext, hooks, true)
			) || new RawSource("{}");
		let source = tryRunOrWebpackError(
			() => hooks.renderChunk.call(moduleSources, renderContext),
			"JavascriptModulesPlugin.getCompilationHooks().renderChunk"
		);
		source = tryRunOrWebpackError(
			() => hooks.render.call(source, renderContext),
			"JavascriptModulesPlugin.getCompilationHooks().render"
		);
		chunk.rendered = true;
		return new ConcatSource(source, ";");
	}

	/**
	 * @param {MainRenderContext} renderContext options object
	 * @param {CompilationHooks} hooks hooks
	 * @returns {Source} the newly generated source from rendering
	 */
	renderMain(renderContext, hooks) {
		const { chunk, chunkGraph, runtimeTemplate } = renderContext;

		const runtimeRequirements = chunkGraph.getTreeRuntimeRequirements(chunk);
		const iife = runtimeTemplate.isIIFE();

		const bootstrap = this.renderBootstrap(renderContext, hooks);

		const allModules = chunkGraph
			.getOrderedChunkModules(chunk, compareModulesByIdentifier)
			.filter(m => m.getSourceTypes().has("javascript"));
		const allStrict = allModules.every(m => m.buildInfo.strict);

		let inlinedModules;
		if (bootstrap.allowInlineStartup) {
			inlinedModules = new Set(chunkGraph.getChunkEntryModulesIterable(chunk));
		}

		let source = new ConcatSource();
		let prefix;
		if (iife) {
			if (runtimeTemplate.supportsConst()) {
				source.add("/******/ (() => { // webpackBootstrap\n");
			} else {
				source.add("/******/ (function() { // webpackBootstrap\n");
			}
			prefix = "/******/ \t";
		} else {
			prefix = "/******/ ";
		}
		if (allStrict) {
			source.add(prefix + '"use strict";\n');
		}

		const chunkModules = Template.renderChunkModules(
			renderContext,
			inlinedModules
				? allModules.filter(m => !inlinedModules.has(m))
				: allModules,
			module =>
				this.renderModule(
					module,
					renderContext,
					hooks,
					allStrict ? "strict" : true
				),
			prefix
		);
		if (
			chunkModules ||
			runtimeRequirements.has(RuntimeGlobals.moduleFactories) ||
			runtimeRequirements.has(RuntimeGlobals.moduleFactoriesAddOnly)
		) {
			source.add(prefix + "var __webpack_modules__ = (");
			source.add(chunkModules || "{}");
			source.add(");\n");
			source.add(
				"/************************************************************************/\n"
			);
		}

		if (bootstrap.header.length > 0) {
			source.add(
				new PrefixSource(
					prefix,
					new OriginalSource(
						Template.asString(bootstrap.header) + "\n",
						"webpack/bootstrap"
					)
				)
			);
			source.add(
				"/************************************************************************/\n"
			);
		}

		const runtimeModules = renderContext.chunkGraph.getChunkRuntimeModulesInOrder(
			chunk
		);

		if (runtimeModules.length > 0) {
			source.add(
				new PrefixSource(
					prefix,
					Template.renderRuntimeModules(runtimeModules, renderContext)
				)
			);
			source.add(
				"/************************************************************************/\n"
			);
		}
		if (inlinedModules) {
			for (const m of inlinedModules) {
				const renderedModule = this.renderModule(
					m,
					renderContext,
					hooks,
					false
				);
				if (renderedModule) {
					const innerStrict = !allStrict && m.buildInfo.strict;
					const iife = innerStrict || inlinedModules.size > 1 || chunkModules;
					if (iife) {
						source.add("!function() {\n");
						if (innerStrict) source.add('"use strict";\n');
						source.add(renderedModule);
						source.add("\n}();\n");
					} else {
						source.add(renderedModule);
						source.add("\n");
					}
				}
			}
		} else {
			source.add(
				new PrefixSource(
					prefix,
					new OriginalSource(
						Template.asString(bootstrap.startup) + "\n",
						"webpack/startup"
					)
				)
			);
		}
		if (iife) {
			source.add("/******/ })()\n");
		}

		/** @type {Source} */
		let finalSource = tryRunOrWebpackError(
			() => hooks.renderMain.call(source, renderContext),
			"JavascriptModulesPlugin.getCompilationHooks().renderMain"
		);
		if (!finalSource) {
			throw new Error(
				"JavascriptModulesPlugin error: JavascriptModulesPlugin.getCompilationHooks().renderMain plugins should return something"
			);
		}
		finalSource = tryRunOrWebpackError(
			() => hooks.render.call(finalSource, renderContext),
			"JavascriptModulesPlugin.getCompilationHooks().render"
		);
		if (!finalSource) {
			throw new Error(
				"JavascriptModulesPlugin error: JavascriptModulesPlugin.getCompilationHooks().render plugins should return something"
			);
		}
		chunk.rendered = true;
		return iife ? new ConcatSource(finalSource, ";") : finalSource;
	}

	/**
	 * @param {RenderBootstrapContext} renderContext options object
	 * @param {CompilationHooks} hooks hooks
	 * @returns {{ header: string[], startup: string[], allowInlineStartup: boolean }} the generated source of the bootstrap code
	 */
	renderBootstrap(renderContext, hooks) {
		const { chunkGraph, moduleGraph, chunk, runtimeTemplate } = renderContext;

		const runtimeRequirements = chunkGraph.getTreeRuntimeRequirements(chunk);

		const requireFunction = runtimeRequirements.has(RuntimeGlobals.require);
		const moduleCache = runtimeRequirements.has(RuntimeGlobals.moduleCache);
		const moduleFactories = runtimeRequirements.has(
			RuntimeGlobals.moduleFactories
		);
		const moduleUsed = runtimeRequirements.has(RuntimeGlobals.module);
		const exportsUsed = runtimeRequirements.has(RuntimeGlobals.exports);
		const requireScopeUsed = runtimeRequirements.has(
			RuntimeGlobals.requireScope
		);
		const interceptModuleExecution = runtimeRequirements.has(
			RuntimeGlobals.interceptModuleExecution
		);
		const returnExportsFromRuntime = runtimeRequirements.has(
			RuntimeGlobals.returnExportsFromRuntime
		);

		const useRequire =
			requireFunction ||
			interceptModuleExecution ||
			returnExportsFromRuntime ||
			moduleUsed ||
			exportsUsed;

		const result = {
			header: [],
			startup: [],
			allowInlineStartup: true
		};

		let buf = result.header;
		let startup = result.startup;

		if (result.allowInlineStartup && moduleFactories) {
			startup.push(
				"// module factories are used so entry inlining is disabled"
			);
			result.allowInlineStartup = false;
		}
		if (result.allowInlineStartup && moduleCache) {
			startup.push("// module cache are used so entry inlining is disabled");
			result.allowInlineStartup = false;
		}
		if (result.allowInlineStartup && interceptModuleExecution) {
			startup.push(
				"// module execution is intercepted so entry inlining is disabled"
			);
			result.allowInlineStartup = false;
		}
		if (result.allowInlineStartup && returnExportsFromRuntime) {
			startup.push(
				"// module exports must be returned from runtime so entry inlining is disabled"
			);
			result.allowInlineStartup = false;
		}

		if (useRequire || moduleCache) {
			buf.push("// The module cache");
			buf.push("var __webpack_module_cache__ = {};");
			buf.push("");
		}

		if (useRequire) {
			buf.push("// The require function");
			buf.push(`function __webpack_require__(moduleId) {`);
			buf.push(Template.indent(this.renderRequire(renderContext, hooks)));
			buf.push("}");
			buf.push("");
		} else if (runtimeRequirements.has(RuntimeGlobals.requireScope)) {
			buf.push("// The require scope");
			buf.push("var __webpack_require__ = {};");
			buf.push("");
		}

		if (
			moduleFactories ||
			runtimeRequirements.has(RuntimeGlobals.moduleFactoriesAddOnly)
		) {
			buf.push("// expose the modules object (__webpack_modules__)");
			buf.push(`${RuntimeGlobals.moduleFactories} = __webpack_modules__;`);
			buf.push("");
		}

		if (moduleCache) {
			buf.push("// expose the module cache");
			buf.push(`${RuntimeGlobals.moduleCache} = __webpack_module_cache__;`);
			buf.push("");
		}

		if (interceptModuleExecution) {
			buf.push("// expose the module execution interceptor");
			buf.push(`${RuntimeGlobals.interceptModuleExecution} = [];`);
			buf.push("");
		}

		if (!runtimeRequirements.has(RuntimeGlobals.startupNoDefault)) {
			if (chunkGraph.getNumberOfEntryModules(chunk) > 0) {
				/** @type {string[]} */
				const buf2 = [];
				const runtimeRequirements = chunkGraph.getTreeRuntimeRequirements(
					chunk
				);
				buf2.push(
					returnExportsFromRuntime
						? "// Load entry module and return exports"
						: "// Load entry module"
				);
				let i = chunkGraph.getNumberOfEntryModules(chunk);
				for (const entryModule of chunkGraph.getChunkEntryModulesIterable(
					chunk
				)) {
					if (
						result.allowInlineStartup &&
						someInIterable(
							moduleGraph.getIncomingConnections(entryModule),
							c => c.originModule && c.active
						)
					) {
						buf2.push(
							"// This entry module is referenced by other modules so it can't be inlined"
						);
						result.allowInlineStartup = false;
					}
					const mayReturn =
						--i === 0 && returnExportsFromRuntime ? "return " : "";
					const moduleId = chunkGraph.getModuleId(entryModule);
					const entryRuntimeRequirements = chunkGraph.getModuleRuntimeRequirements(
						entryModule
					);
					let moduleIdExpr = JSON.stringify(moduleId);
					if (runtimeRequirements.has(RuntimeGlobals.entryModuleId)) {
						moduleIdExpr = `${RuntimeGlobals.entryModuleId} = ${moduleIdExpr}`;
					}
					if (useRequire) {
						buf2.push(`${mayReturn}__webpack_require__(${moduleIdExpr});`);
						if (result.allowInlineStartup) {
							if (entryRuntimeRequirements.has(RuntimeGlobals.module)) {
								result.allowInlineStartup = false;
								buf2.push(
									"// This entry module used 'module' so it can't be inlined"
								);
							} else if (entryRuntimeRequirements.has(RuntimeGlobals.exports)) {
								buf2.push(
									"// This entry module used 'exports' so it can't be inlined"
								);
								result.allowInlineStartup = false;
							}
						}
					} else if (requireScopeUsed) {
						buf2.push(
							`__webpack_modules__[${moduleIdExpr}](0, 0, __webpack_require__);`
						);
					} else {
						buf2.push(`__webpack_modules__[${moduleIdExpr}]();`);
					}
				}
				if (runtimeRequirements.has(RuntimeGlobals.startup)) {
					result.allowInlineStartup = false;
					buf.push(
						Template.asString([
							"// the startup function",
							`${RuntimeGlobals.startup} = ${runtimeTemplate.basicFunction(
								"",
								buf2
							)};`
						])
					);
					buf.push("");
					startup.push("// run startup");
					startup.push(`return ${RuntimeGlobals.startup}();`);
				} else {
					startup.push(
						Template.asString(["// startup", Template.asString(buf2)])
					);
				}
			} else if (runtimeRequirements.has(RuntimeGlobals.startup)) {
				buf.push(
					Template.asString([
						"// the startup function",
						"// It's empty as no entry modules are in this chunk",
						`${RuntimeGlobals.startup} = ${runtimeTemplate.basicFunction(
							"",
							""
						)}`
					])
				);
				buf.push("");
			}
		} else if (runtimeRequirements.has(RuntimeGlobals.startup)) {
			result.allowInlineStartup = false;
			startup.push("// run startup");
			startup.push(`return ${RuntimeGlobals.startup}();`);
		}
		return result;
	}

	/**
	 * @param {RenderBootstrapContext} renderContext options object
	 * @param {CompilationHooks} hooks hooks
	 * @returns {string} the generated source of the require function
	 */
	renderRequire(renderContext, hooks) {
		const {
			chunk,
			chunkGraph,
			runtimeTemplate: { outputOptions }
		} = renderContext;
		const runtimeRequirements = chunkGraph.getTreeRuntimeRequirements(chunk);
		const moduleExecution = runtimeRequirements.has(
			RuntimeGlobals.interceptModuleExecution
		)
			? Template.asString([
					"var execOptions = { id: moduleId, module: module, factory: __webpack_modules__[moduleId], require: __webpack_require__ };",
					`${RuntimeGlobals.interceptModuleExecution}.forEach(function(handler) { handler(execOptions); });`,
					"module = execOptions.module;",
					"execOptions.factory.call(module.exports, module, module.exports, execOptions.require);"
			  ])
			: runtimeRequirements.has(RuntimeGlobals.thisAsExports)
			? Template.asString([
					"__webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);"
			  ])
			: Template.asString([
					"__webpack_modules__[moduleId](module, module.exports, __webpack_require__);"
			  ]);
		const content = Template.asString([
			"// Check if module is in cache",
			"if(__webpack_module_cache__[moduleId]) {",
			Template.indent("return __webpack_module_cache__[moduleId].exports;"),
			"}",
			"// Create a new module (and put it into the cache)",
			"var module = __webpack_module_cache__[moduleId] = {",
			Template.indent(["i: moduleId,", "l: false,", "exports: {}"]),
			"};",
			"",
			outputOptions.strictModuleExceptionHandling
				? Template.asString([
						"// Execute the module function",
						"var threw = true;",
						"try {",
						Template.indent([moduleExecution, "threw = false;"]),
						"} finally {",
						Template.indent([
							"if(threw) delete __webpack_module_cache__[moduleId];"
						]),
						"}"
				  ])
				: Template.asString([
						"// Execute the module function",
						moduleExecution
				  ]),
			"",
			"// Flag the module as loaded",
			"module.l = true;",
			"",
			"// Return the exports of the module",
			"return module.exports;"
		]);
		return tryRunOrWebpackError(
			() => hooks.renderRequire.call(content, renderContext),
			"JavascriptModulesPlugin.getCompilationHooks().renderRequire"
		);
	}
}

module.exports = JavascriptModulesPlugin;
module.exports.chunkHasJs = chunkHasJs;
