refactor isHostResourceType to not receive the context from reconciler and not leak types (#25610)

type validateDOMNesting
move `isHostResourceType` to ReactDOMHostConfig
type `AncestorInfo`
refactor `resourceFormOnly` into `ancestorInfo.containerTagInScope`
provide hostContext from reconciler
This commit is contained in:
Josh Story authored and GitHub committed 2022-11-01 17:14:46 -07:00
1 parent 8e69bc45aa
commit 4ea063b56f
5 files changed
+206 -172

No files matched your search

+2 -126
View File
@@ -28,14 +28,8 @@ import {
getResourcesFromRoot,
markNodeAsResource,
} from './ReactDOMComponentTree';
import {HTML_NAMESPACE} from '../shared/DOMNamespaces';
import {
getCurrentRootHostContainer,
getHostContext,
} from 'react-reconciler/src/ReactFiberHostContext';
import {getResourceFormOnly} from './validateDOMNesting';
import {getNamespace} from './ReactDOMHostConfig';
import {SVG_NAMESPACE} from '../shared/DOMNamespaces';
import {HTML_NAMESPACE, SVG_NAMESPACE} from '../shared/DOMNamespaces';
import {getCurrentRootHostContainer} from 'react-reconciler/src/ReactFiberHostContext';
// 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
@@ -1426,124 +1420,6 @@ function insertResourceInstanceBefore(
}
}
export function isHostResourceType(type: string, props: Props): boolean {
let resourceFormOnly: boolean;
let namespace: string;
if (__DEV__) {
const hostContext = getHostContext();
resourceFormOnly = getResourceFormOnly(hostContext);
namespace = getNamespace(hostContext);
}
switch (type) {
case 'base':
case 'meta': {
return true;
}
case 'title': {
const hostContext = getHostContext();
return getNamespace(hostContext) !== SVG_NAMESPACE;
}
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>.',
);
} else if (namespace === SVG_NAMESPACE) {
console.error(
'Cannot render a <link> with onLoad or onError listeners as a descendent of <svg>.' +
' Try removing onLoad={...} and onError={...} or moving it above the <svg> ancestor.',
);
}
}
return false;
}
switch (props.rel) {
case 'stylesheet': {
const {href, precedence, disabled} = props;
if (__DEV__) {
validateLinkPropsForStyleResource(props);
if (typeof precedence !== 'string') {
if (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.',
);
} else if (namespace === SVG_NAMESPACE) {
console.error(
'Cannot render a <link rel="stylesheet" /> as a descendent of an <svg> element without knowing its precedence.' +
' Consider adding precedence="default" or moving it above the <svg> ancestor.',
);
}
}
}
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) {
if (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 (namespace === SVG_NAMESPACE) {
console.error(
'Cannot render a sync or defer <script> as a descendent of an <svg> element.' +
' Try adding async="" or moving it above the ancestor <svg> element.',
);
}
} else if (onLoad || onError) {
if (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>.',
);
} else if (namespace === SVG_NAMESPACE) {
console.error(
'Cannot render a <script> with onLoad or onError listeners as a descendent of an <svg> element.' +
' Try removing onLoad={...} and onError={...} or moving it above the ancestor <svg> element.',
);
}
}
}
return (async: any) && typeof src === 'string' && !onLoad && !onError;
}
case 'noscript':
case 'template':
case 'style': {
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
+137 -20
View File
@@ -16,6 +16,7 @@ import type {
ObserveVisibleRectsCallback,
} from 'react-reconciler/src/ReactTestSelectors';
import type {ReactScopeInstance} from 'shared/ReactTypes';
import type {AncestorInfoDev} from './validateDOMNesting';
import {
precacheFiberNode,
@@ -47,13 +48,13 @@ import {
} from './ReactDOMComponent';
import {getSelectionInformation, restoreSelection} from './ReactInputSelection';
import setTextContent from './setTextContent';
import {validateDOMNesting, updatedAncestorInfo} from './validateDOMNesting';
import {validateDOMNesting, updatedAncestorInfoDev} from './validateDOMNesting';
import {
isEnabled as ReactBrowserEventEmitterIsEnabled,
setEnabled as ReactBrowserEventEmitterSetEnabled,
getEventPriority,
} from '../events/ReactDOMEventListener';
import {getChildNamespace} from '../shared/DOMNamespaces';
import {getChildNamespace, SVG_NAMESPACE} from '../shared/DOMNamespaces';
import {
ELEMENT_NODE,
TEXT_NODE,
@@ -89,8 +90,8 @@ import {
prepareToRenderResources,
cleanupAfterRenderResources,
clearRootResources,
isHostResourceType,
} from './ReactDOMFloatClient';
import {validateLinkPropsForStyleResource} from '../shared/ReactDOMResourceValidation';
export type Type = string;
export type Props = {
@@ -107,6 +108,9 @@ export type Props = {
top?: null | number,
...
};
type RawProps = {
[string]: mixed,
};
export type EventTargetChildElement = {
type: string,
props: null | {
@@ -136,8 +140,7 @@ export type HydratableInstance = Instance | TextInstance | SuspenseInstance;
export type PublicInstance = Element | Text;
type HostContextDev = {
namespace: string,
ancestorInfo: mixed,
...
ancestorInfo: AncestorInfoDev,
};
type HostContextProd = string;
export type HostContext = HostContextDev | HostContextProd;
@@ -193,7 +196,7 @@ export function getRootHostContext(
}
if (__DEV__) {
const validatedTag = type.toLowerCase();
const ancestorInfo = updatedAncestorInfo(null, validatedTag);
const ancestorInfo = updatedAncestorInfoDev(null, validatedTag);
return {namespace, ancestorInfo};
}
return namespace;
@@ -206,7 +209,7 @@ export function getChildHostContext(
if (__DEV__) {
const parentHostContextDev = ((parentHostContext: any): HostContextDev);
const namespace = getChildNamespace(parentHostContextDev.namespace, type);
const ancestorInfo = updatedAncestorInfo(
const ancestorInfo = updatedAncestorInfoDev(
parentHostContextDev.ancestorInfo,
type,
);
@@ -220,16 +223,6 @@ export function getPublicInstance(instance: Instance): Instance {
return instance;
}
export function getNamespace(hostContext: HostContext): string {
if (__DEV__) {
const hostContextDev: HostContextDev = (hostContext: any);
return hostContextDev.namespace;
} else {
const hostContextProd: HostContextProd = (hostContext: any);
return hostContextProd;
}
}
export function prepareForCommit(containerInfo: Container): Object | null {
eventsEnabled = ReactBrowserEventEmitterIsEnabled();
selectionInformation = getSelectionInformation();
@@ -287,7 +280,7 @@ export function createInstance(
typeof props.children === 'number'
) {
const string = '' + props.children;
const ownAncestorInfo = updatedAncestorInfo(
const ownAncestorInfo = updatedAncestorInfoDev(
hostContextDev.ancestorInfo,
type,
);
@@ -350,7 +343,7 @@ export function prepareUpdate(
typeof newProps.children === 'number')
) {
const string = '' + newProps.children;
const ownAncestorInfo = updatedAncestorInfo(
const ownAncestorInfo = updatedAncestorInfoDev(
hostContextDev.ancestorInfo,
type,
);
@@ -1573,7 +1566,131 @@ export function requestPostPaintCallback(callback: (time: number) => void) {
export const supportsResources = true;
export {isHostResourceType};
export function isHostResourceType(
type: string,
props: RawProps,
hostContext: HostContext,
): boolean {
let outsideHostContainerContext: boolean;
let namespace: string;
if (__DEV__) {
const hostContextDev: HostContextDev = (hostContext: any);
// We can only render resources when we are not within the host container context
outsideHostContainerContext = !hostContextDev.ancestorInfo
.containerTagInScope;
namespace = hostContextDev.namespace;
} else {
const hostContextProd: HostContextProd = (hostContext: any);
namespace = hostContextProd;
}
switch (type) {
case 'base':
case 'meta': {
return true;
}
case 'title': {
return namespace !== SVG_NAMESPACE;
}
case 'link': {
const {onLoad, onError} = props;
if (onLoad || onError) {
if (__DEV__) {
if (outsideHostContainerContext) {
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>.',
);
} else if (namespace === SVG_NAMESPACE) {
console.error(
'Cannot render a <link> with onLoad or onError listeners as a descendent of <svg>.' +
' Try removing onLoad={...} and onError={...} or moving it above the <svg> ancestor.',
);
}
}
return false;
}
switch (props.rel) {
case 'stylesheet': {
const {href, precedence, disabled} = props;
if (__DEV__) {
validateLinkPropsForStyleResource(props);
if (typeof precedence !== 'string') {
if (outsideHostContainerContext) {
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.',
);
} else if (namespace === SVG_NAMESPACE) {
console.error(
'Cannot render a <link rel="stylesheet" /> as a descendent of an <svg> element without knowing its precedence.' +
' Consider adding precedence="default" or moving it above the <svg> ancestor.',
);
}
}
}
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) {
if (outsideHostContainerContext) {
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 (namespace === SVG_NAMESPACE) {
console.error(
'Cannot render a sync or defer <script> as a descendent of an <svg> element.' +
' Try adding async="" or moving it above the ancestor <svg> element.',
);
}
} else if (onLoad || onError) {
if (outsideHostContainerContext) {
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>.',
);
} else if (namespace === SVG_NAMESPACE) {
console.error(
'Cannot render a <script> with onLoad or onError listeners as a descendent of an <svg> element.' +
' Try removing onLoad={...} and onError={...} or moving it above the ancestor <svg> element.',
);
}
}
}
return (async: any) && typeof src === 'string' && !onLoad && !onError;
}
case 'noscript':
case 'template':
case 'style': {
if (__DEV__) {
if (outsideHostContainerContext) {
console.error(
'Cannot render <%s> outside the main document. Try moving it into the root <head> tag.',
type,
);
}
}
return false;
}
}
return false;
}
export function prepareRendererToRender(rootContainer: Container) {
if (enableFloat) {
+57 -22
View File
@@ -3,11 +3,32 @@
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
let validateDOMNesting = () => {};
let updatedAncestorInfo = () => {};
let getResourceFormOnly = () => false;
type ValidateDOMNesting = (?string, ?string, AncestorInfoDev) => void;
let validateDOMNesting: ValidateDOMNesting = (() => {}: any);
type UpdatedAncestorInfoDev = (?AncestorInfoDev, string) => AncestorInfoDev;
let updatedAncestorInfoDev: UpdatedAncestorInfoDev = (() => {}: any);
type Info = {tag: string};
export type AncestorInfoDev = {
current: ?Info,
formTag: ?Info,
aTagInScope: ?Info,
buttonTagInScope: ?Info,
nobrTagInScope: ?Info,
pTagInButtonScope: ?Info,
listItemTagAutoclosing: ?Info,
dlItemTagAutoclosing: ?Info,
// <head> or <body>
containerTagInScope: ?Info,
};
if (__DEV__) {
// This validation code was written based on the HTML5 parsing spec:
@@ -143,7 +164,7 @@ if (__DEV__) {
'rt',
];
const emptyAncestorInfo = {
const emptyAncestorInfoDev: AncestorInfoDev = {
current: null,
formTag: null,
@@ -155,11 +176,11 @@ if (__DEV__) {
listItemTagAutoclosing: null,
dlItemTagAutoclosing: null,
resourceFormOnly: true,
containerTagInScope: null,
};
updatedAncestorInfo = function(oldInfo, tag) {
const ancestorInfo = {...(oldInfo || emptyAncestorInfo)};
updatedAncestorInfoDev = function(oldInfo: ?AncestorInfoDev, tag: string) {
const ancestorInfo = {...(oldInfo || emptyAncestorInfoDev)};
const info = {tag};
if (inScopeTags.indexOf(tag) !== -1) {
@@ -183,10 +204,6 @@ if (__DEV__) {
ancestorInfo.dlItemTagAutoclosing = null;
}
if (tag !== '#document' && tag !== 'html') {
ancestorInfo.resourceFormOnly = false;
}
ancestorInfo.current = info;
if (tag === 'form') {
@@ -210,6 +227,11 @@ if (__DEV__) {
if (tag === 'dd' || tag === 'dt') {
ancestorInfo.dlItemTagAutoclosing = info;
}
if (tag === '#document' || tag === 'html') {
ancestorInfo.containerTagInScope = null;
} else if (!ancestorInfo.containerTagInScope) {
ancestorInfo.containerTagInScope = info;
}
return ancestorInfo;
};
@@ -217,7 +239,10 @@ if (__DEV__) {
/**
* Returns whether
*/
const isTagValidWithParent = function(tag, parentTag) {
const isTagValidWithParent = function(
tag: string,
parentTag: ?string,
): boolean {
// First, let's check if we're in an unusual parsing mode...
switch (parentTag) {
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inselect
@@ -342,7 +367,10 @@ if (__DEV__) {
/**
* Returns whether
*/
const findInvalidAncestorForTag = function(tag, ancestorInfo) {
const findInvalidAncestorForTag = function(
tag: string,
ancestorInfo: AncestorInfoDev,
): ?Info {
switch (tag) {
case 'address':
case 'article':
@@ -408,8 +436,12 @@ if (__DEV__) {
const didWarn = {};
validateDOMNesting = function(childTag, childText, ancestorInfo) {
ancestorInfo = ancestorInfo || emptyAncestorInfo;
validateDOMNesting = function(
childTag: ?string,
childText: ?string,
ancestorInfo: AncestorInfoDev,
) {
ancestorInfo = ancestorInfo || emptyAncestorInfoDev;
const parentInfo = ancestorInfo.current;
const parentTag = parentInfo && parentInfo.tag;
@@ -420,6 +452,11 @@ if (__DEV__) {
);
}
childTag = '#text';
} else if (childTag == null) {
console.error(
'validateDOMNesting: when childText or childTag must be provided',
);
return;
}
const invalidParent = isTagValidWithParent(childTag, parentTag)
@@ -435,7 +472,9 @@ if (__DEV__) {
const ancestorTag = invalidParentOrAncestor.tag;
const warnKey = !!invalidParent + '|' + childTag + '|' + ancestorTag;
const warnKey =
// eslint-disable-next-line react-internal/safe-string-coercion
String(!!invalidParent) + '|' + childTag + '|' + ancestorTag;
if (didWarn[warnKey]) {
return;
}
@@ -444,7 +483,7 @@ if (__DEV__) {
let tagDisplayName = childTag;
let whitespaceInfo = '';
if (childTag === '#text') {
if (/\S/.test(childText)) {
if (childText != null && /\S/.test(childText)) {
tagDisplayName = 'Text nodes';
} else {
tagDisplayName = 'Whitespace text nodes';
@@ -479,10 +518,6 @@ if (__DEV__) {
);
}
};
getResourceFormOnly = hostContextDev => {
return hostContextDev.ancestorInfo.resourceFormOnly;
};
}
export {updatedAncestorInfo, validateDOMNesting, getResourceFormOnly};
export {updatedAncestorInfoDev, validateDOMNesting};
@@ -108,6 +108,7 @@ import {
} from 'shared/ReactSymbols';
import {TransitionTracingMarker} from './ReactFiberTracingMarkerComponent.new';
import {detachOffscreenInstance} from './ReactFiberCommitWork.new';
import {getHostContext} from './ReactFiberHostContext.new';
export type {Fiber};
@@ -503,13 +504,15 @@ export function createFiberFromTypeAndProps(
enableHostSingletons &&
supportsSingletons
) {
fiberTag = isHostResourceType(type, pendingProps)
const hostContext = getHostContext();
fiberTag = isHostResourceType(type, pendingProps, hostContext)
? HostResource
: isHostSingletonType(type)
? HostSingleton
: HostComponent;
} else if (enableFloat && supportsResources) {
fiberTag = isHostResourceType(type, pendingProps)
const hostContext = getHostContext();
fiberTag = isHostResourceType(type, pendingProps, hostContext)
? HostResource
: HostComponent;
} else if (enableHostSingletons && supportsSingletons) {
@@ -108,6 +108,7 @@ import {
} from 'shared/ReactSymbols';
import {TransitionTracingMarker} from './ReactFiberTracingMarkerComponent.old';
import {detachOffscreenInstance} from './ReactFiberCommitWork.old';
import {getHostContext} from './ReactFiberHostContext.old';
export type {Fiber};
@@ -503,13 +504,15 @@ export function createFiberFromTypeAndProps(
enableHostSingletons &&
supportsSingletons
) {
fiberTag = isHostResourceType(type, pendingProps)
const hostContext = getHostContext();
fiberTag = isHostResourceType(type, pendingProps, hostContext)
? HostResource
: isHostSingletonType(type)
? HostSingleton
: HostComponent;
} else if (enableFloat && supportsResources) {
fiberTag = isHostResourceType(type, pendingProps)
const hostContext = getHostContext();
fiberTag = isHostResourceType(type, pendingProps, hostContext)
? HostResource
: HostComponent;
} else if (enableHostSingletons && supportsSingletons) {