constructor({
		context,
		fs,
		resolverFactory,
		options,
		associatedObjectForCache,
		layers = false
	}) {
		super();
		this.hooks = Object.freeze({
			/** @type {AsyncSeriesBailHook<[ResolveData], Module | false | void>} */
			resolve: new AsyncSeriesBailHook(["resolveData"]),
			/** @type {HookMap<AsyncSeriesBailHook<[ResourceDataWithData, ResolveData], true | void>>} */
			resolveForScheme: new HookMap(
				() => new AsyncSeriesBailHook(["resourceData", "resolveData"])
			),
			/** @type {HookMap<AsyncSeriesBailHook<[ResourceDataWithData, ResolveData], true | void>>} */
			resolveInScheme: new HookMap(
				() => new AsyncSeriesBailHook(["resourceData", "resolveData"])
			),
			/** @type {AsyncSeriesBailHook<[ResolveData], Module | undefined>} */
			factorize: new AsyncSeriesBailHook(["resolveData"]),
			/** @type {AsyncSeriesBailHook<[ResolveData], false | void>} */
			beforeResolve: new AsyncSeriesBailHook(["resolveData"]),
			/** @type {AsyncSeriesBailHook<[ResolveData], false | void>} */
			afterResolve: new AsyncSeriesBailHook(["resolveData"]),
			/** @type {AsyncSeriesBailHook<[CreateData, ResolveData], Module | void>} */
			createModule: new AsyncSeriesBailHook(["createData", "resolveData"]),
			/** @type {SyncWaterfallHook<[Module, CreateData, ResolveData]>} */
			module: new SyncWaterfallHook(["module", "createData", "resolveData"]),
			/** @type {HookMap<SyncBailHook<[ParserOptions], Parser | void>>} */
			createParser: new HookMap(() => new SyncBailHook(["parserOptions"])),
			/** @type {HookMap<SyncBailHook<[TODO, ParserOptions], void>>} */
			parser: new HookMap(() => new SyncHook(["parser", "parserOptions"])),
			/** @type {HookMap<SyncBailHook<[GeneratorOptions], Generator | void>>} */
			createGenerator: new HookMap(
				() => new SyncBailHook(["generatorOptions"])
			),
			/** @type {HookMap<SyncBailHook<[TODO, GeneratorOptions], void>>} */
			generator: new HookMap(
				() => new SyncHook(["generator", "generatorOptions"])
			),
			/** @type {HookMap<SyncBailHook<[TODO, ResolveData], Module | void>>} */
			createModuleClass: new HookMap(
				() => new SyncBailHook(["createData", "resolveData"])
			)
		});
		this.resolverFactory = resolverFactory;
		this.ruleSet = ruleSetCompiler.compile([
			{
				rules: /** @type {RuleSetRules} */ (options.defaultRules)
			},
			{
				rules: /** @type {RuleSetRules} */ (options.rules)
			}
		]);
		this.context = context || "";
		this.fs = fs;
		this._globalParserOptions = options.parser;
		this._globalGeneratorOptions = options.generator;
		/** @type {Map<string, WeakMap<ParserOptions, Parser>>} */
		this.parserCache = new Map();
		/** @type {Map<string, WeakMap<GeneratorOptions, Generator>>} */
		this.generatorCache = new Map();
		/** @type {Set<Module>} */
		this._restoredUnsafeCacheEntries = new Set();

		/** @type {(resource: string) => import("./util/identifier").ParsedResource} */
		const cacheParseResource = parseResource.bindCache(
			associatedObjectForCache
		);
		const cachedParseResourceWithoutFragment =
			parseResourceWithoutFragment.bindCache(associatedObjectForCache);
		this._parseResourceWithoutFragment = cachedParseResourceWithoutFragment;

		this.hooks.factorize.tapAsync(
			{
				name: "NormalModuleFactory",
				stage: 100
			},
			(resolveData, callback) => {
				this.hooks.resolve.callAsync(resolveData, (err, result) => {
					if (err) return callback(err);

					// Ignored
					if (result === false) return callback();

					// direct module
					if (result instanceof Module) return callback(null, result);

					if (typeof result === "object")
						throw new Error(
							`${deprecationChangedHookMessage(
								"resolve",
								this.hooks.resolve
							)} Returning a Module object will result in this module used as result.`
						);

					this.hooks.afterResolve.callAsync(resolveData, (err, result) => {
						if (err) return callback(err);

						if (typeof result === "object")
							throw new Error(
								deprecationChangedHookMessage(
									"afterResolve",
									this.hooks.afterResolve
								)
							);

						// Ignored
						if (result === false) return callback();

						const createData = resolveData.createData;

						this.hooks.createModule.callAsync(
							createData,
							resolveData,
							(err, createdModule) => {
								if (!createdModule) {
									if (!resolveData.request) {
										return callback(new Error("Empty dependency (no request)"));
									}

									// TODO webpack 6 make it required and move javascript/wasm/asset properties to own module
									createdModule = this.hooks.createModuleClass
										.for(
											/** @type {ModuleSettings} */
											(createData.settings).type
										)
										.call(createData, resolveData);

									if (!createdModule) {
										createdModule = /** @type {Module} */ (
											new NormalModule(
												/** @type {NormalModuleCreateData} */
												(createData)
											)
										);
									}
								}

								createdModule = this.hooks.module.call(
									createdModule,
									createData,
									resolveData
								);

								return callback(null, createdModule);
							}
						);
					});
				});
			}
		);
		this.hooks.resolve.tapAsync(
			{
				name: "NormalModuleFactory",
				stage: 100
			},
			(data, callback) => {
				const {
					contextInfo,
					context,
					dependencies,
					dependencyType,
					request,
					assertions,
					resolveOptions,
					fileDependencies,
					missingDependencies,
					contextDependencies
				} = data;
				const loaderResolver = this.getResolver("loader");

				/** @type {ResourceData | undefined} */
				let matchResourceData;
				/** @type {string} */
				let unresolvedResource;
				/** @type {ParsedLoaderRequest[]} */
				let elements;
				let noPreAutoLoaders = false;
				let noAutoLoaders = false;
				let noPrePostAutoLoaders = false;

				const contextScheme = getScheme(context);
				/** @type {string | undefined} */
				let scheme = getScheme(request);

				if (!scheme) {
					/** @type {string} */
					let requestWithoutMatchResource = request;
					const matchResourceMatch = MATCH_RESOURCE_REGEX.exec(request);
					if (matchResourceMatch) {
						let matchResource = matchResourceMatch[1];
						// Check if matchResource starts with ./ or ../
						if (matchResource.charCodeAt(0) === 46) {
							// 46 is "."
							const secondChar = matchResource.charCodeAt(1);
							if (
								secondChar === 47 || // 47 is "/"
								(secondChar === 46 && matchResource.charCodeAt(2) === 47) // "../"
							) {
								// Resolve relative path against context
								matchResource = join(this.fs, context, matchResource);
							}
						}

						matchResourceData = {
							...cacheParseResource(matchResource),
							resource: matchResource
						};
						requestWithoutMatchResource = request.slice(
							matchResourceMatch[0].length
						);
					}

					scheme = getScheme(requestWithoutMatchResource);

					if (!scheme && !contextScheme) {
						const firstChar = requestWithoutMatchResource.charCodeAt(0);
						const secondChar = requestWithoutMatchResource.charCodeAt(1);
						noPreAutoLoaders = firstChar === 45 && secondChar === 33; // startsWith "-!"
						noAutoLoaders = noPreAutoLoaders || firstChar === 33; // startsWith "!"
						noPrePostAutoLoaders = firstChar === 33 && secondChar === 33; // startsWith "!!";
						const rawElements = requestWithoutMatchResource
							.slice(
								noPreAutoLoaders || noPrePostAutoLoaders
									? 2
									: noAutoLoaders
										? 1
										: 0
							)
							.split(/!+/);
						unresolvedResource = /** @type {string} */ (rawElements.pop());
						elements = rawElements.map(el => {
							const { path, query } = cachedParseResourceWithoutFragment(el);
							return {
								loader: path,
								options: query ? query.slice(1) : undefined
							};
						});
						scheme = getScheme(unresolvedResource);
					} else {
						unresolvedResource = requestWithoutMatchResource;
						elements = EMPTY_ELEMENTS;
					}
				} else {
					unresolvedResource = request;
					elements = EMPTY_ELEMENTS;
				}

				/** @type {ResolveContext} */
				const resolveContext = {
					fileDependencies,
					missingDependencies,
					contextDependencies
				};

				/** @type {ResourceDataWithData} */
				let resourceData;

				/** @type {undefined | LoaderItem[]} */
				let loaders;

				const continueCallback = needCalls(2, err => {
					if (err) return callback(err);

					// translate option idents
					try {
						for (const item of /** @type {LoaderItem[]} */ (loaders)) {
							if (typeof item.options === "string" && item.options[0] === "?") {
								const ident = item.options.slice(1);
								if (ident === "[[missing ident]]") {
									throw new Error(
										"No ident is provided by referenced loader. " +
											"When using a function for Rule.use in config you need to " +
											"provide an 'ident' property for referenced loader options."
									);
								}
								item.options = this.ruleSet.references.get(ident);
								if (item.options === undefined) {
									throw new Error(
										"Invalid ident is provided by referenced loader"
									);
								}
								item.ident = ident;
							}
						}
					} catch (identErr) {
						return callback(/** @type {Error} */ (identErr));
					}

					if (!resourceData) {
						// ignored
						return callback(null, dependencies[0].createIgnoredModule(context));
					}

					const userRequest =
						(matchResourceData !== undefined
							? `${matchResourceData.resource}!=!`
							: "") +
						stringifyLoadersAndResource(
							/** @type {LoaderItem[]} */ (loaders),
							resourceData.resource
						);

					/** @type {ModuleSettings} */
					const settings = {};
					const useLoadersPost = [];
					const useLoaders = [];
					const useLoadersPre = [];

					// handle .webpack[] suffix
					let resource;
					let match;
					if (
						matchResourceData &&
						typeof (resource = matchResourceData.resource) === "string" &&
						(match = /\.webpack\[([^\]]+)\]$/.exec(resource))
					) {
						settings.type = match[1];
						matchResourceData.resource = matchResourceData.resource.slice(
							0,
							-settings.type.length - 10
						);
					} else {
						settings.type = JAVASCRIPT_MODULE_TYPE_AUTO;
						const resourceDataForRules = matchResourceData || resourceData;
						const result = this.ruleSet.exec({
							resource: resourceDataForRules.path,
							realResource: resourceData.path,
							resourceQuery: resourceDataForRules.query,
							resourceFragment: resourceDataForRules.fragment,
							scheme,
							assertions,
							mimetype: matchResourceData
								? ""
								: resourceData.data.mimetype || "",
							dependency: dependencyType,
							descriptionData: matchResourceData
								? undefined
								: resourceData.data.descriptionFileData,
							issuer: contextInfo.issuer,
							compiler: contextInfo.compiler,
							issuerLayer: contextInfo.issuerLayer || ""
						});
						for (const r of result) {
							// https://github.com/webpack/webpack/issues/16466
							// if a request exists PrePostAutoLoaders, should disable modifying Rule.type
							if (r.type === "type" && noPrePostAutoLoaders) {
								continue;
							}
							if (r.type === "use") {
								if (!noAutoLoaders && !noPrePostAutoLoaders) {
									useLoaders.push(r.value);
								}
							} else if (r.type === "use-post") {
								if (!noPrePostAutoLoaders) {
									useLoadersPost.push(r.value);
								}
							} else if (r.type === "use-pre") {
								if (!noPreAutoLoaders && !noPrePostAutoLoaders) {
									useLoadersPre.push(r.value);
								}
							} else if (
								typeof r.value === "object" &&
								r.value !== null &&
								typeof settings[
									/** @type {keyof ModuleSettings} */ (r.type)
								] === "object" &&
								settings[/** @type {keyof ModuleSettings} */ (r.type)] !== null
							) {
								const type = /** @type {keyof ModuleSettings} */ (r.type);
								settings[type] = cachedCleverMerge(settings[type], r.value);
							} else {
								const type = /** @type {keyof ModuleSettings} */ (r.type);
								settings[type] = r.value;
							}
						}
					}

					/** @type {undefined | LoaderItem[]} */
					let postLoaders;
					/** @type {undefined | LoaderItem[]} */
					let normalLoaders;
					/** @type {undefined | LoaderItem[]} */
					let preLoaders;

					const continueCallback = needCalls(3, err => {
						if (err) {
							return callback(err);
						}
						const allLoaders = /** @type {LoaderItem[]} */ (postLoaders);
						if (matchResourceData === undefined) {
							for (const loader of /** @type {LoaderItem[]} */ (loaders))
								allLoaders.push(loader);
							for (const loader of /** @type {LoaderItem[]} */ (normalLoaders))
								allLoaders.push(loader);
						} else {
							for (const loader of /** @type {LoaderItem[]} */ (normalLoaders))
								allLoaders.push(loader);
							for (const loader of /** @type {LoaderItem[]} */ (loaders))
								allLoaders.push(loader);
						}
						for (const loader of /** @type {LoaderItem[]} */ (preLoaders))
							allLoaders.push(loader);
						const type = /** @type {string} */ (settings.type);
						const resolveOptions = settings.resolve;
						const layer = settings.layer;
						if (layer !== undefined && !layers) {
							return callback(
								new Error(
									"'Rule.layer' is only allowed when 'experiments.layers' is enabled"
								)
							);
						}
						try {
							Object.assign(data.createData, {
								layer:
									layer === undefined ? contextInfo.issuerLayer || null : layer,
								request: stringifyLoadersAndResource(
									allLoaders,
									resourceData.resource
								),
								userRequest,
								rawRequest: request,
								loaders: allLoaders,
								resource: resourceData.resource,
								context:
									resourceData.context || getContext(resourceData.resource),
								matchResource: matchResourceData
									? matchResourceData.resource
									: undefined,
								resourceResolveData: resourceData.data,
								settings,
								type,
								parser: this.getParser(type, settings.parser),
								parserOptions: settings.parser,
								generator: this.getGenerator(type, settings.generator),
								generatorOptions: settings.generator,
								resolveOptions
							});
						} catch (createDataErr) {
							return callback(/** @type {Error} */ (createDataErr));
						}
						callback();
					});
					this.resolveRequestArray(
						contextInfo,
						this.context,
						useLoadersPost,
						loaderResolver,
						resolveContext,
						(err, result) => {
							postLoaders = result;
							continueCallback(err);
						}
					);
					this.resolveRequestArray(
						contextInfo,
						this.context,
						useLoaders,
						loaderResolver,
						resolveContext,
						(err, result) => {
							normalLoaders = result;
							continueCallback(err);
						}
					);
					this.resolveRequestArray(
						contextInfo,
						this.context,
						useLoadersPre,
						loaderResolver,
						resolveContext,
						(err, result) => {
							preLoaders = result;
							continueCallback(err);
						}
					);
				});

				this.resolveRequestArray(
					contextInfo,
					contextScheme ? this.context : context,
					/** @type {LoaderItem[]} */ (elements),
					loaderResolver,
					resolveContext,
					(err, result) => {
						if (err) return continueCallback(err);
						loaders = result;
						continueCallback();
					}
				);

				/**
				 * @param {string} context context
				 */
				const defaultResolve = context => {
					if (/^($|\?)/.test(unresolvedResource)) {
						resourceData = {
							...cacheParseResource(unresolvedResource),
							resource: unresolvedResource,
							data: {}
						};
						continueCallback();
					}

					// resource without scheme and with path
					else {
						const normalResolver = this.getResolver(
							"normal",
							dependencyType
								? cachedSetProperty(
										resolveOptions || EMPTY_RESOLVE_OPTIONS,
										"dependencyType",
										dependencyType
									)
								: resolveOptions
						);
						this.resolveResource(
							contextInfo,
							context,
							unresolvedResource,
							normalResolver,
							resolveContext,
							(err, _resolvedResource, resolvedResourceResolveData) => {
								if (err) return continueCallback(err);
								if (_resolvedResource !== false) {
									const resolvedResource =
										/** @type {string} */
										(_resolvedResource);
									resourceData = {
										...cacheParseResource(resolvedResource),
										resource: resolvedResource,
										data:
											/** @type {ResolveRequest} */
											(resolvedResourceResolveData)
									};
								}
								continueCallback();
							}
						);
					}
				};

				// resource with scheme
				if (scheme) {
					resourceData = {
						resource: unresolvedResource,
						data: {},
						path: undefined,
						query: undefined,
						fragment: undefined,
						context: undefined
					};
					this.hooks.resolveForScheme
						.for(scheme)
						.callAsync(resourceData, data, err => {
							if (err) return continueCallback(err);
							continueCallback();
						});
				}

				// resource within scheme
				else if (contextScheme) {
					resourceData = {
						resource: unresolvedResource,
						data: {},
						path: undefined,
						query: undefined,
						fragment: undefined,
						context: undefined
					};
					this.hooks.resolveInScheme
						.for(contextScheme)
						.callAsync(resourceData, data, (err, handled) => {
							if (err) return continueCallback(err);
							if (!handled) return defaultResolve(this.context);
							continueCallback();
						});
				}

				// resource without scheme and without path
				else defaultResolve(context);
			}
		);
	}
@param {object} param params
@param {string=} param.context context
@param {InputFileSystem} param.fs file system
@param {ResolverFactory} param.resolverFactory resolverFactory
@param {ModuleOptions} param.options options
@param {AssociatedObjectForCache} param.associatedObjectForCache an object to which the cache will be attached
@param {boolean=} param.layers enable layers