Files
react/packages/react-dom-bindings/src/client/ReactDOMFloatClient.js
T
Josh Story fbe8d2ab10 add warnings for non-resources rendered outside body or head (#25532)
Adds some clarifying warnings when you render a component that is almost
a resource but isn't and the element was rendered outside the main
document tree (outside of `<body>` or `<head>`
2022-12-03 11:05:28 -05:00

1436 lines
43 KiB
JavaScript

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {Instance, Container} from './ReactDOMHostConfig';
import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals.js';
const {Dispatcher} = ReactDOMSharedInternals;
import {DOCUMENT_NODE} from '../shared/HTMLNodeType';
import {
warnOnMissingHrefAndRel,
validatePreloadResourceDifference,
validateURLKeyedUpdatedProps,
validateStyleResourceDifference,
validateScriptResourceDifference,
validateLinkPropsForStyleResource,
validateLinkPropsForPreloadResource,
validatePreloadArguments,
validatePreinitArguments,
} from '../shared/ReactDOMResourceValidation';
import {createElement, setInitialProperties} from './ReactDOMComponent';
import {
getResourcesFromRoot,
markNodeAsResource,
} from './ReactDOMComponentTree';
import {HTML_NAMESPACE} from '../shared/DOMNamespaces';
import {
getCurrentRootHostContainer,
getHostContext,
} from 'react-reconciler/src/ReactFiberHostContext';
import {getResourceFormOnly} from './validateDOMNesting';
// 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' | 'script';
type PreloadProps = {
rel: 'preload',
href: string,
[string]: mixed,
};
type PreloadResource = {
type: 'preload',
href: string,
ownerDocument: Document,
props: PreloadProps,
instance: Element,
};
type StyleProps = {
rel: 'stylesheet',
href: string,
'data-precedence': 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,
root: FloatRoot,
};
type ScriptProps = {
src: string,
[string]: mixed,
};
type ScriptResource = {
type: 'script',
src: string,
props: ScriptProps,
instance: ?Element,
root: FloatRoot,
};
type TitleProps = {
[string]: mixed,
};
type TitleResource = {
type: 'title',
props: TitleProps,
count: number,
instance: ?Element,
root: Document,
};
type MetaProps = {
[string]: mixed,
};
type MetaResource = {
type: 'meta',
matcher: string,
property: ?string,
parentResource: ?MetaResource,
props: MetaProps,
count: number,
instance: ?Element,
root: Document,
};
type LinkProps = {
href: string,
rel: string,
[string]: mixed,
};
type LinkResource = {
type: 'link',
props: LinkProps,
count: number,
instance: ?Element,
root: Document,
};
type Props = {[string]: mixed};
type HeadResource = TitleResource | MetaResource | LinkResource;
type Resource = StyleResource | ScriptResource | PreloadResource | HeadResource;
export type RootResources = {
styles: Map<string, StyleResource>,
scripts: Map<string, ScriptResource>,
head: Map<string, HeadResource>,
lastStructuredMeta: Map<string, MetaResource>,
};
// 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'};
// 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(rootContainer: Container) {
// Flot thinks that getRootNode returns a Node but it actually returns a
// Document or ShadowRoot
const rootNode: FloatRoot = (rootContainer.getRootNode(): any);
lastCurrentDocument = getDocumentFromRoot(rootNode);
previousDispatcher = Dispatcher.current;
Dispatcher.current = ReactDOMClientDispatcher;
}
export function cleanupAfterRenderResources() {
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};
export type FloatRoot = Document | ShadowRoot;
// global maps of Resources
const preloadResources: Map<string, PreloadResource> = new Map();
function getCurrentResourceRoot(): null | FloatRoot {
const currentContainer = getCurrentRootHostContainer();
// $FlowFixMe flow should know currentContainer is a Node and has getRootNode
return currentContainer ? currentContainer.getRootNode() : null;
}
// 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 {
const root = getCurrentResourceRoot();
if (root) {
return root.ownerDocument || root;
} else {
try {
return lastCurrentDocument || window.document;
} catch (error) {
return null;
}
}
}
function getDocumentFromRoot(root: FloatRoot): Document {
return root.ownerDocument || root;
}
// --------------------------------------
// ReactDOM.Preload
// --------------------------------------
type PreloadAs = ResourceType;
type PreloadOptions = {as: PreloadAs, crossOrigin?: string, integrity?: 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,
integrity: options.integrity,
};
}
// --------------------------------------
// ReactDOM.preinit
// --------------------------------------
type PreinitAs = 'style' | 'script';
type PreinitOptions = {
as: PreinitAs,
precedence?: string,
crossOrigin?: string,
integrity?: string,
};
function preinit(href: string, options: PreinitOptions) {
if (__DEV__) {
validatePreinitArguments(href, options);
}
if (
typeof href === 'string' &&
href &&
typeof options === 'object' &&
options !== null
) {
const resourceRoot = getCurrentResourceRoot();
const as = options.as;
if (!resourceRoot) {
// 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 styleResources = getResourcesFromRoot(resourceRoot).styles;
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(
styleResources,
resourceRoot,
href,
precedence,
resourceProps,
);
}
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;
}
}
}
}
function preloadPropsFromPreinitOptions(
href: string,
as: ResourceType,
options: PreinitOptions,
): PreloadProps {
return {
href,
rel: 'preload',
as,
crossOrigin: as === 'font' ? '' : options.crossOrigin,
integrity: options.integrity,
};
}
function stylePropsFromPreinitOptions(
href: string,
precedence: string,
options: PreinitOptions,
): StyleProps {
return {
rel: 'stylesheet',
href,
'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
// --------------------------------------
type StyleQualifyingProps = {
rel: 'stylesheet',
href: string,
precedence: string,
[string]: mixed,
};
type PreloadQualifyingProps = {
rel: 'preload',
href: string,
[string]: mixed,
};
type ScriptQualifyingProps = {
src: string,
async: true,
[string]: mixed,
};
function getTitleKey(child: string | number): string {
return 'title:' + child;
}
// 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 {
const resourceRoot = getCurrentResourceRoot();
if (!resourceRoot) {
throw new Error(
'"resourceRoot" was expected to exist. This is a bug in React.',
);
}
switch (type) {
case 'meta': {
let matcher, propertyString, parentResource;
const {
charSet,
content,
httpEquiv,
name,
itemProp,
property,
} = pendingProps;
const headRoot: Document = getDocumentFromRoot(resourceRoot);
const {head: headResources, lastStructuredMeta} = getResourcesFromRoot(
headRoot,
);
if (typeof charSet === 'string') {
matcher = 'meta[charset]';
} else if (typeof content === 'string') {
if (typeof httpEquiv === 'string') {
matcher = `meta[http-equiv="${escapeSelectorAttributeValueInsideDoubleQuotes(
httpEquiv,
)}"][content="${escapeSelectorAttributeValueInsideDoubleQuotes(
content,
)}"]`;
} else if (typeof property === 'string') {
propertyString = property;
matcher = `meta[property="${escapeSelectorAttributeValueInsideDoubleQuotes(
property,
)}"][content="${escapeSelectorAttributeValueInsideDoubleQuotes(
content,
)}"]`;
const parentPropertyPath = property
.split(':')
.slice(0, -1)
.join(':');
parentResource = lastStructuredMeta.get(parentPropertyPath);
if (parentResource) {
// When using parentResource the matcher is not functional for locating
// the instance in the DOM but it still serves as a unique key.
matcher = parentResource.matcher + matcher;
}
} else if (typeof name === 'string') {
matcher = `meta[name="${escapeSelectorAttributeValueInsideDoubleQuotes(
name,
)}"][content="${escapeSelectorAttributeValueInsideDoubleQuotes(
content,
)}"]`;
} else if (typeof itemProp === 'string') {
matcher = `meta[itemprop="${escapeSelectorAttributeValueInsideDoubleQuotes(
itemProp,
)}"][content="${escapeSelectorAttributeValueInsideDoubleQuotes(
content,
)}"]`;
}
}
if (matcher) {
let resource = headResources.get(matcher);
if (!resource) {
resource = {
type: 'meta',
matcher,
property: propertyString,
parentResource,
props: Object.assign({}, pendingProps),
count: 0,
instance: null,
root: headRoot,
};
headResources.set(matcher, resource);
}
if (typeof resource.property === 'string') {
// We cast because flow doesn't know that this resource must be a Meta resource
lastStructuredMeta.set(resource.property, (resource: any));
}
return resource;
}
return null;
}
case 'title': {
let child = pendingProps.children;
if (Array.isArray(child) && child.length === 1) {
child = child[0];
}
if (typeof child === 'string' || typeof child === 'number') {
const headRoot: Document = getDocumentFromRoot(resourceRoot);
const headResources = getResourcesFromRoot(headRoot).head;
const key = getTitleKey(child);
let resource = headResources.get(key);
if (!resource) {
const titleProps = titlePropsFromRawProps(child, pendingProps);
resource = {
type: 'title',
props: titleProps,
count: 0,
instance: null,
root: headRoot,
};
headResources.set(key, resource);
}
return resource;
}
return null;
}
case 'link': {
const {rel} = pendingProps;
switch (rel) {
case 'stylesheet': {
const styleResources = getResourcesFromRoot(resourceRoot).styles;
let didWarn;
if (__DEV__) {
if (currentProps) {
didWarn = validateURLKeyedUpdatedProps(
pendingProps,
currentProps,
'style',
'href',
);
}
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) {
adoptPreloadPropsForStyle(
latestProps,
(resource: any)._dev_preload_props,
);
}
validateStyleResourceDifference(resource.props, latestProps);
}
}
} else {
const resourceProps = stylePropsFromRawProps(styleRawProps);
resource = createStyleResource(
styleResources,
resourceRoot,
href,
precedence,
resourceProps,
);
immediatelyPreloadStyleResource(resource);
}
return resource;
}
return null;
}
case 'preload': {
if (__DEV__) {
validateLinkPropsForPreloadResource(pendingProps);
}
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);
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(
getDocumentFromRoot(resourceRoot),
href,
resourceProps,
);
}
return resource;
}
return null;
}
default: {
const {href, sizes, media} = pendingProps;
if (typeof rel === 'string' && typeof href === 'string') {
const sizeKey =
'::sizes:' + (typeof sizes === 'string' ? sizes : '');
const mediaKey =
'::media:' + (typeof media === 'string' ? media : '');
const key = 'rel:' + rel + '::href:' + href + sizeKey + mediaKey;
const headRoot = getDocumentFromRoot(resourceRoot);
const headResources = getResourcesFromRoot(headRoot).head;
let resource = headResources.get(key);
if (!resource) {
resource = {
type: 'link',
props: Object.assign({}, pendingProps),
count: 0,
instance: null,
root: headRoot,
};
headResources.set(key, resource);
}
return resource;
}
if (__DEV__) {
warnOnMissingHrefAndRel(pendingProps, currentProps);
}
return null;
}
}
}
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.`,
);
}
}
}
function preloadPropsFromRawProps(
rawBorrowedProps: PreloadQualifyingProps,
): PreloadProps {
return Object.assign({}, rawBorrowedProps);
}
function titlePropsFromRawProps(
child: string | number,
rawProps: Props,
): TitleProps {
const props: TitleProps = Object.assign({}, rawProps);
props.children = child;
return props;
}
function stylePropsFromRawProps(rawProps: StyleQualifyingProps): StyleProps {
const props: StyleProps = Object.assign({}, rawProps);
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
// --------------------------------------
export function acquireResource(resource: Resource): Instance {
switch (resource.type) {
case 'title':
case 'link':
case 'meta': {
return acquireHeadResource(resource);
}
case 'style': {
return acquireStyleResource(resource);
}
case 'script': {
return acquireScriptResource(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): void {
switch (resource.type) {
case 'link':
case 'title':
case 'meta': {
return releaseHeadResource(resource);
}
case 'style': {
resource.count--;
return;
}
}
}
function releaseHeadResource(resource: HeadResource): void {
if (--resource.count === 0) {
// the instance will have existed since we acquired it
const instance: Instance = (resource.instance: any);
const parent = instance.parentNode;
if (parent) {
parent.removeChild(instance);
}
resource.instance = null;
}
}
function createResourceInstance(
type: string,
props: Object,
ownerDocument: Document,
): Instance {
const element = createElement(type, props, ownerDocument, HTML_NAMESPACE);
setInitialProperties(element, type, props);
markNodeAsResource(element);
return element;
}
function createStyleResource(
styleResources: Map<string, StyleResource>,
root: FloatRoot,
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 = root.querySelector(
`link[rel="stylesheet"][href="${limitedEscapedHref}"]`,
);
const resource = {
type: 'style',
count: 0,
href,
precedence,
props,
hint: null,
preloaded: false,
loaded: false,
error: false,
root,
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) {
// $FlowFixMe[incompatible-type]: found when upgrading Flow
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;
adoptPreloadPropsForStyle(resource.props, hint.props);
if (__DEV__) {
(resource: any)._dev_preload_props = preloadProps;
}
}
}
return resource;
}
function adoptPreloadPropsForStyle(
styleProps: StyleProps,
preloadProps: PreloadProps,
): void {
if (styleProps.crossOrigin == null)
styleProps.crossOrigin = preloadProps.crossOrigin;
if (styleProps.referrerPolicy == null)
styleProps.referrerPolicy = preloadProps.referrerPolicy;
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(
getDocumentFromRoot(resource.root),
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 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;
}
}
} else {
markNodeAsResource(existingEl);
}
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,
props: PreloadProps,
): PreloadResource {
const limitedEscapedHref = escapeSelectorAttributeValueInsideDoubleQuotes(
href,
);
let element: null | Instance | HTMLElement = ownerDocument.querySelector(
`link[rel="preload"][href="${limitedEscapedHref}"]`,
);
if (!element) {
element = createResourceInstance('link', props, ownerDocument);
insertResourceInstanceBefore(ownerDocument, element, null);
} else {
markNodeAsResource(element);
}
return {
type: 'preload',
href: href,
ownerDocument,
props,
instance: element,
};
}
function acquireHeadResource(resource: HeadResource): Instance {
resource.count++;
let instance = resource.instance;
if (!instance) {
const {props, root, type} = resource;
switch (type) {
case 'title': {
const titles = root.querySelectorAll('title');
for (let i = 0; i < titles.length; i++) {
if (titles[i].textContent === props.children) {
instance = resource.instance = titles[i];
markNodeAsResource(instance);
return instance;
}
}
instance = resource.instance = createResourceInstance(
type,
props,
root,
);
insertResourceInstanceBefore(root, instance, titles.item(0));
break;
}
case 'meta': {
let insertBefore = null;
const metaResource: MetaResource = (resource: any);
const {matcher, property, parentResource} = metaResource;
if (parentResource && typeof property === 'string') {
// This resoruce is a structured meta type with a parent.
// Instead of using the matcher we just traverse forward
// siblings of the parent instance until we find a match
// or exhaust.
const parent = parentResource.instance;
if (parent) {
let node = null;
let nextNode = (insertBefore = parent.nextSibling);
while ((node = nextNode)) {
nextNode = node.nextSibling;
if (node.nodeName === 'META') {
const meta: Element = (node: any);
const propertyAttr = meta.getAttribute('property');
if (typeof propertyAttr !== 'string') {
continue;
} else if (
propertyAttr === property &&
meta.getAttribute('content') === props.content
) {
resource.instance = meta;
markNodeAsResource(meta);
return meta;
} else if (property.startsWith(propertyAttr + ':')) {
// This meta starts a new instance of a parent structure for this meta type
// We need to halt our search here because even if we find a later match it
// is for a different parent element
break;
}
}
}
}
} else if ((instance = root.querySelector(matcher))) {
resource.instance = instance;
markNodeAsResource(instance);
return instance;
}
instance = resource.instance = createResourceInstance(
type,
props,
root,
);
insertResourceInstanceBefore(root, instance, insertBefore);
break;
}
case 'link': {
const linkProps: LinkProps = (props: any);
const limitedEscapedRel = escapeSelectorAttributeValueInsideDoubleQuotes(
linkProps.rel,
);
const limitedEscapedHref = escapeSelectorAttributeValueInsideDoubleQuotes(
linkProps.href,
);
let selector = `link[rel="${limitedEscapedRel}"][href="${limitedEscapedHref}"]`;
if (typeof linkProps.sizes === 'string') {
const limitedEscapedSizes = escapeSelectorAttributeValueInsideDoubleQuotes(
linkProps.sizes,
);
selector += `[sizes="${limitedEscapedSizes}"]`;
}
if (typeof linkProps.media === 'string') {
const limitedEscapedMedia = escapeSelectorAttributeValueInsideDoubleQuotes(
linkProps.media,
);
selector += `[media="${limitedEscapedMedia}"]`;
}
const existingEl = root.querySelector(selector);
if (existingEl) {
instance = resource.instance = existingEl;
markNodeAsResource(instance);
return instance;
}
instance = resource.instance = createResourceInstance(
type,
props,
root,
);
insertResourceInstanceBefore(root, instance, null);
return instance;
}
default: {
throw new Error(
`acquireHeadResource encountered a resource type it did not expect: "${type}". This is a bug in React.`,
);
}
}
}
return instance;
}
function acquireStyleResource(resource: StyleResource): Instance {
let instance = resource.instance;
if (!instance) {
const {props, root, precedence} = resource;
const limitedEscapedHref = escapeSelectorAttributeValueInsideDoubleQuotes(
props.href,
);
const existingEl = root.querySelector(
`link[rel="stylesheet"][data-precedence][href="${limitedEscapedHref}"]`,
);
if (existingEl) {
instance = resource.instance = existingEl;
markNodeAsResource(instance);
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 {
instance = resource.instance = createResourceInstance(
'link',
resource.props,
getDocumentFromRoot(root),
);
attachLoadListeners(instance, resource);
insertStyleInstance(instance, precedence, root);
}
}
resource.count++;
return instance;
}
function acquireScriptResource(resource: ScriptResource): Instance {
let instance = resource.instance;
if (!instance) {
const {props, root} = resource;
const limitedEscapedSrc = escapeSelectorAttributeValueInsideDoubleQuotes(
props.src,
);
const existingEl = root.querySelector(
`script[async][src="${limitedEscapedSrc}"]`,
);
if (existingEl) {
instance = resource.instance = existingEl;
markNodeAsResource(instance);
} else {
instance = resource.instance = createResourceInstance(
'script',
resource.props,
getDocumentFromRoot(root),
);
insertResourceInstanceBefore(getDocumentFromRoot(root), instance, null);
}
}
return 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,
root: FloatRoot,
): void {
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.precedence;
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 {
const parent =
root.nodeType === DOCUMENT_NODE ? ((root: any): Document).head : root;
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 insertResourceInstanceBefore(
ownerDocument: Document,
instance: Instance,
before: ?Node,
): void {
if (__DEV__) {
if (instance.tagName === 'LINK' && (instance: any).rel === 'stylesheet') {
console.error(
'insertResourceInstanceBefore was called with a stylesheet. Stylesheets must be' +
' inserted with insertStyleInstance instead. This is a bug in React.',
);
}
}
const parent = (before && before.parentNode) || ownerDocument.head;
if (parent) {
parent.insertBefore(instance, before);
} 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 {
let resourceFormOnly: boolean;
if (__DEV__) {
const hostContext = getHostContext();
resourceFormOnly = getResourceFormOnly(hostContext);
}
switch (type) {
case 'meta':
case 'title': {
return true;
}
case 'link': {
const {onLoad, onError} = props;
if (onLoad || onError) {
if (__DEV__) {
if (resourceFormOnly) {
console.error(
'Cannot render a <link> with onLoad or onError listeners outside the main document.' +
' Try removing onLoad={...} and onError={...} or moving it into the root <head> tag or' +
' somewhere in the <body>.',
);
}
}
return false;
}
switch (props.rel) {
case 'stylesheet': {
const {href, precedence, disabled} = props;
if (__DEV__) {
validateLinkPropsForStyleResource(props);
if (typeof precedence !== 'string' && resourceFormOnly) {
console.error(
'Cannot render a <link rel="stylesheet" /> outside the main document without knowing its precedence.' +
' Consider adding precedence="default" or moving it into the root <head> tag.',
);
}
}
return (
typeof href === 'string' &&
typeof precedence === 'string' &&
disabled == null
);
}
default: {
const {rel, href} = props;
return typeof href === 'string' && typeof rel === 'string';
}
}
}
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;
if (__DEV__) {
if (async !== true && resourceFormOnly) {
console.error(
'Cannot render a sync or defer <script> outside the main document without knowing its order.' +
' Try adding async="" or moving it into the root <head> tag.',
);
} else if ((onLoad || onError) && resourceFormOnly) {
console.error(
'Cannot render a <script> with onLoad or onError listeners outside the main document.' +
' Try removing onLoad={...} and onError={...} or moving it into the root <head> tag or' +
' somewhere in the <body>.',
);
}
}
return (async: any) && typeof src === 'string' && !onLoad && !onError;
}
case 'base':
case 'template':
case 'style':
case 'noscript': {
if (__DEV__) {
if (resourceFormOnly) {
console.error(
'Cannot render <%s> outside the main document. Try moving it into the root <head> tag.',
type,
);
}
}
return false;
}
}
return false;
}
// 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),
);
}