Skip to content

Commit cc45100

Browse files
committed
support icon and apple-touch-icon as resources
1 parent 701f08a commit cc45100

File tree

4 files changed

+160
-7
lines changed

4 files changed

+160
-7
lines changed

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

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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 = {
@@ -616,6 +629,28 @@ export function getResource(
616629
}
617630
return null;
618631
}
632+
case 'icon':
633+
case 'apple-touch-icon': {
634+
const {href} = pendingProps;
635+
if (typeof href === 'string') {
636+
const key = rel + href;
637+
const headRoot = getDocumentFromRoot(resourceRoot);
638+
const headResources = getResourcesFromRoot(resourceRoot).head;
639+
let resource = headResources.get(key);
640+
if (!resource) {
641+
resource = {
642+
type: 'link',
643+
props: Object.assign({}, pendingProps),
644+
count: 0,
645+
instance: null,
646+
root: headRoot,
647+
};
648+
headResources.set(key, resource);
649+
}
650+
return resource;
651+
}
652+
return null;
653+
}
619654
default: {
620655
if (__DEV__) {
621656
validateUnmatchedLinkResourceProps(pendingProps, currentProps);
@@ -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
}
@@ -1050,6 +1086,30 @@ function acquireHeadResource(resource: HeadResource): Instance {
10501086
insertResourceInstanceBefore(root, instance, insertBefore);
10511087
break;
10521088
}
1089+
case 'link': {
1090+
const linkProps: LinkProps = (props: any);
1091+
const limitedEscapedRel = escapeSelectorAttributeValueInsideDoubleQuotes(
1092+
linkProps.rel,
1093+
);
1094+
const limitedEscapedHref = escapeSelectorAttributeValueInsideDoubleQuotes(
1095+
linkProps.href,
1096+
);
1097+
const existingEl = root.querySelector(
1098+
`link[rel="${limitedEscapedRel}"][href="${limitedEscapedHref}"]`,
1099+
);
1100+
if (existingEl) {
1101+
instance = resource.instance = existingEl;
1102+
markNodeAsResource(instance);
1103+
return instance;
1104+
}
1105+
instance = resource.instance = createResourceInstance(
1106+
type,
1107+
props,
1108+
root,
1109+
);
1110+
insertResourceInstanceBefore(root, instance, null);
1111+
return instance;
1112+
}
10531113
default: {
10541114
throw new Error(
10551115
`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 {
12831343
const {href, onLoad, onError} = props;
12841344
return !onLoad && !onError && typeof href === 'string';
12851345
}
1346+
case 'icon':
1347+
case 'apple-touch-icon': {
1348+
return true;
1349+
}
12861350
}
12871351
return false;
12881352
}

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

Lines changed: 28 additions & 1 deletion
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
@@ -815,6 +827,21 @@ export function resourcesFromLink(props: Props): boolean {
815827
}
816828
return false;
817829
}
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);
841+
resources.headResources.add(resource);
842+
}
843+
return true;
844+
}
818845
}
819846
return false;
820847
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2418,6 +2418,10 @@ export function writeInitialResources(
24182418
pushSelfClosing(target, r.props, 'meta', responseState);
24192419
break;
24202420
}
2421+
case 'link': {
2422+
pushLinkImpl(target, r.props, responseState);
2423+
break;
2424+
}
24212425
}
24222426
r.flushed = true;
24232427
});
@@ -2507,6 +2511,10 @@ export function writeImmediateResources(
25072511
pushSelfClosing(target, r.props, 'meta', responseState);
25082512
break;
25092513
}
2514+
case 'link': {
2515+
pushLinkImpl(target, r.props, responseState);
2516+
break;
2517+
}
25102518
}
25112519
r.flushed = true;
25122520
});

packages/react-dom/src/__tests__/ReactDOMFloat-test.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -941,6 +941,60 @@ describe('ReactDOMFloat', () => {
941941
});
942942

943943
describe('head resources', () => {
944+
// @gate enableFloat
945+
it('can render icons and apple-touch-icons as resources', async () => {
946+
await actIntoEmptyDocument(() => {
947+
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
948+
<>
949+
<html>
950+
<head />
951+
<body>
952+
<link rel="icon" href="foo" />
953+
<div>hello world</div>
954+
</body>
955+
</html>
956+
<link rel="apple-touch-icon" href="foo" />
957+
</>,
958+
);
959+
pipe(writable);
960+
});
961+
expect(getMeaningfulChildren(document)).toEqual(
962+
<html>
963+
<head>
964+
<link rel="icon" href="foo" />
965+
<link rel="apple-touch-icon" href="foo" />
966+
</head>
967+
<body>
968+
<div>hello world</div>
969+
</body>
970+
</html>,
971+
);
972+
973+
ReactDOMClient.hydrateRoot(
974+
document,
975+
<html>
976+
<link rel="apple-touch-icon" href="foo" />
977+
<head />
978+
<body>
979+
<link rel="icon" href="foo" />
980+
<div>hello world</div>
981+
</body>
982+
</html>,
983+
);
984+
expect(Scheduler).toFlushWithoutYielding();
985+
expect(getMeaningfulChildren(document)).toEqual(
986+
<html>
987+
<head>
988+
<link rel="icon" href="foo" />
989+
<link rel="apple-touch-icon" href="foo" />
990+
</head>
991+
<body>
992+
<div>hello world</div>
993+
</body>
994+
</html>,
995+
);
996+
});
997+
944998
// @gate enableFloat
945999
it('can hydrate the right instances for deeply nested structured metas', async () => {
9461000
await actIntoEmptyDocument(() => {

0 commit comments

Comments
 (0)