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