[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)
This commit is contained in:
parent
1d3fc9c9c4
commit
61f9b5e97b
|
@ -133,9 +133,19 @@ type LinkResource = {
|
|||
root: Document,
|
||||
};
|
||||
|
||||
type BaseResource = {
|
||||
type: 'base',
|
||||
matcher: string,
|
||||
props: Props,
|
||||
|
||||
count: number,
|
||||
instance: ?Element,
|
||||
root: Document,
|
||||
};
|
||||
|
||||
type Props = {[string]: mixed};
|
||||
|
||||
type HeadResource = TitleResource | MetaResource | LinkResource;
|
||||
type HeadResource = TitleResource | MetaResource | LinkResource | BaseResource;
|
||||
type Resource = StyleResource | ScriptResource | PreloadResource | HeadResource;
|
||||
|
||||
export type RootResources = {
|
||||
|
@ -443,6 +453,35 @@ export function getResource(
|
|||
);
|
||||
}
|
||||
switch (type) {
|
||||
case 'base': {
|
||||
const headRoot: Document = getDocumentFromRoot(resourceRoot);
|
||||
const headResources = getResourcesFromRoot(headRoot).head;
|
||||
const {target, href} = pendingProps;
|
||||
let matcher = 'base';
|
||||
matcher +=
|
||||
typeof href === 'string'
|
||||
? `[href="${escapeSelectorAttributeValueInsideDoubleQuotes(href)}"]`
|
||||
: ':not([href])';
|
||||
matcher +=
|
||||
typeof target === 'string'
|
||||
? `[target="${escapeSelectorAttributeValueInsideDoubleQuotes(
|
||||
target,
|
||||
)}"]`
|
||||
: ':not([target])';
|
||||
let resource = headResources.get(matcher);
|
||||
if (!resource) {
|
||||
resource = {
|
||||
type: 'base',
|
||||
matcher,
|
||||
props: Object.assign({}, pendingProps),
|
||||
count: 0,
|
||||
instance: null,
|
||||
root: headRoot,
|
||||
};
|
||||
headResources.set(matcher, resource);
|
||||
}
|
||||
return resource;
|
||||
}
|
||||
case 'meta': {
|
||||
let matcher, propertyString, parentResource;
|
||||
const {
|
||||
|
@ -748,6 +787,7 @@ function scriptPropsFromRawProps(rawProps: ScriptQualifyingProps): ScriptProps {
|
|||
|
||||
export function acquireResource(resource: Resource): Instance {
|
||||
switch (resource.type) {
|
||||
case 'base':
|
||||
case 'title':
|
||||
case 'link':
|
||||
case 'meta': {
|
||||
|
@ -1126,6 +1166,27 @@ function acquireHeadResource(resource: HeadResource): Instance {
|
|||
insertResourceInstanceBefore(root, instance, null);
|
||||
return instance;
|
||||
}
|
||||
case 'base': {
|
||||
const baseResource: BaseResource = (resource: any);
|
||||
const {matcher} = baseResource;
|
||||
const base = root.querySelector(matcher);
|
||||
if (base) {
|
||||
instance = resource.instance = base;
|
||||
markNodeAsResource(instance);
|
||||
} else {
|
||||
instance = resource.instance = createResourceInstance(
|
||||
type,
|
||||
props,
|
||||
root,
|
||||
);
|
||||
insertResourceInstanceBefore(
|
||||
root,
|
||||
instance,
|
||||
root.querySelector('base'),
|
||||
);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
default: {
|
||||
throw new Error(
|
||||
`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 {
|
|||
resourceFormOnly = getResourceFormOnly(hostContext);
|
||||
}
|
||||
switch (type) {
|
||||
case 'base':
|
||||
case 'meta':
|
||||
case 'title': {
|
||||
return true;
|
||||
|
@ -1403,7 +1465,6 @@ export function isHostResourceType(type: string, props: Props): boolean {
|
|||
}
|
||||
return (async: any) && typeof src === 'string' && !onLoad && !onError;
|
||||
}
|
||||
case 'base':
|
||||
case 'template':
|
||||
case 'style':
|
||||
case 'noscript': {
|
||||
|
|
|
@ -101,8 +101,19 @@ type LinkResource = {
|
|||
flushed: boolean,
|
||||
};
|
||||
|
||||
type BaseResource = {
|
||||
type: 'base',
|
||||
props: Props,
|
||||
|
||||
flushed: boolean,
|
||||
};
|
||||
|
||||
export type Resource = PreloadResource | StyleResource | ScriptResource;
|
||||
export type HeadResource = TitleResource | MetaResource | LinkResource;
|
||||
export type HeadResource =
|
||||
| TitleResource
|
||||
| MetaResource
|
||||
| LinkResource
|
||||
| BaseResource;
|
||||
|
||||
export type Resources = {
|
||||
// Request local cache
|
||||
|
@ -113,6 +124,7 @@ export type Resources = {
|
|||
|
||||
// Flushing queues for Resource dependencies
|
||||
charset: null | MetaResource,
|
||||
bases: Set<BaseResource>,
|
||||
preconnects: Set<LinkResource>,
|
||||
fontPreloads: Set<PreloadResource>,
|
||||
// usedImagePreloads: Set<PreloadResource>,
|
||||
|
@ -144,6 +156,7 @@ export function createResources(): Resources {
|
|||
|
||||
// cleared on flush
|
||||
charset: null,
|
||||
bases: new Set(),
|
||||
preconnects: new Set(),
|
||||
fontPreloads: new Set(),
|
||||
// usedImagePreloads: new Set(),
|
||||
|
@ -692,9 +705,28 @@ export function resourcesFromElement(type: string, props: Props): boolean {
|
|||
resources.headResources.add(resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
case 'base': {
|
||||
const {target, href} = props;
|
||||
// We mirror the key construction on the client since we will likely unify
|
||||
// this code in the future to better guarantee key semantics are identical
|
||||
// in both environments
|
||||
let key = 'base';
|
||||
key += typeof href === 'string' ? `[href="${href}"]` : ':not([href])';
|
||||
key +=
|
||||
typeof target === 'string' ? `[target="${target}"]` : ':not([target])';
|
||||
if (!resources.headsMap.has(key)) {
|
||||
const resource = {
|
||||
type: 'base',
|
||||
props: Object.assign({}, props),
|
||||
flushed: false,
|
||||
};
|
||||
resources.headsMap.set(key, resource);
|
||||
resources.bases.add(resource);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
|
|
@ -1150,6 +1150,26 @@ function pushStartTextArea(
|
|||
return null;
|
||||
}
|
||||
|
||||
function pushBase(
|
||||
target: Array<Chunk | PrecomputedChunk>,
|
||||
props: Object,
|
||||
responseState: ResponseState,
|
||||
textEmbedded: boolean,
|
||||
): ReactNodeList {
|
||||
if (enableFloat && resourcesFromElement('base', props)) {
|
||||
if (textEmbedded) {
|
||||
// This link follows text but we aren't writing a tag. while not as efficient as possible we need
|
||||
// to be safe and assume text will follow by inserting a textSeparator
|
||||
target.push(textSeparator);
|
||||
}
|
||||
// We have converted this link exclusively to a resource and no longer
|
||||
// need to emit it
|
||||
return null;
|
||||
}
|
||||
|
||||
return pushSelfClosing(target, props, 'base', responseState);
|
||||
}
|
||||
|
||||
function pushMeta(
|
||||
target: Array<Chunk | PrecomputedChunk>,
|
||||
props: Object,
|
||||
|
@ -1853,6 +1873,8 @@ export function pushStartInstance(
|
|||
: pushStartGenericElement(target, props, type, responseState);
|
||||
case 'meta':
|
||||
return pushMeta(target, props, responseState, textEmbedded);
|
||||
case 'base':
|
||||
return pushBase(target, props, responseState, textEmbedded);
|
||||
// Newline eating tags
|
||||
case 'listing':
|
||||
case 'pre': {
|
||||
|
@ -1860,7 +1882,6 @@ export function pushStartInstance(
|
|||
}
|
||||
// Omitted close tags
|
||||
case 'area':
|
||||
case 'base':
|
||||
case 'br':
|
||||
case 'col':
|
||||
case 'embed':
|
||||
|
@ -2493,6 +2514,7 @@ export function writeInitialResources(
|
|||
|
||||
const {
|
||||
charset,
|
||||
bases,
|
||||
preconnects,
|
||||
fontPreloads,
|
||||
precedences,
|
||||
|
@ -2510,6 +2532,12 @@ export function writeInitialResources(
|
|||
resources.charset = null;
|
||||
}
|
||||
|
||||
bases.forEach(r => {
|
||||
pushSelfClosing(target, r.props, 'base', responseState);
|
||||
r.flushed = true;
|
||||
});
|
||||
bases.clear();
|
||||
|
||||
preconnects.forEach(r => {
|
||||
// font preload Resources should not already be flushed so we elide this check
|
||||
pushLinkImpl(target, r.props, responseState);
|
||||
|
|
|
@ -1189,8 +1189,67 @@ describe('ReactDOMFloat', () => {
|
|||
</html>,
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableFloat
|
||||
it('can render icons and apple-touch-icons as resources', async () => {
|
||||
it('can render <base> as a Resource', async () => {
|
||||
await actIntoEmptyDocument(() => {
|
||||
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
|
||||
<html>
|
||||
<head />
|
||||
<body>
|
||||
<base target="_blank" />
|
||||
<base href="foo" />
|
||||
<base target="_self" href="bar" />
|
||||
<div>hello world</div>
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
pipe(writable);
|
||||
});
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
<base target="_blank" />
|
||||
<base href="foo" />
|
||||
<base target="_self" href="bar" />
|
||||
</head>
|
||||
<body>
|
||||
<div>hello world</div>
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
|
||||
ReactDOMClient.hydrateRoot(
|
||||
document,
|
||||
<html>
|
||||
<head />
|
||||
<body>
|
||||
<base target="_blank" />
|
||||
<base href="foo" />
|
||||
<base target="_self" href="bar" />
|
||||
<base target="_top" href="baz" />
|
||||
<div>hello world</div>
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
expect(Scheduler).toFlushWithoutYielding();
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
<base target="_top" href="baz" />
|
||||
<base target="_blank" />
|
||||
<base href="foo" />
|
||||
<base target="_self" href="bar" />
|
||||
</head>
|
||||
<body>
|
||||
<div>hello world</div>
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableFloat
|
||||
it('can render icons and apple-touch-icons as Resources', async () => {
|
||||
await actIntoEmptyDocument(() => {
|
||||
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
|
||||
<>
|
||||
|
|
Loading…
Reference in New Issue