Skip to content

Commit cb17a1b

Browse files
committed
og, thanks I hate it
1 parent 8b742a8 commit cb17a1b

File tree

4 files changed

+443
-137
lines changed

4 files changed

+443
-137
lines changed

packages/react-dom-bindings/src/client/ReactDOMComponentTree.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ export function getResourcesFromRoot(root: FloatRoot): RootResources {
286286
styles: new Map(),
287287
scripts: new Map(),
288288
head: new Map(),
289+
lastStructuredMeta: new Map(),
289290
};
290291
}
291292
return resources;

packages/react-dom-bindings/src/client/ReactDOMFloatClient.js

Lines changed: 118 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,10 @@ type MetaProps = {
107107
};
108108
export type MetaResource = {
109109
type: 'meta',
110-
matcher: string,
110+
key: string,
111+
matchValue: string,
112+
matchAttr: 'charset' | 'http-equiv' | 'name' | 'itemprop' | 'property',
113+
parentResource: ?MetaResource,
111114
props: MetaProps,
112115

113116
count: number,
@@ -123,6 +126,7 @@ export type RootResources = {
123126
styles: Map<string, StyleResource>,
124127
scripts: Map<string, ScriptResource>,
125128
head: Map<string, HeadResource>,
129+
lastStructuredMeta: Map<string, MetaResource>,
126130
};
127131

128132
// Brief on purpose due to insertion by script when streaming late boundaries
@@ -424,41 +428,70 @@ export function getResource(
424428
}
425429
switch (type) {
426430
case 'meta': {
427-
let matcher;
428-
if (typeof pendingProps.charSet === 'string') {
429-
// We don't support charSet metas on the client since they are for describing the encoding
430-
// of the streamed in html and don't make sense to set from the client
431-
} else if (typeof pendingProps.httpEquiv === 'string') {
432-
matcher = `[http-equiv="${escapeSelectorAttributeValueInsideDoubleQuotes(
433-
pendingProps.httpEquiv,
434-
)}"]`;
435-
} else if (typeof pendingProps.name === 'string') {
436-
matcher = `[name="${escapeSelectorAttributeValueInsideDoubleQuotes(
437-
pendingProps.name,
438-
)}"]`;
439-
} else if (typeof pendingProps.itemProp === 'string') {
440-
matcher = `[itemprop="${escapeSelectorAttributeValueInsideDoubleQuotes(
441-
pendingProps.itemProp,
442-
)}"]`;
443-
} else if (typeof pendingProps.property === 'string') {
444-
matcher = `[property="${escapeSelectorAttributeValueInsideDoubleQuotes(
445-
pendingProps.property,
446-
)}"]`;
431+
let key, matchAttr, matchValue, propertyPath, parentResource;
432+
const {
433+
charSet,
434+
content,
435+
httpEquiv,
436+
name,
437+
itemProp,
438+
property,
439+
} = pendingProps;
440+
const headRoot: Document = getDocumentFromRoot(resourceRoot);
441+
const {head: headResources, lastStructuredMeta} = getResourcesFromRoot(
442+
headRoot,
443+
);
444+
if (typeof charSet === 'string') {
445+
matchValue = charSet;
446+
key = matchAttr = 'charset';
447+
} else if (typeof content === 'string') {
448+
const contentKey = '::' + content;
449+
if (typeof httpEquiv === 'string') {
450+
matchValue = httpEquiv;
451+
matchAttr = 'http-equiv';
452+
key = matchAttr + '::' + httpEquiv + contentKey;
453+
} else if (typeof property === 'string') {
454+
matchValue = property;
455+
matchAttr = 'property';
456+
key = matchAttr + '::' + property + contentKey;
457+
propertyPath = property;
458+
const parentPropertyPath = property
459+
.split(':')
460+
.slice(0, -1)
461+
.join(':');
462+
parentResource = lastStructuredMeta.get(parentPropertyPath);
463+
if (parentResource) {
464+
key = parentResource.key + '::child::' + key;
465+
}
466+
} else if (typeof name === 'string') {
467+
matchValue = name;
468+
matchAttr = 'name';
469+
key = matchAttr + '::' + name + contentKey;
470+
} else if (typeof itemProp === 'string') {
471+
matchValue = itemProp;
472+
matchAttr = 'itemprop';
473+
key = matchAttr + '::' + itemProp + contentKey;
474+
}
447475
}
448-
if (matcher) {
449-
const headRoot: Document = getDocumentFromRoot(resourceRoot);
450-
const headResources = getResourcesFromRoot(headRoot).head;
451-
let resource = headResources.get(matcher);
476+
if (key && matchAttr) {
477+
let resource = headResources.get(key);
452478
if (!resource) {
453479
resource = {
454480
type: 'meta',
455-
matcher,
481+
matchValue: ((matchValue: any): string),
482+
matchAttr,
483+
key,
484+
parentResource,
456485
props: Object.assign({}, pendingProps),
457486
count: 0,
458487
instance: null,
459488
root: headRoot,
460489
};
461-
headResources.set(matcher, resource);
490+
headResources.set(key, resource);
491+
}
492+
if (propertyPath) {
493+
// We cast because flow doesn't know that this resource must be a Meta resource
494+
lastStructuredMeta.set(propertyPath, (resource: any));
462495
}
463496
return resource;
464497
}
@@ -960,22 +993,64 @@ function acquireHeadResource(resource: HeadResource): Instance {
960993
break;
961994
}
962995
case 'meta': {
963-
instance = resource.instance = createResourceInstance(
964-
type,
965-
props,
966-
root,
967-
);
996+
let existingEl = null;
997+
let insertBefore = null;
998+
968999
const metaResource: MetaResource = (resource: any);
969-
const metas = root.querySelectorAll('meta' + metaResource.matcher);
970-
// For keymatched metas we simply remove them all because this new meta
971-
// will supercede it. It's possibly, likely even that they may match props
972-
// and another approach is to check if the props are equal and just use the node
973-
// however for simplicity we will purge any server rendered metas before
974-
// constructing a new one here on the client
975-
insertResourceInstanceBefore(root, instance, metas.item(0));
976-
for (let i = 0; i < metas.length; i++) {
977-
const meta = metas[i];
978-
(meta.parentNode: any).removeChild(meta);
1000+
const {matchAttr, matchValue, parentResource} = metaResource;
1001+
1002+
if (matchAttr === 'charset') {
1003+
existingEl = root.querySelector('meta[charset]');
1004+
} else {
1005+
let scope: Document | Element = root;
1006+
let parent = null;
1007+
let parentProperty = null;
1008+
if (matchAttr === 'property' && parentResource) {
1009+
const parentInstance = parentResource.instance;
1010+
if (parentInstance && parentInstance.parentElement) {
1011+
parent = parentInstance;
1012+
insertBefore = parent.nextSibling;
1013+
parentProperty = parentResource.matchValue;
1014+
scope = parentInstance.parentElement;
1015+
}
1016+
}
1017+
const metas = scope.querySelectorAll('meta');
1018+
for (let i = 0; i < metas.length; i++) {
1019+
const meta = metas[i];
1020+
if (parent) {
1021+
if (meta === parent) {
1022+
parent = null;
1023+
}
1024+
continue;
1025+
} else if (
1026+
meta.getAttribute('content') === props.content &&
1027+
meta.getAttribute(matchAttr) === matchValue
1028+
) {
1029+
existingEl = meta;
1030+
break;
1031+
} else {
1032+
const metaProperty = meta.getAttribute('property');
1033+
if (
1034+
parentProperty &&
1035+
metaProperty &&
1036+
parentProperty.startsWith(metaProperty)
1037+
) {
1038+
// We have found aother matching parent meta and need to stop
1039+
break;
1040+
}
1041+
}
1042+
}
1043+
}
1044+
if (existingEl) {
1045+
instance = resource.instance = existingEl;
1046+
markNodeAsResource(instance);
1047+
} else {
1048+
instance = resource.instance = createResourceInstance(
1049+
type,
1050+
props,
1051+
root,
1052+
);
1053+
insertResourceInstanceBefore(root, instance, insertBefore);
9791054
}
9801055
break;
9811056
}
@@ -1166,7 +1241,7 @@ function insertStyleInstance(
11661241
function insertResourceInstanceBefore(
11671242
ownerDocument: Document,
11681243
instance: Instance,
1169-
before: null | Node,
1244+
before: ?Node,
11701245
): void {
11711246
if (__DEV__) {
11721247
if (instance.tagName === 'LINK' && (instance: any).rel === 'stylesheet') {

packages/react-dom-bindings/src/server/ReactDOMFloatServer.js

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ type MetaProps = {
8383
};
8484
type MetaResource = {
8585
type: 'meta',
86+
key: string,
8687
props: MetaProps,
8788

8889
flushed: boolean,
@@ -111,6 +112,9 @@ export type Resources = {
111112
explicitScriptPreloads: Set<PreloadResource>,
112113
headResources: Set<HeadResource>,
113114

115+
// cache for tracking structured meta tags
116+
structuredMetaKeys: Map<string, MetaResource>,
117+
114118
// Module-global-like reference for current boundary resources
115119
boundaryResources: ?BoundaryResources,
116120
...
@@ -138,6 +142,9 @@ export function createResources(): Resources {
138142
explicitScriptPreloads: new Set(),
139143
headResources: new Set(),
140144

145+
// cache for tracking structured meta tags
146+
structuredMetaKeys: new Map(),
147+
141148
// like a module global for currently rendering boundary
142149
boundaryResources: null,
143150
};
@@ -590,25 +597,6 @@ function adoptPreloadPropsForScriptProps(
590597
resourceProps.integrity = preloadProps.integrity;
591598
}
592599

593-
function getTitleKey(child: string | number): string {
594-
return 'title' + child;
595-
}
596-
597-
function getMetaKey(props: Props): ?string {
598-
if (typeof props.charSet === 'string') {
599-
return 'charSet';
600-
} else if (typeof props.httpEquiv === 'string') {
601-
return 'httpEquiv::' + props.httpEquiv;
602-
} else if (typeof props.name === 'string') {
603-
return 'name::' + props.name;
604-
} else if (typeof props.itemProp === 'string') {
605-
return 'itemProp::' + props.itemProp;
606-
} else if (typeof props.property === 'string') {
607-
return 'property::' + props.property;
608-
}
609-
return null;
610-
}
611-
612600
function titlePropsFromRawProps(
613601
child: string | number,
614602
rawProps: Props,
@@ -632,7 +620,7 @@ export function resourcesFromElement(type: string, props: Props): boolean {
632620
child = child[0];
633621
}
634622
if (typeof child === 'string' || typeof child === 'number') {
635-
const key = getTitleKey(child);
623+
const key = 'title::' + child;
636624
let resource = resources.headsMap.get(key);
637625
if (!resource) {
638626
resource = {
@@ -648,18 +636,46 @@ export function resourcesFromElement(type: string, props: Props): boolean {
648636
return false;
649637
}
650638
case 'meta': {
651-
const key = getMetaKey(props);
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+
}
652664
if (key) {
653665
if (!resources.headsMap.has(key)) {
654666
const resource = {
655667
type: 'meta',
668+
key,
656669
props: Object.assign({}, props),
657670
flushed: false,
658671
};
659672
resources.headsMap.set(key, resource);
660673
if (key === 'charSet') {
661674
resources.charset = resource;
662675
} else {
676+
if (propertyPath) {
677+
resources.structuredMetaKeys.set(propertyPath, resource);
678+
}
663679
resources.headResources.add(resource);
664680
}
665681
}

0 commit comments

Comments
 (0)