[Float] Support script preloads (#25432)

* support script preloads

* gates
This commit is contained in:
Josh Story 2022-10-05 09:47:35 -07:00 committed by GitHub
parent 65b3449c89
commit 618388bc32
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 147 additions and 51 deletions

View File

@ -29,7 +29,7 @@ import {getCurrentRootHostContainer} from 'react-reconciler/src/ReactFiberHostCo
// The resource types we support. currently they match the form for the as argument. // The resource types we support. currently they match the form for the as argument.
// In the future this may need to change, especially when modules / scripts are supported // In the future this may need to change, especially when modules / scripts are supported
type ResourceType = 'style' | 'font'; type ResourceType = 'style' | 'font' | 'script';
type PreloadProps = { type PreloadProps = {
rel: 'preload', rel: 'preload',
@ -150,7 +150,7 @@ function getDocumentFromRoot(root: FloatRoot): Document {
// ReactDOM.Preload // ReactDOM.Preload
// -------------------------------------- // --------------------------------------
type PreloadAs = ResourceType; type PreloadAs = ResourceType;
type PreloadOptions = {as: PreloadAs, crossOrigin?: string}; type PreloadOptions = {as: PreloadAs, crossOrigin?: string, integrity?: string};
function preload(href: string, options: PreloadOptions) { function preload(href: string, options: PreloadOptions) {
if (__DEV__) { if (__DEV__) {
validatePreloadArguments(href, options); validatePreloadArguments(href, options);
@ -194,6 +194,7 @@ function preloadPropsFromPreloadOptions(
rel: 'preload', rel: 'preload',
as, as,
crossOrigin: as === 'font' ? '' : options.crossOrigin, crossOrigin: as === 'font' ? '' : options.crossOrigin,
integrity: options.integrity,
}; };
} }
@ -832,7 +833,7 @@ export function isHostResourceType(type: string, props: Props): boolean {
} }
function isResourceAsType(as: mixed): boolean { function isResourceAsType(as: mixed): boolean {
return as === 'style' || as === 'font'; return as === 'style' || as === 'font' || as === 'script';
} }
// When passing user input into querySelector(All) the embedded string must not alter // When passing user input into querySelector(All) the embedded string must not alter

View File

@ -19,7 +19,7 @@ import {
type Props = {[string]: mixed}; type Props = {[string]: mixed};
type ResourceType = 'style' | 'font'; type ResourceType = 'style' | 'font' | 'script';
type PreloadProps = { type PreloadProps = {
rel: 'preload', rel: 'preload',
@ -123,7 +123,7 @@ export const ReactDOMServerDispatcher = {
}; };
type PreloadAs = ResourceType; type PreloadAs = ResourceType;
type PreloadOptions = {as: PreloadAs, crossOrigin?: string}; type PreloadOptions = {as: PreloadAs, crossOrigin?: string, integrity?: string};
function preload(href: string, options: PreloadOptions) { function preload(href: string, options: PreloadOptions) {
if (!currentResources) { if (!currentResources) {
// While we expect that preload calls are primarily going to be observed // While we expect that preload calls are primarily going to be observed
@ -248,6 +248,7 @@ function preloadPropsFromPreloadOptions(
rel: 'preload', rel: 'preload',
as, as,
crossOrigin: as === 'font' ? '' : options.crossOrigin, crossOrigin: as === 'font' ? '' : options.crossOrigin,
integrity: options.integrity,
}; };
} }
@ -526,6 +527,7 @@ export function resourcesFromLink(props: Props): boolean {
return false; return false;
} }
switch (as) { switch (as) {
case 'script':
case 'style': case 'style':
case 'font': { case 'font': {
if (__DEV__) { if (__DEV__) {

View File

@ -16,44 +16,42 @@ export function validateUnmatchedLinkResourceProps(
currentProps: ?Props, currentProps: ?Props,
) { ) {
if (__DEV__) { if (__DEV__) {
if (pendingProps.rel !== 'font' && pendingProps.rel !== 'style') { if (currentProps != null) {
if (currentProps != null) { const originalResourceName =
const originalResourceName = typeof currentProps.href === 'string'
typeof currentProps.href === 'string' ? `Resource with href "${currentProps.href}"`
? `Resource with href "${currentProps.href}"` : 'Resource';
: 'Resource'; const originalRelStatement = getValueDescriptorExpectingEnumForWarning(
const originalRelStatement = getValueDescriptorExpectingEnumForWarning( currentProps.rel,
currentProps.rel, );
); const pendingRelStatement = getValueDescriptorExpectingEnumForWarning(
const pendingRelStatement = getValueDescriptorExpectingEnumForWarning( pendingProps.rel,
pendingProps.rel, );
); const pendingHrefStatement =
const pendingHrefStatement = typeof pendingProps.href === 'string'
typeof pendingProps.href === 'string' ? ` and the updated href is "${pendingProps.href}"`
? ` and the updated href is "${pendingProps.href}"` : '';
: ''; console.error(
console.error( 'A <link> previously rendered as a %s but was updated with a rel type that is not' +
'A <link> previously rendered as a %s but was updated with a rel type that is not' + ' valid for a Resource type. Generally Resources are not expected to ever have updated' +
' valid for a Resource type. Generally Resources are not expected to ever have updated' + ' props however in some limited circumstances it can be valid when changing the href.' +
' props however in some limited circumstances it can be valid when changing the href.' + ' When React encounters props that invalidate the Resource it is the same as not rendering' +
' When React encounters props that invalidate the Resource it is the same as not rendering' + ' a Resource at all. valid rel types for Resources are "stylesheet" and "preload". The previous' +
' a Resource at all. valid rel types for Resources are "font" and "style". The previous' + ' rel for this instance was %s. The updated rel is %s%s.',
' rel for this instance was %s. The updated rel is %s%s.', originalResourceName,
originalResourceName, originalRelStatement,
originalRelStatement, pendingRelStatement,
pendingRelStatement, pendingHrefStatement,
pendingHrefStatement, );
); } else {
} else { const pendingRelStatement = getValueDescriptorExpectingEnumForWarning(
const pendingRelStatement = getValueDescriptorExpectingEnumForWarning( pendingProps.rel,
pendingProps.rel, );
); console.error(
console.error( 'A <link> is rendering as a Resource but has an invalid rel property. The rel encountered is %s.' +
'A <link> is rendering as a Resource but has an invalid rel property. The rel encountered is %s.' + ' This is a bug in React.',
' This is a bug in React.', pendingRelStatement,
pendingRelStatement, );
);
}
} }
} }
} }
@ -517,6 +515,7 @@ export function validatePreloadArguments(href: mixed, options: mixed) {
} }
break; break;
} }
case 'script':
case 'style': { case 'style': {
break; break;
} }
@ -529,7 +528,7 @@ export function validatePreloadArguments(href: mixed, options: mixed) {
' Please use one of the following valid values instead: %s. The href for the preload call where this' + ' Please use one of the following valid values instead: %s. The href for the preload call where this' +
' warning originated is "%s".', ' warning originated is "%s".',
typeOfAs, typeOfAs,
'"style" and "font"', '"style", "font", or "script"',
href, href,
); );
} }
@ -557,7 +556,6 @@ export function validatePreinitArguments(href: mixed, options: mixed) {
} else { } else {
const as = options.as; const as = options.as;
switch (as) { switch (as) {
case 'font':
case 'style': { case 'style': {
break; break;
} }

View File

@ -270,7 +270,7 @@ describe('ReactDOMFloat', () => {
' valid for a Resource type. Generally Resources are not expected to ever have updated' + ' valid for a Resource type. Generally Resources are not expected to ever have updated' +
' props however in some limited circumstances it can be valid when changing the href.' + ' props however in some limited circumstances it can be valid when changing the href.' +
' When React encounters props that invalidate the Resource it is the same as not rendering' + ' When React encounters props that invalidate the Resource it is the same as not rendering' +
' a Resource at all. valid rel types for Resources are "font" and "style". The previous' + ' a Resource at all. valid rel types for Resources are "stylesheet" and "preload". The previous' +
' rel for this instance was "stylesheet". The updated rel is "author" and the updated href is "bar".', ' rel for this instance was "stylesheet". The updated rel is "author" and the updated href is "bar".',
); );
expect(getVisibleChildren(document)).toEqual( expect(getVisibleChildren(document)).toEqual(
@ -407,6 +407,97 @@ describe('ReactDOMFloat', () => {
</html>, </html>,
); );
}); });
// @gate enableFloat
it('supports script preloads', async () => {
function ServerApp() {
ReactDOM.preload('foo', {as: 'script', integrity: 'foo hash'});
ReactDOM.preload('bar', {
as: 'script',
crossOrigin: 'use-credentials',
integrity: 'bar hash',
});
return (
<html>
<link rel="preload" href="baz" as="script" />
<head>
<title>hi</title>
</head>
<body>foo</body>
</html>
);
}
function ClientApp() {
ReactDOM.preload('foo', {as: 'script', integrity: 'foo hash'});
ReactDOM.preload('qux', {as: 'script'});
return (
<html>
<head>
<title>hi</title>
</head>
<body>foo</body>
<link
rel="preload"
href="quux"
as="script"
crossOrigin=""
integrity="quux hash"
/>
</html>
);
}
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<ServerApp />);
pipe(writable);
});
expect(getVisibleChildren(document)).toEqual(
<html>
<head>
<link rel="preload" as="script" href="foo" integrity="foo hash" />
<link
rel="preload"
as="script"
href="bar"
crossorigin="use-credentials"
integrity="bar hash"
/>
<link rel="preload" as="script" href="baz" />
<title>hi</title>
</head>
<body>foo</body>
</html>,
);
ReactDOMClient.hydrateRoot(document, <ClientApp />);
expect(Scheduler).toFlushWithoutYielding();
expect(getVisibleChildren(document)).toEqual(
<html>
<head>
<link rel="preload" as="script" href="foo" integrity="foo hash" />
<link
rel="preload"
as="script"
href="bar"
crossorigin="use-credentials"
integrity="bar hash"
/>
<link rel="preload" as="script" href="baz" />
<title>hi</title>
<link rel="preload" as="script" href="qux" />
<link
rel="preload"
as="script"
href="quux"
crossorigin=""
integrity="quux hash"
/>
</head>
<body>foo</body>
</html>,
);
});
}); });
describe('ReactDOM.preinit as style', () => { describe('ReactDOM.preinit as style', () => {
@ -2885,7 +2976,11 @@ describe('ReactDOMFloat', () => {
(mockError, scenarioNumber) => { (mockError, scenarioNumber) => {
if (__DEV__) { if (__DEV__) {
expect(mockError.mock.calls[scenarioNumber]).toEqual( expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs('undefined', '"style" and "font"', 'foo'), makeArgs(
'undefined',
'"style", "font", or "script"',
'foo',
),
); );
} else { } else {
expect(mockError).not.toHaveBeenCalled(); expect(mockError).not.toHaveBeenCalled();
@ -2898,7 +2993,7 @@ describe('ReactDOMFloat', () => {
(mockError, scenarioNumber) => { (mockError, scenarioNumber) => {
if (__DEV__) { if (__DEV__) {
expect(mockError.mock.calls[scenarioNumber]).toEqual( expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs('null', '"style" and "font"', 'bar'), makeArgs('null', '"style", "font", or "script"', 'bar'),
); );
} else { } else {
expect(mockError).not.toHaveBeenCalled(); expect(mockError).not.toHaveBeenCalled();
@ -2913,7 +3008,7 @@ describe('ReactDOMFloat', () => {
expect(mockError.mock.calls[scenarioNumber]).toEqual( expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs( makeArgs(
'something with type "number"', 'something with type "number"',
'"style" and "font"', '"style", "font", or "script"',
'baz', 'baz',
), ),
); );
@ -2930,7 +3025,7 @@ describe('ReactDOMFloat', () => {
expect(mockError.mock.calls[scenarioNumber]).toEqual( expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs( makeArgs(
'something with type "object"', 'something with type "object"',
'"style" and "font"', '"style", "font", or "script"',
'qux', 'qux',
), ),
); );
@ -2945,7 +3040,7 @@ describe('ReactDOMFloat', () => {
(mockError, scenarioNumber) => { (mockError, scenarioNumber) => {
if (__DEV__) { if (__DEV__) {
expect(mockError.mock.calls[scenarioNumber]).toEqual( expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs('"bar"', '"style" and "font"', 'quux'), makeArgs('"bar"', '"style", "font", or "script"', 'quux'),
); );
} else { } else {
expect(mockError).not.toHaveBeenCalled(); expect(mockError).not.toHaveBeenCalled();