Repository: webpack/webpack (JavaScript)

Issue:
import.meta.webpackContext doesn't work if requiring inside npm packages
<!-- Please don't delete this template because we'll close your issue -->
<!-- Before creating an issue please make sure you are using the latest version of webpack. -->

# Bug report

<!-- Please ask questions on StackOverflow or the webpack Gitter. -->
<!-- https://stackoverflow.com/questions/ask?tags=webpack -->
<!-- https://gitter.im/webpack/webpack -->
<!-- Issues which contain questions or support requests will be closed. -->

**What is the current behavior?**
When requiring files using `import.meta.webpackContext` (added in #15446) that are located within a node_modules folder, eg:

```js
const vuetify = import.meta.webpackContext('vuetify/lib/locale', {
  recursive: false,
  regExp: /(en|hu)\.js$/i,
});
```
It doesn't work properly and throws a warning during compilation
```
 WARNING  Compiled with 2 warnings                                                                                                                                                                                             6:40:36 PM

 warning  in ./node_modules/vuetify/lib/locale/ ./node_modules/vuetify/lib/locale/ sync nonrecursive (en%7Chu)\.js$/
 
Module not found: Error: Can't resolve 'webpack-meta-bug/node_modules/vuetify/lib/locale/vuetify/lib/locale/en.js' in 'webpack-meta-bug'
```

It seems to be adding the context path twice to the request

**If the current behavior is a bug, please provide the steps to reproduce.**
https://github.com/Tofandel/webpack-meta-bug

Use the following webpack config
```
const path = require('path')
module.exports = {
  resolve: {
    modules: [
      'node_modules',
      path.resolve(__dirname, './node_modules'),
    ]
  },
}
```

And use `import.meta.webpackContext('some_package_files')`

<!-- A great way to do this is to provide your configuration via a GitHub repository -->
<!-- The most helpful is a minimal reproduction with instructions on how to reproduce -->
<!-- Repositories with too many files or large `webpack.config.js` files are not suitable -->
<!-- Please only add small code snippets directly into this issue -->
<!-- https://gist.github.com is a good place for longer code snippets -->
<!-- If your issue is caused by a plugin or loader, please create an issue on the loader/plugin repository instead -->

**What is the expected behavior?**
It should require the files correctly

<!-- "It should work" is not a helpful explanation -->
<!-- Explain exactly how it should behave -->

**Other relevant information:**
webpack version:  5.70.0
Node.js version: 16.13.1
Operating System: WSL2 - Ubuntu 20.04 
Additional tools:



Hints:
Issue is happening there
https://github.com/webpack/webpack/blob/main/lib/dependencies/RequireContextPlugin.js#L132
you should remove several identical `resolve.modules` setting ( or in case of `node_modules` just remove this settings )
<img width="944" alt="Screenshot 2022-03-25 at 12 44 27" src="https://user-images.githubusercontent.com/10380560/160096425-412c6f6c-d916-4d05-b62e-7d51844ed762.png">

lets keep issue for now.. need to fix this anyway..
There is a fix #15581 ... It passes tests but the pipeline doesn't want to run on the PR

As for removing duplicate resolve.modules, that's the default of vue-cli, so not really an option given that the bug is indeed here, that was the whole point of this issue and fix in the first place, which took me hours to track down so I can imagine future users trying to use this in a vue project only to find the same bug, only to find after 2hours of debug and searching issues that the only workaround is to do

```js
  chainWebpack: config => {
    config.resolve.modules.delete('node_modules');
  }
```

The problem is the resolution adds an item like
`{context: 'node_modules/vuetify/lib/locales', request: 'vuetify/lib/locales/en.js' }`

When it should be 
`{context: 'node_modules', request: 'vuetify/lib/locales/en.js' }`

Or 

`{context: 'node_modules/vuetify/lib/locales', request: 'en.js' }` (but this one breaks tests)

Patch:
diff --git a/lib/Compilation.js b/lib/Compilation.js
--- a/lib/Compilation.js
+++ b/lib/Compilation.js
@@ -1436,7 +1436,7 @@ BREAKING CHANGE: Asset processing hooks in Compilation has been merged into a si
 	 * @returns {void}
 	 */
 	_processModuleDependencies(module, callback) {
-		/** @type {Array<{factory: ModuleFactory, dependencies: Dependency[], originModule: Module|null}>} */
+		/** @type {Array<{factory: ModuleFactory, dependencies: Dependency[], context: string|undefined, originModule: Module|null}>} */
 		const sortedDependencies = [];
 
 		/** @type {DependenciesBlock} */
@@ -1668,6 +1668,7 @@ BREAKING CHANGE: Asset processing hooks in Compilation has been merged into a si
 					sortedDependencies.push({
 						factory: factoryCacheKey2,
 						dependencies: list,
+						context: dep.getContext(),
 						originModule: module
 					});
 				}
diff --git a/lib/ContextModuleFactory.js b/lib/ContextModuleFactory.js
--- a/lib/ContextModuleFactory.js
+++ b/lib/ContextModuleFactory.js
@@ -292,7 +292,6 @@ module.exports = class ContextModuleFactory extends ModuleFactory {
 		} = options;
 		if (!regExp || !resource) return callback(null, []);
 
-		let severalContexts = false;
 		const addDirectoryChecked = (ctx, directory, visited, callback) => {
 			fs.realpath(directory, (err, realPath) => {
 				if (err) return callback(err);
@@ -359,15 +358,13 @@ module.exports = class ContextModuleFactory extends ModuleFactory {
 											alternatives = alternatives
 												.filter(obj => regExp.test(obj.request))
 												.map(obj => {
-													const request = severalContexts
-														? join(fs, obj.context, obj.request)
-														: obj.request;
 													const dep = new ContextElementDependency(
-														request + resourceQuery + resourceFragment,
+														`${obj.request}${resourceQuery}${resourceFragment}`,
 														obj.request,
 														typePrefix,
 														category,
-														referencedExports
+														referencedExports,
+														obj.context
 													);
 													dep.optional = true;
 													return dep;
@@ -414,7 +411,6 @@ module.exports = class ContextModuleFactory extends ModuleFactory {
 		if (typeof resource === "string") {
 			visitResource(resource, callback);
 		} else {
-			severalContexts = true;
 			asyncLib.map(resource, visitResource, (err, result) => {
 				if (err) return callback(err);
 
diff --git a/lib/Dependency.js b/lib/Dependency.js
--- a/lib/Dependency.js
+++ b/lib/Dependency.js
@@ -182,6 +182,13 @@ class Dependency {
 		this._loc = undefined;
 	}
 
+	/**
+	 * @returns {string | undefined} a request context
+	 */
+	getContext() {
+		return undefined;
+	}
+
 	/**
 	 * @returns {string | null} an identifier to merge equal requests
 	 */
diff --git a/lib/dependencies/ContextElementDependency.js b/lib/dependencies/ContextElementDependency.js
--- a/lib/dependencies/ContextElementDependency.js
+++ b/lib/dependencies/ContextElementDependency.js
@@ -14,11 +14,27 @@ const ModuleDependency = require("./ModuleDependency");
 /** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */
 
 class ContextElementDependency extends ModuleDependency {
-	constructor(request, userRequest, typePrefix, category, referencedExports) {
+	/**
+	 * @param {string} request request
+	 * @param {string|undefined} userRequest user request
+	 * @param {string} typePrefix type prefix
+	 * @param {string} category category
+	 * @param {string[][]=} referencedExports referenced exports
+	 * @param {string=} context context
+	 */
+	constructor(
+		request,
+		userRequest,
+		typePrefix,
+		category,
+		referencedExports,
+		context
+	) {
 		super(request);
 		this.referencedExports = referencedExports;
 		this._typePrefix = typePrefix;
 		this._category = category;
+		this._context = context || undefined;
 
 		if (userRequest) {
 			this.userRequest = userRequest;
@@ -33,6 +49,20 @@ class ContextElementDependency extends ModuleDependency {
 		return "context element";
 	}
 
+	/**
+	 * @returns {string | undefined} a request context
+	 */
+	getContext() {
+		return this._context;
+	}
+
+	/**
+	 * @returns {string | null} an identifier to merge equal requests
+	 */
+	getResourceIdentifier() {
+		return `context${this._context || ""}|${super.getResourceIdentifier()}`;
+	}
+
 	get category() {
 		return this._category;
 	}
@@ -56,6 +86,7 @@ class ContextElementDependency extends ModuleDependency {
 		const { write } = context;
 		write(this._typePrefix);
 		write(this._category);
+		write(this._context);
 		write(this.referencedExports);
 		super.serialize(context);
 	}
@@ -64,6 +95,7 @@ class ContextElementDependency extends ModuleDependency {
 		const { read } = context;
 		this._typePrefix = read();
 		this._category = read();
+		this._context = read();
 		this.referencedExports = read();
 		super.deserialize(context);
 	}
diff --git a/types.d.ts b/types.d.ts
--- a/types.d.ts
+++ b/types.d.ts
@@ -2462,7 +2462,7 @@ declare interface ContainerReferencePluginOptions {
 	shareScope?: string;
 }
 declare abstract class ContextElementDependency extends ModuleDependency {
-	referencedExports: any;
+	referencedExports?: string[][];
 }
 declare class ContextExclusionPlugin {
 	constructor(negativeMatcher: RegExp);
@@ -2645,6 +2645,7 @@ declare class Dependency {
 		endLine?: any,
 		endColumn?: any
 	): void;
+	getContext(): undefined | string;
 	getResourceIdentifier(): null | string;
 	couldAffectReferencingModule(): boolean | typeof TRANSITIVE;
 


Test patch:
diff --git a/test/ContextModuleFactory.unittest.js b/test/ContextModuleFactory.unittest.js
--- a/test/ContextModuleFactory.unittest.js
+++ b/test/ContextModuleFactory.unittest.js
@@ -148,9 +148,10 @@ describe("ContextModuleFactory", () => {
 					expect(res).not.toStrictEqual([]);
 					expect(Array.isArray(res)).toBe(true);
 					expect(res.map(r => r.request)).toEqual([
-						"/a/B/a?query#hash",
-						"/b/A/b?query#hash"
+						"./B/a?query#hash",
+						"./A/b?query#hash"
 					]);
+					expect(res.map(r => r.getContext())).toEqual(["/a", "/b"]);
 					expect(res.map(r => r.userRequest)).toEqual(["./B/a", "./A/b"]);
 					done();
 				}
diff --git a/test/configCases/resolve/issue-15580/index.js b/test/configCases/resolve/issue-15580/index.js
new file mode 100644
--- /dev/null
+++ b/test/configCases/resolve/issue-15580/index.js
@@ -0,0 +1,19 @@
+const locales = import.meta.webpackContext('./locales', {
+	recursive: false,
+	regExp: /(en|hu)\.json$/i,
+});
+const vuetify = import.meta.webpackContext('vuetify/lib/locale', {
+	recursive: false,
+	regExp: /(en|hu)\.json$/i,
+});
+
+it('should resolve "./locales"', () => {
+	expect(locales("./en.json")).toEqual({});
+	expect(() => locales("./hu.json")).toThrow();
+});
+
+it('should resolve "vuetify"', () => {
+	expect(vuetify("./en.json")).toEqual({});
+	expect(vuetify("./hu.json")).toEqual({});
+	expect(() => vuetify("./ru.json")).toThrow();
+});
diff --git a/test/configCases/resolve/issue-15580/locales/en.json b/test/configCases/resolve/issue-15580/locales/en.json
new file mode 100644
--- /dev/null
+++ b/test/configCases/resolve/issue-15580/locales/en.json
@@ -0,0 +1 @@
+{}
diff --git a/test/configCases/resolve/issue-15580/node_modules/vuetify/lib/locale/en.json b/test/configCases/resolve/issue-15580/node_modules/vuetify/lib/locale/en.json
new file mode 100644
--- /dev/null
+++ b/test/configCases/resolve/issue-15580/node_modules/vuetify/lib/locale/en.json
@@ -0,0 +1 @@
+{}
diff --git a/test/configCases/resolve/issue-15580/node_modules/vuetify/lib/locale/hu.json b/test/configCases/resolve/issue-15580/node_modules/vuetify/lib/locale/hu.json
new file mode 100644
--- /dev/null
+++ b/test/configCases/resolve/issue-15580/node_modules/vuetify/lib/locale/hu.json
@@ -0,0 +1 @@
+{}
diff --git a/test/configCases/resolve/issue-15580/node_modules/vuetify/lib/locale/ru.json b/test/configCases/resolve/issue-15580/node_modules/vuetify/lib/locale/ru.json
new file mode 100644
--- /dev/null
+++ b/test/configCases/resolve/issue-15580/node_modules/vuetify/lib/locale/ru.json
@@ -0,0 +1 @@
+{}
diff --git a/test/configCases/resolve/issue-15580/node_modules/vuetify/package.json b/test/configCases/resolve/issue-15580/node_modules/vuetify/package.json
new file mode 100644
--- /dev/null
+++ b/test/configCases/resolve/issue-15580/node_modules/vuetify/package.json
@@ -0,0 +1,4 @@
+{
+  "name": "vuetify",
+  "version": "1.0.0"
+}
diff --git a/test/configCases/resolve/issue-15580/webpack.config.js b/test/configCases/resolve/issue-15580/webpack.config.js
new file mode 100644
--- /dev/null
+++ b/test/configCases/resolve/issue-15580/webpack.config.js
@@ -0,0 +1,8 @@
+const path = require("path");
+
+/** @type {import("../../../../").Configuration} */
+module.exports = {
+	resolve: {
+		modules: ["node_modules", path.resolve(__dirname, "./node_modules")]
+	}
+};