[Fizz/Float] Float for stylesheet resources (#25243)

* [Fizz/Float] Float for stylesheet resources

This commit implements Float in Fizz and on the Client. The initial set of supported APIs is roughly

1. Convert certain stylesheets into style Resources when opting in with precedence prop
2. Emit preloads for stylesheets and explicit preload tags
3. Dedupe all Resources by href
4. Implement ReactDOM.preload() to allow for imperative preloading
5. Implement ReactDOM.preinit() to allow for imperative preinitialization

Currently supports
1. style Resources (link rel "stylesheet")
2. font Resources (preload as "font")

later updates will include support for scripts and modules
This commit is contained in:
Josh Story 2022-09-30 16:14:04 -07:00 committed by GitHub
parent d061e6e7d4
commit 7b25b961df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 7071 additions and 635 deletions

View File

@ -243,6 +243,7 @@ export * from 'react-reconciler/src/ReactFiberHostConfigWithNoHydration';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoScopes';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoTestSelectors';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoMicrotasks';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoResources';
export function appendInitialChild(parentInstance, child) {
if (typeof child === 'string') {
@ -455,3 +456,11 @@ export function detachDeletedInstance(node: Instance): void {
export function requestPostPaintCallback(callback: (time: number) => void) {
// noop
}
export function prepareRendererToRender(container: Container): void {
// noop
}
export function resetRendererAfterRender(): void {
// noop
}

View File

@ -73,7 +73,6 @@ import {
enableTrustedTypesIntegration,
enableCustomElementPropertySupport,
enableClientRenderFallbackOnTextMismatch,
enableFloat,
} from 'shared/ReactFeatureFlags';
import {
mediaEventTypes,
@ -1019,17 +1018,6 @@ export function diffHydratedProperties(
: getPropertyInfo(propKey);
if (rawProps[SUPPRESS_HYDRATION_WARNING] === true) {
// Don't bother comparing. We're ignoring all these warnings.
} else if (
enableFloat &&
tag === 'link' &&
rawProps.rel === 'stylesheet' &&
propKey === 'precedence'
) {
// @TODO this is a temporary rule while we haven't implemented HostResources yet. This is used to allow
// for hydrating Resources (at the moment, stylesheets with a precedence prop) by using a data attribute.
// When we implement HostResources there will be no hydration directly so this code can be deleted
// $FlowFixMe - Should be inferred as not undefined.
extraAttributeNames.delete('data-rprec');
} else if (
propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
propKey === SUPPRESS_HYDRATION_WARNING ||

View File

@ -23,6 +23,7 @@ import type {
import {
HostComponent,
HostResource,
HostText,
HostRoot,
SuspenseComponent,
@ -30,7 +31,7 @@ import {
import {getParentSuspenseInstance} from './ReactDOMHostConfig';
import {enableScopeAPI} from 'shared/ReactFeatureFlags';
import {enableScopeAPI, enableFloat} from 'shared/ReactFeatureFlags';
const randomKey = Math.random()
.toString(36)
@ -166,7 +167,8 @@ export function getInstanceFromNode(node: Node): Fiber | null {
inst.tag === HostComponent ||
inst.tag === HostText ||
inst.tag === SuspenseComponent ||
inst.tag === HostRoot
inst.tag === HostRoot ||
(enableFloat ? inst.tag === HostResource : false)
) {
return inst;
} else {
@ -181,7 +183,11 @@ export function getInstanceFromNode(node: Node): Fiber | null {
* DOM node.
*/
export function getNodeFromInstance(inst: Fiber): Instance | TextInstance {
if (inst.tag === HostComponent || inst.tag === HostText) {
if (
inst.tag === HostComponent ||
inst.tag === HostText ||
(enableFloat ? inst.tag === HostResource : false)
) {
// In Fiber this, is just the state node right now. We assume it will be
// a host component or host text.
return inst.stateNode;

View File

@ -0,0 +1,825 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {Instance} from './ReactDOMHostConfig';
import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals.js';
const {Dispatcher} = ReactDOMSharedInternals;
import {
validateUnmatchedLinkResourceProps,
validatePreloadResourceDifference,
validateHrefKeyedUpdatedProps,
validateStyleResourceDifference,
validateLinkPropsForStyleResource,
validateLinkPropsForPreloadResource,
validatePreloadArguments,
validatePreinitArguments,
} from '../shared/ReactDOMResourceValidation';
import {createElement, setInitialProperties} from './ReactDOMComponent';
import {HTML_NAMESPACE} from '../shared/DOMNamespaces';
// The resource types we support. currently they match the form for the as argument.
// In the future this may need to change, especially when modules / scripts are supported
type ResourceType = 'style' | 'font';
type PreloadProps = {
rel: 'preload',
as: ResourceType,
href: string,
[string]: mixed,
};
type PreloadResource = {
type: 'preload',
href: string,
ownerDocument: Document,
props: PreloadProps,
instance: Element,
};
type StyleProps = {
rel: 'stylesheet',
href: string,
'data-rprec': string,
[string]: mixed,
};
type StyleResource = {
type: 'style',
// Ref count for resource
count: number,
// Resource Descriptors
href: string,
precedence: string,
props: StyleProps,
// Related Resources
hint: ?PreloadResource,
// Insertion
preloaded: boolean,
loaded: boolean,
error: mixed,
instance: ?Element,
ownerDocument: Document,
};
type Props = {[string]: mixed};
type Resource = StyleResource | PreloadResource;
// Brief on purpose due to insertion by script when streaming late boundaries
// s = Status
// l = loaded
// e = errored
type StyleResourceLoadingState = Promise<mixed> & {s?: 'l' | 'e'};
// When rendering we set the currentDocument if one exists. we use this for Resources
// we encounter during render. If this is null and we are dispatching preloads and
// other calls on the ReactDOM module we look for the window global and get the document from there
let currentDocument: ?Document = null;
// It is valid to preload even when we aren't actively rendering. For cases where Float functions are
// called when there is no rendering we track the last used document. It is not safe to insert
// arbitrary resources into the lastCurrentDocument b/c it may not actually be the document
// that the resource is meant to apply too (for example stylesheets or scripts). This is only
// appropriate for resources that don't really have a strict tie to the document itself for example
// preloads
let lastCurrentDocument: ?Document = null;
let previousDispatcher = null;
export function prepareToRenderResources(ownerDocument: Document) {
currentDocument = lastCurrentDocument = ownerDocument;
previousDispatcher = Dispatcher.current;
Dispatcher.current = ReactDOMClientDispatcher;
}
export function cleanupAfterRenderResources() {
currentDocument = null;
Dispatcher.current = previousDispatcher;
previousDispatcher = null;
}
// We want this to be the default dispatcher on ReactDOMSharedInternals but we don't want to mutate
// internals in Module scope. Instead we export it and Internals will import it. There is already a cycle
// from Internals -> ReactDOM -> FloatClient -> Internals so this doesn't introduce a new one.
export const ReactDOMClientDispatcher = {preload, preinit};
// global maps of Resources
const preloadResources: Map<string, PreloadResource> = new Map();
const styleResources: Map<string, StyleResource> = new Map();
// Preloads are somewhat special. Even if we don't have the Document
// used by the root that is rendering a component trying to insert a preload
// we can still seed the file cache by doing the preload on any document we have
// access to. We prefer the currentDocument if it exists, we also prefer the
// lastCurrentDocument if that exists. As a fallback we will use the window.document
// if available.
function getDocumentForPreloads(): ?Document {
try {
return currentDocument || lastCurrentDocument || window.document;
} catch (error) {
return null;
}
}
// --------------------------------------
// ReactDOM.Preload
// --------------------------------------
type PreloadAs = ResourceType;
type PreloadOptions = {as: PreloadAs, crossOrigin?: string};
function preload(href: string, options: PreloadOptions) {
if (__DEV__) {
validatePreloadArguments(href, options);
}
const ownerDocument = getDocumentForPreloads();
if (
typeof href === 'string' &&
href &&
typeof options === 'object' &&
options !== null &&
ownerDocument
) {
const as = options.as;
const resource = preloadResources.get(href);
if (resource) {
if (__DEV__) {
const originallyImplicit =
(resource: any)._dev_implicit_construction === true;
const latestProps = preloadPropsFromPreloadOptions(href, as, options);
validatePreloadResourceDifference(
resource.props,
originallyImplicit,
latestProps,
false,
);
}
} else {
const resourceProps = preloadPropsFromPreloadOptions(href, as, options);
createPreloadResource(ownerDocument, href, resourceProps);
}
}
}
function preloadPropsFromPreloadOptions(
href: string,
as: ResourceType,
options: PreloadOptions,
): PreloadProps {
return {
href,
rel: 'preload',
as,
crossOrigin: as === 'font' ? '' : options.crossOrigin,
};
}
// --------------------------------------
// ReactDOM.preinit
// --------------------------------------
type PreinitAs = 'style';
type PreinitOptions = {
as: PreinitAs,
crossOrigin?: string,
precedence?: string,
};
function preinit(href: string, options: PreinitOptions) {
if (__DEV__) {
validatePreinitArguments(href, options);
}
if (
typeof href === 'string' &&
href &&
typeof options === 'object' &&
options !== null
) {
const as = options.as;
if (!currentDocument) {
// We are going to emit a preload as a best effort fallback since this preinit
// was called outside of a render. Given the passive nature of this fallback
// we do not warn in dev when props disagree if there happens to already be a
// matching preload with this href
const preloadDocument = getDocumentForPreloads();
if (preloadDocument) {
const preloadResource = preloadResources.get(href);
if (!preloadResource) {
const preloadProps = preloadPropsFromPreinitOptions(
href,
as,
options,
);
createPreloadResource(preloadDocument, href, preloadProps);
}
}
return;
}
switch (as) {
case 'style': {
const precedence = options.precedence || 'default';
let resource = styleResources.get(href);
if (resource) {
if (__DEV__) {
const latestProps = stylePropsFromPreinitOptions(
href,
precedence,
options,
);
validateStyleResourceDifference(resource.props, latestProps);
}
} else {
const resourceProps = stylePropsFromPreinitOptions(
href,
precedence,
options,
);
resource = createStyleResource(
currentDocument,
href,
precedence,
resourceProps,
);
}
acquireResource(resource);
}
}
}
}
function preloadPropsFromPreinitOptions(
href: string,
as: ResourceType,
options: PreinitOptions,
): PreloadProps {
return {
href,
rel: 'preload',
as,
crossOrigin: as === 'font' ? '' : options.crossOrigin,
};
}
function stylePropsFromPreinitOptions(
href: string,
precedence: string,
options: PreinitOptions,
): StyleProps {
return {
rel: 'stylesheet',
href,
'data-rprec': precedence,
crossOrigin: options.crossOrigin,
};
}
// --------------------------------------
// Resources from render
// --------------------------------------
type StyleQualifyingProps = {
rel: 'stylesheet',
href: string,
precedence: string,
[string]: mixed,
};
type PreloadQualifyingProps = {
rel: 'preload',
href: string,
as: ResourceType,
[string]: mixed,
};
// This function is called in begin work and we should always have a currentDocument set
export function getResource(
type: string,
pendingProps: Props,
currentProps: null | Props,
): null | Resource {
if (!currentDocument) {
throw new Error(
'"currentDocument" was expected to exist. This is a bug in React.',
);
}
switch (type) {
case 'link': {
const {rel} = pendingProps;
switch (rel) {
case 'stylesheet': {
let didWarn;
if (__DEV__) {
if (currentProps) {
didWarn = validateHrefKeyedUpdatedProps(
pendingProps,
currentProps,
);
}
if (!didWarn) {
didWarn = validateLinkPropsForStyleResource(pendingProps);
}
}
const {precedence, href} = pendingProps;
if (typeof href === 'string' && typeof precedence === 'string') {
// We've asserted all the specific types for StyleQualifyingProps
const styleRawProps: StyleQualifyingProps = (pendingProps: any);
// We construct or get an existing resource for the style itself and return it
let resource = styleResources.get(href);
if (resource) {
if (__DEV__) {
if (!didWarn) {
const latestProps = stylePropsFromRawProps(styleRawProps);
if ((resource: any)._dev_preload_props) {
adoptPreloadProps(
latestProps,
(resource: any)._dev_preload_props,
);
}
validateStyleResourceDifference(resource.props, latestProps);
}
}
} else {
const resourceProps = stylePropsFromRawProps(styleRawProps);
resource = createStyleResource(
currentDocument,
href,
precedence,
resourceProps,
);
immediatelyPreloadStyleResource(resource);
}
return resource;
}
return null;
}
case 'preload': {
if (__DEV__) {
validateLinkPropsForPreloadResource(pendingProps);
}
const {href, as} = pendingProps;
if (typeof href === 'string' && isResourceAsType(as)) {
// We've asserted all the specific types for PreloadQualifyingProps
const preloadRawProps: PreloadQualifyingProps = (pendingProps: any);
let resource = preloadResources.get(href);
if (resource) {
if (__DEV__) {
const originallyImplicit =
(resource: any)._dev_implicit_construction === true;
const latestProps = preloadPropsFromRawProps(preloadRawProps);
validatePreloadResourceDifference(
resource.props,
originallyImplicit,
latestProps,
false,
);
}
} else {
const resourceProps = preloadPropsFromRawProps(preloadRawProps);
resource = createPreloadResource(
currentDocument,
href,
resourceProps,
);
}
return resource;
}
return null;
}
default: {
if (__DEV__) {
validateUnmatchedLinkResourceProps(pendingProps, currentProps);
}
return null;
}
}
}
default: {
throw new Error(
`getResource encountered a resource type it did not expect: "${type}". this is a bug in React.`,
);
}
}
}
function preloadPropsFromRawProps(
rawBorrowedProps: PreloadQualifyingProps,
): PreloadProps {
return Object.assign({}, rawBorrowedProps);
}
function stylePropsFromRawProps(rawProps: StyleQualifyingProps): StyleProps {
const props: StyleProps = Object.assign({}, rawProps);
props['data-rprec'] = rawProps.precedence;
props.precedence = null;
return props;
}
// --------------------------------------
// Resource Reconciliation
// --------------------------------------
export function acquireResource(resource: Resource): Instance {
switch (resource.type) {
case 'style': {
return acquireStyleResource(resource);
}
case 'preload': {
return resource.instance;
}
default: {
throw new Error(
`acquireResource encountered a resource type it did not expect: "${resource.type}". this is a bug in React.`,
);
}
}
}
export function releaseResource(resource: Resource) {
switch (resource.type) {
case 'style': {
resource.count--;
}
}
}
function createResourceInstance(
type: string,
props: Object,
ownerDocument: Document,
): Instance {
const element = createElement(type, props, ownerDocument, HTML_NAMESPACE);
setInitialProperties(element, type, props);
return element;
}
function createStyleResource(
ownerDocument: Document,
href: string,
precedence: string,
props: StyleProps,
): StyleResource {
if (__DEV__) {
if (styleResources.has(href)) {
console.error(
'createStyleResource was called when a style Resource matching the same href already exists. This is a bug in React.',
);
}
}
const limitedEscapedHref = escapeSelectorAttributeValueInsideDoubleQuotes(
href,
);
const existingEl = ownerDocument.querySelector(
`link[rel="stylesheet"][href="${limitedEscapedHref}"]`,
);
const resource = {
type: 'style',
count: 0,
href,
precedence,
props,
hint: null,
preloaded: false,
loaded: false,
error: false,
ownerDocument,
instance: null,
};
styleResources.set(href, resource);
if (existingEl) {
// If we have an existing element in the DOM we don't need to preload this resource nor can we
// adopt props from any preload that might exist already for this resource. We do need to try
// to reify the Resource loading state the best we can.
const loadingState: ?StyleResourceLoadingState = (existingEl: any)._p;
if (loadingState) {
switch (loadingState.s) {
case 'l': {
resource.loaded = true;
break;
}
case 'e': {
resource.error = true;
break;
}
default: {
attachLoadListeners(existingEl, resource);
}
}
} else {
// This is unfortunately just an assumption. The rationale here is that stylesheets without
// a loading state must have been flushed in the shell and would have blocked until loading
// or error. we can't know afterwards which happened for all types of stylesheets (cross origin)
// for instance) and the techniques for determining if a sheet has loaded that we do have still
// fail if the sheet loaded zero rules. At the moment we are going to just opt to assume the
// sheet is loaded if it was flushed in the shell
resource.loaded = true;
}
} else {
const hint = preloadResources.get(href);
if (hint) {
resource.hint = hint;
// If a preload for this style Resource already exists there are certain props we want to adopt
// on the style Resource, primarily focussed on making sure the style network pathways utilize
// the preload pathways. For instance if you have diffreent crossOrigin attributes for a preload
// and a stylesheet the stylesheet will make a new request even if the preload had already loaded
const preloadProps = hint.props;
adoptPreloadProps(resource.props, hint.props);
if (__DEV__) {
(resource: any)._dev_preload_props = preloadProps;
}
}
}
return resource;
}
function adoptPreloadProps(
styleProps: StyleProps,
preloadProps: PreloadProps,
): void {
if (styleProps.crossOrigin == null)
styleProps.crossOrigin = preloadProps.crossOrigin;
if (styleProps.referrerPolicy == null)
styleProps.referrerPolicy = preloadProps.referrerPolicy;
if (styleProps.media == null) styleProps.media = preloadProps.media;
if (styleProps.title == null) styleProps.title = preloadProps.title;
}
function immediatelyPreloadStyleResource(resource: StyleResource) {
// This function must be called synchronously after creating a styleResource otherwise it may
// violate assumptions around the existence of a preload. The reason it is extracted out is we
// don't always want to preload a style, in particular when we are going to synchronously insert
// that style. We confirm the style resource has no preload already and then construct it. If
// we wait and call this later it is possible a preload will already exist for this href
if (resource.loaded === false && resource.hint === null) {
const {href, props} = resource;
const preloadProps = preloadPropsFromStyleProps(props);
resource.hint = createPreloadResource(
resource.ownerDocument,
href,
preloadProps,
);
}
}
function preloadPropsFromStyleProps(props: StyleProps): PreloadProps {
return {
rel: 'preload',
as: 'style',
href: props.href,
crossOrigin: props.crossOrigin,
integrity: props.integrity,
media: props.media,
hrefLang: props.hrefLang,
referrerPolicy: props.referrerPolicy,
};
}
function createPreloadResource(
ownerDocument: Document,
href: string,
props: PreloadProps,
): PreloadResource {
const limitedEscapedHref = escapeSelectorAttributeValueInsideDoubleQuotes(
href,
);
let element = ownerDocument.querySelector(
`link[rel="preload"][href="${limitedEscapedHref}"]`,
);
if (!element) {
element = createResourceInstance('link', props, ownerDocument);
insertPreloadInstance(element, ownerDocument);
}
return {
type: 'preload',
href: href,
ownerDocument,
props,
instance: element,
};
}
function acquireStyleResource(resource: StyleResource): Instance {
if (!resource.instance) {
const {props, ownerDocument, precedence} = resource;
const limitedEscapedHref = escapeSelectorAttributeValueInsideDoubleQuotes(
props.href,
);
const existingEl = ownerDocument.querySelector(
`link[rel="stylesheet"][data-rprec][href="${limitedEscapedHref}"]`,
);
if (existingEl) {
resource.instance = existingEl;
resource.preloaded = true;
const loadingState: ?StyleResourceLoadingState = (existingEl: any)._p;
if (loadingState) {
// if an existingEl is found there should always be a loadingState because if
// the resource was flushed in the head it should have already been found when
// the resource was first created. Still defensively we gate this
switch (loadingState.s) {
case 'l': {
resource.loaded = true;
resource.error = false;
break;
}
case 'e': {
resource.error = true;
break;
}
default: {
attachLoadListeners(existingEl, resource);
}
}
} else {
resource.loaded = true;
}
} else {
const instance = createResourceInstance(
'link',
resource.props,
ownerDocument,
);
attachLoadListeners(instance, resource);
insertStyleInstance(instance, precedence, ownerDocument);
resource.instance = instance;
}
}
resource.count++;
return resource.instance;
}
function attachLoadListeners(instance: Instance, resource: StyleResource) {
const listeners = {};
listeners.load = onResourceLoad.bind(
null,
instance,
resource,
listeners,
loadAndErrorEventListenerOptions,
);
listeners.error = onResourceError.bind(
null,
instance,
resource,
listeners,
loadAndErrorEventListenerOptions,
);
instance.addEventListener(
'load',
listeners.load,
loadAndErrorEventListenerOptions,
);
instance.addEventListener(
'error',
listeners.error,
loadAndErrorEventListenerOptions,
);
}
const loadAndErrorEventListenerOptions = {
passive: true,
};
function onResourceLoad(
instance: Instance,
resource: StyleResource,
listeners: {[string]: () => mixed},
listenerOptions: typeof loadAndErrorEventListenerOptions,
) {
resource.loaded = true;
resource.error = false;
for (const event in listeners) {
instance.removeEventListener(event, listeners[event], listenerOptions);
}
}
function onResourceError(
instance: Instance,
resource: StyleResource,
listeners: {[string]: () => mixed},
listenerOptions: typeof loadAndErrorEventListenerOptions,
) {
resource.loaded = false;
resource.error = true;
for (const event in listeners) {
instance.removeEventListener(event, listeners[event], listenerOptions);
}
}
function insertStyleInstance(
instance: Instance,
precedence: string,
ownerDocument: Document,
): void {
const nodes = ownerDocument.querySelectorAll(
'link[rel="stylesheet"][data-rprec]',
);
const last = nodes.length ? nodes[nodes.length - 1] : null;
let prior = last;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
const nodePrecedence = node.dataset.rprec;
if (nodePrecedence === precedence) {
prior = node;
} else if (prior !== last) {
break;
}
}
if (prior) {
// We get the prior from the document so we know it is in the tree.
// We also know that links can't be the topmost Node so the parentNode
// must exist.
((prior.parentNode: any): Node).insertBefore(instance, prior.nextSibling);
} else {
// @TODO call getRootNode on root.container. if it is a Document, insert into head
// if it is a ShadowRoot insert it into the root node.
const parent = ownerDocument.head;
if (parent) {
parent.insertBefore(instance, parent.firstChild);
} else {
throw new Error(
'While attempting to insert a Resource, React expected the Document to contain' +
' a head element but it was not found.',
);
}
}
}
function insertPreloadInstance(
instance: Instance,
ownerDocument: Document,
): void {
if (!ownerDocument.contains(instance)) {
const parent = ownerDocument.head;
if (parent) {
parent.appendChild(instance);
} else {
throw new Error(
'While attempting to insert a Resource, React expected the Document to contain' +
' a head element but it was not found.',
);
}
}
}
export function isHostResourceType(type: string, props: Props): boolean {
switch (type) {
case 'link': {
switch (props.rel) {
case 'stylesheet': {
if (__DEV__) {
validateLinkPropsForStyleResource(props);
}
const {href, precedence, onLoad, onError, disabled} = props;
return (
typeof href === 'string' &&
typeof precedence === 'string' &&
!onLoad &&
!onError &&
disabled == null
);
}
case 'preload': {
if (__DEV__) {
validateLinkPropsForStyleResource(props);
}
const {href, as, onLoad, onError} = props;
return (
!onLoad &&
!onError &&
typeof href === 'string' &&
isResourceAsType(as)
);
}
}
}
}
return false;
}
function isResourceAsType(as: mixed): boolean {
return as === 'style' || as === 'font';
}
// When passing user input into querySelector(All) the embedded string must not alter
// the semantics of the query. This escape function is safe to use when we know the
// provided value is going to be wrapped in double quotes as part of an attribute selector
// Do not use it anywhere else
// we escape double quotes and backslashes
const escapeSelectorAttributeValueInsideDoubleQuotesRegex = /[\n\"\\]/g;
function escapeSelectorAttributeValueInsideDoubleQuotes(value: string): string {
return value.replace(
escapeSelectorAttributeValueInsideDoubleQuotesRegex,
ch => '\\' + ch.charCodeAt(0).toString(16),
);
}

View File

@ -67,7 +67,11 @@ import {
enableScopeAPI,
enableFloat,
} from 'shared/ReactFeatureFlags';
import {HostComponent, HostText} from 'react-reconciler/src/ReactWorkTags';
import {
HostComponent,
HostResource,
HostText,
} from 'react-reconciler/src/ReactWorkTags';
import {listenToAllSupportedEvents} from '../events/DOMPluginEventSystem';
import {DefaultEventPriority} from 'react-reconciler/src/ReactEventPriorities';
@ -75,6 +79,12 @@ import {DefaultEventPriority} from 'react-reconciler/src/ReactEventPriorities';
// TODO: Remove this deep import when we delete the legacy root API
import {ConcurrentMode, NoMode} from 'react-reconciler/src/ReactTypeOfMode';
import {
prepareToRenderResources,
cleanupAfterRenderResources,
isHostResourceType,
} from './ReactDOMFloatClient';
export type Type = string;
export type Props = {
autoFocus?: boolean,
@ -680,14 +690,6 @@ export function clearContainer(container: Container): void {
export const supportsHydration = true;
export function isHydratableResource(type: string, props: Props): boolean {
return (
type === 'link' &&
typeof (props: any).precedence === 'string' &&
(props: any).rel === 'stylesheet'
);
}
export function canHydrateInstance(
instance: HydratableInstance,
type: string,
@ -761,18 +763,6 @@ export function getSuspenseInstanceFallbackErrorDetails(
digest,
};
}
// let value = {message: undefined, hash: undefined};
// const nextSibling = instance.nextSibling;
// if (nextSibling) {
// const dataset = ((nextSibling: any): HTMLTemplateElement).dataset;
// value.message = dataset.msg;
// value.hash = dataset.hash;
// if (__DEV__) {
// value.stack = dataset.stack;
// }
// }
// return value;
}
export function registerSuspenseInstanceRetry(
@ -788,15 +778,11 @@ function getNextHydratable(node) {
const nodeType = node.nodeType;
if (enableFloat) {
if (nodeType === ELEMENT_NODE) {
if (
((node: any): Element).tagName === 'LINK' &&
((node: any): Element).hasAttribute('data-rprec')
) {
if (isHostResourceInstance(((node: any): Element))) {
continue;
}
break;
}
if (nodeType === TEXT_NODE) {
} else if (nodeType === TEXT_NODE) {
break;
}
} else {
@ -903,43 +889,6 @@ export function hydrateSuspenseInstance(
precacheFiberNode(internalInstanceHandle, suspenseInstance);
}
export function getMatchingResourceInstance(
type: string,
props: Props,
rootHostContainer: Container,
): ?Instance {
if (enableFloat) {
switch (type) {
case 'link': {
if (typeof (props: any).href !== 'string') {
return null;
}
const selector = `link[rel="stylesheet"][data-rprec][href="${
(props: any).href
}"]`;
const link = getOwnerDocumentFromRootContainer(
rootHostContainer,
).querySelector(selector);
if (__DEV__) {
const allLinks = getOwnerDocumentFromRootContainer(
rootHostContainer,
).querySelectorAll(selector);
if (allLinks.length > 1) {
console.error(
'Stylesheet resources need a unique representation in the DOM while hydrating' +
' and more than one matching DOM Node was found. To fix, ensure you are only' +
' rendering one stylesheet link with an href attribute of "%s".',
(props: any).href,
);
}
}
return link;
}
}
}
return null;
}
export function getNextHydratableInstanceAfterSuspenseInstance(
suspenseInstance: SuspenseInstance,
): null | HydratableInstance {
@ -1281,6 +1230,7 @@ export function matchAccessibilityRole(node: Instance, role: string): boolean {
export function getTextContent(fiber: Fiber): string | null {
switch (fiber.tag) {
case HostResource:
case HostComponent:
let textContent = '';
const childNodes = fiber.stateNode.childNodes;
@ -1390,3 +1340,45 @@ export function requestPostPaintCallback(callback: (time: number) => void) {
localRequestAnimationFrame(time => callback(time));
});
}
// -------------------
// Resources
// -------------------
export const supportsResources = true;
export {isHostResourceType};
function isHostResourceInstance(instance: Instance | Container): boolean {
if (instance.nodeType === ELEMENT_NODE) {
switch (instance.tagName.toLowerCase()) {
case 'link': {
const rel = ((instance: any): HTMLLinkElement).rel;
return (
rel === 'preload' ||
(rel === 'stylesheet' && instance.hasAttribute('data-rprec'))
);
}
default: {
return false;
}
}
}
return false;
}
export function prepareRendererToRender(rootContainer: Container) {
if (enableFloat) {
prepareToRenderResources(getOwnerDocumentFromRootContainer(rootContainer));
}
}
export function resetRendererAfterRender() {
if (enableFloat) {
cleanupAfterRenderResources();
}
}
export {
getResource,
acquireResource,
releaseResource,
} from './ReactDOMFloatClient';

View File

@ -33,6 +33,7 @@ import {
HostRoot,
HostPortal,
HostComponent,
HostResource,
HostText,
ScopeComponent,
} from 'react-reconciler/src/ReactWorkTags';
@ -52,6 +53,7 @@ import {
enableLegacyFBSupport,
enableCreateEventHandleAPI,
enableScopeAPI,
enableFloat,
} from 'shared/ReactFeatureFlags';
import {
invokeGuardedCallbackAndCatchFirstError,
@ -621,7 +623,11 @@ export function dispatchEventForPluginEventSystem(
return;
}
const parentTag = parentNode.tag;
if (parentTag === HostComponent || parentTag === HostText) {
if (
parentTag === HostComponent ||
parentTag === HostText ||
(enableFloat ? parentTag === HostResource : false)
) {
node = ancestorInst = parentNode;
continue mainLoop;
}
@ -675,7 +681,10 @@ export function accumulateSinglePhaseListeners(
while (instance !== null) {
const {stateNode, tag} = instance;
// Handle listeners that are on HostComponents (i.e. <div>)
if (tag === HostComponent && stateNode !== null) {
if (
(tag === HostComponent || (enableFloat ? tag === HostResource : false)) &&
stateNode !== null
) {
lastHostComponent = stateNode;
// createEventHandle listeners
@ -786,7 +795,10 @@ export function accumulateTwoPhaseListeners(
while (instance !== null) {
const {stateNode, tag} = instance;
// Handle listeners that are on HostComponents (i.e. <div>)
if (tag === HostComponent && stateNode !== null) {
if (
(tag === HostComponent || (enableFloat ? tag === HostResource : false)) &&
stateNode !== null
) {
const currentTarget = stateNode;
const captureListener = getListener(instance, captureName);
if (captureListener != null) {
@ -883,7 +895,10 @@ function accumulateEnterLeaveListenersForEvent(
if (alternate !== null && alternate === common) {
break;
}
if (tag === HostComponent && stateNode !== null) {
if (
(tag === HostComponent || (enableFloat ? tag === HostResource : false)) &&
stateNode !== null
) {
const currentTarget = stateNode;
if (inCapturePhase) {
const captureListener = getListener(instance, registrationName);

View File

@ -0,0 +1,575 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import {
validatePreloadResourceDifference,
validateStyleResourceDifference,
validateStyleAndHintProps,
validateLinkPropsForStyleResource,
validateLinkPropsForPreloadResource,
validatePreloadArguments,
validatePreinitArguments,
} from '../shared/ReactDOMResourceValidation';
type Props = {[string]: mixed};
type ResourceType = 'style' | 'font';
type PreloadProps = {
rel: 'preload',
as: ResourceType,
href: string,
[string]: mixed,
};
type PreloadResource = {
type: 'preload',
as: ResourceType,
href: string,
props: PreloadProps,
flushed: boolean,
};
type StyleProps = {
rel: 'stylesheet',
href: string,
'data-rprec': string,
[string]: mixed,
};
type StyleResource = {
type: 'style',
href: string,
precedence: string,
props: StyleProps,
flushed: boolean,
inShell: boolean, // flushedInShell
hint: PreloadResource,
};
export type Resource = PreloadResource | StyleResource;
export type Resources = {
// Request local cache
preloadsMap: Map<string, PreloadResource>,
stylesMap: Map<string, StyleResource>,
// Flushing queues for Resource dependencies
explicitPreloads: Set<PreloadResource>,
implicitPreloads: Set<PreloadResource>,
precedences: Map<string, Set<StyleResource>>,
// Module-global-like reference for current boundary resources
boundaryResources: ?BoundaryResources,
};
// @TODO add bootstrap script to implicit preloads
export function createResources(): Resources {
return {
// persistent
preloadsMap: new Map(),
stylesMap: new Map(),
// cleared on flush
explicitPreloads: new Set(),
implicitPreloads: new Set(),
precedences: new Map(),
// like a module global for currently rendering boundary
boundaryResources: null,
};
}
export type BoundaryResources = Set<StyleResource>;
export function createBoundaryResources(): BoundaryResources {
return new Set();
}
export function mergeBoundaryResources(
target: BoundaryResources,
source: BoundaryResources,
) {
source.forEach(resource => target.add(resource));
}
let currentResources: null | Resources = null;
const currentResourcesStack = [];
export function prepareToRenderResources(resources: Resources) {
currentResourcesStack.push(currentResources);
currentResources = resources;
}
export function finishRenderingResources() {
currentResources = currentResourcesStack.pop();
}
export function setCurrentlyRenderingBoundaryResourcesTarget(
resources: Resources,
boundaryResources: null | BoundaryResources,
) {
resources.boundaryResources = boundaryResources;
}
export const ReactDOMServerDispatcher = {
preload,
preinit,
};
type PreloadAs = ResourceType;
type PreloadOptions = {as: PreloadAs, crossOrigin?: string};
function preload(href: string, options: PreloadOptions) {
if (!currentResources) {
// While we expect that preload calls are primarily going to be observed
// during render because effects and events don't run on the server it is
// still possible that these get called in module scope. This is valid on
// the client since there is still a document to interact with but on the
// server we need a request to associate the call to. Because of this we
// simply return and do not warn.
return;
}
if (__DEV__) {
validatePreloadArguments(href, options);
}
if (
typeof href === 'string' &&
href &&
typeof options === 'object' &&
options !== null
) {
const as = options.as;
let resource = currentResources.preloadsMap.get(href);
if (resource) {
if (__DEV__) {
const originallyImplicit =
(resource: any)._dev_implicit_construction === true;
const latestProps = preloadPropsFromPreloadOptions(href, as, options);
validatePreloadResourceDifference(
resource.props,
originallyImplicit,
latestProps,
false,
);
}
} else {
resource = createPreloadResource(
currentResources,
href,
as,
preloadPropsFromPreloadOptions(href, as, options),
);
}
captureExplicitPreloadResourceDependency(currentResources, resource);
}
}
type PreinitAs = 'style';
type PreinitOptions = {
as: PreinitAs,
precedence?: string,
crossOrigin?: string,
};
function preinit(href: string, options: PreinitOptions) {
if (!currentResources) {
// While we expect that preinit calls are primarily going to be observed
// during render because effects and events don't run on the server it is
// still possible that these get called in module scope. This is valid on
// the client since there is still a document to interact with but on the
// server we need a request to associate the call to. Because of this we
// simply return and do not warn.
return;
}
if (__DEV__) {
validatePreinitArguments(href, options);
}
if (
typeof href === 'string' &&
href &&
typeof options === 'object' &&
options !== null
) {
const as = options.as;
switch (as) {
case 'style': {
const precedence = options.precedence || 'default';
let resource = currentResources.stylesMap.get(href);
if (resource) {
if (__DEV__) {
const latestProps = stylePropsFromPreinitOptions(
href,
precedence,
options,
);
validateStyleResourceDifference(resource.props, latestProps);
}
} else {
const resourceProps = stylePropsFromPreinitOptions(
href,
precedence,
options,
);
resource = createStyleResource(
currentResources,
href,
precedence,
resourceProps,
);
}
// Do not associate preinit style resources with any specific boundary regardless of where it is called
captureStyleResourceDependency(currentResources, null, resource);
return;
}
}
}
}
function preloadPropsFromPreloadOptions(
href: string,
as: ResourceType,
options: PreloadOptions,
): PreloadProps {
return {
href,
rel: 'preload',
as,
crossOrigin: as === 'font' ? '' : options.crossOrigin,
};
}
function preloadPropsFromRawProps(
href: string,
as: ResourceType,
rawProps: Props,
): PreloadProps {
const props: PreloadProps = Object.assign({}, rawProps);
props.href = href;
props.rel = 'preload';
props.as = as;
if (as === 'font') {
// Font preloads always need CORS anonymous mode so we set it here
// regardless of the props provided. This should warn elsewhere in
// dev
props.crossOrigin = '';
}
return props;
}
function preloadAsStylePropsFromProps(
href: string,
props: Props | StyleProps,
): PreloadProps {
return {
rel: 'preload',
as: 'style',
href: href,
crossOrigin: props.crossOrigin,
integrity: props.integrity,
media: props.media,
hrefLang: props.hrefLang,
referrerPolicy: props.referrerPolicy,
};
}
function createPreloadResource(
resources: Resources,
href: string,
as: ResourceType,
props: PreloadProps,
): PreloadResource {
const {preloadsMap} = resources;
if (__DEV__) {
if (preloadsMap.has(href)) {
console.error(
'createPreloadResource was called when a preload Resource matching the same href already exists. This is a bug in React.',
);
}
}
const resource = {
type: 'preload',
as,
href,
flushed: false,
props,
};
preloadsMap.set(href, resource);
return resource;
}
function stylePropsFromRawProps(
href: string,
precedence: string,
rawProps: Props,
): StyleProps {
const props: StyleProps = Object.assign({}, rawProps);
props.href = href;
props.rel = 'stylesheet';
props['data-rprec'] = precedence;
delete props.precedence;
return props;
}
function stylePropsFromPreinitOptions(
href: string,
precedence: string,
options: PreinitOptions,
): StyleProps {
return {
rel: 'stylesheet',
href,
'data-rprec': precedence,
crossOrigin: options.crossOrigin,
};
}
function createStyleResource(
resources: Resources,
href: string,
precedence: string,
props: StyleProps,
): StyleResource {
if (__DEV__) {
if (resources.stylesMap.has(href)) {
console.error(
'createStyleResource was called when a style Resource matching the same href already exists. This is a bug in React.',
);
}
}
const {stylesMap, preloadsMap} = resources;
let hint = preloadsMap.get(href);
if (hint) {
// If a preload for this style Resource already exists there are certain props we want to adopt
// on the style Resource, primarily focussed on making sure the style network pathways utilize
// the preload pathways. For instance if you have diffreent crossOrigin attributes for a preload
// and a stylesheet the stylesheet will make a new request even if the preload had already loaded
const preloadProps = hint.props;
if (props.crossOrigin == null) props.crossOrigin = preloadProps.crossOrigin;
if (props.referrerPolicy == null)
props.referrerPolicy = preloadProps.referrerPolicy;
if (props.media == null) props.media = preloadProps.media;
if (props.title == null) props.title = preloadProps.title;
if (__DEV__) {
validateStyleAndHintProps(
preloadProps,
props,
(hint: any)._dev_implicit_construction,
);
}
} else {
const preloadResourceProps = preloadAsStylePropsFromProps(href, props);
hint = createPreloadResource(
resources,
href,
'style',
preloadResourceProps,
);
if (__DEV__) {
(hint: any)._dev_implicit_construction = true;
}
captureImplicitPreloadResourceDependency(resources, hint);
}
const resource = {
type: 'style',
href,
precedence,
flushed: false,
inShell: false,
props,
hint,
};
stylesMap.set(href, resource);
return resource;
}
function captureStyleResourceDependency(
resources: Resources,
boundaryResources: ?BoundaryResources,
styleResource: StyleResource,
): void {
const {precedences} = resources;
const {precedence} = styleResource;
if (boundaryResources) {
boundaryResources.add(styleResource);
if (!precedences.has(precedence)) {
precedences.set(precedence, new Set());
}
} else {
let set = precedences.get(precedence);
if (!set) {
set = new Set();
precedences.set(precedence, set);
}
set.add(styleResource);
}
}
function captureExplicitPreloadResourceDependency(
resources: Resources,
preloadResource: PreloadResource,
): void {
resources.explicitPreloads.add(preloadResource);
}
function captureImplicitPreloadResourceDependency(
resources: Resources,
preloadResource: PreloadResource,
): void {
resources.implicitPreloads.add(preloadResource);
}
// Construct a resource from link props.
export function resourcesFromLink(props: Props): boolean {
if (!currentResources) {
throw new Error(
'"currentResources" was expected to exist. This is a bug in React.',
);
}
const {rel, href} = props;
if (!href || typeof href !== 'string') {
return false;
}
switch (rel) {
case 'stylesheet': {
const {onLoad, onError, precedence, disabled} = props;
if (
typeof precedence !== 'string' ||
onLoad ||
onError ||
disabled != null
) {
// This stylesheet is either not opted into Resource semantics or has conflicting properties which
// disqualify it for such. We can still create a preload resource to help it load faster on the
// client
if (__DEV__) {
validateLinkPropsForStyleResource(props);
}
let preloadResource = currentResources.preloadsMap.get(href);
if (!preloadResource) {
preloadResource = createPreloadResource(
currentResources,
href,
'style',
preloadAsStylePropsFromProps(href, props),
);
if (__DEV__) {
(preloadResource: any)._dev_implicit_construction = true;
}
}
captureImplicitPreloadResourceDependency(
currentResources,
preloadResource,
);
return false;
} else {
// We are able to convert this link element to a resource exclusively. We construct the relevant Resource
// and return true indicating that this link was fully consumed.
let resource = currentResources.stylesMap.get(href);
if (resource) {
if (__DEV__) {
const resourceProps = stylePropsFromRawProps(
href,
precedence,
props,
);
validateStyleResourceDifference(resource.props, resourceProps);
}
} else {
const resourceProps = stylePropsFromRawProps(href, precedence, props);
resource = createStyleResource(
currentResources,
href,
precedence,
resourceProps,
);
}
captureStyleResourceDependency(
currentResources,
currentResources.boundaryResources,
resource,
);
return true;
}
}
case 'preload': {
const {as, onLoad, onError} = props;
if (onLoad || onError) {
// these props signal an opt-out of Resource semantics. We don't warn because there is no
// conflicting opt-in like there is with Style Resources
return false;
}
switch (as) {
case 'style':
case 'font': {
if (__DEV__) {
validateLinkPropsForPreloadResource(props);
}
let resource = currentResources.preloadsMap.get(href);
if (resource) {
if (__DEV__) {
const originallyImplicit =
(resource: any)._dev_implicit_construction === true;
const latestProps = preloadPropsFromRawProps(href, as, props);
validatePreloadResourceDifference(
resource.props,
originallyImplicit,
latestProps,
false,
);
}
} else {
resource = createPreloadResource(
currentResources,
href,
as,
preloadPropsFromRawProps(href, as, props),
);
}
captureExplicitPreloadResourceDependency(currentResources, resource);
return true;
}
}
return false;
}
}
return false;
}
export function hoistResources(
resources: Resources,
source: BoundaryResources,
): void {
if (resources.boundaryResources) {
mergeBoundaryResources(resources.boundaryResources, source);
source.clear();
}
}
export function hoistResourcesToRoot(
resources: Resources,
boundaryResources: BoundaryResources,
): void {
boundaryResources.forEach(resource => {
// all precedences are set upon discovery. so we know we will have a set here
const set: Set<StyleResource> = (resources.precedences.get(
resource.precedence,
): any);
set.add(resource);
});
boundaryResources.clear();
}

View File

@ -8,6 +8,8 @@
*/
import type {ReactNodeList} from 'shared/ReactTypes';
import type {Resources, BoundaryResources} from './ReactDOMFloatServer';
export type {Resources, BoundaryResources};
import {
checkHtmlStringCoercion,
@ -58,6 +60,36 @@ import hasOwnProperty from 'shared/hasOwnProperty';
import sanitizeURL from '../shared/sanitizeURL';
import isArray from 'shared/isArray';
import {
prepareToRenderResources,
finishRenderingResources,
resourcesFromLink,
ReactDOMServerDispatcher,
} from './ReactDOMFloatServer';
export {
createResources,
createBoundaryResources,
setCurrentlyRenderingBoundaryResourcesTarget,
hoistResources,
hoistResourcesToRoot,
} from './ReactDOMFloatServer';
import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher;
export function prepareToRender(resources: Resources): mixed {
prepareToRenderResources(resources);
const previousHostDispatcher = ReactDOMCurrentDispatcher.current;
ReactDOMCurrentDispatcher.current = ReactDOMServerDispatcher;
return previousHostDispatcher;
}
export function cleanupAfterRender(previousDispatcher: mixed) {
finishRenderingResources();
ReactDOMCurrentDispatcher.current = previousDispatcher;
}
// Used to distinguish these contexts from ones used in other renderers.
// E.g. this can be used to distinguish legacy renderers from this modern one.
export const isPrimaryRenderer = true;
@ -73,7 +105,8 @@ export type ResponseState = {
nextSuspenseID: number,
sentCompleteSegmentFunction: boolean,
sentCompleteBoundaryFunction: boolean,
sentClientRenderFunction: boolean, // We allow the legacy renderer to extend this object.
sentClientRenderFunction: boolean,
sentStyleInsertionFunction: boolean, // We allow the legacy renderer to extend this object.
...
};
@ -183,6 +216,7 @@ export function createResponseState(
sentCompleteSegmentFunction: false,
sentCompleteBoundaryFunction: false,
sentClientRenderFunction: false,
sentStyleInsertionFunction: false,
};
}
@ -1088,6 +1122,26 @@ function pushLink(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
responseState: ResponseState,
textEmbedded: boolean,
): ReactNodeList {
if (enableFloat && resourcesFromLink(props)) {
if (textEmbedded) {
// This link follows text but we aren't writing a tag. while not as efficient as possible we need
// to be safe and assume text will follow by inserting a textSeparator
target.push(textSeparator);
}
// We have converted this link exclusively to a resource and no longer
// need to emit it
return null;
}
return pushLinkImpl(target, props, responseState);
}
function pushLinkImpl(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
responseState: ResponseState,
): ReactNodeList {
const isStylesheet = props.rel === 'stylesheet';
target.push(startChunkForTag('link'));
@ -1106,15 +1160,9 @@ function pushLink(
'use `dangerouslySetInnerHTML`.',
);
case 'precedence': {
if (isStylesheet) {
if (propValue === true || typeof propValue === 'string') {
pushAttribute(target, responseState, 'data-rprec', propValue);
} else if (__DEV__) {
throw new Error(
`the "precedence" prop for links to stylesheets expects to receive a string but received something of type "${typeof propValue}" instead.`,
);
}
break;
if (enableFloat && isStylesheet) {
// precedence is a reversed property for stylesheets to opt-into resource semantcs
continue;
}
// intentionally fall through
}
@ -1270,10 +1318,12 @@ function pushStartHead(
tag: string,
responseState: ResponseState,
): ReactNodeList {
// Preamble type is nullable for feature off cases but is guaranteed when feature is on
target = enableFloat ? preamble : target;
return pushStartGenericElement(target, props, tag, responseState);
return pushStartGenericElement(
enableFloat ? preamble : target,
props,
tag,
responseState,
);
}
function pushStartHtml(
@ -1281,12 +1331,10 @@ function pushStartHtml(
preamble: Array<Chunk | PrecomputedChunk>,
props: Object,
tag: string,
formatContext: FormatContext,
responseState: ResponseState,
formatContext: FormatContext,
): ReactNodeList {
// Preamble type is nullable for feature off cases but is guaranteed when feature is on
target = enableFloat ? preamble : target;
if (formatContext.insertionMode === ROOT_HTML_MODE) {
// If we're rendering the html tag and we're at the root (i.e. not in foreignObject)
// then we also emit the DOCTYPE as part of the root content as a convenience for
@ -1517,6 +1565,7 @@ export function pushStartInstance(
props: Object,
responseState: ResponseState,
formatContext: FormatContext,
textEmbedded: boolean,
): ReactNodeList {
if (__DEV__) {
validateARIAProperties(type, props);
@ -1570,7 +1619,7 @@ export function pushStartInstance(
case 'title':
return pushStartTitle(target, props, responseState);
case 'link':
return pushLink(target, props, responseState);
return pushLink(target, props, responseState, textEmbedded);
// Newline eating tags
case 'listing':
case 'pre': {
@ -1613,8 +1662,8 @@ export function pushStartInstance(
preamble,
props,
type,
formatContext,
responseState,
formatContext,
);
}
default: {
@ -1658,17 +1707,24 @@ export function pushEndInstance(
case 'track':
case 'wbr': {
// No close tag needed.
break;
return;
}
// Postamble end tags
case 'body':
case 'html':
target = enableFloat ? postamble : target;
// Intentional fallthrough
default: {
target.push(endTag1, stringToChunk(type), endTag2);
case 'body': {
if (enableFloat) {
postamble.unshift(endTag1, stringToChunk(type), endTag2);
return;
}
break;
}
case 'html':
if (enableFloat) {
postamble.push(endTag1, stringToChunk(type), endTag2);
return;
}
break;
}
target.push(endTag1, stringToChunk(type), endTag2);
}
export function writeCompletedRoot(
@ -1977,7 +2033,9 @@ export function writeEndSegment(
// const SUSPENSE_END_DATA = '/$';
// const SUSPENSE_PENDING_START_DATA = '$?';
// const SUSPENSE_FALLBACK_START_DATA = '$!';
//
// const LOADED = 'l';
// const ERRORED = 'e';
// function clientRenderBoundary(suspenseBoundaryID, errorDigest, errorMsg, errorComponentStack) {
// // Find the fallback's first element.
// const suspenseIdNode = document.getElementById(suspenseBoundaryID);
@ -2000,14 +2058,100 @@ export function writeEndSegment(
// suspenseNode._reactRetry();
// }
// }
//
// function completeBoundary(suspenseBoundaryID, contentID) {
// // Find the fallback's first element.
// const suspenseIdNode = document.getElementById(suspenseBoundaryID);
// resourceMap = new Map();
// function completeBoundaryWithStyles(suspenseBoundaryID, contentID, styles) {
// const precedences = new Map();
// const thisDocument = document;
// let lastResource, node;
// // Seed the precedence list with existing resources
// let nodes = thisDocument.querySelectorAll('link[data-rprec]');
// for (let i = 0;node = nodes[i++];) {
// precedences.set(node.dataset.rprec, lastResource = node);
// }
// let i = 0;
// let dependencies = [];
// let style, href, precedence, attr, loadingState, resourceEl;
// function setStatus(s) {
// this.s = s;
// }
// while (style = styles[i++]) {
// let j = 0;
// href = style[j++];
// // We check if this resource is already in our resourceMap and reuse it if so.
// // If it is already loaded we don't return it as a depenendency since there is nothing
// // to wait for
// loadingState = resourceMap.get(href);
// if (loadingState) {
// if (loadingState.s !== 'l') {
// dependencies.push(loadingState);
// }
// continue;
// }
// // We construct our new resource element, looping over remaining attributes if any
// // setting them to the Element.
// resourceEl = thisDocument.createElement("link");
// resourceEl.href = href;
// resourceEl.rel = 'stylesheet';
// resourceEl.dataset.rprec = precedence = style[j++];
// while(attr = style[j++]) {
// resourceEl.setAttribute(attr, style[j++]);
// }
// // We stash a pending promise in our map by href which will resolve or reject
// // when the underlying resource loads or errors. We add it to the dependencies
// // array to be returned.
// loadingState = resourceEl._p = new Promise((re, rj) => {
// resourceEl.onload = re;
// resourceEl.onerror = rj;
// })
// loadingState.then(
// setStatus.bind(loadingState, LOADED),
// setStatus.bind(loadingState, ERRORED)
// );
// resourceMap.set(href, loadingState);
// dependencies.push(loadingState);
// // The prior style resource is the last one placed at a given
// // precedence or the last resource itself which may be null.
// // We grab this value and then update the last resource for this
// // precedence to be the inserted element, updating the lastResource
// // pointer if needed.
// let prior = precedences.get(precedence) || lastResource;
// if (prior === lastResource) {
// lastResource = resourceEl
// }
// precedences.set(precedence, resourceEl)
// // Finally, we insert the newly constructed instance at an appropriate location
// // in the Document.
// if (prior) {
// prior.parentNode.insertBefore(resourceEl, prior.nextSibling);
// } else {
// let head = thisDocument.head;
// head.insertBefore(resourceEl, head.firstChild);
// }
// }
// Promise.all(dependencies).then(
// completeBoundary.bind(null, suspenseBoundaryID, contentID, ''),
// completeBoundary.bind(null, suspenseBoundaryID, contentID, "Resource failed to load")
// );
// }
// function completeBoundary(suspenseBoundaryID, contentID, errorDigest) {
// const contentNode = document.getElementById(contentID);
// // We'll detach the content node so that regardless of what happens next we don't leave in the tree.
// // This might also help by not causing recalcing each time we move a child from here to the target.
// contentNode.parentNode.removeChild(contentNode);
// // Find the fallback's first element.
// const suspenseIdNode = document.getElementById(suspenseBoundaryID);
// if (!suspenseIdNode) {
// // The user must have already navigated away from this tree.
// // E.g. because the parent was hydrated. That's fine there's nothing to do
@ -2016,51 +2160,57 @@ export function writeEndSegment(
// }
// // Find the boundary around the fallback. This is always the previous node.
// const suspenseNode = suspenseIdNode.previousSibling;
//
// // Clear all the existing children. This is complicated because
// // there can be embedded Suspense boundaries in the fallback.
// // This is similar to clearSuspenseBoundary in ReactDOMHostConfig.
// // TODO: We could avoid this if we never emitted suspense boundaries in fallback trees.
// // They never hydrate anyway. However, currently we support incrementally loading the fallback.
// const parentInstance = suspenseNode.parentNode;
// let node = suspenseNode.nextSibling;
// let depth = 0;
// do {
// if (node && node.nodeType === COMMENT_NODE) {
// const data = node.data;
// if (data === SUSPENSE_END_DATA) {
// if (depth === 0) {
// break;
// } else {
// depth--;
// if (!errorDigest) {
// // Clear all the existing children. This is complicated because
// // there can be embedded Suspense boundaries in the fallback.
// // This is similar to clearSuspenseBoundary in ReactDOMHostConfig.
// // TODO: We could avoid this if we never emitted suspense boundaries in fallback trees.
// // They never hydrate anyway. However, currently we support incrementally loading the fallback.
// const parentInstance = suspenseNode.parentNode;
// let node = suspenseNode.nextSibling;
// let depth = 0;
// do {
// if (node && node.nodeType === COMMENT_NODE) {
// const data = node.data;
// if (data === SUSPENSE_END_DATA) {
// if (depth === 0) {
// break;
// } else {
// depth--;
// }
// } else if (
// data === SUSPENSE_START_DATA ||
// data === SUSPENSE_PENDING_START_DATA ||
// data === SUSPENSE_FALLBACK_START_DATA
// ) {
// depth++;
// }
// } else if (
// data === SUSPENSE_START_DATA ||
// data === SUSPENSE_PENDING_START_DATA ||
// data === SUSPENSE_FALLBACK_START_DATA
// ) {
// depth++;
// }
// const nextNode = node.nextSibling;
// parentInstance.removeChild(node);
// node = nextNode;
// } while (node);
// const endOfBoundary = node;
// // Insert all the children from the contentNode between the start and end of suspense boundary.
// while (contentNode.firstChild) {
// parentInstance.insertBefore(contentNode.firstChild, endOfBoundary);
// }
//
// const nextNode = node.nextSibling;
// parentInstance.removeChild(node);
// node = nextNode;
// } while (node);
//
// const endOfBoundary = node;
//
// // Insert all the children from the contentNode between the start and end of suspense boundary.
// while (contentNode.firstChild) {
// parentInstance.insertBefore(contentNode.firstChild, endOfBoundary);
// suspenseNode.data = SUSPENSE_START_DATA;
// } else {
// suspenseNode.data = SUSPENSE_FALLBACK_START_DATA;
// suspenseIdNode.setAttribute('data-dgst', errorDigest)
// }
// suspenseNode.data = SUSPENSE_START_DATA;
// if (suspenseNode._reactRetry) {
// suspenseNode._reactRetry();
// }
// }
//
// function completeSegment(containerID, placeholderID) {
// const segmentContainer = document.getElementById(containerID);
// const placeholderNode = document.getElementById(placeholderID);
@ -2080,7 +2230,9 @@ export function writeEndSegment(
const completeSegmentFunction =
'function $RS(a,b){a=document.getElementById(a);b=document.getElementById(b);for(a.parentNode.removeChild(a);a.firstChild;)b.parentNode.insertBefore(a.firstChild,b);b.parentNode.removeChild(b)}';
const completeBoundaryFunction =
'function $RC(a,b){a=document.getElementById(a);b=document.getElementById(b);b.parentNode.removeChild(b);if(a){a=a.previousSibling;var f=a.parentNode,c=a.nextSibling,e=0;do{if(c&&8===c.nodeType){var d=c.data;if("/$"===d)if(0===e)break;else e--;else"$"!==d&&"$?"!==d&&"$!"!==d||e++}d=c.nextSibling;f.removeChild(c);c=d}while(c);for(;b.firstChild;)f.insertBefore(b.firstChild,c);a.data="$";a._reactRetry&&a._reactRetry()}}';
'function $RC(b,c,d){c=document.getElementById(c);c.parentNode.removeChild(c);var a=document.getElementById(b);if(a){b=a.previousSibling;if(d)b.data="$!",a.setAttribute("data-dgst",d);else{d=b.parentNode;a=b.nextSibling;var e=0;do{if(a&&a.nodeType===8){var h=a.data;if(h==="/$")if(0===e)break;else e--;else h!=="$"&&h!=="$?"&&h!=="$!"||e++}h=a.nextSibling;d.removeChild(a);a=h}while(a);for(;c.firstChild;)d.insertBefore(c.firstChild,a);b.data="$"}b._reactRetry&&b._reactRetry()}}';
const styleInsertionFunction =
'$RM=new Map;function $RR(p,q,t){function r(l){this.s=l}for(var m=new Map,n=document,g,e,f=n.querySelectorAll("link[data-rprec]"),d=0;e=f[d++];)m.set(e.dataset.rprec,g=e);e=0;f=[];for(var c,h,b,a;c=t[e++];){var k=0;h=c[k++];if(b=$RM.get(h))"l"!==b.s&&f.push(b);else{a=n.createElement("link");a.href=h;a.rel="stylesheet";for(a.dataset.rprec=d=c[k++];b=c[k++];)a.setAttribute(b,c[k++]);b=a._p=new Promise(function(l,u){a.onload=l;a.onerror=u});b.then(r.bind(b,"l"),r.bind(b,"e"));$RM.set(h,b);f.push(b);c=m.get(d)||g;c===g&&(g=a);m.set(d,a);c?c.parentNode.insertBefore(a,c.nextSibling):(d=n.head,d.insertBefore(a,d.firstChild))}}Promise.all(f).then($RC.bind(null,p,q,""),$RC.bind(null,p,q,"Resource failed to load"))}';
const clientRenderFunction =
'function $RX(b,c,d,e){var a=document.getElementById(b);a&&(b=a.previousSibling,b.data="$!",a=a.dataset,c&&(a.dgst=c),d&&(a.msg=d),e&&(a.stck=e),b._reactRetry&&b._reactRetry())}';
@ -2118,23 +2270,51 @@ const completeBoundaryScript1Full = stringToPrecomputedChunk(
completeBoundaryFunction + ';$RC("',
);
const completeBoundaryScript1Partial = stringToPrecomputedChunk('$RC("');
const completeBoundaryWithStylesScript1FullBoth = stringToPrecomputedChunk(
completeBoundaryFunction + ';' + styleInsertionFunction + ';$RR("',
);
const completeBoundaryWithStylesScript1FullPartial = stringToPrecomputedChunk(
styleInsertionFunction + ';$RR("',
);
const completeBoundaryWithStylesScript1Partial = stringToPrecomputedChunk(
'$RR("',
);
const completeBoundaryScript2 = stringToPrecomputedChunk('","');
const completeBoundaryScript3 = stringToPrecomputedChunk('")</script>');
const completeBoundaryScript2a = stringToPrecomputedChunk('",');
const completeBoundaryScript3 = stringToPrecomputedChunk('"');
const completeBoundaryScript4 = stringToPrecomputedChunk(')</script>');
export function writeCompletedBoundaryInstruction(
destination: Destination,
responseState: ResponseState,
boundaryID: SuspenseBoundaryID,
contentSegmentID: number,
boundaryResources: BoundaryResources,
): boolean {
let hasStyleDependencies;
if (enableFloat) {
hasStyleDependencies = hasStyleResourceDependencies(boundaryResources);
}
writeChunk(destination, responseState.startInlineScript);
if (!responseState.sentCompleteBoundaryFunction) {
// The first time we write this, we'll need to include the full implementation.
responseState.sentCompleteBoundaryFunction = true;
writeChunk(destination, completeBoundaryScript1Full);
if (enableFloat && hasStyleDependencies) {
if (!responseState.sentCompleteBoundaryFunction) {
responseState.sentCompleteBoundaryFunction = true;
responseState.sentStyleInsertionFunction = true;
writeChunk(destination, completeBoundaryWithStylesScript1FullBoth);
} else if (!responseState.sentStyleInsertionFunction) {
responseState.sentStyleInsertionFunction = true;
writeChunk(destination, completeBoundaryWithStylesScript1FullPartial);
} else {
writeChunk(destination, completeBoundaryWithStylesScript1Partial);
}
} else {
// Future calls can just reuse the same function.
writeChunk(destination, completeBoundaryScript1Partial);
if (!responseState.sentCompleteBoundaryFunction) {
responseState.sentCompleteBoundaryFunction = true;
writeChunk(destination, completeBoundaryScript1Full);
} else {
writeChunk(destination, completeBoundaryScript1Partial);
}
}
if (boundaryID === null) {
@ -2148,7 +2328,13 @@ export function writeCompletedBoundaryInstruction(
writeChunk(destination, completeBoundaryScript2);
writeChunk(destination, responseState.segmentPrefix);
writeChunk(destination, formattedContentID);
return writeChunkAndReturn(destination, completeBoundaryScript3);
if (enableFloat && hasStyleDependencies) {
writeChunk(destination, completeBoundaryScript2a);
writeStyleResourceDependencies(destination, boundaryResources);
} else {
writeChunk(destination, completeBoundaryScript3);
}
return writeChunkAndReturn(destination, completeBoundaryScript4);
}
const clientRenderScript1Full = stringToPrecomputedChunk(
@ -2209,10 +2395,10 @@ export function writeClientRenderBoundaryInstruction(
return writeChunkAndReturn(destination, clientRenderScript2);
}
const regexForJSStringsInScripts = /[<\u2028\u2029]/g;
const regexForJSStringsInInstructionScripts = /[<\u2028\u2029]/g;
function escapeJSStringsForInstructionScripts(input: string): string {
const escaped = JSON.stringify(input);
return escaped.replace(regexForJSStringsInScripts, match => {
return escaped.replace(regexForJSStringsInInstructionScripts, match => {
switch (match) {
// santizing breaking out of strings and script tags
case '<':
@ -2230,3 +2416,326 @@ function escapeJSStringsForInstructionScripts(input: string): string {
}
});
}
const regexForJSStringsInScripts = /[&><\u2028\u2029]/g;
function escapeJSObjectForInstructionScripts(input: Object): string {
const escaped = JSON.stringify(input);
return escaped.replace(regexForJSStringsInScripts, match => {
switch (match) {
// santizing breaking out of strings and script tags
case '&':
return '\\u0026';
case '>':
return '\\u003e';
case '<':
return '\\u003c';
case '\u2028':
return '\\u2028';
case '\u2029':
return '\\u2029';
default: {
// eslint-disable-next-line react-internal/prod-error-codes
throw new Error(
'escapeJSObjectForInstructionScripts encountered a match it does not know how to replace. this means the match regex and the replacement characters are no longer in sync. This is a bug in React',
);
}
}
});
}
export function writeInitialResources(
destination: Destination,
resources: Resources,
responseState: ResponseState,
): boolean {
const explicitPreloadsTarget = [];
const remainingTarget = [];
const {precedences, explicitPreloads, implicitPreloads} = resources;
// Flush stylesheets first by earliest precedence
precedences.forEach(precedenceResources => {
precedenceResources.forEach(resource => {
// resources should not already be flushed so we elide this check
pushLinkImpl(remainingTarget, resource.props, responseState);
resource.flushed = true;
resource.inShell = true;
resource.hint.flushed = true;
});
});
explicitPreloads.forEach(resource => {
if (!resource.flushed) {
pushLinkImpl(explicitPreloadsTarget, resource.props, responseState);
resource.flushed = true;
}
});
explicitPreloads.clear();
implicitPreloads.forEach(resource => {
if (!resource.flushed) {
pushLinkImpl(remainingTarget, resource.props, responseState);
resource.flushed = true;
}
});
implicitPreloads.clear();
let i;
let r = true;
for (i = 0; i < explicitPreloadsTarget.length - 1; i++) {
writeChunk(destination, explicitPreloadsTarget[i]);
}
if (i < explicitPreloadsTarget.length) {
r = writeChunkAndReturn(destination, explicitPreloadsTarget[i]);
}
for (i = 0; i < remainingTarget.length - 1; i++) {
writeChunk(destination, remainingTarget[i]);
}
if (i < remainingTarget.length) {
r = writeChunkAndReturn(destination, remainingTarget[i]);
}
return r;
}
export function writeImmediateResources(
destination: Destination,
resources: Resources,
responseState: ResponseState,
): boolean {
const {explicitPreloads, implicitPreloads} = resources;
const target = [];
explicitPreloads.forEach(resource => {
if (!resource.flushed) {
pushLinkImpl(target, resource.props, responseState);
resource.flushed = true;
}
});
explicitPreloads.clear();
implicitPreloads.forEach(resource => {
if (!resource.flushed) {
pushLinkImpl(target, resource.props, responseState);
resource.flushed = true;
}
});
implicitPreloads.clear();
let i = 0;
for (; i < target.length - 1; i++) {
writeChunk(destination, target[i]);
}
if (i < target.length) {
return writeChunkAndReturn(destination, target[i]);
}
return false;
}
function hasStyleResourceDependencies(
boundaryResources: BoundaryResources,
): boolean {
const iter = boundaryResources.values();
// At the moment boundaries only accumulate style resources
// so we assume the type is correct and don't check it
while (true) {
const {value: resource} = iter.next();
if (!resource) break;
// If every style Resource flushed in the shell we do not need to send
// any dependencies
if (!resource.inShell) {
return true;
}
}
return false;
}
const arrayFirstOpenBracket = stringToPrecomputedChunk('[');
const arraySubsequentOpenBracket = stringToPrecomputedChunk(',[');
const arrayInterstitial = stringToPrecomputedChunk(',');
const arrayCloseBracket = stringToPrecomputedChunk(']');
function writeStyleResourceDependencies(
destination: Destination,
boundaryResources: BoundaryResources,
): void {
writeChunk(destination, arrayFirstOpenBracket);
let nextArrayOpenBrackChunk = arrayFirstOpenBracket;
boundaryResources.forEach(resource => {
if (resource.inShell) {
// We can elide this dependency because it was flushed in the shell and
// should be ready before content is shown on the client
} else if (resource.flushed) {
writeChunk(destination, nextArrayOpenBrackChunk);
writeStyleResourceDependencyHrefOnly(destination, resource.href);
writeChunk(destination, arrayCloseBracket);
nextArrayOpenBrackChunk = arraySubsequentOpenBracket;
} else {
writeChunk(destination, nextArrayOpenBrackChunk);
writeStyleResourceDependency(
destination,
resource.href,
resource.precedence,
resource.props,
);
writeChunk(destination, arrayCloseBracket);
nextArrayOpenBrackChunk = arraySubsequentOpenBracket;
resource.flushed = true;
resource.hint.flushed = true;
}
});
writeChunk(destination, arrayCloseBracket);
}
function writeStyleResourceDependencyHrefOnly(
destination: Destination,
href: string,
) {
// We should actually enforce this earlier when the resource is created but for
// now we make sure we are actually dealing with a string here.
if (__DEV__) {
checkAttributeStringCoercion(href, 'href');
}
const coercedHref = '' + (href: any);
writeChunk(
destination,
stringToChunk(escapeJSObjectForInstructionScripts(coercedHref)),
);
}
function writeStyleResourceDependency(
destination: Destination,
href: string,
precedence: string,
props: Object,
) {
if (__DEV__) {
checkAttributeStringCoercion(href, 'href');
}
const coercedHref = '' + (href: any);
sanitizeURL(coercedHref);
writeChunk(
destination,
stringToChunk(escapeJSObjectForInstructionScripts(coercedHref)),
);
if (__DEV__) {
checkAttributeStringCoercion(precedence, 'precedence');
}
const coercedPrecedence = '' + (precedence: any);
writeChunk(destination, arrayInterstitial);
writeChunk(
destination,
stringToChunk(escapeJSObjectForInstructionScripts(coercedPrecedence)),
);
for (const propKey in props) {
if (hasOwnProperty.call(props, propKey)) {
const propValue = props[propKey];
if (propValue == null) {
continue;
}
switch (propKey) {
case 'href':
case 'rel':
case 'precedence':
case 'data-rprec': {
break;
}
case 'children':
case 'dangerouslySetInnerHTML':
throw new Error(
`${'link'} is a self-closing tag and must neither have \`children\` nor ` +
'use `dangerouslySetInnerHTML`.',
);
// eslint-disable-next-line-no-fallthrough
default:
writeStyleResourceAttribute(destination, propKey, propValue);
break;
}
}
}
return null;
}
function writeStyleResourceAttribute(
destination: Destination,
name: string,
value: string | boolean | number | Function | Object, // not null or undefined
): void {
let attributeName = name.toLowerCase();
let attributeValue;
switch (typeof value) {
case 'function':
case 'symbol':
return;
}
switch (name) {
// Reserved names
case 'innerHTML':
case 'dangerouslySetInnerHTML':
case 'suppressContentEditableWarning':
case 'suppressHydrationWarning':
case 'style':
// Ignored
return;
// Attribute renames
case 'className':
attributeName = 'class';
break;
// Booleans
case 'hidden':
if (value === false) {
return;
}
attributeValue = '';
break;
// Santized URLs
case 'src':
case 'href': {
if (__DEV__) {
checkAttributeStringCoercion(value, attributeName);
}
attributeValue = '' + (value: any);
sanitizeURL(attributeValue);
break;
}
default: {
if (!isAttributeNameSafe(name)) {
return;
}
}
}
if (
// shouldIgnoreAttribute
// We have already filtered out null/undefined and reserved words.
name.length > 2 &&
(name[0] === 'o' || name[0] === 'O') &&
(name[1] === 'n' || name[1] === 'N')
) {
return;
}
if (__DEV__) {
checkAttributeStringCoercion(value, attributeName);
}
attributeValue = '' + (value: any);
writeChunk(destination, arrayInterstitial);
writeChunk(
destination,
stringToChunk(escapeJSObjectForInstructionScripts(attributeName)),
);
writeChunk(destination, arrayInterstitial);
writeChunk(
destination,
stringToChunk(escapeJSObjectForInstructionScripts(attributeValue)),
);
}

View File

@ -40,6 +40,7 @@ export type ResponseState = {
sentCompleteSegmentFunction: boolean,
sentCompleteBoundaryFunction: boolean,
sentClientRenderFunction: boolean,
sentStyleInsertionFunction: boolean,
// This is an extra field for the legacy renderer
generateStaticMarkup: boolean,
};
@ -61,6 +62,7 @@ export function createResponseState(
sentCompleteSegmentFunction: responseState.sentCompleteSegmentFunction,
sentCompleteBoundaryFunction: responseState.sentCompleteBoundaryFunction,
sentClientRenderFunction: responseState.sentClientRenderFunction,
sentStyleInsertionFunction: responseState.sentStyleInsertionFunction,
// This is an extra field for the legacy renderer
generateStaticMarkup,
};
@ -74,6 +76,8 @@ export function createRootFormatContext(): FormatContext {
}
export type {
Resources,
BoundaryResources,
FormatContext,
SuspenseBoundaryID,
} from './ReactDOMServerFormatConfig';
@ -96,6 +100,15 @@ export {
writeEndPendingSuspenseBoundary,
writePlaceholder,
writeCompletedRoot,
createResources,
createBoundaryResources,
writeInitialResources,
writeImmediateResources,
hoistResources,
hoistResourcesToRoot,
setCurrentlyRenderingBoundaryResourcesTarget,
prepareToRender,
cleanupAfterRender,
} from './ReactDOMServerFormatConfig';
import {stringToChunk} from 'react-server/src/ReactServerStreamConfig';

View File

@ -0,0 +1,18 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
type DispatcherType = {
[string]: mixed,
};
const Dispatcher: {current: null | DispatcherType} = {
current: null,
};
export default Dispatcher;

View File

@ -0,0 +1,21 @@
import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
export function preinit() {
const dispatcher = ReactDOMSharedInternals.Dispatcher.current;
if (dispatcher) {
dispatcher.preinit.apply(this, arguments);
}
// We don't error because preinit needs to be resilient to being called in a variety of scopes
// and the runtime may not be capable of responding. The function is optimistic and not critical
// so we favor silent bailout over warning or erroring.
}
export function preload() {
const dispatcher = ReactDOMSharedInternals.Dispatcher.current;
if (dispatcher) {
dispatcher.preload.apply(this, arguments);
}
// We don't error because preload needs to be resilient to being called in a variety of scopes
// and the runtime may not be capable of responding. The function is optimistic and not critical
// so we favor silent bailout over warning or erroring.
}

View File

@ -0,0 +1,601 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import hasOwnProperty from 'shared/hasOwnProperty';
type Props = {[string]: mixed};
export function validateUnmatchedLinkResourceProps(
pendingProps: Props,
currentProps: ?Props,
) {
if (__DEV__) {
if (pendingProps.rel !== 'font' && pendingProps.rel !== 'style') {
if (currentProps != null) {
const originalResourceName =
typeof currentProps.href === 'string'
? `Resource with href "${currentProps.href}"`
: 'Resource';
const originalRelStatement = getValueDescriptorExpectingEnumForWarning(
currentProps.rel,
);
const pendingRelStatement = getValueDescriptorExpectingEnumForWarning(
pendingProps.rel,
);
const pendingHrefStatement =
typeof pendingProps.href === 'string'
? ` and the updated href is "${pendingProps.href}"`
: '';
console.error(
'A <link> previously rendered as a %s but was updated with a rel type that is not' +
' valid for a Resource type. Generally Resources are not expected to ever have updated' +
' props however in some limited circumstances it can be valid when changing the href.' +
' When React encounters props that invalidate the Resource it is the same as not rendering' +
' a Resource at all. valid rel types for Resources are "font" and "style". The previous' +
' rel for this instance was %s. The updated rel is %s%s.',
originalResourceName,
originalRelStatement,
pendingRelStatement,
pendingHrefStatement,
);
} else {
const pendingRelStatement = getValueDescriptorExpectingEnumForWarning(
pendingProps.rel,
);
console.error(
'A <link> is rendering as a Resource but has an invalid rel property. The rel encountered is %s.' +
' This is a bug in React.',
pendingRelStatement,
);
}
}
}
}
export function validatePreloadResourceDifference(
originalProps: any,
originalImplicit: boolean,
latestProps: any,
latestImplicit: boolean,
) {
if (__DEV__) {
const {href} = originalProps;
const originalWarningName = getResourceNameForWarning(
'preload',
originalProps,
originalImplicit,
);
const latestWarningName = getResourceNameForWarning(
'preload',
latestProps,
latestImplicit,
);
if (latestProps.as !== originalProps.as) {
console.error(
'A %s is using the same href "%s" as a %s. This is always an error and React will only keep the first preload' +
' for any given href, discarding subsequent instances. To fix, find where you are using this href in link' +
' tags or in calls to ReactDOM.preload() or ReactDOM.preinit() and either make the Resource types agree or' +
' update the hrefs to be distinct for different Resource types.',
latestWarningName,
href,
originalWarningName,
);
} else {
let missingProps = null;
let extraProps = null;
let differentProps = null;
if (originalProps.media != null && latestProps.media == null) {
missingProps = missingProps || {};
missingProps.media = originalProps.media;
}
for (const propName in latestProps) {
const propValue = latestProps[propName];
const originalValue = originalProps[propName];
if (propValue != null && propValue !== originalValue) {
if (originalValue == null) {
extraProps = extraProps || {};
extraProps[propName] = propValue;
} else {
differentProps = differentProps || {};
differentProps[propName] = {
original: originalValue,
latest: propValue,
};
}
}
}
if (missingProps || extraProps || differentProps) {
warnDifferentProps(
href,
originalWarningName,
latestWarningName,
extraProps,
missingProps,
differentProps,
);
}
}
}
}
export function validateStyleResourceDifference(
originalProps: any,
latestProps: any,
) {
if (__DEV__) {
const {href} = originalProps;
// eslint-disable-next-line no-labels
const originalWarningName = getResourceNameForWarning(
'style',
originalProps,
false,
);
const latestWarningName = getResourceNameForWarning(
'style',
latestProps,
false,
);
let missingProps = null;
let extraProps = null;
let differentProps = null;
if (originalProps.media != null && latestProps.media == null) {
missingProps = missingProps || {};
missingProps.media = originalProps.media;
}
for (let propName in latestProps) {
const propValue = latestProps[propName];
const originalValue = originalProps[propName];
if (propValue != null && propValue !== originalValue) {
propName = propName === 'data-rprec' ? 'precedence' : propName;
if (originalValue == null) {
extraProps = extraProps || {};
extraProps[propName] = propValue;
} else {
differentProps = differentProps || {};
differentProps[propName] = {
original: originalValue,
latest: propValue,
};
}
}
}
if (missingProps || extraProps || differentProps) {
warnDifferentProps(
href,
originalWarningName,
latestWarningName,
extraProps,
missingProps,
differentProps,
);
}
}
}
export function validateStyleAndHintProps(
preloadProps: any,
styleProps: any,
implicitPreload: boolean,
) {
if (__DEV__) {
const {href} = preloadProps;
const originalWarningName = getResourceNameForWarning(
'preload',
preloadProps,
implicitPreload,
);
const latestWarningName = getResourceNameForWarning(
'style',
styleProps,
false,
);
if (preloadProps.as !== 'style') {
console.error(
'While creating a %s for href "%s" a %s for this same href was found. When preloading a stylesheet the' +
' "as" prop must be of type "style". This most likely ocurred by rending a preload link with an incorrect' +
' "as" prop or by calling ReactDOM.preload with an incorrect "as" option.',
latestWarningName,
href,
originalWarningName,
);
}
let missingProps = null;
let extraProps = null;
let differentProps = null;
for (const propName in styleProps) {
const styleValue = styleProps[propName];
const preloadValue = preloadProps[propName];
switch (propName) {
// Check for difference on specific props that cross over or influence
// the relationship between the preload and stylesheet
case 'crossOrigin':
case 'referrerPolicy':
case 'media':
case 'title': {
if (
preloadValue !== styleValue &&
!(preloadValue == null && styleValue == null)
) {
if (styleValue == null) {
missingProps = missingProps || {};
missingProps[propName] = preloadValue;
} else if (preloadValue == null) {
extraProps = extraProps || {};
extraProps[propName] = styleValue;
} else {
differentProps = differentProps || {};
differentProps[propName] = {
original: preloadValue,
latest: styleValue,
};
}
}
}
}
}
if (missingProps || extraProps || differentProps) {
warnDifferentProps(
href,
originalWarningName,
latestWarningName,
extraProps,
missingProps,
differentProps,
);
}
}
}
function warnDifferentProps(
href: string,
originalName: string,
latestName: string,
extraProps: ?{[string]: any},
missingProps: ?{[string]: any},
differentProps: ?{[string]: {original: any, latest: any}},
): void {
if (__DEV__) {
const juxtaposedNameStatement =
latestName === originalName
? 'an earlier instance of this Resource'
: `a ${originalName} with the same href`;
let comparisonStatement = '';
if (missingProps !== null && typeof missingProps === 'object') {
for (const propName in missingProps) {
comparisonStatement += `\n ${propName}: missing or null in latest props, "${missingProps[propName]}" in original props`;
}
}
if (extraProps !== null && typeof extraProps === 'object') {
for (const propName in extraProps) {
comparisonStatement += `\n ${propName}: "${extraProps[propName]}" in latest props, missing or null in original props`;
}
}
if (differentProps !== null && typeof differentProps === 'object') {
for (const propName in differentProps) {
comparisonStatement += `\n ${propName}: "${differentProps[propName].latest}" in latest props, "${differentProps[propName].original}" in original props`;
}
}
console.error(
'A %s with href "%s" has props that disagree with those found on %s. Resources always use the props' +
' that were provided the first time they are encountered so any differences will be ignored. Please' +
' update Resources that share an href to have props that agree. The differences are described below.%s',
latestName,
href,
juxtaposedNameStatement,
comparisonStatement,
);
}
}
function getResourceNameForWarning(
type: string,
props: Object,
implicit: boolean,
) {
if (__DEV__) {
switch (type) {
case 'style': {
return 'style Resource';
}
case 'preload': {
if (implicit) {
return `preload for a ${props.as} Resource`;
}
return `preload Resource (as "${props.as}")`;
}
}
}
return 'Resource';
}
export function validateHrefKeyedUpdatedProps(
pendingProps: Props,
currentProps: Props,
): boolean {
if (__DEV__) {
// This function should never be called if we don't have hrefs so we don't bother considering
// Whether they are null or undefined
if (pendingProps.href === currentProps.href) {
// If we have the same href we need all other props to be the same
let missingProps;
let extraProps;
let differentProps;
const allProps = Array.from(
new Set(Object.keys(currentProps).concat(Object.keys(pendingProps))),
);
for (let i = 0; i < allProps.length; i++) {
const propName = allProps[i];
const pendingValue = pendingProps[propName];
const currentValue = currentProps[propName];
if (
pendingValue !== currentValue &&
!(pendingValue == null && currentValue == null)
) {
if (pendingValue == null) {
missingProps = missingProps || {};
missingProps[propName] = currentValue;
} else if (currentValue == null) {
extraProps = extraProps || {};
extraProps[propName] = pendingValue;
} else {
differentProps = differentProps || {};
differentProps[propName] = {
original: currentValue,
latest: pendingValue,
};
}
}
}
if (missingProps || extraProps || differentProps) {
const latestWarningName = getResourceNameForWarning(
'style',
currentProps,
false,
);
let comparisonStatement = '';
if (missingProps !== null && typeof missingProps === 'object') {
for (const propName in missingProps) {
comparisonStatement += `\n ${propName}: missing or null in latest props, "${missingProps[propName]}" in original props`;
}
}
if (extraProps !== null && typeof extraProps === 'object') {
for (const propName in extraProps) {
comparisonStatement += `\n ${propName}: "${extraProps[propName]}" in latest props, missing or null in original props`;
}
}
if (differentProps !== null && typeof differentProps === 'object') {
for (const propName in differentProps) {
comparisonStatement += `\n ${propName}: "${differentProps[propName].latest}" in latest props, "${differentProps[propName].original}" in original props`;
}
}
console.error(
'A %s with href "%s" recieved new props with different values from the props used' +
' when this Resource was first rendered. React will only use the props provided when' +
' this resource was first rendered until a new href is provided. Unlike conventional' +
' DOM elements, Resources instances do not have a one to one correspondence with Elements' +
' in the DOM and as such, every instance of a Resource for a single Resource identifier' +
' (href) must have props that agree with each other. The differences are described below.%s',
latestWarningName,
currentProps.href,
comparisonStatement,
);
return true;
}
}
}
return false;
}
export function validateLinkPropsForStyleResource(props: Props): boolean {
if (__DEV__) {
// This should only be called when we know we are opting into Resource semantics (i.e. precedence is not null)
const {href, onLoad, onError, disabled} = props;
const allProps = ['onLoad', 'onError', 'disabled'];
const includedProps = [];
if (onLoad) includedProps.push('onLoad');
if (onError) includedProps.push('onError');
if (disabled != null) includedProps.push('disabled');
const allPropsUnionPhrase = propNamesListJoin(allProps, 'or');
let includedPropsPhrase = propNamesListJoin(includedProps, 'and');
includedPropsPhrase += includedProps.length === 1 ? ' prop' : ' props';
if (includedProps.length) {
console.error(
'A link (rel="stylesheet") element with href "%s" has the precedence prop but also included the %s.' +
' When using %s React will opt out of Resource behavior. If you meant for this' +
' element to be treated as a Resource remove the %s. Otherwise remove the precedence prop.',
href,
includedPropsPhrase,
allPropsUnionPhrase,
includedPropsPhrase,
);
return true;
}
}
return false;
}
function propNamesListJoin(
list: Array<string>,
combinator: 'and' | 'or',
): string {
switch (list.length) {
case 0:
return '';
case 1:
return list[0];
case 2:
return list[0] + ' ' + combinator + ' ' + list[1];
default:
return (
list.slice(0, -1).join(', ') +
', ' +
combinator +
' ' +
list[list.length - 1]
);
}
}
export function validateLinkPropsForPreloadResource(linkProps: any) {
if (__DEV__) {
const {href, as} = linkProps;
if (as === 'font') {
const name = getResourceNameForWarning('preload', linkProps, false);
if (!hasOwnProperty.call(linkProps, 'crossOrigin')) {
console.error(
'A %s with href "%s" did not specify the crossOrigin prop. Font preloads must always use' +
' anonymouse CORS mode. To fix add an empty string, "anonymous", or any other string' +
' value except "use-credentials" for the crossOrigin prop of all font preloads.',
name,
href,
);
} else if (linkProps.crossOrigin === 'use-credentials') {
console.error(
'A %s with href "%s" specified a crossOrigin value of "use-credentials". Font preloads must always use' +
' anonymouse CORS mode. To fix use an empty string, "anonymous", or any other string' +
' value except "use-credentials" for the crossOrigin prop of all font preloads.',
name,
href,
);
}
}
}
}
export function validatePreloadArguments(href: mixed, options: mixed) {
if (__DEV__) {
if (!href || typeof href !== 'string') {
const typeOfArg = getValueDescriptorExpectingObjectForWarning(href);
console.error(
'ReactDOM.preload() expected the first argument to be a string representing an href but found %s instead.',
typeOfArg,
);
} else if (typeof options !== 'object' || options === null) {
const typeOfArg = getValueDescriptorExpectingObjectForWarning(options);
console.error(
'ReactDOM.preload() expected the second argument to be an options argument containing at least an "as" property' +
' specifying the Resource type. It found %s instead. The href for the preload call where this warning originated is "%s".',
typeOfArg,
href,
);
} else {
const as = options.as;
switch (as) {
// Font specific validation of options
case 'font': {
if (options.crossOrigin === 'use-credentials') {
console.error(
'ReactDOM.preload() was called with an "as" type of "font" and with a "crossOrigin" option of "use-credentials".' +
' Fonts preloading must use crossOrigin "anonymous" to be functional. Please update your font preload to omit' +
' the crossOrigin option or change it to any other value than "use-credentials" (Browsers default all other values' +
' to anonymous mode). The href for the preload call where this warning originated is "%s"',
href,
);
}
break;
}
case 'style': {
break;
}
// We have an invalid as type and need to warn
default: {
const typeOfAs = getValueDescriptorExpectingEnumForWarning(as);
console.error(
'ReactDOM.preload() expected a valid "as" type in the options (second) argument but found %s instead.' +
' Please use one of the following valid values instead: %s. The href for the preload call where this' +
' warning originated is "%s".',
typeOfAs,
'"style" and "font"',
href,
);
}
}
}
}
}
export function validatePreinitArguments(href: mixed, options: mixed) {
if (__DEV__) {
if (!href || typeof href !== 'string') {
const typeOfArg = getValueDescriptorExpectingObjectForWarning(href);
console.error(
'ReactDOM.preinit() expected the first argument to be a string representing an href but found %s instead.',
typeOfArg,
);
} else if (typeof options !== 'object' || options === null) {
const typeOfArg = getValueDescriptorExpectingObjectForWarning(options);
console.error(
'ReactDOM.preinit() expected the second argument to be an options argument containing at least an "as" property' +
' specifying the Resource type. It found %s instead. The href for the preload call where this warning originated is "%s".',
typeOfArg,
href,
);
} else {
const as = options.as;
switch (as) {
case 'font':
case 'style': {
break;
}
// We have an invalid as type and need to warn
default: {
const typeOfAs = getValueDescriptorExpectingEnumForWarning(as);
console.error(
'ReactDOM.preinit() expected the second argument to be an options argument containing at least an "as" property' +
' specifying the Resource type. It found %s instead. Currently, the only valid resource type for preinit is "style".' +
' The href for the preinit call where this warning originated is "%s".',
typeOfAs,
href,
);
}
}
}
}
}
function getValueDescriptorExpectingObjectForWarning(thing: any): string {
return thing === null
? 'null'
: thing === undefined
? 'undefined'
: thing === ''
? 'an empty string'
: `something with type "${typeof thing}"`;
}
function getValueDescriptorExpectingEnumForWarning(thing: any): string {
return thing === null
? 'null'
: thing === undefined
? 'undefined'
: thing === ''
? 'an empty string'
: typeof thing === 'string'
? JSON.stringify(thing)
: `something with type "${typeof thing}"`;
}

View File

@ -22,3 +22,5 @@ export {
unstable_runWithPriority, // DO NOT USE: Temporarily exposed to migrate off of Scheduler.runWithPriority.
version,
} from './src/client/ReactDOM';
export {preinit, preload} from 'react-dom-bindings/src/shared/ReactDOMFloat';

View File

@ -7,29 +7,20 @@
* @flow
*/
import {batchedUpdates} from 'react-reconciler/src/ReactFiberReconciler';
import {
enqueueStateRestore,
restoreStateIfNeeded,
} from 'react-dom-bindings/src/events/ReactDOMControlledComponent';
import {
getInstanceFromNode,
getNodeFromInstance,
getFiberCurrentPropsFromNode,
} from 'react-dom-bindings/src/client/ReactDOMComponentTree';
const Internals = {
usingClientEntryPoint: false,
// Keep in sync with ReactTestUtils.js.
// This is an array for better minification.
Events: [
getInstanceFromNode,
getNodeFromInstance,
getFiberCurrentPropsFromNode,
enqueueStateRestore,
restoreStateIfNeeded,
batchedUpdates,
],
type InternalsType = {
usingClientEntryPoint: boolean,
Events: [any, any, any, any, any, any],
Dispatcher: {
current: mixed,
},
};
const Internals: InternalsType = ({
usingClientEntryPoint: false,
Events: null,
Dispatcher: {
current: null,
},
}: any);
export default Internals;

View File

@ -329,6 +329,7 @@ describe('ReactDOMFizzServer', () => {
);
pipe(writable);
});
expect(getVisibleChildren(container)).toEqual(
<div>
<div>Loading...</div>
@ -4319,70 +4320,60 @@ describe('ReactDOMFizzServer', () => {
});
// @gate enableFloat
it('emits html and head start tags (the preamble) before other content if rendered in the shell', async () => {
it('can emit the preamble even if the head renders asynchronously', async () => {
function AsyncNoOutput() {
readText('nooutput');
return null;
}
function AsyncHead() {
readText('head');
return (
<head data-foo="foo">
<title>a title</title>
</head>
);
}
function AsyncBody() {
readText('body');
return (
<body data-bar="bar">
<link rel="preload" as="style" href="foo" />
hello
</body>
);
}
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<>
<title data-baz="baz">a title</title>
<html data-foo="foo">
<head data-bar="bar" />
<body>a body</body>
</html>
</>,
<html data-html="html">
<AsyncNoOutput />
<AsyncHead />
<AsyncBody />
</html>,
);
pipe(writable);
});
await actIntoEmptyDocument(() => {
resolveText('body');
});
await actIntoEmptyDocument(() => {
resolveText('nooutput');
});
// We need to use actIntoEmptyDocument because act assumes that buffered
// content should be fake streamed into the body which is normally true
// but in this test the entire shell was delayed and we need the initial
// construction to be done to get the parsing right
await actIntoEmptyDocument(() => {
resolveText('head');
});
expect(getVisibleChildren(document)).toEqual(
<html data-foo="foo">
<head data-bar="bar">
<title data-baz="baz">a title</title>
<html data-html="html">
<head data-foo="foo">
<link rel="preload" as="style" href="foo" />
<title>a title</title>
</head>
<body>a body</body>
<body data-bar="bar">hello</body>
</html>,
);
// Hydrate the same thing on the client. We expect this to still fail because <title> is not a Resource
// and is unmatched on hydration
const errors = [];
ReactDOMClient.hydrateRoot(
document,
<>
<title data-baz="baz">a title</title>
<html data-foo="foo">
<head data-bar="bar" />
<body>a body</body>
</html>
</>,
{
onRecoverableError: (err, errInfo) => {
errors.push(err.message);
},
},
);
expect(() => {
try {
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toThrow('Invalid insertion of HTML node in #document node.');
} catch (e) {
console.log('e', e);
}
}).toErrorDev(
[
'Warning: Expected server HTML to contain a matching <title> in <#document>.',
'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.',
'Warning: validateDOMNesting(...): <title> cannot appear as a child of <#document>',
],
{withoutStack: 1},
);
expect(errors).toEqual([
'Hydration failed because the initial UI does not match what was rendered on the server.',
'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
]);
expect(getVisibleChildren(document)).toEqual();
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toThrow('The node to be removed is not a child of this node.');
});
// @gate enableFloat
@ -4431,267 +4422,6 @@ describe('ReactDOMFizzServer', () => {
expect(chunks.pop()).toEqual('</body></html>');
});
// @gate enableFloat
it('recognizes stylesheet links as attributes during hydration', async () => {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<>
<link rel="stylesheet" href="foo" precedence="default" />
<html>
<head>
<link rel="author" precedence="this is a nonsense prop" />
</head>
<body>a body</body>
</html>
</>,
);
pipe(writable);
});
// precedence for stylesheets is mapped to a valid data attribute that is recognized on the client
// as opting this node into resource semantics. the use of precedence on the author link is just a
// non standard attribute which React allows but is not given any special treatment.
expect(getVisibleChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="foo" data-rprec="default" />
<link rel="author" precedence="this is a nonsense prop" />
</head>
<body>a body</body>
</html>,
);
// It hydrates successfully
const root = ReactDOMClient.hydrateRoot(
document,
<>
<link rel="stylesheet" href="foo" precedence="default" />
<html>
<head>
<link rel="author" precedence="this is a nonsense prop" />
</head>
<body>a body</body>
</html>
</>,
);
// We manually capture uncaught errors b/c Jest does not play well with errors thrown in
// microtasks after the test completes even when it is expecting to fail (e.g. when the gate is false)
// We need to flush the scheduler at the end even if there was an earlier throw otherwise this test will
// fail even when failure is expected. This is primarily caused by invokeGuardedCallback replaying commit
// phase errors which get rethrown in a microtask
const uncaughtErrors = [];
try {
expect(Scheduler).toFlushWithoutYielding();
expect(getVisibleChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="foo" data-rprec="default" />
<link rel="author" precedence="this is a nonsense prop" />
</head>
<body>a body</body>
</html>,
);
} catch (e) {
uncaughtErrors.push(e);
}
try {
expect(Scheduler).toFlushWithoutYielding();
} catch (e) {
uncaughtErrors.push(e);
}
root.render(
<>
<link rel="stylesheet" href="foo" precedence="default" data-bar="bar" />
<html>
<head />
<body>a body</body>
</html>
</>,
);
try {
expect(Scheduler).toFlushWithoutYielding();
expect(getVisibleChildren(document)).toEqual(
<html>
<head>
<link
rel="stylesheet"
href="foo"
data-rprec="default"
data-bar="bar"
/>
</head>
<body>a body</body>
</html>,
);
} catch (e) {
uncaughtErrors.push(e);
}
try {
expect(Scheduler).toFlushWithoutYielding();
} catch (e) {
uncaughtErrors.push(e);
}
if (uncaughtErrors.length > 0) {
throw uncaughtErrors[0];
}
});
// Temporarily this test is expected to fail everywhere. When we have resource hoisting
// it should start to pass and we can adjust the gate accordingly
// @gate false && enableFloat
it('should insert missing resources during hydration', async () => {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<body>foo</body>
</html>,
);
pipe(writable);
});
const uncaughtErrors = [];
ReactDOMClient.hydrateRoot(
document,
<>
<link rel="stylesheet" href="foo" precedence="foo" />
<html>
<head />
<body>foo</body>
</html>
</>,
);
try {
expect(Scheduler).toFlushWithoutYielding();
expect(getVisibleChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="foo" precedence="foo" />
</head>
<body>foo</body>
</html>,
);
} catch (e) {
uncaughtErrors.push(e);
}
// need to flush again to get the invoke guarded callback error to throw in microtask
try {
expect(Scheduler).toFlushWithoutYielding();
} catch (e) {
uncaughtErrors.push(e);
}
if (uncaughtErrors.length) {
throw uncaughtErrors[0];
}
});
// @gate experimental && enableFloat
it('fail hydration if a suitable resource cannot be found in the DOM for a given location (href)', async () => {
gate(flags => {
if (!(__EXPERIMENTAL__ && flags.enableFloat)) {
throw new Error('bailing out of test');
}
});
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head />
<body>a body</body>
</html>,
);
pipe(writable);
});
const errors = [];
ReactDOMClient.hydrateRoot(
document,
<html>
<head>
<link rel="stylesheet" href="foo" precedence="low" />
</head>
<body>a body</body>
</html>,
{
onRecoverableError(err, errInfo) {
errors.push(err.message);
},
},
);
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toErrorDev(
[
'Warning: A matching Hydratable Resource was not found in the DOM for <link rel="stylesheet" href="foo">',
'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.',
],
{withoutStack: 1},
);
expect(errors).toEqual([
'Hydration failed because the initial UI does not match what was rendered on the server.',
'Hydration failed because the initial UI does not match what was rendered on the server.',
'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
]);
});
// @gate experimental && enableFloat
it('should error in dev when rendering more than one resource for a given location (href)', async () => {
gate(flags => {
if (!(__EXPERIMENTAL__ && flags.enableFloat)) {
throw new Error('bailing out of test');
}
});
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<>
<link rel="stylesheet" href="foo" precedence="low" />
<link rel="stylesheet" href="foo" precedence="high" />
<html>
<head />
<body>a body</body>
</html>
</>,
);
pipe(writable);
});
expect(getVisibleChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="foo" data-rprec="low" />
<link rel="stylesheet" href="foo" data-rprec="high" />
</head>
<body>a body</body>
</html>,
);
const errors = [];
ReactDOMClient.hydrateRoot(
document,
<>
<html>
<head>
<link rel="stylesheet" href="foo" precedence="low" />
<link rel="stylesheet" href="foo" precedence="high" />
</head>
<body>a body</body>
</html>
</>,
{
onRecoverableError(err, errInfo) {
errors.push(err.message);
},
},
);
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toErrorDev([
'Warning: Stylesheet resources need a unique representation in the DOM while hydrating and more than one matching DOM Node was found. To fix, ensure you are only rendering one stylesheet link with an href attribute of "foo"',
'Warning: Stylesheet resources need a unique representation in the DOM while hydrating and more than one matching DOM Node was found. To fix, ensure you are only rendering one stylesheet link with an href attribute of "foo"',
]);
expect(errors).toEqual([]);
});
describe('text separators', () => {
// To force performWork to start before resolving AsyncText but before piping we need to wait until
// after scheduleWork which currently uses setImmediate to delay performWork

File diff suppressed because it is too large Load Diff

View File

@ -50,7 +50,12 @@ import {canUseDOM} from 'shared/ExecutionEnvironment';
import ReactVersion from 'shared/ReactVersion';
import {enableNewReconciler} from 'shared/ReactFeatureFlags';
import {getClosestInstanceFromNode} from 'react-dom-bindings/src/client/ReactDOMComponentTree';
import {
getClosestInstanceFromNode,
getInstanceFromNode,
getNodeFromInstance,
getFiberCurrentPropsFromNode,
} from 'react-dom-bindings/src/client/ReactDOMComponentTree';
import {restoreControlledState} from 'react-dom-bindings/src/client/ReactDOMComponent';
import {
setAttemptSynchronousHydration,
@ -61,7 +66,11 @@ import {
setAttemptHydrationAtPriority,
} from 'react-dom-bindings/src/events/ReactDOMEventReplaying';
import {setBatchingImplementation} from 'react-dom-bindings/src/events/ReactDOMUpdateBatching';
import {setRestoreImplementation} from 'react-dom-bindings/src/events/ReactDOMControlledComponent';
import {
setRestoreImplementation,
enqueueStateRestore,
restoreStateIfNeeded,
} from 'react-dom-bindings/src/events/ReactDOMControlledComponent';
import Internals from '../ReactDOMSharedInternals';
setAttemptSynchronousHydration(attemptSynchronousHydration);
@ -198,6 +207,17 @@ export {
runWithPriority as unstable_runWithPriority,
};
// Keep in sync with ReactTestUtils.js.
// This is an array for better minification.
Internals.Events = [
getInstanceFromNode,
getNodeFromInstance,
getFiberCurrentPropsFromNode,
enqueueStateRestore,
restoreStateIfNeeded,
batchedUpdates,
];
const foundDevTools = injectIntoDevTools({
findFiberByHostInstance: getClosestInstanceFromNode,
bundleType: __DEV__ ? 1 : 0,

View File

@ -13,6 +13,9 @@ import type {
TransitionTracingCallbacks,
} from 'react-reconciler/src/ReactInternalTypes';
import ReactDOMSharedInternals from '../ReactDOMSharedInternals';
const {Dispatcher} = ReactDOMSharedInternals;
import {ReactDOMClientDispatcher} from 'react-dom-bindings/src/client/ReactDOMFloatClient';
import {queueExplicitHydrationTarget} from 'react-dom-bindings/src/events/ReactDOMEventReplaying';
import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
import {enableFloat} from 'shared/ReactFeatureFlags';
@ -235,6 +238,10 @@ export function createRoot(
);
markContainerAsRoot(root.current, container);
if (enableFloat) {
// Set the default dispatcher to the client dispatcher
Dispatcher.current = ReactDOMClientDispatcher;
}
const rootContainerElement: Document | Element | DocumentFragment =
container.nodeType === COMMENT_NODE
? (container.parentNode: any)
@ -319,6 +326,10 @@ export function hydrateRoot(
transitionCallbacks,
);
markContainerAsRoot(root.current, container);
if (enableFloat) {
// Set the default dispatcher to the client dispatcher
Dispatcher.current = ReactDOMClientDispatcher;
}
// This can't be a comment node since hydration doesn't work on comment nodes anyway.
listenToAllSupportedEvents(container);

View File

@ -13,6 +13,7 @@ import {
ClassComponent,
FunctionComponent,
HostComponent,
HostResource,
HostText,
} from 'react-reconciler/src/ReactWorkTags';
import {SyntheticEvent} from 'react-dom-bindings/src/events/SyntheticEvent';
@ -21,6 +22,7 @@ import {
rethrowCaughtError,
invokeGuardedCallbackAndCatchFirstError,
} from 'shared/ReactErrorUtils';
import {enableFloat} from 'shared/ReactFeatureFlags';
import assign from 'shared/assign';
import isArray from 'shared/isArray';
@ -59,7 +61,8 @@ function findAllInRenderedFiberTreeInternal(fiber, test) {
node.tag === HostComponent ||
node.tag === HostText ||
node.tag === ClassComponent ||
node.tag === FunctionComponent
node.tag === FunctionComponent ||
(enableFloat ? node.tag === HostResource : false)
) {
const publicInst = node.stateNode;
if (test(publicInst)) {

View File

@ -323,6 +323,7 @@ export * from 'react-reconciler/src/ReactFiberHostConfigWithNoHydration';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoScopes';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoTestSelectors';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoMicrotasks';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoResources';
export function appendInitialChild(
parentInstance: Instance,
@ -615,3 +616,11 @@ export function detachDeletedInstance(node: Instance): void {
export function requestPostPaintCallback(callback: (time: number) => void) {
// noop
}
export function prepareRendererToRender(container: Container): void {
// noop
}
export function resetRendererAfterRender() {
// noop
}

View File

@ -89,6 +89,7 @@ export * from 'react-reconciler/src/ReactFiberHostConfigWithNoHydration';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoScopes';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoTestSelectors';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoMicrotasks';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoResources';
export function appendInitialChild(
parentInstance: Instance,
@ -514,3 +515,11 @@ export function detachDeletedInstance(node: Instance): void {
export function requestPostPaintCallback(callback: (time: number) => void) {
// noop
}
export function prepareRendererToRender(container: Container): void {
// noop
}
export function resetRendererAfterRender(): void {
// noop
}

View File

@ -57,6 +57,9 @@ SUSPENSE_UPDATE_TO_COMPLETE[0] = SUSPENSE_UPDATE_TO_COMPLETE_TAG;
const SUSPENSE_UPDATE_TO_CLIENT_RENDER = new Uint8Array(1);
SUSPENSE_UPDATE_TO_CLIENT_RENDER[0] = SUSPENSE_UPDATE_TO_CLIENT_RENDER_TAG;
export type Resources = void;
export type BoundaryResources = void;
// Per response,
export type ResponseState = {
nextSuspenseID: number,
@ -142,6 +145,7 @@ export function pushStartInstance(
props: Object,
responseState: ResponseState,
formatContext: FormatContext,
textEmbedded: boolean,
): ReactNodeList {
target.push(
INSTANCE,
@ -291,6 +295,7 @@ export function writeCompletedBoundaryInstruction(
responseState: ResponseState,
boundaryID: SuspenseBoundaryID,
contentSegmentID: number,
resources: BoundaryResources,
): boolean {
writeChunk(destination, SUSPENSE_UPDATE_TO_COMPLETE);
writeChunk(destination, formatID(boundaryID));
@ -309,3 +314,38 @@ export function writeClientRenderBoundaryInstruction(
writeChunk(destination, SUSPENSE_UPDATE_TO_CLIENT_RENDER);
return writeChunkAndReturn(destination, formatID(boundaryID));
}
export function writeInitialResources(
destination: Destination,
resources: Resources,
responseState: ResponseState,
): boolean {
return true;
}
export function writeImmediateResources(
destination: Destination,
resources: Resources,
responseState: ResponseState,
): boolean {
return true;
}
export function hoistResources(
resources: Resources,
boundaryResources: BoundaryResources,
) {}
export function hoistResourcesToRoot(
resources: Resources,
boundaryResources: BoundaryResources,
) {}
export function prepareToRender(resources: Resources) {}
export function cleanupAfterRender(previousDispatcher: mixed) {}
export function createResources() {}
export function createBoundaryResources() {}
export function setCurrentlyRenderingBoundaryResourcesTarget(
resources: Resources,
boundaryResources: ?BoundaryResources,
) {}

View File

@ -51,6 +51,9 @@ type Destination = {
stack: Array<Segment | Instance | SuspenseInstance>,
};
type Resources = null;
type BoundaryResources = null;
const POP = Buffer.from('/', 'utf8');
function write(destination: Destination, buffer: Uint8Array): void {
@ -263,6 +266,22 @@ const ReactNoopServer = ReactFizzServer({
): boolean {
boundary.status = 'client-render';
},
writeInitialResources() {},
writeImmediateResources() {},
createResources(): Resources {
return null;
},
createBoundaryResources(): BoundaryResources {
return null;
},
setCurrentlyRenderingBoundaryResourcesTarget(resources: BoundaryResources) {},
prepareToRender() {},
cleanupAfterRender() {},
});
type Options = {

View File

@ -478,6 +478,8 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
const endTime = Scheduler.unstable_now();
callback(endTime);
},
prepareRendererToRender() {},
resetRendererAfterRender() {},
};
const hostConfig = useMutation

View File

@ -21,6 +21,7 @@ import type {
} from './ReactFiberOffscreenComponent';
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.new';
import {supportsResources, isHostResourceType} from './ReactFiberHostConfig';
import {
createRootStrictEffectsByDefault,
enableCache,
@ -32,6 +33,7 @@ import {
allowConcurrentByDefault,
enableTransitionTracing,
enableDebugTracing,
enableFloat,
} from 'shared/ReactFeatureFlags';
import {NoFlags, Placement, StaticMask} from './ReactFiberFlags';
import {ConcurrentRoot} from './ReactRootTags';
@ -42,6 +44,7 @@ import {
HostComponent,
HostText,
HostPortal,
HostResource,
ForwardRef,
Fragment,
Mode,
@ -494,7 +497,13 @@ export function createFiberFromTypeAndProps(
}
}
} else if (typeof type === 'string') {
fiberTag = HostComponent;
if (enableFloat && supportsResources) {
fiberTag = isHostResourceType(type, pendingProps)
? HostResource
: HostComponent;
} else {
fiberTag = HostComponent;
}
} else {
getTag: switch (type) {
case REACT_FRAGMENT_TYPE:

View File

@ -21,6 +21,7 @@ import type {
} from './ReactFiberOffscreenComponent';
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.old';
import {supportsResources, isHostResourceType} from './ReactFiberHostConfig';
import {
createRootStrictEffectsByDefault,
enableCache,
@ -32,6 +33,7 @@ import {
allowConcurrentByDefault,
enableTransitionTracing,
enableDebugTracing,
enableFloat,
} from 'shared/ReactFeatureFlags';
import {NoFlags, Placement, StaticMask} from './ReactFiberFlags';
import {ConcurrentRoot} from './ReactRootTags';
@ -42,6 +44,7 @@ import {
HostComponent,
HostText,
HostPortal,
HostResource,
ForwardRef,
Fragment,
Mode,
@ -494,7 +497,13 @@ export function createFiberFromTypeAndProps(
}
}
} else if (typeof type === 'string') {
fiberTag = HostComponent;
if (enableFloat && supportsResources) {
fiberTag = isHostResourceType(type, pendingProps)
? HostResource
: HostComponent;
} else {
fiberTag = HostComponent;
}
} else {
getTag: switch (type) {
case REACT_FRAGMENT_TYPE:

View File

@ -40,6 +40,7 @@ import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.new
import {
enableCPUSuspense,
enableUseMutableSource,
enableFloat,
} from 'shared/ReactFeatureFlags';
import checkPropTypes from 'shared/checkPropTypes';
@ -54,6 +55,7 @@ import {
ClassComponent,
HostRoot,
HostComponent,
HostResource,
HostText,
HostPortal,
ForwardRef,
@ -161,7 +163,9 @@ import {
getSuspenseInstanceFallbackErrorDetails,
registerSuspenseInstanceRetry,
supportsHydration,
supportsResources,
isPrimaryRenderer,
getResource,
} from './ReactFiberHostConfig';
import type {SuspenseInstance} from './ReactFiberHostConfig';
import {shouldError, shouldSuspend} from './ReactFiberReconciler';
@ -1580,6 +1584,24 @@ function updateHostComponent(
return workInProgress.child;
}
function updateHostResource(current, workInProgress, renderLanes) {
pushHostContext(workInProgress);
markRef(current, workInProgress);
const currentProps = current === null ? null : current.memoizedProps;
workInProgress.memoizedState = getResource(
workInProgress.type,
workInProgress.pendingProps,
currentProps,
);
reconcileChildren(
current,
workInProgress,
workInProgress.pendingProps.children,
renderLanes,
);
return workInProgress.child;
}
function updateHostText(current, workInProgress) {
if (current === null) {
tryToClaimNextHydratableInstance(workInProgress);
@ -3651,6 +3673,7 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
}
resetHydrationState();
break;
case HostResource:
case HostComponent:
pushHostContext(workInProgress);
break;
@ -3985,6 +4008,11 @@ function beginWork(
}
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
case HostResource:
if (enableFloat && supportsResources) {
return updateHostResource(current, workInProgress, renderLanes);
}
// eslint-disable-next-line no-fallthrough
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes);
case HostText:

View File

@ -40,6 +40,7 @@ import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.old
import {
enableCPUSuspense,
enableUseMutableSource,
enableFloat,
} from 'shared/ReactFeatureFlags';
import checkPropTypes from 'shared/checkPropTypes';
@ -54,6 +55,7 @@ import {
ClassComponent,
HostRoot,
HostComponent,
HostResource,
HostText,
HostPortal,
ForwardRef,
@ -161,7 +163,9 @@ import {
getSuspenseInstanceFallbackErrorDetails,
registerSuspenseInstanceRetry,
supportsHydration,
supportsResources,
isPrimaryRenderer,
getResource,
} from './ReactFiberHostConfig';
import type {SuspenseInstance} from './ReactFiberHostConfig';
import {shouldError, shouldSuspend} from './ReactFiberReconciler';
@ -1580,6 +1584,24 @@ function updateHostComponent(
return workInProgress.child;
}
function updateHostResource(current, workInProgress, renderLanes) {
pushHostContext(workInProgress);
markRef(current, workInProgress);
const currentProps = current === null ? null : current.memoizedProps;
workInProgress.memoizedState = getResource(
workInProgress.type,
workInProgress.pendingProps,
currentProps,
);
reconcileChildren(
current,
workInProgress,
workInProgress.pendingProps.children,
renderLanes,
);
return workInProgress.child;
}
function updateHostText(current, workInProgress) {
if (current === null) {
tryToClaimNextHydratableInstance(workInProgress);
@ -3651,6 +3673,7 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
}
resetHydrationState();
break;
case HostResource:
case HostComponent:
pushHostContext(workInProgress);
break;
@ -3985,6 +4008,11 @@ function beginWork(
}
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
case HostResource:
if (enableFloat && supportsResources) {
return updateHostResource(current, workInProgress, renderLanes);
}
// eslint-disable-next-line no-fallthrough
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes);
case HostText:

View File

@ -51,6 +51,7 @@ import {
enableTransitionTracing,
enableUseEventHook,
enableStrictEffects,
enableFloat,
} from 'shared/ReactFeatureFlags';
import {
FunctionComponent,
@ -58,6 +59,7 @@ import {
ClassComponent,
HostRoot,
HostComponent,
HostResource,
HostText,
HostPortal,
Profiler,
@ -73,7 +75,6 @@ import {
CacheComponent,
TracingMarkerComponent,
} from './ReactWorkTags';
import {detachDeletedInstance} from './ReactFiberHostConfig';
import {
NoFlags,
ContentReset,
@ -117,6 +118,7 @@ import {
supportsMutation,
supportsPersistence,
supportsHydration,
supportsResources,
commitMount,
commitUpdate,
resetTextContent,
@ -141,6 +143,9 @@ import {
prepareScopeUpdate,
prepareForCommit,
beforeActiveInstanceBlur,
detachDeletedInstance,
acquireResource,
releaseResource,
} from './ReactFiberHostConfig';
import {
captureCommitPhaseError,
@ -493,6 +498,7 @@ function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
break;
}
case HostComponent:
case HostResource:
case HostText:
case HostPortal:
case IncompleteClassComponent:
@ -1064,6 +1070,34 @@ function commitLayoutEffectOnFiber(
}
break;
}
case HostResource: {
if (enableFloat && supportsResources) {
recursivelyTraverseLayoutEffects(
finishedRoot,
finishedWork,
committedLanes,
);
if (flags & Update) {
const newResource = finishedWork.memoizedState;
if (current !== null) {
const currentResource = current.memoizedState;
if (currentResource !== newResource) {
releaseResource(currentResource);
}
}
finishedWork.stateNode = newResource
? acquireResource(newResource)
: null;
}
if (flags & Ref) {
safelyAttachRef(finishedWork, finishedWork.return);
}
break;
}
}
// eslint-disable-next-line-no-fallthrough
case HostComponent: {
recursivelyTraverseLayoutEffects(
finishedRoot,
@ -1450,7 +1484,10 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
// children to find all the terminal nodes.
let node: Fiber = finishedWork;
while (true) {
if (node.tag === HostComponent) {
if (
node.tag === HostComponent ||
(enableFloat && supportsResources ? node.tag === HostResource : false)
) {
if (hostSubtreeRoot === null) {
hostSubtreeRoot = node;
try {
@ -1522,6 +1559,7 @@ function commitAttachRef(finishedWork: Fiber) {
const instance = finishedWork.stateNode;
let instanceToUse;
switch (finishedWork.tag) {
case HostResource:
case HostComponent:
instanceToUse = getPublicInstance(instance);
break;
@ -1726,7 +1764,8 @@ function isHostParent(fiber: Fiber): boolean {
return (
fiber.tag === HostComponent ||
fiber.tag === HostRoot ||
fiber.tag === HostPortal
fiber.tag === HostPortal ||
(enableFloat && supportsResources ? fiber.tag === HostResource : false)
);
}
@ -1973,6 +2012,23 @@ function commitDeletionEffectsOnFiber(
// into their subtree. There are simpler cases in the inner switch
// that don't modify the stack.
switch (deletedFiber.tag) {
case HostResource: {
if (enableFloat && supportsResources) {
if (!offscreenSubtreeWasHidden) {
safelyDetachRef(deletedFiber, nearestMountedAncestor);
}
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
releaseResource(deletedFiber.memoizedState);
return;
}
}
// eslint-disable-next-line no-fallthrough
case HostComponent: {
if (!offscreenSubtreeWasHidden) {
safelyDetachRef(deletedFiber, nearestMountedAncestor);
@ -2469,6 +2525,20 @@ function commitMutationEffectsOnFiber(
}
return;
}
case HostResource: {
if (enableFloat && supportsResources) {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
if (flags & Ref) {
if (current !== null) {
safelyDetachRef(current, current.return);
}
}
return;
}
}
// eslint-disable-next-line-no-fallthrough
case HostComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
@ -2860,6 +2930,7 @@ export function disappearLayoutEffects(finishedWork: Fiber) {
recursivelyTraverseDisappearLayoutEffects(finishedWork);
break;
}
case HostResource:
case HostComponent: {
// TODO (Offscreen) Check: flags & RefStatic
safelyDetachRef(finishedWork, finishedWork.return);
@ -2961,6 +3032,7 @@ export function reappearLayoutEffects(
// case HostRoot: {
// ...
// }
case HostResource:
case HostComponent: {
recursivelyTraverseReappearLayoutEffects(
finishedRoot,

View File

@ -51,6 +51,7 @@ import {
enableTransitionTracing,
enableUseEventHook,
enableStrictEffects,
enableFloat,
} from 'shared/ReactFeatureFlags';
import {
FunctionComponent,
@ -58,6 +59,7 @@ import {
ClassComponent,
HostRoot,
HostComponent,
HostResource,
HostText,
HostPortal,
Profiler,
@ -73,7 +75,6 @@ import {
CacheComponent,
TracingMarkerComponent,
} from './ReactWorkTags';
import {detachDeletedInstance} from './ReactFiberHostConfig';
import {
NoFlags,
ContentReset,
@ -117,6 +118,7 @@ import {
supportsMutation,
supportsPersistence,
supportsHydration,
supportsResources,
commitMount,
commitUpdate,
resetTextContent,
@ -141,6 +143,9 @@ import {
prepareScopeUpdate,
prepareForCommit,
beforeActiveInstanceBlur,
detachDeletedInstance,
acquireResource,
releaseResource,
} from './ReactFiberHostConfig';
import {
captureCommitPhaseError,
@ -493,6 +498,7 @@ function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
break;
}
case HostComponent:
case HostResource:
case HostText:
case HostPortal:
case IncompleteClassComponent:
@ -1064,6 +1070,34 @@ function commitLayoutEffectOnFiber(
}
break;
}
case HostResource: {
if (enableFloat && supportsResources) {
recursivelyTraverseLayoutEffects(
finishedRoot,
finishedWork,
committedLanes,
);
if (flags & Update) {
const newResource = finishedWork.memoizedState;
if (current !== null) {
const currentResource = current.memoizedState;
if (currentResource !== newResource) {
releaseResource(currentResource);
}
}
finishedWork.stateNode = newResource
? acquireResource(newResource)
: null;
}
if (flags & Ref) {
safelyAttachRef(finishedWork, finishedWork.return);
}
break;
}
}
// eslint-disable-next-line-no-fallthrough
case HostComponent: {
recursivelyTraverseLayoutEffects(
finishedRoot,
@ -1450,7 +1484,10 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
// children to find all the terminal nodes.
let node: Fiber = finishedWork;
while (true) {
if (node.tag === HostComponent) {
if (
node.tag === HostComponent ||
(enableFloat && supportsResources ? node.tag === HostResource : false)
) {
if (hostSubtreeRoot === null) {
hostSubtreeRoot = node;
try {
@ -1522,6 +1559,7 @@ function commitAttachRef(finishedWork: Fiber) {
const instance = finishedWork.stateNode;
let instanceToUse;
switch (finishedWork.tag) {
case HostResource:
case HostComponent:
instanceToUse = getPublicInstance(instance);
break;
@ -1726,7 +1764,8 @@ function isHostParent(fiber: Fiber): boolean {
return (
fiber.tag === HostComponent ||
fiber.tag === HostRoot ||
fiber.tag === HostPortal
fiber.tag === HostPortal ||
(enableFloat && supportsResources ? fiber.tag === HostResource : false)
);
}
@ -1973,6 +2012,23 @@ function commitDeletionEffectsOnFiber(
// into their subtree. There are simpler cases in the inner switch
// that don't modify the stack.
switch (deletedFiber.tag) {
case HostResource: {
if (enableFloat && supportsResources) {
if (!offscreenSubtreeWasHidden) {
safelyDetachRef(deletedFiber, nearestMountedAncestor);
}
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
releaseResource(deletedFiber.memoizedState);
return;
}
}
// eslint-disable-next-line no-fallthrough
case HostComponent: {
if (!offscreenSubtreeWasHidden) {
safelyDetachRef(deletedFiber, nearestMountedAncestor);
@ -2469,6 +2525,20 @@ function commitMutationEffectsOnFiber(
}
return;
}
case HostResource: {
if (enableFloat && supportsResources) {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
if (flags & Ref) {
if (current !== null) {
safelyDetachRef(current, current.return);
}
}
return;
}
}
// eslint-disable-next-line-no-fallthrough
case HostComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
@ -2860,6 +2930,7 @@ export function disappearLayoutEffects(finishedWork: Fiber) {
recursivelyTraverseDisappearLayoutEffects(finishedWork);
break;
}
case HostResource:
case HostComponent: {
// TODO (Offscreen) Check: flags & RefStatic
safelyDetachRef(finishedWork, finishedWork.return);
@ -2961,6 +3032,7 @@ export function reappearLayoutEffects(
// case HostRoot: {
// ...
// }
case HostResource:
case HostComponent: {
recursivelyTraverseReappearLayoutEffects(
finishedRoot,

View File

@ -45,6 +45,7 @@ import {
ClassComponent,
HostRoot,
HostComponent,
HostResource,
HostText,
HostPortal,
ContextProvider,
@ -92,6 +93,7 @@ import {
prepareUpdate,
supportsMutation,
supportsPersistence,
supportsResources,
cloneInstance,
cloneHiddenInstance,
cloneHiddenTextInstance,
@ -144,6 +146,7 @@ import {
enableProfilerTimer,
enableCache,
enableTransitionTracing,
enableFloat,
} from 'shared/ReactFeatureFlags';
import {
renderDidSuspend,
@ -954,6 +957,26 @@ function completeWork(
}
return null;
}
case HostResource: {
if (enableFloat && supportsResources) {
popHostContext(workInProgress);
const currentRef = current ? current.ref : null;
if (currentRef !== workInProgress.ref) {
markRef(workInProgress);
}
if (
current === null ||
current.memoizedState !== workInProgress.memoizedState
) {
// The workInProgress resource is different than the current one or the current
// one does not exist
markUpdate(workInProgress);
}
bubbleProperties(workInProgress);
return null;
}
}
// eslint-disable-next-line-no-fallthrough
case HostComponent: {
popHostContext(workInProgress);
const type = workInProgress.type;

View File

@ -45,6 +45,7 @@ import {
ClassComponent,
HostRoot,
HostComponent,
HostResource,
HostText,
HostPortal,
ContextProvider,
@ -92,6 +93,7 @@ import {
prepareUpdate,
supportsMutation,
supportsPersistence,
supportsResources,
cloneInstance,
cloneHiddenInstance,
cloneHiddenTextInstance,
@ -144,6 +146,7 @@ import {
enableProfilerTimer,
enableCache,
enableTransitionTracing,
enableFloat,
} from 'shared/ReactFeatureFlags';
import {
renderDidSuspend,
@ -954,6 +957,26 @@ function completeWork(
}
return null;
}
case HostResource: {
if (enableFloat && supportsResources) {
popHostContext(workInProgress);
const currentRef = current ? current.ref : null;
if (currentRef !== workInProgress.ref) {
markRef(workInProgress);
}
if (
current === null ||
current.memoizedState !== workInProgress.memoizedState
) {
// The workInProgress resource is different than the current one or the current
// one does not exist
markUpdate(workInProgress);
}
bubbleProperties(workInProgress);
return null;
}
}
// eslint-disable-next-line-no-fallthrough
case HostComponent: {
popHostContext(workInProgress);
const type = workInProgress.type;

View File

@ -11,6 +11,7 @@ import type {Fiber} from './ReactInternalTypes';
import {
HostComponent,
HostResource,
LazyComponent,
SuspenseComponent,
SuspenseListComponent,
@ -34,6 +35,7 @@ function describeFiber(fiber: Fiber): string {
: null;
const source = __DEV__ ? fiber._debugSource : null;
switch (fiber.tag) {
case HostResource:
case HostComponent:
return describeBuiltInComponentFrame(fiber.type, source, owner);
case LazyComponent:

View File

@ -24,12 +24,10 @@ export const supportsHydration = false;
export const canHydrateInstance = shim;
export const canHydrateTextInstance = shim;
export const canHydrateSuspenseInstance = shim;
export const isHydratableResource = shim;
export const isSuspenseInstancePending = shim;
export const isSuspenseInstanceFallback = shim;
export const getSuspenseInstanceFallbackErrorDetails = shim;
export const registerSuspenseInstanceRetry = shim;
export const getMatchingResourceInstance = shim;
export const getNextHydratableSibling = shim;
export const getFirstHydratableChild = shim;
export const getFirstHydratableChildWithinContainer = shim;

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
// Renderers that don't support hydration
// can re-export everything from this module.
function shim(...args: any) {
throw new Error(
'The current renderer does not support Resources. ' +
'This error is likely caused by a bug in React. ' +
'Please file an issue.',
);
}
// Resources (when unsupported)
export const supportsResources = false;
export const isHostResourceType = shim;
export const getResource = shim;
export const acquireResource = shim;
export const releaseResource = shim;

View File

@ -37,6 +37,7 @@ import {
FunctionComponent,
ForwardRef,
HostComponent,
HostResource,
HostPortal,
HostRoot,
MemoComponent,
@ -47,6 +48,7 @@ import {
REACT_MEMO_TYPE,
REACT_LAZY_TYPE,
} from 'shared/ReactSymbols';
import {enableFloat} from 'shared/ReactFeatureFlags';
let resolveFamily: RefreshHandler | null = null;
// $FlowFixMe Flow gets confused by a WeakSet feature check below.
@ -449,7 +451,10 @@ function findChildHostInstancesForFiberShallowly(
let node: Fiber = fiber;
let foundHostInstances = false;
while (true) {
if (node.tag === HostComponent) {
if (
node.tag === HostComponent ||
(enableFloat ? node.tag === HostResource : false)
) {
// We got a match.
foundHostInstances = true;
hostInstances.add(node.stateNode);

View File

@ -37,6 +37,7 @@ import {
FunctionComponent,
ForwardRef,
HostComponent,
HostResource,
HostPortal,
HostRoot,
MemoComponent,
@ -47,6 +48,7 @@ import {
REACT_MEMO_TYPE,
REACT_LAZY_TYPE,
} from 'shared/ReactSymbols';
import {enableFloat} from 'shared/ReactFeatureFlags';
let resolveFamily: RefreshHandler | null = null;
// $FlowFixMe Flow gets confused by a WeakSet feature check below.
@ -449,7 +451,10 @@ function findChildHostInstancesForFiberShallowly(
let node: Fiber = fiber;
let foundHostInstances = false;
while (true) {
if (node.tag === HostComponent) {
if (
node.tag === HostComponent ||
(enableFloat ? node.tag === HostResource : false)
) {
// We got a match.
foundHostInstances = true;
hostInstances.add(node.stateNode);

View File

@ -34,7 +34,6 @@ import {
NoFlags,
DidCapture,
} from './ReactFiberFlags';
import {enableFloat} from 'shared/ReactFeatureFlags';
import {
createFiberFromHostInstanceForDeletion,
@ -46,9 +45,7 @@ import {
canHydrateInstance,
canHydrateTextInstance,
canHydrateSuspenseInstance,
isHydratableResource,
getNextHydratableSibling,
getMatchingResourceInstance,
getFirstHydratableChild,
getFirstHydratableChildWithinContainer,
getFirstHydratableChildWithinSuspenseInstance,
@ -78,7 +75,6 @@ import {
restoreSuspendedTreeContext,
} from './ReactFiberTreeContext.new';
import {queueRecoverableErrors} from './ReactFiberWorkLoop.new';
import {getRootHostContainer} from './ReactFiberHostContext.new';
// The deepest Fiber on the stack involved in a hydration context.
// This may have been an insertion or a hydration.
@ -408,19 +404,6 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
if (!isHydrating) {
return;
}
if (enableFloat) {
if (
fiber.tag === HostComponent &&
isHydratableResource(fiber.type, fiber.pendingProps)
) {
fiber.stateNode = getMatchingResourceInstance(
fiber.type,
fiber.pendingProps,
getRootHostContainer(),
);
return;
}
}
let nextInstance = nextHydratableInstance;
if (!nextInstance) {
if (shouldClientRenderOnMismatch(fiber)) {
@ -613,30 +596,6 @@ function popHydrationState(fiber: Fiber): boolean {
if (!supportsHydration) {
return false;
}
if (
enableFloat &&
isHydrating &&
isHydratableResource(fiber.type, fiber.memoizedProps)
) {
if (fiber.stateNode === null) {
if (__DEV__) {
const rel = fiber.memoizedProps.rel
? `rel="${fiber.memoizedProps.rel}" `
: '';
const href = fiber.memoizedProps.href
? `href="${fiber.memoizedProps.href}"`
: '';
console.error(
'A matching Hydratable Resource was not found in the DOM for <%s %s%s>.',
fiber.type,
rel,
href,
);
}
throwOnHydrationMismatch(fiber);
}
return true;
}
if (fiber !== hydrationParentFiber) {
// We're deeper than the current hydration context, inside an inserted
// tree.

View File

@ -34,7 +34,6 @@ import {
NoFlags,
DidCapture,
} from './ReactFiberFlags';
import {enableFloat} from 'shared/ReactFeatureFlags';
import {
createFiberFromHostInstanceForDeletion,
@ -46,9 +45,7 @@ import {
canHydrateInstance,
canHydrateTextInstance,
canHydrateSuspenseInstance,
isHydratableResource,
getNextHydratableSibling,
getMatchingResourceInstance,
getFirstHydratableChild,
getFirstHydratableChildWithinContainer,
getFirstHydratableChildWithinSuspenseInstance,
@ -78,7 +75,6 @@ import {
restoreSuspendedTreeContext,
} from './ReactFiberTreeContext.old';
import {queueRecoverableErrors} from './ReactFiberWorkLoop.old';
import {getRootHostContainer} from './ReactFiberHostContext.old';
// The deepest Fiber on the stack involved in a hydration context.
// This may have been an insertion or a hydration.
@ -408,19 +404,6 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
if (!isHydrating) {
return;
}
if (enableFloat) {
if (
fiber.tag === HostComponent &&
isHydratableResource(fiber.type, fiber.pendingProps)
) {
fiber.stateNode = getMatchingResourceInstance(
fiber.type,
fiber.pendingProps,
getRootHostContainer(),
);
return;
}
}
let nextInstance = nextHydratableInstance;
if (!nextInstance) {
if (shouldClientRenderOnMismatch(fiber)) {
@ -613,30 +596,6 @@ function popHydrationState(fiber: Fiber): boolean {
if (!supportsHydration) {
return false;
}
if (
enableFloat &&
isHydrating &&
isHydratableResource(fiber.type, fiber.memoizedProps)
) {
if (fiber.stateNode === null) {
if (__DEV__) {
const rel = fiber.memoizedProps.rel
? `rel="${fiber.memoizedProps.rel}" `
: '';
const href = fiber.memoizedProps.href
? `href="${fiber.memoizedProps.href}"`
: '';
console.error(
'A matching Hydratable Resource was not found in the DOM for <%s %s%s>.',
fiber.type,
rel,
href,
);
}
throwOnHydrationMismatch(fiber);
}
return true;
}
if (fiber !== hydrationParentFiber) {
// We're deeper than the current hydration context, inside an inserted
// tree.

View File

@ -17,12 +17,14 @@ import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFrom
import {
ClassComponent,
HostComponent,
HostResource,
HostRoot,
HostPortal,
HostText,
SuspenseComponent,
} from './ReactWorkTags';
import {NoFlags, Placement, Hydrating} from './ReactFiberFlags';
import {enableFloat} from 'shared/ReactFeatureFlags';
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
@ -273,7 +275,11 @@ export function findCurrentHostFiber(parent: Fiber): Fiber | null {
function findCurrentHostFiberImpl(node: Fiber) {
// Next we'll drill down this component to find the first HostComponent/Text.
if (node.tag === HostComponent || node.tag === HostText) {
if (
node.tag === HostComponent ||
node.tag === HostText ||
(enableFloat ? node.tag === HostResource : false)
) {
return node;
}
@ -298,7 +304,11 @@ export function findCurrentHostFiberWithNoPortals(parent: Fiber): Fiber | null {
function findCurrentHostFiberWithNoPortalsImpl(node: Fiber) {
// Next we'll drill down this component to find the first HostComponent/Text.
if (node.tag === HostComponent || node.tag === HostText) {
if (
node.tag === HostComponent ||
node.tag === HostText ||
(enableFloat ? node.tag === HostResource : false)
) {
return node;
}

View File

@ -19,6 +19,7 @@ import {
ClassComponent,
HostRoot,
HostComponent,
HostResource,
HostPortal,
ContextProvider,
SuspenseComponent,
@ -115,6 +116,7 @@ function unwindWork(
// We unwound to the root without completing it. Exit.
return null;
}
case HostResource:
case HostComponent: {
// TODO: popHydrationState
popHostContext(workInProgress);
@ -233,6 +235,7 @@ function unwindInterruptedWork(
resetMutableSourceWorkInProgressVersions();
break;
}
case HostResource:
case HostComponent: {
popHostContext(interruptedWork);
break;

View File

@ -19,6 +19,7 @@ import {
ClassComponent,
HostRoot,
HostComponent,
HostResource,
HostPortal,
ContextProvider,
SuspenseComponent,
@ -115,6 +116,7 @@ function unwindWork(
// We unwound to the root without completing it. Exit.
return null;
}
case HostResource:
case HostComponent: {
// TODO: popHydrationState
popHostContext(workInProgress);
@ -233,6 +235,7 @@ function unwindInterruptedWork(
resetMutableSourceWorkInProgressVersions();
break;
}
case HostResource:
case HostComponent: {
popHostContext(interruptedWork);
break;

View File

@ -84,6 +84,8 @@ import {
supportsMicrotasks,
errorHydratingContainer,
scheduleMicrotask,
prepareRendererToRender,
resetRendererAfterRender,
} from './ReactFiberHostConfig';
import {
@ -1767,7 +1769,8 @@ function handleThrow(root, thrownValue): void {
}
}
function pushDispatcher() {
function pushDispatcher(container) {
prepareRendererToRender(container);
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
if (prevDispatcher === null) {
@ -1781,6 +1784,7 @@ function pushDispatcher() {
}
function popDispatcher(prevDispatcher) {
resetRendererAfterRender();
ReactCurrentDispatcher.current = prevDispatcher;
}
@ -1850,7 +1854,7 @@ export function renderHasNotSuspendedYet(): boolean {
function renderRootSync(root: FiberRoot, lanes: Lanes) {
const prevExecutionContext = executionContext;
executionContext |= RenderContext;
const prevDispatcher = pushDispatcher();
const prevDispatcher = pushDispatcher(root.containerInfo);
// If the root or lanes have changed, throw out the existing stack
// and prepare a fresh one. Otherwise we'll continue where we left off.
@ -1950,7 +1954,7 @@ function workLoopSync() {
function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
const prevExecutionContext = executionContext;
executionContext |= RenderContext;
const prevDispatcher = pushDispatcher();
const prevDispatcher = pushDispatcher(root.containerInfo);
// If the root or lanes have changed, throw out the existing stack
// and prepare a fresh one. Otherwise we'll continue where we left off.

View File

@ -84,6 +84,8 @@ import {
supportsMicrotasks,
errorHydratingContainer,
scheduleMicrotask,
prepareRendererToRender,
resetRendererAfterRender,
} from './ReactFiberHostConfig';
import {
@ -1767,7 +1769,8 @@ function handleThrow(root, thrownValue): void {
}
}
function pushDispatcher() {
function pushDispatcher(container) {
prepareRendererToRender(container);
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
if (prevDispatcher === null) {
@ -1781,6 +1784,7 @@ function pushDispatcher() {
}
function popDispatcher(prevDispatcher) {
resetRendererAfterRender();
ReactCurrentDispatcher.current = prevDispatcher;
}
@ -1850,7 +1854,7 @@ export function renderHasNotSuspendedYet(): boolean {
function renderRootSync(root: FiberRoot, lanes: Lanes) {
const prevExecutionContext = executionContext;
executionContext |= RenderContext;
const prevDispatcher = pushDispatcher();
const prevDispatcher = pushDispatcher(root.containerInfo);
// If the root or lanes have changed, throw out the existing stack
// and prepare a fresh one. Otherwise we'll continue where we left off.
@ -1950,7 +1954,7 @@ function workLoopSync() {
function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
const prevExecutionContext = executionContext;
executionContext |= RenderContext;
const prevDispatcher = pushDispatcher();
const prevDispatcher = pushDispatcher(root.containerInfo);
// If the root or lanes have changed, throw out the existing stack
// and prepare a fresh one. Otherwise we'll continue where we left off.

View File

@ -10,7 +10,11 @@
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
import type {Instance} from './ReactFiberHostConfig';
import {HostComponent, HostText} from 'react-reconciler/src/ReactWorkTags';
import {
HostComponent,
HostResource,
HostText,
} from 'react-reconciler/src/ReactWorkTags';
import getComponentNameFromType from 'shared/getComponentNameFromType';
import {
findFiberRoot,
@ -150,7 +154,7 @@ function matchSelector(fiber: Fiber, selector: Selector): boolean {
((selector: any): HasPseudoClassSelector).value,
);
case ROLE_TYPE:
if (fiber.tag === HostComponent) {
if (fiber.tag === HostComponent || fiber.tag === HostResource) {
const node = fiber.stateNode;
if (
matchAccessibilityRole(node, ((selector: any): RoleSelector).value)
@ -160,7 +164,11 @@ function matchSelector(fiber: Fiber, selector: Selector): boolean {
}
break;
case TEXT_TYPE:
if (fiber.tag === HostComponent || fiber.tag === HostText) {
if (
fiber.tag === HostComponent ||
fiber.tag === HostText ||
fiber.tag === HostResource
) {
const textContent = getTextContent(fiber);
if (
textContent !== null &&
@ -171,7 +179,7 @@ function matchSelector(fiber: Fiber, selector: Selector): boolean {
}
break;
case TEST_NAME_TYPE:
if (fiber.tag === HostComponent) {
if (fiber.tag === HostComponent || fiber.tag === HostResource) {
const dataTestID = fiber.memoizedProps['data-testname'];
if (
typeof dataTestID === 'string' &&
@ -217,7 +225,10 @@ function findPaths(root: Fiber, selectors: Array<Selector>): Array<Fiber> {
let selectorIndex = ((stack[index++]: any): number);
let selector = selectors[selectorIndex];
if (fiber.tag === HostComponent && isHiddenSubtree(fiber)) {
if (
(fiber.tag === HostComponent || fiber.tag === HostResource) &&
isHiddenSubtree(fiber)
) {
continue;
} else {
while (selector != null && matchSelector(fiber, selector)) {
@ -249,7 +260,10 @@ function hasMatchingPaths(root: Fiber, selectors: Array<Selector>): boolean {
let selectorIndex = ((stack[index++]: any): number);
let selector = selectors[selectorIndex];
if (fiber.tag === HostComponent && isHiddenSubtree(fiber)) {
if (
(fiber.tag === HostComponent || fiber.tag === HostResource) &&
isHiddenSubtree(fiber)
) {
continue;
} else {
while (selector != null && matchSelector(fiber, selector)) {
@ -289,7 +303,7 @@ export function findAllNodes(
let index = 0;
while (index < stack.length) {
const node = ((stack[index++]: any): Fiber);
if (node.tag === HostComponent) {
if (node.tag === HostComponent || node.tag === HostResource) {
if (isHiddenSubtree(node)) {
continue;
}
@ -327,7 +341,10 @@ export function getFindAllNodesFailureDescription(
let selectorIndex = ((stack[index++]: any): number);
const selector = selectors[selectorIndex];
if (fiber.tag === HostComponent && isHiddenSubtree(fiber)) {
if (
(fiber.tag === HostComponent || fiber.tag === HostResource) &&
isHiddenSubtree(fiber)
) {
continue;
} else if (matchSelector(fiber, selector)) {
matchedNames.push(selectorToString(selector));
@ -479,7 +496,7 @@ export function focusWithin(
if (isHiddenSubtree(fiber)) {
continue;
}
if (fiber.tag === HostComponent) {
if (fiber.tag === HostComponent || fiber.tag === HostResource) {
const node = fiber.stateNode;
if (setFocusIfFocusable(node)) {
return true;

View File

@ -33,7 +33,8 @@ export type WorkTag =
| 22
| 23
| 24
| 25;
| 25
| 26;
export const FunctionComponent = 0;
export const ClassComponent = 1;
@ -60,3 +61,4 @@ export const OffscreenComponent = 22;
export const LegacyHiddenComponent = 23;
export const CacheComponent = 24;
export const TracingMarkerComponent = 25;
export const HostResource = 26;

View File

@ -66,6 +66,8 @@ describe('ReactFiberHostContext', () => {
getCurrentEventPriority: function() {
return DefaultEventPriority;
},
prepareRendererToRender: function() {},
resetRendererAfterRender: function() {},
supportsMutation: true,
requestPostPaintCallback: function() {},
});
@ -131,6 +133,8 @@ describe('ReactFiberHostContext', () => {
return DefaultEventPriority;
},
requestPostPaintCallback: function() {},
prepareRendererToRender: function() {},
resetRendererAfterRender: function() {},
supportsMutation: true,
});

View File

@ -68,6 +68,8 @@ export const getInstanceFromScope = $$$hostConfig.getInstanceFromScope;
export const getCurrentEventPriority = $$$hostConfig.getCurrentEventPriority;
export const detachDeletedInstance = $$$hostConfig.detachDeletedInstance;
export const requestPostPaintCallback = $$$hostConfig.requestPostPaintCallback;
export const prepareRendererToRender = $$$hostConfig.prepareRendererToRender;
export const resetRendererAfterRender = $$$hostConfig.resetRendererAfterRender;
// -------------------
// Microtasks
@ -187,6 +189,13 @@ export const didNotFindHydratableTextInstance =
export const didNotFindHydratableSuspenseInstance =
$$$hostConfig.didNotFindHydratableSuspenseInstance;
export const errorHydratingContainer = $$$hostConfig.errorHydratingContainer;
export const isHydratableResource = $$$hostConfig.isHydratableResource;
export const getMatchingResourceInstance =
$$$hostConfig.getMatchingResourceInstance;
// -------------------
// Resources
// (optional)
// -------------------
export const supportsResources = $$$hostConfig.supportsResources;
export const isHostResourceType = $$$hostConfig.isHostResourceType;
export const getResource = $$$hostConfig.getResource;
export const acquireResource = $$$hostConfig.acquireResource;
export const releaseResource = $$$hostConfig.releaseResource;

View File

@ -19,6 +19,7 @@ import {
HostRoot,
HostPortal,
HostComponent,
HostResource,
HostText,
Fragment,
Mode,
@ -77,6 +78,7 @@ export default function getComponentNameFromFiber(fiber: Fiber): string | null {
return getWrappedName(type, type.render, 'ForwardRef');
case Fragment:
return 'Fragment';
case HostResource:
case HostComponent:
// Host component type is the display name (e.g. "div", "View")
return type;

View File

@ -11,6 +11,7 @@
"scheduler": "^0.11.0"
},
"peerDependencies": {
"react": "^17.0.0"
"react": "^17.0.0",
"react-dom": "^17.0.0"
}
}

View File

@ -24,6 +24,8 @@ import type {
SuspenseBoundaryID,
ResponseState,
FormatContext,
Resources,
BoundaryResources,
} from './ReactServerFormatConfig';
import type {ContextSnapshot} from './ReactFizzNewContext';
import type {ComponentStackNode} from './ReactFizzComponentStack';
@ -63,6 +65,15 @@ import {
UNINITIALIZED_SUSPENSE_BOUNDARY_ID,
assignSuspenseBoundaryID,
getChildFormatContext,
writeInitialResources,
writeImmediateResources,
hoistResources,
hoistResourcesToRoot,
prepareToRender,
cleanupAfterRender,
setCurrentlyRenderingBoundaryResourcesTarget,
createResources,
createBoundaryResources,
} from './ReactServerFormatConfig';
import {
constructClassInstance,
@ -147,6 +158,7 @@ type SuspenseBoundary = {
completedSegments: Array<Segment>, // completed but not yet flushed segments.
byteSize: number, // used to determine whether to inline children boundaries.
fallbackAbortableTasks: Set<Task>, // used to cancel task on the fallback if the boundary completes or gets canceled.
resources: BoundaryResources,
};
export type Task = {
@ -199,9 +211,10 @@ export opaque type Request = {
nextSegmentId: number,
allPendingTasks: number, // when it reaches zero, we can close the connection.
pendingRootTasks: number, // when this reaches zero, we've finished at least the root boundary.
resources: Resources,
completedRootSegment: null | Segment, // Completed but not yet flushed root segments.
abortableTasks: Set<Task>,
pingedTasks: Array<Task>,
pingedTasks: Array<Task>, // High priority tasks that should be worked on first.
// Queues to flush in order of priority
clientRenderedBoundaries: Array<SuspenseBoundary>, // Errored or client rendered but not yet flushed.
completedBoundaries: Array<SuspenseBoundary>, // Completed but not yet fully flushed boundaries to show.
@ -262,6 +275,7 @@ export function createRequest(
): Request {
const pingedTasks = [];
const abortSet: Set<Task> = new Set();
const resources: Resources = createResources();
const request = {
destination: null,
responseState,
@ -274,6 +288,7 @@ export function createRequest(
nextSegmentId: 0,
allPendingTasks: 0,
pendingRootTasks: 0,
resources,
completedRootSegment: null,
abortableTasks: abortSet,
pingedTasks: pingedTasks,
@ -337,6 +352,7 @@ function createSuspenseBoundary(
byteSize: 0,
fallbackAbortableTasks,
errorDigest: null,
resources: createBoundaryResources(),
};
}
@ -562,6 +578,12 @@ function renderSuspenseBoundary(
// we're writing to. If something suspends, it'll spawn new suspended task with that context.
task.blockedBoundary = newBoundary;
task.blockedSegment = contentRootSegment;
if (enableFloat) {
setCurrentlyRenderingBoundaryResourcesTarget(
request.resources,
newBoundary.resources,
);
}
try {
// We use the safe form because we don't handle suspending here. Only error handling.
renderNode(request, task, content);
@ -572,6 +594,11 @@ function renderSuspenseBoundary(
contentRootSegment.textEmbedded,
);
contentRootSegment.status = COMPLETED;
if (enableFloat) {
if (newBoundary.pendingTasks === 0) {
hoistCompletedBoundaryResources(request, newBoundary);
}
}
queueCompletedSegment(newBoundary, contentRootSegment);
if (newBoundary.pendingTasks === 0) {
// This must have been the last segment we were waiting on. This boundary is now complete.
@ -592,6 +619,12 @@ function renderSuspenseBoundary(
// We don't need to schedule any task because we know the parent has written yet.
// We do need to fallthrough to create the fallback though.
} finally {
if (enableFloat) {
setCurrentlyRenderingBoundaryResourcesTarget(
request.resources,
parentBoundary ? parentBoundary.resources : null,
);
}
task.blockedBoundary = parentBoundary;
task.blockedSegment = parentSegment;
}
@ -619,6 +652,19 @@ function renderSuspenseBoundary(
popComponentStackInDEV(task);
}
function hoistCompletedBoundaryResources(
request: Request,
completedBoundary: SuspenseBoundary,
): void {
if (request.completedRootSegment !== null || request.pendingRootTasks > 0) {
// The Shell has not flushed yet. we can hoist Resources for this boundary
// all the way to the Root.
hoistResourcesToRoot(request.resources, completedBoundary.resources);
}
// We don't hoist if the root already flushed because late resources will be hoisted
// as boundaries flush
}
function renderBackupSuspenseBoundary(
request: Request,
task: Task,
@ -644,6 +690,7 @@ function renderHostElement(
): void {
pushBuiltInComponentStackInDEV(task, type);
const segment = task.blockedSegment;
const children = pushStartInstance(
segment.chunks,
request.preamble,
@ -651,10 +698,12 @@ function renderHostElement(
props,
request.responseState,
segment.formatContext,
segment.lastPushedText,
);
segment.lastPushedText = false;
const prevContext = segment.formatContext;
segment.formatContext = getChildFormatContext(prevContext, type, props);
// We use the non-destructive form because if something suspends, we still
// need to pop back up and finish this subtree of HTML.
renderNode(request, task, children);
@ -1733,11 +1782,15 @@ function finishedTask(
queueCompletedSegment(boundary, segment);
}
}
if (enableFloat) {
hoistCompletedBoundaryResources(request, boundary);
}
if (boundary.parentFlushed) {
// The segment might be part of a segment that didn't flush yet, but if the boundary's
// parent flushed, we need to schedule the boundary to be emitted.
request.completedBoundaries.push(boundary);
}
// We can now cancel any pending task on the fallback since we won't need to show it anymore.
// This needs to happen after we read the parentFlushed flags because aborting can finish
// work which can trigger user code, which can start flushing, which can change those flags.
@ -1774,6 +1827,13 @@ function finishedTask(
}
function retryTask(request: Request, task: Task): void {
if (enableFloat) {
const blockedBoundary = task.blockedBoundary;
setCurrentlyRenderingBoundaryResourcesTarget(
request.resources,
blockedBoundary ? blockedBoundary.resources : null,
);
}
const segment = task.blockedSegment;
if (segment.status !== PENDING) {
// We completed this by other means before we had a chance to retry it.
@ -1825,6 +1885,9 @@ function retryTask(request: Request, task: Task): void {
erroredTask(request, task.blockedBoundary, segment, x);
}
} finally {
if (enableFloat) {
setCurrentlyRenderingBoundaryResourcesTarget(request.resources, null);
}
if (__DEV__) {
currentTaskInDEV = prevTaskInDEV;
}
@ -1838,6 +1901,7 @@ export function performWork(request: Request): void {
const prevContext = getActiveContext();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = Dispatcher;
const previousHostDispatcher = prepareToRender(request.resources);
let prevGetCurrentStackImpl;
if (__DEV__) {
prevGetCurrentStackImpl = ReactDebugCurrentFrame.getCurrentStack;
@ -1862,6 +1926,8 @@ export function performWork(request: Request): void {
} finally {
setCurrentResponseState(prevResponseState);
ReactCurrentDispatcher.current = prevDispatcher;
cleanupAfterRender(previousHostDispatcher);
if (__DEV__) {
ReactDebugCurrentFrame.getCurrentStack = prevGetCurrentStackImpl;
}
@ -1900,6 +1966,7 @@ function flushSubtree(
const chunks = segment.chunks;
let chunkIdx = 0;
const children = segment.children;
for (let childIdx = 0; childIdx < children.length; childIdx++) {
const nextChild = children[childIdx];
// Write all the chunks up until the next child.
@ -1998,6 +2065,9 @@ function flushSegment(
return writeEndPendingSuspenseBoundary(destination, request.responseState);
} else {
if (enableFloat) {
hoistResources(request.resources, boundary.resources);
}
// We can inline this boundary's content as a complete boundary.
writeStartCompletedSuspenseBoundary(destination, request.responseState);
@ -2019,6 +2089,25 @@ function flushSegment(
}
}
function flushInitialResources(
destination: Destination,
resources: Resources,
responseState: ResponseState,
): void {
writeInitialResources(destination, resources, responseState);
}
function flushImmediateResources(
destination: Destination,
request: Request,
): void {
writeImmediateResources(
destination,
request.resources,
request.responseState,
);
}
function flushClientRenderedBoundary(
request: Request,
destination: Destination,
@ -2054,6 +2143,12 @@ function flushCompletedBoundary(
destination: Destination,
boundary: SuspenseBoundary,
): boolean {
if (enableFloat) {
setCurrentlyRenderingBoundaryResourcesTarget(
request.resources,
boundary.resources,
);
}
const completedSegments = boundary.completedSegments;
let i = 0;
for (; i < completedSegments.length; i++) {
@ -2067,6 +2162,7 @@ function flushCompletedBoundary(
request.responseState,
boundary.id,
boundary.rootSegmentID,
boundary.resources,
);
}
@ -2075,6 +2171,12 @@ function flushPartialBoundary(
destination: Destination,
boundary: SuspenseBoundary,
): boolean {
if (enableFloat) {
setCurrentlyRenderingBoundaryResourcesTarget(
request.resources,
boundary.resources,
);
}
const completedSegments = boundary.completedSegments;
let i = 0;
for (; i < completedSegments.length; i++) {
@ -2138,8 +2240,6 @@ function flushCompletedQueues(
// that item fully and then yield. At that point we remove the already completed
// items up until the point we completed them.
// TODO: Emit preloading.
let i;
const completedRootSegment = request.completedRootSegment;
if (completedRootSegment !== null) {
@ -2150,15 +2250,23 @@ function flushCompletedQueues(
// we expect the preamble to be tiny and will ignore backpressure
writeChunk(destination, preamble[i]);
}
flushInitialResources(
destination,
request.resources,
request.responseState,
);
}
flushSegment(request, destination, completedRootSegment);
request.completedRootSegment = null;
writeCompletedRoot(destination, request.responseState);
} else {
// We haven't flushed the root yet so we don't need to check boundaries further down
// We haven't flushed the root yet so we don't need to check any other branches further down
return;
}
} else if (enableFloat) {
flushImmediateResources(destination, request);
}
// We emit client rendering instructions for already emitted boundaries first.

View File

@ -26,6 +26,8 @@
declare var $$$hostConfig: any;
export opaque type Destination = mixed; // eslint-disable-line no-undef
export opaque type ResponseState = mixed;
export opaque type Resources = mixed;
export opaque type BoundaryResources = mixed;
export opaque type FormatContext = mixed;
export opaque type SuspenseBoundaryID = mixed;
@ -66,3 +68,17 @@ export const writeCompletedBoundaryInstruction =
$$$hostConfig.writeCompletedBoundaryInstruction;
export const writeClientRenderBoundaryInstruction =
$$$hostConfig.writeClientRenderBoundaryInstruction;
export const prepareToRender = $$$hostConfig.prepareToRender;
export const cleanupAfterRender = $$$hostConfig.cleanupAfterRender;
// -------------------------
// Resources
// -------------------------
export const writeInitialResources = $$$hostConfig.writeInitialResources;
export const writeImmediateResources = $$$hostConfig.writeImmediateResources;
export const hoistResources = $$$hostConfig.hoistResources;
export const hoistResourcesToRoot = $$$hostConfig.hoistResourcesToRoot;
export const createResources = $$$hostConfig.createResources;
export const createBoundaryResources = $$$hostConfig.createBoundaryResources;
export const setCurrentlyRenderingBoundaryResourcesTarget =
$$$hostConfig.setCurrentlyRenderingBoundaryResourcesTarget;

View File

@ -46,6 +46,7 @@ export * from 'react-reconciler/src/ReactFiberHostConfigWithNoPersistence';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoHydration';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoTestSelectors';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoMicrotasks';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoResources';
const NO_CONTEXT = {};
const UPDATE_SIGNAL = {};
@ -321,3 +322,11 @@ export function logRecoverableError(error: mixed): void {
export function requestPostPaintCallback(callback: (time: number) => void) {
// noop
}
export function prepareRendererToRender(container: Container): void {
// noop
}
export function resetRendererAfterRender(): void {
// noop
}

View File

@ -27,6 +27,7 @@ import {
FunctionComponent,
ClassComponent,
HostComponent,
HostResource,
HostPortal,
HostText,
HostRoot,
@ -196,6 +197,7 @@ function toTree(node: ?Fiber) {
instance: null,
rendered: childrenToTree(node.child),
};
case HostResource:
case HostComponent: {
return {
nodeType: 'host',
@ -302,7 +304,7 @@ class ReactTestInstance {
}
get instance() {
if (this._fiber.tag === HostComponent) {
if (this._fiber.tag === HostComponent || this._fiber.tag === HostResource) {
return getPublicInstance(this._fiber.stateNode);
} else {
return this._fiber.stateNode;

View File

@ -426,5 +426,11 @@
"438": "An unsupported type was passed to use(): %s",
"439": "We didn't expect to see a forward reference. This is a bug in the React Server.",
"440": "A function wrapped in useEvent can't be called during rendering.",
"441": "An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error."
"441": "An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.",
"442": "The current renderer does not support Resources. This error is likely caused by a bug in React. Please file an issue.",
"443": "acquireResource encountered a resource type it did not expect: \"%s\". this is a bug in React.",
"444": "getResource encountered a resource type it did not expect: \"%s\". this is a bug in React.",
"445": "\"currentResources\" was expected to exist. This is a bug in React.",
"446": "\"currentDocument\" was expected to exist. This is a bug in React.",
"447": "While attempting to insert a Resource, React expected the Document to contain a head element but it was not found."
}

View File

@ -275,7 +275,7 @@ const bundles = [
global: 'ReactDOMServer',
minifyWithProdErrorCodes: true,
wrapWithModuleBoundaries: false,
externals: ['react'],
externals: ['react', 'react-dom'],
babel: opts =>
Object.assign({}, opts, {
plugins: opts.plugins.concat([
@ -288,7 +288,7 @@ const bundles = [
moduleType: RENDERER,
entry: 'react-dom/src/server/ReactDOMLegacyServerNode.js',
name: 'react-dom-server-legacy.node',
externals: ['react', 'stream'],
externals: ['react', 'stream', 'react-dom'],
minifyWithProdErrorCodes: false,
wrapWithModuleBoundaries: false,
babel: opts =>
@ -308,7 +308,7 @@ const bundles = [
global: 'ReactDOMServer',
minifyWithProdErrorCodes: true,
wrapWithModuleBoundaries: false,
externals: ['react'],
externals: ['react', 'react-dom'],
},
{
bundleTypes: [NODE_DEV, NODE_PROD],
@ -318,7 +318,7 @@ const bundles = [
global: 'ReactDOMServer',
minifyWithProdErrorCodes: false,
wrapWithModuleBoundaries: false,
externals: ['react', 'util'],
externals: ['react', 'util', 'react-dom'],
},
{
bundleTypes: __EXPERIMENTAL__ ? [FB_WWW_DEV, FB_WWW_PROD] : [],
@ -327,7 +327,7 @@ const bundles = [
global: 'ReactDOMServerStreaming',
minifyWithProdErrorCodes: false,
wrapWithModuleBoundaries: false,
externals: ['react'],
externals: ['react', 'react-dom'],
},
/******* React DOM Fizz Static *******/
@ -338,7 +338,7 @@ const bundles = [
global: 'ReactDOMStatic',
minifyWithProdErrorCodes: true,
wrapWithModuleBoundaries: false,
externals: ['react'],
externals: ['react', 'react-dom'],
},
{
bundleTypes: __EXPERIMENTAL__ ? [NODE_DEV, NODE_PROD] : [],
@ -348,7 +348,7 @@ const bundles = [
global: 'ReactDOMStatic',
minifyWithProdErrorCodes: false,
wrapWithModuleBoundaries: false,
externals: ['react', 'util', 'stream'],
externals: ['react', 'util', 'stream', 'react-dom'],
},
/******* React Server DOM Webpack Writer *******/
@ -359,7 +359,7 @@ const bundles = [
global: 'ReactServerDOMWriter',
minifyWithProdErrorCodes: false,
wrapWithModuleBoundaries: false,
externals: ['react'],
externals: ['react', 'react-dom'],
},
{
bundleTypes: [NODE_DEV, NODE_PROD],
@ -368,7 +368,7 @@ const bundles = [
global: 'ReactServerDOMWriter',
minifyWithProdErrorCodes: false,
wrapWithModuleBoundaries: false,
externals: ['react', 'util'],
externals: ['react', 'util', 'react-dom'],
},
/******* React Server DOM Webpack Reader *******/

View File

@ -130,7 +130,12 @@ module.exports = [
'react-server-dom-relay/server',
'react-server-dom-relay/src/ReactDOMServerFB.js',
],
paths: ['react-dom', 'react-dom-bindings', 'react-server-dom-relay'],
paths: [
'react-dom',
'react-dom-bindings',
'react-server-dom-relay',
'shared/ReactDOMSharedInternals',
],
isFlowTyped: true,
isServerSupported: true,
},