Repository: webpack/enhanced-resolve
Pull request #331 (2022-02-17 18:02:01)

fix yield
- handle in ignore for AliasPlugin
- handle in ignore for AliasFieldPlugin
- handle for unsafe cache
- handle yield for custom plugins
- use real fs in yield tests
- add tests for AliasField and Alias in yield


codecov[bot]: # [Codecov](https://codecov.io/gh/webpack/enhanced-resolve/pull/331?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=webpack) Report
> Merging [#331](https://codecov.io/gh/webpack/enhanced-resolve/pull/331?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=webpack) (95ef5e9) into [main](https://codecov.io/gh/webpack/enhanced-resolve/commit/b6468e041ddd63b964e70871d0f23ddc2eeb8690?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=webpack) (b6468e0) will **decrease** coverage by `0.64%`.
> The diff coverage is `63.88%`.

[![Impacted file tree graph](https://codecov.io/gh/webpack/enhanced-resolve/pull/331/graphs/tree.svg?width=650&height=150&src=pr&token=tgdM0Gr6Ub&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=webpack)](https://codecov.io/gh/webpack/enhanced-resolve/pull/331?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=webpack)

```diff
@@            Coverage Diff             @@
##             main     #331      +/-   ##
==========================================
- Coverage   94.98%   94.33%   -0.65%     
==========================================
  Files          39       39              
  Lines        1594     1624      +30     
==========================================
+ Hits         1514     1532      +18     
- Misses         80       92      +12     
```


| [Impacted Files](https://codecov.io/gh/webpack/enhanced-resolve/pull/331?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=webpack) | Coverage Δ | |
|---|---|---|
| [lib/UnsafeCachePlugin.js](https://codecov.io/gh/webpack/enhanced-resolve/pull/331/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=webpack#diff-bGliL1Vuc2FmZUNhY2hlUGx1Z2luLmpz) | `60.00% <23.52%> (-34.45%)` | :arrow_down: |
| [lib/AliasFieldPlugin.js](https://codecov.io/gh/webpack/enhanced-resolve/pull/331/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=webpack#diff-bGliL0FsaWFzRmllbGRQbHVnaW4uanM=) | `100.00% <100.00%> (ø)` | |
| [lib/AliasPlugin.js](https://codecov.io/gh/webpack/enhanced-resolve/pull/331/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=webpack#diff-bGliL0FsaWFzUGx1Z2luLmpz) | `100.00% <100.00%> (ø)` | |
| [lib/Resolver.js](https://codecov.io/gh/webpack/enhanced-resolve/pull/331/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=webpack#diff-bGliL1Jlc29sdmVyLmpz) | `88.37% <100.00%> (+1.48%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/webpack/enhanced-resolve/pull/331?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=webpack).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=webpack)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/webpack/enhanced-resolve/pull/331?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=webpack). Last update [b6468e0...95ef5e9](https://codecov.io/gh/webpack/enhanced-resolve/pull/331?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=webpack). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=webpack).


Patch:
diff --git a/lib/AliasFieldPlugin.js b/lib/AliasFieldPlugin.js
index c387db0..4699431 100644
--- a/lib/AliasFieldPlugin.js
+++ b/lib/AliasFieldPlugin.js
@@ -60,6 +60,10 @@ module.exports = class AliasFieldPlugin {
 						...request,
 						path: false
 					};
+					if (typeof resolveContext.yield === "function") {
+						resolveContext.yield(ignoreObj);
+						return callback(null, null);
+					}
 					return callback(null, ignoreObj);
 				}
 				const obj = {
diff --git a/lib/AliasPlugin.js b/lib/AliasPlugin.js
index 6b1367b..34f8259 100644
--- a/lib/AliasPlugin.js
+++ b/lib/AliasPlugin.js
@@ -8,6 +8,7 @@
 const forEachBail = require("./forEachBail");
 
 /** @typedef {import("./Resolver")} Resolver */
+/** @typedef {import("./Resolver").ResolveRequest} ResolveRequest */
 /** @typedef {import("./Resolver").ResolveStepHook} ResolveStepHook */
 /** @typedef {{alias: string|Array<string>|false, name: string, onlyModule?: boolean}} AliasOption */
 
@@ -45,10 +46,15 @@ module.exports = class AliasPlugin {
 							const remainingRequest = innerRequest.substr(item.name.length);
 							const resolveWithAlias = (alias, callback) => {
 								if (alias === false) {
+									/** @type {ResolveRequest} */
 									const ignoreObj = {
 										...request,
 										path: false
 									};
+									if (typeof resolveContext.yield === "function") {
+										resolveContext.yield(ignoreObj);
+										return callback(null, null);
+									}
 									return callback(null, ignoreObj);
 								}
 								if (
diff --git a/lib/Resolver.js b/lib/Resolver.js
index fcdbc0d..5e747a2 100644
--- a/lib/Resolver.js
+++ b/lib/Resolver.js
@@ -275,11 +275,16 @@ class Resolver {
 
 		let yield_;
 		let yieldCalled = false;
+		let finishYield;
 		if (typeof resolveContext.yield === "function") {
 			const old = resolveContext.yield;
 			yield_ = obj => {
-				yieldCalled = true;
 				old(obj);
+				yieldCalled = true;
+			};
+			finishYield = result => {
+				if (result) yield_(result);
+				callback(null);
 			};
 		}
 
@@ -329,8 +334,8 @@ class Resolver {
 				(err, result) => {
 					if (err) return callback(err);
 
+					if (yieldCalled || (result && yield_)) return finishYield(result);
 					if (result) return finishResolved(result);
-					if (yieldCalled) return callback(null);
 
 					return finishWithoutResolve(log);
 				}
@@ -353,8 +358,8 @@ class Resolver {
 				(err, result) => {
 					if (err) return callback(err);
 
+					if (yieldCalled || (result && yield_)) return finishYield(result);
 					if (result) return finishResolved(result);
-					if (yieldCalled) return callback(null);
 
 					// log is missing for the error details
 					// so we redo the resolving for the log info
@@ -372,11 +377,11 @@ class Resolver {
 							yield: yield_,
 							stack: resolveContext.stack
 						},
-						err => {
+						(err, result) => {
 							if (err) return callback(err);
 
 							// In a case that there is a race condition and yield will be called
-							if (yieldCalled) return callback(null);
+							if (yieldCalled || (result && yield_)) return finishYield(result);
 
 							return finishWithoutResolve(log);
 						}
diff --git a/lib/UnsafeCachePlugin.js b/lib/UnsafeCachePlugin.js
index 3c6337a..a2f07f4 100644
--- a/lib/UnsafeCachePlugin.js
+++ b/lib/UnsafeCachePlugin.js
@@ -10,8 +10,9 @@
 /** @typedef {import("./Resolver").ResolveStepHook} ResolveStepHook */
 /** @typedef {{[k: string]: any}} Cache */
 
-function getCacheId(request, withContext) {
+function getCacheId(type, request, withContext) {
 	return JSON.stringify({
+		type,
 		context: withContext ? request.context : "",
 		path: request.path,
 		query: request.query,
@@ -46,18 +47,49 @@ module.exports = class UnsafeCachePlugin {
 			.getHook(this.source)
 			.tapAsync("UnsafeCachePlugin", (request, resolveContext, callback) => {
 				if (!this.filterPredicate(request)) return callback();
-				const cacheId = getCacheId(request, this.withContext);
+				const isYield = typeof resolveContext.yield === "function";
+				const cacheId = getCacheId(
+					isYield ? "yield" : "default",
+					request,
+					this.withContext
+				);
 				const cacheEntry = this.cache[cacheId];
 				if (cacheEntry) {
+					if (isYield) {
+						const yield_ = /** @type {Function} */ (resolveContext.yield);
+						if (Array.isArray(cacheEntry)) {
+							for (const result of cacheEntry) yield_(result);
+						} else {
+							yield_(cacheEntry);
+						}
+						return callback(null, null);
+					}
 					return callback(null, cacheEntry);
 				}
+
+				let yieldFn;
+				let yield_;
+				const yieldResult = [];
+				if (isYield) {
+					yieldFn = resolveContext.yield;
+					yield_ = result => {
+						yieldResult.push(result);
+					};
+				}
+
 				resolver.doResolve(
 					target,
 					request,
 					null,
-					resolveContext,
+					yield_ ? { ...resolveContext, yield: yield_ } : resolveContext,
 					(err, result) => {
 						if (err) return callback(err);
+						if (isYield) {
+							if (result) yieldResult.push(result);
+							for (const result of yieldResult) yieldFn(result);
+							this.cache[cacheId] = yieldResult;
+							return callback(null, null);
+						}
 						if (result) return callback(null, (this.cache[cacheId] = result));
 						callback();
 					}


Test patch:
diff --git a/test/fixtures/yield/a/foo-2/b b/test/fixtures/yield/a/foo-2/b
new file mode 100644
index 0000000..e69de29
diff --git a/test/fixtures/yield/a/foo-2/c b/test/fixtures/yield/a/foo-2/c
new file mode 100644
index 0000000..e69de29
diff --git a/test/fixtures/yield/a/foo/a b/test/fixtures/yield/a/foo/a
new file mode 100644
index 0000000..e69de29
diff --git a/test/fixtures/yield/a/foo/b b/test/fixtures/yield/a/foo/b
new file mode 100644
index 0000000..e69de29
diff --git a/test/fixtures/yield/b/foo/a b/test/fixtures/yield/b/foo/a
new file mode 100644
index 0000000..e69de29
diff --git a/test/fixtures/yield/c/foo/a b/test/fixtures/yield/c/foo/a
new file mode 100644
index 0000000..e69de29
diff --git a/test/fixtures/yield/c/foo/package.json b/test/fixtures/yield/c/foo/package.json
new file mode 100644
index 0000000..0acc4b4
--- /dev/null
+++ b/test/fixtures/yield/c/foo/package.json
@@ -0,0 +1,7 @@
+{
+  "name": "foo",
+  "version": "1.0.0",
+  "browser": {
+    "./a": false
+  }
+}
diff --git a/test/yield.js b/test/yield.js
index dc13d20..2d32b17 100644
--- a/test/yield.js
+++ b/test/yield.js
@@ -1,35 +1,46 @@
 const should = require("should");
+const path = require("path");
+const fs = require("fs");
 
 const { ResolverFactory } = require("../");
-const { Volume } = require("memfs");
+const CachedInputFileSystem = require("../lib/CachedInputFileSystem");
 
 /** @typedef {import("../lib/Resolver").ResolveContext} ResolveContext */
 /** @typedef {ResolveContext & Required<Pick<ResolveContext, 'yield' | 'fileDependencies' | 'contextDependencies' | 'missingDependencies'>>} StrictResolveContext */
 
-const fileSystem = Volume.fromJSON(
-	{
-		"/a/foo/a": "",
-		"/a/foo/b": "",
-		"/a/foo-2/b": "",
-		"/a/foo-2/c": "",
-		"/b/foo/a": ""
-	},
-	"/"
-);
+const nodeFileSystem = new CachedInputFileSystem(fs, 4000);
+const fixtures = path.resolve(__dirname, "fixtures", "yield");
+const makeFixturePaths = paths =>
+	paths.map(pth => (pth ? path.join(fixtures, pth) : pth));
+const contextifyDependencies = paths =>
+	Array.from(paths)
+		.filter(pth => pth.startsWith(fixtures))
+		.map(pth => pth.slice(fixtures.length).split(path.sep).join("/"))
+		.sort();
+const beatifyLogs = logs =>
+	logs.map(l => {
+		const match = /^(\s+)using description file.+(\(relative path:.+\))$/.exec(
+			l
+		);
+		if (match) return `${match[1]}using description file ${match[2]}`;
+		while (l.includes(fixtures)) l = l.replace(fixtures, "fixtures");
+		return l;
+	});
 
 describe("should resolve all aliases", () => {
 	const resolver = ResolverFactory.createResolver({
 		extensions: [".js"],
 		alias: {
-			index: ["/a/foo", "/a/foo-2"]
+			index: makeFixturePaths(["/a/foo", "/a/foo-2"]),
+			foo: false
 		},
-		modules: "/",
-		fileSystem
+		aliasFields: ["browser"],
+		fileSystem: nodeFileSystem
 	});
 	const modulesResolver = ResolverFactory.createResolver({
 		extensions: [".js"],
-		modules: ["a", "b"],
-		fileSystem
+		modules: makeFixturePaths(["a", "b"]),
+		fileSystem: nodeFileSystem
 	});
 
 	it("should yield all b files", done => {
@@ -46,19 +57,19 @@ describe("should resolve all aliases", () => {
 			missingDependencies
 		};
 
-		resolver.resolve({}, "/", "index/b", context, (err, result) => {
+		resolver.resolve({}, fixtures, "index/b", context, (err, result) => {
 			should(err).be.eql(null);
 			should(result).be.eql(undefined);
-			should(paths).be.eql(["/a/foo/b", "/a/foo-2/b"]);
-			should(Array.from(fileDependencies.values()).sort()).be.eql([
-				"/",
+			should(paths).be.eql(makeFixturePaths(["/a/foo/b", "/a/foo-2/b"]));
+			should(contextifyDependencies(fileDependencies)).be.eql([
+				"",
 				"/a",
 				"/a/foo",
 				"/a/foo-2",
 				"/a/foo-2/b",
 				"/a/foo/b"
 			]);
-			should(Array.from(missingDependencies.values()).sort()).be.eql([
+			should(contextifyDependencies(missingDependencies)).be.eql([
 				"/a/foo-2/b",
 				"/a/foo-2/b.js",
 				"/a/foo-2/package.json",
@@ -68,7 +79,7 @@ describe("should resolve all aliases", () => {
 				"/a/package.json",
 				"/package.json"
 			]);
-			should(Array.from(contextDependencies.values()).sort()).be.eql([]);
+			should(Array.from(contextDependencies).sort()).be.eql([]);
 			done();
 		});
 	});
@@ -87,12 +98,12 @@ describe("should resolve all aliases", () => {
 			missingDependencies
 		};
 
-		modulesResolver.resolve({}, "/", "foo/a", context, (err, result) => {
+		modulesResolver.resolve({}, fixtures, "foo/a", context, (err, result) => {
 			should(err).be.eql(null);
 			should(result).be.eql(undefined);
-			should(paths).be.eql(["/a/foo/a", "/b/foo/a"]);
-			should(Array.from(fileDependencies.values()).sort()).be.eql([
-				"/",
+			should(paths).be.eql(makeFixturePaths(["/a/foo/a", "/b/foo/a"]));
+			should(contextifyDependencies(fileDependencies)).be.eql([
+				"",
 				"/a",
 				"/a/foo",
 				"/a/foo/a",
@@ -100,7 +111,7 @@ describe("should resolve all aliases", () => {
 				"/b/foo",
 				"/b/foo/a"
 			]);
-			should(Array.from(missingDependencies.values()).sort()).be.eql([
+			should(contextifyDependencies(missingDependencies)).be.eql([
 				"/a/foo/a",
 				"/a/foo/a.js",
 				"/a/foo/package.json",
@@ -111,7 +122,7 @@ describe("should resolve all aliases", () => {
 				"/b/package.json",
 				"/package.json"
 			]);
-			should(Array.from(contextDependencies.values()).sort()).be.eql([]);
+			should(Array.from(contextDependencies).sort()).be.eql([]);
 			done();
 		});
 	});
@@ -124,10 +135,38 @@ describe("should resolve all aliases", () => {
 			yield: yield_
 		};
 
-		resolver.resolve({}, "/", "index/c", context, (err, result) => {
+		resolver.resolve({}, fixtures, "index/c", context, (err, result) => {
 			should(err).be.eql(null);
 			should(result).be.eql(undefined);
-			should(paths).be.eql(["/a/foo-2/c"]);
+			should(paths).be.eql(makeFixturePaths(["/a/foo-2/c"]));
+			done();
+		});
+	});
+
+	it("should resolve false alias", done => {
+		const paths = [];
+		const yield_ = ({ path }) => paths.push(path);
+		const fileDependencies = new Set();
+		const contextDependencies = new Set();
+		const missingDependencies = new Set();
+		/** @type {ResolveContext} */
+		const context = {
+			yield: yield_,
+			fileDependencies,
+			contextDependencies,
+			missingDependencies
+		};
+
+		resolver.resolve({}, fixtures, "foo", context, (err, result) => {
+			should(err).be.eql(null);
+			should(result).be.eql(undefined);
+			should(paths).be.eql([false]);
+			should(contextifyDependencies(fileDependencies)).be.eql([]);
+			should(contextifyDependencies(missingDependencies)).be.eql([
+				"/node_modules",
+				"/package.json"
+			]);
+			should(Array.from(contextDependencies).sort()).be.eql([]);
 			done();
 		});
 	});
@@ -146,13 +185,13 @@ describe("should resolve all aliases", () => {
 			missingDependencies
 		};
 
-		resolver.resolve({}, "/", "index/unknown", context, (err, result) => {
+		resolver.resolve({}, fixtures, "index/unknown", context, (err, result) => {
 			should(err).not.be.eql(null);
 			should(err).not.be.eql(undefined);
 			should(result).be.eql(undefined);
 			should(paths).be.eql([]);
-			should(Array.from(fileDependencies.values()).sort()).be.eql([]);
-			should(Array.from(missingDependencies.values()).sort()).be.eql([
+			should(contextifyDependencies(fileDependencies)).be.eql([]);
+			should(contextifyDependencies(missingDependencies)).be.eql([
 				"/a/foo-2/package.json",
 				"/a/foo-2/unknown",
 				"/a/foo-2/unknown.js",
@@ -162,8 +201,345 @@ describe("should resolve all aliases", () => {
 				"/a/package.json",
 				"/package.json"
 			]);
-			should(Array.from(contextDependencies.values()).sort()).be.eql([]);
+			should(Array.from(contextDependencies).sort()).be.eql([]);
 			done();
 		});
 	});
+
+	describe("resolve alias field", () => {
+		it("should handle false in alias field", done => {
+			const resolver = ResolverFactory.createResolver({
+				extensions: [".js"],
+				alias: {
+					index: makeFixturePaths(["/c/foo"])
+				},
+				aliasFields: ["browser"],
+				fileSystem: nodeFileSystem
+			});
+			let calls = 0;
+			const paths = [];
+			const logs = [];
+			const yield_ = ({ path }) => {
+				paths.push(path);
+			};
+			const fileDependencies = new Set();
+			const contextDependencies = new Set();
+			const missingDependencies = new Set();
+			/** @type {ResolveContext} */
+			const context = {
+				yield: yield_,
+				fileDependencies,
+				contextDependencies,
+				missingDependencies,
+				log: l => logs.push(l)
+			};
+
+			resolver.resolve({}, fixtures, "index/a", context, (err, result) => {
+				calls++;
+				should(calls).be.eql(1);
+				should(err).be.eql(null);
+				should(result).be.eql(undefined);
+				should(paths).be.eql([false]);
+				should(contextifyDependencies(fileDependencies)).be.eql([
+					"/c/foo/package.json"
+				]);
+				should(contextifyDependencies(missingDependencies)).be.eql([
+					"/c/foo/a",
+					"/c/foo/a.js",
+					"/package.json"
+				]);
+				should(Array.from(contextDependencies).sort()).be.eql([]);
+				should(beatifyLogs(logs)).be.eql([
+					"resolve 'index/a' in 'fixtures'",
+					"  Parsed request is a module",
+					"  using description file (relative path: ./test/fixtures/yield)",
+					`    aliased with mapping 'index': '${["fixtures", "c", "foo"].join(
+						path.sep
+					)}' to '${["fixtures", "c", "foo"].join(path.sep)}/a'`,
+					"      using description file (relative path: ./test/fixtures/yield)",
+					"        using description file (relative path: ./a)",
+					"          .js",
+					`            ${["fixtures", "c", "foo", "a.js"].join(
+						path.sep
+					)} doesn't exist`,
+					"          as directory",
+					`            ${["fixtures", "c", "foo", "a"].join(
+						path.sep
+					)} is not a directory`
+				]);
+				done();
+			});
+		});
+
+		describe("alias + alias field", () => {
+			const createResolver = aliases =>
+				ResolverFactory.createResolver({
+					extensions: [".js"],
+					alias: {
+						index: makeFixturePaths(aliases)
+					},
+					aliasFields: ["browser"],
+					fileSystem: nodeFileSystem
+				});
+			const cLog = [
+				`    aliased with mapping 'index': '${["fixtures", "c", "foo"].join(
+					path.sep
+				)}' to '${["fixtures", "c", "foo"].join(path.sep)}/a'`,
+				"      using description file (relative path: ./test/fixtures/yield)",
+				"        using description file (relative path: ./a)",
+				"          .js",
+				`            ${["fixtures", "c", "foo", "a.js"].join(
+					path.sep
+				)} doesn't exist`,
+				"          as directory",
+				`            ${["fixtures", "c", "foo", "a"].join(
+					path.sep
+				)} is not a directory`
+			];
+			const aLog = [
+				`    aliased with mapping 'index': '${["fixtures", "a", "foo"].join(
+					path.sep
+				)}' to '${["fixtures", "a", "foo"].join(path.sep)}/a'`,
+				"      using description file (relative path: ./test/fixtures/yield)",
+				"        using description file (relative path: ./test/fixtures/yield/a/foo/a)",
+				"          no extension",
+				`            existing file: ${["fixtures", "a", "foo", "a"].join(
+					path.sep
+				)}`,
+				`              reporting result ${["fixtures", "a", "foo", "a"].join(
+					path.sep
+				)}`,
+				"          .js",
+				`            ${["fixtures", "a", "foo", "a.js"].join(
+					path.sep
+				)} doesn't exist`,
+				"          as directory",
+				`            ${["fixtures", "a", "foo", "a"].join(
+					path.sep
+				)} is not a directory`
+			];
+			let resolver;
+
+			it("default order", done => {
+				resolver = createResolver(["/c/foo", "/a/foo"]);
+				run(
+					done,
+					[false, "/a/foo/a"],
+					[
+						"resolve 'index/a' in 'fixtures'",
+						"  Parsed request is a module",
+						"  using description file (relative path: ./test/fixtures/yield)",
+						...cLog,
+						...aLog
+					]
+				);
+			});
+
+			it("reverse order", done => {
+				resolver = createResolver(["/a/foo", "/c/foo"]);
+				run(
+					done,
+					["/a/foo/a", false],
+					[
+						"resolve 'index/a' in 'fixtures'",
+						"  Parsed request is a module",
+						"  using description file (relative path: ./test/fixtures/yield)",
+						...aLog,
+						...cLog
+					]
+				);
+			});
+
+			function run(done, expectedResult, expectedLogs) {
+				let calls = 0;
+				const paths = [];
+				const logs = [];
+				const yield_ = ({ path }) => paths.push(path);
+				const fileDependencies = new Set();
+				const contextDependencies = new Set();
+				const missingDependencies = new Set();
+				/** @type {ResolveContext} */
+				const context = {
+					yield: yield_,
+					fileDependencies,
+					contextDependencies,
+					missingDependencies,
+					log: l => logs.push(l)
+				};
+
+				resolver.resolve({}, fixtures, "index/a", context, (err, result) => {
+					calls++;
+					should(calls).be.eql(1);
+					should(err).be.eql(null);
+					should(result).be.eql(undefined);
+					should(paths).be.eql(makeFixturePaths(expectedResult));
+					should(contextifyDependencies(fileDependencies)).be.eql([
+						"",
+						"/a",
+						"/a/foo",
+						"/a/foo/a",
+						"/c/foo/package.json"
+					]);
+					should(contextifyDependencies(missingDependencies)).be.eql([
+						"/a/foo/a",
+						"/a/foo/a.js",
+						"/a/foo/package.json",
+						"/a/package.json",
+						"/c/foo/a",
+						"/c/foo/a.js",
+						"/package.json"
+					]);
+					should(Array.from(contextDependencies).sort()).be.eql([]);
+					should(beatifyLogs(logs)).be.eql(expectedLogs);
+					done();
+				});
+			}
+		});
+
+		describe("custom plugins", () => {
+			const createResolver = plugin =>
+				ResolverFactory.createResolver({
+					extensions: [".js"],
+					plugins: [plugin],
+					fileSystem: nodeFileSystem
+				});
+
+			it("should correctly handle resolve in callback", done => {
+				const getResult = request => ({ ...request, path: "/a" });
+				const resolver = createResolver({
+					apply(resolver) {
+						resolver
+							.getHook("described-resolve")
+							.tapAsync(
+								"MyResolverPlugin",
+								(request, resolveContext, callback) => {
+									callback(null, getResult(request));
+								}
+							);
+					}
+				});
+				const paths = [];
+				const context = {
+					yield: obj => paths.push(obj.path)
+				};
+				resolver.resolve({}, fixtures, "unknown", context, (err, result) => {
+					if (err) done(err);
+					should(err).be.eql(null);
+					should(result).be.eql(undefined);
+					should(paths).be.eql(["/a"]);
+					done();
+				});
+			});
+
+			it("should correctly handle error in callback", done => {
+				const resolver = createResolver({
+					apply(resolver) {
+						resolver
+							.getHook("described-resolve")
+							.tapAsync("MyResolverPlugin", (_, __, callback) =>
+								callback(new Error("error"))
+							);
+					}
+				});
+				const paths = [];
+				const context = {
+					yield: obj => paths.push(obj.path)
+				};
+				resolver.resolve({}, fixtures, "unknown", context, (err, result) => {
+					should(err).not.be.eql(null);
+					should(/** @type {Error} */ (err).message).be.eql("error");
+					should(result).be.eql(undefined);
+					should(paths).be.eql([]);
+					done();
+				});
+			});
+		});
+
+		describe("unsafe cache", () => {
+			// same case as in "should yield all b files"
+			it("should return result from cache", done => {
+				const cache = {};
+				const resolver = ResolverFactory.createResolver({
+					extensions: [".js"],
+					alias: {
+						index: makeFixturePaths(["/a/foo", "/a/foo-2"])
+					},
+					unsafeCache: cache,
+					fileSystem: nodeFileSystem
+				});
+				resolver.resolve({}, fixtures, "index/b", { yield: () => {} }, err => {
+					if (err) done(err);
+					const paths = [];
+
+					resolver.resolve(
+						{},
+						fixtures,
+						"index/b",
+						{ yield: obj => paths.push(obj.path) },
+						(err, result) => {
+							should(err).be.eql(null);
+							should(result).be.eql(undefined);
+							should(paths).be.eql(
+								makeFixturePaths(["/a/foo/b", "/a/foo-2/b"])
+							);
+							// original + 2 aliases
+							should(Object.keys(cache)).have.length(3);
+							const cacheId = Object.keys(cache).find(id => {
+								const { request } = JSON.parse(id);
+								return request === "index/b";
+							});
+							should(cacheId).not.be.eql(undefined);
+							should(
+								Array.isArray(cache[/** @type {string} */ (cacheId)])
+							).be.eql(true);
+							should(
+								cache[/** @type {string} */ (cacheId)].map(o => o.path)
+							).be.eql(makeFixturePaths(["/a/foo/b", "/a/foo-2/b"]));
+							done();
+						}
+					);
+				});
+			});
+
+			// same as "should handle false in alias field"
+			it("should return ignore result from cache", done => {
+				const cache = {};
+				const resolver = ResolverFactory.createResolver({
+					extensions: [".js"],
+					alias: {
+						foo: false
+					},
+					unsafeCache: cache,
+					fileSystem: nodeFileSystem
+				});
+				resolver.resolve({}, fixtures, "foo", { yield: () => {} }, err => {
+					if (err) done(err);
+					const paths = [];
+
+					resolver.resolve(
+						{},
+						fixtures,
+						"foo",
+						{ yield: obj => paths.push(obj.path) },
+						(err, result) => {
+							should(err).be.eql(null);
+							should(result).be.eql(undefined);
+							should(paths).be.eql([false]);
+							// original + 0 aliases
+							should(Object.keys(cache)).have.length(1);
+							const cacheId = Object.keys(cache)[0];
+							should(cacheId).not.be.eql(undefined);
+							should(
+								Array.isArray(cache[/** @type {string} */ (cacheId)])
+							).be.eql(true);
+							should(
+								cache[/** @type {string} */ (cacheId)].map(o => o.path)
+							).be.eql([false]);
+							done();
+						}
+					);
+				});
+			});
+		});
+	});
 });