@samitouri / QOS-React-2 / commits / 61f9b5e97b

[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)

Josh Story committed Oct 23, 2022 at 15:03 UTC 61f9b5e97b36de205c7d16256334cd85b92adc2d
4 files changed +187 -7
packages/react-dom-bindings/src/client/ReactDOMFloatClient.js
+63 -2
@@ -133,9 +133,19 @@ type LinkResource = {
133 root: Document,
134 };
135
136 +type BaseResource = {
137 + type: 'base',
138 + matcher: string,
139 + props: Props,
140 +
141 + count: number,
142 + instance: ?Element,
143 + root: Document,
144 +};
145 +
146 type Props = {[string]: mixed};
147
138 -type HeadResource = TitleResource | MetaResource | LinkResource;
148 +type HeadResource = TitleResource | MetaResource | LinkResource | BaseResource;
149 type Resource = StyleResource | ScriptResource | PreloadResource | HeadResource;
150
151 export type RootResources = {
@@ -443,6 +453,35 @@ export function getResource(
453 );
454 }
455 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 + }
485 case 'meta': {
486 let matcher, propertyString, parentResource;
487 const {
@@ -748,6 +787,7 @@ function scriptPropsFromRawProps(rawProps: ScriptQualifyingProps): ScriptProps {
787
788 export function acquireResource(resource: Resource): Instance {
789 switch (resource.type) {
790 + case 'base':
791 case 'title':
792 case 'link':
793 case 'meta': {
@@ -1126,6 +1166,27 @@ function acquireHeadResource(resource: HeadResource): Instance {
1166 insertResourceInstanceBefore(root, instance, null);
1167 return instance;
1168 }
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 + }
1190 default: {
1191 throw new Error(
1192 `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 {
1402 resourceFormOnly = getResourceFormOnly(hostContext);
1403 }
1404 switch (type) {
1405 + case 'base':
1406 case 'meta':
1407 case 'title': {
1408 return true;
@@ -1403,7 +1465,6 @@ export function isHostResourceType(type: string, props: Props): boolean {
1465 }
1466 return (async: any) && typeof src === 'string' && !onLoad && !onError;
1467 }
1406 - case 'base':
1468 case 'template':
1469 case 'style':
1470 case 'noscript': {
packages/react-dom-bindings/src/server/ReactDOMFloatServer.js
+35 -3
@@ -101,8 +101,19 @@ type LinkResource = {
101 flushed: boolean,
102 };
103
104 +type BaseResource = {
105 + type: 'base',
106 + props: Props,
107 +
108 + flushed: boolean,
109 +};
110 +
111 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;
117
118 export type Resources = {
119 // Request local cache
@@ -113,6 +124,7 @@ export type Resources = {
124
125 // Flushing queues for Resource dependencies
126 charset: null | MetaResource,
127 + bases: Set<BaseResource>,
128 preconnects: Set<LinkResource>,
129 fontPreloads: Set<PreloadResource>,
130 // usedImagePreloads: Set<PreloadResource>,
@@ -144,6 +156,7 @@ export function createResources(): Resources {
156
157 // cleared on flush
158 charset: null,
159 + bases: new Set(),
160 preconnects: new Set(),
161 fontPreloads: new Set(),
162 // usedImagePreloads: new Set(),
@@ -692,9 +705,28 @@ export function resourcesFromElement(type: string, props: Props): boolean {
705 resources.headResources.add(resource);
706 }
707 }
695 - return true;
708 }
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;
730 }
731 }
732 return false;
packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js
+29 -1
@@ -1150,6 +1150,26 @@ function pushStartTextArea(
1150 return null;
1151 }
1152
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 +
1173 function pushMeta(
1174 target: Array<Chunk | PrecomputedChunk>,
1175 props: Object,
@@ -1853,6 +1873,8 @@ export function pushStartInstance(
1873 : pushStartGenericElement(target, props, type, responseState);
1874 case 'meta':
1875 return pushMeta(target, props, responseState, textEmbedded);
1876 + case 'base':
1877 + return pushBase(target, props, responseState, textEmbedded);
1878 // Newline eating tags
1879 case 'listing':
1880 case 'pre': {
@@ -1860,7 +1882,6 @@ export function pushStartInstance(
1882 }
1883 // Omitted close tags
1884 case 'area':
1863 - case 'base':
1885 case 'br':
1886 case 'col':
1887 case 'embed':
@@ -2493,6 +2514,7 @@ export function writeInitialResources(
2514
2515 const {
2516 charset,
2517 + bases,
2518 preconnects,
2519 fontPreloads,
2520 precedences,
@@ -2510,6 +2532,12 @@ export function writeInitialResources(
2532 resources.charset = null;
2533 }
2534
2535 + bases.forEach(r => {
2536 + pushSelfClosing(target, r.props, 'base', responseState);
2537 + r.flushed = true;
2538 + });
2539 + bases.clear();
2540 +
2541 preconnects.forEach(r => {
2542 // font preload Resources should not already be flushed so we elide this check
2543 pushLinkImpl(target, r.props, responseState);
packages/react-dom/src/__tests__/ReactDOMFloat-test.js
+60 -1
@@ -1189,8 +1189,67 @@ describe('ReactDOMFloat', () => {
1189 </html>,
1190 );
1191 });
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 +
1251 // @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 () => {
1253 await actIntoEmptyDocument(() => {
1254 const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
1255 <>