Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Float] support <base> as Resource #25546

Merged
merged 1 commit into from
Oct 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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