doResolve(hook, request, message, resolveContext, callback) {
		const stackEntry = Resolver.createStackEntry(hook, request);

		/** @type {Set<string> | undefined} */
		let newStack;
		if (resolveContext.stack) {
			newStack = new Set(resolveContext.stack);
			if (resolveContext.stack.has(stackEntry)) {
				/**
				 * Prevent recursion
				 * @type {Error & {recursion?: boolean}}
				 */
				const recursionError = new Error(
					`Recursion in resolving\nStack:\n  ${[...newStack].join("\n  ")}`,
				);
				recursionError.recursion = true;
				if (resolveContext.log) {
					resolveContext.log("abort resolving because of recursion");
				}
				return callback(recursionError);
			}
			newStack.add(stackEntry);
		} else {
			// creating a set with new Set([item])
			// allocates a new array that has to be garbage collected
			// this is an EXTREMELY hot path, so let's avoid it
			newStack = new Set();
			newStack.add(stackEntry);
		}
		this.hooks.resolveStep.call(hook, request);

		if (hook.isUsed()) {
			const innerContext = createInnerContext(
				{
					log: resolveContext.log,
					yield: resolveContext.yield,
					fileDependencies: resolveContext.fileDependencies,
					contextDependencies: resolveContext.contextDependencies,
					missingDependencies: resolveContext.missingDependencies,
					stack: newStack,
				},
				message,
			);
			return hook.callAsync(request, innerContext, (err, result) => {
				if (err) return callback(err);
				if (result) return callback(null, result);
				callback();
			});
		}
		callback();
	}
@param {ResolveStepHook} hook hook
@param {ResolveRequest} request request
@param {null|string} message string
@param {ResolveContext} resolveContext resolver context
@param {(err?: null|Error, result?: ResolveRequest) => void} callback callback
@returns {void}