Skip to content

Commit

Permalink
[Float] support <base> as Resource (#25546)
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
gnoff authored and rickhanlonii committed Dec 3, 2022
1 parent 2ce7008 commit c49882e
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 7 deletions.
65 changes: 63 additions & 2 deletions packages/react-dom-bindings/src/client/ReactDOMFloatClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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': {
Expand Down Expand Up @@ -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.`,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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': {
Expand Down
38 changes: 35 additions & 3 deletions packages/react-dom-bindings/src/server/ReactDOMFloatServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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>,
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -692,9 +705,28 @@ export function resourcesFromElement(type: string, props: Props): boolean {
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1853,14 +1873,15 @@ 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': {
return pushStartPreformattedElement(target, props, type, responseState);
}
// Omitted close tags
case 'area':
case 'base':
case 'br':
case 'col':
case 'embed':
Expand Down Expand Up @@ -2493,6 +2514,7 @@ export function writeInitialResources(

const {
charset,
bases,
preconnects,
fontPreloads,
precedences,
Expand All @@ -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);
Expand Down
61 changes: 60 additions & 1 deletion packages/react-dom/src/__tests__/ReactDOMFloat-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1189,8 +1189,67 @@ describe('ReactDOMFloat', () => {
</html>,
);
});

// @gate enableFloat
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 () => {
it('can render icons and apple-touch-icons as Resources', async () => {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<>
Expand Down

0 comments on commit c49882e

Please sign in to comment.