From 618388bc323356a5a9c0a6f162f516d35a3b4b2d Mon Sep 17 00:00:00 2001 From: Josh Story Date: Wed, 5 Oct 2022 09:47:35 -0700 Subject: [PATCH] [Float] Support script preloads (#25432) * support script preloads * gates --- .../src/client/ReactDOMFloatClient.js | 7 +- .../src/server/ReactDOMFloatServer.js | 6 +- .../src/shared/ReactDOMResourceValidation.js | 78 +++++++------ .../src/__tests__/ReactDOMFloat-test.js | 107 +++++++++++++++++- 4 files changed, 147 insertions(+), 51 deletions(-) diff --git a/packages/react-dom-bindings/src/client/ReactDOMFloatClient.js b/packages/react-dom-bindings/src/client/ReactDOMFloatClient.js index cf803261ab92f..5bcdfcd8d189d 100644 --- a/packages/react-dom-bindings/src/client/ReactDOMFloatClient.js +++ b/packages/react-dom-bindings/src/client/ReactDOMFloatClient.js @@ -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 diff --git a/packages/react-dom-bindings/src/server/ReactDOMFloatServer.js b/packages/react-dom-bindings/src/server/ReactDOMFloatServer.js index 59c77ee6f781e..b8f541b5250b5 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMFloatServer.js +++ b/packages/react-dom-bindings/src/server/ReactDOMFloatServer.js @@ -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__) { diff --git a/packages/react-dom-bindings/src/shared/ReactDOMResourceValidation.js b/packages/react-dom-bindings/src/shared/ReactDOMResourceValidation.js index 111803bde4d01..aaad9e9fbbc62 100644 --- a/packages/react-dom-bindings/src/shared/ReactDOMResourceValidation.js +++ b/packages/react-dom-bindings/src/shared/ReactDOMResourceValidation.js @@ -16,44 +16,42 @@ export function validateUnmatchedLinkResourceProps( currentProps: ?Props, ) { if (__DEV__) { - if (pendingProps.rel !== 'font' && pendingProps.rel !== 'style') { - if (currentProps != null) { - const originalResourceName = - typeof currentProps.href === 'string' - ? `Resource with href "${currentProps.href}"` - : 'Resource'; - const originalRelStatement = getValueDescriptorExpectingEnumForWarning( - currentProps.rel, - ); - const pendingRelStatement = getValueDescriptorExpectingEnumForWarning( - pendingProps.rel, - ); - const pendingHrefStatement = - typeof pendingProps.href === 'string' - ? ` and the updated href is "${pendingProps.href}"` - : ''; - console.error( - 'A 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' + - ' 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' + - ' rel for this instance was %s. The updated rel is %s%s.', - originalResourceName, - originalRelStatement, - pendingRelStatement, - pendingHrefStatement, - ); - } else { - const pendingRelStatement = getValueDescriptorExpectingEnumForWarning( - pendingProps.rel, - ); - console.error( - 'A is rendering as a Resource but has an invalid rel property. The rel encountered is %s.' + - ' This is a bug in React.', - pendingRelStatement, - ); - } + if (currentProps != null) { + const originalResourceName = + typeof currentProps.href === 'string' + ? `Resource with href "${currentProps.href}"` + : 'Resource'; + const originalRelStatement = getValueDescriptorExpectingEnumForWarning( + currentProps.rel, + ); + const pendingRelStatement = getValueDescriptorExpectingEnumForWarning( + pendingProps.rel, + ); + const pendingHrefStatement = + typeof pendingProps.href === 'string' + ? ` and the updated href is "${pendingProps.href}"` + : ''; + console.error( + 'A 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' + + ' 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 "stylesheet" and "preload". The previous' + + ' rel for this instance was %s. The updated rel is %s%s.', + originalResourceName, + originalRelStatement, + pendingRelStatement, + pendingHrefStatement, + ); + } else { + const pendingRelStatement = getValueDescriptorExpectingEnumForWarning( + pendingProps.rel, + ); + console.error( + 'A is rendering as a Resource but has an invalid rel property. The rel encountered is %s.' + + ' This is a bug in React.', + pendingRelStatement, + ); } } } @@ -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; } diff --git a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js index 6c45c68839a14..8c5fafdb71c63 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js @@ -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', () => { , ); }); + + // @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 ( + + + + hi + + foo + + ); + } + function ClientApp() { + ReactDOM.preload('foo', {as: 'script', integrity: 'foo hash'}); + ReactDOM.preload('qux', {as: 'script'}); + return ( + + + hi + + foo + + + ); + } + + await actIntoEmptyDocument(() => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); + pipe(writable); + }); + expect(getVisibleChildren(document)).toEqual( + + + + + + hi + + foo + , + ); + + ReactDOMClient.hydrateRoot(document, ); + expect(Scheduler).toFlushWithoutYielding(); + + expect(getVisibleChildren(document)).toEqual( + + + + + + hi + + + + foo + , + ); + }); }); 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();