main
js 195 lines 5.76 KB
Raw
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 as 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 as 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[unsupported-syntax]
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 function getVisibleChildren(element: Element): React$Node {
143 const children = [];
144 let node: any = element.firstChild;
145 while (node) {
146 if (node.nodeType === 1) {
147 if (
148 ((node.tagName !== 'SCRIPT' && node.tagName !== 'script') ||
149 node.hasAttribute('data-meaningful')) &&
150 node.tagName !== 'TEMPLATE' &&
151 node.tagName !== 'template' &&
152 !node.hasAttribute('hidden') &&
153 !node.hasAttribute('aria-hidden') &&
154 // Ignore the render blocking expect
155 (node.getAttribute('rel') !== 'expect' ||
156 node.getAttribute('blocking') !== 'render')
157 ) {
158 const props: any = {};
159 const attributes = node.attributes;
160 for (let i = 0; i < attributes.length; i++) {
161 if (
162 attributes[i].name === 'id' &&
163 attributes[i].value.includes(':')
164 ) {
165 // We assume this is a React added ID that's a non-visual implementation detail.
166 continue;
167 }
168 props[attributes[i].name] = attributes[i].value;
169 }
170 const nestedChildren = getVisibleChildren(node);
171 if (nestedChildren !== undefined) {
172 props.children = nestedChildren;
173 }
174 children.push(
175 require('react').createElement(node.tagName.toLowerCase(), props),
176 );
177 }
178 } else if (node.nodeType === 3) {
179 children.push(node.data);
180 }
181 node = node.nextSibling;
182 }
183 return children.length === 0
184 ? undefined
185 : children.length === 1
186 ? children[0]
187 : children;
188 }
189
190 export {
191 insertNodesAndExecuteScripts,
192 mergeOptions,
193 stripExternalRuntimeInNodes,
194 getVisibleChildren,
195 };