Skip to content

Commit d1ced9f

Browse files
authored
[Float] support all links as Resources (#25515)
stacked on #25514 This PR adds support for any type of Link as long as it has a string rel and href and does not include an onLoad or onError property. The semantics for generic link resources matches other head resources, they will be inserted and removed as their ref counts go positive and back to zero. Keys are based on rel, href, sizes, and media. on the server preconnect and prefetch-dns are privileged and will emit near the start of the stream.
1 parent 6dbccb9 commit d1ced9f

File tree

5 files changed

+371
-50
lines changed

5 files changed

+371
-50
lines changed

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

+88-15
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals.js';
1313
const {Dispatcher} = ReactDOMSharedInternals;
1414
import {DOCUMENT_NODE} from '../shared/HTMLNodeType';
1515
import {
16-
validateUnmatchedLinkResourceProps,
16+
warnOnMissingHrefAndRel,
1717
validatePreloadResourceDifference,
1818
validateURLKeyedUpdatedProps,
1919
validateStyleResourceDifference,
@@ -54,7 +54,7 @@ type StyleProps = {
5454
'data-precedence': string,
5555
[string]: mixed,
5656
};
57-
export type StyleResource = {
57+
type StyleResource = {
5858
type: 'style',
5959

6060
// Ref count for resource
@@ -79,7 +79,7 @@ type ScriptProps = {
7979
src: string,
8080
[string]: mixed,
8181
};
82-
export type ScriptResource = {
82+
type ScriptResource = {
8383
type: 'script',
8484
src: string,
8585
props: ScriptProps,
@@ -88,12 +88,10 @@ export type ScriptResource = {
8888
root: FloatRoot,
8989
};
9090

91-
export type HeadResource = TitleResource | MetaResource;
92-
9391
type TitleProps = {
9492
[string]: mixed,
9593
};
96-
export type TitleResource = {
94+
type TitleResource = {
9795
type: 'title',
9896
props: TitleProps,
9997

@@ -105,7 +103,7 @@ export type TitleResource = {
105103
type MetaProps = {
106104
[string]: mixed,
107105
};
108-
export type MetaResource = {
106+
type MetaResource = {
109107
type: 'meta',
110108
matcher: string,
111109
property: ?string,
@@ -117,8 +115,23 @@ export type MetaResource = {
117115
root: Document,
118116
};
119117

118+
type LinkProps = {
119+
href: string,
120+
rel: string,
121+
[string]: mixed,
122+
};
123+
type LinkResource = {
124+
type: 'link',
125+
props: LinkProps,
126+
127+
count: number,
128+
instance: ?Element,
129+
root: Document,
130+
};
131+
120132
type Props = {[string]: mixed};
121133

134+
type HeadResource = TitleResource | MetaResource | LinkResource;
122135
type Resource = StyleResource | ScriptResource | PreloadResource | HeadResource;
123136

124137
export type RootResources = {
@@ -617,8 +630,30 @@ export function getResource(
617630
return null;
618631
}
619632
default: {
633+
const {href, sizes, media} = pendingProps;
634+
if (typeof rel === 'string' && typeof href === 'string') {
635+
const sizeKey =
636+
'::sizes:' + (typeof sizes === 'string' ? sizes : '');
637+
const mediaKey =
638+
'::media:' + (typeof media === 'string' ? media : '');
639+
const key = 'rel:' + rel + '::href:' + href + sizeKey + mediaKey;
640+
const headRoot = getDocumentFromRoot(resourceRoot);
641+
const headResources = getResourcesFromRoot(headRoot).head;
642+
let resource = headResources.get(key);
643+
if (!resource) {
644+
resource = {
645+
type: 'link',
646+
props: Object.assign({}, pendingProps),
647+
count: 0,
648+
instance: null,
649+
root: headRoot,
650+
};
651+
headResources.set(key, resource);
652+
}
653+
return resource;
654+
}
620655
if (__DEV__) {
621-
validateUnmatchedLinkResourceProps(pendingProps, currentProps);
656+
warnOnMissingHrefAndRel(pendingProps, currentProps);
622657
}
623658
return null;
624659
}
@@ -710,6 +745,7 @@ function scriptPropsFromRawProps(rawProps: ScriptQualifyingProps): ScriptProps {
710745
export function acquireResource(resource: Resource): Instance {
711746
switch (resource.type) {
712747
case 'title':
748+
case 'link':
713749
case 'meta': {
714750
return acquireHeadResource(resource);
715751
}
@@ -732,6 +768,7 @@ export function acquireResource(resource: Resource): Instance {
732768

733769
export function releaseResource(resource: Resource): void {
734770
switch (resource.type) {
771+
case 'link':
735772
case 'title':
736773
case 'meta': {
737774
return releaseHeadResource(resource);
@@ -1050,6 +1087,41 @@ function acquireHeadResource(resource: HeadResource): Instance {
10501087
insertResourceInstanceBefore(root, instance, insertBefore);
10511088
break;
10521089
}
1090+
case 'link': {
1091+
const linkProps: LinkProps = (props: any);
1092+
const limitedEscapedRel = escapeSelectorAttributeValueInsideDoubleQuotes(
1093+
linkProps.rel,
1094+
);
1095+
const limitedEscapedHref = escapeSelectorAttributeValueInsideDoubleQuotes(
1096+
linkProps.href,
1097+
);
1098+
let selector = `link[rel="${limitedEscapedRel}"][href="${limitedEscapedHref}"]`;
1099+
if (typeof linkProps.sizes === 'string') {
1100+
const limitedEscapedSizes = escapeSelectorAttributeValueInsideDoubleQuotes(
1101+
linkProps.sizes,
1102+
);
1103+
selector += `[sizes="${limitedEscapedSizes}"]`;
1104+
}
1105+
if (typeof linkProps.media === 'string') {
1106+
const limitedEscapedMedia = escapeSelectorAttributeValueInsideDoubleQuotes(
1107+
linkProps.media,
1108+
);
1109+
selector += `[media="${limitedEscapedMedia}"]`;
1110+
}
1111+
const existingEl = root.querySelector(selector);
1112+
if (existingEl) {
1113+
instance = resource.instance = existingEl;
1114+
markNodeAsResource(instance);
1115+
return instance;
1116+
}
1117+
instance = resource.instance = createResourceInstance(
1118+
type,
1119+
props,
1120+
root,
1121+
);
1122+
insertResourceInstanceBefore(root, instance, null);
1123+
return instance;
1124+
}
10531125
default: {
10541126
throw new Error(
10551127
`acquireHeadResource encountered a resource type it did not expect: "${type}". This is a bug in React.`,
@@ -1265,26 +1337,27 @@ export function isHostResourceType(type: string, props: Props): boolean {
12651337
return true;
12661338
}
12671339
case 'link': {
1340+
const {onLoad, onError} = props;
1341+
if (onLoad || onError) {
1342+
return false;
1343+
}
12681344
switch (props.rel) {
12691345
case 'stylesheet': {
12701346
if (__DEV__) {
12711347
validateLinkPropsForStyleResource(props);
12721348
}
1273-
const {href, precedence, onLoad, onError, disabled} = props;
1349+
const {href, precedence, disabled} = props;
12741350
return (
12751351
typeof href === 'string' &&
12761352
typeof precedence === 'string' &&
1277-
!onLoad &&
1278-
!onError &&
12791353
disabled == null
12801354
);
12811355
}
1282-
case 'preload': {
1283-
const {href, onLoad, onError} = props;
1284-
return !onLoad && !onError && typeof href === 'string';
1356+
default: {
1357+
const {rel, href} = props;
1358+
return typeof href === 'string' && typeof rel === 'string';
12851359
}
12861360
}
1287-
return false;
12881361
}
12891362
case 'script': {
12901363
// We don't validate because it is valid to use async with onLoad/onError unlike combining

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

+46-4
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,20 @@ type MetaResource = {
8989
flushed: boolean,
9090
};
9191

92+
type LinkProps = {
93+
href: string,
94+
rel: string,
95+
[string]: mixed,
96+
};
97+
type LinkResource = {
98+
type: 'link',
99+
props: LinkProps,
100+
101+
flushed: boolean,
102+
};
103+
92104
export type Resource = PreloadResource | StyleResource | ScriptResource;
93-
export type HeadResource = TitleResource | MetaResource;
105+
export type HeadResource = TitleResource | MetaResource | LinkResource;
94106

95107
export type Resources = {
96108
// Request local cache
@@ -101,6 +113,7 @@ export type Resources = {
101113

102114
// Flushing queues for Resource dependencies
103115
charset: null | MetaResource,
116+
preconnects: Set<LinkResource>,
104117
fontPreloads: Set<PreloadResource>,
105118
// usedImagePreloads: Set<PreloadResource>,
106119
precedences: Map<string, Set<StyleResource>>,
@@ -131,6 +144,7 @@ export function createResources(): Resources {
131144

132145
// cleared on flush
133146
charset: null,
147+
preconnects: new Set(),
134148
fontPreloads: new Set(),
135149
// usedImagePreloads: new Set(),
136150
precedences: new Map(),
@@ -697,10 +711,11 @@ export function resourcesFromLink(props: Props): boolean {
697711
const resources = currentResources;
698712

699713
const {rel, href} = props;
700-
if (!href || typeof href !== 'string') {
714+
if (!href || typeof href !== 'string' || !rel || typeof rel !== 'string') {
701715
return false;
702716
}
703717

718+
let key = '';
704719
switch (rel) {
705720
case 'stylesheet': {
706721
const {onLoad, onError, precedence, disabled} = props;
@@ -813,10 +828,37 @@ export function resourcesFromLink(props: Props): boolean {
813828
return true;
814829
}
815830
}
816-
return false;
831+
break;
817832
}
818833
}
819-
return false;
834+
if (props.onLoad || props.onError) {
835+
return false;
836+
}
837+
838+
const sizes = typeof props.sizes === 'string' ? props.sizes : '';
839+
const media = typeof props.media === 'string' ? props.media : '';
840+
key =
841+
'rel:' + rel + '::href:' + href + '::sizes:' + sizes + '::media:' + media;
842+
let resource = resources.headsMap.get(key);
843+
if (!resource) {
844+
resource = {
845+
type: 'link',
846+
props: Object.assign({}, props),
847+
flushed: false,
848+
};
849+
resources.headsMap.set(key, resource);
850+
switch (rel) {
851+
case 'preconnect':
852+
case 'dns-prefetch': {
853+
resources.preconnects.add(resource);
854+
break;
855+
}
856+
default: {
857+
resources.headResources.add(resource);
858+
}
859+
}
860+
}
861+
return true;
820862
}
821863

822864
// Construct a resource from link props.

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

+24
Original file line numberDiff line numberDiff line change
@@ -2340,6 +2340,7 @@ export function writeInitialResources(
23402340

23412341
const {
23422342
charset,
2343+
preconnects,
23432344
fontPreloads,
23442345
precedences,
23452346
usedStylePreloads,
@@ -2356,6 +2357,13 @@ export function writeInitialResources(
23562357
resources.charset = null;
23572358
}
23582359

2360+
preconnects.forEach(r => {
2361+
// font preload Resources should not already be flushed so we elide this check
2362+
pushLinkImpl(target, r.props, responseState);
2363+
r.flushed = true;
2364+
});
2365+
preconnects.clear();
2366+
23592367
fontPreloads.forEach(r => {
23602368
// font preload Resources should not already be flushed so we elide this check
23612369
pushLinkImpl(target, r.props, responseState);
@@ -2418,6 +2426,10 @@ export function writeInitialResources(
24182426
pushSelfClosing(target, r.props, 'meta', responseState);
24192427
break;
24202428
}
2429+
case 'link': {
2430+
pushLinkImpl(target, r.props, responseState);
2431+
break;
2432+
}
24212433
}
24222434
r.flushed = true;
24232435
});
@@ -2450,6 +2462,7 @@ export function writeImmediateResources(
24502462

24512463
const {
24522464
charset,
2465+
preconnects,
24532466
fontPreloads,
24542467
usedStylePreloads,
24552468
scripts,
@@ -2465,6 +2478,13 @@ export function writeImmediateResources(
24652478
resources.charset = null;
24662479
}
24672480

2481+
preconnects.forEach(r => {
2482+
// font preload Resources should not already be flushed so we elide this check
2483+
pushLinkImpl(target, r.props, responseState);
2484+
r.flushed = true;
2485+
});
2486+
preconnects.clear();
2487+
24682488
fontPreloads.forEach(r => {
24692489
// font preload Resources should not already be flushed so we elide this check
24702490
pushLinkImpl(target, r.props, responseState);
@@ -2507,6 +2527,10 @@ export function writeImmediateResources(
25072527
pushSelfClosing(target, r.props, 'meta', responseState);
25082528
break;
25092529
}
2530+
case 'link': {
2531+
pushLinkImpl(target, r.props, responseState);
2532+
break;
2533+
}
25102534
}
25112535
r.flushed = true;
25122536
});

0 commit comments

Comments
 (0)