Skip to content

Commit

Permalink
support all link types as resources
Browse files Browse the repository at this point in the history
  • Loading branch information
gnoff committed Oct 21, 2022
1 parent cc45100 commit 989bda8
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 71 deletions.
59 changes: 34 additions & 25 deletions packages/react-dom-bindings/src/client/ReactDOMFloatClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals.js';
const {Dispatcher} = ReactDOMSharedInternals;
import {DOCUMENT_NODE} from '../shared/HTMLNodeType';
import {
validateUnmatchedLinkResourceProps,
warnOnMissingHrefAndRel,
validatePreloadResourceDifference,
validateURLKeyedUpdatedProps,
validateStyleResourceDifference,
Expand Down Expand Up @@ -629,13 +629,16 @@ export function getResource(
}
return null;
}
case 'icon':
case 'apple-touch-icon': {
const {href} = pendingProps;
if (typeof href === 'string') {
const key = rel + href;
default: {
const {href, sizes, media} = pendingProps;
if (typeof rel === 'string' && typeof href === 'string') {
const sizeKey =
'::sizes:' + (typeof sizes === 'string' ? sizes : '');
const mediaKey =
'::media:' + (typeof media === 'string' ? media : '');
const key = 'rel:' + rel + '::href:' + href + sizeKey + mediaKey;
const headRoot = getDocumentFromRoot(resourceRoot);
const headResources = getResourcesFromRoot(resourceRoot).head;
const headResources = getResourcesFromRoot(headRoot).head;
let resource = headResources.get(key);
if (!resource) {
resource = {
Expand All @@ -649,11 +652,8 @@ export function getResource(
}
return resource;
}
return null;
}
default: {
if (__DEV__) {
validateUnmatchedLinkResourceProps(pendingProps, currentProps);
warnOnMissingHrefAndRel(pendingProps, currentProps);
}
return null;
}
Expand Down Expand Up @@ -768,6 +768,7 @@ export function acquireResource(resource: Resource): Instance {

export function releaseResource(resource: Resource): void {
switch (resource.type) {
case 'link':
case 'title':
case 'meta': {
return releaseHeadResource(resource);
Expand Down Expand Up @@ -1094,9 +1095,20 @@ function acquireHeadResource(resource: HeadResource): Instance {
const limitedEscapedHref = escapeSelectorAttributeValueInsideDoubleQuotes(
linkProps.href,
);
const existingEl = root.querySelector(
`link[rel="${limitedEscapedRel}"][href="${limitedEscapedHref}"]`,
);
let selector = `link[rel="${limitedEscapedRel}"][href="${limitedEscapedHref}"]`;
if (typeof linkProps.sizes === 'string') {
const limitedEscapedSizes = escapeSelectorAttributeValueInsideDoubleQuotes(
linkProps.sizes,
);
selector += `[sizes="${limitedEscapedSizes}"]`;
}
if (typeof linkProps.media === 'string') {
const limitedEscapedMedia = escapeSelectorAttributeValueInsideDoubleQuotes(
linkProps.media,
);
selector += `[media="${limitedEscapedMedia}"]`;
}
const existingEl = root.querySelector(selector);
if (existingEl) {
instance = resource.instance = existingEl;
markNodeAsResource(instance);
Expand Down Expand Up @@ -1325,30 +1337,27 @@ export function isHostResourceType(type: string, props: Props): boolean {
return true;
}
case 'link': {
const {onLoad, onError} = props;
if (onLoad || onError) {
return false;
}
switch (props.rel) {
case 'stylesheet': {
if (__DEV__) {
validateLinkPropsForStyleResource(props);
}
const {href, precedence, onLoad, onError, disabled} = props;
const {href, precedence, disabled} = props;
return (
typeof href === 'string' &&
typeof precedence === 'string' &&
!onLoad &&
!onError &&
disabled == null
);
}
case 'preload': {
const {href, onLoad, onError} = props;
return !onLoad && !onError && typeof href === 'string';
}
case 'icon':
case 'apple-touch-icon': {
return true;
default: {
const {rel, href} = props;
return typeof href === 'string' && typeof rel === 'string';
}
}
return false;
}
case 'script': {
// We don't validate because it is valid to use async with onLoad/onError unlike combining
Expand Down
45 changes: 30 additions & 15 deletions packages/react-dom-bindings/src/server/ReactDOMFloatServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export type Resources = {

// Flushing queues for Resource dependencies
charset: null | MetaResource,
preconnects: Set<LinkResource>,
fontPreloads: Set<PreloadResource>,
// usedImagePreloads: Set<PreloadResource>,
precedences: Map<string, Set<StyleResource>>,
Expand Down Expand Up @@ -143,6 +144,7 @@ export function createResources(): Resources {

// cleared on flush
charset: null,
preconnects: new Set(),
fontPreloads: new Set(),
// usedImagePreloads: new Set(),
precedences: new Map(),
Expand Down Expand Up @@ -709,10 +711,11 @@ export function resourcesFromLink(props: Props): boolean {
const resources = currentResources;

const {rel, href} = props;
if (!href || typeof href !== 'string') {
if (!href || typeof href !== 'string' || !rel || typeof rel !== 'string') {
return false;
}

let key = '';
switch (rel) {
case 'stylesheet': {
const {onLoad, onError, precedence, disabled} = props;
Expand Down Expand Up @@ -825,25 +828,37 @@ export function resourcesFromLink(props: Props): boolean {
return true;
}
}
return false;
break;
}
case 'icon':
case 'apple-touch-icon': {
const key = rel + href;
let resource = resources.headsMap.get(key);
if (!resource) {
resource = {
type: 'link',
props: Object.assign({}, props),
flushed: false,
};
resources.headsMap.set(key, resource);
}
if (props.onLoad || props.onError) {
return false;
}

const sizes = typeof props.sizes === 'string' ? props.sizes : '';
const media = typeof props.media === 'string' ? props.media : '';
key =
'rel:' + rel + '::href:' + href + '::sizes:' + sizes + '::media:' + media;
let resource = resources.headsMap.get(key);
if (!resource) {
resource = {
type: 'link',
props: Object.assign({}, props),
flushed: false,
};
resources.headsMap.set(key, resource);
switch (rel) {
case 'preconnect':
case 'prefetch-dns': {
resources.preconnects.add(resource);
break;
}
default: {
resources.headResources.add(resource);
}
return true;
}
}
return false;
return true;
}

// Construct a resource from link props.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2340,6 +2340,7 @@ export function writeInitialResources(

const {
charset,
preconnects,
fontPreloads,
precedences,
usedStylePreloads,
Expand All @@ -2356,6 +2357,13 @@ export function writeInitialResources(
resources.charset = null;
}

preconnects.forEach(r => {
// font preload Resources should not already be flushed so we elide this check
pushLinkImpl(target, r.props, responseState);
r.flushed = true;
});
preconnects.clear();

fontPreloads.forEach(r => {
// font preload Resources should not already be flushed so we elide this check
pushLinkImpl(target, r.props, responseState);
Expand Down Expand Up @@ -2454,6 +2462,7 @@ export function writeImmediateResources(

const {
charset,
preconnects,
fontPreloads,
usedStylePreloads,
scripts,
Expand All @@ -2469,6 +2478,13 @@ export function writeImmediateResources(
resources.charset = null;
}

preconnects.forEach(r => {
// font preload Resources should not already be flushed so we elide this check
pushLinkImpl(target, r.props, responseState);
r.flushed = true;
});
preconnects.clear();

fontPreloads.forEach(r => {
// font preload Resources should not already be flushed so we elide this check
pushLinkImpl(target, r.props, responseState);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import hasOwnProperty from 'shared/hasOwnProperty';

type Props = {[string]: mixed};

export function validateUnmatchedLinkResourceProps(
export function warnOnMissingHrefAndRel(
pendingProps: Props,
currentProps: ?Props,
) {
Expand All @@ -24,34 +24,52 @@ export function validateUnmatchedLinkResourceProps(
const originalRelStatement = getValueDescriptorExpectingEnumForWarning(
currentProps.rel,
);
const pendingRelStatement = getValueDescriptorExpectingEnumForWarning(
const pendingRel = getValueDescriptorExpectingEnumForWarning(
pendingProps.rel,
);
const pendingHrefStatement =
typeof pendingProps.href === 'string'
? ` and the updated href is "${pendingProps.href}"`
: '';
console.error(
'A <link> previously rendered as a %s but was updated with a rel type that is not' +
' valid for a Resource type. Generally Resources are not expected to ever have updated' +
' props however in some limited circumstances it can be valid when changing the href.' +
' When React encounters props that invalidate the Resource it is the same as not rendering' +
' a Resource at all. valid rel types for Resources are "stylesheet" and "preload". The previous' +
' rel for this instance was %s. The updated rel is %s%s.',
originalResourceName,
originalRelStatement,
pendingRelStatement,
pendingHrefStatement,
const pendingHref = getValueDescriptorExpectingEnumForWarning(
pendingProps.href,
);
if (typeof pendingProps.rel !== 'string') {
console.error(
'A <link> previously rendered as a %s with rel "%s" but was updated with an invalid rel: %s. When a link' +
' does not have a valid rel prop it is not represented in the DOM. If this is intentional, instead' +
' do not render the <link> anymore.',
originalResourceName,
originalRelStatement,
pendingRel,
);
} else if (typeof pendingProps.href !== 'string') {
console.error(
'A <link> previously rendered as a %s but was updated with an invalid href prop: %s. When a link' +
' does not have a valid href prop it is not represented in the DOM. If this is intentional, instead' +
' do not render the <link> anymore.',
originalResourceName,
pendingHref,
);
}
} else {
const pendingRelStatement = getValueDescriptorExpectingEnumForWarning(
const pendingRel = getValueDescriptorExpectingEnumForWarning(
pendingProps.rel,
);
console.error(
'A <link> is rendering as a Resource but has an invalid rel property. The rel encountered is %s.' +
' This is a bug in React.',
pendingRelStatement,
const pendingHref = getValueDescriptorExpectingEnumForWarning(
pendingProps.href,
);
if (typeof pendingProps.rel !== 'string') {
console.error(
'A <link> is rendering with an invalid rel: %s. When a link' +
' does not have a valid rel prop it is not represented in the DOM. If this is intentional, instead' +
' do not render the <link> anymore.',
pendingRel,
);
} else if (typeof pendingProps.href !== 'string') {
console.error(
'A <link> is rendering with an invalid href: %s. When a link' +
' does not have a valid href prop it is not represented in the DOM. If this is intentional, instead' +
' do not render the <link> anymore.',
pendingHref,
);
}
}
}
}
Expand Down
Loading

0 comments on commit 989bda8

Please sign in to comment.