From 8a780a4b283540a7ca50926e89ee27ac9a2bf0b1 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Sun, 10 Sep 2023 15:13:23 -0700 Subject: [PATCH] When Float was first developed the internal implementation and external interface were the same. This is problematic for a few reasons. One, the public interface is typed but it is also untrusted and we should not assume that it is actually respected. Two, the internal implementations can get called from places other than the the public interface and having to construct an options argument that ends up being destructured to process the request is computationally wasteful and may limit JIT optimizations to some degree. Lastly, the wire format was not as compressed as it could be and it was untyped. This refactor aims to address that by separating the public interface from the internal implementations so we can solve these challenges and also make it easier to change Float in the future The internal dispatcher method preinit is now preinitStyle and preinitScript. The internal dispatcher method preinitModule is now preinitModuleScript in anticipation of different implementations for other module types in the future The wire format is explicitly typed and only includes options if they are actually used omitting undefined and nulls. Some function arguments are not options even if they are optional. For instance precedence can be null/undefined because we deafult it to 'default' however we don't cosnider this an option because it is not something we transparently apply as props to the underlying instance. Fixes a problem with keying images in flight wehre srcset and sizes were not being taken into account. Moves argument validation into the ReactDOMFloat file where it is shared with all runtimes that expose these methods --- .../react-client/src/ReactFlightClient.js | 13 +- .../src/client/ReactFiberConfigDOM.js | 613 ++++++------------ .../ReactDOMFlightServerHostDispatcher.js | 177 +++-- .../src/server/ReactFizzConfigDOM.js | 518 +++++---------- .../src/server/ReactFlightServerConfigDOM.js | 60 +- .../src/shared/ReactDOMResourceValidation.js | 40 -- .../src/shared/ReactFlightClientConfigDOM.js | 99 ++- .../src/__tests__/ReactDOMFloat-test.js | 2 - .../react-dom-server-rendering-stub-test.js | 2 +- .../react-dom/src/shared/ReactDOMFloat.js | 311 ++++++++- .../react-dom/src/shared/ReactDOMTypes.js | 54 +- .../src/__tests__/ReactFlightDOM-test.js | 44 +- .../__tests__/ReactFlightDOMBrowser-test.js | 2 +- .../react-server/src/ReactFlightServer.js | 13 +- .../forks/ReactFlightServerConfig.custom.js | 4 +- 15 files changed, 974 insertions(+), 978 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 0b9d6b9f48011..cfbe30e671c78 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -17,7 +17,10 @@ import type { StringDecoder, } from './ReactFlightClientConfig'; -import type {HintModel} from 'react-server/src/ReactFlightServerConfig'; +import type { + HintCode, + HintModel, +} from 'react-server/src/ReactFlightServerConfig'; import type {CallServerCallback} from './ReactFlightReplyClient'; @@ -915,12 +918,12 @@ function resolvePostponeDev( } } -function resolveHint( +function resolveHint( response: Response, - code: string, + code: Code, model: UninitializedModel, ): void { - const hintModel: HintModel = parseModel(response, model); + const hintModel: HintModel = parseModel(response, model); dispatchHint(code, hintModel); } @@ -1044,7 +1047,7 @@ function processFullRow( return; } case 72 /* "H" */: { - const code = row[0]; + const code: HintCode = (row[0]: any); resolveHint(response, code, row.slice(1)); return; } diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index dafc4ff17eb22..d526c4643ac3a 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -20,12 +20,12 @@ import type {ReactScopeInstance} from 'shared/ReactTypes'; import type {AncestorInfoDev} from './validateDOMNesting'; import type {FormStatus} from 'react-dom-bindings/src/shared/ReactDOMFormActions'; import type { - PrefetchDNSOptions, - PreconnectOptions, - PreloadOptions, - PreloadModuleOptions, - PreinitOptions, - PreinitModuleOptions, + CrossOriginEnum, + PreloadImplOptions, + PreloadModuleImplOptions, + PreinitStyleOptions, + PreinitScriptOptions, + PreinitModuleScriptOptions, } from 'react-dom/src/shared/ReactDOMTypes'; import {NotPending} from 'react-dom-bindings/src/shared/ReactDOMFormActions'; @@ -107,12 +107,7 @@ import { HostSingleton, } from 'react-reconciler/src/ReactWorkTags'; import {listenToAllSupportedEvents} from '../events/DOMPluginEventSystem'; -import { - validatePreinitArguments, - validateLinkPropsForStyleResource, - getValueDescriptorExpectingObjectForWarning, - getValueDescriptorExpectingEnumForWarning, -} from '../shared/ReactDOMResourceValidation'; +import {validateLinkPropsForStyleResource} from '../shared/ReactDOMResourceValidation'; import escapeSelectorAttributeValueInsideDoubleQuotes from './escapeSelectorAttributeValueInsideDoubleQuotes'; export type Type = string; @@ -2112,8 +2107,9 @@ export const ReactDOMClientDispatcher: HostDispatcher = { preconnect, preload, preloadModule, - preinit, - preinitModule, + preinitStyle, + preinitScript, + preinitModuleScript, }; // We expect this to get inlined. It is a function mostly to communicate the special nature of @@ -2128,8 +2124,8 @@ function getDocumentForImperativeFloatMethods(): Document { function preconnectAs( rel: 'preconnect' | 'dns-prefetch', - crossOrigin: null | '' | 'use-credentials', href: string, + crossOrigin: ?CrossOriginEnum, ) { const ownerDocument = getDocumentForImperativeFloatMethods(); if (typeof href === 'string' && href) { @@ -2153,119 +2149,37 @@ function preconnectAs( } } -function prefetchDNS(href: string, options?: ?PrefetchDNSOptions) { +function prefetchDNS(href: string) { if (!enableFloat) { return; } - if (__DEV__) { - if (typeof href !== 'string' || !href) { - console.error( - 'ReactDOM.prefetchDNS(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.', - getValueDescriptorExpectingObjectForWarning(href), - ); - } else if (options != null) { - if ( - typeof options === 'object' && - hasOwnProperty.call(options, 'crossOrigin') - ) { - console.error( - 'ReactDOM.prefetchDNS(): Expected only one argument, `href`, but encountered %s as a second argument instead. This argument is reserved for future options and is currently disallowed. It looks like the you are attempting to set a crossOrigin property for this DNS lookup hint. Browsers do not perform DNS queries using CORS and setting this attribute on the resource hint has no effect. Try calling ReactDOM.prefetchDNS() with just a single string argument, `href`.', - getValueDescriptorExpectingEnumForWarning(options), - ); - } else { - console.error( - 'ReactDOM.prefetchDNS(): Expected only one argument, `href`, but encountered %s as a second argument instead. This argument is reserved for future options and is currently disallowed. Try calling ReactDOM.prefetchDNS() with just a single string argument, `href`.', - getValueDescriptorExpectingEnumForWarning(options), - ); - } - } - } - preconnectAs('dns-prefetch', null, href); + preconnectAs('dns-prefetch', href, null); } -function preconnect(href: string, options?: ?PreconnectOptions) { +function preconnect(href: string, crossOrigin?: ?CrossOriginEnum) { if (!enableFloat) { return; } - if (__DEV__) { - if (typeof href !== 'string' || !href) { - console.error( - 'ReactDOM.preconnect(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.', - getValueDescriptorExpectingObjectForWarning(href), - ); - } else if (options != null && typeof options !== 'object') { - console.error( - 'ReactDOM.preconnect(): Expected the `options` argument (second) to be an object but encountered %s instead. The only supported option at this time is `crossOrigin` which accepts a string.', - getValueDescriptorExpectingEnumForWarning(options), - ); - } else if (options != null && typeof options.crossOrigin !== 'string') { - console.error( - 'ReactDOM.preconnect(): Expected the `crossOrigin` option (second argument) to be a string but encountered %s instead. Try removing this option or passing a string value instead.', - getValueDescriptorExpectingObjectForWarning(options.crossOrigin), - ); - } - } - const crossOrigin = - options == null || typeof options.crossOrigin !== 'string' - ? null - : options.crossOrigin === 'use-credentials' - ? 'use-credentials' - : ''; - preconnectAs('preconnect', crossOrigin, href); + preconnectAs('preconnect', href, crossOrigin); } -function preload(href: string, options: PreloadOptions) { +function preload(href: string, as: string, options?: ?PreloadImplOptions) { if (!enableFloat) { return; } - if (__DEV__) { - // TODO move this to ReactDOMFloat and expose a stricter function interface or possibly - // typed functions (preloadImage, preloadStyle, ...) - let encountered = ''; - if (typeof href !== 'string' || !href) { - encountered += `The \`href\` argument encountered was ${getValueDescriptorExpectingObjectForWarning( - href, - )}.`; - } - if (options == null || typeof options !== 'object') { - encountered += `The \`options\` argument encountered was ${getValueDescriptorExpectingObjectForWarning( - options, - )}.`; - } else if (typeof options.as !== 'string' || !options.as) { - encountered += `The \`as\` option encountered was ${getValueDescriptorExpectingObjectForWarning( - options.as, - )}.`; - } - if (encountered) { - console.error( - 'ReactDOM.preload(): Expected two arguments, a non-empty `href` string and an `options` object with an `as` property valid for a `` tag. %s', - encountered, - ); - } - } const ownerDocument = getDocumentForImperativeFloatMethods(); - if ( - typeof href === 'string' && - href && - typeof options === 'object' && - options !== null && - typeof options.as === 'string' && - options.as && - ownerDocument - ) { - const as = options.as; + if (href && as && ownerDocument) { let preloadSelector = `link[rel="preload"][as="${escapeSelectorAttributeValueInsideDoubleQuotes( as, )}"]`; if (as === 'image') { - const {imageSrcSet, imageSizes} = options; - if (typeof imageSrcSet === 'string' && imageSrcSet !== '') { + if (options && options.imageSrcSet) { preloadSelector += `[imagesrcset="${escapeSelectorAttributeValueInsideDoubleQuotes( - imageSrcSet, + options.imageSrcSet, )}"]`; - if (typeof imageSizes === 'string') { + if (typeof options.imageSizes === 'string') { preloadSelector += `[imagesizes="${escapeSelectorAttributeValueInsideDoubleQuotes( - imageSizes, + options.imageSizes, )}"]`; } } else { @@ -2291,7 +2205,19 @@ function preload(href: string, options: PreloadOptions) { break; } if (!preloadPropsMap.has(key)) { - const preloadProps = preloadPropsFromPreloadOptions(href, as, options); + const preloadProps = Object.assign( + ({ + rel: 'preload', + // There is a bug in Safari where imageSrcSet is not respected on preload links + // so we omit the href here if we have imageSrcSet b/c safari will load the wrong image. + // This harms older browers that do not support imageSrcSet by making their preloads not work + // but this population is shrinking fast and is already small so we accept this tradeoff. + href: + as === 'image' && options && options.imageSrcSet ? undefined : href, + as, + }: PreloadProps), + options, + ); preloadPropsMap.set(key, preloadProps); if (null === ownerDocument.querySelector(preloadSelector)) { @@ -2317,35 +2243,12 @@ function preload(href: string, options: PreloadOptions) { } } -function preloadModule(href: string, options?: ?PreloadModuleOptions) { +function preloadModule(href: string, options?: ?PreloadModuleImplOptions) { if (!enableFloat) { return; } - if (__DEV__) { - let encountered = ''; - if (typeof href !== 'string' || !href) { - encountered += ` The \`href\` argument encountered was ${getValueDescriptorExpectingObjectForWarning( - href, - )}.`; - } - if (options !== undefined && typeof options !== 'object') { - encountered += ` The \`options\` argument encountered was ${getValueDescriptorExpectingObjectForWarning( - options, - )}.`; - } else if (options && 'as' in options && typeof options.as !== 'string') { - encountered += ` The \`as\` option encountered was ${getValueDescriptorExpectingObjectForWarning( - options.as, - )}.`; - } - if (encountered) { - console.error( - 'ReactDOM.preloadModule(): Expected two arguments, a non-empty `href` string and, optionally, an `options` object with an `as` property valid for a `` tag.%s', - encountered, - ); - } - } const ownerDocument = getDocumentForImperativeFloatMethods(); - if (typeof href === 'string' && href) { + if (href) { const as = options && typeof options.as === 'string' ? options.as : 'script'; const preloadSelector = `link[rel="modulepreload"][as="${escapeSelectorAttributeValueInsideDoubleQuotes( @@ -2368,12 +2271,14 @@ function preloadModule(href: string, options?: ?PreloadModuleOptions) { } if (!preloadPropsMap.has(key)) { - const preloadProps = preloadModulePropsFromPreloadModuleOptions( - href, - as, + const props: PreloadModuleProps = Object.assign( + ({ + rel: 'modulepreload', + href, + }: PreloadModuleProps), options, ); - preloadPropsMap.set(key, preloadProps); + preloadPropsMap.set(key, props); if (null === ownerDocument.querySelector(preloadSelector)) { switch (as) { @@ -2389,7 +2294,7 @@ function preloadModule(href: string, options?: ?PreloadModuleOptions) { } } const instance = ownerDocument.createElement('link'); - setInitialProperties(instance, 'link', preloadProps); + setInitialProperties(instance, 'link', props); markNodeAsHoistable(instance); (ownerDocument.head: any).appendChild(instance); } @@ -2397,314 +2302,198 @@ function preloadModule(href: string, options?: ?PreloadModuleOptions) { } } -function preloadPropsFromPreloadOptions( +function preinitStyle( href: string, - as: string, - options: PreloadOptions, -): PreloadProps { - return { - rel: 'preload', - as, - // There is a bug in Safari where imageSrcSet is not respected on preload links - // so we omit the href here if we have imageSrcSet b/c safari will load the wrong image. - // This harms older browers that do not support imageSrcSet by making their preloads not work - // but this population is shrinking fast and is already small so we accept this tradeoff. - href: as === 'image' && options.imageSrcSet ? undefined : href, - crossOrigin: as === 'font' ? '' : options.crossOrigin, - integrity: options.integrity, - type: options.type, - nonce: options.nonce, - fetchPriority: options.fetchPriority, - imageSrcSet: options.imageSrcSet, - imageSizes: options.imageSizes, - referrerPolicy: options.referrerPolicy, - }; -} - -function preloadModulePropsFromPreloadModuleOptions( - href: string, - as: string, - options: ?PreloadModuleOptions, -): PreloadModuleProps { - return { - rel: 'modulepreload', - as: as !== 'script' ? as : undefined, - href, - crossOrigin: options ? options.crossOrigin : undefined, - integrity: options ? options.integrity : undefined, - }; -} - -function preinit(href: string, options: PreinitOptions) { + precedence: ?string, + options?: ?PreinitStyleOptions, +) { if (!enableFloat) { return; } - if (__DEV__) { - validatePreinitArguments(href, options); - } const ownerDocument = getDocumentForImperativeFloatMethods(); - if ( - typeof href === 'string' && - href && - typeof options === 'object' && - options !== null - ) { - const as = options.as; - - switch (as) { - case 'style': { - const styles = getResourcesFromRoot(ownerDocument).hoistableStyles; - - const key = getStyleKey(href); - const precedence = options.precedence || 'default'; + if (href) { + const styles = getResourcesFromRoot(ownerDocument).hoistableStyles; - // Check if this resource already exists - let resource = styles.get(key); - if (resource) { - // We can early return. The resource exists and there is nothing - // more to do - return; - } + const key = getStyleKey(href); + precedence = precedence || 'default'; - const state = { - loading: NotLoaded, - preload: null, - }; + // Check if this resource already exists + let resource = styles.get(key); + if (resource) { + // We can early return. The resource exists and there is nothing + // more to do + return; + } - // Attempt to hydrate instance from DOM - let instance: null | Instance = ownerDocument.querySelector( - getStylesheetSelectorFromKey(key), - ); - if (instance) { - state.loading = Loaded; - } else { - // Construct a new instance and insert it - const stylesheetProps = stylesheetPropsFromPreinitOptions( - href, - precedence, - options, - ); - const preloadProps = preloadPropsMap.get(key); - if (preloadProps) { - adoptPreloadPropsForStylesheet(stylesheetProps, preloadProps); - } - const link = (instance = ownerDocument.createElement('link')); - markNodeAsHoistable(link); - setInitialProperties(link, 'link', stylesheetProps); - - (link: any)._p = new Promise((resolve, reject) => { - link.onload = resolve; - link.onerror = reject; - }); - link.addEventListener('load', () => { - state.loading |= Loaded; - }); - link.addEventListener('error', () => { - state.loading |= Errored; - }); - - state.loading |= Inserted; - insertStylesheet(instance, precedence, ownerDocument); - } + const state = { + loading: NotLoaded, + preload: null, + }; - // Construct a Resource and cache it - resource = { - type: 'stylesheet', - instance, - count: 1, - state, - }; - styles.set(key, resource); - return; + // Attempt to hydrate instance from DOM + let instance: null | Instance = ownerDocument.querySelector( + getStylesheetSelectorFromKey(key), + ); + if (instance) { + state.loading = Loaded; + } else { + // Construct a new instance and insert it + const stylesheetProps = Object.assign( + ({ + rel: 'stylesheet', + href, + 'data-precedence': precedence, + }: StylesheetProps), + options, + ); + const preloadProps = preloadPropsMap.get(key); + if (preloadProps) { + adoptPreloadPropsForStylesheet(stylesheetProps, preloadProps); } - case 'script': { - const src = href; - const scripts = getResourcesFromRoot(ownerDocument).hoistableScripts; + const link = (instance = ownerDocument.createElement('link')); + markNodeAsHoistable(link); + setInitialProperties(link, 'link', stylesheetProps); - const key = getScriptKey(src); - - // Check if this resource already exists - let resource = scripts.get(key); - if (resource) { - // We can early return. The resource exists and there is nothing - // more to do - return; - } - - // Attempt to hydrate instance from DOM - let instance: null | Instance = ownerDocument.querySelector( - getScriptSelectorFromKey(key), - ); - if (!instance) { - // Construct a new instance and insert it - const scriptProps = scriptPropsFromPreinitOptions(src, options); - // Adopt certain preload props - const preloadProps = preloadPropsMap.get(key); - if (preloadProps) { - adoptPreloadPropsForScript(scriptProps, preloadProps); - } - instance = ownerDocument.createElement('script'); - markNodeAsHoistable(instance); - setInitialProperties(instance, 'link', scriptProps); - (ownerDocument.head: any).appendChild(instance); - } + (link: any)._p = new Promise((resolve, reject) => { + link.onload = resolve; + link.onerror = reject; + }); + link.addEventListener('load', () => { + state.loading |= Loaded; + }); + link.addEventListener('error', () => { + state.loading |= Errored; + }); - // Construct a Resource and cache it - resource = { - type: 'script', - instance, - count: 1, - state: null, - }; - scripts.set(key, resource); - return; - } + state.loading |= Inserted; + insertStylesheet(instance, precedence, ownerDocument); } + + // Construct a Resource and cache it + resource = { + type: 'stylesheet', + instance, + count: 1, + state, + }; + styles.set(key, resource); + return; } } -function preinitModule(href: string, options?: ?PreinitModuleOptions) { +function preinitScript(src: string, options?: ?PreinitScriptOptions) { if (!enableFloat) { return; } - if (__DEV__) { - let encountered = ''; - if (typeof href !== 'string' || !href) { - encountered += ` The \`href\` argument encountered was ${getValueDescriptorExpectingObjectForWarning( - href, - )}.`; + const ownerDocument = getDocumentForImperativeFloatMethods(); + + if (src) { + const scripts = getResourcesFromRoot(ownerDocument).hoistableScripts; + + const key = getScriptKey(src); + + // Check if this resource already exists + let resource = scripts.get(key); + if (resource) { + // We can early return. The resource exists and there is nothing + // more to do + return; } - if (options !== undefined && typeof options !== 'object') { - encountered += ` The \`options\` argument encountered was ${getValueDescriptorExpectingObjectForWarning( + + // Attempt to hydrate instance from DOM + let instance: null | Instance = ownerDocument.querySelector( + getScriptSelectorFromKey(key), + ); + if (!instance) { + // Construct a new instance and insert it + const scriptProps = Object.assign( + ({ + src, + async: true, + }: ScriptProps), options, - )}.`; - } else if (options && 'as' in options && options.as !== 'script') { - encountered += ` The \`as\` option encountered was ${getValueDescriptorExpectingEnumForWarning( - options.as, - )}.`; - } - if (encountered) { - console.error( - 'ReactDOM.preinitModule(): Expected up to two arguments, a non-empty `href` string and, optionally, an `options` object with a valid `as` property.%s', - encountered, ); - } else { - const as = - options && typeof options.as === 'string' ? options.as : 'script'; - switch (as) { - case 'script': { - break; - } - - // We have an invalid as type and need to warn - default: { - const typeOfAs = getValueDescriptorExpectingEnumForWarning(as); - console.error( - 'ReactDOM.preinitModule(): Currently the only supported "as" type for this function is "script"' + - ' but received "%s" instead. This warning was generated for `href` "%s". In the future other' + - ' module types will be supported, aligning with the import-attributes proposal. Learn more here:' + - ' (https://github.com/tc39/proposal-import-attributes)', - typeOfAs, - href, - ); - } + // Adopt certain preload props + const preloadProps = preloadPropsMap.get(key); + if (preloadProps) { + adoptPreloadPropsForScript(scriptProps, preloadProps); } + instance = ownerDocument.createElement('script'); + markNodeAsHoistable(instance); + setInitialProperties(instance, 'link', scriptProps); + (ownerDocument.head: any).appendChild(instance); } - } - const ownerDocument = getDocumentForImperativeFloatMethods(); - if (typeof href === 'string' && href) { - const as = - options && typeof options.as === 'string' ? options.as : 'script'; + // Construct a Resource and cache it + resource = { + type: 'script', + instance, + count: 1, + state: null, + }; + scripts.set(key, resource); + return; + } +} - switch (as) { - case 'script': { - const src = href; - const scripts = getResourcesFromRoot(ownerDocument).hoistableScripts; +function preinitModuleScript( + src: string, + options?: ?PreinitModuleScriptOptions, +) { + if (!enableFloat) { + return; + } + const ownerDocument = getDocumentForImperativeFloatMethods(); - const key = getScriptKey(src); + if (src) { + const scripts = getResourcesFromRoot(ownerDocument).hoistableScripts; - // Check if this resource already exists - let resource = scripts.get(key); - if (resource) { - // We can early return. The resource exists and there is nothing - // more to do - return; - } + const key = getScriptKey(src); - // Attempt to hydrate instance from DOM - let instance: null | Instance = ownerDocument.querySelector( - getScriptSelectorFromKey(key), - ); - if (!instance) { - // Construct a new instance and insert it - const scriptProps = modulePropsFromPreinitModuleOptions(src, options); - // Adopt certain preload props - const preloadProps = preloadPropsMap.get(key); - if (preloadProps) { - adoptPreloadPropsForScript(scriptProps, preloadProps); - } - instance = ownerDocument.createElement('script'); - markNodeAsHoistable(instance); - setInitialProperties(instance, 'link', scriptProps); - (ownerDocument.head: any).appendChild(instance); - } + // Check if this resource already exists + let resource = scripts.get(key); + if (resource) { + // We can early return. The resource exists and there is nothing + // more to do + return; + } - // Construct a Resource and cache it - resource = { - type: 'script', - instance, - count: 1, - state: null, - }; - scripts.set(key, resource); - return; + // Attempt to hydrate instance from DOM + let instance: null | Instance = ownerDocument.querySelector( + getScriptSelectorFromKey(key), + ); + if (!instance) { + // Construct a new instance and insert it + const scriptProps = Object.assign( + ({ + src, + async: true, + type: 'module', + }: ScriptProps), + options, + ); + // Adopt certain preload props + const preloadProps = preloadPropsMap.get(key); + if (preloadProps) { + adoptPreloadPropsForScript(scriptProps, preloadProps); } + instance = ownerDocument.createElement('script'); + markNodeAsHoistable(instance); + setInitialProperties(instance, 'link', scriptProps); + (ownerDocument.head: any).appendChild(instance); } - } -} - -function stylesheetPropsFromPreinitOptions( - href: string, - precedence: string, - options: PreinitOptions, -): StylesheetProps { - return { - rel: 'stylesheet', - href, - 'data-precedence': precedence, - crossOrigin: options.crossOrigin, - integrity: options.integrity, - fetchPriority: options.fetchPriority, - }; -} - -function scriptPropsFromPreinitOptions( - src: string, - options: PreinitOptions, -): ScriptProps { - return { - src, - async: true, - crossOrigin: options.crossOrigin, - integrity: options.integrity, - nonce: options.nonce, - fetchPriority: options.fetchPriority, - }; -} -function modulePropsFromPreinitModuleOptions( - src: string, - options: ?PreinitModuleOptions, -): ScriptProps { - return { - src, - async: true, - type: 'module', - crossOrigin: options ? options.crossOrigin : undefined, - integrity: options ? options.integrity : undefined, - }; + // Construct a Resource and cache it + resource = { + type: 'script', + instance, + count: 1, + state: null, + }; + scripts.set(key, resource); + return; + } } type StyleTagQualifyingProps = { diff --git a/packages/react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js b/packages/react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js index 5655a3ab80526..36913899bb030 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js +++ b/packages/react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js @@ -9,12 +9,12 @@ import type { HostDispatcher, - PrefetchDNSOptions, - PreconnectOptions, - PreloadOptions, - PreloadModuleOptions, - PreinitOptions, - PreinitModuleOptions, + CrossOriginEnum, + PreloadImplOptions, + PreloadModuleImplOptions, + PreinitStyleOptions, + PreinitScriptOptions, + PreinitModuleScriptOptions, } from 'react-dom/src/shared/ReactDOMTypes'; import {enableFloat} from 'shared/ReactFeatureFlags'; @@ -30,137 +30,232 @@ export const ReactDOMFlightServerDispatcher: HostDispatcher = { preconnect, preload, preloadModule, - preinit, - preinitModule, + preinitStyle, + preinitScript, + preinitModuleScript, }; -function prefetchDNS(href: string, options?: ?PrefetchDNSOptions) { +function prefetchDNS(href: string) { + if (enableFloat) { + if (typeof href === 'string' && href) { + const request = resolveRequest(); + if (request) { + const hints = getHints(request); + const key = 'D|' + href; + if (hints.has(key)) { + // duplicate hint + return; + } + hints.add(key); + emitHint(request, 'D', href); + } + } + } +} + +function preconnect(href: string, crossOrigin?: ?CrossOriginEnum) { if (enableFloat) { if (typeof href === 'string') { const request = resolveRequest(); if (request) { const hints = getHints(request); - const key = 'D' + href; + + const key = `C|${crossOrigin == null ? 'null' : crossOrigin}|${href}`; if (hints.has(key)) { // duplicate hint return; } hints.add(key); - if (options) { - emitHint(request, 'D', [href, options]); + if (typeof crossOrigin === 'string') { + emitHint(request, 'C', [href, crossOrigin]); } else { - emitHint(request, 'D', href); + emitHint(request, 'C', href); } } } } } -function preconnect(href: string, options?: ?PreconnectOptions) { +function preload(href: string, as: string, options?: ?PreloadImplOptions) { if (enableFloat) { if (typeof href === 'string') { const request = resolveRequest(); if (request) { const hints = getHints(request); - const crossOrigin = - options == null || typeof options.crossOrigin !== 'string' - ? null - : options.crossOrigin === 'use-credentials' - ? 'use-credentials' - : ''; - - const key = `C${crossOrigin === null ? 'null' : crossOrigin}|${href}`; + let key = 'L'; + if (as === 'image' && options) { + key += getImagePreloadKey( + href, + options.imageSrcSet, + options.imageSizes, + ); + } else { + key += `[${as}]${href}`; + } if (hints.has(key)) { // duplicate hint return; } hints.add(key); - if (options) { - emitHint(request, 'C', [href, options]); + + const trimmed = trimOptions(options); + if (trimmed) { + emitHint(request, 'L', [href, as, trimmed]); } else { - emitHint(request, 'C', href); + emitHint(request, 'L', [href, as]); } } } } } -function preload(href: string, options: PreloadOptions) { +function preloadModule(href: string, options?: ?PreloadModuleImplOptions) { if (enableFloat) { if (typeof href === 'string') { const request = resolveRequest(); if (request) { const hints = getHints(request); - const key = 'L' + href; + const key = 'm|' + href; if (hints.has(key)) { // duplicate hint return; } hints.add(key); - emitHint(request, 'L', [href, options]); + + const trimmed = trimOptions(options); + if (trimmed) { + return emitHint(request, 'm', [href, trimmed]); + } else { + return emitHint(request, 'm', href); + } } } } } -function preloadModule(href: string, options?: ?PreloadModuleOptions) { +function preinitStyle( + href: string, + precedence: ?string, + options?: ?PreinitStyleOptions, +) { if (enableFloat) { if (typeof href === 'string') { const request = resolveRequest(); if (request) { const hints = getHints(request); - const key = 'm' + href; + const key = 'S|' + href; if (hints.has(key)) { // duplicate hint return; } hints.add(key); - if (options) { - emitHint(request, 'm', [href, options]); + + const trimmed = trimOptions(options); + if (trimmed) { + return emitHint(request, 'S', [ + href, + typeof precedence === 'string' ? precedence : 0, + trimmed, + ]); + } else if (typeof precedence === 'string') { + return emitHint(request, 'S', [href, precedence]); } else { - emitHint(request, 'm', href); + return emitHint(request, 'S', href); } } } } } -function preinit(href: string, options: PreinitOptions) { +function preinitScript(href: string, options?: ?PreinitScriptOptions) { if (enableFloat) { if (typeof href === 'string') { const request = resolveRequest(); if (request) { const hints = getHints(request); - const key = 'I' + href; + const key = 'X|' + href; if (hints.has(key)) { // duplicate hint return; } hints.add(key); - emitHint(request, 'I', [href, options]); + + const trimmed = trimOptions(options); + if (trimmed) { + return emitHint(request, 'X', [href, trimmed]); + } else { + return emitHint(request, 'X', href); + } } } } } -function preinitModule(href: string, options?: ?PreinitModuleOptions) { +function preinitModuleScript( + href: string, + options?: ?PreinitModuleScriptOptions, +) { if (enableFloat) { if (typeof href === 'string') { const request = resolveRequest(); if (request) { const hints = getHints(request); - const key = 'M' + href; + const key = 'M|' + href; if (hints.has(key)) { // duplicate hint return; } hints.add(key); - if (options) { - emitHint(request, 'M', [href, options]); + + const trimmed = trimOptions(options); + if (trimmed) { + return emitHint(request, 'M', [href, trimmed]); } else { - emitHint(request, 'M', href); + return emitHint(request, 'M', href); } } } } } + +// Flight normally encodes undefined as a special character however for directive option +// arguments we don't want to send unnecessary keys and bloat the payload so we create a +// trimmed object which omits any keys with null or undefined values. +// This is only typesafe because these option objects have entirely optional fields where +// null and undefined represent the same thing as no property. +function trimOptions< + T: + | PreloadImplOptions + | PreloadModuleImplOptions + | PreinitStyleOptions + | PreinitScriptOptions + | PreinitModuleScriptOptions, +>(options: ?T): ?T { + if (options == null) return null; + let hasProperties = false; + const trimmed: T = ({}: any); + for (const key in options) { + if (options[key] != null) { + hasProperties = true; + (trimmed: any)[key] = options[key]; + } + } + return hasProperties ? trimmed : null; +} + +function getImagePreloadKey( + href: string, + imageSrcSet: ?string, + imageSizes: ?string, +) { + let uniquePart = ''; + if (typeof imageSrcSet === 'string' && imageSrcSet !== '') { + uniquePart += '[' + imageSrcSet + ']'; + if (typeof imageSizes === 'string') { + uniquePart += '[' + imageSizes + ']'; + } + } else { + uniquePart += '[][]' + href; + } + return `[image]${uniquePart}`; +} diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js index ee76a4925f9a7..3900a76c92cf4 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js @@ -9,12 +9,12 @@ import type {ReactNodeList, ReactCustomFormAction} from 'shared/ReactTypes'; import type { - PrefetchDNSOptions, - PreconnectOptions, - PreloadOptions, - PreloadModuleOptions, - PreinitOptions, - PreinitModuleOptions, + CrossOriginEnum, + PreloadImplOptions, + PreloadModuleImplOptions, + PreinitStyleOptions, + PreinitScriptOptions, + PreinitModuleScriptOptions, ImportMap, } from 'react-dom/src/shared/ReactDOMTypes'; @@ -79,10 +79,7 @@ import { formReplaying as formReplayingRuntime, } from './fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings'; -import { - getValueDescriptorExpectingObjectForWarning, - getValueDescriptorExpectingEnumForWarning, -} from '../shared/ReactDOMResourceValidation'; +import {getValueDescriptorExpectingObjectForWarning} from '../shared/ReactDOMResourceValidation'; import {NotPending} from '../shared/ReactDOMFormActions'; @@ -94,8 +91,9 @@ const ReactDOMServerDispatcher = { preconnect, preload, preloadModule, - preinit, - preinitModule, + preinitStyle, + preinitScript, + preinitModuleScript, }; export function prepareHostDispatcher() { @@ -5001,7 +4999,7 @@ function getResourceKey(as: string, href: string): string { return `[${as}]${href}`; } -function prefetchDNS(href: string, options?: ?PrefetchDNSOptions) { +function prefetchDNS(href: string) { if (!enableFloat) { return; } @@ -5015,29 +5013,6 @@ function prefetchDNS(href: string, options?: ?PrefetchDNSOptions) { return; } const resumableState = getResumableState(request); - if (__DEV__) { - if (typeof href !== 'string' || !href) { - console.error( - 'ReactDOM.prefetchDNS(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.', - getValueDescriptorExpectingObjectForWarning(href), - ); - } else if (options != null) { - if ( - typeof options === 'object' && - options.hasOwnProperty('crossOrigin') - ) { - console.error( - 'ReactDOM.prefetchDNS(): Expected only one argument, `href`, but encountered %s as a second argument instead. This argument is reserved for future options and is currently disallowed. It looks like the you are attempting to set a crossOrigin property for this DNS lookup hint. Browsers do not perform DNS queries using CORS and setting this attribute on the resource hint has no effect. Try calling ReactDOM.prefetchDNS() with just a single string argument, `href`.', - getValueDescriptorExpectingEnumForWarning(options), - ); - } else { - console.error( - 'ReactDOM.prefetchDNS(): Expected only one argument, `href`, but encountered %s as a second argument instead. This argument is reserved for future options and is currently disallowed. Try calling ReactDOM.prefetchDNS() with just a single string argument, `href`.', - getValueDescriptorExpectingEnumForWarning(options), - ); - } - } - } if (typeof href === 'string' && href) { const key = getResourceKey('prefetchDNS', href); @@ -5060,7 +5035,7 @@ function prefetchDNS(href: string, options?: ?PrefetchDNSOptions) { } } -function preconnect(href: string, options?: ?PreconnectOptions) { +function preconnect(href: string, crossOrigin: ?CrossOriginEnum) { if (!enableFloat) { return; } @@ -5074,35 +5049,10 @@ function preconnect(href: string, options?: ?PreconnectOptions) { return; } const resumableState = getResumableState(request); - if (__DEV__) { - if (typeof href !== 'string' || !href) { - console.error( - 'ReactDOM.preconnect(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.', - getValueDescriptorExpectingObjectForWarning(href), - ); - } else if (options != null && typeof options !== 'object') { - console.error( - 'ReactDOM.preconnect(): Expected the `options` argument (second) to be an object but encountered %s instead. The only supported option at this time is `crossOrigin` which accepts a string.', - getValueDescriptorExpectingEnumForWarning(options), - ); - } else if (options != null && typeof options.crossOrigin !== 'string') { - console.error( - 'ReactDOM.preconnect(): Expected the `crossOrigin` option (second argument) to be a string but encountered %s instead. Try removing this option or passing a string value instead.', - getValueDescriptorExpectingObjectForWarning(options.crossOrigin), - ); - } - } if (typeof href === 'string' && href) { - const crossOrigin = - options == null || typeof options.crossOrigin !== 'string' - ? null - : options.crossOrigin === 'use-credentials' - ? 'use-credentials' - : ''; - const key = `[preconnect][${ - crossOrigin === null ? 'null' : crossOrigin + typeof crossOrigin === 'string' ? crossOrigin : 'null' }]${href}`; let resource = resumableState.preconnectsMap.get(key); if (!resource) { @@ -5123,7 +5073,7 @@ function preconnect(href: string, options?: ?PreconnectOptions) { } } -function preload(href: string, options: PreloadOptions) { +function preload(href: string, as: string, options?: ?PreloadImplOptions) { if (!enableFloat) { return; } @@ -5137,62 +5087,39 @@ function preload(href: string, options: PreloadOptions) { return; } const resumableState = getResumableState(request); - if (__DEV__) { - let encountered = ''; - if (typeof href !== 'string' || !href) { - encountered += ` The \`href\` argument encountered was ${getValueDescriptorExpectingObjectForWarning( - href, - )}.`; - } - if (options == null || typeof options !== 'object') { - encountered += ` The \`options\` argument encountered was ${getValueDescriptorExpectingObjectForWarning( - options, - )}.`; - } else if (typeof options.as !== 'string' || !options.as) { - encountered += ` The \`as\` option encountered was ${getValueDescriptorExpectingObjectForWarning( - options.as, - )}.`; - } - if (encountered) { - console.error( - 'ReactDOM.preload(): Expected two arguments, a non-empty `href` string and an `options` object with an `as` property valid for a `` tag.%s', - encountered, - ); - } - } - if ( - typeof href === 'string' && - href && - typeof options === 'object' && - options !== null && - typeof options.as === 'string' && - options.as - ) { - const as = options.as; + if (as && href) { + options = options || {}; let key: string; if (as === 'image') { // For image preloads the key contains either the imageSrcSet + imageSizes or the href but not // both. This is to prevent identical calls with the same srcSet and sizes to be duplicated // by varying the href. this is an edge case but it is the most correct behavior. - const {imageSrcSet, imageSizes} = options; - key = getImagePreloadKey(href, imageSrcSet, imageSizes); + key = getImagePreloadKey(href, options.imageSrcSet, options.imageSizes); } else { key = getResourceKey(as, href); } let resource = resumableState.preloadsMap.get(key); if (!resource) { + const props = Object.assign( + ({ + rel: 'preload', + href: as === 'image' && options.imageSrcSet ? undefined : href, + as, + }: PreloadProps), + options, + ); resource = { type: 'preload', chunks: [], state: NoState, - props: preloadPropsFromPreloadOptions(href, as, options), + props, }; resumableState.preloadsMap.set(key, resource); pushLinkImpl(resource.chunks, resource.props); } if (as === 'font') { resumableState.fontPreloads.add(resource); - } else if (as === 'image' && options.fetchPriority === 'high') { + } else if (as === 'image' && resource.props.fetchPriority === 'high') { resumableState.highImagePreloads.add(resource); } else { resumableState.bulkPreloads.add(resource); @@ -5201,7 +5128,10 @@ function preload(href: string, options: PreloadOptions) { } } -function preloadModule(href: string, options?: ?PreloadModuleOptions): void { +function preloadModule( + href: string, + options?: ?PreloadModuleImplOptions, +): void { if (!enableFloat) { return; } @@ -5215,40 +5145,24 @@ function preloadModule(href: string, options?: ?PreloadModuleOptions): void { return; } const resumableState = getResumableState(request); - if (__DEV__) { - let encountered = ''; - if (typeof href !== 'string' || !href) { - encountered += ` The \`href\` argument encountered was ${getValueDescriptorExpectingObjectForWarning( - href, - )}.`; - } - if (options !== undefined && typeof options !== 'object') { - encountered += ` The \`options\` argument encountered was ${getValueDescriptorExpectingObjectForWarning( - options, - )}.`; - } else if (options && 'as' in options && typeof options.as !== 'string') { - encountered += ` The \`as\` option encountered was ${getValueDescriptorExpectingObjectForWarning( - options.as, - )}.`; - } - if (encountered) { - console.error( - 'ReactDOM.preloadModule(): Expected two arguments, a non-empty `href` string and, optionally, an `options` object with an `as` property valid for a `` tag.%s', - encountered, - ); - } - } - if (typeof href === 'string' && href) { + if (href) { const as = options && typeof options.as === 'string' ? options.as : 'script'; const key = getResourceKey(as, href); let resource = resumableState.preloadsMap.get(key); + const props: PreloadModuleProps = Object.assign( + ({ + rel: 'modulepreload', + href, + }: PreloadModuleProps), + options, + ); if (!resource) { resource = { type: 'preload', chunks: [], state: NoState, - props: preloadModulePropsFromPreloadModuleOptions(href, as, options), + props, }; resumableState.preloadsMap.set(key, resource); pushLinkImpl(resource.chunks, resource.props); @@ -5258,7 +5172,11 @@ function preloadModule(href: string, options?: ?PreloadModuleOptions): void { } } -function preinit(href: string, options: PreinitOptions): void { +function preinitStyle( + href: string, + precedence: ?string, + options?: ?PreinitStyleOptions, +): void { if (!enableFloat) { return; } @@ -5272,102 +5190,64 @@ function preinit(href: string, options: PreinitOptions): void { return; } const resumableState = getResumableState(request); - if (__DEV__) { - if (typeof href !== 'string' || !href) { - console.error( - 'ReactDOM.preinit(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.', - getValueDescriptorExpectingObjectForWarning(href), - ); - } else if (options == null || typeof options !== 'object') { - console.error( - 'ReactDOM.preinit(): Expected the `options` argument (second) to be an object with an `as` property describing the type of resource to be preinitialized but encountered %s instead.', - getValueDescriptorExpectingEnumForWarning(options), - ); - } else if (options.as !== 'style' && options.as !== 'script') { - console.error( - 'ReactDOM.preinit(): Expected the `as` property in the `options` argument (second) to contain a valid value describing the type of resource to be preinitialized but encountered %s instead. Valid values for `as` are "style" and "script".', - getValueDescriptorExpectingEnumForWarning(options.as), + if (href) { + const as = 'style'; + const key = getResourceKey(as, href); + let resource = resumableState.stylesMap.get(key); + if (!resource) { + precedence = precedence || 'default'; + let state = NoState; + const preloadResource = resumableState.preloadsMap.get(key); + if (preloadResource && preloadResource.state & Flushed) { + state = PreloadFlushed; + } + const props: StylesheetProps = Object.assign( + ({ + rel: 'stylesheet', + href, + 'data-precedence': precedence, + }: StylesheetProps), + options, ); - } - } - if ( - typeof href === 'string' && - href && - typeof options === 'object' && - options !== null - ) { - const as = options.as; - switch (as) { - case 'style': { - const key = getResourceKey(as, href); - let resource = resumableState.stylesMap.get(key); - const precedence = options.precedence || 'default'; - if (!resource) { - let state = NoState; - const preloadResource = resumableState.preloadsMap.get(key); - if (preloadResource && preloadResource.state & Flushed) { - state = PreloadFlushed; - } - resource = { - type: 'stylesheet', - chunks: ([]: Array), - state, - props: stylesheetPropsFromPreinitOptions(href, precedence, options), - }; - resumableState.stylesMap.set(key, resource); - let precedenceSet = resumableState.precedences.get(precedence); - if (!precedenceSet) { - precedenceSet = new Set(); - resumableState.precedences.set(precedence, precedenceSet); - const emptyStyleResource = { - type: 'style', - chunks: ([]: Array), - state: NoState, - props: { - precedence, - hrefs: ([]: Array), - }, - }; - precedenceSet.add(emptyStyleResource); - if (__DEV__) { - if (resumableState.stylePrecedences.has(precedence)) { - console.error( - 'React constructed an empty style resource when a style resource already exists for this precedence: "%s". This is a bug in React.', - precedence, - ); - } - } - resumableState.stylePrecedences.set(precedence, emptyStyleResource); + resource = { + type: 'stylesheet', + chunks: ([]: Array), + state, + props, + }; + resumableState.stylesMap.set(key, resource); + let precedenceSet = resumableState.precedences.get(precedence); + if (!precedenceSet) { + precedenceSet = new Set(); + resumableState.precedences.set(precedence, precedenceSet); + const emptyStyleResource = { + type: 'style', + chunks: ([]: Array), + state: NoState, + props: { + precedence, + hrefs: ([]: Array), + }, + }; + precedenceSet.add(emptyStyleResource); + if (__DEV__) { + if (resumableState.stylePrecedences.has(precedence)) { + console.error( + 'React constructed an empty style resource when a style resource already exists for this precedence: "%s". This is a bug in React.', + precedence, + ); } - precedenceSet.add(resource); - flushResources(request); } - return; - } - case 'script': { - const src = href; - const key = getResourceKey(as, src); - let resource = resumableState.scriptsMap.get(key); - if (!resource) { - resource = { - type: 'script', - chunks: [], - state: NoState, - props: null, - }; - resumableState.scriptsMap.set(key, resource); - const resourceProps = scriptPropsFromPreinitOptions(src, options); - resumableState.scripts.add(resource); - pushScriptImpl(resource.chunks, resourceProps); - flushResources(request); - } - return; + resumableState.stylePrecedences.set(precedence, emptyStyleResource); } + precedenceSet.add(resource); + flushResources(request); } + return; } } -function preinitModule(href: string, options?: ?PreinitModuleOptions): void { +function preinitScript(src: string, options?: ?PreinitScriptOptions): void { if (!enableFloat) { return; } @@ -5381,77 +5261,73 @@ function preinitModule(href: string, options?: ?PreinitModuleOptions): void { return; } const resumableState = getResumableState(request); - if (__DEV__) { - let encountered = ''; - if (typeof href !== 'string' || !href) { - encountered += ` The \`href\` argument encountered was ${getValueDescriptorExpectingObjectForWarning( - href, - )}.`; - } - if (options !== undefined && typeof options !== 'object') { - encountered += ` The \`options\` argument encountered was ${getValueDescriptorExpectingObjectForWarning( + if (src) { + const key = getResourceKey('script', src); + let resource = resumableState.scriptsMap.get(key); + if (!resource) { + resource = { + type: 'script', + chunks: [], + state: NoState, + props: null, + }; + resumableState.scriptsMap.set(key, resource); + const props: ScriptProps = Object.assign( + ({ + src, + async: true, + }: ScriptProps), options, - )}.`; - } else if (options && 'as' in options && options.as !== 'script') { - encountered += ` The \`as\` option encountered was ${getValueDescriptorExpectingEnumForWarning( - options.as, - )}.`; - } - if (encountered) { - console.error( - 'ReactDOM.preinitModule(): Expected up to two arguments, a non-empty `href` string and, optionally, an `options` object with a valid `as` property.%s', - encountered, ); - } else { - const as = - options && typeof options.as === 'string' ? options.as : 'script'; - switch (as) { - case 'script': { - break; - } - - // We have an invalid as type and need to warn - default: { - const typeOfAs = getValueDescriptorExpectingEnumForWarning(as); - console.error( - 'ReactDOM.preinitModule(): Currently the only supported "as" type for this function is "script"' + - ' but received "%s" instead. This warning was generated for `href` "%s". In the future other' + - ' module types will be supported, aligning with the import-attributes proposal. Learn more here:' + - ' (https://github.com/tc39/proposal-import-attributes)', - typeOfAs, - href, - ); - } - } + resumableState.scripts.add(resource); + pushScriptImpl(resource.chunks, props); + flushResources(request); } + return; } - if (typeof href === 'string' && href) { - const as = - options && typeof options.as === 'string' ? options.as : 'script'; - switch (as) { - case 'script': { - const src = href; - const key = getResourceKey(as, src); - let resource = resumableState.scriptsMap.get(key); - if (!resource) { - resource = { - type: 'script', - chunks: [], - state: NoState, - props: null, - }; - resumableState.scriptsMap.set(key, resource); - const resourceProps = modulePropsFromPreinitModuleOptions( - src, - options, - ); - resumableState.scripts.add(resource); - pushScriptImpl(resource.chunks, resourceProps); - flushResources(request); - } - return; - } +} + +function preinitModuleScript( + src: string, + options?: ?PreinitModuleScriptOptions, +): void { + if (!enableFloat) { + return; + } + const request = resolveRequest(); + if (!request) { + // In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also + // possibly get them from the stack if we are not in an async context. Since we were not able to resolve + // the resources for this call in either case we opt to do nothing. We can consider making this a warning + // but there may be times where calling a function outside of render is intentional (i.e. to warm up data + // fetching) and we don't want to warn in those cases. + return; + } + const resumableState = getResumableState(request); + if (src) { + const key = getResourceKey('script', src); + let resource = resumableState.scriptsMap.get(key); + if (!resource) { + resource = { + type: 'script', + chunks: [], + state: NoState, + props: null, + }; + resumableState.scriptsMap.set(key, resource); + const props = Object.assign( + ({ + src, + type: 'module', + async: true, + }: ModuleProps), + options, + ); + resumableState.scripts.add(resource); + pushScriptImpl(resource.chunks, props); + flushResources(request); } + return; } } @@ -5561,44 +5437,6 @@ function internalPreinitScript( return; } -function preloadPropsFromPreloadOptions( - href: string, - as: string, - options: PreloadOptions, -): PreloadProps { - return { - rel: 'preload', - as, - // There is a bug in Safari where imageSrcSet is not respected on preload links - // so we omit the href here if we have imageSrcSet b/c safari will load the wrong image. - // This harms older browers that do not support imageSrcSet by making their preloads not work - // but this population is shrinking fast and is already small so we accept this tradeoff. - href: as === 'image' && options.imageSrcSet ? undefined : href, - crossOrigin: as === 'font' ? '' : options.crossOrigin, - integrity: options.integrity, - type: options.type, - nonce: options.nonce, - fetchPriority: options.fetchPriority, - imageSrcSet: options.imageSrcSet, - imageSizes: options.imageSizes, - referrerPolicy: options.referrerPolicy, - }; -} - -function preloadModulePropsFromPreloadModuleOptions( - href: string, - as: string, - options: ?PreloadModuleOptions, -): PreloadModuleProps { - return { - rel: 'modulepreload', - as: as !== 'script' ? as : undefined, - href, - crossOrigin: options ? options.crossOrigin : undefined, - integrity: options ? options.integrity : undefined, - }; -} - function preloadAsStylePropsFromProps(href: string, props: any): PreloadProps { return { rel: 'preload', @@ -5613,21 +5451,6 @@ function preloadAsStylePropsFromProps(href: string, props: any): PreloadProps { }; } -function stylesheetPropsFromPreinitOptions( - href: string, - precedence: string, - options: PreinitOptions, -): StylesheetProps { - return { - rel: 'stylesheet', - href, - 'data-precedence': precedence, - crossOrigin: options.crossOrigin, - integrity: options.integrity, - fetchPriority: options.fetchPriority, - }; -} - function stylesheetPropsFromRawProps(rawProps: any): StylesheetProps { return { ...rawProps, @@ -5646,33 +5469,6 @@ function adoptPreloadPropsForStylesheetProps( resourceProps.integrity = preloadProps.integrity; } -function scriptPropsFromPreinitOptions( - src: string, - options: PreinitOptions, -): ScriptProps { - return { - src, - async: true, - crossOrigin: options.crossOrigin, - integrity: options.integrity, - nonce: options.nonce, - fetchPriority: options.fetchPriority, - }; -} - -function modulePropsFromPreinitModuleOptions( - src: string, - options: ?PreinitModuleOptions, -): ModuleProps { - return { - src, - type: 'module', - async: true, - crossOrigin: options ? options.crossOrigin : undefined, - integrity: options ? options.integrity : undefined, - }; -} - function adoptPreloadPropsForScriptProps( resourceProps: ScriptProps, preloadProps: PreloadProps, diff --git a/packages/react-dom-bindings/src/server/ReactFlightServerConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFlightServerConfigDOM.js index e9475579a143f..336b1d3ef412b 100644 --- a/packages/react-dom-bindings/src/server/ReactFlightServerConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFlightServerConfigDOM.js @@ -8,12 +8,12 @@ */ import type { - PrefetchDNSOptions, - PreconnectOptions, - PreloadOptions, - PreloadModuleOptions, - PreinitOptions, - PreinitModuleOptions, + CrossOriginEnum, + PreloadImplOptions, + PreloadModuleImplOptions, + PreinitStyleOptions, + PreinitScriptOptions, + PreinitModuleScriptOptions, } from 'react-dom/src/shared/ReactDOMTypes'; import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals'; @@ -29,19 +29,41 @@ export function prepareHostDispatcher(): void { // E.g. this can be used to distinguish legacy renderers from this modern one. export const isPrimaryRenderer = true; -export type HintModel = - | string - | [ - string, - ( - | PrefetchDNSOptions - | PreconnectOptions - | PreloadOptions - | PreloadModuleOptions - | PreinitOptions - | PreinitModuleOptions - ), - ]; +// We use zero to represent the absence of an explicit precedence because it is +// small, smaller than how we encode undefined, and is unambiguous. We could use +// a different tuple structure to encode this instead but this makes the runtime +// cost cheaper by eliminating a type checks in more positions. +type UnspecifiedPrecedence = 0; + +// prettier-ignore +type TypeMap = { + // prefetchDNS(href) + 'D': /* href */ string, + // preconnect(href, options) + 'C': + | /* href */ string + | [/* href */ string, CrossOriginEnum], + // preconnect(href, options) + 'L': + | [/* href */ string, /* as */ string] + | [/* href */ string, /* as */ string, PreloadImplOptions], + 'm': + | /* href */ string + | [/* href */ string, PreloadModuleImplOptions], + 'S': + | /* href */ string + | [/* href */ string, /* precedence */ string] + | [/* href */ string, /* precedence */ string | UnspecifiedPrecedence, PreinitStyleOptions], + 'X': + | /* href */ string + | [/* href */ string, PreinitScriptOptions], + 'M': + | /* href */ string + | [/* href */ string, PreinitModuleScriptOptions], +} + +export type HintCode = $Keys; +export type HintModel = TypeMap[T]; export type Hints = Set; diff --git a/packages/react-dom-bindings/src/shared/ReactDOMResourceValidation.js b/packages/react-dom-bindings/src/shared/ReactDOMResourceValidation.js index fd6d068d32202..6e0b56479dd3e 100644 --- a/packages/react-dom-bindings/src/shared/ReactDOMResourceValidation.js +++ b/packages/react-dom-bindings/src/shared/ReactDOMResourceValidation.js @@ -62,46 +62,6 @@ function propNamesListJoin( } } -export function validatePreinitArguments(href: mixed, options: mixed) { - if (__DEV__) { - if (!href || typeof href !== 'string') { - const typeOfArg = getValueDescriptorExpectingObjectForWarning(href); - console.error( - 'ReactDOM.preinit() expected the first argument to be a string representing an href but found %s instead.', - typeOfArg, - ); - } else if (typeof options !== 'object' || options === null) { - const typeOfArg = getValueDescriptorExpectingObjectForWarning(options); - console.error( - 'ReactDOM.preinit() expected the second argument to be an options argument containing at least an "as" property' + - ' specifying the Resource type. It found %s instead. The href for the preload call where this warning originated is "%s".', - typeOfArg, - href, - ); - } else { - const as = options.as; - switch (as) { - case 'style': - case 'script': { - break; - } - - // We have an invalid as type and need to warn - default: { - const typeOfAs = getValueDescriptorExpectingEnumForWarning(as); - console.error( - 'ReactDOM.preinit() expected the second argument to be an options argument containing at least an "as" property' + - ' specifying the Resource type. It found %s instead. Currently, valid resource types for for preinit are "style"' + - ' and "script". The href for the preinit call where this warning originated is "%s".', - typeOfAs, - href, - ); - } - } - } - } -} - export function getValueDescriptorExpectingObjectForWarning( thing: any, ): string { diff --git a/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js b/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js index ed09056585094..150f16f48d56f 100644 --- a/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js +++ b/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js @@ -10,56 +10,103 @@ // This client file is in the shared folder because it applies to both SSR and browser contexts. // It is the configuraiton of the FlightClient behavior which can run in either environment. -import type {HintModel} from '../server/ReactFlightServerConfigDOM'; +import type {HintCode, HintModel} from '../server/ReactFlightServerConfigDOM'; import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals'; const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher; -export function dispatchHint(code: string, model: HintModel): void { +export function dispatchHint( + code: Code, + model: HintModel, +): void { const dispatcher = ReactDOMCurrentDispatcher.current; if (dispatcher) { - let href, options; - if (typeof model === 'string') { - href = model; - } else { - href = model[0]; - options = model[1]; - } switch (code) { case 'D': { - // $FlowFixMe[prop-missing] options are not refined to their types by code - dispatcher.prefetchDNS(href, options); + const refined = refineModel(code, model); + const href = refined; + dispatcher.prefetchDNS(href); return; } case 'C': { - // $FlowFixMe[prop-missing] options are not refined to their types by code - dispatcher.preconnect(href, options); + const refined = refineModel(code, model); + if (typeof refined === 'string') { + const href = refined; + dispatcher.preconnect(href); + } else { + const href = refined[0]; + const crossOrigin = refined[1]; + dispatcher.preconnect(href, crossOrigin); + } return; } case 'L': { - // $FlowFixMe[prop-missing] options are not refined to their types by code - // $FlowFixMe[incompatible-call] options are not refined to their types by code - dispatcher.preload(href, options); + const refined = refineModel(code, model); + const href = refined[0]; + const as = refined[1]; + if (refined.length === 3) { + const options = refined[2]; + dispatcher.preload(href, as, options); + } else { + dispatcher.preload(href, as); + } return; } case 'm': { - // $FlowFixMe[prop-missing] options are not refined to their types by code - // $FlowFixMe[incompatible-call] options are not refined to their types by code - dispatcher.preloadModule(href, options); + const refined = refineModel(code, model); + if (typeof refined === 'string') { + const href = refined; + dispatcher.preloadModule(href); + } else { + const href = refined[0]; + const options = refined[1]; + dispatcher.preloadModule(href, options); + } + return; + } + case 'S': { + const refined = refineModel(code, model); + if (typeof refined === 'string') { + const href = refined; + dispatcher.preinitStyle(href); + } else { + const href = refined[0]; + const precedence = refined[1] === 0 ? undefined : refined[1]; + const options = refined.length === 3 ? refined[2] : undefined; + dispatcher.preinitStyle(href, precedence, options); + } return; } - case 'I': { - // $FlowFixMe[prop-missing] options are not refined to their types by code - // $FlowFixMe[incompatible-call] options are not refined to their types by code - dispatcher.preinit(href, options); + case 'X': { + const refined = refineModel(code, model); + if (typeof refined === 'string') { + const href = refined; + dispatcher.preinitScript(href); + } else { + const href = refined[0]; + const options = refined[1]; + dispatcher.preinitScript(href, options); + } return; } case 'M': { - // $FlowFixMe[prop-missing] options are not refined to their types by code - // $FlowFixMe[incompatible-call] options are not refined to their types by code - dispatcher.preinitModule(href, options); + const refined = refineModel(code, model); + if (typeof refined === 'string') { + const href = refined; + dispatcher.preinitModuleScript(href); + } else { + const href = refined[0]; + const options = refined[1]; + dispatcher.preinitModuleScript(href, options); + } return; } } } } + +// Flow is having troulbe refining the HintModels so we help it a bit. +// This should be compiled out in the production build. +function refineModel(code: T, model: HintModel): HintModel { + return model; +} diff --git a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js index 010daf772e344..bff5b6b262e87 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js @@ -5341,8 +5341,6 @@ body { ]); expect(getMeaningfulChildren(document.body)).toEqual(
-
foo
', + '
foo
', ); }); diff --git a/packages/react-dom/src/shared/ReactDOMFloat.js b/packages/react-dom/src/shared/ReactDOMFloat.js index 5ebb810952602..34f36c541b0c9 100644 --- a/packages/react-dom/src/shared/ReactDOMFloat.js +++ b/packages/react-dom/src/shared/ReactDOMFloat.js @@ -7,6 +7,7 @@ * @flow */ import type { + CrossOriginEnum, PreconnectOptions, PreloadOptions, PreloadModuleOptions, @@ -18,37 +19,64 @@ import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals'; const Dispatcher = ReactDOMSharedInternals.Dispatcher; export function prefetchDNS(href: string) { - let passedOptionArg: any; if (__DEV__) { - if (arguments[1] !== undefined) { - passedOptionArg = arguments[1]; - } - } - const dispatcher = Dispatcher.current; - if (dispatcher) { - if (__DEV__) { - if (passedOptionArg !== undefined) { - // prefetchDNS will warn if you pass reserved options arg. We pass it along in Dev only to - // elicit the warning. In prod we do not forward since it is not a part of the interface. - // @TODO move all arg validation into this file. It needs to be universal anyway so may as well lock down the interace here and - // let the rest of the codebase trust the types - dispatcher.prefetchDNS(href, passedOptionArg); + if (typeof href !== 'string' || !href) { + console.error( + 'ReactDOM.prefetchDNS(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.', + getValueDescriptorExpectingObjectForWarning(href), + ); + } else if (arguments.length > 1) { + const options = arguments[1]; + if ( + typeof options === 'object' && + options.hasOwnProperty('crossOrigin') + ) { + console.error( + 'ReactDOM.prefetchDNS(): Expected only one argument, `href`, but encountered %s as a second argument instead. This argument is reserved for future options and is currently disallowed. It looks like the you are attempting to set a crossOrigin property for this DNS lookup hint. Browsers do not perform DNS queries using CORS and setting this attribute on the resource hint has no effect. Try calling ReactDOM.prefetchDNS() with just a single string argument, `href`.', + getValueDescriptorExpectingEnumForWarning(options), + ); } else { - dispatcher.prefetchDNS(href); + console.error( + 'ReactDOM.prefetchDNS(): Expected only one argument, `href`, but encountered %s as a second argument instead. This argument is reserved for future options and is currently disallowed. Try calling ReactDOM.prefetchDNS() with just a single string argument, `href`.', + getValueDescriptorExpectingEnumForWarning(options), + ); } - } else { - dispatcher.prefetchDNS(href); } } + const dispatcher = Dispatcher.current; + if (dispatcher && typeof href === 'string') { + dispatcher.prefetchDNS(href); + } // We don't error because preconnect needs to be resilient to being called in a variety of scopes // and the runtime may not be capable of responding. The function is optimistic and not critical // so we favor silent bailout over warning or erroring. } export function preconnect(href: string, options?: ?PreconnectOptions) { + if (__DEV__) { + if (typeof href !== 'string' || !href) { + console.error( + 'ReactDOM.preconnect(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.', + getValueDescriptorExpectingObjectForWarning(href), + ); + } else if (options != null && typeof options !== 'object') { + console.error( + 'ReactDOM.preconnect(): Expected the `options` argument (second) to be an object but encountered %s instead. The only supported option at this time is `crossOrigin` which accepts a string.', + getValueDescriptorExpectingEnumForWarning(options), + ); + } else if (options != null && typeof options.crossOrigin !== 'string') { + console.error( + 'ReactDOM.preconnect(): Expected the `crossOrigin` option (second argument) to be a string but encountered %s instead. Try removing this option or passing a string value instead.', + getValueDescriptorExpectingObjectForWarning(options.crossOrigin), + ); + } + } const dispatcher = Dispatcher.current; - if (dispatcher) { - dispatcher.preconnect(href, options); + if (dispatcher && typeof href === 'string') { + const crossOrigin = options + ? getCrossOrigin('preconnect', options.crossOrigin) + : null; + dispatcher.preconnect(href, crossOrigin); } // We don't error because preconnect needs to be resilient to being called in a variety of scopes // and the runtime may not be capable of responding. The function is optimistic and not critical @@ -56,9 +84,61 @@ export function preconnect(href: string, options?: ?PreconnectOptions) { } export function preload(href: string, options: PreloadOptions) { + if (__DEV__) { + let encountered = ''; + if (typeof href !== 'string' || !href) { + encountered += ` The \`href\` argument encountered was ${getValueDescriptorExpectingObjectForWarning( + href, + )}.`; + } + if (options == null || typeof options !== 'object') { + encountered += ` The \`options\` argument encountered was ${getValueDescriptorExpectingObjectForWarning( + options, + )}.`; + } else if (typeof options.as !== 'string' || !options.as) { + encountered += ` The \`as\` option encountered was ${getValueDescriptorExpectingObjectForWarning( + options.as, + )}.`; + } + if (encountered) { + console.error( + 'ReactDOM.preload(): Expected two arguments, a non-empty `href` string and an `options` object with an `as` property valid for a `` tag.%s', + encountered, + ); + } + } const dispatcher = Dispatcher.current; - if (dispatcher) { - dispatcher.preload(href, options); + if ( + dispatcher && + typeof href === 'string' && + // We check existence because we cannot enforce this function is actually called with the stated type + typeof options === 'object' && + options !== null && + typeof options.as === 'string' + ) { + const as = options.as; + const crossOrigin = getCrossOrigin(as, options.crossOrigin); + dispatcher.preload(href, as, { + crossOrigin, + integrity: + typeof options.integrity === 'string' ? options.integrity : undefined, + nonce: typeof options.nonce === 'string' ? options.nonce : undefined, + type: typeof options.type === 'string' ? options.type : undefined, + fetchPriority: + typeof options.fetchPriority === 'string' + ? options.fetchPriority + : undefined, + referrerPolicy: + typeof options.referrerPolicy === 'string' + ? options.referrerPolicy + : undefined, + imageSrcSet: + typeof options.imageSrcSet === 'string' + ? options.imageSrcSet + : undefined, + imageSizes: + typeof options.imageSizes === 'string' ? options.imageSizes : undefined, + }); } // We don't error because preload needs to be resilient to being called in a variety of scopes // and the runtime may not be capable of responding. The function is optimistic and not critical @@ -66,9 +146,45 @@ export function preload(href: string, options: PreloadOptions) { } export function preloadModule(href: string, options?: ?PreloadModuleOptions) { + if (__DEV__) { + let encountered = ''; + if (typeof href !== 'string' || !href) { + encountered += ` The \`href\` argument encountered was ${getValueDescriptorExpectingObjectForWarning( + href, + )}.`; + } + if (options !== undefined && typeof options !== 'object') { + encountered += ` The \`options\` argument encountered was ${getValueDescriptorExpectingObjectForWarning( + options, + )}.`; + } else if (options && 'as' in options && typeof options.as !== 'string') { + encountered += ` The \`as\` option encountered was ${getValueDescriptorExpectingObjectForWarning( + options.as, + )}.`; + } + if (encountered) { + console.error( + 'ReactDOM.preloadModule(): Expected two arguments, a non-empty `href` string and, optionally, an `options` object with an `as` property valid for a `` tag.%s', + encountered, + ); + } + } const dispatcher = Dispatcher.current; - if (dispatcher) { - dispatcher.preloadModule(href, options); + if (dispatcher && typeof href === 'string') { + if (options) { + const crossOrigin = getCrossOrigin(options.as, options.crossOrigin); + dispatcher.preloadModule(href, { + as: + typeof options.as === 'string' && options.as !== 'script' + ? options.as + : undefined, + crossOrigin, + integrity: + typeof options.integrity === 'string' ? options.integrity : undefined, + }); + } else { + dispatcher.preloadModule(href); + } } // We don't error because preload needs to be resilient to being called in a variety of scopes // and the runtime may not be capable of responding. The function is optimistic and not critical @@ -76,9 +192,57 @@ export function preloadModule(href: string, options?: ?PreloadModuleOptions) { } export function preinit(href: string, options: PreinitOptions) { + if (__DEV__) { + if (typeof href !== 'string' || !href) { + console.error( + 'ReactDOM.preinit(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.', + getValueDescriptorExpectingObjectForWarning(href), + ); + } else if (options == null || typeof options !== 'object') { + console.error( + 'ReactDOM.preinit(): Expected the `options` argument (second) to be an object with an `as` property describing the type of resource to be preinitialized but encountered %s instead.', + getValueDescriptorExpectingEnumForWarning(options), + ); + } else if (options.as !== 'style' && options.as !== 'script') { + console.error( + 'ReactDOM.preinit(): Expected the `as` property in the `options` argument (second) to contain a valid value describing the type of resource to be preinitialized but encountered %s instead. Valid values for `as` are "style" and "script".', + getValueDescriptorExpectingEnumForWarning(options.as), + ); + } + } const dispatcher = Dispatcher.current; - if (dispatcher) { - dispatcher.preinit(href, options); + if ( + dispatcher && + typeof href === 'string' && + options && + typeof options.as === 'string' + ) { + const as = options.as; + const crossOrigin = getCrossOrigin(as, options.crossOrigin); + const integrity = + typeof options.integrity === 'string' ? options.integrity : undefined; + const fetchPriority = + typeof options.fetchPriority === 'string' + ? options.fetchPriority + : undefined; + if (as === 'style') { + dispatcher.preinitStyle( + href, + typeof options.precedence === 'string' ? options.precedence : undefined, + { + crossOrigin, + integrity, + fetchPriority, + }, + ); + } else if (as === 'script') { + dispatcher.preinitScript(href, { + crossOrigin, + integrity, + fetchPriority, + nonce: typeof options.nonce === 'string' ? options.nonce : undefined, + }); + } } // We don't error because preinit needs to be resilient to being called in a variety of scopes // and the runtime may not be capable of responding. The function is optimistic and not critical @@ -86,11 +250,104 @@ export function preinit(href: string, options: PreinitOptions) { } export function preinitModule(href: string, options?: ?PreinitModuleOptions) { + if (__DEV__) { + let encountered = ''; + if (typeof href !== 'string' || !href) { + encountered += ` The \`href\` argument encountered was ${getValueDescriptorExpectingObjectForWarning( + href, + )}.`; + } + if (options !== undefined && typeof options !== 'object') { + encountered += ` The \`options\` argument encountered was ${getValueDescriptorExpectingObjectForWarning( + options, + )}.`; + } else if (options && 'as' in options && options.as !== 'script') { + encountered += ` The \`as\` option encountered was ${getValueDescriptorExpectingEnumForWarning( + options.as, + )}.`; + } + if (encountered) { + console.error( + 'ReactDOM.preinitModule(): Expected up to two arguments, a non-empty `href` string and, optionally, an `options` object with a valid `as` property.%s', + encountered, + ); + } else { + const as = + options && typeof options.as === 'string' ? options.as : 'script'; + switch (as) { + case 'script': { + break; + } + + // We have an invalid as type and need to warn + default: { + const typeOfAs = getValueDescriptorExpectingEnumForWarning(as); + console.error( + 'ReactDOM.preinitModule(): Currently the only supported "as" type for this function is "script"' + + ' but received "%s" instead. This warning was generated for `href` "%s". In the future other' + + ' module types will be supported, aligning with the import-attributes proposal. Learn more here:' + + ' (https://github.com/tc39/proposal-import-attributes)', + typeOfAs, + href, + ); + } + } + } + } const dispatcher = Dispatcher.current; - if (dispatcher) { - dispatcher.preinitModule(href, options); + if (dispatcher && typeof href === 'string') { + if ( + options == null || + (typeof options === 'object' && + (options.as == null || options.as === 'script')) + ) { + const crossOrigin = options + ? getCrossOrigin(undefined, options.crossOrigin) + : undefined; + dispatcher.preinitModuleScript(href, { + crossOrigin, + integrity: + options && typeof options.integrity === 'string' + ? options.integrity + : undefined, + }); + } } // We don't error because preinit needs to be resilient to being called in a variety of scopes // and the runtime may not be capable of responding. The function is optimistic and not critical // so we favor silent bailout over warning or erroring. } + +function getCrossOrigin(as: ?string, crossOrigin: ?string): ?CrossOriginEnum { + return as === 'font' + ? '' + : typeof crossOrigin === 'string' + ? crossOrigin === 'use-credentials' + ? 'use-credentials' + : '' + : undefined; +} + +function getValueDescriptorExpectingObjectForWarning(thing: any): string { + return thing === null + ? '`null`' + : thing === undefined + ? '`undefined`' + : thing === '' + ? 'an empty string' + : `something with type "${typeof thing}"`; +} + +function getValueDescriptorExpectingEnumForWarning(thing: any): string { + return thing === null + ? '`null`' + : thing === undefined + ? '`undefined`' + : thing === '' + ? 'an empty string' + : typeof thing === 'string' + ? JSON.stringify(thing) + : typeof thing === 'number' + ? '`' + thing + '`' + : `something with type "${typeof thing}"`; +} diff --git a/packages/react-dom/src/shared/ReactDOMTypes.js b/packages/react-dom/src/shared/ReactDOMTypes.js index 3e8c7d1a79d22..af54a9e27bc65 100644 --- a/packages/react-dom/src/shared/ReactDOMTypes.js +++ b/packages/react-dom/src/shared/ReactDOMTypes.js @@ -39,13 +39,55 @@ export type PreinitModuleOptions = { integrity?: string, }; +export type CrossOriginEnum = '' | 'use-credentials'; +export type FetchPriorityEnum = 'high' | 'low' | 'auto'; + +export type PreloadImplOptions = { + crossOrigin?: ?CrossOriginEnum, + integrity?: ?string, + nonce?: ?string, + type?: ?string, + fetchPriority?: ?FetchPriorityEnum, + referrerPolicy?: ?string, + imageSrcSet?: ?string, + imageSizes?: ?string, +}; +export type PreloadModuleImplOptions = { + as?: ?string, + crossOrigin?: ?CrossOriginEnum, + integrity?: ?string, +}; +export type PreinitStyleOptions = { + crossOrigin?: ?string, + integrity?: ?string, + fetchPriority?: ?FetchPriorityEnum, +}; +export type PreinitScriptOptions = { + crossOrigin?: ?string, + integrity?: ?string, + fetchPriority?: ?FetchPriorityEnum, + nonce?: ?string, +}; +export type PreinitModuleScriptOptions = { + crossOrigin?: ?CrossOriginEnum, + integrity?: string, +}; + export type HostDispatcher = { - prefetchDNS: (href: string, options?: ?PrefetchDNSOptions) => void, - preconnect: (href: string, options?: ?PreconnectOptions) => void, - preload: (href: string, options: PreloadOptions) => void, - preloadModule: (href: string, options?: ?PreloadModuleOptions) => void, - preinit: (href: string, options: PreinitOptions) => void, - preinitModule: (href: string, options?: ?PreinitModuleOptions) => void, + prefetchDNS: (href: string) => void, + preconnect: (href: string, crossOrigin?: ?CrossOriginEnum) => void, + preload: (href: string, as: string, options?: ?PreloadImplOptions) => void, + preloadModule: (href: string, options?: ?PreloadModuleImplOptions) => void, + preinitStyle: ( + href: string, + precedence: ?string, + options?: ?PreinitStyleOptions, + ) => void, + preinitScript: (src: string, options?: PreinitScriptOptions) => void, + preinitModuleScript: ( + src: string, + options?: ?PreinitModuleScriptOptions, + ) => void, }; export type ImportMap = { diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js index acb5724ce5006..f86db1943a3fa 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js @@ -1270,29 +1270,19 @@ describe('ReactFlightDOM', () => { - +