Skip to content

Commit 618388b

Browse files
authored
[Float] Support script preloads (#25432)
* support script preloads * gates
1 parent 65b3449 commit 618388b

File tree

4 files changed

+147
-51
lines changed

4 files changed

+147
-51
lines changed

packages/react-dom-bindings/src/client/ReactDOMFloatClient.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {getCurrentRootHostContainer} from 'react-reconciler/src/ReactFiberHostCo
2929

3030
// The resource types we support. currently they match the form for the as argument.
3131
// 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';
3333

3434
type PreloadProps = {
3535
rel: 'preload',
@@ -150,7 +150,7 @@ function getDocumentFromRoot(root: FloatRoot): Document {
150150
// ReactDOM.Preload
151151
// --------------------------------------
152152
type PreloadAs = ResourceType;
153-
type PreloadOptions = {as: PreloadAs, crossOrigin?: string};
153+
type PreloadOptions = {as: PreloadAs, crossOrigin?: string, integrity?: string};
154154
function preload(href: string, options: PreloadOptions) {
155155
if (__DEV__) {
156156
validatePreloadArguments(href, options);
@@ -194,6 +194,7 @@ function preloadPropsFromPreloadOptions(
194194
rel: 'preload',
195195
as,
196196
crossOrigin: as === 'font' ? '' : options.crossOrigin,
197+
integrity: options.integrity,
197198
};
198199
}
199200

@@ -832,7 +833,7 @@ export function isHostResourceType(type: string, props: Props): boolean {
832833
}
833834

834835
function isResourceAsType(as: mixed): boolean {
835-
return as === 'style' || as === 'font';
836+
return as === 'style' || as === 'font' || as === 'script';
836837
}
837838

838839
// When passing user input into querySelector(All) the embedded string must not alter

packages/react-dom-bindings/src/server/ReactDOMFloatServer.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919

2020
type Props = {[string]: mixed};
2121

22-
type ResourceType = 'style' | 'font';
22+
type ResourceType = 'style' | 'font' | 'script';
2323

2424
type PreloadProps = {
2525
rel: 'preload',
@@ -123,7 +123,7 @@ export const ReactDOMServerDispatcher = {
123123
};
124124

125125
type PreloadAs = ResourceType;
126-
type PreloadOptions = {as: PreloadAs, crossOrigin?: string};
126+
type PreloadOptions = {as: PreloadAs, crossOrigin?: string, integrity?: string};
127127
function preload(href: string, options: PreloadOptions) {
128128
if (!currentResources) {
129129
// While we expect that preload calls are primarily going to be observed
@@ -248,6 +248,7 @@ function preloadPropsFromPreloadOptions(
248248
rel: 'preload',
249249
as,
250250
crossOrigin: as === 'font' ? '' : options.crossOrigin,
251+
integrity: options.integrity,
251252
};
252253
}
253254

@@ -526,6 +527,7 @@ export function resourcesFromLink(props: Props): boolean {
526527
return false;
527528
}
528529
switch (as) {
530+
case 'script':
529531
case 'style':
530532
case 'font': {
531533
if (__DEV__) {

packages/react-dom-bindings/src/shared/ReactDOMResourceValidation.js

+38-40
Original file line numberDiff line numberDiff line change
@@ -16,44 +16,42 @@ export function validateUnmatchedLinkResourceProps(
1616
currentProps: ?Props,
1717
) {
1818
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+
);
5755
}
5856
}
5957
}
@@ -517,6 +515,7 @@ export function validatePreloadArguments(href: mixed, options: mixed) {
517515
}
518516
break;
519517
}
518+
case 'script':
520519
case 'style': {
521520
break;
522521
}
@@ -529,7 +528,7 @@ export function validatePreloadArguments(href: mixed, options: mixed) {
529528
' Please use one of the following valid values instead: %s. The href for the preload call where this' +
530529
' warning originated is "%s".',
531530
typeOfAs,
532-
'"style" and "font"',
531+
'"style", "font", or "script"',
533532
href,
534533
);
535534
}
@@ -557,7 +556,6 @@ export function validatePreinitArguments(href: mixed, options: mixed) {
557556
} else {
558557
const as = options.as;
559558
switch (as) {
560-
case 'font':
561559
case 'style': {
562560
break;
563561
}

packages/react-dom/src/__tests__/ReactDOMFloat-test.js

+101-6
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ describe('ReactDOMFloat', () => {
270270
' valid for a Resource type. Generally Resources are not expected to ever have updated' +
271271
' props however in some limited circumstances it can be valid when changing the href.' +
272272
' 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' +
274274
' rel for this instance was "stylesheet". The updated rel is "author" and the updated href is "bar".',
275275
);
276276
expect(getVisibleChildren(document)).toEqual(
@@ -407,6 +407,97 @@ describe('ReactDOMFloat', () => {
407407
</html>,
408408
);
409409
});
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+
});
410501
});
411502

412503
describe('ReactDOM.preinit as style', () => {
@@ -2885,7 +2976,11 @@ describe('ReactDOMFloat', () => {
28852976
(mockError, scenarioNumber) => {
28862977
if (__DEV__) {
28872978
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+
),
28892984
);
28902985
} else {
28912986
expect(mockError).not.toHaveBeenCalled();
@@ -2898,7 +2993,7 @@ describe('ReactDOMFloat', () => {
28982993
(mockError, scenarioNumber) => {
28992994
if (__DEV__) {
29002995
expect(mockError.mock.calls[scenarioNumber]).toEqual(
2901-
makeArgs('null', '"style" and "font"', 'bar'),
2996+
makeArgs('null', '"style", "font", or "script"', 'bar'),
29022997
);
29032998
} else {
29042999
expect(mockError).not.toHaveBeenCalled();
@@ -2913,7 +3008,7 @@ describe('ReactDOMFloat', () => {
29133008
expect(mockError.mock.calls[scenarioNumber]).toEqual(
29143009
makeArgs(
29153010
'something with type "number"',
2916-
'"style" and "font"',
3011+
'"style", "font", or "script"',
29173012
'baz',
29183013
),
29193014
);
@@ -2930,7 +3025,7 @@ describe('ReactDOMFloat', () => {
29303025
expect(mockError.mock.calls[scenarioNumber]).toEqual(
29313026
makeArgs(
29323027
'something with type "object"',
2933-
'"style" and "font"',
3028+
'"style", "font", or "script"',
29343029
'qux',
29353030
),
29363031
);
@@ -2945,7 +3040,7 @@ describe('ReactDOMFloat', () => {
29453040
(mockError, scenarioNumber) => {
29463041
if (__DEV__) {
29473042
expect(mockError.mock.calls[scenarioNumber]).toEqual(
2948-
makeArgs('"bar"', '"style" and "font"', 'quux'),
3043+
makeArgs('"bar"', '"style", "font", or "script"', 'quux'),
29493044
);
29503045
} else {
29513046
expect(mockError).not.toHaveBeenCalled();

0 commit comments

Comments
 (0)