@samitouri / QOS-React-2 / commits / 618388bc32

[Float] Support script preloads (#25432)

* support script preloads * gates

Josh Story committed Oct 5, 2022 at 09:47 UTC 618388bc323356a5a9c0a6f162f516d35a3b4b2d
4 files changed +147 -51
packages/react-dom-bindings/src/client/ReactDOMFloatClient.js
+4 -3
@@ -29,7 +29,7 @@ import {getCurrentRootHostContainer} from 'react-reconciler/src/ReactFiberHostCo
29
30 // The resource types we support. currently they match the form for the as argument.
31 // In the future this may need to change, especially when modules / scripts are supported
32 -type ResourceType = 'style' | 'font';
32 +type ResourceType = 'style' | 'font' | 'script';
33
34 type PreloadProps = {
35 rel: 'preload',
@@ -150,7 +150,7 @@ function getDocumentFromRoot(root: FloatRoot): Document {
150 // ReactDOM.Preload
151 // --------------------------------------
152 type PreloadAs = ResourceType;
153 -type PreloadOptions = {as: PreloadAs, crossOrigin?: string};
153 +type PreloadOptions = {as: PreloadAs, crossOrigin?: string, integrity?: string};
154 function preload(href: string, options: PreloadOptions) {
155 if (__DEV__) {
156 validatePreloadArguments(href, options);
@@ -194,6 +194,7 @@ function preloadPropsFromPreloadOptions(
194 rel: 'preload',
195 as,
196 crossOrigin: as === 'font' ? '' : options.crossOrigin,
197 + integrity: options.integrity,
198 };
199 }
200
@@ -832,7 +833,7 @@ export function isHostResourceType(type: string, props: Props): boolean {
833 }
834
835 function isResourceAsType(as: mixed): boolean {
835 - return as === 'style' || as === 'font';
836 + return as === 'style' || as === 'font' || as === 'script';
837 }
838
839 // When passing user input into querySelector(All) the embedded string must not alter
packages/react-dom-bindings/src/server/ReactDOMFloatServer.js
+4 -2
@@ -19,7 +19,7 @@ import {
19
20 type Props = {[string]: mixed};
21
22 -type ResourceType = 'style' | 'font';
22 +type ResourceType = 'style' | 'font' | 'script';
23
24 type PreloadProps = {
25 rel: 'preload',
@@ -123,7 +123,7 @@ export const ReactDOMServerDispatcher = {
123 };
124
125 type PreloadAs = ResourceType;
126 -type PreloadOptions = {as: PreloadAs, crossOrigin?: string};
126 +type PreloadOptions = {as: PreloadAs, crossOrigin?: string, integrity?: string};
127 function preload(href: string, options: PreloadOptions) {
128 if (!currentResources) {
129 // While we expect that preload calls are primarily going to be observed
@@ -248,6 +248,7 @@ function preloadPropsFromPreloadOptions(
248 rel: 'preload',
249 as,
250 crossOrigin: as === 'font' ? '' : options.crossOrigin,
251 + integrity: options.integrity,
252 };
253 }
254
@@ -526,6 +527,7 @@ export function resourcesFromLink(props: Props): boolean {
527 return false;
528 }
529 switch (as) {
530 + case 'script':
531 case 'style':
532 case 'font': {
533 if (__DEV__) {
packages/react-dom-bindings/src/shared/ReactDOMResourceValidation.js
+38 -40
@@ -16,44 +16,42 @@ export function validateUnmatchedLinkResourceProps(
16 currentProps: ?Props,
17 ) {
18 if (__DEV__) {
19 - if (pendingProps.rel !== 'font' && pendingProps.rel !== 'style') {
20 - if (currentProps != null) {
21 - const originalResourceName =
22 - typeof currentProps.href === 'string'
23 - ? `Resource with href "${currentProps.href}"`
24 - : 'Resource';
25 - const originalRelStatement = getValueDescriptorExpectingEnumForWarning(
26 - currentProps.rel,
27 - );
28 - const pendingRelStatement = getValueDescriptorExpectingEnumForWarning(
29 - pendingProps.rel,
30 - );
31 - const pendingHrefStatement =
32 - typeof pendingProps.href === 'string'
33 - ? ` and the updated href is "${pendingProps.href}"`
34 - : '';
35 - console.error(
36 - 'A <link> previously rendered as a %s but was updated with a rel type that is not' +
37 - ' valid for a Resource type. Generally Resources are not expected to ever have updated' +
38 - ' props however in some limited circumstances it can be valid when changing the href.' +
39 - ' When React encounters props that invalidate the Resource it is the same as not rendering' +
40 - ' a Resource at all. valid rel types for Resources are "font" and "style". The previous' +
41 - ' rel for this instance was %s. The updated rel is %s%s.',
42 - originalResourceName,
43 - originalRelStatement,
44 - pendingRelStatement,
45 - pendingHrefStatement,
46 - );
47 - } else {
48 - const pendingRelStatement = getValueDescriptorExpectingEnumForWarning(
49 - pendingProps.rel,
50 - );
51 - console.error(
52 - 'A <link> is rendering as a Resource but has an invalid rel property. The rel encountered is %s.' +
53 - ' This is a bug in React.',
54 - pendingRelStatement,
55 - );
56 - }
19 + if (currentProps != null) {
20 + const originalResourceName =
21 + typeof currentProps.href === 'string'
22 + ? `Resource with href "${currentProps.href}"`
23 + : 'Resource';
24 + const originalRelStatement = getValueDescriptorExpectingEnumForWarning(
25 + currentProps.rel,
26 + );
27 + const pendingRelStatement = getValueDescriptorExpectingEnumForWarning(
28 + pendingProps.rel,
29 + );
30 + const pendingHrefStatement =
31 + typeof pendingProps.href === 'string'
32 + ? ` and the updated href is "${pendingProps.href}"`
33 + : '';
34 + console.error(
35 + 'A <link> previously rendered as a %s but was updated with a rel type that is not' +
36 + ' valid for a Resource type. Generally Resources are not expected to ever have updated' +
37 + ' props however in some limited circumstances it can be valid when changing the href.' +
38 + ' When React encounters props that invalidate the Resource it is the same as not rendering' +
39 + ' a Resource at all. valid rel types for Resources are "stylesheet" and "preload". The previous' +
40 + ' rel for this instance was %s. The updated rel is %s%s.',
41 + originalResourceName,
42 + originalRelStatement,
43 + pendingRelStatement,
44 + pendingHrefStatement,
45 + );
46 + } else {
47 + const pendingRelStatement = getValueDescriptorExpectingEnumForWarning(
48 + pendingProps.rel,
49 + );
50 + console.error(
51 + 'A <link> is rendering as a Resource but has an invalid rel property. The rel encountered is %s.' +
52 + ' This is a bug in React.',
53 + pendingRelStatement,
54 + );
55 }
56 }
57 }
@@ -517,6 +515,7 @@ export function validatePreloadArguments(href: mixed, options: mixed) {
515 }
516 break;
517 }
518 + case 'script':
519 case 'style': {
520 break;
521 }
@@ -529,7 +528,7 @@ export function validatePreloadArguments(href: mixed, options: mixed) {
528 ' Please use one of the following valid values instead: %s. The href for the preload call where this' +
529 ' warning originated is "%s".',
530 typeOfAs,
532 - '"style" and "font"',
531 + '"style", "font", or "script"',
532 href,
533 );
534 }
@@ -557,7 +556,6 @@ export function validatePreinitArguments(href: mixed, options: mixed) {
556 } else {
557 const as = options.as;
558 switch (as) {
560 - case 'font':
559 case 'style': {
560 break;
561 }
packages/react-dom/src/__tests__/ReactDOMFloat-test.js
+101 -6
@@ -270,7 +270,7 @@ describe('ReactDOMFloat', () => {
270 ' valid for a Resource type. Generally Resources are not expected to ever have updated' +
271 ' props however in some limited circumstances it can be valid when changing the href.' +
272 ' When React encounters props that invalidate the Resource it is the same as not rendering' +
273 - ' a Resource at all. valid rel types for Resources are "font" and "style". The previous' +
273 + ' a Resource at all. valid rel types for Resources are "stylesheet" and "preload". The previous' +
274 ' rel for this instance was "stylesheet". The updated rel is "author" and the updated href is "bar".',
275 );
276 expect(getVisibleChildren(document)).toEqual(
@@ -407,6 +407,97 @@ describe('ReactDOMFloat', () => {
407 </html>,
408 );
409 });
410 +
411 + // @gate enableFloat
412 + it('supports script preloads', async () => {
413 + function ServerApp() {
414 + ReactDOM.preload('foo', {as: 'script', integrity: 'foo hash'});
415 + ReactDOM.preload('bar', {
416 + as: 'script',
417 + crossOrigin: 'use-credentials',
418 + integrity: 'bar hash',
419 + });
420 + return (
421 + <html>
422 + <link rel="preload" href="baz" as="script" />
423 + <head>
424 + <title>hi</title>
425 + </head>
426 + <body>foo</body>
427 + </html>
428 + );
429 + }
430 + function ClientApp() {
431 + ReactDOM.preload('foo', {as: 'script', integrity: 'foo hash'});
432 + ReactDOM.preload('qux', {as: 'script'});
433 + return (
434 + <html>
435 + <head>
436 + <title>hi</title>
437 + </head>
438 + <body>foo</body>
439 + <link
440 + rel="preload"
441 + href="quux"
442 + as="script"
443 + crossOrigin=""
444 + integrity="quux hash"
445 + />
446 + </html>
447 + );
448 + }
449 +
450 + await actIntoEmptyDocument(() => {
451 + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<ServerApp />);
452 + pipe(writable);
453 + });
454 + expect(getVisibleChildren(document)).toEqual(
455 + <html>
456 + <head>
457 + <link rel="preload" as="script" href="foo" integrity="foo hash" />
458 + <link
459 + rel="preload"
460 + as="script"
461 + href="bar"
462 + crossorigin="use-credentials"
463 + integrity="bar hash"
464 + />
465 + <link rel="preload" as="script" href="baz" />
466 + <title>hi</title>
467 + </head>
468 + <body>foo</body>
469 + </html>,
470 + );
471 +
472 + ReactDOMClient.hydrateRoot(document, <ClientApp />);
473 + expect(Scheduler).toFlushWithoutYielding();
474 +
475 + expect(getVisibleChildren(document)).toEqual(
476 + <html>
477 + <head>
478 + <link rel="preload" as="script" href="foo" integrity="foo hash" />
479 + <link
480 + rel="preload"
481 + as="script"
482 + href="bar"
483 + crossorigin="use-credentials"
484 + integrity="bar hash"
485 + />
486 + <link rel="preload" as="script" href="baz" />
487 + <title>hi</title>
488 + <link rel="preload" as="script" href="qux" />
489 + <link
490 + rel="preload"
491 + as="script"
492 + href="quux"
493 + crossorigin=""
494 + integrity="quux hash"
495 + />
496 + </head>
497 + <body>foo</body>
498 + </html>,
499 + );
500 + });
501 });
502
503 describe('ReactDOM.preinit as style', () => {
@@ -2885,7 +2976,11 @@ describe('ReactDOMFloat', () => {
2976 (mockError, scenarioNumber) => {
2977 if (__DEV__) {
2978 expect(mockError.mock.calls[scenarioNumber]).toEqual(
2888 - makeArgs('undefined', '"style" and "font"', 'foo'),
2979 + makeArgs(
2980 + 'undefined',
2981 + '"style", "font", or "script"',
2982 + 'foo',
2983 + ),
2984 );
2985 } else {
2986 expect(mockError).not.toHaveBeenCalled();
@@ -2898,7 +2993,7 @@ describe('ReactDOMFloat', () => {
2993 (mockError, scenarioNumber) => {
2994 if (__DEV__) {
2995 expect(mockError.mock.calls[scenarioNumber]).toEqual(
2901 - makeArgs('null', '"style" and "font"', 'bar'),
2996 + makeArgs('null', '"style", "font", or "script"', 'bar'),
2997 );
2998 } else {
2999 expect(mockError).not.toHaveBeenCalled();
@@ -2913,7 +3008,7 @@ describe('ReactDOMFloat', () => {
3008 expect(mockError.mock.calls[scenarioNumber]).toEqual(
3009 makeArgs(
3010 'something with type "number"',
2916 - '"style" and "font"',
3011 + '"style", "font", or "script"',
3012 'baz',
3013 ),
3014 );
@@ -2930,7 +3025,7 @@ describe('ReactDOMFloat', () => {
3025 expect(mockError.mock.calls[scenarioNumber]).toEqual(
3026 makeArgs(
3027 'something with type "object"',
2933 - '"style" and "font"',
3028 + '"style", "font", or "script"',
3029 'qux',
3030 ),
3031 );
@@ -2945,7 +3040,7 @@ describe('ReactDOMFloat', () => {
3040 (mockError, scenarioNumber) => {
3041 if (__DEV__) {
3042 expect(mockError.mock.calls[scenarioNumber]).toEqual(
2948 - makeArgs('"bar"', '"style" and "font"', 'quux'),
3043 + makeArgs('"bar"', '"style", "font", or "script"', 'quux'),
3044 );
3045 } else {
3046 expect(mockError).not.toHaveBeenCalled();