Repository: webpack/webpack (JavaScript)

Issue:
Use of dynamic imports w/ expressions + ContextReplacementPlugin makes builds non-reproducible / project location dependant
<!-- 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 using dynamic imports with expressions together with `ContextReplacementPlugin` in order to limit the possible file matches, some module ids which should deterministic, are actually dependent on the project's location.
Meaning two identical projects (code, dependencies, configuration etc..) built on the same machine have different build outputs based on their location on the filesystem.

**If the current behavior is a bug, please provide the steps to reproduce.**

- use dynamic imports with expessions
  - ie: **import(`../i18n/${locale}.json`)**)
- configure Webpack to use `ContextReplacementPlugin` to limit which JSON files should be bundled
  - ie: `new webpack.ContextReplacementPlugin(/i18n/, regExpForLocales)`
- build the project for production
- duplicate the project at another location on the filesystem at a different path root level
  - ie: `/home/project` vs `/home/work/projects`
- build the project for production at the new location
- diff both output bundles, observe that both bundles are different, invalidating the project build's reproducibility 

```bash
$ diff -u context-a/dist/main.js folder-b/context-b/dist/main.js
```
```diff
--- context-a/dist/main.js	2022-01-10 14:52:49.395750091 +0100
+++ folder-b/context-b/dist/main.js	2022-01-10 14:52:52.143773687 +0100
@@ -4,7 +4,7 @@
     t,
     o,
     n = {
-      595: (e, r, t) => {
+      174: (e, r, t) => {
         var o = { "./a.json": [359, 359], "./b.json": [376, 376] };
         function n(e) {
           if (!t.o(o, e))
@@ -16,7 +16,7 @@
             n = r[0];
           return t.e(r[1]).then(() => t.t(n, 19));
         }
-        (n.keys = () => Object.keys(o)), (n.id = 595), (e.exports = n);
+        (n.keys = () => Object.keys(o)), (n.id = 174), (e.exports = n);
       },
     },
     i = {};

```

[Minimal reproduction repository](https://github.com/ziir/webpack-repro-dynamic-import-context-replacement-deterministic-module-id)

<!-- 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?**

- both bundles should be identical irrelevant of their location on the filesystem

<!-- "It should work" is not a helpful explanation -->
<!-- Explain exactly how it should behave -->

**Other relevant information:**
webpack version:  *5.65.0*
Node.js version:  *14.18.1*
Operating System: *Linux Ubuntu 20.04.3 LTS*
Additional tools: *N/A*



Hints:
Can you provide your configuration file?
Hi @alexander-akait, updated the issue's description with a link to the [minimal reproduction repository](https://github.com/ziir/webpack-repro-dynamic-import-context-replacement-deterministic-module-id) containing [the configuration file](https://github.com/ziir/webpack-repro-dynamic-import-context-replacement-deterministic-module-id/blob/main/context-a/webpack.config.js)
I believe the issue presents specifically when the regex of `ContextReplacementPlugin` contains `|` characters.

When hashing modules (in `DeterministicModuleIdsPlugin`), we make calls to `IDHelpers.getFullModuleName` -> `makePathsRelative` -> `_makePathsRelative`. Here, the identifier is split on `|` and `!` characters, then passed to `absoluteToRequest`.

While `absoluteToRequest` handles regex's in identifiers, the splitting in `_makePathsRelative` splits up the regex in the identifier to look like 2 seperate "pieces", which `absoluteToRequest` will then attempt to `path.relative`-ize

Patch:
diff --git a/lib/ContextModule.js b/lib/ContextModule.js
--- a/lib/ContextModule.js
+++ b/lib/ContextModule.js
@@ -149,12 +149,9 @@ class ContextModule extends Module {
 		this.resolveDependencies = undefined;
 	}
 
-	prettyRegExp(regexString) {
-		// remove the "/" at the front and the beginning
-		// "/foo/" -> "foo"
-		return regexString
-			.substring(1, regexString.length - 1)
-			.replace(/!/g, "%21");
+	_prettyRegExp(regexString, stripSlash = true) {
+		const str = (regexString + "").replace(/!/g, "%21").replace(/\|/g, "%7C");
+		return stripSlash ? str.substring(1, str.length - 1) : str;
 	}
 
 	_createIdentifier() {
@@ -175,13 +172,19 @@ class ContextModule extends Module {
 			identifier += `|${this.options.addon}`;
 		}
 		if (this.options.regExp) {
-			identifier += `|${this.options.regExp}`;
+			identifier += `|${this._prettyRegExp(this.options.regExp, false)}`;
 		}
 		if (this.options.include) {
-			identifier += `|include: ${this.options.include}`;
+			identifier += `|include: ${this._prettyRegExp(
+				this.options.include,
+				false
+			)}`;
 		}
 		if (this.options.exclude) {
-			identifier += `|exclude: ${this.options.exclude}`;
+			identifier += `|exclude: ${this._prettyRegExp(
+				this.options.exclude,
+				false
+			)}`;
 		}
 		if (this.options.referencedExports) {
 			identifier += `|referencedExports: ${JSON.stringify(
@@ -231,13 +234,13 @@ class ContextModule extends Module {
 			identifier += ` ${requestShortener.shorten(this.options.addon)}`;
 		}
 		if (this.options.regExp) {
-			identifier += ` ${this.prettyRegExp(this.options.regExp + "")}`;
+			identifier += ` ${this._prettyRegExp(this.options.regExp)}`;
 		}
 		if (this.options.include) {
-			identifier += ` include: ${this.prettyRegExp(this.options.include + "")}`;
+			identifier += ` include: ${this._prettyRegExp(this.options.include)}`;
 		}
 		if (this.options.exclude) {
-			identifier += ` exclude: ${this.prettyRegExp(this.options.exclude + "")}`;
+			identifier += ` exclude: ${this._prettyRegExp(this.options.exclude)}`;
 		}
 		if (this.options.referencedExports) {
 			identifier += ` referencedExports: ${this.options.referencedExports
@@ -286,13 +289,13 @@ class ContextModule extends Module {
 			)}`;
 		}
 		if (this.options.regExp) {
-			identifier += ` ${this.prettyRegExp(this.options.regExp + "")}`;
+			identifier += ` ${this._prettyRegExp(this.options.regExp)}`;
 		}
 		if (this.options.include) {
-			identifier += ` include: ${this.prettyRegExp(this.options.include + "")}`;
+			identifier += ` include: ${this._prettyRegExp(this.options.include)}`;
 		}
 		if (this.options.exclude) {
-			identifier += ` exclude: ${this.prettyRegExp(this.options.exclude + "")}`;
+			identifier += ` exclude: ${this._prettyRegExp(this.options.exclude)}`;
 		}
 		if (this.options.referencedExports) {
 			identifier += ` referencedExports: ${this.options.referencedExports


Test patch:
diff --git a/test/ContextModule.unittest.js b/test/ContextModule.unittest.js
new file mode 100644
--- /dev/null
+++ b/test/ContextModule.unittest.js
@@ -0,0 +1,22 @@
+"use strict";
+
+const ContextModule = require("../lib/ContextModule");
+
+describe("contextModule", () => {
+	let contextModule;
+	let request;
+	beforeEach(() => {
+		request = "/some/request";
+	});
+	describe("#identifier", () => {
+		it("returns an safe identifier for this module", () => {
+			contextModule = new ContextModule(() => {}, {
+				type: "javascript/auto",
+				request,
+				mode: "lazy",
+				regExp: /a|b/
+			});
+			expect(contextModule.identifier()).toContain("/a%7Cb/");
+		});
+	});
+});