Repository: webpack/webpack (JavaScript)

Issue:
ModuleFederationPlugin ignores singleton: true when requiredVersion is false
<!-- 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 creating a new ModuleFederationPlugin with shared libraries, if one of the packages has a configuration of `singleton: true, requiredVersion: false`, it will produce a remote entry file that uses the "loadFallback" function to try and get the package. This can lead to package duplication if a newer version of the library is provided by this module (or another module).

**If the current behavior is a bug, please provide the steps to reproduce.**

Sample repo: https://github.com/colacadstink/WebpackModuleFederationBug

The dist directory is created from the standard webpack.config.js, and the workingDist directory from the webpack.working.config.js file. The only differences between these files are the output directories and the requiredVersion of lodash.

Note the differences between dist/remoteEntry.js and workingDist/remoteEntry.js, specifically on line 380:

- The dist copy of the file is using the loadFallback function, which will always return the highest version number available, regardless of if another instance has already been created; this can lead to situations where more than one instance is constructed, breaking the singleton flag. 
- The workingDist copy uses the loadSingletonVersionCheckFallback function with a version argument of [0]. This will let us use whatever version of the package is currently loaded, regardless of version, which is the same behavior I'd expected with the requiredVersion being false.

<!-- 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?**

I feel as if this should be acting similarly to what happens if the requiredVersion is set to '*', which causes the method used in the remote entry file to be "loadStrictSingletonVersionCheckFallback" with a version argument of `[0]` (which does work as expected).

<!-- "It should work" is not a helpful explanation -->
<!-- Explain exactly how it should behave -->

**Other relevant information:**
webpack version:  v5.64.2
Node.js version: Tested on v12.16.1 and v12.20.1
Operating System: n/a, but definitely there on Windows
Additional tools: I used Lodash for demonstration purposes, but there's nothing special about it.



Hints:
Minor clarification: Using `"*"` is **not** identical to using `false`. `false` should (again, as I understand it) accept all versions, including pre-release versions like `1.0.0-SNAPSHOT`, whereas `"*"` will reject that version (as it should per semver standards). So right now, if I want to have a singleton instance of a package, not care what version it is, including allowing pre-release versions, I cannot do so without potentially having two instances of my package running around at the same time.

Patch:
diff --git a/lib/sharing/ConsumeSharedModule.js b/lib/sharing/ConsumeSharedModule.js
--- a/lib/sharing/ConsumeSharedModule.js
+++ b/lib/sharing/ConsumeSharedModule.js
@@ -210,6 +210,10 @@ class ConsumeSharedModule extends Module {
 			}
 			args.push(stringifyHoley(requiredVersion));
 			fn += "VersionCheck";
+		} else {
+			if (singleton) {
+				fn += "Singleton";
+			}
 		}
 		if (fallbackCode) {
 			fn += "Fallback";
diff --git a/lib/sharing/ConsumeSharedRuntimeModule.js b/lib/sharing/ConsumeSharedRuntimeModule.js
--- a/lib/sharing/ConsumeSharedRuntimeModule.js
+++ b/lib/sharing/ConsumeSharedRuntimeModule.js
@@ -108,6 +108,13 @@ class ConsumeSharedRuntimeModule extends RuntimeModule {
 					`return "Unsatisfied version " + version + " from " + (version && scope[key][version].from) + " of shared singleton module " + key + " (required " + rangeToString(requiredVersion) + ")"`
 				]
 			)};`,
+			`var getSingleton = ${runtimeTemplate.basicFunction(
+				"scope, scopeName, key, requiredVersion",
+				[
+					"var version = findSingletonVersionKey(scope, key);",
+					"return get(scope[key][version]);"
+				]
+			)};`,
 			`var getSingletonVersion = ${runtimeTemplate.basicFunction(
 				"scope, scopeName, key, requiredVersion",
 				[
@@ -202,6 +209,13 @@ class ConsumeSharedRuntimeModule extends RuntimeModule {
 					"return get(findValidVersion(scope, key, version) || warnInvalidVersion(scope, scopeName, key, version) || findVersion(scope, key));"
 				]
 			)});`,
+			`var loadSingleton = /*#__PURE__*/ init(${runtimeTemplate.basicFunction(
+				"scopeName, scope, key",
+				[
+					"ensureExistence(scopeName, key);",
+					"return getSingleton(scope, scopeName, key);"
+				]
+			)});`,
 			`var loadSingletonVersionCheck = /*#__PURE__*/ init(${runtimeTemplate.basicFunction(
 				"scopeName, scope, key, version",
 				[
@@ -230,6 +244,13 @@ class ConsumeSharedRuntimeModule extends RuntimeModule {
 					"return get(findValidVersion(scope, key, version) || warnInvalidVersion(scope, scopeName, key, version) || findVersion(scope, key));"
 				]
 			)});`,
+			`var loadSingletonFallback = /*#__PURE__*/ init(${runtimeTemplate.basicFunction(
+				"scopeName, scope, key, fallback",
+				[
+					`if(!scope || !${RuntimeGlobals.hasOwnProperty}(scope, key)) return fallback();`,
+					"return getSingleton(scope, scopeName, key);"
+				]
+			)});`,
 			`var loadSingletonVersionCheckFallback = /*#__PURE__*/ init(${runtimeTemplate.basicFunction(
 				"scopeName, scope, key, version, fallback",
 				[


Test patch:
diff --git a/test/configCases/sharing/consume-module/index.js b/test/configCases/sharing/consume-module/index.js
--- a/test/configCases/sharing/consume-module/index.js
+++ b/test/configCases/sharing/consume-module/index.js
@@ -196,6 +196,15 @@ it("should handle version matching correctly in strict and singleton mode", asyn
 				get: () => () => "shared singleton",
 				from: 'container-a'
 			}
+		},
+		singletonWithoutVersion: {
+			"1.0.0": {
+				get: () => () => "shared singleton v1.0.0",
+				loaded: true
+			},
+			"2.0.0": {
+				get: () => () => "shared singleton v2.0.0"
+			}
 		}
 	};
 	{
@@ -235,3 +244,21 @@ it("should handle version matching correctly in strict and singleton mode", asyn
 		);
 	}
 });
+
+it("should not instantiate multiple singletons even if a higher version exists", async () => {
+	__webpack_share_scopes__["default"] = {
+		singletonWithoutVersion: {
+			"1.0.0": {
+				get: () => () => "shared singleton v1.0.0",
+				loaded: true
+			},
+			"2.0.0": {
+				get: () => () => "shared singleton v2.0.0"
+			}
+		}
+	};
+	{
+		const result = await import("singletonWithoutVersion");
+		expect(result.default).toBe("shared singleton v1.0.0");
+	}
+});
diff --git a/test/configCases/sharing/consume-module/node_modules/singletonWithoutVersion.js b/test/configCases/sharing/consume-module/node_modules/singletonWithoutVersion.js
new file mode 100644
--- /dev/null
+++ b/test/configCases/sharing/consume-module/node_modules/singletonWithoutVersion.js
@@ -0,0 +1 @@
+module.exports = "singleton without version";
diff --git a/test/configCases/sharing/consume-module/webpack.config.js b/test/configCases/sharing/consume-module/webpack.config.js
--- a/test/configCases/sharing/consume-module/webpack.config.js
+++ b/test/configCases/sharing/consume-module/webpack.config.js
@@ -54,6 +54,10 @@ module.exports = {
 					requiredVersion: "1.1.0",
 					singleton: true,
 					strictVersion: false
+				},
+				singletonWithoutVersion: {
+					requiredVersion: false,
+					singleton: true
 				}
 			}
 		})