mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
49eba01930
Refactors Resources to have a more compact and memory efficient struture. Resources generally are just an Array of chunks. A resource is flushed when it's chunks is length zero. A resource does not have any other state. Stylesheets and Style tags are different and have been modeled as a unit as a StyleQueue. This object stores the style rules to flush as part of style tags using precedence as well as all the stylesheets associated with the precedence. Stylesheets still need to track state because it affects how we issue boundary completion instructions. Additionally stylesheets encode chunks lazily because we may never write them as html if they are discovered late. The preload props transfer is now maximally compact (only stores the props we would ever actually adopt) and only stores props for stylesheets and scripts because other preloads have no resource counterpart to adopt props into. The ResumableState maps that track which keys have been observed are being overloaded. Previously if a key was found it meant that a resource already exists (either in this render or in a prior prerender). Now we discriminate between null and object values. If map value is null we can assume the resource exists but if it is an object that represents a prior preload for that resource and the resource must still be constructed.
224 lines
6.5 KiB
JavaScript
224 lines
6.5 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
*/
|
|
'use strict';
|
|
|
|
async function insertNodesAndExecuteScripts(
|
|
source: Document | Element,
|
|
target: Node,
|
|
CSPnonce: string | null,
|
|
) {
|
|
const ownerDocument = target.ownerDocument || target;
|
|
|
|
// We need to remove the script content for any scripts that would not run based on CSP
|
|
// We restore the script content after moving the nodes into the target
|
|
const badNonceScriptNodes: Map<Element, string> = new Map();
|
|
if (CSPnonce) {
|
|
const scripts = source.querySelectorAll('script');
|
|
for (let i = 0; i < scripts.length; i++) {
|
|
const script = scripts[i];
|
|
if (
|
|
!script.hasAttribute('src') &&
|
|
script.getAttribute('nonce') !== CSPnonce
|
|
) {
|
|
badNonceScriptNodes.set(script, script.textContent);
|
|
script.textContent = '';
|
|
}
|
|
}
|
|
}
|
|
let lastChild = null;
|
|
while (source.firstChild) {
|
|
const node = source.firstChild;
|
|
if (lastChild === node) {
|
|
throw new Error('Infinite loop.');
|
|
}
|
|
lastChild = node;
|
|
|
|
if (node.nodeType === 1) {
|
|
const element: Element = (node: any);
|
|
if (
|
|
// $FlowFixMe[prop-missing]
|
|
element.dataset != null &&
|
|
(element.dataset.rxi != null ||
|
|
element.dataset.rri != null ||
|
|
element.dataset.rci != null ||
|
|
element.dataset.rsi != null)
|
|
) {
|
|
// Fizz external runtime instructions are expected to be in the body.
|
|
// When we have renderIntoContainer and renderDocument this will be
|
|
// more enforceable. At the moment you can misconfigure your stream and end up
|
|
// with instructions that are deep in the document
|
|
(ownerDocument.body: any).appendChild(element);
|
|
} else {
|
|
target.appendChild(element);
|
|
|
|
if (element.nodeName === 'SCRIPT') {
|
|
await executeScript(element);
|
|
} else {
|
|
const scripts = element.querySelectorAll('script');
|
|
for (let i = 0; i < scripts.length; i++) {
|
|
const script = scripts[i];
|
|
await executeScript(script);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
target.appendChild(node);
|
|
}
|
|
}
|
|
|
|
// restore the textContent now that we have finished attempting to execute scripts
|
|
badNonceScriptNodes.forEach((scriptContent, script) => {
|
|
script.textContent = scriptContent;
|
|
});
|
|
}
|
|
|
|
async function executeScript(script: Element) {
|
|
const ownerDocument = script.ownerDocument;
|
|
if (script.parentNode == null) {
|
|
throw new Error(
|
|
'executeScript expects to be called on script nodes that are currently in a document',
|
|
);
|
|
}
|
|
const parent = script.parentNode;
|
|
const scriptSrc = script.getAttribute('src');
|
|
if (scriptSrc) {
|
|
if (document !== ownerDocument) {
|
|
throw new Error(
|
|
'You must set the current document to the global document to use script src in tests',
|
|
);
|
|
}
|
|
|
|
try {
|
|
// $FlowFixMe
|
|
require(scriptSrc);
|
|
} catch (x) {
|
|
const event = new window.ErrorEvent('error', {error: x});
|
|
window.dispatchEvent(event);
|
|
}
|
|
} else {
|
|
const newScript = ownerDocument.createElement('script');
|
|
newScript.textContent = script.textContent;
|
|
// make sure to add nonce back to script if it exists
|
|
for (let i = 0; i < script.attributes.length; i++) {
|
|
const attribute = script.attributes[i];
|
|
newScript.setAttribute(attribute.name, attribute.value);
|
|
}
|
|
|
|
parent.insertBefore(newScript, script);
|
|
parent.removeChild(script);
|
|
}
|
|
}
|
|
|
|
function mergeOptions(options: Object, defaultOptions: Object): Object {
|
|
return {
|
|
...defaultOptions,
|
|
...options,
|
|
};
|
|
}
|
|
|
|
function stripExternalRuntimeInNodes(
|
|
nodes: HTMLElement[] | HTMLCollection<HTMLElement>,
|
|
externalRuntimeSrc: string | null,
|
|
): HTMLElement[] {
|
|
if (!Array.isArray(nodes)) {
|
|
nodes = Array.from(nodes);
|
|
}
|
|
if (externalRuntimeSrc == null) {
|
|
return nodes;
|
|
}
|
|
return nodes.filter(
|
|
n =>
|
|
(n.tagName !== 'SCRIPT' && n.tagName !== 'script') ||
|
|
n.getAttribute('src') !== externalRuntimeSrc,
|
|
);
|
|
}
|
|
|
|
// Since JSDOM doesn't implement a streaming HTML parser, we manually overwrite
|
|
// readyState here (currently read by ReactDOMServerExternalRuntime). This does
|
|
// not trigger event callbacks, but we do not rely on any right now.
|
|
async function withLoadingReadyState<T>(
|
|
fn: () => T,
|
|
document: Document,
|
|
): Promise<T> {
|
|
// JSDOM implements readyState in document's direct prototype, but this may
|
|
// change in later versions
|
|
let prevDescriptor = null;
|
|
let proto: Object = document;
|
|
while (proto != null) {
|
|
prevDescriptor = Object.getOwnPropertyDescriptor(proto, 'readyState');
|
|
if (prevDescriptor != null) {
|
|
break;
|
|
}
|
|
proto = Object.getPrototypeOf(proto);
|
|
}
|
|
Object.defineProperty(document, 'readyState', {
|
|
get() {
|
|
return 'loading';
|
|
},
|
|
configurable: true,
|
|
});
|
|
const result = await fn();
|
|
// $FlowFixMe[incompatible-type]
|
|
delete document.readyState;
|
|
if (prevDescriptor) {
|
|
Object.defineProperty(proto, 'readyState', prevDescriptor);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function getVisibleChildren(element: Element): React$Node {
|
|
const children = [];
|
|
let node: any = element.firstChild;
|
|
while (node) {
|
|
if (node.nodeType === 1) {
|
|
if (
|
|
((node.tagName !== 'SCRIPT' && node.tagName !== 'script') ||
|
|
node.hasAttribute('data-meaningful')) &&
|
|
node.tagName !== 'TEMPLATE' &&
|
|
node.tagName !== 'template' &&
|
|
!node.hasAttribute('hidden') &&
|
|
!node.hasAttribute('aria-hidden')
|
|
) {
|
|
const props: any = {};
|
|
const attributes = node.attributes;
|
|
for (let i = 0; i < attributes.length; i++) {
|
|
if (
|
|
attributes[i].name === 'id' &&
|
|
attributes[i].value.includes(':')
|
|
) {
|
|
// We assume this is a React added ID that's a non-visual implementation detail.
|
|
continue;
|
|
}
|
|
props[attributes[i].name] = attributes[i].value;
|
|
}
|
|
props.children = getVisibleChildren(node);
|
|
children.push(
|
|
require('react').createElement(node.tagName.toLowerCase(), props),
|
|
);
|
|
}
|
|
} else if (node.nodeType === 3) {
|
|
children.push(node.data);
|
|
}
|
|
node = node.nextSibling;
|
|
}
|
|
return children.length === 0
|
|
? undefined
|
|
: children.length === 1
|
|
? children[0]
|
|
: children;
|
|
}
|
|
|
|
export {
|
|
insertNodesAndExecuteScripts,
|
|
mergeOptions,
|
|
stripExternalRuntimeInNodes,
|
|
withLoadingReadyState,
|
|
getVisibleChildren,
|
|
};
|