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. 'use strict';
    
  10. 
    
  11. async function insertNodesAndExecuteScripts(
    
  12.   source: Document | Element,
    
  13.   target: Node,
    
  14.   CSPnonce: string | null,
    
  15. ) {
    
  16.   const ownerDocument = target.ownerDocument || target;
    
  17. 
    
  18.   // We need to remove the script content for any scripts that would not run based on CSP
    
  19.   // We restore the script content after moving the nodes into the target
    
  20.   const badNonceScriptNodes: Map<Element, string> = new Map();
    
  21.   if (CSPnonce) {
    
  22.     const scripts = source.querySelectorAll('script');
    
  23.     for (let i = 0; i < scripts.length; i++) {
    
  24.       const script = scripts[i];
    
  25.       if (
    
  26.         !script.hasAttribute('src') &&
    
  27.         script.getAttribute('nonce') !== CSPnonce
    
  28.       ) {
    
  29.         badNonceScriptNodes.set(script, script.textContent);
    
  30.         script.textContent = '';
    
  31.       }
    
  32.     }
    
  33.   }
    
  34.   let lastChild = null;
    
  35.   while (source.firstChild) {
    
  36.     const node = source.firstChild;
    
  37.     if (lastChild === node) {
    
  38.       throw new Error('Infinite loop.');
    
  39.     }
    
  40.     lastChild = node;
    
  41. 
    
  42.     if (node.nodeType === 1) {
    
  43.       const element: Element = (node: any);
    
  44.       if (
    
  45.         // $FlowFixMe[prop-missing]
    
  46.         element.dataset != null &&
    
  47.         (element.dataset.rxi != null ||
    
  48.           element.dataset.rri != null ||
    
  49.           element.dataset.rci != null ||
    
  50.           element.dataset.rsi != null)
    
  51.       ) {
    
  52.         // Fizz external runtime instructions are expected to be in the body.
    
  53.         // When we have renderIntoContainer and renderDocument this will be
    
  54.         // more enforceable. At the moment you can misconfigure your stream and end up
    
  55.         // with instructions that are deep in the document
    
  56.         (ownerDocument.body: any).appendChild(element);
    
  57.       } else {
    
  58.         target.appendChild(element);
    
  59. 
    
  60.         if (element.nodeName === 'SCRIPT') {
    
  61.           await executeScript(element);
    
  62.         } else {
    
  63.           const scripts = element.querySelectorAll('script');
    
  64.           for (let i = 0; i < scripts.length; i++) {
    
  65.             const script = scripts[i];
    
  66.             await executeScript(script);
    
  67.           }
    
  68.         }
    
  69.       }
    
  70.     } else {
    
  71.       target.appendChild(node);
    
  72.     }
    
  73.   }
    
  74. 
    
  75.   // restore the textContent now that we have finished attempting to execute scripts
    
  76.   badNonceScriptNodes.forEach((scriptContent, script) => {
    
  77.     script.textContent = scriptContent;
    
  78.   });
    
  79. }
    
  80. 
    
  81. async function executeScript(script: Element) {
    
  82.   const ownerDocument = script.ownerDocument;
    
  83.   if (script.parentNode == null) {
    
  84.     throw new Error(
    
  85.       'executeScript expects to be called on script nodes that are currently in a document',
    
  86.     );
    
  87.   }
    
  88.   const parent = script.parentNode;
    
  89.   const scriptSrc = script.getAttribute('src');
    
  90.   if (scriptSrc) {
    
  91.     if (document !== ownerDocument) {
    
  92.       throw new Error(
    
  93.         'You must set the current document to the global document to use script src in tests',
    
  94.       );
    
  95.     }
    
  96. 
    
  97.     try {
    
  98.       // $FlowFixMe
    
  99.       require(scriptSrc);
    
  100.     } catch (x) {
    
  101.       const event = new window.ErrorEvent('error', {error: x});
    
  102.       window.dispatchEvent(event);
    
  103.     }
    
  104.   } else {
    
  105.     const newScript = ownerDocument.createElement('script');
    
  106.     newScript.textContent = script.textContent;
    
  107.     // make sure to add nonce back to script if it exists
    
  108.     for (let i = 0; i < script.attributes.length; i++) {
    
  109.       const attribute = script.attributes[i];
    
  110.       newScript.setAttribute(attribute.name, attribute.value);
    
  111.     }
    
  112. 
    
  113.     parent.insertBefore(newScript, script);
    
  114.     parent.removeChild(script);
    
  115.   }
    
  116. }
    
  117. 
    
  118. function mergeOptions(options: Object, defaultOptions: Object): Object {
    
  119.   return {
    
  120.     ...defaultOptions,
    
  121.     ...options,
    
  122.   };
    
  123. }
    
  124. 
    
  125. function stripExternalRuntimeInNodes(
    
  126.   nodes: HTMLElement[] | HTMLCollection<HTMLElement>,
    
  127.   externalRuntimeSrc: string | null,
    
  128. ): HTMLElement[] {
    
  129.   if (!Array.isArray(nodes)) {
    
  130.     nodes = Array.from(nodes);
    
  131.   }
    
  132.   if (externalRuntimeSrc == null) {
    
  133.     return nodes;
    
  134.   }
    
  135.   return nodes.filter(
    
  136.     n =>
    
  137.       (n.tagName !== 'SCRIPT' && n.tagName !== 'script') ||
    
  138.       n.getAttribute('src') !== externalRuntimeSrc,
    
  139.   );
    
  140. }
    
  141. 
    
  142. // Since JSDOM doesn't implement a streaming HTML parser, we manually overwrite
    
  143. // readyState here (currently read by ReactDOMServerExternalRuntime). This does
    
  144. // not trigger event callbacks, but we do not rely on any right now.
    
  145. async function withLoadingReadyState<T>(
    
  146.   fn: () => T,
    
  147.   document: Document,
    
  148. ): Promise<T> {
    
  149.   // JSDOM implements readyState in document's direct prototype, but this may
    
  150.   // change in later versions
    
  151.   let prevDescriptor = null;
    
  152.   let proto: Object = document;
    
  153.   while (proto != null) {
    
  154.     prevDescriptor = Object.getOwnPropertyDescriptor(proto, 'readyState');
    
  155.     if (prevDescriptor != null) {
    
  156.       break;
    
  157.     }
    
  158.     proto = Object.getPrototypeOf(proto);
    
  159.   }
    
  160.   Object.defineProperty(document, 'readyState', {
    
  161.     get() {
    
  162.       return 'loading';
    
  163.     },
    
  164.     configurable: true,
    
  165.   });
    
  166.   const result = await fn();
    
  167.   // $FlowFixMe[incompatible-type]
    
  168.   delete document.readyState;
    
  169.   if (prevDescriptor) {
    
  170.     Object.defineProperty(proto, 'readyState', prevDescriptor);
    
  171.   }
    
  172.   return result;
    
  173. }
    
  174. 
    
  175. function getVisibleChildren(element: Element): React$Node {
    
  176.   const children = [];
    
  177.   let node: any = element.firstChild;
    
  178.   while (node) {
    
  179.     if (node.nodeType === 1) {
    
  180.       if (
    
  181.         ((node.tagName !== 'SCRIPT' && node.tagName !== 'script') ||
    
  182.           node.hasAttribute('data-meaningful')) &&
    
  183.         node.tagName !== 'TEMPLATE' &&
    
  184.         node.tagName !== 'template' &&
    
  185.         !node.hasAttribute('hidden') &&
    
  186.         !node.hasAttribute('aria-hidden')
    
  187.       ) {
    
  188.         const props: any = {};
    
  189.         const attributes = node.attributes;
    
  190.         for (let i = 0; i < attributes.length; i++) {
    
  191.           if (
    
  192.             attributes[i].name === 'id' &&
    
  193.             attributes[i].value.includes(':')
    
  194.           ) {
    
  195.             // We assume this is a React added ID that's a non-visual implementation detail.
    
  196.             continue;
    
  197.           }
    
  198.           props[attributes[i].name] = attributes[i].value;
    
  199.         }
    
  200.         props.children = getVisibleChildren(node);
    
  201.         children.push(
    
  202.           require('react').createElement(node.tagName.toLowerCase(), props),
    
  203.         );
    
  204.       }
    
  205.     } else if (node.nodeType === 3) {
    
  206.       children.push(node.data);
    
  207.     }
    
  208.     node = node.nextSibling;
    
  209.   }
    
  210.   return children.length === 0
    
  211.     ? undefined
    
  212.     : children.length === 1
    
  213.     ? children[0]
    
  214.     : children;
    
  215. }
    
  216. 
    
  217. export {
    
  218.   insertNodesAndExecuteScripts,
    
  219.   mergeOptions,
    
  220.   stripExternalRuntimeInNodes,
    
  221.   withLoadingReadyState,
    
  222.   getVisibleChildren,
    
  223. };