[Float] add support for scripts and other enhancements (#25480)

* float enhance!!!

Support preinit as script
Support resources from async scripts
Support saving the precedence place when rendering the shell

There was a significant change to the flushing order of resources which follows the general principal of...
1. stuff that blocks display
2. stuff that we know will be used
3. stuff that was explicitly preloaded

As a consequence if you preinit a style now it won't automatically flush in the shell unless you actually depend on it in your tree. To avoid races with precedence order we now emit a tag that saves the place amongst the precedence hierarchy so late insertions still end up where they were intended

There is also a novel hydration pathway for certain tags. If you render an async script with an onLoad or onError it will always treat it like an insertion rather than a hydration.

* restore preinit style flushing behavior and nits
This commit is contained in:
Josh Story
2022-12-03 10:40:05 -05:00
committed by Rick Hanlon
parent 07e9f60e5b
commit f2d94fdb33
15 changed files with 1398 additions and 496 deletions
@@ -7,7 +7,11 @@
* @flow
*/
import type {FloatRoot, StyleResource} from './ReactDOMFloatClient';
import type {
FloatRoot,
StyleResource,
ScriptResource,
} from './ReactDOMFloatClient';
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
import type {ReactScopeInstance} from 'shared/ReactTypes';
import type {
@@ -48,7 +52,7 @@ const internalContainerInstanceKey = '__reactContainer$' + randomKey;
const internalEventHandlersKey = '__reactEvents$' + randomKey;
const internalEventHandlerListenersKey = '__reactListeners$' + randomKey;
const internalEventHandlesSetKey = '__reactHandles$' + randomKey;
const internalRootNodeStylesSetKey = '__reactStyles$' + randomKey;
const internalRootNodeResourcesKey = '__reactResources$' + randomKey;
export function detachDeletedInstance(node: Instance): void {
// TODO: This function is only called on host components. I don't think all of
@@ -278,10 +282,15 @@ export function doesTargetHaveEventHandle(
return eventHandles.has(eventHandle);
}
export function getStylesFromRoot(root: FloatRoot): Map<string, StyleResource> {
let styles = (root: any)[internalRootNodeStylesSetKey];
if (!styles) {
styles = (root: any)[internalRootNodeStylesSetKey] = new Map();
export function getResourcesFromRoot(
root: FloatRoot,
): {styles: Map<string, StyleResource>, scripts: Map<string, ScriptResource>} {
let resources = (root: any)[internalRootNodeResourcesKey];
if (!resources) {
resources = (root: any)[internalRootNodeResourcesKey] = {
styles: new Map(),
scripts: new Map(),
};
}
return styles;
return resources;
}
+235 -46
View File
@@ -15,15 +15,16 @@ import {DOCUMENT_NODE} from '../shared/HTMLNodeType';
import {
validateUnmatchedLinkResourceProps,
validatePreloadResourceDifference,
validateHrefKeyedUpdatedProps,
validateURLKeyedUpdatedProps,
validateStyleResourceDifference,
validateScriptResourceDifference,
validateLinkPropsForStyleResource,
validateLinkPropsForPreloadResource,
validatePreloadArguments,
validatePreinitArguments,
} from '../shared/ReactDOMResourceValidation';
import {createElement, setInitialProperties} from './ReactDOMComponent';
import {getStylesFromRoot} from './ReactDOMComponentTree';
import {getResourcesFromRoot} from './ReactDOMComponentTree';
import {HTML_NAMESPACE} from '../shared/DOMNamespaces';
import {getCurrentRootHostContainer} from 'react-reconciler/src/ReactFiberHostContext';
@@ -33,7 +34,6 @@ type ResourceType = 'style' | 'font' | 'script';
type PreloadProps = {
rel: 'preload',
as: ResourceType,
href: string,
[string]: mixed,
};
@@ -48,7 +48,7 @@ type PreloadResource = {
type StyleProps = {
rel: 'stylesheet',
href: string,
'data-rprec': string,
'data-precedence': string,
[string]: mixed,
};
export type StyleResource = {
@@ -72,10 +72,22 @@ export type StyleResource = {
instance: ?Element,
root: FloatRoot,
};
type ScriptProps = {
src: string,
[string]: mixed,
};
export type ScriptResource = {
type: 'script',
src: string,
props: ScriptProps,
instance: ?Element,
root: FloatRoot,
};
type Props = {[string]: mixed};
type Resource = StyleResource | PreloadResource;
type Resource = StyleResource | ScriptResource | PreloadResource;
// Brief on purpose due to insertion by script when streaming late boundaries
// s = Status
@@ -202,11 +214,12 @@ function preloadPropsFromPreloadOptions(
// ReactDOM.preinit
// --------------------------------------
type PreinitAs = 'style';
type PreinitAs = 'style' | 'script';
type PreinitOptions = {
as: PreinitAs,
crossOrigin?: string,
precedence?: string,
crossOrigin?: string,
integrity?: string,
};
function preinit(href: string, options: PreinitOptions) {
if (__DEV__) {
@@ -243,7 +256,7 @@ function preinit(href: string, options: PreinitOptions) {
switch (as) {
case 'style': {
const styleResources = getStylesFromRoot(resourceRoot);
const styleResources = getResourcesFromRoot(resourceRoot).styles;
const precedence = options.precedence || 'default';
let resource = styleResources.get(href);
if (resource) {
@@ -270,6 +283,28 @@ function preinit(href: string, options: PreinitOptions) {
);
}
acquireResource(resource);
return;
}
case 'script': {
const src = href;
const scriptResources = getResourcesFromRoot(resourceRoot).scripts;
let resource = scriptResources.get(src);
if (resource) {
if (__DEV__) {
const latestProps = scriptPropsFromPreinitOptions(src, options);
validateScriptResourceDifference(resource.props, latestProps);
}
} else {
const resourceProps = scriptPropsFromPreinitOptions(src, options);
resource = createScriptResource(
scriptResources,
resourceRoot,
src,
resourceProps,
);
}
acquireResource(resource);
return;
}
}
}
@@ -285,6 +320,7 @@ function preloadPropsFromPreinitOptions(
rel: 'preload',
as,
crossOrigin: as === 'font' ? '' : options.crossOrigin,
integrity: options.integrity,
};
}
@@ -296,11 +332,23 @@ function stylePropsFromPreinitOptions(
return {
rel: 'stylesheet',
href,
'data-rprec': precedence,
'data-precedence': precedence,
crossOrigin: options.crossOrigin,
};
}
function scriptPropsFromPreinitOptions(
src: string,
options: PreinitOptions,
): ScriptProps {
return {
src,
async: true,
crossOrigin: options.crossOrigin,
integrity: options.integrity,
};
}
// --------------------------------------
// Resources from render
// --------------------------------------
@@ -314,7 +362,11 @@ type StyleQualifyingProps = {
type PreloadQualifyingProps = {
rel: 'preload',
href: string,
as: ResourceType,
[string]: mixed,
};
type ScriptQualifyingProps = {
src: string,
async: true,
[string]: mixed,
};
@@ -335,13 +387,15 @@ export function getResource(
const {rel} = pendingProps;
switch (rel) {
case 'stylesheet': {
const styleResources = getStylesFromRoot(resourceRoot);
const styleResources = getResourcesFromRoot(resourceRoot).styles;
let didWarn;
if (__DEV__) {
if (currentProps) {
didWarn = validateHrefKeyedUpdatedProps(
didWarn = validateURLKeyedUpdatedProps(
pendingProps,
currentProps,
'style',
'href',
);
}
if (!didWarn) {
@@ -360,7 +414,7 @@ export function getResource(
if (!didWarn) {
const latestProps = stylePropsFromRawProps(styleRawProps);
if ((resource: any)._dev_preload_props) {
adoptPreloadProps(
adoptPreloadPropsForStyle(
latestProps,
(resource: any)._dev_preload_props,
);
@@ -387,8 +441,8 @@ export function getResource(
if (__DEV__) {
validateLinkPropsForPreloadResource(pendingProps);
}
const {href, as} = pendingProps;
if (typeof href === 'string' && isResourceAsType(as)) {
const {href} = pendingProps;
if (typeof href === 'string') {
// We've asserted all the specific types for PreloadQualifyingProps
const preloadRawProps: PreloadQualifyingProps = (pendingProps: any);
let resource = preloadResources.get(href);
@@ -424,6 +478,49 @@ export function getResource(
}
}
}
case 'script': {
const scriptResources = getResourcesFromRoot(resourceRoot).scripts;
let didWarn;
if (__DEV__) {
if (currentProps) {
didWarn = validateURLKeyedUpdatedProps(
pendingProps,
currentProps,
'script',
'src',
);
}
}
const {src, async} = pendingProps;
if (async && typeof src === 'string') {
const scriptRawProps: ScriptQualifyingProps = (pendingProps: any);
let resource = scriptResources.get(src);
if (resource) {
if (__DEV__) {
if (!didWarn) {
const latestProps = scriptPropsFromRawProps(scriptRawProps);
if ((resource: any)._dev_preload_props) {
adoptPreloadPropsForScript(
latestProps,
(resource: any)._dev_preload_props,
);
}
validateScriptResourceDifference(resource.props, latestProps);
}
}
} else {
const resourceProps = scriptPropsFromRawProps(scriptRawProps);
resource = createScriptResource(
scriptResources,
resourceRoot,
src,
resourceProps,
);
}
return resource;
}
return null;
}
default: {
throw new Error(
`getResource encountered a resource type it did not expect: "${type}". this is a bug in React.`,
@@ -440,12 +537,17 @@ function preloadPropsFromRawProps(
function stylePropsFromRawProps(rawProps: StyleQualifyingProps): StyleProps {
const props: StyleProps = Object.assign({}, rawProps);
props['data-rprec'] = rawProps.precedence;
props['data-precedence'] = rawProps.precedence;
props.precedence = null;
return props;
}
function scriptPropsFromRawProps(rawProps: ScriptQualifyingProps): ScriptProps {
const props: ScriptProps = Object.assign({}, rawProps);
return props;
}
// --------------------------------------
// Resource Reconciliation
// --------------------------------------
@@ -455,6 +557,9 @@ export function acquireResource(resource: Resource): Instance {
case 'style': {
return acquireStyleResource(resource);
}
case 'script': {
return acquireScriptResource(resource);
}
case 'preload': {
return resource.instance;
}
@@ -558,7 +663,7 @@ function createStyleResource(
// 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);
adoptPreloadPropsForStyle(resource.props, hint.props);
if (__DEV__) {
(resource: any)._dev_preload_props = preloadProps;
}
@@ -568,7 +673,7 @@ function createStyleResource(
return resource;
}
function adoptPreloadProps(
function adoptPreloadPropsForStyle(
styleProps: StyleProps,
preloadProps: PreloadProps,
): void {
@@ -576,7 +681,6 @@ function adoptPreloadProps(
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;
}
@@ -610,6 +714,63 @@ function preloadPropsFromStyleProps(props: StyleProps): PreloadProps {
};
}
function createScriptResource(
scriptResources: Map<string, ScriptResource>,
root: FloatRoot,
src: string,
props: ScriptProps,
): ScriptResource {
if (__DEV__) {
if (scriptResources.has(src)) {
console.error(
'createScriptResource was called when a script Resource matching the same src already exists. This is a bug in React.',
);
}
}
const limitedEscapedSrc = escapeSelectorAttributeValueInsideDoubleQuotes(src);
const existingEl = root.querySelector(
`script[async][src="${limitedEscapedSrc}"]`,
);
const resource = {
type: 'script',
src,
props,
root,
instance: existingEl || null,
};
scriptResources.set(src, resource);
if (!existingEl) {
const hint = preloadResources.get(src);
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;
adoptPreloadPropsForScript(props, hint.props);
if (__DEV__) {
(resource: any)._dev_preload_props = preloadProps;
}
}
}
return resource;
}
function adoptPreloadPropsForScript(
scriptProps: ScriptProps,
preloadProps: PreloadProps,
): void {
if (scriptProps.crossOrigin == null)
scriptProps.crossOrigin = preloadProps.crossOrigin;
if (scriptProps.referrerPolicy == null)
scriptProps.referrerPolicy = preloadProps.referrerPolicy;
if (scriptProps.integrity == null)
scriptProps.referrerPolicy = preloadProps.integrity;
}
function createPreloadResource(
ownerDocument: Document,
href: string,
@@ -623,7 +784,7 @@ function createPreloadResource(
);
if (!element) {
element = createResourceInstance('link', props, ownerDocument);
insertPreloadInstance(element, ownerDocument);
insertResourceInstance(element, ownerDocument);
}
return {
type: 'preload',
@@ -641,7 +802,7 @@ function acquireStyleResource(resource: StyleResource): Instance {
props.href,
);
const existingEl = root.querySelector(
`link[rel="stylesheet"][data-rprec][href="${limitedEscapedHref}"]`,
`link[rel="stylesheet"][data-precedence][href="${limitedEscapedHref}"]`,
);
if (existingEl) {
resource.instance = existingEl;
@@ -685,6 +846,31 @@ function acquireStyleResource(resource: StyleResource): Instance {
return resource.instance;
}
function acquireScriptResource(resource: ScriptResource): Instance {
if (!resource.instance) {
const {props, root} = resource;
const limitedEscapedSrc = escapeSelectorAttributeValueInsideDoubleQuotes(
props.src,
);
const existingEl = root.querySelector(
`script[async][src="${limitedEscapedSrc}"]`,
);
if (existingEl) {
resource.instance = existingEl;
} else {
const instance = createResourceInstance(
'script',
resource.props,
getDocumentFromRoot(root),
);
insertResourceInstance(instance, getDocumentFromRoot(root));
resource.instance = instance;
}
}
return resource.instance;
}
function attachLoadListeners(instance: Instance, resource: StyleResource) {
const listeners = {};
listeners.load = onResourceLoad.bind(
@@ -749,12 +935,14 @@ function insertStyleInstance(
precedence: string,
root: FloatRoot,
): void {
const nodes = root.querySelectorAll('link[rel="stylesheet"][data-rprec]');
const nodes = root.querySelectorAll(
'link[rel="stylesheet"][data-precedence]',
);
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;
const nodePrecedence = node.dataset.precedence;
if (nodePrecedence === precedence) {
prior = node;
} else if (prior !== last) {
@@ -780,21 +968,27 @@ function insertStyleInstance(
}
}
function insertPreloadInstance(
function insertResourceInstance(
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.',
if (__DEV__) {
if (instance.tagName === 'LINK' && (instance: any).rel === 'stylesheet') {
console.error(
'insertResourceInstance was called with a stylesheet. Stylesheets must be' +
' inserted with insertStyleInstance instead. This is a bug in React.',
);
}
}
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 {
@@ -815,27 +1009,22 @@ export function isHostResourceType(type: string, props: Props): boolean {
);
}
case 'preload': {
if (__DEV__) {
validateLinkPropsForStyleResource(props);
}
const {href, as, onLoad, onError} = props;
return (
!onLoad &&
!onError &&
typeof href === 'string' &&
isResourceAsType(as)
);
const {href, onLoad, onError} = props;
return !onLoad && !onError && typeof href === 'string';
}
}
return false;
}
case 'script': {
// We don't validate because it is valid to use async with onLoad/onError unlike combining
// precedence with these for style resources
const {src, async, onLoad, onError} = props;
return (async: any) && typeof src === 'string' && !onLoad && !onError;
}
}
return false;
}
function isResourceAsType(as: mixed): boolean {
return as === 'style' || as === 'font' || as === 'script';
}
// 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
@@ -794,6 +794,20 @@ export function bindInstance(
export const supportsHydration = true;
// With Resources, some HostComponent types will never be server rendered and need to be
// inserted without breaking hydration
export function isHydratable(type: string, props: Props): boolean {
if (enableFloat) {
if (type === 'script') {
const {async, onLoad, onError} = (props: any);
return !(async && (onLoad || onError));
}
return true;
} else {
return true;
}
}
export function canHydrateInstance(
instance: HydratableInstance,
type: string,
@@ -889,12 +903,26 @@ function getNextHydratable(node) {
const rel = linkEl.rel;
if (
rel === 'preload' ||
(rel === 'stylesheet' && linkEl.hasAttribute('data-rprec'))
(rel === 'stylesheet' && linkEl.hasAttribute('data-precedence'))
) {
continue;
}
break;
}
case 'STYLE': {
const styleEl: HTMLStyleElement = (element: any);
if (styleEl.hasAttribute('data-precedence')) {
continue;
}
break;
}
case 'SCRIPT': {
const scriptEl: HTMLScriptElement = (element: any);
if (scriptEl.hasAttribute('async')) {
continue;
}
break;
}
case 'HTML':
case 'HEAD':
case 'BODY': {
@@ -908,14 +936,31 @@ function getNextHydratable(node) {
} else if (enableFloat) {
if (nodeType === ELEMENT_NODE) {
const element: Element = (node: any);
if (element.tagName === 'LINK') {
const linkEl: HTMLLinkElement = (element: any);
const rel = linkEl.rel;
if (
rel === 'preload' ||
(rel === 'stylesheet' && linkEl.hasAttribute('data-rprec'))
) {
continue;
switch (element.tagName) {
case 'LINK': {
const linkEl: HTMLLinkElement = (element: any);
const rel = linkEl.rel;
if (
rel === 'preload' ||
(rel === 'stylesheet' && linkEl.hasAttribute('data-precedence'))
) {
continue;
}
break;
}
case 'STYLE': {
const styleEl: HTMLStyleElement = (element: any);
if (styleEl.hasAttribute('data-precedence')) {
continue;
}
break;
}
case 'SCRIPT': {
const scriptEl: HTMLScriptElement = (element: any);
if (scriptEl.hasAttribute('async')) {
continue;
}
break;
}
}
break;
+279 -104
View File
@@ -11,6 +11,8 @@ import {
validatePreloadResourceDifference,
validateStyleResourceDifference,
validateStyleAndHintProps,
validateScriptResourceDifference,
validateScriptAndHintProps,
validateLinkPropsForStyleResource,
validateLinkPropsForPreloadResource,
validatePreloadArguments,
@@ -38,7 +40,7 @@ type PreloadResource = {
type StyleProps = {
rel: 'stylesheet',
href: string,
'data-rprec': string,
'data-precedence': string,
[string]: mixed,
};
type StyleResource = {
@@ -50,22 +52,44 @@ type StyleResource = {
flushed: boolean,
inShell: boolean, // flushedInShell
hint: PreloadResource,
set: Set<StyleResource>, // the precedence set this resource should be flushed in
};
export type Resource = PreloadResource | StyleResource;
type ScriptProps = {
src: string,
[string]: mixed,
};
type ScriptResource = {
type: 'script',
src: string,
props: ScriptProps,
flushed: boolean,
hint: PreloadResource,
};
export type Resource = PreloadResource | StyleResource | ScriptResource;
export type Resources = {
// Request local cache
preloadsMap: Map<string, PreloadResource>,
stylesMap: Map<string, StyleResource>,
scriptsMap: Map<string, ScriptResource>,
// Flushing queues for Resource dependencies
explicitPreloads: Set<PreloadResource>,
implicitPreloads: Set<PreloadResource>,
fontPreloads: Set<PreloadResource>,
// usedImagePreloads: Set<PreloadResource>,
precedences: Map<string, Set<StyleResource>>,
usedStylePreloads: Set<PreloadResource>,
scripts: Set<ScriptResource>,
usedScriptPreloads: Set<PreloadResource>,
explicitStylePreloads: Set<PreloadResource>,
// explicitImagePreloads: Set<PreloadResource>,
explicitScriptPreloads: Set<PreloadResource>,
// Module-global-like reference for current boundary resources
boundaryResources: ?BoundaryResources,
...
};
// @TODO add bootstrap script to implicit preloads
@@ -74,11 +98,18 @@ export function createResources(): Resources {
// persistent
preloadsMap: new Map(),
stylesMap: new Map(),
scriptsMap: new Map(),
// cleared on flush
explicitPreloads: new Set(),
implicitPreloads: new Set(),
fontPreloads: new Set(),
// usedImagePreloads: new Set(),
precedences: new Map(),
usedStylePreloads: new Set(),
scripts: new Set(),
usedScriptPreloads: new Set(),
explicitStylePreloads: new Set(),
// explicitImagePreloads: new Set(),
explicitScriptPreloads: new Set(),
// like a module global for currently rendering boundary
boundaryResources: null,
@@ -91,13 +122,6 @@ 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 = [];
@@ -134,6 +158,7 @@ function preload(href: string, options: PreloadOptions) {
// simply return and do not warn.
return;
}
const resources = currentResources;
if (__DEV__) {
validatePreloadArguments(href, options);
}
@@ -144,8 +169,7 @@ function preload(href: string, options: PreloadOptions) {
options !== null
) {
const as = options.as;
// $FlowFixMe[incompatible-use] found when upgrading Flow
let resource = currentResources.preloadsMap.get(href);
let resource = resources.preloadsMap.get(href);
if (resource) {
if (__DEV__) {
const originallyImplicit =
@@ -160,23 +184,35 @@ function preload(href: string, options: PreloadOptions) {
}
} else {
resource = createPreloadResource(
// $FlowFixMe[incompatible-call] found when upgrading Flow
currentResources,
resources,
href,
as,
preloadPropsFromPreloadOptions(href, as, options),
);
}
// $FlowFixMe[incompatible-call] found when upgrading Flow
captureExplicitPreloadResourceDependency(currentResources, resource);
switch (as) {
case 'font': {
resources.fontPreloads.add(resource);
break;
}
case 'style': {
resources.explicitStylePreloads.add(resource);
break;
}
case 'script': {
resources.explicitScriptPreloads.add(resource);
break;
}
}
}
}
type PreinitAs = 'style';
type PreinitAs = 'style' | 'script';
type PreinitOptions = {
as: PreinitAs,
precedence?: string,
crossOrigin?: string,
integrity?: string,
};
function preinit(href: string, options: PreinitOptions) {
if (!currentResources) {
@@ -188,6 +224,7 @@ function preinit(href: string, options: PreinitOptions) {
// simply return and do not warn.
return;
}
const resources = currentResources;
if (__DEV__) {
validatePreinitArguments(href, options);
}
@@ -200,38 +237,48 @@ function preinit(href: string, options: PreinitOptions) {
const as = options.as;
switch (as) {
case 'style': {
const precedence = options.precedence || 'default';
// $FlowFixMe[incompatible-use] found when upgrading Flow
let resource = currentResources.stylesMap.get(href);
let resource = resources.stylesMap.get(href);
if (resource) {
if (__DEV__) {
const latestProps = stylePropsFromPreinitOptions(
href,
precedence,
resource.precedence,
options,
);
validateStyleResourceDifference(resource.props, latestProps);
}
} else {
const precedence = options.precedence || 'default';
const resourceProps = stylePropsFromPreinitOptions(
href,
precedence,
options,
);
resource = createStyleResource(
// $FlowFixMe[incompatible-call] found when upgrading Flow
currentResources,
resources,
href,
precedence,
resourceProps,
);
}
resource.set.add(resource);
resources.explicitStylePreloads.add(resource.hint);
// Do not associate preinit style resources with any specific boundary regardless of where it is called
// $FlowFixMe[incompatible-call] found when upgrading Flow
captureStyleResourceDependency(currentResources, null, resource);
return;
}
case 'script': {
const src = href;
let resource = resources.scriptsMap.get(src);
if (resource) {
if (__DEV__) {
const latestProps = scriptPropsFromPreinitOptions(src, options);
validateScriptResourceDifference(resource.props, latestProps);
}
} else {
const scriptProps = scriptPropsFromPreinitOptions(src, options);
resource = createScriptResource(resources, src, scriptProps);
resources.scripts.add(resource);
}
return;
}
}
@@ -286,6 +333,20 @@ function preloadAsStylePropsFromProps(
};
}
function preloadAsScriptPropsFromProps(
href: string,
props: Props | ScriptProps,
): PreloadProps {
return {
rel: 'preload',
as: 'script',
href,
crossOrigin: props.crossOrigin,
integrity: props.integrity,
referrerPolicy: props.referrerPolicy,
};
}
function createPreloadResource(
resources: Resources,
href: string,
@@ -320,7 +381,7 @@ function stylePropsFromRawProps(
const props: StyleProps = Object.assign({}, rawProps);
props.href = href;
props.rel = 'stylesheet';
props['data-rprec'] = precedence;
props['data-precedence'] = precedence;
delete props.precedence;
return props;
@@ -334,7 +395,7 @@ function stylePropsFromPreinitOptions(
return {
rel: 'stylesheet',
href,
'data-rprec': precedence,
'data-precedence': precedence,
crossOrigin: options.crossOrigin,
};
}
@@ -352,7 +413,15 @@ function createStyleResource(
);
}
}
const {stylesMap, preloadsMap} = resources;
const {stylesMap, preloadsMap, precedences} = resources;
// If this is the first time we've seen this precedence we encode it's position in our set even though
// we don't add the resource to this set yet
let precedenceSet = precedences.get(precedence);
if (!precedenceSet) {
precedenceSet = new Set();
precedences.set(precedence, precedenceSet);
}
let hint = preloadsMap.get(href);
if (hint) {
@@ -360,16 +429,11 @@ function createStyleResource(
// 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;
adoptPreloadPropsForStyleProps(props, hint.props);
if (__DEV__) {
validateStyleAndHintProps(
preloadProps,
hint.props,
props,
(hint: any)._dev_implicit_construction,
);
@@ -385,7 +449,7 @@ function createStyleResource(
if (__DEV__) {
(hint: any)._dev_implicit_construction = true;
}
captureImplicitPreloadResourceDependency(resources, hint);
resources.explicitStylePreloads.add(hint);
}
const resource = {
@@ -396,47 +460,107 @@ function createStyleResource(
inShell: false,
props,
hint,
set: precedenceSet,
};
stylesMap.set(href, resource);
return resource;
}
function captureStyleResourceDependency(
resources: Resources,
boundaryResources: ?BoundaryResources,
styleResource: StyleResource,
function adoptPreloadPropsForStyleProps(
resourceProps: StyleProps,
preloadProps: PreloadProps,
): void {
const {precedences} = resources;
const {precedence} = styleResource;
if (resourceProps.crossOrigin == null)
resourceProps.crossOrigin = preloadProps.crossOrigin;
if (resourceProps.referrerPolicy == null)
resourceProps.referrerPolicy = preloadProps.referrerPolicy;
if (resourceProps.title == null) resourceProps.title = preloadProps.title;
}
if (boundaryResources) {
boundaryResources.add(styleResource);
if (!precedences.has(precedence)) {
precedences.set(precedence, new Set());
function scriptPropsFromPreinitOptions(
src: string,
options: PreinitOptions,
): ScriptProps {
return {
src,
async: true,
crossOrigin: options.crossOrigin,
integrity: options.integrity,
};
}
function scriptPropsFromRawProps(src: string, rawProps: Props): ScriptProps {
const props = Object.assign({}, rawProps);
props.src = src;
return props;
}
function createScriptResource(
resources: Resources,
src: string,
props: ScriptProps,
): ScriptResource {
if (__DEV__) {
if (resources.scriptsMap.has(src)) {
console.error(
'createScriptResource was called when a script Resource matching the same src already exists. This is a bug in React.',
);
}
}
const {scriptsMap, preloadsMap} = resources;
let hint = preloadsMap.get(src);
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
adoptPreloadPropsForScriptProps(props, hint.props);
if (__DEV__) {
validateScriptAndHintProps(
hint.props,
props,
(hint: any)._dev_implicit_construction,
);
}
} else {
let set = precedences.get(precedence);
if (!set) {
set = new Set();
precedences.set(precedence, set);
const preloadResourceProps = preloadAsScriptPropsFromProps(src, props);
hint = createPreloadResource(
resources,
src,
'script',
preloadResourceProps,
);
if (__DEV__) {
(hint: any)._dev_implicit_construction = true;
}
set.add(styleResource);
resources.explicitScriptPreloads.add(hint);
}
const resource = {
type: 'script',
src,
flushed: false,
props,
hint,
};
scriptsMap.set(src, resource);
return resource;
}
function captureExplicitPreloadResourceDependency(
resources: Resources,
preloadResource: PreloadResource,
function adoptPreloadPropsForScriptProps(
resourceProps: ScriptProps,
preloadProps: PreloadProps,
): void {
resources.explicitPreloads.add(preloadResource);
}
function captureImplicitPreloadResourceDependency(
resources: Resources,
preloadResource: PreloadResource,
): void {
resources.implicitPreloads.add(preloadResource);
if (resourceProps.crossOrigin == null)
resourceProps.crossOrigin = preloadProps.crossOrigin;
if (resourceProps.referrerPolicy == null)
resourceProps.referrerPolicy = preloadProps.referrerPolicy;
if (resourceProps.integrity == null)
resourceProps.integrity = preloadProps.integrity;
}
// Construct a resource from link props.
@@ -446,6 +570,8 @@ export function resourcesFromLink(props: Props): boolean {
'"currentResources" was expected to exist. This is a bug in React.',
);
}
const resources = currentResources;
const {rel, href} = props;
if (!href || typeof href !== 'string') {
return false;
@@ -467,11 +593,11 @@ export function resourcesFromLink(props: Props): boolean {
validateLinkPropsForStyleResource(props);
}
// $FlowFixMe[incompatible-use] found when upgrading Flow
let preloadResource = currentResources.preloadsMap.get(href);
let preloadResource = resources.preloadsMap.get(href);
if (!preloadResource) {
preloadResource = createPreloadResource(
// $FlowFixMe[incompatible-call] found when upgrading Flow
currentResources,
resources,
href,
'style',
preloadAsStylePropsFromProps(href, props),
@@ -479,17 +605,14 @@ export function resourcesFromLink(props: Props): boolean {
if (__DEV__) {
(preloadResource: any)._dev_implicit_construction = true;
}
resources.usedStylePreloads.add(preloadResource);
}
captureImplicitPreloadResourceDependency(
// $FlowFixMe[incompatible-call] found when upgrading Flow
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);
let resource = resources.stylesMap.get(href);
if (resource) {
if (__DEV__) {
const resourceProps = stylePropsFromRawProps(
@@ -497,6 +620,7 @@ export function resourcesFromLink(props: Props): boolean {
precedence,
props,
);
adoptPreloadPropsForStyleProps(resourceProps, resource.hint.props);
validateStyleResourceDifference(resource.props, resourceProps);
}
} else {
@@ -508,24 +632,18 @@ export function resourcesFromLink(props: Props): boolean {
precedence,
resourceProps,
);
resources.usedStylePreloads.add(resource.hint);
}
if (resources.boundaryResources) {
resources.boundaryResources.add(resource);
} else {
resource.set.add(resource);
}
captureStyleResourceDependency(
// $FlowFixMe[incompatible-call] found when upgrading Flow
currentResources,
// $FlowFixMe[incompatible-use] found when upgrading Flow
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;
}
const {as} = props;
switch (as) {
case 'script':
case 'style':
@@ -533,8 +651,7 @@ export function resourcesFromLink(props: Props): boolean {
if (__DEV__) {
validateLinkPropsForPreloadResource(props);
}
// $FlowFixMe[incompatible-use] found when upgrading Flow
let resource = currentResources.preloadsMap.get(href);
let resource = resources.preloadsMap.get(href);
if (resource) {
if (__DEV__) {
const originallyImplicit =
@@ -549,15 +666,26 @@ export function resourcesFromLink(props: Props): boolean {
}
} else {
resource = createPreloadResource(
// $FlowFixMe[incompatible-call] found when upgrading Flow
currentResources,
resources,
href,
as,
preloadPropsFromRawProps(href, as, props),
);
switch (as) {
case 'script': {
resources.explicitScriptPreloads.add(resource);
break;
}
case 'style': {
resources.explicitStylePreloads.add(resource);
break;
}
case 'font': {
resources.fontPreloads.add(resource);
break;
}
}
}
// $FlowFixMe[incompatible-call] found when upgrading Flow
captureExplicitPreloadResourceDependency(currentResources, resource);
return true;
}
}
@@ -567,12 +695,65 @@ export function resourcesFromLink(props: Props): boolean {
return false;
}
// Construct a resource from link props.
export function resourcesFromScript(props: Props): boolean {
if (!currentResources) {
throw new Error(
'"currentResources" was expected to exist. This is a bug in React.',
);
}
const resources = currentResources;
const {src, async, onLoad, onError} = props;
if (!src || typeof src !== 'string') {
return false;
}
if (async) {
if (onLoad || onError) {
if (__DEV__) {
// validate
}
let preloadResource = resources.preloadsMap.get(src);
if (!preloadResource) {
preloadResource = createPreloadResource(
// $FlowFixMe[incompatible-call] found when upgrading Flow
resources,
src,
'script',
preloadAsScriptPropsFromProps(src, props),
);
if (__DEV__) {
(preloadResource: any)._dev_implicit_construction = true;
}
resources.usedScriptPreloads.add(preloadResource);
}
} else {
let resource = resources.scriptsMap.get(src);
if (resource) {
if (__DEV__) {
const latestProps = scriptPropsFromRawProps(src, props);
adoptPreloadPropsForScriptProps(latestProps, resource.hint.props);
validateScriptResourceDifference(resource.props, latestProps);
}
} else {
const resourceProps = scriptPropsFromRawProps(src, props);
resource = createScriptResource(resources, src, resourceProps);
resources.scripts.add(resource);
}
}
return true;
}
return false;
}
export function hoistResources(
resources: Resources,
source: BoundaryResources,
): void {
if (resources.boundaryResources) {
mergeBoundaryResources(resources.boundaryResources, source);
const currentBoundaryResources = resources.boundaryResources;
if (currentBoundaryResources) {
source.forEach(resource => currentBoundaryResources.add(resource));
source.clear();
}
}
@@ -581,12 +762,6 @@ 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.forEach(resource => resource.set.add(resource));
boundaryResources.clear();
}
@@ -64,6 +64,7 @@ import {
prepareToRenderResources,
finishRenderingResources,
resourcesFromLink,
resourcesFromScript,
ReactDOMServerDispatcher,
} from './ReactDOMFloatServer';
export {
@@ -1349,6 +1350,26 @@ function pushStartHtml(
return pushStartGenericElement(target, props, tag, responseState);
}
function pushStartScript(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
responseState: ResponseState,
textEmbedded: boolean,
): ReactNodeList {
if (enableFloat && resourcesFromScript(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 pushStartGenericElement(target, props, 'script', responseState);
}
function pushStartGenericElement(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
@@ -1625,6 +1646,8 @@ export function pushStartInstance(
return pushStartTitle(target, props, responseState);
case 'link':
return pushLink(target, props, responseState, textEmbedded);
case 'script':
return pushStartScript(target, props, responseState, textEmbedded);
// Newline eating tags
case 'listing':
case 'pre': {
@@ -2235,57 +2258,90 @@ function escapeJSObjectForInstructionScripts(input: Object): string {
});
}
const precedencePlaceholderStart = stringToPrecomputedChunk(
'<style data-precedence="',
);
const precedencePlaceholderEnd = stringToPrecomputedChunk('"></style>');
export function writeInitialResources(
destination: Destination,
resources: Resources,
responseState: ResponseState,
): boolean {
const explicitPreloadsTarget = [];
const remainingTarget = [];
function flushLinkResource(resource) {
if (!resource.flushed) {
pushLinkImpl(target, resource.props, responseState);
resource.flushed = true;
}
}
const {precedences, explicitPreloads, implicitPreloads} = resources;
const target = [];
const {
fontPreloads,
precedences,
usedStylePreloads,
scripts,
usedScriptPreloads,
explicitStylePreloads,
explicitScriptPreloads,
} = resources;
fontPreloads.forEach(r => {
// font preload Resources should not already be flushed so we elide this check
pushLinkImpl(target, r.props, responseState);
r.flushed = true;
});
fontPreloads.clear();
// 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;
precedences.forEach((p, precedence) => {
if (p.size) {
p.forEach(r => {
// resources should not already be flushed so we elide this check
pushLinkImpl(target, r.props, responseState);
r.flushed = true;
r.inShell = true;
r.hint.flushed = true;
});
p.clear();
} else {
target.push(
precedencePlaceholderStart,
escapeTextForBrowser(stringToChunk(precedence)),
precedencePlaceholderEnd,
);
}
});
explicitPreloads.clear();
implicitPreloads.forEach(resource => {
if (!resource.flushed) {
pushLinkImpl(remainingTarget, resource.props, responseState);
resource.flushed = true;
}
usedStylePreloads.forEach(flushLinkResource);
usedStylePreloads.clear();
scripts.forEach(r => {
// should never be flushed already
pushStartGenericElement(target, r.props, 'script', responseState);
pushEndInstance(target, target, 'script', r.props);
r.flushed = true;
r.hint.flushed = true;
});
implicitPreloads.clear();
scripts.clear();
usedScriptPreloads.forEach(flushLinkResource);
usedScriptPreloads.clear();
explicitStylePreloads.forEach(flushLinkResource);
explicitStylePreloads.clear();
explicitScriptPreloads.forEach(flushLinkResource);
explicitScriptPreloads.clear();
let i;
let r = true;
for (i = 0; i < explicitPreloadsTarget.length - 1; i++) {
writeChunk(destination, explicitPreloadsTarget[i]);
for (i = 0; i < target.length - 1; i++) {
writeChunk(destination, target[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]);
if (i < target.length) {
r = writeChunkAndReturn(destination, target[i]);
}
return r;
}
@@ -2295,33 +2351,61 @@ export function writeImmediateResources(
resources: Resources,
responseState: ResponseState,
): boolean {
const {explicitPreloads, implicitPreloads} = resources;
function flushLinkResource(resource) {
if (!resource.flushed) {
pushLinkImpl(target, resource.props, responseState);
resource.flushed = true;
}
}
const target = [];
explicitPreloads.forEach(resource => {
if (!resource.flushed) {
pushLinkImpl(target, resource.props, responseState);
resource.flushed = true;
}
});
explicitPreloads.clear();
const {
fontPreloads,
usedStylePreloads,
scripts,
usedScriptPreloads,
explicitStylePreloads,
explicitScriptPreloads,
} = resources;
implicitPreloads.forEach(resource => {
if (!resource.flushed) {
pushLinkImpl(target, resource.props, responseState);
resource.flushed = true;
}
fontPreloads.forEach(r => {
// font preload Resources should not already be flushed so we elide this check
pushLinkImpl(target, r.props, responseState);
r.flushed = true;
});
implicitPreloads.clear();
fontPreloads.clear();
let i = 0;
for (; i < target.length - 1; i++) {
usedStylePreloads.forEach(flushLinkResource);
usedStylePreloads.clear();
scripts.forEach(r => {
// should never be flushed already
pushStartGenericElement(target, r.props, 'script', responseState);
pushEndInstance(target, target, 'script', r.props);
r.flushed = true;
r.hint.flushed = true;
});
scripts.clear();
usedScriptPreloads.forEach(flushLinkResource);
usedScriptPreloads.clear();
explicitStylePreloads.forEach(flushLinkResource);
explicitStylePreloads.clear();
explicitScriptPreloads.forEach(flushLinkResource);
explicitScriptPreloads.clear();
let i;
let r = true;
for (i = 0; i < target.length - 1; i++) {
writeChunk(destination, target[i]);
}
if (i < target.length) {
return writeChunkAndReturn(destination, target[i]);
r = writeChunkAndReturn(destination, target[i]);
}
return false;
return r;
}
function hasStyleResourceDependencies(
@@ -2434,7 +2518,7 @@ function writeStyleResourceDependency(
case 'href':
case 'rel':
case 'precedence':
case 'data-rprec': {
case 'data-precedence': {
break;
}
case 'children':
@@ -57,9 +57,11 @@ export function completeBoundaryWithStyles(
let lastResource, node;
// Seed the precedence list with existing resources
const nodes = thisDocument.querySelectorAll('link[data-rprec]');
const nodes = thisDocument.querySelectorAll(
'link[data-precedence],style[data-precedence]',
);
for (let i = 0; (node = nodes[i++]); ) {
precedences.set(node.dataset['rprec'], (lastResource = node));
precedences.set(node.dataset['precedence'], (lastResource = node));
}
let i = 0;
@@ -89,7 +91,7 @@ export function completeBoundaryWithStyles(
resourceEl = thisDocument.createElement('link');
resourceEl.href = href;
resourceEl.rel = 'stylesheet';
resourceEl.dataset['rprec'] = precedence = style[j++];
resourceEl.dataset['precedence'] = precedence = style[j++];
while ((attr = style[j++])) {
resourceEl.setAttribute(attr, style[j++]);
}
@@ -6,6 +6,6 @@ export const clientRenderBoundary =
export const completeBoundary =
'$RC=function(b,c,e){c=document.getElementById(c);c.parentNode.removeChild(c);var a=document.getElementById(b);if(a){b=a.previousSibling;if(e)b.data="$!",a.setAttribute("data-dgst",e);else{e=b.parentNode;a=b.nextSibling;var f=0;do{if(a&&8===a.nodeType){var d=a.data;if("/$"===d)if(0===f)break;else f--;else"$"!==d&&"$?"!==d&&"$!"!==d||f++}d=a.nextSibling;e.removeChild(a);a=d}while(a);for(;c.firstChild;)e.insertBefore(c.firstChild,a);b.data="$"}b._reactRetry&&b._reactRetry()}};';
export const completeBoundaryWithStyles =
'$RM=new Map;\n$RR=function(p,q,v){function r(l){this.s=l}for(var t=$RC,u=$RM,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=v[e++];){var k=0;h=c[k++];if(b=u.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,w){a.onload=l;a.onerror=w});b.then(r.bind(b,"l"),r.bind(b,"e"));u.set(h,\nb);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(t.bind(null,p,q,""),t.bind(null,p,q,"Resource failed to load"))};';
'$RM=new Map;\n$RR=function(p,q,v){function r(l){this.s=l}for(var t=$RC,u=$RM,m=new Map,n=document,g,e,f=n.querySelectorAll("link[data-precedence],style[data-precedence]"),d=0;e=f[d++];)m.set(e.dataset.precedence,g=e);e=0;f=[];for(var c,h,b,a;c=v[e++];){var k=0;h=c[k++];if(b=u.get(h))"l"!==b.s&&f.push(b);else{a=n.createElement("link");a.href=h;a.rel="stylesheet";for(a.dataset.precedence=d=c[k++];b=c[k++];)a.setAttribute(b,c[k++]);b=a._p=new Promise(function(l,w){a.onload=l;a.onerror=w});b.then(r.bind(b,\n"l"),r.bind(b,"e"));u.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(t.bind(null,p,q,""),t.bind(null,p,q,"Resource failed to load"))};';
export const completeSegment =
'$RS=function(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)};';
@@ -115,6 +115,7 @@ export function validatePreloadResourceDifference(
if (missingProps || extraProps || differentProps) {
warnDifferentProps(
href,
'href',
originalWarningName,
latestWarningName,
extraProps,
@@ -156,7 +157,7 @@ export function validateStyleResourceDifference(
const originalValue = originalProps[propName];
if (propValue != null && propValue !== originalValue) {
propName = propName === 'data-rprec' ? 'precedence' : propName;
propName = propName === 'data-precedence' ? 'precedence' : propName;
if (originalValue == null) {
extraProps = extraProps || {};
extraProps[propName] = propValue;
@@ -173,6 +174,7 @@ export function validateStyleResourceDifference(
if (missingProps || extraProps || differentProps) {
warnDifferentProps(
href,
'href',
originalWarningName,
latestWarningName,
extraProps,
@@ -183,6 +185,58 @@ export function validateStyleResourceDifference(
}
}
export function validateScriptResourceDifference(
originalProps: any,
latestProps: any,
) {
if (__DEV__) {
const {src} = originalProps;
// eslint-disable-next-line no-labels
const originalWarningName = getResourceNameForWarning(
'script',
originalProps,
false,
);
const latestWarningName = getResourceNameForWarning(
'script',
latestProps,
false,
);
let extraProps = null;
let differentProps = null;
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 (extraProps || differentProps) {
warnDifferentProps(
src,
'src',
originalWarningName,
latestWarningName,
extraProps,
null,
differentProps,
);
}
}
}
export function validateStyleAndHintProps(
preloadProps: any,
styleProps: any,
@@ -205,7 +259,7 @@ export function validateStyleAndHintProps(
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 must be of type "style". This most likely ocurred by rendering a preload link with an incorrect' +
' "as" prop or by calling ReactDOM.preload with an incorrect "as" option.',
latestWarningName,
href,
@@ -252,6 +306,86 @@ export function validateStyleAndHintProps(
if (missingProps || extraProps || differentProps) {
warnDifferentProps(
href,
'href',
originalWarningName,
latestWarningName,
extraProps,
missingProps,
differentProps,
);
}
}
}
export function validateScriptAndHintProps(
preloadProps: any,
scriptProps: any,
implicitPreload: boolean,
) {
if (__DEV__) {
const {href} = preloadProps;
const originalWarningName = getResourceNameForWarning(
'preload',
preloadProps,
implicitPreload,
);
const latestWarningName = getResourceNameForWarning(
'script',
scriptProps,
false,
);
if (preloadProps.as !== 'script') {
console.error(
'While creating a %s for href "%s" a %s for this same url was found. When preloading a script the' +
' "as" prop must be of type "script". This most likely ocurred by rendering 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 scriptProps) {
const scriptValue = scriptProps[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 'integrity': {
if (
preloadValue !== scriptValue &&
!(preloadValue == null && scriptValue == null)
) {
if (scriptValue == null) {
missingProps = missingProps || {};
missingProps[propName] = preloadValue;
} else if (preloadValue == null) {
extraProps = extraProps || {};
extraProps[propName] = scriptValue;
} else {
differentProps = differentProps || {};
differentProps[propName] = {
original: preloadValue,
latest: scriptValue,
};
}
}
}
}
}
if (missingProps || extraProps || differentProps) {
warnDifferentProps(
href,
'href',
originalWarningName,
latestWarningName,
extraProps,
@@ -263,7 +397,8 @@ export function validateStyleAndHintProps(
}
function warnDifferentProps(
href: string,
url: string,
urlPropKey: string,
originalName: string,
latestName: string,
extraProps: ?{[string]: any},
@@ -274,7 +409,7 @@ function warnDifferentProps(
const juxtaposedNameStatement =
latestName === originalName
? 'an earlier instance of this Resource'
: `a ${originalName} with the same href`;
: `a ${originalName} with the same ${urlPropKey}`;
let comparisonStatement = '';
if (missingProps !== null && typeof missingProps === 'object') {
@@ -294,12 +429,14 @@ function warnDifferentProps(
}
console.error(
'A %s with href "%s" has props that disagree with those found on %s. Resources always use the props' +
'A %s with %s "%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',
' update Resources that share an %s to have props that agree. The differences are described below.%s',
latestName,
href,
urlPropKey,
url,
juxtaposedNameStatement,
urlPropKey,
comparisonStatement,
);
}
@@ -315,6 +452,9 @@ function getResourceNameForWarning(
case 'style': {
return 'style Resource';
}
case 'script': {
return 'script Resource';
}
case 'preload': {
if (implicit) {
return `preload for a ${props.as} Resource`;
@@ -326,15 +466,17 @@ function getResourceNameForWarning(
return 'Resource';
}
export function validateHrefKeyedUpdatedProps(
export function validateURLKeyedUpdatedProps(
pendingProps: Props,
currentProps: Props,
resourceType: 'style' | 'script' | 'href',
urlPropKey: 'href' | 'src',
): boolean {
if (__DEV__) {
// This function should never be called if we don't have hrefs so we don't bother considering
// This function should never be called if we don't have /srcs 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
if (pendingProps[urlPropKey] === currentProps[urlPropKey]) {
// If we have the same href/src we need all other props to be the same
let missingProps;
let extraProps;
let differentProps;
@@ -366,7 +508,7 @@ export function validateHrefKeyedUpdatedProps(
}
if (missingProps || extraProps || differentProps) {
const latestWarningName = getResourceNameForWarning(
'style',
resourceType,
currentProps,
false,
);
@@ -388,14 +530,17 @@ export function validateHrefKeyedUpdatedProps(
}
}
console.error(
'A %s with href "%s" recieved new props with different values from the props used' +
'A %s with %s "%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' +
' this resource was first rendered until a new %s 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',
' (%s) must have props that agree with each other. The differences are described below.%s',
latestWarningName,
currentProps.href,
urlPropKey,
currentProps[urlPropKey],
urlPropKey,
urlPropKey,
comparisonStatement,
);
return true;
@@ -556,7 +701,8 @@ export function validatePreinitArguments(href: mixed, options: mixed) {
} else {
const as = options.as;
switch (as) {
case 'style': {
case 'style':
case 'script': {
break;
}
@@ -565,8 +711,8 @@ export function validatePreinitArguments(href: mixed, options: mixed) {
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".',
' specifying the Resource type. It found %s instead. Currently, valid resource types for for preinit are "style"' +
' and "script". The href for the preinit call where this warning originated is "%s".',
typeOfAs,
href,
);
File diff suppressed because it is too large Load Diff
@@ -50,7 +50,7 @@ describe('react-dom-server-rendering-stub', () => {
}
const html = ReactDOMFizzServer.renderToString(<App />);
expect(html).toEqual(
'<link href="foo" rel="preload" as="style"/><link rel="stylesheet" href="bar" data-rprec="default"/><div>foo</div>',
'<link rel="stylesheet" href="bar" data-precedence="default"/><link href="foo" rel="preload" as="style"/><div>foo</div>',
);
});
@@ -21,6 +21,7 @@ function shim(...args: any): empty {
// Hydration (when unsupported)
export type SuspenseInstance = mixed;
export const supportsHydration = false;
export const isHydratable = shim;
export const canHydrateInstance = shim;
export const canHydrateTextInstance = shim;
export const canHydrateSuspenseInstance = shim;
@@ -35,7 +35,7 @@ import {
NoFlags,
DidCapture,
} from './ReactFiberFlags';
import {enableHostSingletons} from 'shared/ReactFeatureFlags';
import {enableHostSingletons, enableFloat} from 'shared/ReactFeatureFlags';
import {
createFiberFromHostInstanceForDeletion,
@@ -45,6 +45,7 @@ import {
shouldSetTextContent,
supportsHydration,
supportsSingletons,
isHydratable,
canHydrateInstance,
canHydrateTextInstance,
canHydrateSuspenseInstance,
@@ -438,6 +439,13 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
if (!isHydrating) {
return;
}
if (enableFloat && !isHydratable(fiber.type, fiber.pendingProps)) {
// This fiber never hydrates from the DOM and always does an insert
fiber.flags = (fiber.flags & ~Hydrating) | Placement;
isHydrating = false;
hydrationParentFiber = fiber;
return;
}
let nextInstance = nextHydratableInstance;
if (!nextInstance) {
if (shouldClientRenderOnMismatch(fiber)) {
@@ -35,7 +35,7 @@ import {
NoFlags,
DidCapture,
} from './ReactFiberFlags';
import {enableHostSingletons} from 'shared/ReactFeatureFlags';
import {enableHostSingletons, enableFloat} from 'shared/ReactFeatureFlags';
import {
createFiberFromHostInstanceForDeletion,
@@ -45,6 +45,7 @@ import {
shouldSetTextContent,
supportsHydration,
supportsSingletons,
isHydratable,
canHydrateInstance,
canHydrateTextInstance,
canHydrateSuspenseInstance,
@@ -438,6 +439,13 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
if (!isHydrating) {
return;
}
if (enableFloat && !isHydratable(fiber.type, fiber.pendingProps)) {
// This fiber never hydrates from the DOM and always does an insert
fiber.flags = (fiber.flags & ~Hydrating) | Placement;
isHydrating = false;
hydrationParentFiber = fiber;
return;
}
let nextInstance = nextHydratableInstance;
if (!nextInstance) {
if (shouldClientRenderOnMismatch(fiber)) {
@@ -130,6 +130,7 @@ export const cloneHiddenTextInstance = $$$hostConfig.cloneHiddenTextInstance;
// Hydration
// (optional)
// -------------------
export const isHydratable = $$$hostConfig.isHydratable;
export const canHydrateInstance = $$$hostConfig.canHydrateInstance;
export const canHydrateTextInstance = $$$hostConfig.canHydrateTextInstance;
export const canHydrateSuspenseInstance =
@@ -72,7 +72,7 @@ async function main() {
// couldn't figure it out. Good enough for now. This only affects the inline
// Fizz runtime, and should break immediately if there were a mistake, so I'm
// not too worried about it.
outputCode = outputCode.replaceAll(
outputCode = outputCode.replace(
/window\.(\$[A-z0-9_]*)/g,
(_, variableName) => variableName
);