Repository: webpack/enhanced-resolve
Pull request #428 (2024-07-18 20:14:08)

fix: handle invalid files in `imports` and `exports` field properly
fixes https://github.com/webpack/enhanced-resolve/pull/420

partially - https://github.com/webpack/enhanced-resolve/issues/400


Patch:
diff --git a/lib/ExportsFieldPlugin.js b/lib/ExportsFieldPlugin.js
index f31ff32..1c45b88 100644
--- a/lib/ExportsFieldPlugin.js
+++ b/lib/ExportsFieldPlugin.js
@@ -9,7 +9,10 @@ const DescriptionFileUtils = require("./DescriptionFileUtils");
 const forEachBail = require("./forEachBail");
 const { processExportsField } = require("./util/entrypoints");
 const { parseIdentifier } = require("./util/identifier");
-const { checkImportsExportsFieldTarget } = require("./util/path");
+const {
+	invalidSegmentRegEx,
+	deprecatedInvalidSegmentRegEx
+} = require("./util/path");
 
 /** @typedef {import("./Resolver")} Resolver */
 /** @typedef {import("./Resolver").JsonObject} JsonObject */
@@ -79,6 +82,8 @@ module.exports = class ExportsFieldPlugin {
 
 				/** @type {string[]} */
 				let paths;
+				/** @type {string | null} */
+				let usedField;
 
 				try {
 					// We attach the cache to the description file instead of the exportsField value
@@ -94,7 +99,10 @@ module.exports = class ExportsFieldPlugin {
 							fieldProcessor
 						);
 					}
-					paths = fieldProcessor(remainingRequest, this.conditionNames);
+					[paths, usedField] = fieldProcessor(
+						remainingRequest,
+						this.conditionNames
+					);
 				} catch (/** @type {unknown} */ err) {
 					if (resolveContext.log) {
 						resolveContext.log(
@@ -126,10 +134,31 @@ module.exports = class ExportsFieldPlugin {
 
 						const [relativePath, query, fragment] = parsedIdentifier;
 
-						const error = checkImportsExportsFieldTarget(relativePath);
+						if (relativePath.length === 0 || !relativePath.startsWith("./")) {
+							if (paths.length === 1) {
+								return callback(
+									new Error(
+										`Invalid "exports" target "${p}" defined for "${usedField}" in the package config ${request.descriptionFilePath}, targets must start with "./"`
+									)
+								);
+							}
+
+							return callback();
+						}
 
-						if (error) {
-							return callback(error);
+						if (
+							invalidSegmentRegEx.exec(relativePath.slice(2)) !== null &&
+							deprecatedInvalidSegmentRegEx.test(relativePath.slice(2)) !== null
+						) {
+							if (paths.length === 1) {
+								return callback(
+									new Error(
+										`Trying to access out of package scope. Requesting ${relativePath}`
+									)
+								);
+							}
+
+							return callback();
 						}
 
 						/** @type {ResolveRequest} */
diff --git a/lib/ImportsFieldPlugin.js b/lib/ImportsFieldPlugin.js
index 5ccb3bb..56462b3 100644
--- a/lib/ImportsFieldPlugin.js
+++ b/lib/ImportsFieldPlugin.js
@@ -9,7 +9,10 @@ const DescriptionFileUtils = require("./DescriptionFileUtils");
 const forEachBail = require("./forEachBail");
 const { processImportsField } = require("./util/entrypoints");
 const { parseIdentifier } = require("./util/identifier");
-const { checkImportsExportsFieldTarget } = require("./util/path");
+const {
+	invalidSegmentRegEx,
+	deprecatedInvalidSegmentRegEx
+} = require("./util/path");
 
 /** @typedef {import("./Resolver")} Resolver */
 /** @typedef {import("./Resolver").JsonObject} JsonObject */
@@ -97,7 +100,7 @@ module.exports = class ImportsFieldPlugin {
 							fieldProcessor
 						);
 					}
-					paths = fieldProcessor(remainingRequest, this.conditionNames);
+					[paths] = fieldProcessor(remainingRequest, this.conditionNames);
 				} catch (/** @type {unknown} */ err) {
 					if (resolveContext.log) {
 						resolveContext.log(
@@ -129,15 +132,24 @@ module.exports = class ImportsFieldPlugin {
 
 						const [path_, query, fragment] = parsedIdentifier;
 
-						const error = checkImportsExportsFieldTarget(path_);
-
-						if (error) {
-							return callback(error);
-						}
-
 						switch (path_.charCodeAt(0)) {
 							// should be relative
 							case dotCode: {
+								if (
+									invalidSegmentRegEx.exec(path_.slice(2)) !== null &&
+									deprecatedInvalidSegmentRegEx.test(path_.slice(2)) !== null
+								) {
+									if (paths.length === 1) {
+										return callback(
+											new Error(
+												`Trying to access out of package scope. Requesting ${path_}`
+											)
+										);
+									}
+
+									return callback();
+								}
+
 								/** @type {ResolveRequest} */
 								const obj = {
 									...request,
diff --git a/lib/util/entrypoints.js b/lib/util/entrypoints.js
index bba26a6..683a8e8 100644
--- a/lib/util/entrypoints.js
+++ b/lib/util/entrypoints.js
@@ -16,7 +16,7 @@
  * @callback FieldProcessor
  * @param {string} request request
  * @param {Set<string>} conditionNames condition names
- * @returns {string[]} resolved paths
+ * @returns {[string[], string | null]} resolved paths with used field
  */
 
 /*
@@ -72,6 +72,7 @@ Conditional mapping nested in another conditional mapping is called nested mappi
 
 */
 
+const { parseIdentifier } = require("./identifier");
 const slashCode = "/".charCodeAt(0);
 const dotCode = ".".charCodeAt(0);
 const hashCode = "#".charCodeAt(0);
@@ -100,7 +101,7 @@ module.exports.processImportsField = function processImportsField(
 	importsField
 ) {
 	return createFieldProcessor(
-		buildImportsField(importsField),
+		importsField,
 		request => "#" + request,
 		assertImportsFieldRequest,
 		assertImportTarget
@@ -125,9 +126,10 @@ function createFieldProcessor(
 
 		const match = findMatch(normalizeRequest(request), field);
 
-		if (match === null) return [];
+		if (match === null) return [[], null];
 
-		const [mapping, remainingRequest, isSubpathMapping, isPattern] = match;
+		const [mapping, remainingRequest, isSubpathMapping, isPattern, usedField] =
+			match;
 
 		/** @type {DirectMapping|null} */
 		let direct = null;
@@ -139,19 +141,22 @@ function createFieldProcessor(
 			);
 
 			// matching not found
-			if (direct === null) return [];
+			if (direct === null) return [[], null];
 		} else {
 			direct = /** @type {DirectMapping} */ (mapping);
 		}
 
-		return directMapping(
-			remainingRequest,
-			isPattern,
-			isSubpathMapping,
-			direct,
-			conditionNames,
-			assertTarget
-		);
+		return [
+			directMapping(
+				remainingRequest,
+				isPattern,
+				isSubpathMapping,
+				direct,
+				conditionNames,
+				assertTarget
+			),
+			usedField
+		];
 	};
 }
 
@@ -200,18 +205,15 @@ function assertImportsFieldRequest(request) {
  * @param {boolean} expectFolder is folder expected
  */
 function assertExportTarget(exp, expectFolder) {
-	if (
-		exp.charCodeAt(0) === slashCode ||
-		(exp.charCodeAt(0) === dotCode && exp.charCodeAt(1) !== slashCode)
-	) {
-		throw new Error(
-			`Export should be relative path and start with "./", got ${JSON.stringify(
-				exp
-			)}.`
-		);
+	const parsedIdentifier = parseIdentifier(exp);
+
+	if (!parsedIdentifier) {
+		return;
 	}
 
-	const isFolder = exp.charCodeAt(exp.length - 1) === slashCode;
+	const [relativePath] = parsedIdentifier;
+	const isFolder =
+		relativePath.charCodeAt(relativePath.length - 1) === slashCode;
 
 	if (isFolder !== expectFolder) {
 		throw new Error(
@@ -231,7 +233,15 @@ function assertExportTarget(exp, expectFolder) {
  * @param {boolean} expectFolder is folder expected
  */
 function assertImportTarget(imp, expectFolder) {
-	const isFolder = imp.charCodeAt(imp.length - 1) === slashCode;
+	const parsedIdentifier = parseIdentifier(imp);
+
+	if (!parsedIdentifier) {
+		return;
+	}
+
+	const [relativePath] = parsedIdentifier;
+	const isFolder =
+		relativePath.charCodeAt(relativePath.length - 1) === slashCode;
 
 	if (isFolder !== expectFolder) {
 		throw new Error(
@@ -271,7 +281,7 @@ function patternKeyCompare(a, b) {
  * Trying to match request to field
  * @param {string} request request
  * @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
+ * @returns {[MappingValue, string, boolean, boolean, string]|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, field) {
 	if (
@@ -281,7 +291,7 @@ function findMatch(request, field) {
 	) {
 		const target = /** @type {{[k: string]: MappingValue}} */ (field)[request];
 
-		return [target, "", false, false];
+		return [target, "", false, false, request];
 	}
 
 	/** @type {string} */
@@ -332,7 +342,8 @@ function findMatch(request, field) {
 		target,
 		/** @type {string} */ (bestMatchSubpath),
 		isSubpathMapping,
-		isPattern
+		isPattern,
+		bestMatch
 	];
 }
 
@@ -560,39 +571,3 @@ function buildExportsField(field) {
 
 	return field;
 }
-
-/**
- * @param {ImportsField} field imports field
- * @returns {ImportsField} normalized imports field
- */
-function buildImportsField(field) {
-	const keys = Object.keys(field);
-
-	for (let i = 0; i < keys.length; i++) {
-		const key = keys[i];
-
-		if (key.charCodeAt(0) !== hashCode) {
-			throw new Error(
-				`Imports field key should start with "#" (key: ${JSON.stringify(key)})`
-			);
-		}
-
-		if (key.length === 1) {
-			throw new Error(
-				`Imports field key should have at least 2 characters (key: ${JSON.stringify(
-					key
-				)})`
-			);
-		}
-
-		if (key.charCodeAt(1) === slashCode) {
-			throw new Error(
-				`Imports field key should not start with "#/" (key: ${JSON.stringify(
-					key
-				)})`
-			);
-		}
-	}
-
-	return field;
-}
diff --git a/lib/util/path.js b/lib/util/path.js
index 4a65c6e..bbb0e4d 100644
--- a/lib/util/path.js
+++ b/lib/util/path.js
@@ -33,6 +33,14 @@ const PathType = Object.freeze({
 });
 exports.PathType = PathType;
 
+const invalidSegmentRegEx =
+	/(^|\\|\/)((\.|%2e)(\.|%2e)?|(n|%6e|%4e)(o|%6f|%4f)(d|%64|%44)(e|%65|%45)(_|%5f)(m|%6d|%4d)(o|%6f|%4f)(d|%64|%44)(u|%75|%55)(l|%6c|%4c)(e|%65|%45)(s|%73|%53))?(\\|\/|$)/i;
+exports.invalidSegmentRegEx = invalidSegmentRegEx;
+
+const deprecatedInvalidSegmentRegEx =
+	/(^|\\|\/)((\.|%2e)(\.|%2e)?|(n|%6e|%4e)(o|%6f|%4f)(d|%64|%44)(e|%65|%45)(_|%5f)(m|%6d|%4d)(o|%6f|%4f)(d|%64|%44)(u|%75|%55)(l|%6c|%4c)(e|%65|%45)(s|%73|%53))(\\|\/|$)/i;
+exports.deprecatedInvalidSegmentRegEx = deprecatedInvalidSegmentRegEx;
+
 /**
  * @param {string} p a path
  * @returns {PathType} type of path
@@ -193,37 +201,3 @@ const cachedJoin = (rootPath, request) => {
 	return cacheEntry;
 };
 exports.cachedJoin = cachedJoin;
-
-/**
- * @param {string} relativePath relative path
- * @returns {undefined|Error} nothing or an error
- */
-const checkImportsExportsFieldTarget = relativePath => {
-	let lastNonSlashIndex = 0;
-	let slashIndex = relativePath.indexOf("/", 1);
-	let cd = 0;
-
-	while (slashIndex !== -1) {
-		const folder = relativePath.slice(lastNonSlashIndex, slashIndex);
-
-		switch (folder) {
-			case "..": {
-				cd--;
-				if (cd < 0)
-					return new Error(
-						`Trying to access out of package scope. Requesting ${relativePath}`
-					);
-				break;
-			}
-			case ".":
-				break;
-			default:
-				cd++;
-				break;
-		}
-
-		lastNonSlashIndex = slashIndex + 1;
-		slashIndex = relativePath.indexOf("/", lastNonSlashIndex);
-	}
-};
-exports.checkImportsExportsFieldTarget = checkImportsExportsFieldTarget;
diff --git a/package.json b/package.json
index f7dfe6a..e7846a3 100644
--- a/package.json
+++ b/package.json
@@ -54,7 +54,7 @@
     "pretty": "prettier --loglevel warn --write \"lib/**/*.{js,json}\" \"test/*.js\"",
     "pretest": "yarn lint",
     "spelling": "cspell \"**\"",
-    "test:only": "jest",
+    "test:only": "node_modules/.bin/jest",
     "test:watch": "yarn test:only -- --watch",
     "test:coverage": "yarn test:only -- --collectCoverageFrom=\"lib/**/*.js\" --coverage",
     "test": "yarn test:coverage",


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 0c337cd..8abbadb 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -26,7 +26,7 @@ jobs:
       fail-fast: false
       matrix:
         os: [ubuntu-latest, windows-latest, macos-latest]
-        node-version: [10.x, 12.x, 14.x, 16.x, 18.x, 20.x, 22.x]
+        node-version: [10.x, 12.x, 14.x, 16.x, 18.x, 20.x, 22.4]
     runs-on: ${{ matrix.os }}
     steps:
       - uses: actions/checkout@v4
diff --git a/test/exportsField.test.js b/test/exportsField.test.js
index b8a0375..016dc2a 100644
--- a/test/exportsField.test.js
+++ b/test/exportsField.test.js
@@ -10,6 +10,11 @@ const fixture = path.resolve(__dirname, "fixtures", "exports-field");
 const fixture2 = path.resolve(__dirname, "fixtures", "exports-field2");
 const fixture3 = path.resolve(__dirname, "fixtures", "exports-field3");
 const fixture4 = path.resolve(__dirname, "fixtures", "exports-field-error");
+const fixture5 = path.resolve(
+	__dirname,
+	"fixtures",
+	"exports-field-invalid-package-target"
+);
 
 describe("Process exports field", function exportsField() {
 	/** @type {Array<{name: string, expect: string[]|Error, suite: [ExportsField, string, string[]]}>} */
@@ -808,17 +813,6 @@ describe("Process exports field", function exportsField() {
 				[]
 			]
 		},
-		{
-			name: "mapping to a folder root #6 (wildcard)",
-			expect: new Error(), // not a valid mapping
-			suite: [
-				{
-					"./*": "."
-				},
-				"./timezones/pdt.mjs",
-				[]
-			]
-		},
 		{
 			name: "mapping to a folder root #7",
 			expect: [], // incorrect export field, but value did not processed
@@ -1070,17 +1064,6 @@ describe("Process exports field", function exportsField() {
 		{
 			name: "incorrect exports field #2",
 			expect: new Error(),
-			suite: [
-				{
-					"./utils/": "/a/"
-				},
-				"./utils/index.mjs",
-				[]
-			]
-		},
-		{
-			name: "incorrect exports field #3",
-			expect: new Error(),
 			suite: [
 				{
 					"/utils/": {
@@ -1093,21 +1076,7 @@ describe("Process exports field", function exportsField() {
 			]
 		},
 		{
-			name: "incorrect exports field #4",
-			expect: new Error(),
-			suite: [
-				{
-					"./utils/": {
-						browser: "/a/",
-						default: "/b/"
-					}
-				},
-				"./utils/index.mjs",
-				["browser"]
-			]
-		},
-		{
-			name: "incorrect exports field #4 (wildcard)",
+			name: "incorrect exports field #3 (wildcard)",
 			expect: new Error(),
 			suite: [
 				{
@@ -1121,7 +1090,7 @@ describe("Process exports field", function exportsField() {
 			]
 		},
 		{
-			name: "incorrect exports field #5",
+			name: "incorrect exports field #4",
 			expect: [],
 			suite: [
 				{
@@ -1132,7 +1101,7 @@ describe("Process exports field", function exportsField() {
 			]
 		},
 		{
-			name: "incorrect exports field #6",
+			name: "incorrect exports field #5",
 			expect: [],
 			suite: [
 				{
@@ -1143,7 +1112,7 @@ describe("Process exports field", function exportsField() {
 			]
 		},
 		{
-			name: "incorrect exports field #7",
+			name: "incorrect exports field #6",
 			expect: [],
 			suite: [
 				{
@@ -1157,7 +1126,7 @@ describe("Process exports field", function exportsField() {
 			]
 		},
 		{
-			name: "incorrect exports field #8",
+			name: "incorrect exports field #7",
 			expect: [],
 			suite: [
 				{
@@ -1276,28 +1245,6 @@ describe("Process exports field", function exportsField() {
 		},
 		{
 			name: "backtracking package base #3",
-			expect: new Error(),
-			suite: [
-				{
-					"./utils/": "../src/"
-				},
-				"./utils/index",
-				[]
-			]
-		},
-		{
-			name: "backtracking package base #3 (wildcard)",
-			expect: new Error(),
-			suite: [
-				{
-					"./utils/*": "../src/*"
-				},
-				"./utils/index",
-				[]
-			]
-		},
-		{
-			name: "backtracking package base #4",
 			expect: ["./../src/index"], // we don't handle backtracking here
 			suite: [
 				{
@@ -1308,7 +1255,7 @@ describe("Process exports field", function exportsField() {
 			]
 		},
 		{
-			name: "backtracking package base #4 (wildcard)",
+			name: "backtracking package base #3 (wildcard)",
 			expect: ["./../src/index"], // we don't handle backtracking here
 			suite: [
 				{
@@ -1319,7 +1266,7 @@ describe("Process exports field", function exportsField() {
 			]
 		},
 		{
-			name: "backtracking package base #5",
+			name: "backtracking package base #4",
 			// fits package base depth
 			expect: ["./src/../index.js"],
 			suite: [
@@ -1331,7 +1278,7 @@ describe("Process exports field", function exportsField() {
 			]
 		},
 		{
-			name: "backtracking package base #6",
+			name: "backtracking package base #5",
 			// fits package base depth,
 			// but matching works as string concatenation not file paths
 			expect: [],
@@ -1344,35 +1291,7 @@ describe("Process exports field", function exportsField() {
 			]
 		},
 		{
-			name: "backtracking package base #7",
-			// enhanced-resolve don't know is this same package or not
-			expect: new Error(),
-			suite: [
-				{
-					"./utils/": {
-						browser: "../this/"
-					}
-				},
-				"./utils/index",
-				["browser"]
-			]
-		},
-		{
-			name: "backtracking package base #7",
-			// enhanced-resolve don't know is this same package or not
-			expect: new Error(),
-			suite: [
-				{
-					"./utils/*": {
-						browser: "../this/*"
-					}
-				},
-				"./utils/index",
-				["browser"]
-			]
-		},
-		{
-			name: "backtracking package base #8",
+			name: "backtracking package base #6",
 			expect: ["./utils/../index"],
 			suite: [
 				{
@@ -1385,7 +1304,7 @@ describe("Process exports field", function exportsField() {
 			]
 		},
 		{
-			name: "backtracking package base #8 (wildcard)",
+			name: "backtracking package base #6 (wildcard)",
 			expect: ["./utils/../index"],
 			suite: [
 				{
@@ -1398,7 +1317,7 @@ describe("Process exports field", function exportsField() {
 			]
 		},
 		{
-			name: "backtracking package base #9",
+			name: "backtracking package base #7",
 			expect: ["./dist/index"],
 			suite: [
 				{
@@ -1410,7 +1329,7 @@ describe("Process exports field", function exportsField() {
 			]
 		},
 		{
-			name: "backtracking package base #9 (wildcard)",
+			name: "backtracking package base #7 (wildcard)",
 			expect: ["./dist/index"],
 			suite: [
 				{
@@ -2218,14 +2137,14 @@ describe("Process exports field", function exportsField() {
 					processExportsField(testCase.suite[0])(
 						testCase.suite[1],
 						new Set(testCase.suite[2])
-					);
+					)[0];
 				}).toThrowError();
 			} else {
 				expect(
 					processExportsField(testCase.suite[0])(
 						testCase.suite[1],
 						new Set(testCase.suite[2])
-					)
+					)[0]
 				).toEqual(testCase.expect);
 			}
 		});
@@ -2487,7 +2406,7 @@ describe("ExportsFieldPlugin", () => {
 			(err, result) => {
 				if (!err) return done(new Error(`expect error, got ${result}`));
 				expect(err).toBeInstanceOf(Error);
-				expect(err.message).toMatch(/out of package scope/);
+				expect(err.message).toMatch(/Can't resolve/);
 				done();
 			}
 		);
@@ -3022,4 +2941,232 @@ describe("ExportsFieldPlugin", () => {
 			done();
 		});
 	});
+
+	it("invalid package target #1", done => {
+		resolver.resolve(
+			{},
+			fixture5,
+			"@exports-field/bad-specifier",
+			{},
+			(err, result) => {
+				if (err) return done(err);
+				if (!result) return done(new Error("No result"));
+				expect(result).toEqual(path.resolve(fixture5, "./a.js") + "?foo=../");
+				done();
+			}
+		);
+	});
+
+	it("invalid package target #2", done => {
+		resolver.resolve(
+			{},
+			fixture5,
+			"@exports-field/bad-specifier/foo/file.js",
+			{},
+			(err, result) => {
+				if (err) return done(err);
+				if (!result) return done(new Error("No result"));
+				expect(result).toEqual(
+					path.resolve(fixture5, "./a.js") + "?foo=../#../"
+				);
+				done();
+			}
+		);
+	});
+
+	it("invalid package target #3", done => {
+		resolver.resolve(
+			{},
+			fixture5,
+			"@exports-field/bad-specifier/bar",
+			{},
+			(err, result) => {
+				if (!err) return done(new Error(`expect error, got ${result}`));
+				expect(err).toBeInstanceOf(Error);
+				expect(err.message).toMatch(
+					/Invalid "exports" target "-bad-specifier-" defined for "\.\/bar"/
+				);
+				done();
+			}
+		);
+	});
+
+	it("invalid package target #4", done => {
+		resolver.resolve(
+			{},
+			fixture5,
+			"@exports-field/bad-specifier/baz-multi",
+			{},
+			(err, result) => {
+				if (!err) return done(new Error(`expect error, got ${result}`));
+				expect(err).toBeInstanceOf(Error);
+				expect(err.message).toMatch(/Can't resolve/);
+				done();
+			}
+		);
+	});
+
+	it("invalid package target #5", done => {
+		resolver.resolve(
+			{},
+			fixture5,
+			"@exports-field/bad-specifier/pattern/a.js",
+			{},
+			(err, result) => {
+				if (err) return done(err);
+				if (!result) return done(new Error("No result"));
+				expect(result).toEqual(path.resolve(fixture5, "./a.js"));
+				done();
+			}
+		);
+	});
+
+	it("invalid package target #6", done => {
+		resolver.resolve(
+			{},
+			fixture5,
+			"@exports-field/bad-specifier/slash",
+			{},
+			(err, result) => {
+				if (err) return done(err);
+				if (!result) return done(new Error("No result"));
+				expect(result).toEqual(path.resolve(fixture5, "./a.js"));
+				done();
+			}
+		);
+	});
+
+	it("invalid package target #7", done => {
+		resolver.resolve(
+			{},
+			fixture5,
+			"@exports-field/bad-specifier/no-slash",
+			{},
+			(err, result) => {
+				if (err) return done(err);
+				if (!result) return done(new Error("No result"));
+				expect(result).toEqual(path.resolve(fixture5, "./a.js"));
+				done();
+			}
+		);
+	});
+
+	it("invalid package target #8", done => {
+		resolver.resolve(
+			{},
+			fixture5,
+			"@exports-field/bad-specifier/utils/index.mjs",
+			{},
+			(err, result) => {
+				if (!err) return done(new Error(`expect error, got ${result}`));
+				expect(err).toBeInstanceOf(Error);
+				expect(err.message).toMatch(
+					/Invalid "exports" target "\/b\/index\.mjs" defined for "\.\/utils\//
+				);
+				done();
+			}
+		);
+	});
+
+	it("invalid package target #9", done => {
+		resolver.resolve(
+			{},
+			fixture5,
+			"@exports-field/bad-specifier/utils1/index.mjs",
+			{},
+			(err, result) => {
+				if (!err) return done(new Error(`expect error, got ${result}`));
+				expect(err).toBeInstanceOf(Error);
+				expect(err.message).toMatch(
+					/Invalid "exports" target "\/a\/index.mjs" defined for "\.\/utils1\/"/
+				);
+				done();
+			}
+		);
+	});
+
+	it("invalid package target #10", done => {
+		resolver.resolve(
+			{},
+			fixture5,
+			"@exports-field/bad-specifier/utils2/index",
+			{},
+			(err, result) => {
+				if (!err) return done(new Error(`expect error, got ${result}`));
+				expect(err).toBeInstanceOf(Error);
+				expect(err.message).toMatch(
+					/Invalid "exports" target "\.\.\/this\/index" defined for "\.\/utils2\/"/
+				);
+				done();
+			}
+		);
+	});
+
+	it("invalid package target #11", done => {
+		resolver.resolve(
+			{},
+			fixture5,
+			"@exports-field/bad-specifier/utils3/index",
+			{},
+			(err, result) => {
+				if (!err) return done(new Error(`expect error, got ${result}`));
+				expect(err).toBeInstanceOf(Error);
+				expect(err.message).toMatch(
+					/Invalid "exports" target "\.\.\/this\/index" defined for "\.\/utils3\/\*"/
+				);
+				done();
+			}
+		);
+	});
+
+	it("invalid package target #12", done => {
+		resolver.resolve(
+			{},
+			fixture5,
+			"@exports-field/bad-specifier/utils4/index",
+			{},
+			(err, result) => {
+				if (!err) return done(new Error(`expect error, got ${result}`));
+				expect(err).toBeInstanceOf(Error);
+				expect(err.message).toMatch(
+					/Invalid "exports" target "\.\.\/src\/index" defined for "\.\/utils4\/\*"/
+				);
+				done();
+			}
+		);
+	});
+
+	it("invalid package target #13", done => {
+		resolver.resolve(
+			{},
+			fixture5,
+			"@exports-field/bad-specifier/utils5/index",
+			{},
+			(err, result) => {
+				if (!err) return done(new Error(`expect error, got ${result}`));
+				expect(err).toBeInstanceOf(Error);
+				expect(err.message).toMatch(
+					/Invalid "exports" target "\.\.\/src\/index" defined for "\.\/utils5\/"/
+				);
+				done();
+			}
+		);
+	});
+
+	it("invalid package target #14", done => {
+		resolver.resolve(
+			{},
+			fixture5,
+			"@exports-field/bad-specifier/timezones/pdt.mjs",
+			{},
+			(err, result) => {
+				if (!err) return done(new Error(`expect error, got ${result}`));
+				expect(err).toBeInstanceOf(Error);
+				expect(err.message).toMatch(
+					/Invalid "exports" target "\." defined for "\.\/\*"/
+				);
+				done();
+			}
+		);
+	});
 });
diff --git a/test/fixtures/exports-field-invalid-package-target/a.js b/test/fixtures/exports-field-invalid-package-target/a.js
new file mode 100644
index 0000000..bd816ea
--- /dev/null
+++ b/test/fixtures/exports-field-invalid-package-target/a.js
@@ -0,0 +1,1 @@
+module.exports = 1;
diff --git a/test/fixtures/exports-field-invalid-package-target/package.json b/test/fixtures/exports-field-invalid-package-target/package.json
new file mode 100644
index 0000000..d0aa09e
--- /dev/null
+++ b/test/fixtures/exports-field-invalid-package-target/package.json
@@ -0,0 +1,44 @@
+{
+  "name": "@exports-field/bad-specifier",
+  "version": "1.0.0",
+  "exports": {
+    ".": [
+      "",
+      "./a/../b/../../pack1/index.js",
+      "././-bad-specifier-",
+      "-bad-specifier-",
+      "file.js",
+      "./a.js?foo=../"
+    ],
+    "./foo/*": [
+      "",
+      "./a/../b/../../pack1/index.js",
+      "././-bad-specifier-",
+      "-bad-specifier-",
+      "file.js",
+      "./a.js?foo=../#../"
+    ],
+    "./bar": "-bad-specifier-",
+    "./baz": ["-bad-specifier-"],
+    "./baz-multi": ["-bad-specifier-", "foo"],
+    "./pattern/*.js": {
+      "default": ["-bad-specifier-", "./*.js"]
+    },
+    "./slash": ["/bar", "./a.js"],
+    "./no-slash": [".bar", "./a.js"],
+    "./utils/": {
+      "browser": "/a/",
+      "default": "/b/"
+    },
+    "./utils1/": "/a/",
+    "./utils2/": {
+      "default": "../this/"
+    },
+    "./utils3/*": {
+      "default": "../this/*"
+    },
+    "./utils4/*": "../src/*",
+    "./utils5/": "../src/",
+    "./*": "."
+  }
+}
diff --git a/test/fixtures/imports-field-different/a.js b/test/fixtures/imports-field-different/a.js
new file mode 100644
index 0000000..bd816ea
--- /dev/null
+++ b/test/fixtures/imports-field-different/a.js
@@ -0,0 +1,1 @@
+module.exports = 1;
diff --git a/test/fixtures/imports-field-different/package.json b/test/fixtures/imports-field-different/package.json
new file mode 100644
index 0000000..3627277
--- /dev/null
+++ b/test/fixtures/imports-field-different/package.json
@@ -0,0 +1,39 @@
+{
+  "name": "@exports-field/bad-specifier",
+  "version": "1.0.0",
+  "imports": {
+    "/utils/": "./a/",
+    "/utils1/": {
+      "browser": "./a/",
+      "default": "./b/"
+    },
+    "a": "a.js",
+    "#": "a.js",
+    "#/dep": "a.js",
+    "#dep/": "a.js",
+    "#dep": [
+      "./a/../b/../../pack1/index.js",
+      "././-bad-specifier-",
+      "./a.js?foo=../"
+    ],
+
+    "#dep/foo/*": [
+      "./a/../b/../../pack1/index.js",
+      "././-bad-specifier-",
+      "./a.js?foo=../#../"
+    ],
+    "#dep/bar": "-bad-specifier-",
+    "#dep/baz": ["-bad-specifier-"],
+    "#dep/baz-multi": ["-bad-specifier-", "not-found"],
+    "#dep/pattern/*.js": {
+      "default": ["-bad-specifier-", "./*.js"]
+    },
+    "#dep/array": ["./a.js", "-bad-specifier-"],
+    "#dep/array2": ["-bad-specifier-", "./a.js"],
+    "#dep/array3": ["./a.js"],
+    "#dep/empty": "",
+    "#dep/with-bad": ["../foo", "./a.js"],
+    "#dep/with-bad2": ["./a.js" ,"../foo"],
+    "#timezones/": "./data/timezones"
+  }
+}
diff --git a/test/importsField.test.js b/test/importsField.test.js
index ea6702f..db19d67 100644
--- a/test/importsField.test.js
+++ b/test/importsField.test.js
@@ -7,6 +7,7 @@ const CachedInputFileSystem = require("../lib/CachedInputFileSystem");
 /** @typedef {import("../lib/util/entrypoints").ImportsField} ImportsField */
 
 const fixture = path.resolve(__dirname, "fixtures", "imports-field");
+const fixture1 = path.resolve(__dirname, "fixtures", "imports-field-different");
 
 describe("Process imports field", function exportsField() {
 	/** @type {Array<{name: string, expect: string[]|Error, suite: [ImportsField, string, string[]]}>} */
@@ -791,31 +792,6 @@ describe("Process imports field", function exportsField() {
 		//#region Incorrect imports field definition
 		{
 			name: "incorrect exports field #1",
-			expect: new Error(),
-			suite: [
-				{
-					"/utils/": "./a/"
-				},
-				"#a/index.mjs",
-				[]
-			]
-		},
-		{
-			name: "incorrect exports field #2",
-			expect: new Error(),
-			suite: [
-				{
-					"/utils/": {
-						browser: "./a/",
-						default: "./b/"
-					}
-				},
-				"#a/index.mjs",
-				["browser"]
-			]
-		},
-		{
-			name: "incorrect exports field #3",
 			expect: [],
 			suite: [
 				{
@@ -826,7 +802,7 @@ describe("Process imports field", function exportsField() {
 			]
 		},
 		{
-			name: "incorrect exports field #4",
+			name: "incorrect exports field #2",
 			expect: [],
 			suite: [
 				{
@@ -837,7 +813,7 @@ describe("Process imports field", function exportsField() {
 			]
 		},
 		{
-			name: "incorrect exports field #5",
+			name: "incorrect exports field #3",
 			expect: [],
 			suite: [
 				{
@@ -851,7 +827,7 @@ describe("Process imports field", function exportsField() {
 			]
 		},
 		{
-			name: "incorrect exports field #6",
+			name: "incorrect exports field #4",
 			expect: [],
 			suite: [
 				{
@@ -1171,7 +1147,7 @@ describe("Process imports field", function exportsField() {
 					processImportsField(testCase.suite[0])(
 						testCase.suite[1],
 						new Set(testCase.suite[2])
-					)
+					)[0]
 				).toEqual(testCase.expect);
 			}
 		});
@@ -1348,4 +1324,150 @@ describe("ImportsFieldPlugin", () => {
 			done();
 		});
 	});
+
+	it("should work and throw an error on invalid imports #1", done => {
+		resolver.resolve({}, fixture, "#/dep", {}, (err, result) => {
+			if (!err) return done(new Error(`expect error, got ${result}`));
+			expect(err).toBeInstanceOf(Error);
+			expect(err.message).toMatch(/Request should not start with "#\/"/);
+			done();
+		});
+	});
+
+	it("should work and throw an error on invalid imports #2", done => {
+		resolver.resolve({}, fixture, "#dep/", {}, (err, result) => {
+			if (!err) return done(new Error(`expect error, got ${result}`));
+			expect(err).toBeInstanceOf(Error);
+			expect(err.message).toMatch(
+				/Resolving to directories is not possible with the imports field \(request was #dep\/\)/
+			);
+			done();
+		});
+	});
+
+	it("should work with invalid imports #1", done => {
+		resolver.resolve({}, fixture1, "#dep", {}, (err, result) => {
+			if (err) return done(err);
+			if (!result) return done(new Error("No result"));
+			expect(result).toEqual(path.resolve(fixture1, "./a.js") + "?foo=../");
+			done();
+		});
+	});
+
+	it("should work with invalid imports #2", done => {
+		resolver.resolve({}, fixture1, "#dep/foo/a.js", {}, (err, result) => {
+			if (err) return done(err);
+			if (!result) return done(new Error("No result"));
+			expect(result).toEqual(path.resolve(fixture1, "./a.js") + "?foo=../#../");
+			done();
+		});
+	});
+
+	it("should work with invalid imports #3", done => {
+		resolver.resolve({}, fixture1, "#dep/bar", {}, (err, result) => {
+			if (!err) return done(new Error(`expect error, got ${result}`));
+			expect(err).toBeInstanceOf(Error);
+			expect(err.message).toMatch(/Can't resolve '#dep\/bar' in/);
+			done();
+		});
+	});
+
+	it("should work with invalid imports #3", done => {
+		resolver.resolve({}, fixture1, "#dep/baz", {}, (err, result) => {
+			if (!err) return done(new Error(`expect error, got ${result}`));
+			expect(err).toBeInstanceOf(Error);
+			expect(err.message).toMatch(/Can't resolve '#dep\/baz' in/);
+			done();
+		});
+	});
+
+	it("should work with invalid imports #4", done => {
+		resolver.resolve({}, fixture1, "#dep/baz-multi", {}, (err, result) => {
+			if (!err) return done(new Error(`expect error, got ${result}`));
+			expect(err).toBeInstanceOf(Error);
+			expect(err.message).toMatch(/Can't resolve '#dep\/baz-multi' in/);
+			done();
+		});
+	});
+
+	it("should work with invalid imports #5", done => {
+		resolver.resolve({}, fixture1, "#dep/baz-multi", {}, (err, result) => {
+			if (!err) return done(new Error(`expect error, got ${result}`));
+			expect(err).toBeInstanceOf(Error);
+			expect(err.message).toMatch(/Can't resolve '#dep\/baz-multi' in/);
+			done();
+		});
+	});
+
+	it("should work with invalid imports #6", done => {
+		resolver.resolve({}, fixture1, "#dep/pattern/a.js", {}, (err, result) => {
+			if (err) return done(err);
+			if (!result) return done(new Error("No result"));
+			expect(result).toEqual(path.resolve(fixture1, "./a.js"));
+			done();
+		});
+	});
+
+	it("should work with invalid imports #7", done => {
+		resolver.resolve({}, fixture1, "#dep/array", {}, (err, result) => {
+			if (err) return done(err);
+			if (!result) return done(new Error("No result"));
+			expect(result).toEqual(path.resolve(fixture1, "./a.js"));
+			done();
+		});
+	});
+
+	it("should work with invalid imports #8", done => {
+		resolver.resolve({}, fixture1, "#dep/array2", {}, (err, result) => {
+			if (err) return done(err);
+			if (!result) return done(new Error("No result"));
+			expect(result).toEqual(path.resolve(fixture1, "./a.js"));
+			done();
+		});
+	});
+
+	it("should work with invalid imports #9", done => {
+		resolver.resolve({}, fixture1, "#dep/array3", {}, (err, result) => {
+			if (err) return done(err);
+			if (!result) return done(new Error("No result"));
+			expect(result).toEqual(path.resolve(fixture1, "./a.js"));
+			done();
+		});
+	});
+
+	it("should work with invalid imports #10", done => {
+		resolver.resolve({}, fixture1, "#dep/empty", {}, (err, result) => {
+			if (!err) return done(new Error(`expect error, got ${result}`));
+			expect(err).toBeInstanceOf(Error);
+			expect(err.message).toMatch(/Can't resolve '#dep\/empty' in/);
+			done();
+		});
+	});
+
+	it("should work with invalid imports #10", done => {
+		resolver.resolve({}, fixture1, "#dep/with-bad", {}, (err, result) => {
+			if (err) return done(err);
+			if (!result) return done(new Error("No result"));
+			expect(result).toEqual(path.resolve(fixture1, "./a.js"));
+			done();
+		});
+	});
+
+	it("should work with invalid imports #10", done => {
+		resolver.resolve({}, fixture1, "#dep/with-bad2", {}, (err, result) => {
+			if (err) return done(err);
+			if (!result) return done(new Error("No result"));
+			expect(result).toEqual(path.resolve(fixture1, "./a.js"));
+			done();
+		});
+	});
+
+	it("should work with invalid imports #11", done => {
+		resolver.resolve({}, fixture1, "#timezones/pdt.mjs", {}, (err, result) => {
+			if (!err) return done(new Error(`expect error, got ${result}`));
+			expect(err).toBeInstanceOf(Error);
+			expect(err.message).toMatch(/Expecting folder to folder mapping/);
+			done();
+		});
+	});
 });
diff --git a/test/path.test.js b/test/path.test.js
deleted file mode 100644
index 8629ea9..0000000
--- a/test/path.test.js
+++ /dev/null
@@ -1,29 +0,0 @@
-const { checkImportsExportsFieldTarget } = require("../lib/util/path");
-
-describe("checkImportsExportsFieldTarget", () => {
-	/**
-	 * @type {string[]}
-	 */
-	const errorCases = [
-		"../a.js",
-		"../",
-		"./a/b/../../../c.js",
-		"./a/b/../../../",
-		"./../../c.js",
-		"./../../",
-		"./a/../b/../../c.js",
-		"./a/../b/../../",
-		"./././../"
-	];
-
-	errorCases.forEach(case_ => {
-		it(case_, done => {
-			const error = checkImportsExportsFieldTarget(case_);
-			if (!error) return done("expect error");
-
-			expect(error).toBeInstanceOf(Error);
-			expect(error.message).toMatch(/Trying to access out of package scope/);
-			done();
-		});
-	});
-});