Skip to content

Commit 989bda8

Browse files
committed
support all link types as resources
1 parent cc45100 commit 989bda8

File tree

5 files changed

+243
-71
lines changed

5 files changed

+243
-71
lines changed

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

Lines changed: 34 additions & 25 deletions
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,
@@ -629,13 +629,16 @@ export function getResource(
629629
}
630630
return null;
631631
}
632-
case 'icon':
633-
case 'apple-touch-icon': {
634-
const {href} = pendingProps;
635-
if (typeof href === 'string') {
636-
const key = rel + href;
632+
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;
637640
const headRoot = getDocumentFromRoot(resourceRoot);
638-
const headResources = getResourcesFromRoot(resourceRoot).head;
641+
const headResources = getResourcesFromRoot(headRoot).head;
639642
let resource = headResources.get(key);
640643
if (!resource) {
641644
resource = {
@@ -649,11 +652,8 @@ export function getResource(
649652
}
650653
return resource;
651654
}
652-
return null;
653-
}
654-
default: {
655655
if (__DEV__) {
656-
validateUnmatchedLinkResourceProps(pendingProps, currentProps);
656+
warnOnMissingHrefAndRel(pendingProps, currentProps);
657657
}
658658
return null;
659659
}
@@ -768,6 +768,7 @@ export function acquireResource(resource: Resource): Instance {
768768

769769
export function releaseResource(resource: Resource): void {
770770
switch (resource.type) {
771+
case 'link':
771772
case 'title':
772773
case 'meta': {
773774
return releaseHeadResource(resource);
@@ -1094,9 +1095,20 @@ function acquireHeadResource(resource: HeadResource): Instance {
10941095
const limitedEscapedHref = escapeSelectorAttributeValueInsideDoubleQuotes(
10951096
linkProps.href,
10961097
);
1097-
const existingEl = root.querySelector(
1098-
`link[rel="${limitedEscapedRel}"][href="${limitedEscapedHref}"]`,
1099-
);
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);
11001112
if (existingEl) {
11011113
instance = resource.instance = existingEl;
11021114
markNodeAsResource(instance);
@@ -1325,30 +1337,27 @@ export function isHostResourceType(type: string, props: Props): boolean {
13251337
return true;
13261338
}
13271339
case 'link': {
1340+
const {onLoad, onError} = props;
1341+
if (onLoad || onError) {
1342+
return false;
1343+
}
13281344
switch (props.rel) {
13291345
case 'stylesheet': {
13301346
if (__DEV__) {
13311347
validateLinkPropsForStyleResource(props);
13321348
}
1333-
const {href, precedence, onLoad, onError, disabled} = props;
1349+
const {href, precedence, disabled} = props;
13341350
return (
13351351
typeof href === 'string' &&
13361352
typeof precedence === 'string' &&
1337-
!onLoad &&
1338-
!onError &&
13391353
disabled == null
13401354
);
13411355
}
1342-
case 'preload': {
1343-
const {href, onLoad, onError} = props;
1344-
return !onLoad && !onError && typeof href === 'string';
1345-
}
1346-
case 'icon':
1347-
case 'apple-touch-icon': {
1348-
return true;
1356+
default: {
1357+
const {rel, href} = props;
1358+
return typeof href === 'string' && typeof rel === 'string';
13491359
}
13501360
}
1351-
return false;
13521361
}
13531362
case 'script': {
13541363
// We don't validate because it is valid to use async with onLoad/onError unlike combining

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

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export type Resources = {
113113

114114
// Flushing queues for Resource dependencies
115115
charset: null | MetaResource,
116+
preconnects: Set<LinkResource>,
116117
fontPreloads: Set<PreloadResource>,
117118
// usedImagePreloads: Set<PreloadResource>,
118119
precedences: Map<string, Set<StyleResource>>,
@@ -143,6 +144,7 @@ export function createResources(): Resources {
143144

144145
// cleared on flush
145146
charset: null,
147+
preconnects: new Set(),
146148
fontPreloads: new Set(),
147149
// usedImagePreloads: new Set(),
148150
precedences: new Map(),
@@ -709,10 +711,11 @@ export function resourcesFromLink(props: Props): boolean {
709711
const resources = currentResources;
710712

711713
const {rel, href} = props;
712-
if (!href || typeof href !== 'string') {
714+
if (!href || typeof href !== 'string' || !rel || typeof rel !== 'string') {
713715
return false;
714716
}
715717

718+
let key = '';
716719
switch (rel) {
717720
case 'stylesheet': {
718721
const {onLoad, onError, precedence, disabled} = props;
@@ -825,25 +828,37 @@ export function resourcesFromLink(props: Props): boolean {
825828
return true;
826829
}
827830
}
828-
return false;
831+
break;
829832
}
830-
case 'icon':
831-
case 'apple-touch-icon': {
832-
const key = rel + href;
833-
let resource = resources.headsMap.get(key);
834-
if (!resource) {
835-
resource = {
836-
type: 'link',
837-
props: Object.assign({}, props),
838-
flushed: false,
839-
};
840-
resources.headsMap.set(key, resource);
833+
}
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 'prefetch-dns': {
853+
resources.preconnects.add(resource);
854+
break;
855+
}
856+
default: {
841857
resources.headResources.add(resource);
842858
}
843-
return true;
844859
}
845860
}
846-
return false;
861+
return true;
847862
}
848863

849864
// Construct a resource from link props.

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

Lines changed: 16 additions & 0 deletions
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);
@@ -2454,6 +2462,7 @@ export function writeImmediateResources(
24542462

24552463
const {
24562464
charset,
2465+
preconnects,
24572466
fontPreloads,
24582467
usedStylePreloads,
24592468
scripts,
@@ -2469,6 +2478,13 @@ export function writeImmediateResources(
24692478
resources.charset = null;
24702479
}
24712480

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+
24722488
fontPreloads.forEach(r => {
24732489
// font preload Resources should not already be flushed so we elide this check
24742490
pushLinkImpl(target, r.props, responseState);

packages/react-dom-bindings/src/shared/ReactDOMResourceValidation.js

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import hasOwnProperty from 'shared/hasOwnProperty';
1111

1212
type Props = {[string]: mixed};
1313

14-
export function validateUnmatchedLinkResourceProps(
14+
export function warnOnMissingHrefAndRel(
1515
pendingProps: Props,
1616
currentProps: ?Props,
1717
) {
@@ -24,34 +24,52 @@ export function validateUnmatchedLinkResourceProps(
2424
const originalRelStatement = getValueDescriptorExpectingEnumForWarning(
2525
currentProps.rel,
2626
);
27-
const pendingRelStatement = getValueDescriptorExpectingEnumForWarning(
27+
const pendingRel = getValueDescriptorExpectingEnumForWarning(
2828
pendingProps.rel,
2929
);
30-
const pendingHrefStatement =
31-
typeof pendingProps.href === 'string'
32-
? ` and the updated href is "${pendingProps.href}"`
33-
: '';
34-
console.error(
35-
'A <link> previously rendered as a %s but was updated with a rel type that is not' +
36-
' valid for a Resource type. Generally Resources are not expected to ever have updated' +
37-
' props however in some limited circumstances it can be valid when changing the href.' +
38-
' When React encounters props that invalidate the Resource it is the same as not rendering' +
39-
' a Resource at all. valid rel types for Resources are "stylesheet" and "preload". The previous' +
40-
' rel for this instance was %s. The updated rel is %s%s.',
41-
originalResourceName,
42-
originalRelStatement,
43-
pendingRelStatement,
44-
pendingHrefStatement,
30+
const pendingHref = getValueDescriptorExpectingEnumForWarning(
31+
pendingProps.href,
4532
);
33+
if (typeof pendingProps.rel !== 'string') {
34+
console.error(
35+
'A <link> previously rendered as a %s with rel "%s" but was updated with an invalid rel: %s. When a link' +
36+
' does not have a valid rel prop it is not represented in the DOM. If this is intentional, instead' +
37+
' do not render the <link> anymore.',
38+
originalResourceName,
39+
originalRelStatement,
40+
pendingRel,
41+
);
42+
} else if (typeof pendingProps.href !== 'string') {
43+
console.error(
44+
'A <link> previously rendered as a %s but was updated with an invalid href prop: %s. When a link' +
45+
' does not have a valid href prop it is not represented in the DOM. If this is intentional, instead' +
46+
' do not render the <link> anymore.',
47+
originalResourceName,
48+
pendingHref,
49+
);
50+
}
4651
} else {
47-
const pendingRelStatement = getValueDescriptorExpectingEnumForWarning(
52+
const pendingRel = getValueDescriptorExpectingEnumForWarning(
4853
pendingProps.rel,
4954
);
50-
console.error(
51-
'A <link> is rendering as a Resource but has an invalid rel property. The rel encountered is %s.' +
52-
' This is a bug in React.',
53-
pendingRelStatement,
55+
const pendingHref = getValueDescriptorExpectingEnumForWarning(
56+
pendingProps.href,
5457
);
58+
if (typeof pendingProps.rel !== 'string') {
59+
console.error(
60+
'A <link> is rendering with an invalid rel: %s. When a link' +
61+
' does not have a valid rel prop it is not represented in the DOM. If this is intentional, instead' +
62+
' do not render the <link> anymore.',
63+
pendingRel,
64+
);
65+
} else if (typeof pendingProps.href !== 'string') {
66+
console.error(
67+
'A <link> is rendering with an invalid href: %s. When a link' +
68+
' does not have a valid href prop it is not represented in the DOM. If this is intentional, instead' +
69+
' do not render the <link> anymore.',
70+
pendingHref,
71+
);
72+
}
5573
}
5674
}
5775
}

0 commit comments

Comments
 (0)