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 * as acorn from 'acorn-loose';
    
  11. 
    
  12. type ResolveContext = {
    
  13.   conditions: Array<string>,
    
  14.   parentURL: string | void,
    
  15. };
    
  16. 
    
  17. type ResolveFunction = (
    
  18.   string,
    
  19.   ResolveContext,
    
  20.   ResolveFunction,
    
  21. ) => {url: string} | Promise<{url: string}>;
    
  22. 
    
  23. type GetSourceContext = {
    
  24.   format: string,
    
  25. };
    
  26. 
    
  27. type GetSourceFunction = (
    
  28.   string,
    
  29.   GetSourceContext,
    
  30.   GetSourceFunction,
    
  31. ) => Promise<{source: Source}>;
    
  32. 
    
  33. type TransformSourceContext = {
    
  34.   format: string,
    
  35.   url: string,
    
  36. };
    
  37. 
    
  38. type TransformSourceFunction = (
    
  39.   Source,
    
  40.   TransformSourceContext,
    
  41.   TransformSourceFunction,
    
  42. ) => Promise<{source: Source}>;
    
  43. 
    
  44. type LoadContext = {
    
  45.   conditions: Array<string>,
    
  46.   format: string | null | void,
    
  47.   importAssertions: Object,
    
  48. };
    
  49. 
    
  50. type LoadFunction = (
    
  51.   string,
    
  52.   LoadContext,
    
  53.   LoadFunction,
    
  54. ) => Promise<{format: string, shortCircuit?: boolean, source: Source}>;
    
  55. 
    
  56. type Source = string | ArrayBuffer | Uint8Array;
    
  57. 
    
  58. let warnedAboutConditionsFlag = false;
    
  59. 
    
  60. let stashedGetSource: null | GetSourceFunction = null;
    
  61. let stashedResolve: null | ResolveFunction = null;
    
  62. 
    
  63. export async function resolve(
    
  64.   specifier: string,
    
  65.   context: ResolveContext,
    
  66.   defaultResolve: ResolveFunction,
    
  67. ): Promise<{url: string}> {
    
  68.   // We stash this in case we end up needing to resolve export * statements later.
    
  69.   stashedResolve = defaultResolve;
    
  70. 
    
  71.   if (!context.conditions.includes('react-server')) {
    
  72.     context = {
    
  73.       ...context,
    
  74.       conditions: [...context.conditions, 'react-server'],
    
  75.     };
    
  76.     if (!warnedAboutConditionsFlag) {
    
  77.       warnedAboutConditionsFlag = true;
    
  78.       // eslint-disable-next-line react-internal/no-production-logging
    
  79.       console.warn(
    
  80.         'You did not run Node.js with the `--conditions react-server` flag. ' +
    
  81.           'Any "react-server" override will only work with ESM imports.',
    
  82.       );
    
  83.     }
    
  84.   }
    
  85.   return await defaultResolve(specifier, context, defaultResolve);
    
  86. }
    
  87. 
    
  88. export async function getSource(
    
  89.   url: string,
    
  90.   context: GetSourceContext,
    
  91.   defaultGetSource: GetSourceFunction,
    
  92. ): Promise<{source: Source}> {
    
  93.   // We stash this in case we end up needing to resolve export * statements later.
    
  94.   stashedGetSource = defaultGetSource;
    
  95.   return defaultGetSource(url, context, defaultGetSource);
    
  96. }
    
  97. 
    
  98. function addLocalExportedNames(names: Map<string, string>, node: any) {
    
  99.   switch (node.type) {
    
  100.     case 'Identifier':
    
  101.       names.set(node.name, node.name);
    
  102.       return;
    
  103.     case 'ObjectPattern':
    
  104.       for (let i = 0; i < node.properties.length; i++)
    
  105.         addLocalExportedNames(names, node.properties[i]);
    
  106.       return;
    
  107.     case 'ArrayPattern':
    
  108.       for (let i = 0; i < node.elements.length; i++) {
    
  109.         const element = node.elements[i];
    
  110.         if (element) addLocalExportedNames(names, element);
    
  111.       }
    
  112.       return;
    
  113.     case 'Property':
    
  114.       addLocalExportedNames(names, node.value);
    
  115.       return;
    
  116.     case 'AssignmentPattern':
    
  117.       addLocalExportedNames(names, node.left);
    
  118.       return;
    
  119.     case 'RestElement':
    
  120.       addLocalExportedNames(names, node.argument);
    
  121.       return;
    
  122.     case 'ParenthesizedExpression':
    
  123.       addLocalExportedNames(names, node.expression);
    
  124.       return;
    
  125.   }
    
  126. }
    
  127. 
    
  128. function transformServerModule(
    
  129.   source: string,
    
  130.   body: any,
    
  131.   url: string,
    
  132.   loader: LoadFunction,
    
  133. ): string {
    
  134.   // If the same local name is exported more than once, we only need one of the names.
    
  135.   const localNames: Map<string, string> = new Map();
    
  136.   const localTypes: Map<string, string> = new Map();
    
  137. 
    
  138.   for (let i = 0; i < body.length; i++) {
    
  139.     const node = body[i];
    
  140.     switch (node.type) {
    
  141.       case 'ExportAllDeclaration':
    
  142.         // If export * is used, the other file needs to explicitly opt into "use server" too.
    
  143.         break;
    
  144.       case 'ExportDefaultDeclaration':
    
  145.         if (node.declaration.type === 'Identifier') {
    
  146.           localNames.set(node.declaration.name, 'default');
    
  147.         } else if (node.declaration.type === 'FunctionDeclaration') {
    
  148.           if (node.declaration.id) {
    
  149.             localNames.set(node.declaration.id.name, 'default');
    
  150.             localTypes.set(node.declaration.id.name, 'function');
    
  151.           } else {
    
  152.             // TODO: This needs to be rewritten inline because it doesn't have a local name.
    
  153.           }
    
  154.         }
    
  155.         continue;
    
  156.       case 'ExportNamedDeclaration':
    
  157.         if (node.declaration) {
    
  158.           if (node.declaration.type === 'VariableDeclaration') {
    
  159.             const declarations = node.declaration.declarations;
    
  160.             for (let j = 0; j < declarations.length; j++) {
    
  161.               addLocalExportedNames(localNames, declarations[j].id);
    
  162.             }
    
  163.           } else {
    
  164.             const name = node.declaration.id.name;
    
  165.             localNames.set(name, name);
    
  166.             if (node.declaration.type === 'FunctionDeclaration') {
    
  167.               localTypes.set(name, 'function');
    
  168.             }
    
  169.           }
    
  170.         }
    
  171.         if (node.specifiers) {
    
  172.           const specifiers = node.specifiers;
    
  173.           for (let j = 0; j < specifiers.length; j++) {
    
  174.             const specifier = specifiers[j];
    
  175.             localNames.set(specifier.local.name, specifier.exported.name);
    
  176.           }
    
  177.         }
    
  178.         continue;
    
  179.     }
    
  180.   }
    
  181.   if (localNames.size === 0) {
    
  182.     return source;
    
  183.   }
    
  184.   let newSrc = source + '\n\n;';
    
  185.   newSrc +=
    
  186.     'import {registerServerReference} from "react-server-dom-webpack/server";\n';
    
  187.   localNames.forEach(function (exported, local) {
    
  188.     if (localTypes.get(local) !== 'function') {
    
  189.       // We first check if the export is a function and if so annotate it.
    
  190.       newSrc += 'if (typeof ' + local + ' === "function") ';
    
  191.     }
    
  192.     newSrc += 'registerServerReference(' + local + ',';
    
  193.     newSrc += JSON.stringify(url) + ',';
    
  194.     newSrc += JSON.stringify(exported) + ');\n';
    
  195.   });
    
  196.   return newSrc;
    
  197. }
    
  198. 
    
  199. function addExportNames(names: Array<string>, node: any) {
    
  200.   switch (node.type) {
    
  201.     case 'Identifier':
    
  202.       names.push(node.name);
    
  203.       return;
    
  204.     case 'ObjectPattern':
    
  205.       for (let i = 0; i < node.properties.length; i++)
    
  206.         addExportNames(names, node.properties[i]);
    
  207.       return;
    
  208.     case 'ArrayPattern':
    
  209.       for (let i = 0; i < node.elements.length; i++) {
    
  210.         const element = node.elements[i];
    
  211.         if (element) addExportNames(names, element);
    
  212.       }
    
  213.       return;
    
  214.     case 'Property':
    
  215.       addExportNames(names, node.value);
    
  216.       return;
    
  217.     case 'AssignmentPattern':
    
  218.       addExportNames(names, node.left);
    
  219.       return;
    
  220.     case 'RestElement':
    
  221.       addExportNames(names, node.argument);
    
  222.       return;
    
  223.     case 'ParenthesizedExpression':
    
  224.       addExportNames(names, node.expression);
    
  225.       return;
    
  226.   }
    
  227. }
    
  228. 
    
  229. function resolveClientImport(
    
  230.   specifier: string,
    
  231.   parentURL: string,
    
  232. ): {url: string} | Promise<{url: string}> {
    
  233.   // Resolve an import specifier as if it was loaded by the client. This doesn't use
    
  234.   // the overrides that this loader does but instead reverts to the default.
    
  235.   // This resolution algorithm will not necessarily have the same configuration
    
  236.   // as the actual client loader. It should mostly work and if it doesn't you can
    
  237.   // always convert to explicit exported names instead.
    
  238.   const conditions = ['node', 'import'];
    
  239.   if (stashedResolve === null) {
    
  240.     throw new Error(
    
  241.       'Expected resolve to have been called before transformSource',
    
  242.     );
    
  243.   }
    
  244.   return stashedResolve(specifier, {conditions, parentURL}, stashedResolve);
    
  245. }
    
  246. 
    
  247. async function parseExportNamesInto(
    
  248.   body: any,
    
  249.   names: Array<string>,
    
  250.   parentURL: string,
    
  251.   loader: LoadFunction,
    
  252. ): Promise<void> {
    
  253.   for (let i = 0; i < body.length; i++) {
    
  254.     const node = body[i];
    
  255.     switch (node.type) {
    
  256.       case 'ExportAllDeclaration':
    
  257.         if (node.exported) {
    
  258.           addExportNames(names, node.exported);
    
  259.           continue;
    
  260.         } else {
    
  261.           const {url} = await resolveClientImport(node.source.value, parentURL);
    
  262.           const {source} = await loader(
    
  263.             url,
    
  264.             {format: 'module', conditions: [], importAssertions: {}},
    
  265.             loader,
    
  266.           );
    
  267.           if (typeof source !== 'string') {
    
  268.             throw new Error('Expected the transformed source to be a string.');
    
  269.           }
    
  270.           let childBody;
    
  271.           try {
    
  272.             childBody = acorn.parse(source, {
    
  273.               ecmaVersion: '2024',
    
  274.               sourceType: 'module',
    
  275.             }).body;
    
  276.           } catch (x) {
    
  277.             // eslint-disable-next-line react-internal/no-production-logging
    
  278.             console.error('Error parsing %s %s', url, x.message);
    
  279.             continue;
    
  280.           }
    
  281.           await parseExportNamesInto(childBody, names, url, loader);
    
  282.           continue;
    
  283.         }
    
  284.       case 'ExportDefaultDeclaration':
    
  285.         names.push('default');
    
  286.         continue;
    
  287.       case 'ExportNamedDeclaration':
    
  288.         if (node.declaration) {
    
  289.           if (node.declaration.type === 'VariableDeclaration') {
    
  290.             const declarations = node.declaration.declarations;
    
  291.             for (let j = 0; j < declarations.length; j++) {
    
  292.               addExportNames(names, declarations[j].id);
    
  293.             }
    
  294.           } else {
    
  295.             addExportNames(names, node.declaration.id);
    
  296.           }
    
  297.         }
    
  298.         if (node.specifiers) {
    
  299.           const specifiers = node.specifiers;
    
  300.           for (let j = 0; j < specifiers.length; j++) {
    
  301.             addExportNames(names, specifiers[j].exported);
    
  302.           }
    
  303.         }
    
  304.         continue;
    
  305.     }
    
  306.   }
    
  307. }
    
  308. 
    
  309. async function transformClientModule(
    
  310.   body: any,
    
  311.   url: string,
    
  312.   loader: LoadFunction,
    
  313. ): Promise<string> {
    
  314.   const names: Array<string> = [];
    
  315. 
    
  316.   await parseExportNamesInto(body, names, url, loader);
    
  317. 
    
  318.   if (names.length === 0) {
    
  319.     return '';
    
  320.   }
    
  321. 
    
  322.   let newSrc =
    
  323.     'import {registerClientReference} from "react-server-dom-webpack/server";\n';
    
  324.   for (let i = 0; i < names.length; i++) {
    
  325.     const name = names[i];
    
  326.     if (name === 'default') {
    
  327.       newSrc += 'export default ';
    
  328.       newSrc += 'registerClientReference(function() {';
    
  329.       newSrc +=
    
  330.         'throw new Error(' +
    
  331.         JSON.stringify(
    
  332.           `Attempted to call the default export of ${url} from the server` +
    
  333.             `but it's on the client. It's not possible to invoke a client function from ` +
    
  334.             `the server, it can only be rendered as a Component or passed to props of a` +
    
  335.             `Client Component.`,
    
  336.         ) +
    
  337.         ');';
    
  338.     } else {
    
  339.       newSrc += 'export const ' + name + ' = ';
    
  340.       newSrc += 'registerClientReference(function() {';
    
  341.       newSrc +=
    
  342.         'throw new Error(' +
    
  343.         JSON.stringify(
    
  344.           `Attempted to call ${name}() from the server but ${name} is on the client. ` +
    
  345.             `It's not possible to invoke a client function from the server, it can ` +
    
  346.             `only be rendered as a Component or passed to props of a Client Component.`,
    
  347.         ) +
    
  348.         ');';
    
  349.     }
    
  350.     newSrc += '},';
    
  351.     newSrc += JSON.stringify(url) + ',';
    
  352.     newSrc += JSON.stringify(name) + ');\n';
    
  353.   }
    
  354.   return newSrc;
    
  355. }
    
  356. 
    
  357. async function loadClientImport(
    
  358.   url: string,
    
  359.   defaultTransformSource: TransformSourceFunction,
    
  360. ): Promise<{format: string, shortCircuit?: boolean, source: Source}> {
    
  361.   if (stashedGetSource === null) {
    
  362.     throw new Error(
    
  363.       'Expected getSource to have been called before transformSource',
    
  364.     );
    
  365.   }
    
  366.   // TODO: Validate that this is another module by calling getFormat.
    
  367.   const {source} = await stashedGetSource(
    
  368.     url,
    
  369.     {format: 'module'},
    
  370.     stashedGetSource,
    
  371.   );
    
  372.   const result = await defaultTransformSource(
    
  373.     source,
    
  374.     {format: 'module', url},
    
  375.     defaultTransformSource,
    
  376.   );
    
  377.   return {format: 'module', source: result.source};
    
  378. }
    
  379. 
    
  380. async function transformModuleIfNeeded(
    
  381.   source: string,
    
  382.   url: string,
    
  383.   loader: LoadFunction,
    
  384. ): Promise<string> {
    
  385.   // Do a quick check for the exact string. If it doesn't exist, don't
    
  386.   // bother parsing.
    
  387.   if (
    
  388.     source.indexOf('use client') === -1 &&
    
  389.     source.indexOf('use server') === -1
    
  390.   ) {
    
  391.     return source;
    
  392.   }
    
  393. 
    
  394.   let body;
    
  395.   try {
    
  396.     body = acorn.parse(source, {
    
  397.       ecmaVersion: '2024',
    
  398.       sourceType: 'module',
    
  399.     }).body;
    
  400.   } catch (x) {
    
  401.     // eslint-disable-next-line react-internal/no-production-logging
    
  402.     console.error('Error parsing %s %s', url, x.message);
    
  403.     return source;
    
  404.   }
    
  405. 
    
  406.   let useClient = false;
    
  407.   let useServer = false;
    
  408.   for (let i = 0; i < body.length; i++) {
    
  409.     const node = body[i];
    
  410.     if (node.type !== 'ExpressionStatement' || !node.directive) {
    
  411.       break;
    
  412.     }
    
  413.     if (node.directive === 'use client') {
    
  414.       useClient = true;
    
  415.     }
    
  416.     if (node.directive === 'use server') {
    
  417.       useServer = true;
    
  418.     }
    
  419.   }
    
  420. 
    
  421.   if (!useClient && !useServer) {
    
  422.     return source;
    
  423.   }
    
  424. 
    
  425.   if (useClient && useServer) {
    
  426.     throw new Error(
    
  427.       'Cannot have both "use client" and "use server" directives in the same file.',
    
  428.     );
    
  429.   }
    
  430. 
    
  431.   if (useClient) {
    
  432.     return transformClientModule(body, url, loader);
    
  433.   }
    
  434. 
    
  435.   return transformServerModule(source, body, url, loader);
    
  436. }
    
  437. 
    
  438. export async function transformSource(
    
  439.   source: Source,
    
  440.   context: TransformSourceContext,
    
  441.   defaultTransformSource: TransformSourceFunction,
    
  442. ): Promise<{source: Source}> {
    
  443.   const transformed = await defaultTransformSource(
    
  444.     source,
    
  445.     context,
    
  446.     defaultTransformSource,
    
  447.   );
    
  448.   if (context.format === 'module') {
    
  449.     const transformedSource = transformed.source;
    
  450.     if (typeof transformedSource !== 'string') {
    
  451.       throw new Error('Expected source to have been transformed to a string.');
    
  452.     }
    
  453.     const newSrc = await transformModuleIfNeeded(
    
  454.       transformedSource,
    
  455.       context.url,
    
  456.       (url: string, ctx: LoadContext, defaultLoad: LoadFunction) => {
    
  457.         return loadClientImport(url, defaultTransformSource);
    
  458.       },
    
  459.     );
    
  460.     return {source: newSrc};
    
  461.   }
    
  462.   return transformed;
    
  463. }
    
  464. 
    
  465. export async function load(
    
  466.   url: string,
    
  467.   context: LoadContext,
    
  468.   defaultLoad: LoadFunction,
    
  469. ): Promise<{format: string, shortCircuit?: boolean, source: Source}> {
    
  470.   const result = await defaultLoad(url, context, defaultLoad);
    
  471.   if (result.format === 'module') {
    
  472.     if (typeof result.source !== 'string') {
    
  473.       throw new Error('Expected source to have been loaded into a string.');
    
  474.     }
    
  475.     const newSrc = await transformModuleIfNeeded(
    
  476.       result.source,
    
  477.       url,
    
  478.       defaultLoad,
    
  479.     );
    
  480.     return {format: 'module', source: newSrc};
    
  481.   }
    
  482.   return result;
    
  483. }