From cc45100ad1e09320d7d466f370401f5d40afc5d0 Mon Sep 17 00:00:00 2001
From: Josh Story
Date: Wed, 19 Oct 2022 13:17:35 -0700
Subject: [PATCH] support icon and apple-touch-icon as resources
---
.../src/client/ReactDOMFloatClient.js | 76 +++++++++++++++++--
.../src/server/ReactDOMFloatServer.js | 29 ++++++-
.../src/server/ReactDOMServerFormatConfig.js | 8 ++
.../src/__tests__/ReactDOMFloat-test.js | 54 +++++++++++++
4 files changed, 160 insertions(+), 7 deletions(-)
diff --git a/packages/react-dom-bindings/src/client/ReactDOMFloatClient.js b/packages/react-dom-bindings/src/client/ReactDOMFloatClient.js
index 67c44b102074d..0a67341ba8612 100644
--- a/packages/react-dom-bindings/src/client/ReactDOMFloatClient.js
+++ b/packages/react-dom-bindings/src/client/ReactDOMFloatClient.js
@@ -54,7 +54,7 @@ type StyleProps = {
'data-precedence': string,
[string]: mixed,
};
-export type StyleResource = {
+type StyleResource = {
type: 'style',
// Ref count for resource
@@ -79,7 +79,7 @@ type ScriptProps = {
src: string,
[string]: mixed,
};
-export type ScriptResource = {
+type ScriptResource = {
type: 'script',
src: string,
props: ScriptProps,
@@ -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,
@@ -105,7 +103,7 @@ export type TitleResource = {
type MetaProps = {
[string]: mixed,
};
-export type MetaResource = {
+type MetaResource = {
type: 'meta',
matcher: string,
property: ?string,
@@ -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 = {
@@ -616,6 +629,28 @@ export function getResource(
}
return null;
}
+ case 'icon':
+ case 'apple-touch-icon': {
+ const {href} = pendingProps;
+ if (typeof href === 'string') {
+ const key = rel + href;
+ const headRoot = getDocumentFromRoot(resourceRoot);
+ const headResources = getResourcesFromRoot(resourceRoot).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;
+ }
+ return null;
+ }
default: {
if (__DEV__) {
validateUnmatchedLinkResourceProps(pendingProps, currentProps);
@@ -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);
}
@@ -1050,6 +1086,30 @@ 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,
+ );
+ const existingEl = root.querySelector(
+ `link[rel="${limitedEscapedRel}"][href="${limitedEscapedHref}"]`,
+ );
+ 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.`,
@@ -1283,6 +1343,10 @@ export function isHostResourceType(type: string, props: Props): boolean {
const {href, onLoad, onError} = props;
return !onLoad && !onError && typeof href === 'string';
}
+ case 'icon':
+ case 'apple-touch-icon': {
+ return true;
+ }
}
return false;
}
diff --git a/packages/react-dom-bindings/src/server/ReactDOMFloatServer.js b/packages/react-dom-bindings/src/server/ReactDOMFloatServer.js
index e71f8e465ce85..5f7051365a0dc 100644
--- a/packages/react-dom-bindings/src/server/ReactDOMFloatServer.js
+++ b/packages/react-dom-bindings/src/server/ReactDOMFloatServer.js
@@ -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
@@ -815,6 +827,21 @@ export function resourcesFromLink(props: Props): boolean {
}
return false;
}
+ 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);
+ resources.headResources.add(resource);
+ }
+ return true;
+ }
}
return false;
}
diff --git a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js
index 181ac82f9194f..83d9b243a9ea0 100644
--- a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js
+++ b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js
@@ -2418,6 +2418,10 @@ export function writeInitialResources(
pushSelfClosing(target, r.props, 'meta', responseState);
break;
}
+ case 'link': {
+ pushLinkImpl(target, r.props, responseState);
+ break;
+ }
}
r.flushed = true;
});
@@ -2507,6 +2511,10 @@ export function writeImmediateResources(
pushSelfClosing(target, r.props, 'meta', responseState);
break;
}
+ case 'link': {
+ pushLinkImpl(target, r.props, responseState);
+ break;
+ }
}
r.flushed = true;
});
diff --git a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js
index 0ef44d9743319..f15f9806840d3 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js
@@ -941,6 +941,60 @@ describe('ReactDOMFloat', () => {
});
describe('head resources', () => {
+ // @gate enableFloat
+ it('can render icons and apple-touch-icons as resources', async () => {
+ await actIntoEmptyDocument(() => {
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
+ <>
+
+
+
+
+ hello world
+
+
+
+ >,
+ );
+ pipe(writable);
+ });
+ expect(getMeaningfulChildren(document)).toEqual(
+
+
+
+
+
+
+ hello world
+
+ ,
+ );
+
+ ReactDOMClient.hydrateRoot(
+ document,
+
+
+
+
+ hello world
+
+ ,
+ );
+ expect(Scheduler).toFlushWithoutYielding();
+ expect(getMeaningfulChildren(document)).toEqual(
+
+
+
+
+
+
+ hello world
+
+ ,
+ );
+ });
+
// @gate enableFloat
it('can hydrate the right instances for deeply nested structured metas', async () => {
await actIntoEmptyDocument(() => {