Repository: facebook/react
Pull request: update ReactFlightWebpackPlugin to be compatiable with webpack v5

<!--
  Thanks for submitting a pull request!
  We appreciate you spending the time to work on these changes. Please provide enough information so that others can review your pull request. The three fields below are mandatory.

  Before submitting a pull request, please make sure the following is done:

  1. Fork [the repository](https://github.com/facebook/react) and create your branch from `main`.
  2. Run `yarn` in the repository root.
  3. If you've fixed a bug or added code that should be tested, add tests!
  4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch TestName` is helpful in development.
  5. Run `yarn test --prod` to test in the production environment. It supports the same options as `yarn test`.
  6. If you need a debugger, run `yarn debug-test --watch TestName`, open `chrome://inspect`, and press "Inspect".
  7. Format your code with [prettier](https://github.com/prettier/prettier) (`yarn prettier`).
  8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only check changed files.
  9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).
  10. If you haven't already, complete the CLA.

  Learn more about contributing: https://reactjs.org/docs/how-to-contribute.html
-->

## Summary

<!--
 Explain the **motivation** for making this change. What existing problem does the pull request solve?
-->

Current React Server Component uses webpack 4 when webpack 5 had been out for more than 1 year.
This PR updated the current plugin `packages/react-server-dom-webpack/src/ReactFlightWebpackPlugin.js` to be compatible with the latest webpack 5

## How did you test this change?

<!--
  Demonstrate the code is solid. Example: The exact commands you ran and their output, screenshots / videos if the pull request changes the user interface.
  How exactly did you verify that your PR solves the issue you wanted to solve?
  If you leave this empty, your PR will very likely be closed.
-->

The original development was done in https://github.com/reactjs/server-components-demo/pull/52 and written in typescript. 

I tested it by using the plugin in `server-components-demo` project
- run `npm run start`
- manually tested the application for everything to be the same.
- compare `react-client-manifest.json` build output in webpack 4 & webpack 5 (the content are exactly the same with the exception of vendor files are named differently in webpack 5)
- compare build assets output to be the same


Changed files:
filename: packages/react-server-dom-webpack/package.json
patch: @@ -50,7 +50,7 @@
   "peerDependencies": {
     "react": "^17.0.0",
     "react-dom": "^17.0.0",
-    "webpack": "^4.43.0"
+    "webpack": "^5.59.0"
   },
   "dependencies": {
     "acorn": "^6.2.1",

filename: packages/react-server-dom-webpack/src/ReactFlightWebpackPlugin.js
patch: @@ -14,8 +14,13 @@ import asyncLib from 'neo-async';
 
 import ModuleDependency from 'webpack/lib/dependencies/ModuleDependency';
 import NullDependency from 'webpack/lib/dependencies/NullDependency';
-import AsyncDependenciesBlock from 'webpack/lib/AsyncDependenciesBlock';
 import Template from 'webpack/lib/Template';
+import {
+  sources,
+  WebpackError,
+  Compilation,
+  AsyncDependenciesBlock,
+} from 'webpack';
 
 import isArray from 'shared/isArray';
 
@@ -34,6 +39,7 @@ class ClientReferenceDependency extends ModuleDependency {
 // We use the Flight client implementation because you can't get to these
 // without the client runtime so it's the first time in the loading sequence
 // you might want them.
+const clientImportName = 'react-server-dom-webpack';
 const clientFileName = require.resolve('../');
 
 type ClientReferenceSearchPath = {
@@ -97,33 +103,35 @@ export default class ReactFlightWebpackPlugin {
   }
 
   apply(compiler: any) {
+    const _this = this;
     let resolvedClientReferences;
-    const run = (params, callback) => {
-      // First we need to find all client files on the file system. We do this early so
-      // that we have them synchronously available later when we need them. This might
-      // not be needed anymore since we no longer need to compile the module itself in
-      // a special way. So it's probably better to do this lazily and in parallel with
-      // other compilation.
-      const contextResolver = compiler.resolverFactory.get('context', {});
-      this.resolveAllClientFiles(
-        compiler.context,
-        contextResolver,
-        compiler.inputFileSystem,
-        compiler.createContextModuleFactory(),
-        (err, resolvedClientRefs) => {
-          if (err) {
-            callback(err);
-            return;
-          }
-          resolvedClientReferences = resolvedClientRefs;
-          callback();
-        },
-      );
-    };
+    let clientFileNameFound = false;
+
+    // Find all client files on the file system
+    compiler.hooks.beforeCompile.tapAsync(
+      PLUGIN_NAME,
+      ({contextModuleFactory}, callback) => {
+        const contextResolver = compiler.resolverFactory.get('context', {});
+
+        _this.resolveAllClientFiles(
+          compiler.context,
+          contextResolver,
+          compiler.inputFileSystem,
+          contextModuleFactory,
+          function(err, resolvedClientRefs) {
+            if (err) {
+              callback(err);
+              return;
+            }
+
+            resolvedClientReferences = resolvedClientRefs;
+            callback();
+          },
+        );
+      },
+    );
 
-    compiler.hooks.run.tapAsync(PLUGIN_NAME, run);
-    compiler.hooks.watchRun.tapAsync(PLUGIN_NAME, run);
-    compiler.hooks.compilation.tap(
+    compiler.hooks.thisCompilation.tap(
       PLUGIN_NAME,
       (compilation, {normalModuleFactory}) => {
         compilation.dependencyFactories.set(
@@ -135,86 +143,140 @@ export default class ReactFlightWebpackPlugin {
           new NullDependency.Template(),
         );
 
-        compilation.hooks.buildModule.tap(PLUGIN_NAME, module => {
+        const handler = parser => {
           // We need to add all client references as dependency of something in the graph so
           // Webpack knows which entries need to know about the relevant chunks and include the
           // map in their runtime. The things that actually resolves the dependency is the Flight
           // client runtime. So we add them as a dependency of the Flight client runtime.
           // Anything that imports the runtime will be made aware of these chunks.
-          // TODO: Warn if we don't find this file anywhere in the compilation.
-          if (module.resource !== clientFileName) {
-            return;
-          }
-          if (resolvedClientReferences) {
-            for (let i = 0; i < resolvedClientReferences.length; i++) {
-              const dep = resolvedClientReferences[i];
-              const chunkName = this.chunkName
-                .replace(/\[index\]/g, '' + i)
-                .replace(/\[request\]/g, Template.toPath(dep.userRequest));
-
-              const block = new AsyncDependenciesBlock(
-                {
-                  name: chunkName,
-                },
-                module,
-                null,
-                dep.require,
-              );
-              block.addDependency(dep);
-              module.addBlock(block);
+          parser.hooks.program.tap(PLUGIN_NAME, () => {
+            const module = parser.state.module;
+
+            if (module.resource !== clientFileName) {
+              return;
             }
-          }
-        });
+
+            clientFileNameFound = true;
+
+            if (resolvedClientReferences) {
+              for (let i = 0; i < resolvedClientReferences.length; i++) {
+                const dep = resolvedClientReferences[i];
+
+                const chunkName = _this.chunkName
+                  .replace(/\[index\]/g, '' + i)
+                  .replace(/\[request\]/g, Template.toPath(dep.userRequest));
+
+                const block = new AsyncDependenciesBlock(
+                  {
+                    name: chunkName,
+                  },
+                  null,
+                  dep.request,
+                );
+
+                block.addDependency(dep);
+                module.addBlock(block);
+              }
+            }
+          });
+        };
+
+        normalModuleFactory.hooks.parser
+          .for('javascript/auto')
+          .tap('HarmonyModulesPlugin', handler);
+
+        normalModuleFactory.hooks.parser
+          .for('javascript/esm')
+          .tap('HarmonyModulesPlugin', handler);
+
+        normalModuleFactory.hooks.parser
+          .for('javascript/dynamic')
+          .tap('HarmonyModulesPlugin', handler);
       },
     );
 
-    compiler.hooks.emit.tap(PLUGIN_NAME, compilation => {
-      const json = {};
-      compilation.chunkGroups.forEach(chunkGroup => {
-        const chunkIds = chunkGroup.chunks.map(c => c.id);
-
-        function recordModule(id, mod) {
-          // TODO: Hook into deps instead of the target module.
-          // That way we know by the type of dep whether to include.
-          // It also resolves conflicts when the same module is in multiple chunks.
-          if (!/\.client\.js$/.test(mod.resource)) {
+    compiler.hooks.make.tap(PLUGIN_NAME, compilation => {
+      compilation.hooks.processAssets.tap(
+        {
+          name: PLUGIN_NAME,
+          stage: Compilation.PROCESS_ASSETS_STAGE_REPORT,
+        },
+        function() {
+          if (clientFileNameFound === false) {
+            compilation.warnings.push(
+              new WebpackError(
+                `Client runtime at ${clientImportName} was not found. React Server Components module map file ${_this.manifestFilename} was not created.`,
+              ),
+            );
             return;
           }
-          const moduleExports = {};
-          ['', '*'].concat(mod.buildMeta.providedExports).forEach(name => {
-            moduleExports[name] = {
-              id: id,
-              chunks: chunkIds,
-              name: name,
-            };
-          });
-          const href = pathToFileURL(mod.resource).href;
-          if (href !== undefined) {
-            json[href] = moduleExports;
-          }
-        }
 
-        chunkGroup.chunks.forEach(chunk => {
-          chunk.getModules().forEach(mod => {
-            recordModule(mod.id, mod);
-            // If this is a concatenation, register each child to the parent ID.
-            if (mod.modules) {
-              mod.modules.forEach(concatenatedMod => {
-                recordModule(mod.id, concatenatedMod);
-              });
+          const json = {};
+          compilation.chunkGroups.forEach(function(chunkGroup) {
+            const chunkIds = chunkGroup.chunks.map(function(c) {
+              return c.id;
+            });
+
+            function recordModule(id, module) {
+              // TODO: Hook into deps instead of the target module.
+              // That way we know by the type of dep whether to include.
+              // It also resolves conflicts when the same module is in multiple chunks.
+
+              if (!/\.client\.(js|ts)x?$/.test(module.resource)) {
+                return;
+              }
+
+              const moduleProvidedExports = compilation.moduleGraph
+                .getExportsInfo(module)
+                .getProvidedExports();
+
+              const moduleExports = {};
+              ['', '*']
+                .concat(
+                  Array.isArray(moduleProvidedExports)
+                    ? moduleProvidedExports
+                    : [],
+                )
+                .forEach(function(name) {
+                  moduleExports[name] = {
+                    id,
+                    chunks: chunkIds,
+                    name: name,
+                  };
+                });
+              const href = pathToFileURL(module.resource).href;
+
+              if (href !== undefined) {
+                json[href] = moduleExports;
+              }
             }
+
+            chunkGroup.chunks.forEach(function(chunk) {
+              const chunkModules = compilation.chunkGraph.getChunkModulesIterable(
+                chunk,
+              );
+
+              Array.from(chunkModules).forEach(function(module) {
+                const moduleId = compilation.chunkGraph.getModuleId(module);
+
+                recordModule(moduleId, module);
+                // If this is a concatenation, register each child to the parent ID.
+                if (module.modules) {
+                  module.modules.forEach(concatenatedMod => {
+                    recordModule(moduleId, concatenatedMod);
+                  });
+                }
+              });
+            });
           });
-        });
-      });
-      const output = JSON.stringify(json, null, 2);
-      compilation.assets[this.manifestFilename] = {
-        source() {
-          return output;
-        },
-        size() {
-          return output.length;
+
+          const output = JSON.stringify(json, null, 2);
+          compilation.emitAsset(
+            _this.manifestFilename,
+            new sources.RawSource(output, false),
+          );
         },
-      };
+      );
     });
   }
 
@@ -268,7 +330,8 @@ export default class ReactFlightWebpackPlugin {
               (err2: null | Error, deps: Array<ModuleDependency>) => {
                 if (err2) return cb(err2);
                 const clientRefDeps = deps.map(dep => {
-                  const request = join(resolvedDirectory, dep.request);
+                  // use userRequest instead of request. request always end with undefined which is wrong
+                  const request = join(resolvedDirectory, dep.userRequest);
                   const clientRefDep = new ClientReferenceDependency(request);
                   clientRefDep.userRequest = dep.userRequest;
                   return clientRefDep;