Repository: webpack/webpack (JavaScript)

Issue:
New case for "__webpack_modules__[moduleId] is not a function" error with Module Federation
<!-- 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 GitHub Discussions. -->
<!-- https://github.com/webpack/webpack/discussions -->
<!-- https://stackoverflow.com/questions/ask?tags=webpack -->
<!-- Issues which contain questions or support requests will be closed. -->

**What is the current behavior?**
With a certain dependency graph, webpack module federation applications crash with error:
```
__webpack_modules__[moduleId] is not a function
or Cannot read properties of undefined (reading 'call')
```
This is indeed a known bug and had been fixed, but it happens again with module federation https://github.com/webpack/webpack/issues/11770
Issue describe:
* Folder structure:
```
/src
├── /components
│   ├── Button.js
├── /helpers
│   ├── index.js
├── /utils
│   ├── index.js
├── App.js
```
* Dependency graph:

![example remote app](https://github.com/webpack/webpack/assets/41647635/955ec8c3-7751-485e-a17e-f3ca847beb50)

* Bundle result: 

![chunking result](https://github.com/webpack/webpack/assets/41647635/a49bd137-2474-48ff-a5cc-a23ae184b5de)

* What is the problem:
`./src/utils/index.jsx` module is declared in `src_helpers_index_js` but required in `src_comopnent_Button_js`, so in runtime, it cannot find the module

Like I mentioned, this is a known issue and @sokra had fixed it in https://github.com/webpack/webpack/issues/11770. But he didn't handle for module federation case, because in this case, two federated modules `./RemoteApp` and `./helpers` are using the same runtime id

**If the current behavior is a bug, please provide the steps to reproduce.**
https://github.com/tqnghia1998/webpack-chunking-error-demo  (latest webpack version, just yarn install, then yarn start)

<!-- 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?**
No runtime errors, `./src/utils/index.jsx` should be initialized when loading RemoteApp

<!-- "It should work" is not a helpful explanation -->
<!-- Explain exactly how it should behave -->

**Other relevant information:**
Webpack version:  5.89.0
Node.js version: v18.12.1
Operating System: Mac 14.2.1
Additional tools: -



Hints:
PR welcome

Patch:
diff --git a/lib/buildChunkGraph.js b/lib/buildChunkGraph.js
--- a/lib/buildChunkGraph.js
+++ b/lib/buildChunkGraph.js
@@ -44,7 +44,7 @@ const { getEntryRuntime, mergeRuntime } = require("./util/runtime");
  * @property {boolean | undefined} minAvailableModulesOwned true, if minAvailableModules is owned and can be modified
  * @property {ModuleSetPlus[]} availableModulesToBeMerged enqueued updates to the minimal set of available modules
  * @property {Set<Module>=} skippedItems modules that were skipped because module is already available in parent chunks (need to reconsider when minAvailableModules is shrinking)
- * @property {Set<[Module, ConnectionState]>=} skippedModuleConnections referenced modules that where skipped because they were not active in this runtime
+ * @property {Set<[Module, ModuleGraphConnection[]]>=} skippedModuleConnections referenced modules that where skipped because they were not active in this runtime
  * @property {ModuleSetPlus | undefined} resultingAvailableModules set of modules available including modules from this chunk group
  * @property {Set<ChunkGroupInfo> | undefined} children set of children chunk groups, that will be revisited when availableModules shrink
  * @property {Set<ChunkGroupInfo> | undefined} availableSources set of chunk groups that are the source for minAvailableModules
@@ -73,6 +73,25 @@ const bySetSize = (a, b) => {
 	return b.size + b.plus.size - a.size - a.plus.size;
 };
 
+/**
+ * @param {ModuleGraphConnection[]} connections list of connections
+ * @param {RuntimeSpec} runtime for which runtime
+ * @returns {ConnectionState} connection state
+ */
+const getActiveStateOfConnections = (connections, runtime) => {
+	let merged = connections[0].getActiveState(runtime);
+	if (merged === true) return true;
+	for (let i = 1; i < connections.length; i++) {
+		const c = connections[i];
+		merged = ModuleGraphConnection.addConnectionStates(
+			merged,
+			c.getActiveState(runtime)
+		);
+		if (merged === true) return true;
+	}
+	return merged;
+};
+
 const extractBlockModules = (module, moduleGraph, runtime, blockModulesMap) => {
 	let blockCache;
 	let modules;
@@ -99,9 +118,6 @@ const extractBlockModules = (module, moduleGraph, runtime, blockModulesMap) => {
 		if (!m) continue;
 		// We skip weak connections
 		if (connection.weak) continue;
-		const state = connection.getActiveState(runtime);
-		// We skip inactive connections
-		if (state === false) continue;
 
 		const block = moduleGraph.getParentBlock(d);
 		let index = moduleGraph.getParentBlockIndex(d);
@@ -115,41 +131,47 @@ const extractBlockModules = (module, moduleGraph, runtime, blockModulesMap) => {
 			modules = blockModulesMap.get((blockCache = block));
 		}
 
-		const i = index << 2;
+		const i = index * 3;
 		modules[i] = m;
-		modules[i + 1] = state;
+		modules[i + 1] = connection.getActiveState(runtime);
+		modules[i + 2] = connection;
 	}
 
 	for (const modules of arrays) {
 		if (modules.length === 0) continue;
 		let indexMap;
 		let length = 0;
-		outer: for (let j = 0; j < modules.length; j += 2) {
+		outer: for (let j = 0; j < modules.length; j += 3) {
 			const m = modules[j];
 			if (m === undefined) continue;
 			const state = modules[j + 1];
+			const connection = modules[j + 2];
 			if (indexMap === undefined) {
 				let i = 0;
-				for (; i < length; i += 2) {
+				for (; i < length; i += 3) {
 					if (modules[i] === m) {
 						const merged = modules[i + 1];
+						modules[i + 2].push(connection);
 						if (merged === true) continue outer;
 						modules[i + 1] = ModuleGraphConnection.addConnectionStates(
 							merged,
 							state
 						);
+						continue outer;
 					}
 				}
 				modules[length] = m;
 				length++;
 				modules[length] = state;
 				length++;
+				modules[length] = [connection];
+				length++;
 				if (length > 30) {
 					// To avoid worse case performance, we will use an index map for
 					// linear cost access, which allows to maintain O(n) complexity
 					// while keeping allocations down to a minimum
 					indexMap = new Map();
-					for (let i = 0; i < length; i += 2) {
+					for (let i = 0; i < length; i += 3) {
 						indexMap.set(modules[i], i + 1);
 					}
 				}
@@ -157,6 +179,7 @@ const extractBlockModules = (module, moduleGraph, runtime, blockModulesMap) => {
 				const idx = indexMap.get(m);
 				if (idx !== undefined) {
 					const merged = modules[idx];
+					modules[idx + 1].push(connection);
 					if (merged === true) continue outer;
 					modules[idx] = ModuleGraphConnection.addConnectionStates(
 						merged,
@@ -168,6 +191,8 @@ const extractBlockModules = (module, moduleGraph, runtime, blockModulesMap) => {
 					modules[length] = state;
 					indexMap.set(m, length);
 					length++;
+					modules[length] = [connection];
+					length++;
 				}
 			}
 		}
@@ -207,7 +232,7 @@ const visitModules = (
 	 *
 	 * @param {DependenciesBlock} block block
 	 * @param {RuntimeSpec} runtime runtime
-	 * @returns {(Module | ConnectionState)[]} block modules in flatten tuples
+	 * @returns {(Module | ConnectionState | ModuleGraphConnection[])[]} block modules in flatten tuples
 	 */
 	const getBlockModules = (block, runtime) => {
 		if (blockModulesMapRuntime !== runtime) {
@@ -382,7 +407,7 @@ const visitModules = (
 	/** @type {QueueItem[]} */
 	let queueDelayed = [];
 
-	/** @type {[Module, ConnectionState][]} */
+	/** @type {[Module, ModuleGraphConnection[]][]} */
 	const skipConnectionBuffer = [];
 	/** @type {Module[]} */
 	const skipBuffer = [];
@@ -582,7 +607,7 @@ const visitModules = (
 			const { minAvailableModules } = chunkGroupInfo;
 			// Buffer items because order need to be reversed to get indices correct
 			// Traverse all referenced modules
-			for (let i = 0; i < blockModules.length; i += 2) {
+			for (let i = 0; i < blockModules.length; i += 3) {
 				const refModule = /** @type {Module} */ (blockModules[i]);
 				if (chunkGraph.isModuleInChunk(refModule, chunk)) {
 					// skip early if already connected
@@ -592,7 +617,11 @@ const visitModules = (
 					blockModules[i + 1]
 				);
 				if (activeState !== true) {
-					skipConnectionBuffer.push([refModule, activeState]);
+					const connections = /** @type {ModuleGraphConnection[]} */ (
+						blockModules[i + 2]
+					);
+					skipConnectionBuffer.push([refModule, connections]);
+					// We skip inactive connections
 					if (activeState === false) continue;
 				}
 				if (
@@ -666,7 +695,7 @@ const visitModules = (
 
 		if (blockModules !== undefined) {
 			// Traverse all referenced modules
-			for (let i = 0; i < blockModules.length; i += 2) {
+			for (let i = 0; i < blockModules.length; i += 3) {
 				const refModule = /** @type {Module} */ (blockModules[i]);
 				const activeState = /** @type {ConnectionState} */ (
 					blockModules[i + 1]
@@ -1172,7 +1201,11 @@ const visitModules = (
 					/** @type {ModuleSetPlus} */
 					(info.minAvailableModules);
 				for (const entry of info.skippedModuleConnections) {
-					const [module, activeState] = entry;
+					const [module, connections] = entry;
+					const activeState = getActiveStateOfConnections(
+						connections,
+						info.runtime
+					);
 					if (activeState === false) continue;
 					if (activeState === true) {
 						info.skippedModuleConnections.delete(entry);
@@ -1286,7 +1319,7 @@ const visitModules = (
 				return;
 			}
 
-			for (let i = 0; i < blockModules.length; i += 2) {
+			for (let i = 0; i < blockModules.length; i += 3) {
 				const refModule = /** @type {Module} */ (blockModules[i]);
 				const activeState = /** @type {ConnectionState} */ (
 					blockModules[i + 1]


Test patch:
diff --git a/test/__snapshots__/StatsTestCases.basictest.js.snap b/test/__snapshots__/StatsTestCases.basictest.js.snap
--- a/test/__snapshots__/StatsTestCases.basictest.js.snap
+++ b/test/__snapshots__/StatsTestCases.basictest.js.snap
@@ -3355,8 +3355,8 @@ cacheable modules 807 bytes
   ./first.js 236 bytes [built] [code generated]
   ./second.js 202 bytes [built] [code generated]
   ./vendor.js 25 bytes [built] [code generated]
-  ./common2.js 25 bytes [built] [code generated]
   ./module_first.js 31 bytes [built] [code generated]
+  ./common2.js 25 bytes [built] [code generated]
   ./lazy_first.js 91 bytes [built] [code generated]
   ./lazy_shared.js 56 bytes [built] [code generated]
   ./lazy_second.js 91 bytes [built] [code generated]
@@ -3382,8 +3382,8 @@ cacheable modules 975 bytes
       ModuleConcatenation bailout: Cannot concat with ./common_lazy_shared.js: Module ./common_lazy_shared.js is referenced from different chunks by these modules: ./lazy_shared.js
     ./common_lazy_shared.js 25 bytes [built] [code generated]
   orphan modules 118 bytes [orphan]
-    ./common2.js 25 bytes [orphan] [built]
     ./module_first.js 31 bytes [orphan] [built]
+    ./common2.js 25 bytes [orphan] [built]
     ./common.js 37 bytes [orphan] [built]
       ModuleConcatenation bailout: Module is not in any chunk
     ./common_lazy.js 25 bytes [orphan] [built]
@@ -3457,10 +3457,10 @@ cacheable modules 1.22 KiB
     |   [no exports]
     |   [no exports used]
     |   Statement (ExpressionStatement) with side effects in source code at 4:0-30
-    | ./node_modules/big-module/a.js 58 bytes [built]
-    |   [only some exports used: a]
     | ./node_modules/module-with-export/index.js 1.01 KiB [built]
     |   [only some exports used: smallVar]
+    | ./node_modules/big-module/a.js 58 bytes [built]
+    |   [only some exports used: a]
   ./node_modules/module-with-export/emptyModule.js 43 bytes [built] [code generated]
     [used exports unknown]
     ModuleConcatenation bailout: Module is not an ECMAScript module
diff --git a/test/configCases/chunk-graph/issue-17989/entry-a.js b/test/configCases/chunk-graph/issue-17989/entry-a.js
new file mode 100644
--- /dev/null
+++ b/test/configCases/chunk-graph/issue-17989/entry-a.js
@@ -0,0 +1,10 @@
+import loadModule from "./shared"
+
+it("should not have a.add from entry-a + entry-b", () => {
+	return loadModule().then(module => {
+		const { arg } = module;
+		expect(arg).toBe(42)
+		expect(typeof __webpack_modules__["./util2.js"]).toBe("function")
+		expect(require.cache["./util2.js"]).toBe(undefined); // not loaded on __webpack_require__.c["./util2.js"]
+	});
+});
diff --git a/test/configCases/chunk-graph/issue-17989/entry-b.js b/test/configCases/chunk-graph/issue-17989/entry-b.js
new file mode 100644
--- /dev/null
+++ b/test/configCases/chunk-graph/issue-17989/entry-b.js
@@ -0,0 +1,17 @@
+it("should have util2.js in util chunk", () => {
+  return import("./shared")
+    .then(({ default: loadModule }) => loadModule())
+    .then((module) => {
+      let arg = module.arg;
+      expect(arg).toBe(42)
+      expect(typeof __webpack_modules__["./util2.js"]).toBe("function")
+      expect(typeof require.cache["./util2.js"]).toBe("object"); // loaded on __webpack_require__.c["./util2.js"]
+      return arg
+    })
+    .then(arg => {
+      return import("./util1").then(module => {
+        let res = module.f(arg);
+        expect(res).toBe(84);
+      })
+    })
+});
diff --git a/test/configCases/chunk-graph/issue-17989/shared.js b/test/configCases/chunk-graph/issue-17989/shared.js
new file mode 100644
--- /dev/null
+++ b/test/configCases/chunk-graph/issue-17989/shared.js
@@ -0,0 +1 @@
+export default () => import("./util")
diff --git a/test/configCases/chunk-graph/issue-17989/test.config.js b/test/configCases/chunk-graph/issue-17989/test.config.js
new file mode 100644
--- /dev/null
+++ b/test/configCases/chunk-graph/issue-17989/test.config.js
@@ -0,0 +1,5 @@
+module.exports = {
+	findBundle: function (i, options) {
+		return ["a.js", "b.js"];
+	}
+};
diff --git a/test/configCases/chunk-graph/issue-17989/util.js b/test/configCases/chunk-graph/issue-17989/util.js
new file mode 100644
--- /dev/null
+++ b/test/configCases/chunk-graph/issue-17989/util.js
@@ -0,0 +1 @@
+export { arg } from './util1'
diff --git a/test/configCases/chunk-graph/issue-17989/util1.js b/test/configCases/chunk-graph/issue-17989/util1.js
new file mode 100644
--- /dev/null
+++ b/test/configCases/chunk-graph/issue-17989/util1.js
@@ -0,0 +1,2 @@
+export const arg = 42
+export { f } from "./util2"
diff --git a/test/configCases/chunk-graph/issue-17989/util2.js b/test/configCases/chunk-graph/issue-17989/util2.js
new file mode 100644
--- /dev/null
+++ b/test/configCases/chunk-graph/issue-17989/util2.js
@@ -0,0 +1 @@
+export const f = a => a * 2
diff --git a/test/configCases/chunk-graph/issue-17989/webpack.config.js b/test/configCases/chunk-graph/issue-17989/webpack.config.js
new file mode 100644
--- /dev/null
+++ b/test/configCases/chunk-graph/issue-17989/webpack.config.js
@@ -0,0 +1,17 @@
+/** @type {import("../../../../").Configuration} */
+module.exports = {
+	entry: {
+		a: "./entry-a",
+		b: "./entry-b"
+	},
+	optimization: {
+		sideEffects: true,
+		providedExports: true,
+		usedExports: true,
+		concatenateModules: false,
+		moduleIds: "named"
+	},
+	output: {
+		filename: "[name].js"
+	}
+};