@samitouri / QOS-React-2 / commits / 4ea063b56f

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

Josh Story committed Nov 1, 2022 at 17:14 UTC 4ea063b56ff7ccc69c4cc67c30fd6f31f9880464
5 files changed +206 -172
packages/react-dom-bindings/src/client/ReactDOMFloatClient.js
+2 -126
@@ -28,14 +28,8 @@ import {
28 getResourcesFromRoot,
29 markNodeAsResource,
30 } from './ReactDOMComponentTree';
31 -import {HTML_NAMESPACE} from '../shared/DOMNamespaces';
32 -import {
33 - getCurrentRootHostContainer,
34 - getHostContext,
35 -} from 'react-reconciler/src/ReactFiberHostContext';
36 -import {getResourceFormOnly} from './validateDOMNesting';
37 -import {getNamespace} from './ReactDOMHostConfig';
38 -import {SVG_NAMESPACE} from '../shared/DOMNamespaces';
31 +import {HTML_NAMESPACE, SVG_NAMESPACE} from '../shared/DOMNamespaces';
32 +import {getCurrentRootHostContainer} from 'react-reconciler/src/ReactFiberHostContext';
33
34 // The resource types we support. currently they match the form for the as argument.
35 // In the future this may need to change, especially when modules / scripts are supported
@@ -1426,124 +1420,6 @@ function insertResourceInstanceBefore(
1420 }
1421 }
1422
1429 -export function isHostResourceType(type: string, props: Props): boolean {
1430 - let resourceFormOnly: boolean;
1431 - let namespace: string;
1432 - if (__DEV__) {
1433 - const hostContext = getHostContext();
1434 - resourceFormOnly = getResourceFormOnly(hostContext);
1435 - namespace = getNamespace(hostContext);
1436 - }
1437 - switch (type) {
1438 - case 'base':
1439 - case 'meta': {
1440 - return true;
1441 - }
1442 - case 'title': {
1443 - const hostContext = getHostContext();
1444 - return getNamespace(hostContext) !== SVG_NAMESPACE;
1445 - }
1446 - case 'link': {
1447 - const {onLoad, onError} = props;
1448 - if (onLoad || onError) {
1449 - if (__DEV__) {
1450 - if (resourceFormOnly) {
1451 - console.error(
1452 - 'Cannot render a <link> with onLoad or onError listeners outside the main document.' +
1453 - ' Try removing onLoad={...} and onError={...} or moving it into the root <head> tag or' +
1454 - ' somewhere in the <body>.',
1455 - );
1456 - } else if (namespace === SVG_NAMESPACE) {
1457 - console.error(
1458 - 'Cannot render a <link> with onLoad or onError listeners as a descendent of <svg>.' +
1459 - ' Try removing onLoad={...} and onError={...} or moving it above the <svg> ancestor.',
1460 - );
1461 - }
1462 - }
1463 - return false;
1464 - }
1465 - switch (props.rel) {
1466 - case 'stylesheet': {
1467 - const {href, precedence, disabled} = props;
1468 - if (__DEV__) {
1469 - validateLinkPropsForStyleResource(props);
1470 - if (typeof precedence !== 'string') {
1471 - if (resourceFormOnly) {
1472 - console.error(
1473 - 'Cannot render a <link rel="stylesheet" /> outside the main document without knowing its precedence.' +
1474 - ' Consider adding precedence="default" or moving it into the root <head> tag.',
1475 - );
1476 - } else if (namespace === SVG_NAMESPACE) {
1477 - console.error(
1478 - 'Cannot render a <link rel="stylesheet" /> as a descendent of an <svg> element without knowing its precedence.' +
1479 - ' Consider adding precedence="default" or moving it above the <svg> ancestor.',
1480 - );
1481 - }
1482 - }
1483 - }
1484 - return (
1485 - typeof href === 'string' &&
1486 - typeof precedence === 'string' &&
1487 - disabled == null
1488 - );
1489 - }
1490 - default: {
1491 - const {rel, href} = props;
1492 - return typeof href === 'string' && typeof rel === 'string';
1493 - }
1494 - }
1495 - }
1496 - case 'script': {
1497 - // We don't validate because it is valid to use async with onLoad/onError unlike combining
1498 - // precedence with these for style resources
1499 - const {src, async, onLoad, onError} = props;
1500 - if (__DEV__) {
1501 - if (async !== true) {
1502 - if (resourceFormOnly) {
1503 - console.error(
1504 - 'Cannot render a sync or defer <script> outside the main document without knowing its order.' +
1505 - ' Try adding async="" or moving it into the root <head> tag.',
1506 - );
1507 - } else if (namespace === SVG_NAMESPACE) {
1508 - console.error(
1509 - 'Cannot render a sync or defer <script> as a descendent of an <svg> element.' +
1510 - ' Try adding async="" or moving it above the ancestor <svg> element.',
1511 - );
1512 - }
1513 - } else if (onLoad || onError) {
1514 - if (resourceFormOnly) {
1515 - console.error(
1516 - 'Cannot render a <script> with onLoad or onError listeners outside the main document.' +
1517 - ' Try removing onLoad={...} and onError={...} or moving it into the root <head> tag or' +
1518 - ' somewhere in the <body>.',
1519 - );
1520 - } else if (namespace === SVG_NAMESPACE) {
1521 - console.error(
1522 - 'Cannot render a <script> with onLoad or onError listeners as a descendent of an <svg> element.' +
1523 - ' Try removing onLoad={...} and onError={...} or moving it above the ancestor <svg> element.',
1524 - );
1525 - }
1526 - }
1527 - }
1528 - return (async: any) && typeof src === 'string' && !onLoad && !onError;
1529 - }
1530 - case 'noscript':
1531 - case 'template':
1532 - case 'style': {
1533 - if (__DEV__) {
1534 - if (resourceFormOnly) {
1535 - console.error(
1536 - 'Cannot render <%s> outside the main document. Try moving it into the root <head> tag.',
1537 - type,
1538 - );
1539 - }
1540 - }
1541 - return false;
1542 - }
1543 - }
1544 - return false;
1545 -}
1546 -
1423 // When passing user input into querySelector(All) the embedded string must not alter
1424 // the semantics of the query. This escape function is safe to use when we know the
1425 // provided value is going to be wrapped in double quotes as part of an attribute selector
packages/react-dom-bindings/src/client/ReactDOMHostConfig.js
+137 -20
@@ -16,6 +16,7 @@ import type {
16 ObserveVisibleRectsCallback,
17 } from 'react-reconciler/src/ReactTestSelectors';
18 import type {ReactScopeInstance} from 'shared/ReactTypes';
19 +import type {AncestorInfoDev} from './validateDOMNesting';
20
21 import {
22 precacheFiberNode,
@@ -47,13 +48,13 @@ import {
48 } from './ReactDOMComponent';
49 import {getSelectionInformation, restoreSelection} from './ReactInputSelection';
50 import setTextContent from './setTextContent';
50 -import {validateDOMNesting, updatedAncestorInfo} from './validateDOMNesting';
51 +import {validateDOMNesting, updatedAncestorInfoDev} from './validateDOMNesting';
52 import {
53 isEnabled as ReactBrowserEventEmitterIsEnabled,
54 setEnabled as ReactBrowserEventEmitterSetEnabled,
55 getEventPriority,
56 } from '../events/ReactDOMEventListener';
56 -import {getChildNamespace} from '../shared/DOMNamespaces';
57 +import {getChildNamespace, SVG_NAMESPACE} from '../shared/DOMNamespaces';
58 import {
59 ELEMENT_NODE,
60 TEXT_NODE,
@@ -89,8 +90,8 @@ import {
90 prepareToRenderResources,
91 cleanupAfterRenderResources,
92 clearRootResources,
92 - isHostResourceType,
93 } from './ReactDOMFloatClient';
94 +import {validateLinkPropsForStyleResource} from '../shared/ReactDOMResourceValidation';
95
96 export type Type = string;
97 export type Props = {
@@ -107,6 +108,9 @@ export type Props = {
108 top?: null | number,
109 ...
110 };
111 +type RawProps = {
112 + [string]: mixed,
113 +};
114 export type EventTargetChildElement = {
115 type: string,
116 props: null | {
@@ -136,8 +140,7 @@ export type HydratableInstance = Instance | TextInstance | SuspenseInstance;
140 export type PublicInstance = Element | Text;
141 type HostContextDev = {
142 namespace: string,
139 - ancestorInfo: mixed,
140 - ...
143 + ancestorInfo: AncestorInfoDev,
144 };
145 type HostContextProd = string;
146 export type HostContext = HostContextDev | HostContextProd;
@@ -193,7 +196,7 @@ export function getRootHostContext(
196 }
197 if (__DEV__) {
198 const validatedTag = type.toLowerCase();
196 - const ancestorInfo = updatedAncestorInfo(null, validatedTag);
199 + const ancestorInfo = updatedAncestorInfoDev(null, validatedTag);
200 return {namespace, ancestorInfo};
201 }
202 return namespace;
@@ -206,7 +209,7 @@ export function getChildHostContext(
209 if (__DEV__) {
210 const parentHostContextDev = ((parentHostContext: any): HostContextDev);
211 const namespace = getChildNamespace(parentHostContextDev.namespace, type);
209 - const ancestorInfo = updatedAncestorInfo(
212 + const ancestorInfo = updatedAncestorInfoDev(
213 parentHostContextDev.ancestorInfo,
214 type,
215 );
@@ -220,16 +223,6 @@ export function getPublicInstance(instance: Instance): Instance {
223 return instance;
224 }
225
223 -export function getNamespace(hostContext: HostContext): string {
224 - if (__DEV__) {
225 - const hostContextDev: HostContextDev = (hostContext: any);
226 - return hostContextDev.namespace;
227 - } else {
228 - const hostContextProd: HostContextProd = (hostContext: any);
229 - return hostContextProd;
230 - }
231 -}
232 -
226 export function prepareForCommit(containerInfo: Container): Object | null {
227 eventsEnabled = ReactBrowserEventEmitterIsEnabled();
228 selectionInformation = getSelectionInformation();
@@ -287,7 +280,7 @@ export function createInstance(
280 typeof props.children === 'number'
281 ) {
282 const string = '' + props.children;
290 - const ownAncestorInfo = updatedAncestorInfo(
283 + const ownAncestorInfo = updatedAncestorInfoDev(
284 hostContextDev.ancestorInfo,
285 type,
286 );
@@ -350,7 +343,7 @@ export function prepareUpdate(
343 typeof newProps.children === 'number')
344 ) {
345 const string = '' + newProps.children;
353 - const ownAncestorInfo = updatedAncestorInfo(
346 + const ownAncestorInfo = updatedAncestorInfoDev(
347 hostContextDev.ancestorInfo,
348 type,
349 );
@@ -1573,7 +1566,131 @@ export function requestPostPaintCallback(callback: (time: number) => void) {
1566
1567 export const supportsResources = true;
1568
1576 -export {isHostResourceType};
1569 +export function isHostResourceType(
1570 + type: string,
1571 + props: RawProps,
1572 + hostContext: HostContext,
1573 +): boolean {
1574 + let outsideHostContainerContext: boolean;
1575 + let namespace: string;
1576 + if (__DEV__) {
1577 + const hostContextDev: HostContextDev = (hostContext: any);
1578 + // We can only render resources when we are not within the host container context
1579 + outsideHostContainerContext = !hostContextDev.ancestorInfo
1580 + .containerTagInScope;
1581 + namespace = hostContextDev.namespace;
1582 + } else {
1583 + const hostContextProd: HostContextProd = (hostContext: any);
1584 + namespace = hostContextProd;
1585 + }
1586 + switch (type) {
1587 + case 'base':
1588 + case 'meta': {
1589 + return true;
1590 + }
1591 + case 'title': {
1592 + return namespace !== SVG_NAMESPACE;
1593 + }
1594 + case 'link': {
1595 + const {onLoad, onError} = props;
1596 + if (onLoad || onError) {
1597 + if (__DEV__) {
1598 + if (outsideHostContainerContext) {
1599 + console.error(
1600 + 'Cannot render a <link> with onLoad or onError listeners outside the main document.' +
1601 + ' Try removing onLoad={...} and onError={...} or moving it into the root <head> tag or' +
1602 + ' somewhere in the <body>.',
1603 + );
1604 + } else if (namespace === SVG_NAMESPACE) {
1605 + console.error(
1606 + 'Cannot render a <link> with onLoad or onError listeners as a descendent of <svg>.' +
1607 + ' Try removing onLoad={...} and onError={...} or moving it above the <svg> ancestor.',
1608 + );
1609 + }
1610 + }
1611 + return false;
1612 + }
1613 + switch (props.rel) {
1614 + case 'stylesheet': {
1615 + const {href, precedence, disabled} = props;
1616 + if (__DEV__) {
1617 + validateLinkPropsForStyleResource(props);
1618 + if (typeof precedence !== 'string') {
1619 + if (outsideHostContainerContext) {
1620 + console.error(
1621 + 'Cannot render a <link rel="stylesheet" /> outside the main document without knowing its precedence.' +
1622 + ' Consider adding precedence="default" or moving it into the root <head> tag.',
1623 + );
1624 + } else if (namespace === SVG_NAMESPACE) {
1625 + console.error(
1626 + 'Cannot render a <link rel="stylesheet" /> as a descendent of an <svg> element without knowing its precedence.' +
1627 + ' Consider adding precedence="default" or moving it above the <svg> ancestor.',
1628 + );
1629 + }
1630 + }
1631 + }
1632 + return (
1633 + typeof href === 'string' &&
1634 + typeof precedence === 'string' &&
1635 + disabled == null
1636 + );
1637 + }
1638 + default: {
1639 + const {rel, href} = props;
1640 + return typeof href === 'string' && typeof rel === 'string';
1641 + }
1642 + }
1643 + }
1644 + case 'script': {
1645 + // We don't validate because it is valid to use async with onLoad/onError unlike combining
1646 + // precedence with these for style resources
1647 + const {src, async, onLoad, onError} = props;
1648 + if (__DEV__) {
1649 + if (async !== true) {
1650 + if (outsideHostContainerContext) {
1651 + console.error(
1652 + 'Cannot render a sync or defer <script> outside the main document without knowing its order.' +
1653 + ' Try adding async="" or moving it into the root <head> tag.',
1654 + );
1655 + } else if (namespace === SVG_NAMESPACE) {
1656 + console.error(
1657 + 'Cannot render a sync or defer <script> as a descendent of an <svg> element.' +
1658 + ' Try adding async="" or moving it above the ancestor <svg> element.',
1659 + );
1660 + }
1661 + } else if (onLoad || onError) {
1662 + if (outsideHostContainerContext) {
1663 + console.error(
1664 + 'Cannot render a <script> with onLoad or onError listeners outside the main document.' +
1665 + ' Try removing onLoad={...} and onError={...} or moving it into the root <head> tag or' +
1666 + ' somewhere in the <body>.',
1667 + );
1668 + } else if (namespace === SVG_NAMESPACE) {
1669 + console.error(
1670 + 'Cannot render a <script> with onLoad or onError listeners as a descendent of an <svg> element.' +
1671 + ' Try removing onLoad={...} and onError={...} or moving it above the ancestor <svg> element.',
1672 + );
1673 + }
1674 + }
1675 + }
1676 + return (async: any) && typeof src === 'string' && !onLoad && !onError;
1677 + }
1678 + case 'noscript':
1679 + case 'template':
1680 + case 'style': {
1681 + if (__DEV__) {
1682 + if (outsideHostContainerContext) {
1683 + console.error(
1684 + 'Cannot render <%s> outside the main document. Try moving it into the root <head> tag.',
1685 + type,
1686 + );
1687 + }
1688 + }
1689 + return false;
1690 + }
1691 + }
1692 + return false;
1693 +}
1694
1695 export function prepareRendererToRender(rootContainer: Container) {
1696 if (enableFloat) {
packages/react-dom-bindings/src/client/validateDOMNesting.js
+57 -22
@@ -3,11 +3,32 @@
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 */
9
8 -let validateDOMNesting = () => {};
9 -let updatedAncestorInfo = () => {};
10 -let getResourceFormOnly = () => false;
10 +type ValidateDOMNesting = (?string, ?string, AncestorInfoDev) => void;
11 +let validateDOMNesting: ValidateDOMNesting = (() => {}: any);
12 +
13 +type UpdatedAncestorInfoDev = (?AncestorInfoDev, string) => AncestorInfoDev;
14 +let updatedAncestorInfoDev: UpdatedAncestorInfoDev = (() => {}: any);
15 +
16 +type Info = {tag: string};
17 +export type AncestorInfoDev = {
18 + current: ?Info,
19 +
20 + formTag: ?Info,
21 + aTagInScope: ?Info,
22 + buttonTagInScope: ?Info,
23 + nobrTagInScope: ?Info,
24 + pTagInButtonScope: ?Info,
25 +
26 + listItemTagAutoclosing: ?Info,
27 + dlItemTagAutoclosing: ?Info,
28 +
29 + // <head> or <body>
30 + containerTagInScope: ?Info,
31 +};
32
33 if (__DEV__) {
34 // This validation code was written based on the HTML5 parsing spec:
@@ -143,7 +164,7 @@ if (__DEV__) {
164 'rt',
165 ];
166
146 - const emptyAncestorInfo = {
167 + const emptyAncestorInfoDev: AncestorInfoDev = {
168 current: null,
169
170 formTag: null,
@@ -155,11 +176,11 @@ if (__DEV__) {
176 listItemTagAutoclosing: null,
177 dlItemTagAutoclosing: null,
178
158 - resourceFormOnly: true,
179 + containerTagInScope: null,
180 };
181
161 - updatedAncestorInfo = function(oldInfo, tag) {
162 - const ancestorInfo = {...(oldInfo || emptyAncestorInfo)};
182 + updatedAncestorInfoDev = function(oldInfo: ?AncestorInfoDev, tag: string) {
183 + const ancestorInfo = {...(oldInfo || emptyAncestorInfoDev)};
184 const info = {tag};
185
186 if (inScopeTags.indexOf(tag) !== -1) {
@@ -183,10 +204,6 @@ if (__DEV__) {
204 ancestorInfo.dlItemTagAutoclosing = null;
205 }
206
186 - if (tag !== '#document' && tag !== 'html') {
187 - ancestorInfo.resourceFormOnly = false;
188 - }
189 -
207 ancestorInfo.current = info;
208
209 if (tag === 'form') {
@@ -210,6 +227,11 @@ if (__DEV__) {
227 if (tag === 'dd' || tag === 'dt') {
228 ancestorInfo.dlItemTagAutoclosing = info;
229 }
230 + if (tag === '#document' || tag === 'html') {
231 + ancestorInfo.containerTagInScope = null;
232 + } else if (!ancestorInfo.containerTagInScope) {
233 + ancestorInfo.containerTagInScope = info;
234 + }
235
236 return ancestorInfo;
237 };
@@ -217,7 +239,10 @@ if (__DEV__) {
239 /**
240 * Returns whether
241 */
220 - const isTagValidWithParent = function(tag, parentTag) {
242 + const isTagValidWithParent = function(
243 + tag: string,
244 + parentTag: ?string,
245 + ): boolean {
246 // First, let's check if we're in an unusual parsing mode...
247 switch (parentTag) {
248 // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inselect
@@ -342,7 +367,10 @@ if (__DEV__) {
367 /**
368 * Returns whether
369 */
345 - const findInvalidAncestorForTag = function(tag, ancestorInfo) {
370 + const findInvalidAncestorForTag = function(
371 + tag: string,
372 + ancestorInfo: AncestorInfoDev,
373 + ): ?Info {
374 switch (tag) {
375 case 'address':
376 case 'article':
@@ -408,8 +436,12 @@ if (__DEV__) {
436
437 const didWarn = {};
438
411 - validateDOMNesting = function(childTag, childText, ancestorInfo) {
412 - ancestorInfo = ancestorInfo || emptyAncestorInfo;
439 + validateDOMNesting = function(
440 + childTag: ?string,
441 + childText: ?string,
442 + ancestorInfo: AncestorInfoDev,
443 + ) {
444 + ancestorInfo = ancestorInfo || emptyAncestorInfoDev;
445 const parentInfo = ancestorInfo.current;
446 const parentTag = parentInfo && parentInfo.tag;
447
@@ -420,6 +452,11 @@ if (__DEV__) {
452 );
453 }
454 childTag = '#text';
455 + } else if (childTag == null) {
456 + console.error(
457 + 'validateDOMNesting: when childText or childTag must be provided',
458 + );
459 + return;
460 }
461
462 const invalidParent = isTagValidWithParent(childTag, parentTag)
@@ -435,7 +472,9 @@ if (__DEV__) {
472
473 const ancestorTag = invalidParentOrAncestor.tag;
474
438 - const warnKey = !!invalidParent + '|' + childTag + '|' + ancestorTag;
475 + const warnKey =
476 + // eslint-disable-next-line react-internal/safe-string-coercion
477 + String(!!invalidParent) + '|' + childTag + '|' + ancestorTag;
478 if (didWarn[warnKey]) {
479 return;
480 }
@@ -444,7 +483,7 @@ if (__DEV__) {
483 let tagDisplayName = childTag;
484 let whitespaceInfo = '';
485 if (childTag === '#text') {
447 - if (/\S/.test(childText)) {
486 + if (childText != null && /\S/.test(childText)) {
487 tagDisplayName = 'Text nodes';
488 } else {
489 tagDisplayName = 'Whitespace text nodes';
@@ -479,10 +518,6 @@ if (__DEV__) {
518 );
519 }
520 };
482 -
483 - getResourceFormOnly = hostContextDev => {
484 - return hostContextDev.ancestorInfo.resourceFormOnly;
485 - };
521 }
522
488 -export {updatedAncestorInfo, validateDOMNesting, getResourceFormOnly};
523 +export {updatedAncestorInfoDev, validateDOMNesting};
packages/react-reconciler/src/ReactFiber.new.js
+5 -2
@@ -108,6 +108,7 @@ import {
108 } from 'shared/ReactSymbols';
109 import {TransitionTracingMarker} from './ReactFiberTracingMarkerComponent.new';
110 import {detachOffscreenInstance} from './ReactFiberCommitWork.new';
111 +import {getHostContext} from './ReactFiberHostContext.new';
112
113 export type {Fiber};
114
@@ -503,13 +504,15 @@ export function createFiberFromTypeAndProps(
504 enableHostSingletons &&
505 supportsSingletons
506 ) {
506 - fiberTag = isHostResourceType(type, pendingProps)
507 + const hostContext = getHostContext();
508 + fiberTag = isHostResourceType(type, pendingProps, hostContext)
509 ? HostResource
510 : isHostSingletonType(type)
511 ? HostSingleton
512 : HostComponent;
513 } else if (enableFloat && supportsResources) {
512 - fiberTag = isHostResourceType(type, pendingProps)
514 + const hostContext = getHostContext();
515 + fiberTag = isHostResourceType(type, pendingProps, hostContext)
516 ? HostResource
517 : HostComponent;
518 } else if (enableHostSingletons && supportsSingletons) {
packages/react-reconciler/src/ReactFiber.old.js
+5 -2
@@ -108,6 +108,7 @@ import {
108 } from 'shared/ReactSymbols';
109 import {TransitionTracingMarker} from './ReactFiberTracingMarkerComponent.old';
110 import {detachOffscreenInstance} from './ReactFiberCommitWork.old';
111 +import {getHostContext} from './ReactFiberHostContext.old';
112
113 export type {Fiber};
114
@@ -503,13 +504,15 @@ export function createFiberFromTypeAndProps(
504 enableHostSingletons &&
505 supportsSingletons
506 ) {
506 - fiberTag = isHostResourceType(type, pendingProps)
507 + const hostContext = getHostContext();
508 + fiberTag = isHostResourceType(type, pendingProps, hostContext)
509 ? HostResource
510 : isHostSingletonType(type)
511 ? HostSingleton
512 : HostComponent;
513 } else if (enableFloat && supportsResources) {
512 - fiberTag = isHostResourceType(type, pendingProps)
514 + const hostContext = getHostContext();
515 + fiberTag = isHostResourceType(type, pendingProps, hostContext)
516 ? HostResource
517 : HostComponent;
518 } else if (enableHostSingletons && supportsSingletons) {