Skip to content

Commit

Permalink
[Float] support all links as Resources (#25515)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
gnoff authored and rickhanlonii committed Dec 3, 2022
1 parent 4b09b0a commit f11a0e0
Show file tree
Hide file tree
Showing 5 changed files with 371 additions and 50 deletions.
103 changes: 88 additions & 15 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 @@ -54,7 +54,7 @@ type StyleProps = {
'data-precedence': string,
[string]: mixed,
};
export type StyleResource = {
type StyleResource = {
type: 'style',

// Ref count for resource
Expand All @@ -79,7 +79,7 @@ type ScriptProps = {
src: string,
[string]: mixed,
};
export type ScriptResource = {
type ScriptResource = {
type: 'script',
src: string,
props: ScriptProps,
Expand All @@ -88,12 +88,10 @@ export type ScriptResource = {
root: FloatRoot,
};

export type HeadResource = TitleResource | MetaResource;

type TitleProps = {
[string]: mixed,
};
export type TitleResource = {
type TitleResource = {
type: 'title',
props: TitleProps,

Expand All @@ -105,7 +103,7 @@ export type TitleResource = {
type MetaProps = {
[string]: mixed,
};
export type MetaResource = {
type MetaResource = {
type: 'meta',
matcher: string,
property: ?string,
Expand All @@ -117,8 +115,23 @@ export type MetaResource = {
root: Document,
};

type LinkProps = {
href: string,
rel: string,
[string]: mixed,
};
type LinkResource = {
type: 'link',
props: LinkProps,

count: number,
instance: ?Element,
root: Document,
};

type Props = {[string]: mixed};

type HeadResource = TitleResource | MetaResource | LinkResource;
type Resource = StyleResource | ScriptResource | PreloadResource | HeadResource;

export type RootResources = {
Expand Down Expand Up @@ -617,8 +630,30 @@ export function getResource(
return null;
}
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(headRoot).head;
let resource = headResources.get(key);
if (!resource) {
resource = {
type: 'link',
props: Object.assign({}, pendingProps),
count: 0,
instance: null,
root: headRoot,
};
headResources.set(key, resource);
}
return resource;
}
if (__DEV__) {
validateUnmatchedLinkResourceProps(pendingProps, currentProps);
warnOnMissingHrefAndRel(pendingProps, currentProps);
}
return null;
}
Expand Down Expand Up @@ -710,6 +745,7 @@ function scriptPropsFromRawProps(rawProps: ScriptQualifyingProps): ScriptProps {
export function acquireResource(resource: Resource): Instance {
switch (resource.type) {
case 'title':
case 'link':
case 'meta': {
return acquireHeadResource(resource);
}
Expand All @@ -732,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 @@ -1050,6 +1087,41 @@ function acquireHeadResource(resource: HeadResource): Instance {
insertResourceInstanceBefore(root, instance, insertBefore);
break;
}
case 'link': {
const linkProps: LinkProps = (props: any);
const limitedEscapedRel = escapeSelectorAttributeValueInsideDoubleQuotes(
linkProps.rel,
);
const limitedEscapedHref = escapeSelectorAttributeValueInsideDoubleQuotes(
linkProps.href,
);
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);
return instance;
}
instance = resource.instance = createResourceInstance(
type,
props,
root,
);
insertResourceInstanceBefore(root, instance, null);
return instance;
}
default: {
throw new Error(
`acquireHeadResource encountered a resource type it did not expect: "${type}". This is a bug in React.`,
Expand Down Expand Up @@ -1265,26 +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';
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
50 changes: 46 additions & 4 deletions packages/react-dom-bindings/src/server/ReactDOMFloatServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,20 @@ type MetaResource = {
flushed: boolean,
};

type LinkProps = {
href: string,
rel: string,
[string]: mixed,
};
type LinkResource = {
type: 'link',
props: LinkProps,

flushed: boolean,
};

export type Resource = PreloadResource | StyleResource | ScriptResource;
export type HeadResource = TitleResource | MetaResource;
export type HeadResource = TitleResource | MetaResource | LinkResource;

export type Resources = {
// Request local cache
Expand All @@ -101,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 @@ -131,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 @@ -697,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 @@ -813,10 +828,37 @@ export function resourcesFromLink(props: Props): boolean {
return true;
}
}
return false;
break;
}
}
return false;
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 'dns-prefetch': {
resources.preconnects.add(resource);
break;
}
default: {
resources.headResources.add(resource);
}
}
}
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 @@ -2418,6 +2426,10 @@ export function writeInitialResources(
pushSelfClosing(target, r.props, 'meta', responseState);
break;
}
case 'link': {
pushLinkImpl(target, r.props, responseState);
break;
}
}
r.flushed = true;
});
Expand Down Expand Up @@ -2450,6 +2462,7 @@ export function writeImmediateResources(

const {
charset,
preconnects,
fontPreloads,
usedStylePreloads,
scripts,
Expand All @@ -2465,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 Expand Up @@ -2507,6 +2527,10 @@ export function writeImmediateResources(
pushSelfClosing(target, r.props, 'meta', responseState);
break;
}
case 'link': {
pushLinkImpl(target, r.props, responseState);
break;
}
}
r.flushed = true;
});
Expand Down
Loading

0 comments on commit f11a0e0

Please sign in to comment.