1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @flow
    
  8.  */
    
  9. 
    
  10. import type {ImportManifestEntry} from './shared/ReactFlightImportMetadata';
    
  11. 
    
  12. import {join} from 'path';
    
  13. import {pathToFileURL} from 'url';
    
  14. import asyncLib from 'neo-async';
    
  15. import * as acorn from 'acorn-loose';
    
  16. 
    
  17. import ModuleDependency from 'webpack/lib/dependencies/ModuleDependency';
    
  18. import NullDependency from 'webpack/lib/dependencies/NullDependency';
    
  19. import Template from 'webpack/lib/Template';
    
  20. import {
    
  21.   sources,
    
  22.   WebpackError,
    
  23.   Compilation,
    
  24.   AsyncDependenciesBlock,
    
  25. } from 'webpack';
    
  26. 
    
  27. import isArray from 'shared/isArray';
    
  28. 
    
  29. class ClientReferenceDependency extends ModuleDependency {
    
  30.   constructor(request: mixed) {
    
  31.     super(request);
    
  32.   }
    
  33. 
    
  34.   get type(): string {
    
  35.     return 'client-reference';
    
  36.   }
    
  37. }
    
  38. 
    
  39. // This is the module that will be used to anchor all client references to.
    
  40. // I.e. it will have all the client files as async deps from this point on.
    
  41. // We use the Flight client implementation because you can't get to these
    
  42. // without the client runtime so it's the first time in the loading sequence
    
  43. // you might want them.
    
  44. const clientImportName = 'react-server-dom-webpack/client';
    
  45. const clientFileName = require.resolve('../client.browser.js');
    
  46. 
    
  47. type ClientReferenceSearchPath = {
    
  48.   directory: string,
    
  49.   recursive?: boolean,
    
  50.   include: RegExp,
    
  51.   exclude?: RegExp,
    
  52. };
    
  53. 
    
  54. type ClientReferencePath = string | ClientReferenceSearchPath;
    
  55. 
    
  56. type Options = {
    
  57.   isServer: boolean,
    
  58.   clientReferences?: ClientReferencePath | $ReadOnlyArray<ClientReferencePath>,
    
  59.   chunkName?: string,
    
  60.   clientManifestFilename?: string,
    
  61.   ssrManifestFilename?: string,
    
  62. };
    
  63. 
    
  64. const PLUGIN_NAME = 'React Server Plugin';
    
  65. 
    
  66. export default class ReactFlightWebpackPlugin {
    
  67.   clientReferences: $ReadOnlyArray<ClientReferencePath>;
    
  68.   chunkName: string;
    
  69.   clientManifestFilename: string;
    
  70.   ssrManifestFilename: string;
    
  71. 
    
  72.   constructor(options: Options) {
    
  73.     if (!options || typeof options.isServer !== 'boolean') {
    
  74.       throw new Error(
    
  75.         PLUGIN_NAME + ': You must specify the isServer option as a boolean.',
    
  76.       );
    
  77.     }
    
  78.     if (options.isServer) {
    
  79.       throw new Error('TODO: Implement the server compiler.');
    
  80.     }
    
  81.     if (!options.clientReferences) {
    
  82.       this.clientReferences = [
    
  83.         {
    
  84.           directory: '.',
    
  85.           recursive: true,
    
  86.           include: /\.(js|ts|jsx|tsx)$/,
    
  87.         },
    
  88.       ];
    
  89.     } else if (
    
  90.       typeof options.clientReferences === 'string' ||
    
  91.       !isArray(options.clientReferences)
    
  92.     ) {
    
  93.       this.clientReferences = [(options.clientReferences: $FlowFixMe)];
    
  94.     } else {
    
  95.       // $FlowFixMe[incompatible-type] found when upgrading Flow
    
  96.       this.clientReferences = options.clientReferences;
    
  97.     }
    
  98.     if (typeof options.chunkName === 'string') {
    
  99.       this.chunkName = options.chunkName;
    
  100.       if (!/\[(index|request)\]/.test(this.chunkName)) {
    
  101.         this.chunkName += '[index]';
    
  102.       }
    
  103.     } else {
    
  104.       this.chunkName = 'client[index]';
    
  105.     }
    
  106.     this.clientManifestFilename =
    
  107.       options.clientManifestFilename || 'react-client-manifest.json';
    
  108.     this.ssrManifestFilename =
    
  109.       options.ssrManifestFilename || 'react-ssr-manifest.json';
    
  110.   }
    
  111. 
    
  112.   apply(compiler: any) {
    
  113.     const _this = this;
    
  114.     let resolvedClientReferences;
    
  115.     let clientFileNameFound = false;
    
  116. 
    
  117.     // Find all client files on the file system
    
  118.     compiler.hooks.beforeCompile.tapAsync(
    
  119.       PLUGIN_NAME,
    
  120.       ({contextModuleFactory}, callback) => {
    
  121.         const contextResolver = compiler.resolverFactory.get('context', {});
    
  122.         const normalResolver = compiler.resolverFactory.get('normal');
    
  123. 
    
  124.         _this.resolveAllClientFiles(
    
  125.           compiler.context,
    
  126.           contextResolver,
    
  127.           normalResolver,
    
  128.           compiler.inputFileSystem,
    
  129.           contextModuleFactory,
    
  130.           function (err, resolvedClientRefs) {
    
  131.             if (err) {
    
  132.               callback(err);
    
  133.               return;
    
  134.             }
    
  135. 
    
  136.             resolvedClientReferences = resolvedClientRefs;
    
  137.             callback();
    
  138.           },
    
  139.         );
    
  140.       },
    
  141.     );
    
  142. 
    
  143.     compiler.hooks.thisCompilation.tap(
    
  144.       PLUGIN_NAME,
    
  145.       (compilation, {normalModuleFactory}) => {
    
  146.         compilation.dependencyFactories.set(
    
  147.           ClientReferenceDependency,
    
  148.           normalModuleFactory,
    
  149.         );
    
  150.         compilation.dependencyTemplates.set(
    
  151.           ClientReferenceDependency,
    
  152.           new NullDependency.Template(),
    
  153.         );
    
  154. 
    
  155.         // $FlowFixMe[missing-local-annot]
    
  156.         const handler = parser => {
    
  157.           // We need to add all client references as dependency of something in the graph so
    
  158.           // Webpack knows which entries need to know about the relevant chunks and include the
    
  159.           // map in their runtime. The things that actually resolves the dependency is the Flight
    
  160.           // client runtime. So we add them as a dependency of the Flight client runtime.
    
  161.           // Anything that imports the runtime will be made aware of these chunks.
    
  162.           parser.hooks.program.tap(PLUGIN_NAME, () => {
    
  163.             const module = parser.state.module;
    
  164. 
    
  165.             if (module.resource !== clientFileName) {
    
  166.               return;
    
  167.             }
    
  168. 
    
  169.             clientFileNameFound = true;
    
  170. 
    
  171.             if (resolvedClientReferences) {
    
  172.               // $FlowFixMe[incompatible-use] found when upgrading Flow
    
  173.               for (let i = 0; i < resolvedClientReferences.length; i++) {
    
  174.                 // $FlowFixMe[incompatible-use] found when upgrading Flow
    
  175.                 const dep = resolvedClientReferences[i];
    
  176. 
    
  177.                 const chunkName = _this.chunkName
    
  178.                   .replace(/\[index\]/g, '' + i)
    
  179.                   .replace(/\[request\]/g, Template.toPath(dep.userRequest));
    
  180. 
    
  181.                 const block = new AsyncDependenciesBlock(
    
  182.                   {
    
  183.                     name: chunkName,
    
  184.                   },
    
  185.                   null,
    
  186.                   dep.request,
    
  187.                 );
    
  188. 
    
  189.                 block.addDependency(dep);
    
  190.                 module.addBlock(block);
    
  191.               }
    
  192.             }
    
  193.           });
    
  194.         };
    
  195. 
    
  196.         normalModuleFactory.hooks.parser
    
  197.           .for('javascript/auto')
    
  198.           .tap('HarmonyModulesPlugin', handler);
    
  199. 
    
  200.         normalModuleFactory.hooks.parser
    
  201.           .for('javascript/esm')
    
  202.           .tap('HarmonyModulesPlugin', handler);
    
  203. 
    
  204.         normalModuleFactory.hooks.parser
    
  205.           .for('javascript/dynamic')
    
  206.           .tap('HarmonyModulesPlugin', handler);
    
  207.       },
    
  208.     );
    
  209. 
    
  210.     compiler.hooks.make.tap(PLUGIN_NAME, compilation => {
    
  211.       compilation.hooks.processAssets.tap(
    
  212.         {
    
  213.           name: PLUGIN_NAME,
    
  214.           stage: Compilation.PROCESS_ASSETS_STAGE_REPORT,
    
  215.         },
    
  216.         function () {
    
  217.           if (clientFileNameFound === false) {
    
  218.             compilation.warnings.push(
    
  219.               new WebpackError(
    
  220.                 `Client runtime at ${clientImportName} was not found. React Server Components module map file ${_this.clientManifestFilename} was not created.`,
    
  221.               ),
    
  222.             );
    
  223.             return;
    
  224.           }
    
  225. 
    
  226.           const configuredCrossOriginLoading =
    
  227.             compilation.outputOptions.crossOriginLoading;
    
  228.           const crossOriginMode =
    
  229.             typeof configuredCrossOriginLoading === 'string'
    
  230.               ? configuredCrossOriginLoading === 'use-credentials'
    
  231.                 ? configuredCrossOriginLoading
    
  232.                 : 'anonymous'
    
  233.               : null;
    
  234. 
    
  235.           const resolvedClientFiles = new Set(
    
  236.             (resolvedClientReferences || []).map(ref => ref.request),
    
  237.           );
    
  238. 
    
  239.           const clientManifest: {
    
  240.             [string]: ImportManifestEntry,
    
  241.           } = {};
    
  242.           type SSRModuleMap = {
    
  243.             [string]: {
    
  244.               [string]: {specifier: string, name: string},
    
  245.             },
    
  246.           };
    
  247.           const moduleMap: SSRModuleMap = {};
    
  248.           const ssrBundleConfig: {
    
  249.             moduleLoading: {
    
  250.               prefix: string,
    
  251.               crossOrigin: string | null,
    
  252.             },
    
  253.             moduleMap: SSRModuleMap,
    
  254.           } = {
    
  255.             moduleLoading: {
    
  256.               prefix: compilation.outputOptions.publicPath || '',
    
  257.               crossOrigin: crossOriginMode,
    
  258.             },
    
  259.             moduleMap,
    
  260.           };
    
  261. 
    
  262.           // We figure out which files are always loaded by any initial chunk (entrypoint).
    
  263.           // We use this to filter out chunks that Flight will never need to load
    
  264.           const emptySet: Set<string> = new Set();
    
  265.           const runtimeChunkFiles: Set<string> = emptySet;
    
  266.           compilation.entrypoints.forEach(entrypoint => {
    
  267.             const runtimeChunk = entrypoint.getRuntimeChunk();
    
  268.             if (runtimeChunk) {
    
  269.               runtimeChunk.files.forEach(runtimeFile => {
    
  270.                 runtimeChunkFiles.add(runtimeFile);
    
  271.               });
    
  272.             }
    
  273.           });
    
  274. 
    
  275.           compilation.chunkGroups.forEach(function (chunkGroup) {
    
  276.             const chunks: Array<string> = [];
    
  277.             chunkGroup.chunks.forEach(function (c) {
    
  278.               // eslint-disable-next-line no-for-of-loops/no-for-of-loops
    
  279.               for (const file of c.files) {
    
  280.                 if (!file.endsWith('.js')) return;
    
  281.                 if (file.endsWith('.hot-update.js')) return;
    
  282.                 chunks.push(c.id, file);
    
  283.                 break;
    
  284.               }
    
  285.             });
    
  286. 
    
  287.             // $FlowFixMe[missing-local-annot]
    
  288.             function recordModule(id: $FlowFixMe, module) {
    
  289.               // TODO: Hook into deps instead of the target module.
    
  290.               // That way we know by the type of dep whether to include.
    
  291.               // It also resolves conflicts when the same module is in multiple chunks.
    
  292.               if (!resolvedClientFiles.has(module.resource)) {
    
  293.                 return;
    
  294.               }
    
  295. 
    
  296.               const href = pathToFileURL(module.resource).href;
    
  297. 
    
  298.               if (href !== undefined) {
    
  299.                 const ssrExports: {
    
  300.                   [string]: {specifier: string, name: string},
    
  301.                 } = {};
    
  302. 
    
  303.                 clientManifest[href] = {
    
  304.                   id,
    
  305.                   chunks,
    
  306.                   name: '*',
    
  307.                 };
    
  308.                 ssrExports['*'] = {
    
  309.                   specifier: href,
    
  310.                   name: '*',
    
  311.                 };
    
  312. 
    
  313.                 // TODO: If this module ends up split into multiple modules, then
    
  314.                 // we should encode each the chunks needed for the specific export.
    
  315.                 // When the module isn't split, it doesn't matter and we can just
    
  316.                 // encode the id of the whole module. This code doesn't currently
    
  317.                 // deal with module splitting so is likely broken from ESM anyway.
    
  318.                 /*
    
  319.                 clientManifest[href + '#'] = {
    
  320.                   id,
    
  321.                   chunks,
    
  322.                   name: '',
    
  323.                 };
    
  324.                 ssrExports[''] = {
    
  325.                   specifier: href,
    
  326.                   name: '',
    
  327.                 };
    
  328. 
    
  329.                 const moduleProvidedExports = compilation.moduleGraph
    
  330.                   .getExportsInfo(module)
    
  331.                   .getProvidedExports();
    
  332. 
    
  333.                 if (Array.isArray(moduleProvidedExports)) {
    
  334.                   moduleProvidedExports.forEach(function (name) {
    
  335.                     clientManifest[href + '#' + name] = {
    
  336.                       id,
    
  337.                       chunks,
    
  338.                       name: name,
    
  339.                     };
    
  340.                     ssrExports[name] = {
    
  341.                       specifier: href,
    
  342.                       name: name,
    
  343.                     };
    
  344.                   });
    
  345.                 }
    
  346.                 */
    
  347. 
    
  348.                 moduleMap[id] = ssrExports;
    
  349.               }
    
  350.             }
    
  351. 
    
  352.             chunkGroup.chunks.forEach(function (chunk) {
    
  353.               const chunkModules =
    
  354.                 compilation.chunkGraph.getChunkModulesIterable(chunk);
    
  355. 
    
  356.               Array.from(chunkModules).forEach(function (module) {
    
  357.                 const moduleId = compilation.chunkGraph.getModuleId(module);
    
  358. 
    
  359.                 recordModule(moduleId, module);
    
  360.                 // If this is a concatenation, register each child to the parent ID.
    
  361.                 if (module.modules) {
    
  362.                   module.modules.forEach(concatenatedMod => {
    
  363.                     recordModule(moduleId, concatenatedMod);
    
  364.                   });
    
  365.                 }
    
  366.               });
    
  367.             });
    
  368.           });
    
  369. 
    
  370.           const clientOutput = JSON.stringify(clientManifest, null, 2);
    
  371.           compilation.emitAsset(
    
  372.             _this.clientManifestFilename,
    
  373.             new sources.RawSource(clientOutput, false),
    
  374.           );
    
  375.           const ssrOutput = JSON.stringify(ssrBundleConfig, null, 2);
    
  376.           compilation.emitAsset(
    
  377.             _this.ssrManifestFilename,
    
  378.             new sources.RawSource(ssrOutput, false),
    
  379.           );
    
  380.         },
    
  381.       );
    
  382.     });
    
  383.   }
    
  384. 
    
  385.   // This attempts to replicate the dynamic file path resolution used for other wildcard
    
  386.   // resolution in Webpack is using.
    
  387.   resolveAllClientFiles(
    
  388.     context: string,
    
  389.     contextResolver: any,
    
  390.     normalResolver: any,
    
  391.     fs: any,
    
  392.     contextModuleFactory: any,
    
  393.     callback: (
    
  394.       err: null | Error,
    
  395.       result?: $ReadOnlyArray<ClientReferenceDependency>,
    
  396.     ) => void,
    
  397.   ) {
    
  398.     function hasUseClientDirective(source: string): boolean {
    
  399.       if (source.indexOf('use client') === -1) {
    
  400.         return false;
    
  401.       }
    
  402.       let body;
    
  403.       try {
    
  404.         body = acorn.parse(source, {
    
  405.           ecmaVersion: '2024',
    
  406.           sourceType: 'module',
    
  407.         }).body;
    
  408.       } catch (x) {
    
  409.         return false;
    
  410.       }
    
  411.       for (let i = 0; i < body.length; i++) {
    
  412.         const node = body[i];
    
  413.         if (node.type !== 'ExpressionStatement' || !node.directive) {
    
  414.           break;
    
  415.         }
    
  416.         if (node.directive === 'use client') {
    
  417.           return true;
    
  418.         }
    
  419.       }
    
  420.       return false;
    
  421.     }
    
  422. 
    
  423.     asyncLib.map(
    
  424.       this.clientReferences,
    
  425.       (
    
  426.         clientReferencePath: string | ClientReferenceSearchPath,
    
  427.         cb: (
    
  428.           err: null | Error,
    
  429.           result?: $ReadOnlyArray<ClientReferenceDependency>,
    
  430.         ) => void,
    
  431.       ): void => {
    
  432.         if (typeof clientReferencePath === 'string') {
    
  433.           cb(null, [new ClientReferenceDependency(clientReferencePath)]);
    
  434.           return;
    
  435.         }
    
  436.         const clientReferenceSearch: ClientReferenceSearchPath =
    
  437.           clientReferencePath;
    
  438.         contextResolver.resolve(
    
  439.           {},
    
  440.           context,
    
  441.           clientReferencePath.directory,
    
  442.           {},
    
  443.           (err, resolvedDirectory) => {
    
  444.             if (err) return cb(err);
    
  445.             const options = {
    
  446.               resource: resolvedDirectory,
    
  447.               resourceQuery: '',
    
  448.               recursive:
    
  449.                 clientReferenceSearch.recursive === undefined
    
  450.                   ? true
    
  451.                   : clientReferenceSearch.recursive,
    
  452.               regExp: clientReferenceSearch.include,
    
  453.               include: undefined,
    
  454.               exclude: clientReferenceSearch.exclude,
    
  455.             };
    
  456.             contextModuleFactory.resolveDependencies(
    
  457.               fs,
    
  458.               options,
    
  459.               (err2: null | Error, deps: Array<any /*ModuleDependency*/>) => {
    
  460.                 if (err2) return cb(err2);
    
  461. 
    
  462.                 const clientRefDeps = deps.map(dep => {
    
  463.                   // use userRequest instead of request. request always end with undefined which is wrong
    
  464.                   const request = join(resolvedDirectory, dep.userRequest);
    
  465.                   const clientRefDep = new ClientReferenceDependency(request);
    
  466.                   clientRefDep.userRequest = dep.userRequest;
    
  467.                   return clientRefDep;
    
  468.                 });
    
  469. 
    
  470.                 asyncLib.filter(
    
  471.                   clientRefDeps,
    
  472.                   (
    
  473.                     clientRefDep: ClientReferenceDependency,
    
  474.                     filterCb: (err: null | Error, truthValue: boolean) => void,
    
  475.                   ) => {
    
  476.                     normalResolver.resolve(
    
  477.                       {},
    
  478.                       context,
    
  479.                       clientRefDep.request,
    
  480.                       {},
    
  481.                       (err3: null | Error, resolvedPath: mixed) => {
    
  482.                         if (err3 || typeof resolvedPath !== 'string') {
    
  483.                           return filterCb(null, false);
    
  484.                         }
    
  485.                         fs.readFile(
    
  486.                           resolvedPath,
    
  487.                           'utf-8',
    
  488.                           (err4: null | Error, content: string) => {
    
  489.                             if (err4 || typeof content !== 'string') {
    
  490.                               return filterCb(null, false);
    
  491.                             }
    
  492.                             const useClient = hasUseClientDirective(content);
    
  493.                             filterCb(null, useClient);
    
  494.                           },
    
  495.                         );
    
  496.                       },
    
  497.                     );
    
  498.                   },
    
  499.                   cb,
    
  500.                 );
    
  501.               },
    
  502.             );
    
  503.           },
    
  504.         );
    
  505.       },
    
  506.       (
    
  507.         err: null | Error,
    
  508.         result: $ReadOnlyArray<$ReadOnlyArray<ClientReferenceDependency>>,
    
  509.       ): void => {
    
  510.         if (err) return callback(err);
    
  511.         const flat: Array<any> = [];
    
  512.         for (let i = 0; i < result.length; i++) {
    
  513.           // $FlowFixMe[method-unbinding]
    
  514.           flat.push.apply(flat, result[i]);
    
  515.         }
    
  516.         callback(null, flat);
    
  517.       },
    
  518.     );
    
  519.   }
    
  520. }