Repository: webpack/enhanced-resolve (js)

Issue:
[Question] The behavior of importsField conflicts with `require.resolve`
For the [out of package scope of importField](https://github.com/webpack/enhanced-resolve/blob/main/test/importsField.js#L1214) example, there are some inconsistencies between `require.resolve` and `enhanced-resolve`.

Executing `require.resolve('#b')` in the importsField folder will report an error directly:

<img width="1193" alt="image" src="https://user-images.githubusercontent.com/30187863/169676352-db069fd3-c1d2-4c88-a508-eb194f681a3b.png">

> node version: 12.22.10

But the above test case is executed normally.

So, should we add [checkTarget](https://github.com/webpack/enhanced-resolve/blob/main/lib/util/path.js#L195) logic to importsField as well?

Pull request:
fix imports field
- throw error in imports field if trying to access out of package scope
- fix utility function `checkImportsExportsFieldTarget`

fixes #346 

Patch:
diff --git a/lib/ExportsFieldPlugin.js b/lib/ExportsFieldPlugin.js
index 2730753..45a514f 100644
--- a/lib/ExportsFieldPlugin.js
+++ b/lib/ExportsFieldPlugin.js
@@ -10,7 +10,7 @@ const DescriptionFileUtils = require("./DescriptionFileUtils");
 const forEachBail = require("./forEachBail");
 const { processExportsField } = require("./util/entrypoints");
 const { parseIdentifier } = require("./util/identifier");
-const { checkExportsFieldTarget } = require("./util/path");
+const { checkImportsExportsFieldTarget } = require("./util/path");
 
 /** @typedef {import("./Resolver")} Resolver */
 /** @typedef {import("./Resolver").ResolveStepHook} ResolveStepHook */
@@ -116,7 +116,7 @@ module.exports = class ExportsFieldPlugin {
 
 						const [relativePath, query, fragment] = parsedIdentifier;
 
-						const error = checkExportsFieldTarget(relativePath);
+						const error = checkImportsExportsFieldTarget(relativePath);
 
 						if (error) {
 							return callback(error);
diff --git a/lib/ImportsFieldPlugin.js b/lib/ImportsFieldPlugin.js
index 8be64a0..1807160 100644
--- a/lib/ImportsFieldPlugin.js
+++ b/lib/ImportsFieldPlugin.js
@@ -10,6 +10,7 @@ const DescriptionFileUtils = require("./DescriptionFileUtils");
 const forEachBail = require("./forEachBail");
 const { processImportsField } = require("./util/entrypoints");
 const { parseIdentifier } = require("./util/identifier");
+const { checkImportsExportsFieldTarget } = require("./util/path");
 
 /** @typedef {import("./Resolver")} Resolver */
 /** @typedef {import("./Resolver").ResolveStepHook} ResolveStepHook */
@@ -118,6 +119,12 @@ 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: {
diff --git a/lib/util/path.js b/lib/util/path.js
index b84c146..d7c148a 100644
--- a/lib/util/path.js
+++ b/lib/util/path.js
@@ -192,9 +192,9 @@ const cachedJoin = (rootPath, request) => {
 };
 exports.cachedJoin = cachedJoin;
 
-const checkExportsFieldTarget = relativePath => {
-	let lastNonSlashIndex = 2;
-	let slashIndex = relativePath.indexOf("/", 2);
+const checkImportsExportsFieldTarget = relativePath => {
+	let lastNonSlashIndex = 0;
+	let slashIndex = relativePath.indexOf("/", 1);
 	let cd = 0;
 
 	while (slashIndex !== -1) {
@@ -209,6 +209,8 @@ const checkExportsFieldTarget = relativePath => {
 					);
 				break;
 			}
+			case ".":
+				break;
 			default:
 				cd++;
 				break;
@@ -218,4 +220,4 @@ const checkExportsFieldTarget = relativePath => {
 		slashIndex = relativePath.indexOf("/", lastNonSlashIndex);
 	}
 };
-exports.checkExportsFieldTarget = checkExportsFieldTarget;
+exports.checkImportsExportsFieldTarget = checkImportsExportsFieldTarget;


Test patch:
diff --git a/test/exportsField.js b/test/exportsField.js
index 03f62b8..d2f2d68 100644
--- a/test/exportsField.js
+++ b/test/exportsField.js
@@ -10,6 +10,7 @@ const CachedInputFileSystem = require("../lib/CachedInputFileSystem");
 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");
 
 describe("Process exports field", function exportsField() {
 	/** @type {Array<{name: string, expect: string[]|Error, suite: [ExportsField, string, string[]]}>} */
@@ -2391,6 +2392,15 @@ describe("ExportsFieldPlugin", () => {
 		});
 	});
 
+	it("should throw error if target is invalid", done => {
+		resolver.resolve({}, fixture4, "exports-field", {}, (err, result) => {
+			if (!err) throw new Error(`expect error, got ${result}`);
+			err.should.be.instanceof(Error);
+			err.message.should.match(/Trying to access out of package scope/);
+			done();
+		});
+	});
+
 	it("throw error if exports field is invalid", done => {
 		resolver.resolve(
 			{},
diff --git a/test/fixtures/exports-field-error/node_modules/exports-field/package.json b/test/fixtures/exports-field-error/node_modules/exports-field/package.json
new file mode 100644
index 0000000..f0edf44
--- /dev/null
+++ b/test/fixtures/exports-field-error/node_modules/exports-field/package.json
@@ -0,0 +1,7 @@
+{
+  "name": "exports-field",
+  "version": "1.0.0",
+  "exports": {
+    ".": "./a/../b/../../pack1/index.js"
+  }
+}
diff --git a/test/fixtures/exports-field-error/node_modules/pack1/index.js b/test/fixtures/exports-field-error/node_modules/pack1/index.js
new file mode 100644
index 0000000..e69de29
diff --git a/test/importsField.js b/test/importsField.js
index a075a31..334cd87 100644
--- a/test/importsField.js
+++ b/test/importsField.js
@@ -1211,11 +1211,11 @@ describe("ImportsFieldPlugin", () => {
 		);
 	});
 
-	it("should resolve out of package scope", done => {
+	it("should disallow resolve out of package scope", done => {
 		resolver.resolve({}, fixture, "#b", {}, (err, result) => {
-			if (err) return done(err);
-			if (!result) throw new Error("No result");
-			result.should.equal(path.resolve(fixture, "../b.js"));
+			if (!err) throw new Error(`expect error, got ${result}`);
+			err.should.be.instanceof(Error);
+			err.message.should.match(/Trying to access out of package scope/);
 			done();
 		});
 	});
@@ -1229,10 +1229,10 @@ describe("ImportsFieldPlugin", () => {
 			conditionNames: ["webpack"]
 		});
 
-		resolver.resolve({}, fixture, "#b", {}, (err, result) => {
+		resolver.resolve({}, fixture, "#imports-field", {}, (err, result) => {
 			if (err) return done(err);
 			if (!result) throw new Error("No result");
-			result.should.equal(path.resolve(fixture, "../b.js"));
+			result.should.equal(path.resolve(fixture, "./b.js"));
 			done();
 		});
 	});
diff --git a/test/path.js b/test/path.js
new file mode 100644
index 0000000..8e9fd8f
--- /dev/null
+++ b/test/path.js
@@ -0,0 +1,30 @@
+require("should");
+
+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");
+			error.should.be.instanceof(Error);
+			error.message.should.match(/Trying to access out of package scope/);
+			done();
+		});
+	});
+});