Repository: webpack/enhanced-resolve
Pull request #353 (2022-07-26 11:17:31)

support wildcards pattern with common suffix in imports/exports field
solve https://github.com/webpack/enhanced-resolve/issues/352

linux-foundation-easycla[bot]:  <a href="https://api.easycla.lfx.linuxfoundation.org/v2/repository-provider/github/sign/18915764/5400204/353/#/?version=2"><img src="https://s3.amazonaws.com/cla-project-logo-prod/cla-not-signed.svg" alt="CLA Not Signed" align="left" height="28" width="328"></a><br/><br /><ul><li><a href='https://api.easycla.lfx.linuxfoundation.org/v2/repository-provider/github/sign/18915764/5400204/353/#/?version=2' target='_blank'>:x:</a> - login: @bvanjoi / name: bohanzh . The commit (ae049297da4879bbf523432fee886610b94e712b) is not authorized under a signed CLA. <a href='https://api.easycla.lfx.linuxfoundation.org/v2/repository-provider/github/sign/18915764/5400204/353/#/?version=2' target='_blank'>Please click here to be authorized</a>. For further assistance with EasyCLA, <a href='https://jira.linuxfoundation.org/servicedesk/customer/portal/4' target='_blank'>please submit a support request ticket</a>.</li></ul>

Patch:
diff --git a/README.md b/README.md
index a710610..644305c 100644
--- a/README.md
+++ b/README.md
@@ -63,10 +63,10 @@ const myResolver = ResolverFactory.createResolver({
 
 // resolve a file with the new resolver
 const context = {};
+const resolveContext = {};
 const lookupStartPath = "/Users/webpack/some/root/dir";
 const request = "./path/to-look-up.js";
-const resolveContext = {};
-myResolver.resolve(context, lookupStartPath, request, resolveContext, (
+myResolver.resolve({}, lookupStartPath, request, resolveContext, (
 	err /*Error*/,
 	filepath /*string*/
 ) => {
@@ -83,7 +83,7 @@ myResolver.resolve(context, lookupStartPath, request, resolveContext, (
 | extensionAlias   | {}                          | An object which maps extension to extension aliases                                                                                                       |
 | cachePredicate   | function() { return true }; | A function which decides whether a request should be cached or not. An object is passed to the function with `path` and `request` properties.             |
 | cacheWithContext | true                        | If unsafe cache is enabled, includes `request.context` in the cache key                                                                                   |
-| conditionNames   | []                          | A list of exports field condition names                                                                                                                   |
+| conditionNames   | ["node"]                    | A list of exports field condition names                                                                                                                   |
 | descriptionFiles | ["package.json"]            | A list of description files to read from                                                                                                                  |
 | enforceExtension | false                       | Enforce that a extension from extensions must be used                                                                                                     |
 | exportsFields    | ["exports"]                 | A list of exports fields in description files                                                                                                             |
diff --git a/lib/Resolver.js b/lib/Resolver.js
index 9395144..875b435 100644
--- a/lib/Resolver.js
+++ b/lib/Resolver.js
@@ -45,18 +45,10 @@ const {
  * @param {T=} result
  */
 
-/** @typedef {function((NodeJS.ErrnoException | null)=, (string | Buffer)[] | IDirent[]=): void} DirentArrayCallback */
-
-/**
- * @typedef {Object} ReaddirOptions
- * @property {BufferEncoding | null | 'buffer'} [encoding]
- * @property {boolean | undefined} [withFileTypes=false]
- */
-
 /**
  * @typedef {Object} FileSystem
  * @property {(function(string, FileSystemCallback<Buffer | string>): void) & function(string, object, FileSystemCallback<Buffer | string>): void} readFile
- * @property {function(string, (ReaddirOptions | BufferEncoding | null | undefined | 'buffer' | DirentArrayCallback)=,  DirentArrayCallback=): void} readdir
+ * @property {(function(string, FileSystemCallback<(Buffer | string)[] | FileSystemDirent[]>): void) & function(string, object, FileSystemCallback<(Buffer | string)[] | FileSystemDirent[]>): void} readdir
  * @property {((function(string, FileSystemCallback<object>): void) & function(string, object, FileSystemCallback<object>): void)=} readJson
  * @property {(function(string, FileSystemCallback<Buffer | string>): void) & function(string, object, FileSystemCallback<Buffer | string>): void} readlink
  * @property {(function(string, FileSystemCallback<FileSystemStats>): void) & function(string, object, FileSystemCallback<Buffer | string>): void=} lstat
diff --git a/lib/util/entrypoints.js b/lib/util/entrypoints.js
index b925d62..36e335e 100644
--- a/lib/util/entrypoints.js
+++ b/lib/util/entrypoints.js
@@ -11,14 +11,6 @@
 /** @typedef {Record<string, MappingValue>|ConditionalMapping|DirectMapping} ExportsField */
 /** @typedef {Record<string, MappingValue>} ImportsField */
 
-/**
- * @typedef {Object} PathTreeNode
- * @property {Map<string, PathTreeNode>|null} children
- * @property {MappingValue} folder
- * @property {Map<string, MappingValue>|null} wildcards
- * @property {Map<string, MappingValue>} files
- */
-
 /**
  * Processing exports/imports field
  * @callback FieldProcessor
@@ -83,6 +75,7 @@ Conditional mapping nested in another conditional mapping is called nested mappi
 const slashCode = "/".charCodeAt(0);
 const dotCode = ".".charCodeAt(0);
 const hashCode = "#".charCodeAt(0);
+const patternRegEx = /\*/g;
 
 /**
  * @param {ExportsField} exportsField the exports field
@@ -92,7 +85,8 @@ module.exports.processExportsField = function processExportsField(
 	exportsField
 ) {
 	return createFieldProcessor(
-		buildExportsFieldPathTree(exportsField),
+		buildExportsField(exportsField),
+		request => (request.length === 0 ? "." : "./" + request),
 		assertExportsFieldRequest,
 		assertExportTarget
 	);
@@ -106,27 +100,34 @@ module.exports.processImportsField = function processImportsField(
 	importsField
 ) {
 	return createFieldProcessor(
-		buildImportsFieldPathTree(importsField),
+		buildImportsField(importsField),
+		request => "#" + request,
 		assertImportsFieldRequest,
 		assertImportTarget
 	);
 };
 
 /**
- * @param {PathTreeNode} treeRoot root
+ * @param {ExportsField | ImportsField} field root
+ * @param {(s: string) => string} normalizeRequest Normalize request, for `imports` field it adds `#`, for `exports` field it adds `.` or `./`
  * @param {(s: string) => string} assertRequest assertRequest
  * @param {(s: string, f: boolean) => void} assertTarget assertTarget
  * @returns {FieldProcessor} field processor
  */
-function createFieldProcessor(treeRoot, assertRequest, assertTarget) {
+function createFieldProcessor(
+	field,
+	normalizeRequest,
+	assertRequest,
+	assertTarget
+) {
 	return function fieldProcessor(request, conditionNames) {
 		request = assertRequest(request);
 
-		const match = findMatch(request, treeRoot);
+		const match = findMatch(normalizeRequest(request), field);
 
 		if (match === null) return [];
 
-		const [mapping, remainRequestIndex] = match;
+		const [mapping, remainingRequest, isSubpathMapping, isPattern] = match;
 
 		/** @type {DirectMapping|null} */
 		let direct = null;
@@ -143,16 +144,10 @@ function createFieldProcessor(treeRoot, assertRequest, assertTarget) {
 			direct = /** @type {DirectMapping} */ (mapping);
 		}
 
-		const remainingRequest =
-			remainRequestIndex === request.length + 1
-				? undefined
-				: remainRequestIndex < 0
-				? request.slice(-remainRequestIndex - 1)
-				: request.slice(remainRequestIndex);
-
 		return directMapping(
 			remainingRequest,
-			remainRequestIndex < 0,
+			isPattern,
+			isSubpathMapping,
 			direct,
 			conditionNames,
 			assertTarget
@@ -251,101 +246,87 @@ function assertImportTarget(imp, expectFolder) {
 	}
 }
 
+function patternKeyCompare(a, b) {
+	const aPatternIndex = a.indexOf("*");
+	const bPatternIndex = b.indexOf("*");
+	const baseLenA = aPatternIndex === -1 ? a.length : aPatternIndex + 1;
+	const baseLenB = bPatternIndex === -1 ? b.length : bPatternIndex + 1;
+
+	if (baseLenA > baseLenB) return -1;
+	if (baseLenB > baseLenA) return 1;
+	if (aPatternIndex === -1) return 1;
+	if (bPatternIndex === -1) return -1;
+	if (a.length > b.length) return -1;
+	if (b.length > a.length) return 1;
+
+	return 0;
+}
+
 /**
  * Trying to match request to field
  * @param {string} request request
- * @param {PathTreeNode} treeRoot path tree root
- * @returns {[MappingValue, number]|null} match or null, number is negative and one less when it's a folder mapping, number is request.length + 1 for direct mappings
+ * @param {ExportsField | ImportsField} field exports or import field
+ * @returns {[MappingValue, string, boolean, boolean]|null} match or null, number is negative and one less when it's a folder mapping, number is request.length + 1 for direct mappings
  */
-function findMatch(request, treeRoot) {
-	if (request.length === 0) {
-		const value = treeRoot.files.get("");
-
-		return value ? [value, 1] : null;
-	}
-
+function findMatch(request, field) {
 	if (
-		treeRoot.children === null &&
-		treeRoot.folder === null &&
-		treeRoot.wildcards === null
+		Object.prototype.hasOwnProperty.call(field, request) &&
+		!request.includes("*") &&
+		!request.endsWith("/")
 	) {
-		const value = treeRoot.files.get(request);
+		const target = field[request];
 
-		return value ? [value, request.length + 1] : null;
+		return [target, "", false, false];
 	}
 
-	let node = treeRoot;
-	let lastNonSlashIndex = 0;
-	let slashIndex = request.indexOf("/", 0);
-
-	/** @type {[MappingValue, number]|null} */
-	let lastFolderMatch = null;
-
-	const applyFolderMapping = () => {
-		const folderMapping = node.folder;
-		if (folderMapping) {
-			if (lastFolderMatch) {
-				lastFolderMatch[0] = folderMapping;
-				lastFolderMatch[1] = -lastNonSlashIndex - 1;
-			} else {
-				lastFolderMatch = [folderMapping, -lastNonSlashIndex - 1];
-			}
-		}
-	};
+	let bestMatch = "";
+	let bestMatchSubpath;
 
-	const applyWildcardMappings = (wildcardMappings, remainingRequest) => {
-		if (wildcardMappings) {
-			for (const [key, target] of wildcardMappings) {
-				if (remainingRequest.startsWith(key)) {
-					if (!lastFolderMatch) {
-						lastFolderMatch = [target, lastNonSlashIndex + key.length];
-					} else if (lastFolderMatch[1] < lastNonSlashIndex + key.length) {
-						lastFolderMatch[0] = target;
-						lastFolderMatch[1] = lastNonSlashIndex + key.length;
-					}
-				}
+	const keys = Object.getOwnPropertyNames(field);
+
+	for (let i = 0; i < keys.length; i++) {
+		const key = keys[i];
+		const patternIndex = key.indexOf("*");
+
+		if (patternIndex !== -1 && request.startsWith(key.slice(0, patternIndex))) {
+			const patternTrailer = key.slice(patternIndex + 1);
+
+			if (
+				request.length >= key.length &&
+				request.endsWith(patternTrailer) &&
+				patternKeyCompare(bestMatch, key) === 1 &&
+				key.lastIndexOf("*") === patternIndex
+			) {
+				bestMatch = key;
+				bestMatchSubpath = request.slice(
+					patternIndex,
+					request.length - patternTrailer.length
+				);
 			}
 		}
-	};
-
-	while (slashIndex !== -1) {
-		applyFolderMapping();
-
-		const wildcardMappings = node.wildcards;
-
-		if (!wildcardMappings && node.children === null) return lastFolderMatch;
-
-		const folder = request.slice(lastNonSlashIndex, slashIndex);
-
-		applyWildcardMappings(wildcardMappings, folder);
-
-		if (node.children === null) return lastFolderMatch;
-
-		const newNode = node.children.get(folder);
-
-		if (!newNode) {
-			return lastFolderMatch;
+		// For legacy `./foo/`
+		else if (
+			key[key.length - 1] === "/" &&
+			request.startsWith(key) &&
+			patternKeyCompare(bestMatch, key) === 1
+		) {
+			bestMatch = key;
+			bestMatchSubpath = request.slice(key.length);
 		}
-
-		node = newNode;
-		lastNonSlashIndex = slashIndex + 1;
-		slashIndex = request.indexOf("/", lastNonSlashIndex);
-	}
-
-	const remainingRequest =
-		lastNonSlashIndex > 0 ? request.slice(lastNonSlashIndex) : request;
-
-	const value = node.files.get(remainingRequest);
-
-	if (value) {
-		return [value, request.length + 1];
 	}
 
-	applyFolderMapping();
+	if (bestMatch === "") return null;
 
-	applyWildcardMappings(node.wildcards, remainingRequest);
+	const target = field[bestMatch];
+	const isSubpathMapping = bestMatch.endsWith("/");
+	const isPattern = bestMatch.includes("*");
 
-	return lastFolderMatch;
+	return [
+		target,
+		/** @type {string} */ (bestMatchSubpath),
+		isSubpathMapping,
+		isPattern
+	];
 }
 
 /**
@@ -360,7 +341,8 @@ function isConditionalMapping(mapping) {
 
 /**
  * @param {string|undefined} remainingRequest remaining request when folder mapping, undefined for file mappings
- * @param {boolean} subpathMapping true, for subpath mappings
+ * @param {boolean} isPattern true, if mapping is a pattern (contains "*")
+ * @param {boolean} isSubpathMapping true, for subpath mappings
  * @param {DirectMapping|null} mappingTarget direct export
  * @param {Set<string>} conditionNames condition names
  * @param {(d: string, f: boolean) => void} assert asserting direct value
@@ -368,7 +350,8 @@ function isConditionalMapping(mapping) {
  */
 function directMapping(
 	remainingRequest,
-	subpathMapping,
+	isPattern,
+	isSubpathMapping,
 	mappingTarget,
 	conditionNames,
 	assert
@@ -377,7 +360,13 @@ function directMapping(
 
 	if (typeof mappingTarget === "string") {
 		return [
-			targetMapping(remainingRequest, subpathMapping, mappingTarget, assert)
+			targetMapping(
+				remainingRequest,
+				isPattern,
+				isSubpathMapping,
+				mappingTarget,
+				assert
+			)
 		];
 	}
 
@@ -386,7 +375,13 @@ function directMapping(
 	for (const exp of mappingTarget) {
 		if (typeof exp === "string") {
 			targets.push(
-				targetMapping(remainingRequest, subpathMapping, exp, assert)
+				targetMapping(
+					remainingRequest,
+					isPattern,
+					isSubpathMapping,
+					exp,
+					assert
+				)
 			);
 			continue;
 		}
@@ -395,7 +390,8 @@ function directMapping(
 		if (!mapping) continue;
 		const innerExports = directMapping(
 			remainingRequest,
-			subpathMapping,
+			isPattern,
+			isSubpathMapping,
 			mapping,
 			conditionNames,
 			assert
@@ -410,27 +406,43 @@ function directMapping(
 
 /**
  * @param {string|undefined} remainingRequest remaining request when folder mapping, undefined for file mappings
- * @param {boolean} subpathMapping true, for subpath mappings
+ * @param {boolean} isPattern true, if mapping is a pattern (contains "*")
+ * @param {boolean} isSubpathMapping true, for subpath mappings
  * @param {string} mappingTarget direct export
  * @param {(d: string, f: boolean) => void} assert asserting direct value
  * @returns {string} mapping result
  */
 function targetMapping(
 	remainingRequest,
-	subpathMapping,
+	isPattern,
+	isSubpathMapping,
 	mappingTarget,
 	assert
 ) {
 	if (remainingRequest === undefined) {
 		assert(mappingTarget, false);
+
 		return mappingTarget;
 	}
-	if (subpathMapping) {
+
+	if (isSubpathMapping) {
 		assert(mappingTarget, true);
+
 		return mappingTarget + remainingRequest;
 	}
+
 	assert(mappingTarget, false);
-	return mappingTarget.replace(/\*/g, remainingRequest.replace(/\$/g, "$$"));
+
+	let result = mappingTarget;
+
+	if (isPattern) {
+		result = result.replace(
+			patternRegEx,
+			remainingRequest.replace(/\$/g, "$$")
+		);
+	}
+
+	return result;
 }
 
 /**
@@ -487,93 +499,14 @@ function conditionalMapping(conditionalMapping_, conditionNames) {
 	return null;
 }
 
-/**
- * Internal helper to create path tree node
- * to ensure that each node gets the same hidden class
- * @returns {PathTreeNode} node
- */
-function createNode() {
-	return {
-		children: null,
-		folder: null,
-		wildcards: null,
-		files: new Map()
-	};
-}
-
-/**
- * Internal helper for building path tree
- * @param {PathTreeNode} root root
- * @param {string} path path
- * @param {MappingValue} target target
- */
-function walkPath(root, path, target) {
-	if (path.length === 0) {
-		root.folder = target;
-		return;
-	}
-
-	let node = root;
-	// Typical path tree can looks like
-	// root
-	// - files: ["a.js", "b.js"]
-	// - children:
-	//    node1:
-	//    - files: ["a.js", "b.js"]
-	let lastNonSlashIndex = 0;
-	let slashIndex = path.indexOf("/", 0);
-
-	while (slashIndex !== -1) {
-		const folder = path.slice(lastNonSlashIndex, slashIndex);
-		let newNode;
-
-		if (node.children === null) {
-			newNode = createNode();
-			node.children = new Map();
-			node.children.set(folder, newNode);
-		} else {
-			newNode = node.children.get(folder);
-
-			if (!newNode) {
-				newNode = createNode();
-				node.children.set(folder, newNode);
-			}
-		}
-
-		node = newNode;
-		lastNonSlashIndex = slashIndex + 1;
-		slashIndex = path.indexOf("/", lastNonSlashIndex);
-	}
-
-	if (lastNonSlashIndex >= path.length) {
-		node.folder = target;
-	} else {
-		const file = lastNonSlashIndex > 0 ? path.slice(lastNonSlashIndex) : path;
-		if (file.endsWith("*")) {
-			if (node.wildcards === null) node.wildcards = new Map();
-			node.wildcards.set(file.slice(0, -1), target);
-		} else {
-			node.files.set(file, target);
-		}
-	}
-}
-
 /**
  * @param {ExportsField} field exports field
- * @returns {PathTreeNode} tree root
+ * @returns {ExportsField} normalized exports field
  */
-function buildExportsFieldPathTree(field) {
-	const root = createNode();
-
+function buildExportsField(field) {
 	// handle syntax sugar, if exports field is direct mapping for "."
-	if (typeof field === "string") {
-		root.files.set("", field);
-
-		return root;
-	} else if (Array.isArray(field)) {
-		root.files.set("", field.slice());
-
-		return root;
+	if (typeof field === "string" || Array.isArray(field)) {
+		return { ".": field };
 	}
 
 	const keys = Object.keys(field);
@@ -596,8 +529,7 @@ function buildExportsFieldPathTree(field) {
 					i++;
 				}
 
-				root.files.set("", field);
-				return root;
+				return { ".": field };
 			}
 
 			throw new Error(
@@ -608,7 +540,6 @@ function buildExportsFieldPathTree(field) {
 		}
 
 		if (key.length === 1) {
-			root.files.set("", field[key]);
 			continue;
 		}
 
@@ -619,20 +550,16 @@ function buildExportsFieldPathTree(field) {
 				)})`
 			);
 		}
-
-		walkPath(root, key.slice(2), field[key]);
 	}
 
-	return root;
+	return field;
 }
 
 /**
  * @param {ImportsField} field imports field
- * @returns {PathTreeNode} root
+ * @returns {ImportsField} normalized imports field
  */
-function buildImportsFieldPathTree(field) {
-	const root = createNode();
-
+function buildImportsField(field) {
 	const keys = Object.keys(field);
 
 	for (let i = 0; i < keys.length; i++) {
@@ -659,9 +586,7 @@ function buildImportsFieldPathTree(field) {
 				)})`
 			);
 		}
-
-		walkPath(root, key.slice(1), field[key]);
 	}
 
-	return root;
+	return field;
 }
diff --git a/types.d.ts b/types.d.ts
index 2c0b668..0143653 100644
--- a/types.d.ts
+++ b/types.d.ts
@@ -45,31 +45,17 @@ declare class CachedInputFileSystem {
 		): void;
 	};
 	statSync: (arg0: string, arg1?: object) => FileSystemStats;
-	readdir: (
-		arg0: string,
-		arg1?:
-			| null
-			| ((
-					arg0?: null | NodeJS.ErrnoException,
-					arg1?: (string | Buffer)[] | any[]
-			  ) => void)
-			| ReaddirOptions
-			| "ascii"
-			| "utf8"
-			| "utf-8"
-			| "utf16le"
-			| "ucs2"
-			| "ucs-2"
-			| "base64"
-			| "latin1"
-			| "binary"
-			| "hex"
-			| "buffer",
-		arg2?: (
-			arg0?: null | NodeJS.ErrnoException,
-			arg1?: (string | Buffer)[] | any[]
-		) => void
-	) => void;
+	readdir: {
+		(
+			arg0: string,
+			arg1: FileSystemCallback<(string | Buffer)[] | FileSystemDirent[]>
+		): void;
+		(
+			arg0: string,
+			arg1: object,
+			arg2: FileSystemCallback<(string | Buffer)[] | FileSystemDirent[]>
+		): void;
+	};
 	readdirSync: (
 		arg0: string,
 		arg1?: object
@@ -121,31 +107,17 @@ declare interface FileSystem {
 			arg2: FileSystemCallback<string | Buffer>
 		): void;
 	};
-	readdir: (
-		arg0: string,
-		arg1?:
-			| null
-			| ((
-					arg0?: null | NodeJS.ErrnoException,
-					arg1?: (string | Buffer)[] | any[]
-			  ) => void)
-			| ReaddirOptions
-			| "ascii"
-			| "utf8"
-			| "utf-8"
-			| "utf16le"
-			| "ucs2"
-			| "ucs-2"
-			| "base64"
-			| "latin1"
-			| "binary"
-			| "hex"
-			| "buffer",
-		arg2?: (
-			arg0?: null | NodeJS.ErrnoException,
-			arg1?: (string | Buffer)[] | any[]
-		) => void
-	) => void;
+	readdir: {
+		(
+			arg0: string,
+			arg1: FileSystemCallback<(string | Buffer)[] | FileSystemDirent[]>
+		): void;
+		(
+			arg0: string,
+			arg1: object,
+			arg2: FileSystemCallback<(string | Buffer)[] | FileSystemDirent[]>
+		): void;
+	};
 	readJson?: {
 		(arg0: string, arg1: FileSystemCallback<object>): void;
 		(arg0: string, arg1: object, arg2: FileSystemCallback<object>): void;
@@ -213,22 +185,6 @@ declare interface PossibleFileSystemError {
 	path?: string;
 	syscall?: string;
 }
-declare interface ReaddirOptions {
-	encoding?:
-		| null
-		| "ascii"
-		| "utf8"
-		| "utf-8"
-		| "utf16le"
-		| "ucs2"
-		| "ucs-2"
-		| "base64"
-		| "latin1"
-		| "binary"
-		| "hex"
-		| "buffer";
-	withFileTypes?: boolean;
-}
 
 /**
  * Resolve context
diff --git a/yarn.lock b/yarn.lock
index c2c1c8f..72cf4c5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1933,9 +1933,11 @@ json-stringify-safe@^5.0.1:
   integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
 
 json5@^2.1.2:
-  version "2.2.3"
-  resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
-  integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43"
+  integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==
+  dependencies:
+    minimist "^1.2.5"
 
 jsonfile@^6.0.1:
   version "6.1.0"
@@ -2104,9 +2106,9 @@ minimatch@3.0.4, minimatch@^3.0.4:
     brace-expansion "^1.1.7"
 
 minimist@^1.2.5:
-  version "1.2.8"
-  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
-  integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
+  version "1.2.5"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
+  integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
 
 mkdirp@^0.5.1:
   version "0.5.5"


Test patch:
diff --git a/test/exportsField.js b/test/exportsField.js
index d2f2d68..035fc75 100644
--- a/test/exportsField.js
+++ b/test/exportsField.js
@@ -594,6 +594,56 @@ describe("Process exports field", function exportsField() {
 				[]
 			]
 		},
+		{
+			name: "Direct mapping #11",
+			expect: ["./foo.js"],
+			suite: [
+				{
+					"./": "./",
+					"./*": "./*",
+					"./dist/index.js": "./dist/index.js"
+				},
+				"./foo.js",
+				[]
+			]
+		},
+		{
+			name: "Direct mapping #12",
+			expect: ["./foo/bar/baz.js"],
+			suite: [
+				{
+					"./": "./",
+					"./*": "./*",
+					"./dist/index.js": "./dist/index.js"
+				},
+				"./foo/bar/baz.js",
+				[]
+			]
+		},
+		{
+			name: "Direct mapping #13",
+			expect: ["./foo/bar/baz.js"],
+			suite: [
+				{
+					"./": "./",
+					"./dist/index.js": "./dist/index.js"
+				},
+				"./foo/bar/baz.js",
+				[]
+			]
+		},
+		{
+			name: "Direct mapping #14",
+			expect: ["./foo/bar/baz.js"],
+			suite: [
+				{
+					"./*": "./*",
+					"./dist/index.js": "./dist/index.js"
+				},
+				"./foo/bar/baz.js",
+				[]
+			]
+		},
 		//#endregion
 
 		//#region Direct and conditional mapping
@@ -1953,6 +2003,212 @@ describe("Process exports field", function exportsField() {
 				"./a/b/d/c.js",
 				[]
 			]
+		},
+		{
+			name: "wildcard pattern #1",
+			expect: ["./A/b.js"],
+			suite: [
+				{
+					"./a/*.js": "./A/*.js"
+				},
+				"./a/b.js",
+				[]
+			]
+		},
+		{
+			name: "wildcard pattern #2",
+			expect: ["./A/b/c.js"],
+			suite: [
+				{
+					"./a/*.js": "./A/*.js"
+				},
+				"./a/b/c.js",
+				[]
+			]
+		},
+		{
+			name: "wildcard pattern #3",
+			expect: ["./A/b/c.js"],
+			suite: [
+				{
+					"./a/*/c.js": "./A/*/c.js"
+				},
+				"./a/b/c.js",
+				[]
+			]
+		},
+		{
+			name: "wildcard pattern #4",
+			expect: ["./A/b/b.js"],
+			suite: [
+				{
+					"./a/*/c.js": "./A/*/*.js"
+				},
+				"./a/b/c.js",
+				[]
+			]
+		},
+		{
+			name: "wildcard pattern #5",
+			expect: ["./browser/index.js"],
+			suite: [
+				{
+					"./lib/*": {
+						browser: ["./browser/*"]
+					},
+					"./dist/*.js": {
+						node: "./*.js",
+						default: "./browser/*.js"
+					}
+				},
+				"./dist/index.js",
+				["browser"]
+			]
+		},
+		{
+			name: "wildcard pattern #5",
+			expect: ["./browser/index.js"],
+			suite: [
+				{
+					"./lib/*": {
+						browser: ["./browser/*"]
+					},
+					"./dist/*.js": {
+						node: "./*.js",
+						default: "./browser/*.js"
+					}
+				},
+				"./lib/index.js",
+				["browser"]
+			]
+		},
+		{
+			name: "wildcard pattern #6",
+			expect: ["./browser/foo/bar.js"],
+			suite: [
+				{
+					"./lib/*/bar.js": {
+						browser: ["./browser/*/bar.js"]
+					},
+					"./dist/*/bar.js": {
+						node: "./*.js",
+						default: "./browser/*.js"
+					}
+				},
+				"./lib/foo/bar.js",
+				["browser"]
+			]
+		},
+		{
+			name: "wildcard pattern #6",
+			expect: ["./browser/foo.js"],
+			suite: [
+				{
+					"./lib/*/bar.js": {
+						browser: ["./browser/*/bar.js"]
+					},
+					"./dist/*/bar.js": {
+						node: "./*.js",
+						default: "./browser/*.js"
+					}
+				},
+				"./dist/foo/bar.js",
+				["browser"]
+			]
+		},
+		{
+			name: "wildcard pattern #7",
+			expect: ["./browser/foo/default.js"],
+			suite: [
+				{
+					"./lib/*/bar.js": {
+						browser: ["./browser/*/bar.js"]
+					},
+					"./dist/*/bar.js": {
+						node: "./*.js",
+						default: "./browser/*/default.js"
+					}
+				},
+				"./dist/foo/bar.js",
+				["default"]
+			]
+		},
+		{
+			name: "wildcard pattern #8",
+			expect: ["./A/b/b/b.js"],
+			suite: [
+				{
+					"./a/*/c.js": "./A/*/*/*.js"
+				},
+				"./a/b/c.js",
+				[]
+			]
+		},
+		{
+			name: "wildcard pattern #9",
+			expect: ["./A/b/b/b.js", "./B/b/b/b.js"],
+			suite: [
+				{
+					"./a/*/c.js": ["./A/*/*/*.js", "./B/*/*/*.js"]
+				},
+				"./a/b/c.js",
+				[]
+			]
+		},
+		{
+			name: "wildcard pattern #10",
+			expect: ["./A/b/b/b.js"],
+			suite: [
+				{
+					"./a/foo-*/c.js": "./A/*/*/*.js"
+				},
+				"./a/foo-b/c.js",
+				[]
+			]
+		},
+		{
+			name: "wildcard pattern #11",
+			expect: ["./A/b/b/b.js"],
+			suite: [
+				{
+					"./a/*-foo/c.js": "./A/*/*/*.js"
+				},
+				"./a/b-foo/c.js",
+				[]
+			]
+		},
+		{
+			name: "wildcard pattern #12",
+			expect: ["./A/b/b/b.js"],
+			suite: [
+				{
+					"./a/foo-*-foo/c.js": "./A/*/*/*.js"
+				},
+				"./a/foo-b-foo/c.js",
+				[]
+			]
+		},
+		{
+			name: "wildcard pattern #13",
+			expect: ["./A/b/c/d.js"],
+			suite: [
+				{
+					"./a/foo-*-foo/c.js": "./A/b/c/d.js"
+				},
+				"./a/foo-b-foo/c.js",
+				[]
+			]
+		},
+		{
+			name: "wildcard pattern #13",
+			expect: ["./A/b/c/*.js"],
+			suite: [
+				{
+					"./a/foo-foo/c.js": "./A/b/c/*.js"
+				},
+				"./a/foo-foo/c.js",
+				[]
+			]
 		}
 	];
 
@@ -2452,4 +2708,207 @@ describe("ExportsFieldPlugin", () => {
 			}
 		);
 	});
+
+	it("should resolve with wildcard pattern #1", done => {
+		const fixture = path.resolve(
+			__dirname,
+			"./fixtures/imports-exports-wildcard/"
+		);
+
+		resolver.resolve({}, fixture, "m/features/f.js", {}, (err, result) => {
+			if (err) return done(err);
+			if (!result) throw new Error("No result");
+			result.should.equal(
+				path.resolve(fixture, "./node_modules/m/src/features/f.js")
+			);
+			done();
+		});
+	});
+
+	it("should resolve with wildcard pattern #2", done => {
+		const fixture = path.resolve(
+			__dirname,
+			"./fixtures/imports-exports-wildcard/"
+		);
+
+		resolver.resolve({}, fixture, "m/features/y/y.js", {}, (err, result) => {
+			if (err) return done(err);
+			if (!result) throw new Error("No result");
+			result.should.equal(
+				path.resolve(fixture, "./node_modules/m/src/features/y/y.js")
+			);
+			done();
+		});
+	});
+
+	it("should resolve with wildcard pattern #2", done => {
+		const fixture = path.resolve(
+			__dirname,
+			"./fixtures/imports-exports-wildcard/"
+		);
+
+		resolver.resolve({}, fixture, "m/features/y/y.js", {}, (err, result) => {
+			if (err) return done(err);
+			if (!result) throw new Error("No result");
+			result.should.equal(
+				path.resolve(fixture, "./node_modules/m/src/features/y/y.js")
+			);
+			done();
+		});
+	});
+
+	it("should resolve with wildcard pattern #3", done => {
+		const fixture = path.resolve(
+			__dirname,
+			"./fixtures/imports-exports-wildcard/"
+		);
+
+		resolver.resolve(
+			{},
+			fixture,
+			"m/features-no-ext/y/y.js",
+			{},
+			(err, result) => {
+				if (err) return done(err);
+				if (!result) throw new Error("No result");
+				result.should.equal(
+					path.resolve(fixture, "./node_modules/m/src/features/y/y.js")
+				);
+				done();
+			}
+		);
+	});
+
+	it("should resolve with wildcard pattern #4", done => {
+		const fixture = path.resolve(
+			__dirname,
+			"./fixtures/imports-exports-wildcard/"
+		);
+
+		resolver.resolve({}, fixture, "m/middle/nested/f.js", {}, (err, result) => {
+			if (err) return done(err);
+			if (!result) throw new Error("No result");
+			result.should.equal(
+				path.resolve(fixture, "./node_modules/m/src/middle/nested/f.js")
+			);
+			done();
+		});
+	});
+
+	it("should resolve with wildcard pattern #5", done => {
+		const fixture = path.resolve(
+			__dirname,
+			"./fixtures/imports-exports-wildcard/"
+		);
+
+		resolver.resolve(
+			{},
+			fixture,
+			"m/middle-1/nested/f.js",
+			{},
+			(err, result) => {
+				if (err) return done(err);
+				if (!result) throw new Error("No result");
+				result.should.equal(
+					path.resolve(fixture, "./node_modules/m/src/middle-1/nested/f.js")
+				);
+				done();
+			}
+		);
+	});
+
+	it("should resolve with wildcard pattern #6", done => {
+		const fixture = path.resolve(
+			__dirname,
+			"./fixtures/imports-exports-wildcard/"
+		);
+
+		resolver.resolve(
+			{},
+			fixture,
+			"m/middle-2/nested/f.js",
+			{},
+			(err, result) => {
+				if (err) return done(err);
+				if (!result) throw new Error("No result");
+				result.should.equal(
+					path.resolve(fixture, "./node_modules/m/src/middle-2/nested/f.js")
+				);
+				done();
+			}
+		);
+	});
+
+	it("should resolve with wildcard pattern #7", done => {
+		const fixture = path.resolve(
+			__dirname,
+			"./fixtures/imports-exports-wildcard/"
+		);
+
+		resolver.resolve({}, fixture, "m/middle-3/nested/f", {}, (err, result) => {
+			if (err) return done(err);
+			if (!result) throw new Error("No result");
+			result.should.equal(
+				path.resolve(
+					fixture,
+					"./node_modules/m/src/middle-3/nested/f/nested/f.js"
+				)
+			);
+			done();
+		});
+	});
+
+	it("should resolve with wildcard pattern #8", done => {
+		const fixture = path.resolve(
+			__dirname,
+			"./fixtures/imports-exports-wildcard/"
+		);
+
+		resolver.resolve({}, fixture, "m/middle-4/f/nested", {}, (err, result) => {
+			if (err) return done(err);
+			if (!result) throw new Error("No result");
+			result.should.equal(
+				path.resolve(fixture, "./node_modules/m/src/middle-4/f/f.js")
+			);
+			done();
+		});
+	});
+
+	it("should resolve with wildcard pattern #9", done => {
+		const fixture = path.resolve(
+			__dirname,
+			"./fixtures/imports-exports-wildcard/"
+		);
+
+		resolver.resolve({}, fixture, "m/middle-5/f$/$", {}, (err, result) => {
+			if (err) return done(err);
+			if (!result) throw new Error("No result");
+			result.should.equal(
+				path.resolve(fixture, "./node_modules/m/src/middle-5/f$/$.js")
+			);
+			done();
+		});
+	});
+
+	it("should throw error if target is 'null'", done => {
+		const fixture = path.resolve(
+			__dirname,
+			"./fixtures/imports-exports-wildcard/"
+		);
+
+		resolver.resolve(
+			{},
+			fixture,
+			"m/features/internal/file.js",
+			{},
+			(err, result) => {
+				if (!err) throw new Error(`expect error, got ${result}`);
+				err.should.be.instanceof(Error);
+				err.message.should.match(
+					/Package path \.\/features\/internal\/file\.js is not exported/
+				);
+				done();
+			}
+		);
+	});
 });
diff --git a/test/fixtures/imports-exports-wildcard/node_modules/m/package.json b/test/fixtures/imports-exports-wildcard/node_modules/m/package.json
new file mode 100644
index 0000000..aa2be49
--- /dev/null
+++ b/test/fixtures/imports-exports-wildcard/node_modules/m/package.json
@@ -0,0 +1,17 @@
+{
+  "name": "m",
+  "exports": {
+    "./features-no-ext/*": "./src/features/*",
+    "./features/*.js": "./src/features/*.js",
+    "./features/internal/*": null,
+    "./middle/nested/f.js": "./src/middle/nested/f.js",
+    "./middle-1/nested/*.js": "./src/middle-1/nested/*.js",
+    "./middle-2/*/f.js": "./src/middle-2/*/f.js",
+    "./middle-3/*": "./src/middle-3/*/*.js",
+    "./middle-4/*/nested": "./src/middle-4/*/*.js",
+    "./middle-5/*/$": "./src/middle-5/*/$.js"
+  },
+  "imports": {
+    "#internal/*.js": "./src/internal/*.js"
+  }
+}
diff --git a/test/fixtures/imports-exports-wildcard/node_modules/m/src/features/f.js b/test/fixtures/imports-exports-wildcard/node_modules/m/src/features/f.js
new file mode 100644
index 0000000..e69de29
diff --git a/test/fixtures/imports-exports-wildcard/node_modules/m/src/features/internal/file.js b/test/fixtures/imports-exports-wildcard/node_modules/m/src/features/internal/file.js
new file mode 100644
index 0000000..e69de29
diff --git a/test/fixtures/imports-exports-wildcard/node_modules/m/src/features/y/y.js b/test/fixtures/imports-exports-wildcard/node_modules/m/src/features/y/y.js
new file mode 100644
index 0000000..e69de29
diff --git a/test/fixtures/imports-exports-wildcard/node_modules/m/src/internal/i.js b/test/fixtures/imports-exports-wildcard/node_modules/m/src/internal/i.js
new file mode 100644
index 0000000..e69de29
diff --git a/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-1/f.js b/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-1/f.js
new file mode 100644
index 0000000..e69de29
diff --git a/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-1/nested/f.js b/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-1/nested/f.js
new file mode 100644
index 0000000..e69de29
diff --git a/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-2/nested/f.js b/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-2/nested/f.js
new file mode 100644
index 0000000..e5b70a8
--- /dev/null
+++ b/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-2/nested/f.js
@@ -0,0 +1,1 @@
+module.exports = { nested: "nested" }
diff --git a/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-3/nested/f/nested/f.js b/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-3/nested/f/nested/f.js
new file mode 100644
index 0000000..e69de29
diff --git a/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-4/f/f.js b/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-4/f/f.js
new file mode 100644
index 0000000..e69de29
diff --git a/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-5/f$/$.js b/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-5/f$/$.js
new file mode 100644
index 0000000..e69de29
diff --git a/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-5/f/$.js b/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-5/f/$.js
new file mode 100644
index 0000000..e69de29
diff --git a/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle/f.js b/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle/f.js
new file mode 100644
index 0000000..e69de29
diff --git a/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle/nested/f.js b/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle/nested/f.js
new file mode 100644
index 0000000..e69de29
diff --git a/test/importsField.js b/test/importsField.js
index 334cd87..59eacba 100644
--- a/test/importsField.js
+++ b/test/importsField.js
@@ -1357,4 +1357,17 @@ describe("ImportsFieldPlugin", () => {
 			}
 		);
 	});
+
+	it("should resolve with wildcard pattern", done => {
+		const fixture = path.resolve(
+			__dirname,
+			"./fixtures/imports-exports-wildcard/node_modules/m/"
+		);
+		resolver.resolve({}, fixture, "#internal/i.js", {}, (err, result) => {
+			if (err) return done(err);
+			if (!result) throw new Error("No result");
+			result.should.equal(path.resolve(fixture, "./src/internal/i.js"));
+			done();
+		});
+	});
 });