Repository: webpack/enhanced-resolve (js)

Issue:
`extensionAlias` is not applied to module exports
Let’s say we have a TypeScript mono repo with one package that depends on another. The actual TypeScript configuration is left out, because it’s not needed for this example.

`webpack.config.js`: 

```js
/** @type {import('webpack').Configuration} */
export default {
  entry: './packages/foo/index.ts',
  mode: 'development',
  resolve: {
    extensions: ['.js', '.ts'],
    extensionAlias: {
      '.js': ['.js', '.ts']
    }
  }
}
```

`package.json`: 

```json
{
  "type": "module",
  "workspaces": [
    "packages/*"
  ],
  "dependencies": {
    "webpack": "^5.0.0",
    "webpack-cli": "^4.0.0"
  }
}
```

`foo/packages/package.json`: 

```json
{
  "name": "foo",
  "type": "module",
  "exports": "./index.js"
}
```

`foo/packages/index.ts`: 

```ts
import { bar } from 'bar'

console.log(bar)
```

`bar/packages/package.json`: 

```json
{
  "name": "bar",
  "type": "module",
  "exports": "./index.js"
}
```

`bar/packages/index.ts`: 

```ts
export const bar = 'bar'
```

Now if we build using Webpack, it throws the following error:

```
~/Projects/bug$ webpack
asset main.js 2.34 KiB [emitted] (name: main)
runtime modules 274 bytes 1 module
./packages/foo/index.ts 44 bytes [built] [code generated]

ERROR in ./packages/foo/index.ts 1:0-25
Module not found: Error: Can't resolve 'bar' in '/home/remco/Projects/bug/packages/foo'
resolve 'bar' in '/home/remco/Projects/bug/packages/foo'
  Parsed request is a module
  using description file: /home/remco/Projects/bug/packages/foo/package.json (relative path: .)
    Field 'browser' doesn't contain a valid alias configuration
    resolve as module
      /home/remco/Projects/bug/packages/foo/node_modules doesn't exist or is not a directory
      /home/remco/Projects/bug/packages/node_modules doesn't exist or is not a directory
      looking for modules in /home/remco/Projects/bug/node_modules
        single file module
          using description file: /home/remco/Projects/bug/package.json (relative path: ./node_modules/bar)
            no extension
              Field 'browser' doesn't contain a valid alias configuration
              /home/remco/Projects/bug/node_modules/bar is not a file
            .js
              Field 'browser' doesn't contain a valid alias configuration
              /home/remco/Projects/bug/node_modules/bar.js doesn't exist
            .ts
              Field 'browser' doesn't contain a valid alias configuration
              /home/remco/Projects/bug/node_modules/bar.ts doesn't exist
        existing directory /home/remco/Projects/bug/node_modules/bar
          using description file: /home/remco/Projects/bug/node_modules/bar/package.json (relative path: .)
            using exports field: ./index.js
              using description file: /home/remco/Projects/bug/node_modules/bar/package.json (relative path: ./index.js)
                no extension
                  Field 'browser' doesn't contain a valid alias configuration
                  /home/remco/Projects/bug/node_modules/bar/index.js doesn't exist
                .js
                  Field 'browser' doesn't contain a valid alias configuration
                  /home/remco/Projects/bug/node_modules/bar/index.js.js doesn't exist
                .ts
                  Field 'browser' doesn't contain a valid alias configuration
                  /home/remco/Projects/bug/node_modules/bar/index.js.ts doesn't exist
                as directory
                  /home/remco/Projects/bug/node_modules/bar/index.js doesn't exist
      /home/remco/Projects/node_modules doesn't exist or is not a directory
      /home/remco/node_modules doesn't exist or is not a directory
      /home/node_modules doesn't exist or is not a directory
      /node_modules doesn't exist or is not a directory
```

This shows that `extensionAlias` isn’t applied when resolving `import 'bar'` from the `foo` project. This can be worked around by removing the `exports` field from `package.json`, but that will cause Node.js to show deprecation warnings if the package would be published and consumed.

Pull request:
fix: resolve for exports fields and exports fields
fixes #355

Patch:
diff --git a/lib/ExtensionAliasPlugin.js b/lib/ExtensionAliasPlugin.js
index 76ce9b4..84e0463 100644
--- a/lib/ExtensionAliasPlugin.js
+++ b/lib/ExtensionAliasPlugin.js
@@ -36,17 +36,40 @@ module.exports = class ExtensionAliasPlugin {
 			.tapAsync("ExtensionAliasPlugin", (request, resolveContext, callback) => {
 				const requestPath = request.request;
 				if (!requestPath || !requestPath.endsWith(extension)) return callback();
-				const resolve = (alias, callback) => {
-					resolver.doResolve(
+				const isAliasString = typeof alias === "string";
+				const resolve = (alias, callback, index) => {
+					const newRequest = `${requestPath.slice(
+						0,
+						-extension.length
+					)}${alias}`;
+
+					return resolver.doResolve(
 						target,
 						{
 							...request,
-							request: `${requestPath.slice(0, -extension.length)}${alias}`,
+							request: newRequest,
 							fullySpecified: true
 						},
 						`aliased from extension alias with mapping '${extension}' to '${alias}'`,
 						resolveContext,
-						callback
+						(err, result) => {
+							// Throw error if we are on the last alias (for multiple aliases) and it failed, always throw if we are not an array or we have only one alias
+							if (!isAliasString && index) {
+								if (index !== this.options.alias.length) {
+									if (resolveContext.log) {
+										resolveContext.log(
+											`Failed to alias from extension alias with mapping '${extension}' to '${alias}' for '${newRequest}': ${err}`
+										);
+									}
+
+									return callback(null, result);
+								}
+
+								return callback(err, result);
+							} else {
+								callback(err, result);
+							}
+						}
 					);
 				};
 
@@ -56,7 +79,7 @@ module.exports = class ExtensionAliasPlugin {
 					// Don't allow other aliasing or raw request
 					return callback(null, null);
 				};
-				if (typeof alias === "string") {
+				if (isAliasString) {
 					resolve(alias, stoppingCallback);
 				} else if (alias.length > 1) {
 					forEachBail(alias, resolve, stoppingCallback);
diff --git a/lib/forEachBail.js b/lib/forEachBail.js
index 031a562..c3c989e 100644
--- a/lib/forEachBail.js
+++ b/lib/forEachBail.js
@@ -11,13 +11,17 @@ module.exports = function forEachBail(array, iterator, callback) {
 	let i = 0;
 	const next = () => {
 		let loop = undefined;
-		iterator(array[i++], (err, result) => {
-			if (err || result !== undefined || i >= array.length) {
-				return callback(err, result);
-			}
-			if (loop === false) while (next());
-			loop = true;
-		});
+		iterator(
+			array[i++],
+			(err, result) => {
+				if (err || result !== undefined || i >= array.length) {
+					return callback(err, result);
+				}
+				if (loop === false) while (next());
+				loop = true;
+			},
+			i
+		);
 		if (!loop) loop = false;
 		return loop;
 	};


Test patch:
diff --git a/test/exportsField.js b/test/exportsField.js
index 035fc75..cf8089e 100644
--- a/test/exportsField.js
+++ b/test/exportsField.js
@@ -2911,4 +2911,125 @@ describe("ExportsFieldPlugin", () => {
 			}
 		);
 	});
+
+	it("should resolve with the `extensionAlias` option", done => {
+		const resolver = ResolverFactory.createResolver({
+			extensions: [".js"],
+			extensionAlias: {
+				".js": [".ts", ".js"]
+			},
+			fileSystem: nodeFileSystem,
+			fullySpecified: true,
+			conditionNames: ["webpack", "default"]
+		});
+		const fixture = path.resolve(
+			__dirname,
+			"./fixtures/exports-field-and-extension-alias/"
+		);
+
+		resolver.resolve({}, fixture, "@org/pkg/string.js", {}, (err, result) => {
+			if (err) return done(err);
+			if (!result) throw new Error("No result");
+			result.should.equal(
+				path.resolve(fixture, "./node_modules/@org/pkg/dist/string.js")
+			);
+			done();
+		});
+	});
+
+	it("should resolve with the `extensionAlias` option #2", done => {
+		const resolver = ResolverFactory.createResolver({
+			extensions: [".js"],
+			extensionAlias: {
+				".js": [".ts", ".js"]
+			},
+			fileSystem: nodeFileSystem,
+			fullySpecified: true,
+			conditionNames: ["webpack", "default"]
+		});
+		const fixture = path.resolve(
+			__dirname,
+			"./fixtures/exports-field-and-extension-alias/"
+		);
+
+		resolver.resolve({}, fixture, "pkg/string.js", {}, (err, result) => {
+			if (err) return done(err);
+			if (!result) throw new Error("No result");
+			result.should.equal(
+				path.resolve(fixture, "./node_modules/pkg/dist/string.js")
+			);
+			done();
+		});
+	});
+
+	it("should resolve with the `extensionAlias` option #3", done => {
+		const resolver = ResolverFactory.createResolver({
+			extensions: [".js"],
+			extensionAlias: {
+				".js": [".foo", ".baz", ".baz", ".ts", ".js"]
+			},
+			fileSystem: nodeFileSystem,
+			fullySpecified: true,
+			conditionNames: ["webpack", "default"]
+		});
+		const fixture = path.resolve(
+			__dirname,
+			"./fixtures/exports-field-and-extension-alias/"
+		);
+
+		resolver.resolve({}, fixture, "pkg/string.js", {}, (err, result) => {
+			if (err) return done(err);
+			if (!result) throw new Error("No result");
+			result.should.equal(
+				path.resolve(fixture, "./node_modules/pkg/dist/string.js")
+			);
+			done();
+		});
+	});
+
+	it("should throw error with the `extensionAlias` option", done => {
+		const resolver = ResolverFactory.createResolver({
+			extensions: [".js"],
+			extensionAlias: {
+				".js": [".ts"]
+			},
+			fileSystem: nodeFileSystem,
+			fullySpecified: true,
+			conditionNames: ["webpack", "default"]
+		});
+		const fixture = path.resolve(
+			__dirname,
+			"./fixtures/exports-field-and-extension-alias/"
+		);
+
+		resolver.resolve({}, fixture, "pkg/string.js", {}, (err, result) => {
+			if (!err) throw new Error(`expect error, got ${result}`);
+			err.should.be.instanceof(Error);
+			err.message.should.match(/Package path \.\/string\.ts is not exported/);
+			done();
+		});
+	});
+
+	it("should throw error with the `extensionAlias` option #2", done => {
+		const resolver = ResolverFactory.createResolver({
+			extensions: [".js"],
+			extensionAlias: {
+				".js": ".ts"
+			},
+			fileSystem: nodeFileSystem,
+			fullySpecified: true,
+			conditionNames: ["webpack", "default"]
+		});
+		const fixture = path.resolve(
+			__dirname,
+			"./fixtures/exports-field-and-extension-alias/"
+		);
+
+		resolver.resolve({}, fixture, "pkg/string.js", {}, (err, result) => {
+			if (!err) throw new Error(`expect error, got ${result}`);
+			err.should.be.instanceof(Error);
+			err.message.should.match(/Package path \.\/string\.ts is not exported/);
+			done();
+		});
+	});
 });
diff --git a/test/fixtures/exports-field-and-extension-alias/node_modules/@org/pkg/dist/string.js b/test/fixtures/exports-field-and-extension-alias/node_modules/@org/pkg/dist/string.js
new file mode 100644
index 0000000..d1007aa
--- /dev/null
+++ b/test/fixtures/exports-field-and-extension-alias/node_modules/@org/pkg/dist/string.js
@@ -0,0 +1,1 @@
+export default "string";
diff --git a/test/fixtures/exports-field-and-extension-alias/node_modules/@org/pkg/dist/string.test.d.ts b/test/fixtures/exports-field-and-extension-alias/node_modules/@org/pkg/dist/string.test.d.ts
new file mode 100644
index 0000000..cb0ff5c
--- /dev/null
+++ b/test/fixtures/exports-field-and-extension-alias/node_modules/@org/pkg/dist/string.test.d.ts
@@ -0,0 +1,1 @@
+export {};
diff --git a/test/fixtures/exports-field-and-extension-alias/node_modules/@org/pkg/package.json b/test/fixtures/exports-field-and-extension-alias/node_modules/@org/pkg/package.json
new file mode 100644
index 0000000..b523a1e
--- /dev/null
+++ b/test/fixtures/exports-field-and-extension-alias/node_modules/@org/pkg/package.json
@@ -0,0 +1,9 @@
+{
+  "name": "@org/pkg",
+  "exports": {
+    "./*.js": {
+      "types": "./dist/*.d.ts",
+      "default": "./dist/*.js"
+    }
+  }
+}
diff --git a/test/fixtures/exports-field-and-extension-alias/node_modules/pkg/dist/string.js b/test/fixtures/exports-field-and-extension-alias/node_modules/pkg/dist/string.js
new file mode 100644
index 0000000..d1007aa
--- /dev/null
+++ b/test/fixtures/exports-field-and-extension-alias/node_modules/pkg/dist/string.js
@@ -0,0 +1,1 @@
+export default "string";
diff --git a/test/fixtures/exports-field-and-extension-alias/node_modules/pkg/dist/string.test.d.ts b/test/fixtures/exports-field-and-extension-alias/node_modules/pkg/dist/string.test.d.ts
new file mode 100644
index 0000000..cb0ff5c
--- /dev/null
+++ b/test/fixtures/exports-field-and-extension-alias/node_modules/pkg/dist/string.test.d.ts
@@ -0,0 +1,1 @@
+export {};
diff --git a/test/fixtures/exports-field-and-extension-alias/node_modules/pkg/package.json b/test/fixtures/exports-field-and-extension-alias/node_modules/pkg/package.json
new file mode 100644
index 0000000..8e55fb4
--- /dev/null
+++ b/test/fixtures/exports-field-and-extension-alias/node_modules/pkg/package.json
@@ -0,0 +1,9 @@
+{
+  "name": "@raviqqe/hidash",
+  "exports": {
+    "./*.js": {
+      "types": "./dist/*.d.ts",
+      "default": "./dist/*.js"
+    }
+  }
+}