[Float] support meta tags as Resources (#25514)
Stacked on #25508 This PR adds meta tags as a resource type. metas are classified in the following priority 1. charset 2. http-equiv 3. property 4. name 5. itemprop when using property, there is special logic for og type properties where a `property="og:image:height"` following a `property="og:image"` will inherit the key of the previous tag. this relies on timing effects to stay consistent so when mounting new metas it is important that if structured properties are being used all members of a structure mount together. This is similarly true for arrays where the implicit sequential order defines the array structure. if you need an array you need to mount all array members in the same pass.
Josh Story committed
Oct 21, 2022 at 15:21 UTC
973b90bdf6f4a1d9b3864d93985d4a204f233855
11 files changed
+823
-159
packages/react-dom-bindings/src/client/ReactDOMComponentTree.js
+1
@@ -286,6 +286,7 @@ export function getResourcesFromRoot(root: FloatRoot): RootResources {
286
styles: new Map(),
287
scripts: new Map(),
288
head: new Map(),
289
+ lastStructuredMeta: new Map(),
290
};
291
}
292
return resources;
packages/react-dom-bindings/src/client/ReactDOMFloatClient.js
+187
-88
@@ -88,13 +88,29 @@ export type ScriptResource = {
88
root: FloatRoot,
89
};
90
91
-type HeadProps = {
91
+export type HeadResource = TitleResource | MetaResource;
92
+
93
+type TitleProps = {
94
+ [string]: mixed,
95
+};
96
+export type TitleResource = {
97
+ type: 'title',
98
+ props: TitleProps,
99
+
100
+ count: number,
101
+ instance: ?Element,
102
+ root: Document,
103
+};
104
+
105
+type MetaProps = {
106
[string]: mixed,
107
};
94
-export type HeadResource = {
95
- type: 'head',
96
- instanceType: string,
97
- props: HeadProps,
108
+export type MetaResource = {
109
+ type: 'meta',
110
+ matcher: string,
111
+ property: ?string,
112
+ parentResource: ?MetaResource,
113
+ props: MetaProps,
114
115
count: number,
116
instance: ?Element,
@@ -109,6 +125,7 @@ export type RootResources = {
125
styles: Map<string, StyleResource>,
126
scripts: Map<string, ScriptResource>,
127
head: Map<string, HeadResource>,
128
+ lastStructuredMeta: Map<string, MetaResource>,
129
};
130
131
// Brief on purpose due to insertion by script when streaming late boundaries
@@ -409,6 +426,84 @@ export function getResource(
426
);
427
}
428
switch (type) {
429
+ case 'meta': {
430
+ let matcher, propertyString, parentResource;
431
+ const {
432
+ charSet,
433
+ content,
434
+ httpEquiv,
435
+ name,
436
+ itemProp,
437
+ property,
438
+ } = pendingProps;
439
+ const headRoot: Document = getDocumentFromRoot(resourceRoot);
440
+ const {head: headResources, lastStructuredMeta} = getResourcesFromRoot(
441
+ headRoot,
442
+ );
443
+ if (typeof charSet === 'string') {
444
+ matcher = 'meta[charset]';
445
+ } else if (typeof content === 'string') {
446
+ if (typeof httpEquiv === 'string') {
447
+ matcher = `meta[http-equiv="${escapeSelectorAttributeValueInsideDoubleQuotes(
448
+ httpEquiv,
449
+ )}"][content="${escapeSelectorAttributeValueInsideDoubleQuotes(
450
+ content,
451
+ )}"]`;
452
+ } else if (typeof property === 'string') {
453
+ propertyString = property;
454
+ matcher = `meta[property="${escapeSelectorAttributeValueInsideDoubleQuotes(
455
+ property,
456
+ )}"][content="${escapeSelectorAttributeValueInsideDoubleQuotes(
457
+ content,
458
+ )}"]`;
459
+
460
+ const parentPropertyPath = property
461
+ .split(':')
462
+ .slice(0, -1)
463
+ .join(':');
464
+ parentResource = lastStructuredMeta.get(parentPropertyPath);
465
+ if (parentResource) {
466
+ // When using parentResource the matcher is not functional for locating
467
+ // the instance in the DOM but it still serves as a unique key.
468
+ matcher = parentResource.matcher + matcher;
469
+ }
470
+ } else if (typeof name === 'string') {
471
+ matcher = `meta[name="${escapeSelectorAttributeValueInsideDoubleQuotes(
472
+ name,
473
+ )}"][content="${escapeSelectorAttributeValueInsideDoubleQuotes(
474
+ content,
475
+ )}"]`;
476
+ } else if (typeof itemProp === 'string') {
477
+ matcher = `meta[itemprop="${escapeSelectorAttributeValueInsideDoubleQuotes(
478
+ itemProp,
479
+ )}"][content="${escapeSelectorAttributeValueInsideDoubleQuotes(
480
+ content,
481
+ )}"]`;
482
+ }
483
+ }
484
+ if (matcher) {
485
+ let resource = headResources.get(matcher);
486
+ if (!resource) {
487
+ resource = {
488
+ type: 'meta',
489
+ matcher,
490
+ property: propertyString,
491
+ parentResource,
492
+ props: Object.assign({}, pendingProps),
493
+ count: 0,
494
+ instance: null,
495
+ root: headRoot,
496
+ };
497
+ headResources.set(matcher, resource);
498
+ }
499
+ if (typeof resource.property === 'string') {
500
+ // We cast because flow doesn't know that this resource must be a Meta resource
501
+ lastStructuredMeta.set(resource.property, (resource: any));
502
+ }
503
+ return resource;
504
+ }
505
+ return null;
506
+ }
507
case 'title': {
508
let child = pendingProps.children;
509
if (Array.isArray(child) && child.length === 1) {
@@ -421,13 +516,14 @@ export function getResource(
516
let resource = headResources.get(key);
517
if (!resource) {
518
const titleProps = titlePropsFromRawProps(child, pendingProps);
424
- resource = createHeadResource(
425
- headResources,
426
- headRoot,
427
- 'title',
428
- key,
429
- titleProps,
430
- );
519
+ resource = {
520
+ type: 'title',
521
+ props: titleProps,
522
+ count: 0,
523
+ instance: null,
524
+ root: headRoot,
525
+ };
526
+ headResources.set(key, resource);
527
}
528
return resource;
529
}
@@ -588,8 +684,8 @@ function preloadPropsFromRawProps(
684
function titlePropsFromRawProps(
685
child: string | number,
686
rawProps: Props,
591
-): HeadProps {
592
- const props: HeadProps = Object.assign({}, rawProps);
687
+): TitleProps {
688
+ const props: TitleProps = Object.assign({}, rawProps);
689
props.children = child;
690
return props;
691
}
@@ -613,7 +709,8 @@ function scriptPropsFromRawProps(rawProps: ScriptQualifyingProps): ScriptProps {
709
710
export function acquireResource(resource: Resource): Instance {
711
switch (resource.type) {
616
- case 'head': {
712
+ case 'title':
713
+ case 'meta': {
714
return acquireHeadResource(resource);
715
}
716
case 'style': {
@@ -635,7 +732,8 @@ export function acquireResource(resource: Resource): Instance {
732
733
export function releaseResource(resource: Resource): void {
734
switch (resource.type) {
638
- case 'head': {
735
+ case 'title':
736
+ case 'meta': {
737
return releaseHeadResource(resource);
738
}
739
case 'style': {
@@ -668,35 +766,6 @@ function createResourceInstance(
766
return element;
767
}
768
671
-function createHeadResource(
672
- headResources: Map<string, HeadResource>,
673
- root: Document,
674
- instanceType: string,
675
- key: string,
676
- props: HeadProps,
677
-): HeadResource {
678
- if (__DEV__) {
679
- if (headResources.has(key)) {
680
- console.error(
681
- 'createHeadResource was called when a head Resource matching the same key already exists. This is a bug in React.',
682
- );
683
- }
684
- }
685
-
686
- const resource: HeadResource = {
687
- type: 'head',
688
- instanceType,
689
- props,
690
-
691
- count: 0,
692
- instance: null,
693
- root,
694
- };
695
-
696
- headResources.set(key, resource);
697
- return resource;
698
-}
699
-
769
function createStyleResource(
770
styleResources: Map<string, StyleResource>,
771
root: FloatRoot,
@@ -894,7 +963,7 @@ function createPreloadResource(
963
);
964
if (!element) {
965
element = createResourceInstance('link', props, ownerDocument);
897
- appendResourceInstance(element, ownerDocument);
966
+ insertResourceInstanceBefore(ownerDocument, element, null);
967
} else {
968
markNodeAsResource(element);
969
}
@@ -911,8 +980,8 @@ function acquireHeadResource(resource: HeadResource): Instance {
980
resource.count++;
981
let instance = resource.instance;
982
if (!instance) {
914
- const {props, root, instanceType} = resource;
915
- switch (instanceType) {
983
+ const {props, root, type} = resource;
984
+ switch (type) {
985
case 'title': {
986
const titles = root.querySelectorAll('title');
987
for (let i = 0; i < titles.length; i++) {
@@ -922,18 +991,70 @@ function acquireHeadResource(resource: HeadResource): Instance {
991
return instance;
992
}
993
}
994
+ instance = resource.instance = createResourceInstance(
995
+ type,
996
+ props,
997
+ root,
998
+ );
999
+ insertResourceInstanceBefore(root, instance, titles.item(0));
1000
+ break;
1001
+ }
1002
+ case 'meta': {
1003
+ let insertBefore = null;
1004
+
1005
+ const metaResource: MetaResource = (resource: any);
1006
+ const {matcher, property, parentResource} = metaResource;
1007
+
1008
+ if (parentResource && typeof property === 'string') {
1009
+ // This resoruce is a structured meta type with a parent.
1010
+ // Instead of using the matcher we just traverse forward
1011
+ // siblings of the parent instance until we find a match
1012
+ // or exhaust.
1013
+ const parent = parentResource.instance;
1014
+ if (parent) {
1015
+ let node = null;
1016
+ let nextNode = (insertBefore = parent.nextSibling);
1017
+ while ((node = nextNode)) {
1018
+ nextNode = node.nextSibling;
1019
+ if (node.nodeName === 'META') {
1020
+ const meta: Element = (node: any);
1021
+ const propertyAttr = meta.getAttribute('property');
1022
+ if (typeof propertyAttr !== 'string') {
1023
+ continue;
1024
+ } else if (
1025
+ propertyAttr === property &&
1026
+ meta.getAttribute('content') === props.content
1027
+ ) {
1028
+ resource.instance = meta;
1029
+ markNodeAsResource(meta);
1030
+ return meta;
1031
+ } else if (property.startsWith(propertyAttr + ':')) {
1032
+ // This meta starts a new instance of a parent structure for this meta type
1033
+ // We need to halt our search here because even if we find a later match it
1034
+ // is for a different parent element
1035
+ break;
1036
+ }
1037
+ }
1038
+ }
1039
+ }
1040
+ } else if ((instance = root.querySelector(matcher))) {
1041
+ resource.instance = instance;
1042
+ markNodeAsResource(instance);
1043
+ return instance;
1044
+ }
1045
+ instance = resource.instance = createResourceInstance(
1046
+ type,
1047
+ props,
1048
+ root,
1049
+ );
1050
+ insertResourceInstanceBefore(root, instance, insertBefore);
1051
+ break;
1052
+ }
1053
+ default: {
1054
+ throw new Error(
1055
+ `acquireHeadResource encountered a resource type it did not expect: "${type}". This is a bug in React.`,
1056
+ );
1057
}
926
- }
927
- instance = resource.instance = createResourceInstance(
928
- instanceType,
929
- props,
930
- root,
931
- );
932
-
933
- if (instanceType === 'title') {
934
- prependResourceInstance(instance, root);
935
- } else {
936
- appendResourceInstance(instance, root);
1058
}
1059
}
1060
return instance;
@@ -1010,7 +1131,7 @@ function acquireScriptResource(resource: ScriptResource): Instance {
1131
getDocumentFromRoot(root),
1132
);
1133
1013
- appendResourceInstance(instance, getDocumentFromRoot(root));
1134
+ insertResourceInstanceBefore(getDocumentFromRoot(root), instance, null);
1135
}
1136
}
1137
return instance;
@@ -1113,45 +1234,22 @@ function insertStyleInstance(
1234
}
1235
}
1236
1116
-function prependResourceInstance(
1117
- instance: Instance,
1237
+function insertResourceInstanceBefore(
1238
ownerDocument: Document,
1119
-): void {
1120
- if (__DEV__) {
1121
- if (instance.tagName === 'LINK' && (instance: any).rel === 'stylesheet') {
1122
- console.error(
1123
- 'prependResourceInstance was called with a stylesheet. Stylesheets must be' +
1124
- ' inserted with insertStyleInstance instead. This is a bug in React.',
1125
- );
1126
- }
1127
- }
1128
-
1129
- const parent = ownerDocument.head;
1130
- if (parent) {
1131
- parent.insertBefore(instance, parent.firstChild);
1132
- } else {
1133
- throw new Error(
1134
- 'While attempting to insert a Resource, React expected the Document to contain' +
1135
- ' a head element but it was not found.',
1136
- );
1137
- }
1138
-}
1139
-
1140
-function appendResourceInstance(
1239
instance: Instance,
1142
- ownerDocument: Document,
1240
+ before: ?Node,
1241
): void {
1242
if (__DEV__) {
1243
if (instance.tagName === 'LINK' && (instance: any).rel === 'stylesheet') {
1244
console.error(
1147
- 'appendResourceInstance was called with a stylesheet. Stylesheets must be' +
1245
+ 'insertResourceInstanceBefore was called with a stylesheet. Stylesheets must be' +
1246
' inserted with insertStyleInstance instead. This is a bug in React.',
1247
);
1248
}
1249
}
1152
- const parent = ownerDocument.head;
1250
+ const parent = (before && before.parentNode) || ownerDocument.head;
1251
if (parent) {
1154
- parent.appendChild(instance);
1252
+ parent.insertBefore(instance, before);
1253
} else {
1254
throw new Error(
1255
'While attempting to insert a Resource, React expected the Document to contain' +
@@ -1162,6 +1260,7 @@ function appendResourceInstance(
1260
1261
export function isHostResourceType(type: string, props: Props): boolean {
1262
switch (type) {
1263
+ case 'meta':
1264
case 'title': {
1265
return true;
1266
}
packages/react-dom-bindings/src/client/ReactDOMHostConfig.js
+6
-5
@@ -714,13 +714,14 @@ export function unhideTextInstance(
714
export function clearContainer(container: Container): void {
715
if (enableHostSingletons) {
716
const nodeType = container.nodeType;
717
- if (nodeType === DOCUMENT_NODE || nodeType === ELEMENT_NODE) {
717
+ if (nodeType === DOCUMENT_NODE) {
718
+ clearContainerSparingly(container);
719
+ } else if (nodeType === ELEMENT_NODE) {
720
switch (container.nodeName) {
719
- case '#document':
721
case 'HTML':
722
case 'HEAD':
723
case 'BODY':
723
- clearContainerChildren(container);
724
+ clearContainerSparingly(container);
725
return;
726
default: {
727
container.textContent = '';
@@ -742,7 +743,7 @@ export function clearContainer(container: Container): void {
743
}
744
}
745
745
-function clearContainerChildren(container: Node) {
746
+function clearContainerSparingly(container: Node) {
747
let node;
748
let nextNode: ?Node = container.firstChild;
749
if (nextNode && nextNode.nodeType === DOCUMENT_TYPE_NODE) {
@@ -756,7 +757,7 @@ function clearContainerChildren(container: Node) {
757
case 'HEAD':
758
case 'BODY': {
759
const element: Element = (node: any);
759
- clearContainerChildren(element);
760
+ clearContainerSparingly(element);
761
// If these singleton instances had previously been rendered with React they
762
// may still hold on to references to the previous fiber tree. We detatch them
763
// prospectively to reset them to a baseline starting state since we cannot create
packages/react-dom-bindings/src/server/ReactDOMFloatServer.js
+79
-41
@@ -68,19 +68,29 @@ type ScriptResource = {
68
hint: PreloadResource,
69
};
70
71
-type HeadProps = {
71
+type TitleProps = {
72
[string]: mixed,
73
};
74
-type HeadResource = {
75
- type: 'head',
76
- instanceType: string,
77
- props: HeadProps,
74
+type TitleResource = {
75
+ type: 'title',
76
+ props: TitleProps,
77
+
78
+ flushed: boolean,
79
+};
80
+
81
+type MetaProps = {
82
+ [string]: mixed,
83
+};
84
+type MetaResource = {
85
+ type: 'meta',
86
+ key: string,
87
+ props: MetaProps,
88
89
flushed: boolean,
80
- allowLate: boolean,
90
};
91
92
export type Resource = PreloadResource | StyleResource | ScriptResource;
93
+export type HeadResource = TitleResource | MetaResource;
94
95
export type Resources = {
96
// Request local cache
@@ -90,7 +100,7 @@ export type Resources = {
100
headsMap: Map<string, HeadResource>,
101
102
// Flushing queues for Resource dependencies
93
- charset: null | HeadResource,
103
+ charset: null | MetaResource,
104
fontPreloads: Set<PreloadResource>,
105
// usedImagePreloads: Set<PreloadResource>,
106
precedences: Map<string, Set<StyleResource>>,
@@ -102,6 +112,9 @@ export type Resources = {
112
explicitScriptPreloads: Set<PreloadResource>,
113
headResources: Set<HeadResource>,
114
115
+ // cache for tracking structured meta tags
116
+ structuredMetaKeys: Map<string, MetaResource>,
117
+
118
// Module-global-like reference for current boundary resources
119
boundaryResources: ?BoundaryResources,
120
...
@@ -129,6 +142,9 @@ export function createResources(): Resources {
142
explicitScriptPreloads: new Set(),
143
headResources: new Set(),
144
145
+ // cache for tracking structured meta tags
146
+ structuredMetaKeys: new Map(),
147
+
148
// like a module global for currently rendering boundary
149
boundaryResources: null,
150
};
@@ -581,40 +597,10 @@ function adoptPreloadPropsForScriptProps(
597
resourceProps.integrity = preloadProps.integrity;
598
}
599
584
-function createHeadResource(
585
- resources: Resources,
586
- key: string,
587
- instanceType: string,
588
- props: HeadProps,
589
-): HeadResource {
590
- if (__DEV__) {
591
- if (resources.headsMap.has(key)) {
592
- console.error(
593
- 'createScriptResource was called when a script Resource matching the same src already exists. This is a bug in React.',
594
- );
595
- }
596
- }
597
-
598
- const resource: HeadResource = {
599
- type: 'head',
600
- instanceType,
601
- props,
602
-
603
- flushed: false,
604
- allowLate: true,
605
- };
606
- resources.headsMap.set(key, resource);
607
- return resource;
608
-}
609
-
610
-function getTitleKey(child: string | number): string {
611
- return 'title' + child;
612
-}
613
-
600
function titlePropsFromRawProps(
601
child: string | number,
602
rawProps: Props,
617
-): HeadProps {
603
+): TitleProps {
604
const props = Object.assign({}, rawProps);
605
props.children = child;
606
return props;
@@ -634,17 +620,69 @@ export function resourcesFromElement(type: string, props: Props): boolean {
620
child = child[0];
621
}
622
if (typeof child === 'string' || typeof child === 'number') {
637
- const key = getTitleKey(child);
623
+ const key = 'title::' + child;
624
let resource = resources.headsMap.get(key);
625
if (!resource) {
640
- const titleProps = titlePropsFromRawProps(child, props);
641
- resource = createHeadResource(resources, key, 'title', titleProps);
626
+ resource = {
627
+ type: 'title',
628
+ props: titlePropsFromRawProps(child, props),
629
+ flushed: false,
630
+ };
631
+ resources.headsMap.set(key, resource);
632
resources.headResources.add(resource);
633
}
634
return true;
635
}
636
return false;
637
}
638
+ case 'meta': {
639
+ let key, propertyPath;
640
+ if (typeof props.charSet === 'string') {
641
+ key = 'charSet';
642
+ } else if (typeof props.content === 'string') {
643
+ const contentKey = '::' + props.content;
644
+ if (typeof props.httpEquiv === 'string') {
645
+ key = 'httpEquiv::' + props.httpEquiv + contentKey;
646
+ } else if (typeof props.name === 'string') {
647
+ key = 'name::' + props.name + contentKey;
648
+ } else if (typeof props.itemProp === 'string') {
649
+ key = 'itemProp::' + props.itemProp + contentKey;
650
+ } else if (typeof props.property === 'string') {
651
+ const {property} = props;
652
+ key = 'property::' + property + contentKey;
653
+ propertyPath = property;
654
+ const parentPath = property
655
+ .split(':')
656
+ .slice(0, -1)
657
+ .join(':');
658
+ const parentResource = resources.structuredMetaKeys.get(parentPath);
659
+ if (parentResource) {
660
+ key = parentResource.key + '::child::' + key;
661
+ }
662
+ }
663
+ }
664
+ if (key) {
665
+ if (!resources.headsMap.has(key)) {
666
+ const resource = {
667
+ type: 'meta',
668
+ key,
669
+ props: Object.assign({}, props),
670
+ flushed: false,
671
+ };
672
+ resources.headsMap.set(key, resource);
673
+ if (key === 'charSet') {
674
+ resources.charset = resource;
675
+ } else {
676
+ if (propertyPath) {
677
+ resources.structuredMetaKeys.set(propertyPath, resource);
678
+ }
679
+ resources.headResources.add(resource);
680
+ }
681
+ }
682
+ return true;
683
+ }
684
+ return false;
685
+ }
686
}
687
return false;
688
}
packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js
+60
-11
@@ -1150,6 +1150,26 @@ function pushStartTextArea(
1150
return null;
1151
}
1152
1153
+function pushMeta(
1154
+ target: Array<Chunk | PrecomputedChunk>,
1155
+ props: Object,
1156
+ responseState: ResponseState,
1157
+ textEmbedded: boolean,
1158
+): ReactNodeList {
1159
+ if (enableFloat && resourcesFromElement('meta', props)) {
1160
+ if (textEmbedded) {
1161
+ // This link follows text but we aren't writing a tag. while not as efficient as possible we need
1162
+ // to be safe and assume text will follow by inserting a textSeparator
1163
+ target.push(textSeparator);
1164
+ }
1165
+ // We have converted this link exclusively to a resource and no longer
1166
+ // need to emit it
1167
+ return null;
1168
+ }
1169
+
1170
+ return pushSelfClosing(target, props, 'meta', responseState);
1171
+}
1172
+
1173
function pushLink(
1174
target: Array<Chunk | PrecomputedChunk>,
1175
props: Object,
@@ -1688,6 +1708,8 @@ export function pushStartInstance(
1708
return pushLink(target, props, responseState, textEmbedded);
1709
case 'script':
1710
return pushStartScript(target, props, responseState, textEmbedded);
1711
+ case 'meta':
1712
+ return pushMeta(target, props, responseState, textEmbedded);
1713
// Newline eating tags
1714
case 'listing':
1715
case 'pre': {
@@ -1702,7 +1724,6 @@ export function pushStartInstance(
1724
case 'hr':
1725
case 'img':
1726
case 'keygen':
1705
- case 'meta':
1727
case 'param':
1728
case 'source':
1729
case 'track':
@@ -2318,6 +2339,7 @@ export function writeInitialResources(
2339
const target = [];
2340
2341
const {
2342
+ charset,
2343
fontPreloads,
2344
precedences,
2345
usedStylePreloads,
@@ -2328,6 +2350,12 @@ export function writeInitialResources(
2350
headResources,
2351
} = resources;
2352
2353
+ if (charset) {
2354
+ pushSelfClosing(target, charset.props, 'meta', responseState);
2355
+ charset.flushed = true;
2356
+ resources.charset = null;
2357
+ }
2358
+
2359
fontPreloads.forEach(r => {
2360
// font preload Resources should not already be flushed so we elide this check
2361
pushLinkImpl(target, r.props, responseState);
@@ -2377,12 +2405,19 @@ export function writeInitialResources(
2405
explicitScriptPreloads.clear();
2406
2407
headResources.forEach(r => {
2380
- if (r.instanceType === 'title') {
2381
- pushStartTitleImpl(target, r.props, responseState);
2382
- if (typeof r.props.children === 'string') {
2383
- target.push(escapeTextForBrowser(stringToChunk(r.props.children)));
2408
+ switch (r.type) {
2409
+ case 'title': {
2410
+ pushStartTitleImpl(target, r.props, responseState);
2411
+ if (typeof r.props.children === 'string') {
2412
+ target.push(escapeTextForBrowser(stringToChunk(r.props.children)));
2413
+ }
2414
+ pushEndInstance(target, target, 'title', r.props);
2415
+ break;
2416
+ }
2417
+ case 'meta': {
2418
+ pushSelfClosing(target, r.props, 'meta', responseState);
2419
+ break;
2420
}
2385
- pushEndInstance(target, target, 'title', r.props);
2421
}
2422
r.flushed = true;
2423
});
@@ -2414,6 +2449,7 @@ export function writeImmediateResources(
2449
const target = [];
2450
2451
const {
2452
+ charset,
2453
fontPreloads,
2454
usedStylePreloads,
2455
scripts,
@@ -2423,6 +2459,12 @@ export function writeImmediateResources(
2459
headResources,
2460
} = resources;
2461
2462
+ if (charset) {
2463
+ pushSelfClosing(target, charset.props, 'meta', responseState);
2464
+ charset.flushed = true;
2465
+ resources.charset = null;
2466
+ }
2467
+
2468
fontPreloads.forEach(r => {
2469
// font preload Resources should not already be flushed so we elide this check
2470
pushLinkImpl(target, r.props, responseState);
@@ -2452,12 +2494,19 @@ export function writeImmediateResources(
2494
explicitScriptPreloads.clear();
2495
2496
headResources.forEach(r => {
2455
- if (r.instanceType === 'title') {
2456
- pushStartTitle(target, r.props, responseState);
2457
- if (typeof r.props.children === 'string') {
2458
- target.push(escapeTextForBrowser(stringToChunk(r.props.children)));
2497
+ switch (r.type) {
2498
+ case 'title': {
2499
+ pushStartTitleImpl(target, r.props, responseState);
2500
+ if (typeof r.props.children === 'string') {
2501
+ target.push(escapeTextForBrowser(stringToChunk(r.props.children)));
2502
+ }
2503
+ pushEndInstance(target, target, 'title', r.props);
2504
+ break;
2505
+ }
2506
+ case 'meta': {
2507
+ pushSelfClosing(target, r.props, 'meta', responseState);
2508
+ break;
2509
}
2460
- pushEndInstance(target, target, 'title', r.props);
2510
}
2511
r.flushed = true;
2512
});
packages/react-dom/src/__tests__/ReactDOMFloat-test.js
+472
@@ -941,6 +941,478 @@ describe('ReactDOMFloat', () => {
941
});
942
943
describe('head resources', () => {
944
+ // @gate enableFloat
945
+ it('can hydrate the right instances for deeply nested structured metas', async () => {
946
+ await actIntoEmptyDocument(() => {
947
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
948
+ <>
949
+ <html>
950
+ <head />
951
+ <body>
952
+ <div>hello world</div>
953
+ </body>
954
+ </html>
955
+ <meta property="og:foo" content="one" />
956
+ <meta property="og:foo:bar" content="bar" />
957
+ <meta property="og:foo:bar:baz" content="baz" />
958
+ <meta property="og:foo" content="two" />
959
+ <meta property="og:foo:bar" content="bar" />
960
+ <meta property="og:foo:bar:baz" content="baz" />
961
+ </>,
962
+ );
963
+ pipe(writable);
964
+ });
965
+ expect(getMeaningfulChildren(document)).toEqual(
966
+ <html>
967
+ <head>
968
+ <meta property="og:foo" content="one" />
969
+ <meta property="og:foo:bar" content="bar" />
970
+ <meta property="og:foo:bar:baz" content="baz" />
971
+ <meta property="og:foo" content="two" />
972
+ <meta property="og:foo:bar" content="bar" />
973
+ <meta property="og:foo:bar:baz" content="baz" />
974
+ </head>
975
+ <body>
976
+ <div>hello world</div>
977
+ </body>
978
+ </html>,
979
+ );
980
+
981
+ const root = ReactDOMClient.hydrateRoot(
982
+ document,
983
+ <>
984
+ <html>
985
+ <head />
986
+ <body>
987
+ <div>hello world</div>
988
+ </body>
989
+ </html>
990
+ <meta property="og:foo" content="one" />
991
+ <meta property="og:foo:bar" content="bar" />
992
+ <meta property="og:foo:bar:baz" content="baz" />
993
+ <meta property="og:foo" content="two" />
994
+ <meta property="og:foo:bar" content="bar" />
995
+ <meta property="og:foo:bar:baz" content="baz" />
996
+ </>,
997
+ );
998
+ expect(Scheduler).toFlushWithoutYielding();
999
+ expect(getMeaningfulChildren(document)).toEqual(
1000
+ <html>
1001
+ <head>
1002
+ <meta property="og:foo" content="one" />
1003
+ <meta property="og:foo:bar" content="bar" />
1004
+ <meta property="og:foo:bar:baz" content="baz" />
1005
+ <meta property="og:foo" content="two" />
1006
+ <meta property="og:foo:bar" content="bar" />
1007
+ <meta property="og:foo:bar:baz" content="baz" />
1008
+ </head>
1009
+ <body>
1010
+ <div>hello world</div>
1011
+ </body>
1012
+ </html>,
1013
+ );
1014
+
1015
+ root.render(
1016
+ <>
1017
+ <html>
1018
+ <head />
1019
+ <body>
1020
+ <div>hello world</div>
1021
+ </body>
1022
+ </html>
1023
+ </>,
1024
+ );
1025
+ expect(Scheduler).toFlushWithoutYielding();
1026
+ expect(getMeaningfulChildren(document)).toEqual(
1027
+ <html>
1028
+ <head />
1029
+ <body>
1030
+ <div>hello world</div>
1031
+ </body>
1032
+ </html>,
1033
+ );
1034
+ });
1035
+
1036
+ // @gate enableFloat
1037
+ it('can insert meta tags in the expected location', async () => {
1038
+ await actIntoEmptyDocument(() => {
1039
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
1040
+ <>
1041
+ <html>
1042
+ <head />
1043
+ <body>
1044
+ <div>hello world</div>
1045
+ </body>
1046
+ </html>
1047
+ <meta charSet="utf-8" />
1048
+ <meta name="google-site-verification" content="somehash1" />
1049
+ <meta name="google-site-verification" content="somehash2" />
1050
+ <meta
1051
+ name="description"
1052
+ property="og:description"
1053
+ content="my site"
1054
+ />
1055
+ <meta property="og:image" content="foo" />
1056
+ <meta property="og:image:width" content="100" />
1057
+ <meta httpEquiv="refresh" content="dont actually" />
1058
+ <meta property="og:image:height" content="100" />
1059
+ <meta property="og:image" content="bar" />
1060
+ <meta property="og:image:width" content="100" />
1061
+ <meta itemProp="someprop" content="somevalue" />
1062
+ <meta property="og:image:height" content="100" />
1063
+ <meta property="og:description:foo" content="foo" />
1064
+ </>,
1065
+ );
1066
+ pipe(writable);
1067
+ });
1068
+ expect(getMeaningfulChildren(document)).toEqual(
1069
+ <html>
1070
+ <head>
1071
+ <meta charset="utf-8" />
1072
+ <meta name="google-site-verification" content="somehash1" />
1073
+ <meta name="google-site-verification" content="somehash2" />
1074
+ <meta
1075
+ name="description"
1076
+ property="og:description"
1077
+ content="my site"
1078
+ />
1079
+ <meta property="og:image" content="foo" />
1080
+ <meta property="og:image:width" content="100" />
1081
+ <meta http-equiv="refresh" content="dont actually" />
1082
+ <meta property="og:image:height" content="100" />
1083
+ <meta property="og:image" content="bar" />
1084
+ <meta property="og:image:width" content="100" />
1085
+ <meta itemprop="someprop" content="somevalue" />
1086
+ <meta property="og:image:height" content="100" />
1087
+ <meta property="og:description:foo" content="foo" />
1088
+ </head>
1089
+ <body>
1090
+ <div>hello world</div>
1091
+ </body>
1092
+ </html>,
1093
+ );
1094
+
1095
+ const root = ReactDOMClient.hydrateRoot(
1096
+ document,
1097
+ <html>
1098
+ <head>
1099
+ <meta property="og:image" content="bar" />
1100
+ <meta property="og:image:width" content="100" />
1101
+ <meta property="og:image:height" content="100" />
1102
+ </head>
1103
+ <body>
1104
+ <div>hello world</div>
1105
+ </body>
1106
+ </html>,
1107
+ );
1108
+ expect(Scheduler).toFlushWithoutYielding();
1109
+ expect(getMeaningfulChildren(document)).toEqual(
1110
+ <html>
1111
+ <head>
1112
+ <meta charset="utf-8" />
1113
+ <meta name="google-site-verification" content="somehash1" />
1114
+ <meta name="google-site-verification" content="somehash2" />
1115
+ <meta
1116
+ name="description"
1117
+ property="og:description"
1118
+ content="my site"
1119
+ />
1120
+ <meta property="og:image" content="foo" />
1121
+ <meta property="og:image:width" content="100" />
1122
+ <meta http-equiv="refresh" content="dont actually" />
1123
+ <meta property="og:image:height" content="100" />
1124
+ <meta property="og:image" content="bar" />
1125
+ <meta property="og:image:width" content="100" />
1126
+ <meta itemprop="someprop" content="somevalue" />
1127
+ <meta property="og:image:height" content="100" />
1128
+ <meta property="og:description:foo" content="foo" />
1129
+ </head>
1130
+ <body>
1131
+ <div>hello world</div>
1132
+ </body>
1133
+ </html>,
1134
+ );
1135
+
1136
+ root.render(
1137
+ <html>
1138
+ <head>
1139
+ <meta property="og:image" content="bar" />
1140
+ <meta property="og:image:width" content="100" />
1141
+ <meta property="og:image:height" content="100" />
1142
+ <meta property="og:description" content="my site" />
1143
+ <meta
1144
+ itemProp="description bar"
1145
+ property="og:description:bar"
1146
+ content="bar"
1147
+ />
1148
+ </head>
1149
+ <body>
1150
+ <div>hello world</div>
1151
+ </body>
1152
+ </html>,
1153
+ );
1154
+ expect(Scheduler).toFlushWithoutYielding();
1155
+ expect(getMeaningfulChildren(document)).toEqual(
1156
+ <html>
1157
+ <head>
1158
+ <meta charset="utf-8" />
1159
+ <meta name="google-site-verification" content="somehash1" />
1160
+ <meta name="google-site-verification" content="somehash2" />
1161
+ <meta
1162
+ name="description"
1163
+ property="og:description"
1164
+ content="my site"
1165
+ />
1166
+ <meta
1167
+ itemprop="description bar"
1168
+ property="og:description:bar"
1169
+ content="bar"
1170
+ />
1171
+ <meta property="og:image" content="foo" />
1172
+ <meta property="og:image:width" content="100" />
1173
+ <meta http-equiv="refresh" content="dont actually" />
1174
+ <meta property="og:image:height" content="100" />
1175
+ <meta property="og:image" content="bar" />
1176
+ <meta property="og:image:width" content="100" />
1177
+ <meta itemprop="someprop" content="somevalue" />
1178
+ <meta property="og:image:height" content="100" />
1179
+ <meta property="og:description:foo" content="foo" />
1180
+ </head>
1181
+ <body>
1182
+ <div>hello world</div>
1183
+ </body>
1184
+ </html>,
1185
+ );
1186
+ });
1187
+
1188
+ // @gate enableFloat
1189
+ it('can render meta tags with og properties with structured data', async () => {
1190
+ await actIntoEmptyDocument(() => {
1191
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
1192
+ <>
1193
+ <html>
1194
+ <head />
1195
+ <body>
1196
+ <div>hello world</div>
1197
+ </body>
1198
+ </html>
1199
+ <meta property="og:image" content="foo" />
1200
+ <meta property="og:image:width" content="100" />
1201
+ <meta property="og:image:height" content="100" />
1202
+ <meta property="og:image" content="bar" />
1203
+ <meta property="og:image:width" content="100" />
1204
+ <meta property="og:image:height" content="100" />
1205
+ </>,
1206
+ );
1207
+ pipe(writable);
1208
+ });
1209
+ expect(getMeaningfulChildren(document)).toEqual(
1210
+ <html>
1211
+ <head>
1212
+ <meta property="og:image" content="foo" />
1213
+ <meta property="og:image:width" content="100" />
1214
+ <meta property="og:image:height" content="100" />
1215
+ <meta property="og:image" content="bar" />
1216
+ <meta property="og:image:width" content="100" />
1217
+ <meta property="og:image:height" content="100" />
1218
+ </head>
1219
+ <body>
1220
+ <div>hello world</div>
1221
+ </body>
1222
+ </html>,
1223
+ );
1224
+
1225
+ const root = ReactDOMClient.hydrateRoot(
1226
+ document,
1227
+ <html>
1228
+ <head />
1229
+ <body>
1230
+ <meta property="og:image" content="foo" />
1231
+ <meta property="og:image:width" content="100" />
1232
+ <meta property="og:image:height" content="100" />
1233
+ <meta property="og:image" content="bar" />
1234
+ <meta property="og:image:width" content="100" />
1235
+ <meta property="og:image:height" content="100" />
1236
+ <div>hello world</div>
1237
+ </body>
1238
+ </html>,
1239
+ );
1240
+ expect(Scheduler).toFlushWithoutYielding();
1241
+ expect(getMeaningfulChildren(document)).toEqual(
1242
+ <html>
1243
+ <head>
1244
+ <meta property="og:image" content="foo" />
1245
+ <meta property="og:image:width" content="100" />
1246
+ <meta property="og:image:height" content="100" />
1247
+ <meta property="og:image" content="bar" />
1248
+ <meta property="og:image:width" content="100" />
1249
+ <meta property="og:image:height" content="100" />
1250
+ </head>
1251
+ <body>
1252
+ <div>hello world</div>
1253
+ </body>
1254
+ </html>,
1255
+ );
1256
+
1257
+ root.render(
1258
+ <html>
1259
+ <head />
1260
+ <body>
1261
+ <meta property="og:image" content="foo" />
1262
+ <meta property="og:image:width" content="100" />
1263
+ <meta property="og:image:height" content="100" />
1264
+ <meta property="og:image" content="bar" />
1265
+ <meta property="og:image:height" content="100" />
1266
+ <div>hello world</div>
1267
+ </body>
1268
+ </html>,
1269
+ );
1270
+ expect(Scheduler).toFlushWithoutYielding();
1271
+ expect(getMeaningfulChildren(document)).toEqual(
1272
+ <html>
1273
+ <head>
1274
+ <meta property="og:image" content="foo" />
1275
+ <meta property="og:image:width" content="100" />
1276
+ <meta property="og:image:height" content="100" />
1277
+ <meta property="og:image" content="bar" />
1278
+ <meta property="og:image:height" content="100" />
1279
+ </head>
1280
+ <body>
1281
+ <div>hello world</div>
1282
+ </body>
1283
+ </html>,
1284
+ );
1285
+
1286
+ root.render(
1287
+ <html>
1288
+ <head />
1289
+ <body>
1290
+ <meta property="og:image" content="foo" />
1291
+ <meta property="og:image:width" content="100" />
1292
+ <meta property="og:image:height" content="100" />
1293
+ <meta property="og:image:foo" content="foo" />
1294
+ <meta property="og:image" content="bar" />
1295
+ <meta property="og:image:height" content="100" />
1296
+ <div>hello world</div>
1297
+ </body>
1298
+ </html>,
1299
+ );
1300
+ expect(Scheduler).toFlushWithoutYielding();
1301
+ expect(getMeaningfulChildren(document)).toEqual(
1302
+ <html>
1303
+ <head>
1304
+ <meta property="og:image" content="foo" />
1305
+ <meta property="og:image:foo" content="foo" />
1306
+ <meta property="og:image:width" content="100" />
1307
+ <meta property="og:image:height" content="100" />
1308
+ <meta property="og:image" content="bar" />
1309
+ <meta property="og:image:height" content="100" />
1310
+ </head>
1311
+ <body>
1312
+ <div>hello world</div>
1313
+ </body>
1314
+ </html>,
1315
+ );
1316
+
1317
+ root.render(
1318
+ <html>
1319
+ <head />
1320
+ <body>
1321
+ <meta property="og:image" content="foo" />
1322
+ <meta property="og:image:width" content="100" />
1323
+ <meta property="og:image:width:bar" content="bar" />
1324
+ <meta property="og:image:height" content="100" />
1325
+ <meta property="og:image:foo" content="foo" />
1326
+ <meta property="og:image" content="bar" />
1327
+ <meta property="og:image:height" content="100" />
1328
+ <div>hello world</div>
1329
+ </body>
1330
+ </html>,
1331
+ );
1332
+ expect(Scheduler).toFlushWithoutYielding();
1333
+ expect(getMeaningfulChildren(document)).toEqual(
1334
+ <html>
1335
+ <head>
1336
+ <meta property="og:image" content="foo" />
1337
+ <meta property="og:image:foo" content="foo" />
1338
+ <meta property="og:image:height" content="100" />
1339
+ <meta property="og:image:width" content="100" />
1340
+ <meta property="og:image:width:bar" content="bar" />
1341
+ <meta property="og:image" content="bar" />
1342
+ <meta property="og:image:height" content="100" />
1343
+ </head>
1344
+ <body>
1345
+ <div>hello world</div>
1346
+ </body>
1347
+ </html>,
1348
+ );
1349
+ });
1350
+
1351
+ // @gate enableFloat
1352
+ it('can render meta tags as resources', async () => {
1353
+ await actIntoEmptyDocument(() => {
1354
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
1355
+ <>
1356
+ <html>
1357
+ <head />
1358
+ <body>
1359
+ <div>hello world</div>
1360
+ </body>
1361
+ </html>
1362
+ <meta name="robots" content="noindex" />
1363
+ <meta httpEquiv="content-security-policy" content="foo" />
1364
+ <meta itemProp="description" content="desc" />
1365
+ <meta property="description" content="desc2" />
1366
+ <meta charSet="utf-8" />
1367
+ </>,
1368
+ );
1369
+ pipe(writable);
1370
+ });
1371
+ expect(getMeaningfulChildren(document)).toEqual(
1372
+ <html>
1373
+ <head>
1374
+ <meta charset="utf-8" />
1375
+ <meta name="robots" content="noindex" />
1376
+ <meta http-equiv="content-security-policy" content="foo" />
1377
+ <meta itemprop="description" content="desc" />
1378
+ <meta property="description" content="desc2" />
1379
+ </head>
1380
+ <body>
1381
+ <div>hello world</div>
1382
+ </body>
1383
+ </html>,
1384
+ );
1385
+
1386
+ ReactDOMClient.hydrateRoot(
1387
+ document,
1388
+ <html>
1389
+ <head>
1390
+ <meta charSet="utf-8" />
1391
+ </head>
1392
+ <body>
1393
+ <meta name="robots" content="noindex" />
1394
+ <meta httpEquiv="content-security-policy" content="foo" />
1395
+ <div>hello world</div>
1396
+ </body>
1397
+ </html>,
1398
+ );
1399
+ expect(Scheduler).toFlushWithoutYielding();
1400
+ expect(getMeaningfulChildren(document)).toEqual(
1401
+ <html>
1402
+ <head>
1403
+ <meta charset="utf-8" />
1404
+ <meta name="robots" content="noindex" />
1405
+ <meta http-equiv="content-security-policy" content="foo" />
1406
+ <meta itemprop="description" content="desc" />
1407
+ <meta property="description" content="desc2" />
1408
+ </head>
1409
+ <body>
1410
+ <div>hello world</div>
1411
+ </body>
1412
+ </html>,
1413
+ );
1414
+ });
1415
+
1416
// @gate enableFloat
1417
it('can rendering title tags anywhere in the tree', async () => {
1418
await actIntoEmptyDocument(() => {
packages/react-dom/src/__tests__/ReactDOMServerIntegrationAttributes-test.js
+3
-3
@@ -497,9 +497,9 @@ describe('ReactDOMServerIntegration', () => {
497
itRenders(
498
'badly cased aliased HTML attribute with a warning',
499
async render => {
500
- const e = await render(<meta httpequiv="refresh" />, 1);
501
- expect(e.hasAttribute('http-equiv')).toBe(false);
502
- expect(e.getAttribute('httpequiv')).toBe('refresh');
500
+ const e = await render(<form acceptcharset="utf-8" />, 1);
501
+ expect(e.hasAttribute('accept-charset')).toBe(false);
502
+ expect(e.getAttribute('acceptcharset')).toBe('utf-8');
503
},
504
);
505
packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js
+7
-8
@@ -287,10 +287,10 @@ describe('ReactDOM HostSingleton', () => {
287
expect(getVisibleChildren(document)).toEqual(
288
<html data-client-foo="foo">
289
<head>
290
- <title>a client title</title>
290
<link rel="stylesheet" href="resource" />
291
<link rel="stylesheet" href="3rdparty" />
292
<link rel="stylesheet" href="3rdparty2" />
293
+ <title>a client title</title>
294
</head>
295
<body data-client-baz="baz">
296
<style>
@@ -326,11 +326,10 @@ describe('ReactDOM HostSingleton', () => {
326
expect(getVisibleChildren(document)).toEqual(
327
<html data-client-foo="foo">
328
<head>
329
- <title>a client title</title>
329
<link rel="stylesheet" href="resource" />
330
<link rel="stylesheet" href="3rdparty" />
331
<link rel="stylesheet" href="3rdparty2" />
333
- <meta />
332
+ <title>a client title</title>
333
</head>
334
<body data-client-baz="baz">
335
<style>
@@ -365,10 +364,10 @@ describe('ReactDOM HostSingleton', () => {
364
expect(getVisibleChildren(document)).toEqual(
365
<html data-client-foo="foo">
366
<head>
368
- <title>a client title</title>
367
<link rel="stylesheet" href="resource" />
368
<link rel="stylesheet" href="3rdparty" />
369
<link rel="stylesheet" href="3rdparty2" />
370
+ <title>a client title</title>
371
</head>
372
<body data-client-baz="baz">
373
<style>
@@ -401,10 +400,10 @@ describe('ReactDOM HostSingleton', () => {
400
expect(getVisibleChildren(document)).toEqual(
401
<html data-client-foo="foo">
402
<head>
404
- <title>a client title</title>
403
<link rel="stylesheet" href="resource" />
404
<link rel="stylesheet" href="3rdparty" />
405
<link rel="stylesheet" href="3rdparty2" />
406
+ <title>a client title</title>
407
</head>
408
<body>
409
<style>
@@ -498,10 +497,10 @@ describe('ReactDOM HostSingleton', () => {
497
expect(getVisibleChildren(document)).toEqual(
498
<html data-client-foo="foo">
499
<head>
501
- <title>a client title</title>
500
<link rel="stylesheet" href="resource" />
501
<link rel="stylesheet" href="3rdparty" />
502
<link rel="stylesheet" href="3rdparty2" />
503
+ <title>a client title</title>
504
</head>
505
<body data-client-baz="baz">
506
<style>
@@ -764,9 +763,9 @@ describe('ReactDOM HostSingleton', () => {
763
expect(getVisibleChildren(document)).toEqual(
764
<html>
765
<head>
767
- <title>something new</title>
766
<link rel="stylesheet" href="headbefore" />
767
<link rel="stylesheet" href="headafter" />
768
+ <title>something new</title>
769
</head>
770
<body>
771
<link rel="stylesheet" href="bodybefore" />
@@ -800,9 +799,9 @@ describe('ReactDOM HostSingleton', () => {
799
expect(getVisibleChildren(document)).toEqual(
800
<html>
801
<head>
803
- <title>something new</title>
802
<link rel="stylesheet" href="before" />
803
<link rel="stylesheet" href="after" />
804
+ <title>something new</title>
805
</head>
806
<body />
807
</html>,
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+3
-1
@@ -2057,7 +2057,9 @@ function commitDeletionEffectsOnFiber(
2057
nearestMountedAncestor,
2058
deletedFiber,
2059
);
2060
- releaseResource(deletedFiber.memoizedState);
2060
+ if (deletedFiber.memoizedState) {
2061
+ releaseResource(deletedFiber.memoizedState);
2062
+ }
2063
return;
2064
}
2065
}
packages/react-reconciler/src/ReactFiberCommitWork.old.js
+3
-1
@@ -2057,7 +2057,9 @@ function commitDeletionEffectsOnFiber(
2057
nearestMountedAncestor,
2058
deletedFiber,
2059
);
2060
- releaseResource(deletedFiber.memoizedState);
2060
+ if (deletedFiber.memoizedState) {
2061
+ releaseResource(deletedFiber.memoizedState);
2062
+ }
2063
return;
2064
}
2065
}
scripts/error-codes/codes.json
+2
-1
@@ -441,5 +441,6 @@
441
"453": "React expected a <head> element (document.head) to exist in the Document but one was not found. React never removes the head for any Document it renders into so the cause is likely in some other script running on this page.",
442
"454": "React expected a <body> element (document.body) to exist in the Document but one was not found. React never removes the body for any Document it renders into so the cause is likely in some other script running on this page.",
443
"455": "This CacheSignal was requested outside React which means that it is immediately aborted.",
444
- "456": "Calling Offscreen.detach before instance handle has been set."
444
+ "456": "Calling Offscreen.detach before instance handle has been set.",
445
+ "457": "acquireHeadResource encountered a resource type it did not expect: \"%s\". This is a bug in React."
446
}