Skip to content

Commit 61f9b5e

Browse files
authored
[Float] support <base> as Resource (#25546)
keys off `target` and `href`. prepends on insertion similar to title. only flushes on the server in the shell (should probably add a warning if there are any to flush in a boundary)
1 parent 1d3fc9c commit 61f9b5e

File tree

4 files changed

+187
-7
lines changed

4 files changed

+187
-7
lines changed

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

+63-2
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,19 @@ type LinkResource = {
133133
root: Document,
134134
};
135135

136+
type BaseResource = {
137+
type: 'base',
138+
matcher: string,
139+
props: Props,
140+
141+
count: number,
142+
instance: ?Element,
143+
root: Document,
144+
};
145+
136146
type Props = {[string]: mixed};
137147

138-
type HeadResource = TitleResource | MetaResource | LinkResource;
148+
type HeadResource = TitleResource | MetaResource | LinkResource | BaseResource;
139149
type Resource = StyleResource | ScriptResource | PreloadResource | HeadResource;
140150

141151
export type RootResources = {
@@ -443,6 +453,35 @@ export function getResource(
443453
);
444454
}
445455
switch (type) {
456+
case 'base': {
457+
const headRoot: Document = getDocumentFromRoot(resourceRoot);
458+
const headResources = getResourcesFromRoot(headRoot).head;
459+
const {target, href} = pendingProps;
460+
let matcher = 'base';
461+
matcher +=
462+
typeof href === 'string'
463+
? `[href="${escapeSelectorAttributeValueInsideDoubleQuotes(href)}"]`
464+
: ':not([href])';
465+
matcher +=
466+
typeof target === 'string'
467+
? `[target="${escapeSelectorAttributeValueInsideDoubleQuotes(
468+
target,
469+
)}"]`
470+
: ':not([target])';
471+
let resource = headResources.get(matcher);
472+
if (!resource) {
473+
resource = {
474+
type: 'base',
475+
matcher,
476+
props: Object.assign({}, pendingProps),
477+
count: 0,
478+
instance: null,
479+
root: headRoot,
480+
};
481+
headResources.set(matcher, resource);
482+
}
483+
return resource;
484+
}
446485
case 'meta': {
447486
let matcher, propertyString, parentResource;
448487
const {
@@ -748,6 +787,7 @@ function scriptPropsFromRawProps(rawProps: ScriptQualifyingProps): ScriptProps {
748787

749788
export function acquireResource(resource: Resource): Instance {
750789
switch (resource.type) {
790+
case 'base':
751791
case 'title':
752792
case 'link':
753793
case 'meta': {
@@ -1126,6 +1166,27 @@ function acquireHeadResource(resource: HeadResource): Instance {
11261166
insertResourceInstanceBefore(root, instance, null);
11271167
return instance;
11281168
}
1169+
case 'base': {
1170+
const baseResource: BaseResource = (resource: any);
1171+
const {matcher} = baseResource;
1172+
const base = root.querySelector(matcher);
1173+
if (base) {
1174+
instance = resource.instance = base;
1175+
markNodeAsResource(instance);
1176+
} else {
1177+
instance = resource.instance = createResourceInstance(
1178+
type,
1179+
props,
1180+
root,
1181+
);
1182+
insertResourceInstanceBefore(
1183+
root,
1184+
instance,
1185+
root.querySelector('base'),
1186+
);
1187+
}
1188+
return instance;
1189+
}
11291190
default: {
11301191
throw new Error(
11311192
`acquireHeadResource encountered a resource type it did not expect: "${type}". This is a bug in React.`,
@@ -1341,6 +1402,7 @@ export function isHostResourceType(type: string, props: Props): boolean {
13411402
resourceFormOnly = getResourceFormOnly(hostContext);
13421403
}
13431404
switch (type) {
1405+
case 'base':
13441406
case 'meta':
13451407
case 'title': {
13461408
return true;
@@ -1403,7 +1465,6 @@ export function isHostResourceType(type: string, props: Props): boolean {
14031465
}
14041466
return (async: any) && typeof src === 'string' && !onLoad && !onError;
14051467
}
1406-
case 'base':
14071468
case 'template':
14081469
case 'style':
14091470
case 'noscript': {

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

+35-3
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,19 @@ type LinkResource = {
101101
flushed: boolean,
102102
};
103103

104+
type BaseResource = {
105+
type: 'base',
106+
props: Props,
107+
108+
flushed: boolean,
109+
};
110+
104111
export type Resource = PreloadResource | StyleResource | ScriptResource;
105-
export type HeadResource = TitleResource | MetaResource | LinkResource;
112+
export type HeadResource =
113+
| TitleResource
114+
| MetaResource
115+
| LinkResource
116+
| BaseResource;
106117

107118
export type Resources = {
108119
// Request local cache
@@ -113,6 +124,7 @@ export type Resources = {
113124

114125
// Flushing queues for Resource dependencies
115126
charset: null | MetaResource,
127+
bases: Set<BaseResource>,
116128
preconnects: Set<LinkResource>,
117129
fontPreloads: Set<PreloadResource>,
118130
// usedImagePreloads: Set<PreloadResource>,
@@ -144,6 +156,7 @@ export function createResources(): Resources {
144156

145157
// cleared on flush
146158
charset: null,
159+
bases: new Set(),
147160
preconnects: new Set(),
148161
fontPreloads: new Set(),
149162
// usedImagePreloads: new Set(),
@@ -692,9 +705,28 @@ export function resourcesFromElement(type: string, props: Props): boolean {
692705
resources.headResources.add(resource);
693706
}
694707
}
695-
return true;
696708
}
697-
return false;
709+
return true;
710+
}
711+
case 'base': {
712+
const {target, href} = props;
713+
// We mirror the key construction on the client since we will likely unify
714+
// this code in the future to better guarantee key semantics are identical
715+
// in both environments
716+
let key = 'base';
717+
key += typeof href === 'string' ? `[href="${href}"]` : ':not([href])';
718+
key +=
719+
typeof target === 'string' ? `[target="${target}"]` : ':not([target])';
720+
if (!resources.headsMap.has(key)) {
721+
const resource = {
722+
type: 'base',
723+
props: Object.assign({}, props),
724+
flushed: false,
725+
};
726+
resources.headsMap.set(key, resource);
727+
resources.bases.add(resource);
728+
}
729+
return true;
698730
}
699731
}
700732
return false;

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

+29-1
Original file line numberDiff line numberDiff line change
@@ -1150,6 +1150,26 @@ function pushStartTextArea(
11501150
return null;
11511151
}
11521152

1153+
function pushBase(
1154+
target: Array<Chunk | PrecomputedChunk>,
1155+
props: Object,
1156+
responseState: ResponseState,
1157+
textEmbedded: boolean,
1158+
): ReactNodeList {
1159+
if (enableFloat && resourcesFromElement('base', props)) {
1160+
if (textEmbedded) {
1161+
// This link follows text but we aren't writing a tag. while not as efficient as possible we need
1162+
// to be safe and assume text will follow by inserting a textSeparator
1163+
target.push(textSeparator);
1164+
}
1165+
// We have converted this link exclusively to a resource and no longer
1166+
// need to emit it
1167+
return null;
1168+
}
1169+
1170+
return pushSelfClosing(target, props, 'base', responseState);
1171+
}
1172+
11531173
function pushMeta(
11541174
target: Array<Chunk | PrecomputedChunk>,
11551175
props: Object,
@@ -1853,14 +1873,15 @@ export function pushStartInstance(
18531873
: pushStartGenericElement(target, props, type, responseState);
18541874
case 'meta':
18551875
return pushMeta(target, props, responseState, textEmbedded);
1876+
case 'base':
1877+
return pushBase(target, props, responseState, textEmbedded);
18561878
// Newline eating tags
18571879
case 'listing':
18581880
case 'pre': {
18591881
return pushStartPreformattedElement(target, props, type, responseState);
18601882
}
18611883
// Omitted close tags
18621884
case 'area':
1863-
case 'base':
18641885
case 'br':
18651886
case 'col':
18661887
case 'embed':
@@ -2493,6 +2514,7 @@ export function writeInitialResources(
24932514

24942515
const {
24952516
charset,
2517+
bases,
24962518
preconnects,
24972519
fontPreloads,
24982520
precedences,
@@ -2510,6 +2532,12 @@ export function writeInitialResources(
25102532
resources.charset = null;
25112533
}
25122534

2535+
bases.forEach(r => {
2536+
pushSelfClosing(target, r.props, 'base', responseState);
2537+
r.flushed = true;
2538+
});
2539+
bases.clear();
2540+
25132541
preconnects.forEach(r => {
25142542
// font preload Resources should not already be flushed so we elide this check
25152543
pushLinkImpl(target, r.props, responseState);

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

+60-1
Original file line numberDiff line numberDiff line change
@@ -1189,8 +1189,67 @@ describe('ReactDOMFloat', () => {
11891189
</html>,
11901190
);
11911191
});
1192+
1193+
// @gate enableFloat
1194+
it('can render <base> as a Resource', async () => {
1195+
await actIntoEmptyDocument(() => {
1196+
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
1197+
<html>
1198+
<head />
1199+
<body>
1200+
<base target="_blank" />
1201+
<base href="foo" />
1202+
<base target="_self" href="bar" />
1203+
<div>hello world</div>
1204+
</body>
1205+
</html>,
1206+
);
1207+
pipe(writable);
1208+
});
1209+
expect(getMeaningfulChildren(document)).toEqual(
1210+
<html>
1211+
<head>
1212+
<base target="_blank" />
1213+
<base href="foo" />
1214+
<base target="_self" href="bar" />
1215+
</head>
1216+
<body>
1217+
<div>hello world</div>
1218+
</body>
1219+
</html>,
1220+
);
1221+
1222+
ReactDOMClient.hydrateRoot(
1223+
document,
1224+
<html>
1225+
<head />
1226+
<body>
1227+
<base target="_blank" />
1228+
<base href="foo" />
1229+
<base target="_self" href="bar" />
1230+
<base target="_top" href="baz" />
1231+
<div>hello world</div>
1232+
</body>
1233+
</html>,
1234+
);
1235+
expect(Scheduler).toFlushWithoutYielding();
1236+
expect(getMeaningfulChildren(document)).toEqual(
1237+
<html>
1238+
<head>
1239+
<base target="_top" href="baz" />
1240+
<base target="_blank" />
1241+
<base href="foo" />
1242+
<base target="_self" href="bar" />
1243+
</head>
1244+
<body>
1245+
<div>hello world</div>
1246+
</body>
1247+
</html>,
1248+
);
1249+
});
1250+
11921251
// @gate enableFloat
1193-
it('can render icons and apple-touch-icons as resources', async () => {
1252+
it('can render icons and apple-touch-icons as Resources', async () => {
11941253
await actIntoEmptyDocument(() => {
11951254
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
11961255
<>

0 commit comments

Comments
 (0)