[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.
// 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 = {
rel: 'preload',
@ -150,7 +150,7 @@ function getDocumentFromRoot(root: FloatRoot): Document {
// ReactDOM.Preload
// --------------------------------------
type PreloadAs = ResourceType;
type PreloadOptions = {as: PreloadAs, crossOrigin?: string};
type PreloadOptions = {as: PreloadAs, crossOrigin?: string, integrity?: string};
function preload(href: string, options: PreloadOptions) {
if (__DEV__) {
validatePreloadArguments(href, options);
@ -194,6 +194,7 @@ function preloadPropsFromPreloadOptions(
rel: 'preload',
as,
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 {
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

View File

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

View File

@ -16,7 +16,6 @@ export function validateUnmatchedLinkResourceProps(
currentProps: ?Props,
) {
if (__DEV__) {
if (pendingProps.rel !== 'font' && pendingProps.rel !== 'style') {
if (currentProps != null) {
const originalResourceName =
typeof currentProps.href === 'string'
@ -37,7 +36,7 @@ export function validateUnmatchedLinkResourceProps(
' 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.' +
' 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 %s. The updated rel is %s%s.',
originalResourceName,
originalRelStatement,
@ -56,7 +55,6 @@ export function validateUnmatchedLinkResourceProps(
}
}
}
}
export function validatePreloadResourceDifference(
originalProps: any,
@ -517,6 +515,7 @@ export function validatePreloadArguments(href: mixed, options: mixed) {
}
break;
}
case 'script':
case 'style': {
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' +
' warning originated is "%s".',
typeOfAs,
'"style" and "font"',
'"style", "font", or "script"',
href,
);
}
@ -557,7 +556,6 @@ export function validatePreinitArguments(href: mixed, options: mixed) {
} else {
const as = options.as;
switch (as) {
case 'font':
case 'style': {
break;
}

View File

@ -270,7 +270,7 @@ describe('ReactDOMFloat', () => {
' 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.' +
' 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".',
);
expect(getVisibleChildren(document)).toEqual(
@ -407,6 +407,97 @@ describe('ReactDOMFloat', () => {
</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', () => {
@ -2885,7 +2976,11 @@ describe('ReactDOMFloat', () => {
(mockError, scenarioNumber) => {
if (__DEV__) {
expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs('undefined', '"style" and "font"', 'foo'),
makeArgs(
'undefined',
'"style", "font", or "script"',
'foo',
),
);
} else {
expect(mockError).not.toHaveBeenCalled();
@ -2898,7 +2993,7 @@ describe('ReactDOMFloat', () => {
(mockError, scenarioNumber) => {
if (__DEV__) {
expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs('null', '"style" and "font"', 'bar'),
makeArgs('null', '"style", "font", or "script"', 'bar'),
);
} else {
expect(mockError).not.toHaveBeenCalled();
@ -2913,7 +3008,7 @@ describe('ReactDOMFloat', () => {
expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs(
'something with type "number"',
'"style" and "font"',
'"style", "font", or "script"',
'baz',
),
);
@ -2930,7 +3025,7 @@ describe('ReactDOMFloat', () => {
expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs(
'something with type "object"',
'"style" and "font"',
'"style", "font", or "script"',
'qux',
),
);
@ -2945,7 +3040,7 @@ describe('ReactDOMFloat', () => {
(mockError, scenarioNumber) => {
if (__DEV__) {
expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs('"bar"', '"style" and "font"', 'quux'),
makeArgs('"bar"', '"style", "font", or "script"', 'quux'),
);
} else {
expect(mockError).not.toHaveBeenCalled();