[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,
|
root: Document,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type BaseResource = {
|
||||||
|
type: 'base',
|
||||||
|
matcher: string,
|
||||||
|
props: Props,
|
||||||
|
|
||||||
|
count: number,
|
||||||
|
instance: ?Element,
|
||||||
|
root: Document,
|
||||||
|
};
|
||||||
|
|
||||||
type Props = {[string]: mixed};
|
type Props = {[string]: mixed};
|
||||||
|
|
||||||
type HeadResource = TitleResource | MetaResource | LinkResource;
|
type HeadResource = TitleResource | MetaResource | LinkResource | BaseResource;
|
||||||
type Resource = StyleResource | ScriptResource | PreloadResource | HeadResource;
|
type Resource = StyleResource | ScriptResource | PreloadResource | HeadResource;
|
||||||
|
|
||||||
export type RootResources = {
|
export type RootResources = {
|
||||||
|
@ -443,6 +453,35 @@ export function getResource(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
switch (type) {
|
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': {
|
case 'meta': {
|
||||||
let matcher, propertyString, parentResource;
|
let matcher, propertyString, parentResource;
|
||||||
const {
|
const {
|
||||||
|
@ -748,6 +787,7 @@ function scriptPropsFromRawProps(rawProps: ScriptQualifyingProps): ScriptProps {
|
||||||
|
|
||||||
export function acquireResource(resource: Resource): Instance {
|
export function acquireResource(resource: Resource): Instance {
|
||||||
switch (resource.type) {
|
switch (resource.type) {
|
||||||
|
case 'base':
|
||||||
case 'title':
|
case 'title':
|
||||||
case 'link':
|
case 'link':
|
||||||
case 'meta': {
|
case 'meta': {
|
||||||
|
@ -1126,6 +1166,27 @@ function acquireHeadResource(resource: HeadResource): Instance {
|
||||||
insertResourceInstanceBefore(root, instance, null);
|
insertResourceInstanceBefore(root, instance, null);
|
||||||
return instance;
|
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: {
|
default: {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`acquireHeadResource encountered a resource type it did not expect: "${type}". This is a bug in React.`,
|
`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);
|
resourceFormOnly = getResourceFormOnly(hostContext);
|
||||||
}
|
}
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
case 'base':
|
||||||
case 'meta':
|
case 'meta':
|
||||||
case 'title': {
|
case 'title': {
|
||||||
return true;
|
return true;
|
||||||
|
@ -1403,7 +1465,6 @@ export function isHostResourceType(type: string, props: Props): boolean {
|
||||||
}
|
}
|
||||||
return (async: any) && typeof src === 'string' && !onLoad && !onError;
|
return (async: any) && typeof src === 'string' && !onLoad && !onError;
|
||||||
}
|
}
|
||||||
case 'base':
|
|
||||||
case 'template':
|
case 'template':
|
||||||
case 'style':
|
case 'style':
|
||||||
case 'noscript': {
|
case 'noscript': {
|
||||||
|
|
|
@ -101,8 +101,19 @@ type LinkResource = {
|
||||||
flushed: boolean,
|
flushed: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type BaseResource = {
|
||||||
|
type: 'base',
|
||||||
|
props: Props,
|
||||||
|
|
||||||
|
flushed: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
export type Resource = PreloadResource | StyleResource | ScriptResource;
|
export type Resource = PreloadResource | StyleResource | ScriptResource;
|
||||||
export type HeadResource = TitleResource | MetaResource | LinkResource;
|
export type HeadResource =
|
||||||
|
| TitleResource
|
||||||
|
| MetaResource
|
||||||
|
| LinkResource
|
||||||
|
| BaseResource;
|
||||||
|
|
||||||
export type Resources = {
|
export type Resources = {
|
||||||
// Request local cache
|
// Request local cache
|
||||||
|
@ -113,6 +124,7 @@ export type Resources = {
|
||||||
|
|
||||||
// Flushing queues for Resource dependencies
|
// Flushing queues for Resource dependencies
|
||||||
charset: null | MetaResource,
|
charset: null | MetaResource,
|
||||||
|
bases: Set<BaseResource>,
|
||||||
preconnects: Set<LinkResource>,
|
preconnects: Set<LinkResource>,
|
||||||
fontPreloads: Set<PreloadResource>,
|
fontPreloads: Set<PreloadResource>,
|
||||||
// usedImagePreloads: Set<PreloadResource>,
|
// usedImagePreloads: Set<PreloadResource>,
|
||||||
|
@ -144,6 +156,7 @@ export function createResources(): Resources {
|
||||||
|
|
||||||
// cleared on flush
|
// cleared on flush
|
||||||
charset: null,
|
charset: null,
|
||||||
|
bases: new Set(),
|
||||||
preconnects: new Set(),
|
preconnects: new Set(),
|
||||||
fontPreloads: new Set(),
|
fontPreloads: new Set(),
|
||||||
// usedImagePreloads: new Set(),
|
// usedImagePreloads: new Set(),
|
||||||
|
@ -692,9 +705,28 @@ export function resourcesFromElement(type: string, props: Props): boolean {
|
||||||
resources.headResources.add(resource);
|
resources.headResources.add(resource);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
return false;
|
return true;
|
||||||
|
}
|
||||||
|
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;
|
return false;
|
||||||
|
|
|
@ -1150,6 +1150,26 @@ function pushStartTextArea(
|
||||||
return null;
|
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(
|
function pushMeta(
|
||||||
target: Array<Chunk | PrecomputedChunk>,
|
target: Array<Chunk | PrecomputedChunk>,
|
||||||
props: Object,
|
props: Object,
|
||||||
|
@ -1853,6 +1873,8 @@ export function pushStartInstance(
|
||||||
: pushStartGenericElement(target, props, type, responseState);
|
: pushStartGenericElement(target, props, type, responseState);
|
||||||
case 'meta':
|
case 'meta':
|
||||||
return pushMeta(target, props, responseState, textEmbedded);
|
return pushMeta(target, props, responseState, textEmbedded);
|
||||||
|
case 'base':
|
||||||
|
return pushBase(target, props, responseState, textEmbedded);
|
||||||
// Newline eating tags
|
// Newline eating tags
|
||||||
case 'listing':
|
case 'listing':
|
||||||
case 'pre': {
|
case 'pre': {
|
||||||
|
@ -1860,7 +1882,6 @@ export function pushStartInstance(
|
||||||
}
|
}
|
||||||
// Omitted close tags
|
// Omitted close tags
|
||||||
case 'area':
|
case 'area':
|
||||||
case 'base':
|
|
||||||
case 'br':
|
case 'br':
|
||||||
case 'col':
|
case 'col':
|
||||||
case 'embed':
|
case 'embed':
|
||||||
|
@ -2493,6 +2514,7 @@ export function writeInitialResources(
|
||||||
|
|
||||||
const {
|
const {
|
||||||
charset,
|
charset,
|
||||||
|
bases,
|
||||||
preconnects,
|
preconnects,
|
||||||
fontPreloads,
|
fontPreloads,
|
||||||
precedences,
|
precedences,
|
||||||
|
@ -2510,6 +2532,12 @@ export function writeInitialResources(
|
||||||
resources.charset = null;
|
resources.charset = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bases.forEach(r => {
|
||||||
|
pushSelfClosing(target, r.props, 'base', responseState);
|
||||||
|
r.flushed = true;
|
||||||
|
});
|
||||||
|
bases.clear();
|
||||||
|
|
||||||
preconnects.forEach(r => {
|
preconnects.forEach(r => {
|
||||||
// font preload Resources should not already be flushed so we elide this check
|
// font preload Resources should not already be flushed so we elide this check
|
||||||
pushLinkImpl(target, r.props, responseState);
|
pushLinkImpl(target, r.props, responseState);
|
||||||
|
|
|
@ -1189,8 +1189,67 @@ describe('ReactDOMFloat', () => {
|
||||||
</html>,
|
</html>,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// @gate enableFloat
|
// @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(() => {
|
await actIntoEmptyDocument(() => {
|
||||||
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
|
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
|
||||||
<>
|
<>
|
||||||
|
|
Loading…
Reference in New Issue