@samitouri / QOS-React / commits / bbc8530ed7

[Float] Refactor public interface and internal HostDispatcher implementation (#27361)

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 where 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 * Fixes crossOrigin serialization to use empty string except when 'use-credentials'

Josh Story committed Sep 12, 2023 at 08:09 UTC bbc8530ed7a67859583a7c990ac51cd39c7746e5
15 files changed +974 -978
packages/react-client/src/ReactFlightClient.js
+8 -5
@@ -17,7 +17,10 @@ import type {
17 StringDecoder,
18 } from './ReactFlightClientConfig';
19
20 -import type {HintModel} from 'react-server/src/ReactFlightServerConfig';
20 +import type {
21 + HintCode,
22 + HintModel,
23 +} from 'react-server/src/ReactFlightServerConfig';
24
25 import type {CallServerCallback} from './ReactFlightReplyClient';
26
@@ -915,12 +918,12 @@ function resolvePostponeDev(
918 }
919 }
920
918 -function resolveHint(
921 +function resolveHint<Code: HintCode>(
922 response: Response,
920 - code: string,
923 + code: Code,
924 model: UninitializedModel,
925 ): void {
923 - const hintModel: HintModel = parseModel(response, model);
926 + const hintModel: HintModel<Code> = parseModel(response, model);
927 dispatchHint(code, hintModel);
928 }
929
@@ -1044,7 +1047,7 @@ function processFullRow(
1047 return;
1048 }
1049 case 72 /* "H" */: {
1047 - const code = row[0];
1050 + const code: HintCode = (row[0]: any);
1051 resolveHint(response, code, row.slice(1));
1052 return;
1053 }
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+201 -412
@@ -20,12 +20,12 @@ import type {ReactScopeInstance} from 'shared/ReactTypes';
20 import type {AncestorInfoDev} from './validateDOMNesting';
21 import type {FormStatus} from 'react-dom-bindings/src/shared/ReactDOMFormActions';
22 import type {
23 - PrefetchDNSOptions,
24 - PreconnectOptions,
25 - PreloadOptions,
26 - PreloadModuleOptions,
27 - PreinitOptions,
28 - PreinitModuleOptions,
23 + CrossOriginEnum,
24 + PreloadImplOptions,
25 + PreloadModuleImplOptions,
26 + PreinitStyleOptions,
27 + PreinitScriptOptions,
28 + PreinitModuleScriptOptions,
29 } from 'react-dom/src/shared/ReactDOMTypes';
30
31 import {NotPending} from 'react-dom-bindings/src/shared/ReactDOMFormActions';
@@ -107,12 +107,7 @@ import {
107 HostSingleton,
108 } from 'react-reconciler/src/ReactWorkTags';
109 import {listenToAllSupportedEvents} from '../events/DOMPluginEventSystem';
110 -import {
111 - validatePreinitArguments,
112 - validateLinkPropsForStyleResource,
113 - getValueDescriptorExpectingObjectForWarning,
114 - getValueDescriptorExpectingEnumForWarning,
115 -} from '../shared/ReactDOMResourceValidation';
110 +import {validateLinkPropsForStyleResource} from '../shared/ReactDOMResourceValidation';
111 import escapeSelectorAttributeValueInsideDoubleQuotes from './escapeSelectorAttributeValueInsideDoubleQuotes';
112
113 export type Type = string;
@@ -2120,8 +2115,9 @@ export const ReactDOMClientDispatcher: HostDispatcher = {
2115 preconnect,
2116 preload,
2117 preloadModule,
2123 - preinit,
2124 - preinitModule,
2118 + preinitStyle,
2119 + preinitScript,
2120 + preinitModuleScript,
2121 };
2122
2123 // We expect this to get inlined. It is a function mostly to communicate the special nature of
@@ -2136,8 +2132,8 @@ function getDocumentForImperativeFloatMethods(): Document {
2132
2133 function preconnectAs(
2134 rel: 'preconnect' | 'dns-prefetch',
2139 - crossOrigin: null | '' | 'use-credentials',
2135 href: string,
2136 + crossOrigin: ?CrossOriginEnum,
2137 ) {
2138 const ownerDocument = getDocumentForImperativeFloatMethods();
2139 if (typeof href === 'string' && href) {
@@ -2161,119 +2157,37 @@ function preconnectAs(
2157 }
2158 }
2159
2164 -function prefetchDNS(href: string, options?: ?PrefetchDNSOptions) {
2160 +function prefetchDNS(href: string) {
2161 if (!enableFloat) {
2162 return;
2163 }
2168 - if (__DEV__) {
2169 - if (typeof href !== 'string' || !href) {
2170 - console.error(
2171 - 'ReactDOM.prefetchDNS(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.',
2172 - getValueDescriptorExpectingObjectForWarning(href),
2173 - );
2174 - } else if (options != null) {
2175 - if (
2176 - typeof options === 'object' &&
2177 - hasOwnProperty.call(options, 'crossOrigin')
2178 - ) {
2179 - console.error(
2180 - '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`.',
2181 - getValueDescriptorExpectingEnumForWarning(options),
2182 - );
2183 - } else {
2184 - console.error(
2185 - '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`.',
2186 - getValueDescriptorExpectingEnumForWarning(options),
2187 - );
2188 - }
2189 - }
2190 - }
2191 - preconnectAs('dns-prefetch', null, href);
2164 + preconnectAs('dns-prefetch', href, null);
2165 }
2166
2194 -function preconnect(href: string, options?: ?PreconnectOptions) {
2167 +function preconnect(href: string, crossOrigin?: ?CrossOriginEnum) {
2168 if (!enableFloat) {
2169 return;
2170 }
2198 - if (__DEV__) {
2199 - if (typeof href !== 'string' || !href) {
2200 - console.error(
2201 - 'ReactDOM.preconnect(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.',
2202 - getValueDescriptorExpectingObjectForWarning(href),
2203 - );
2204 - } else if (options != null && typeof options !== 'object') {
2205 - console.error(
2206 - '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.',
2207 - getValueDescriptorExpectingEnumForWarning(options),
2208 - );
2209 - } else if (options != null && typeof options.crossOrigin !== 'string') {
2210 - console.error(
2211 - '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.',
2212 - getValueDescriptorExpectingObjectForWarning(options.crossOrigin),
2213 - );
2214 - }
2215 - }
2216 - const crossOrigin =
2217 - options == null || typeof options.crossOrigin !== 'string'
2218 - ? null
2219 - : options.crossOrigin === 'use-credentials'
2220 - ? 'use-credentials'
2221 - : '';
2222 - preconnectAs('preconnect', crossOrigin, href);
2171 + preconnectAs('preconnect', href, crossOrigin);
2172 }
2173
2225 -function preload(href: string, options: PreloadOptions) {
2174 +function preload(href: string, as: string, options?: ?PreloadImplOptions) {
2175 if (!enableFloat) {
2176 return;
2177 }
2229 - if (__DEV__) {
2230 - // TODO move this to ReactDOMFloat and expose a stricter function interface or possibly
2231 - // typed functions (preloadImage, preloadStyle, ...)
2232 - let encountered = '';
2233 - if (typeof href !== 'string' || !href) {
2234 - encountered += `The \`href\` argument encountered was ${getValueDescriptorExpectingObjectForWarning(
2235 - href,
2236 - )}.`;
2237 - }
2238 - if (options == null || typeof options !== 'object') {
2239 - encountered += `The \`options\` argument encountered was ${getValueDescriptorExpectingObjectForWarning(
2240 - options,
2241 - )}.`;
2242 - } else if (typeof options.as !== 'string' || !options.as) {
2243 - encountered += `The \`as\` option encountered was ${getValueDescriptorExpectingObjectForWarning(
2244 - options.as,
2245 - )}.`;
2246 - }
2247 - if (encountered) {
2248 - console.error(
2249 - 'ReactDOM.preload(): Expected two arguments, a non-empty `href` string and an `options` object with an `as` property valid for a `<link rel="preload" as="..." />` tag. %s',
2250 - encountered,
2251 - );
2252 - }
2253 - }
2178 const ownerDocument = getDocumentForImperativeFloatMethods();
2255 - if (
2256 - typeof href === 'string' &&
2257 - href &&
2258 - typeof options === 'object' &&
2259 - options !== null &&
2260 - typeof options.as === 'string' &&
2261 - options.as &&
2262 - ownerDocument
2263 - ) {
2264 - const as = options.as;
2179 + if (href && as && ownerDocument) {
2180 let preloadSelector = `link[rel="preload"][as="${escapeSelectorAttributeValueInsideDoubleQuotes(
2181 as,
2182 )}"]`;
2183 if (as === 'image') {
2269 - const {imageSrcSet, imageSizes} = options;
2270 - if (typeof imageSrcSet === 'string' && imageSrcSet !== '') {
2184 + if (options && options.imageSrcSet) {
2185 preloadSelector += `[imagesrcset="${escapeSelectorAttributeValueInsideDoubleQuotes(
2272 - imageSrcSet,
2186 + options.imageSrcSet,
2187 )}"]`;
2274 - if (typeof imageSizes === 'string') {
2188 + if (typeof options.imageSizes === 'string') {
2189 preloadSelector += `[imagesizes="${escapeSelectorAttributeValueInsideDoubleQuotes(
2276 - imageSizes,
2190 + options.imageSizes,
2191 )}"]`;
2192 }
2193 } else {
@@ -2299,7 +2213,19 @@ function preload(href: string, options: PreloadOptions) {
2213 break;
2214 }
2215 if (!preloadPropsMap.has(key)) {
2302 - const preloadProps = preloadPropsFromPreloadOptions(href, as, options);
2216 + const preloadProps = Object.assign(
2217 + ({
2218 + rel: 'preload',
2219 + // There is a bug in Safari where imageSrcSet is not respected on preload links
2220 + // so we omit the href here if we have imageSrcSet b/c safari will load the wrong image.
2221 + // This harms older browers that do not support imageSrcSet by making their preloads not work
2222 + // but this population is shrinking fast and is already small so we accept this tradeoff.
2223 + href:
2224 + as === 'image' && options && options.imageSrcSet ? undefined : href,
2225 + as,
2226 + }: PreloadProps),
2227 + options,
2228 + );
2229 preloadPropsMap.set(key, preloadProps);
2230
2231 if (null === ownerDocument.querySelector(preloadSelector)) {
@@ -2325,35 +2251,12 @@ function preload(href: string, options: PreloadOptions) {
2251 }
2252 }
2253
2328 -function preloadModule(href: string, options?: ?PreloadModuleOptions) {
2254 +function preloadModule(href: string, options?: ?PreloadModuleImplOptions) {
2255 if (!enableFloat) {
2256 return;
2257 }
2332 - if (__DEV__) {
2333 - let encountered = '';
2334 - if (typeof href !== 'string' || !href) {
2335 - encountered += ` The \`href\` argument encountered was ${getValueDescriptorExpectingObjectForWarning(
2336 - href,
2337 - )}.`;
2338 - }
2339 - if (options !== undefined && typeof options !== 'object') {
2340 - encountered += ` The \`options\` argument encountered was ${getValueDescriptorExpectingObjectForWarning(
2341 - options,
2342 - )}.`;
2343 - } else if (options && 'as' in options && typeof options.as !== 'string') {
2344 - encountered += ` The \`as\` option encountered was ${getValueDescriptorExpectingObjectForWarning(
2345 - options.as,
2346 - )}.`;
2347 - }
2348 - if (encountered) {
2349 - console.error(
2350 - 'ReactDOM.preloadModule(): Expected two arguments, a non-empty `href` string and, optionally, an `options` object with an `as` property valid for a `<link rel="modulepreload" as="..." />` tag.%s',
2351 - encountered,
2352 - );
2353 - }
2354 - }
2258 const ownerDocument = getDocumentForImperativeFloatMethods();
2356 - if (typeof href === 'string' && href) {
2259 + if (href) {
2260 const as =
2261 options && typeof options.as === 'string' ? options.as : 'script';
2262 const preloadSelector = `link[rel="modulepreload"][as="${escapeSelectorAttributeValueInsideDoubleQuotes(
@@ -2376,12 +2279,14 @@ function preloadModule(href: string, options?: ?PreloadModuleOptions) {
2279 }
2280
2281 if (!preloadPropsMap.has(key)) {
2379 - const preloadProps = preloadModulePropsFromPreloadModuleOptions(
2380 - href,
2381 - as,
2282 + const props: PreloadModuleProps = Object.assign(
2283 + ({
2284 + rel: 'modulepreload',
2285 + href,
2286 + }: PreloadModuleProps),
2287 options,
2288 );
2384 - preloadPropsMap.set(key, preloadProps);
2289 + preloadPropsMap.set(key, props);
2290
2291 if (null === ownerDocument.querySelector(preloadSelector)) {
2292 switch (as) {
@@ -2397,7 +2302,7 @@ function preloadModule(href: string, options?: ?PreloadModuleOptions) {
2302 }
2303 }
2304 const instance = ownerDocument.createElement('link');
2400 - setInitialProperties(instance, 'link', preloadProps);
2305 + setInitialProperties(instance, 'link', props);
2306 markNodeAsHoistable(instance);
2307 (ownerDocument.head: any).appendChild(instance);
2308 }
@@ -2405,314 +2310,198 @@ function preloadModule(href: string, options?: ?PreloadModuleOptions) {
2310 }
2311 }
2312
2408 -function preloadPropsFromPreloadOptions(
2313 +function preinitStyle(
2314 href: string,
2410 - as: string,
2411 - options: PreloadOptions,
2412 -): PreloadProps {
2413 - return {
2414 - rel: 'preload',
2415 - as,
2416 - // There is a bug in Safari where imageSrcSet is not respected on preload links
2417 - // so we omit the href here if we have imageSrcSet b/c safari will load the wrong image.
2418 - // This harms older browers that do not support imageSrcSet by making their preloads not work
2419 - // but this population is shrinking fast and is already small so we accept this tradeoff.
2420 - href: as === 'image' && options.imageSrcSet ? undefined : href,
2421 - crossOrigin: as === 'font' ? '' : options.crossOrigin,
2422 - integrity: options.integrity,
2423 - type: options.type,
2424 - nonce: options.nonce,
2425 - fetchPriority: options.fetchPriority,
2426 - imageSrcSet: options.imageSrcSet,
2427 - imageSizes: options.imageSizes,
2428 - referrerPolicy: options.referrerPolicy,
2429 - };
2430 -}
2431 -
2432 -function preloadModulePropsFromPreloadModuleOptions(
2433 - href: string,
2434 - as: string,
2435 - options: ?PreloadModuleOptions,
2436 -): PreloadModuleProps {
2437 - return {
2438 - rel: 'modulepreload',
2439 - as: as !== 'script' ? as : undefined,
2440 - href,
2441 - crossOrigin: options ? options.crossOrigin : undefined,
2442 - integrity: options ? options.integrity : undefined,
2443 - };
2444 -}
2445 -
2446 -function preinit(href: string, options: PreinitOptions) {
2315 + precedence: ?string,
2316 + options?: ?PreinitStyleOptions,
2317 +) {
2318 if (!enableFloat) {
2319 return;
2320 }
2450 - if (__DEV__) {
2451 - validatePreinitArguments(href, options);
2452 - }
2321 const ownerDocument = getDocumentForImperativeFloatMethods();
2322
2455 - if (
2456 - typeof href === 'string' &&
2457 - href &&
2458 - typeof options === 'object' &&
2459 - options !== null
2460 - ) {
2461 - const as = options.as;
2462 -
2463 - switch (as) {
2464 - case 'style': {
2465 - const styles = getResourcesFromRoot(ownerDocument).hoistableStyles;
2466 -
2467 - const key = getStyleKey(href);
2468 - const precedence = options.precedence || 'default';
2323 + if (href) {
2324 + const styles = getResourcesFromRoot(ownerDocument).hoistableStyles;
2325
2470 - // Check if this resource already exists
2471 - let resource = styles.get(key);
2472 - if (resource) {
2473 - // We can early return. The resource exists and there is nothing
2474 - // more to do
2475 - return;
2476 - }
2326 + const key = getStyleKey(href);
2327 + precedence = precedence || 'default';
2328
2478 - const state = {
2479 - loading: NotLoaded,
2480 - preload: null,
2481 - };
2329 + // Check if this resource already exists
2330 + let resource = styles.get(key);
2331 + if (resource) {
2332 + // We can early return. The resource exists and there is nothing
2333 + // more to do
2334 + return;
2335 + }
2336
2483 - // Attempt to hydrate instance from DOM
2484 - let instance: null | Instance = ownerDocument.querySelector(
2485 - getStylesheetSelectorFromKey(key),
2486 - );
2487 - if (instance) {
2488 - state.loading = Loaded;
2489 - } else {
2490 - // Construct a new instance and insert it
2491 - const stylesheetProps = stylesheetPropsFromPreinitOptions(
2492 - href,
2493 - precedence,
2494 - options,
2495 - );
2496 - const preloadProps = preloadPropsMap.get(key);
2497 - if (preloadProps) {
2498 - adoptPreloadPropsForStylesheet(stylesheetProps, preloadProps);
2499 - }
2500 - const link = (instance = ownerDocument.createElement('link'));
2501 - markNodeAsHoistable(link);
2502 - setInitialProperties(link, 'link', stylesheetProps);
2503 -
2504 - (link: any)._p = new Promise((resolve, reject) => {
2505 - link.onload = resolve;
2506 - link.onerror = reject;
2507 - });
2508 - link.addEventListener('load', () => {
2509 - state.loading |= Loaded;
2510 - });
2511 - link.addEventListener('error', () => {
2512 - state.loading |= Errored;
2513 - });
2514 -
2515 - state.loading |= Inserted;
2516 - insertStylesheet(instance, precedence, ownerDocument);
2517 - }
2337 + const state = {
2338 + loading: NotLoaded,
2339 + preload: null,
2340 + };
2341
2519 - // Construct a Resource and cache it
2520 - resource = {
2521 - type: 'stylesheet',
2522 - instance,
2523 - count: 1,
2524 - state,
2525 - };
2526 - styles.set(key, resource);
2527 - return;
2342 + // Attempt to hydrate instance from DOM
2343 + let instance: null | Instance = ownerDocument.querySelector(
2344 + getStylesheetSelectorFromKey(key),
2345 + );
2346 + if (instance) {
2347 + state.loading = Loaded;
2348 + } else {
2349 + // Construct a new instance and insert it
2350 + const stylesheetProps = Object.assign(
2351 + ({
2352 + rel: 'stylesheet',
2353 + href,
2354 + 'data-precedence': precedence,
2355 + }: StylesheetProps),
2356 + options,
2357 + );
2358 + const preloadProps = preloadPropsMap.get(key);
2359 + if (preloadProps) {
2360 + adoptPreloadPropsForStylesheet(stylesheetProps, preloadProps);
2361 }
2529 - case 'script': {
2530 - const src = href;
2531 - const scripts = getResourcesFromRoot(ownerDocument).hoistableScripts;
2362 + const link = (instance = ownerDocument.createElement('link'));
2363 + markNodeAsHoistable(link);
2364 + setInitialProperties(link, 'link', stylesheetProps);
2365
2533 - const key = getScriptKey(src);
2534 -
2535 - // Check if this resource already exists
2536 - let resource = scripts.get(key);
2537 - if (resource) {
2538 - // We can early return. The resource exists and there is nothing
2539 - // more to do
2540 - return;
2541 - }
2542 -
2543 - // Attempt to hydrate instance from DOM
2544 - let instance: null | Instance = ownerDocument.querySelector(
2545 - getScriptSelectorFromKey(key),
2546 - );
2547 - if (!instance) {
2548 - // Construct a new instance and insert it
2549 - const scriptProps = scriptPropsFromPreinitOptions(src, options);
2550 - // Adopt certain preload props
2551 - const preloadProps = preloadPropsMap.get(key);
2552 - if (preloadProps) {
2553 - adoptPreloadPropsForScript(scriptProps, preloadProps);
2554 - }
2555 - instance = ownerDocument.createElement('script');
2556 - markNodeAsHoistable(instance);
2557 - setInitialProperties(instance, 'link', scriptProps);
2558 - (ownerDocument.head: any).appendChild(instance);
2559 - }
2366 + (link: any)._p = new Promise((resolve, reject) => {
2367 + link.onload = resolve;
2368 + link.onerror = reject;
2369 + });
2370 + link.addEventListener('load', () => {
2371 + state.loading |= Loaded;
2372 + });
2373 + link.addEventListener('error', () => {
2374 + state.loading |= Errored;
2375 + });
2376
2561 - // Construct a Resource and cache it
2562 - resource = {
2563 - type: 'script',
2564 - instance,
2565 - count: 1,
2566 - state: null,
2567 - };
2568 - scripts.set(key, resource);
2569 - return;
2570 - }
2377 + state.loading |= Inserted;
2378 + insertStylesheet(instance, precedence, ownerDocument);
2379 }
2380 +
2381 + // Construct a Resource and cache it
2382 + resource = {
2383 + type: 'stylesheet',
2384 + instance,
2385 + count: 1,
2386 + state,
2387 + };
2388 + styles.set(key, resource);
2389 + return;
2390 }
2391 }
2392
2575 -function preinitModule(href: string, options?: ?PreinitModuleOptions) {
2393 +function preinitScript(src: string, options?: ?PreinitScriptOptions) {
2394 if (!enableFloat) {
2395 return;
2396 }
2579 - if (__DEV__) {
2580 - let encountered = '';
2581 - if (typeof href !== 'string' || !href) {
2582 - encountered += ` The \`href\` argument encountered was ${getValueDescriptorExpectingObjectForWarning(
2583 - href,
2584 - )}.`;
2397 + const ownerDocument = getDocumentForImperativeFloatMethods();
2398 +
2399 + if (src) {
2400 + const scripts = getResourcesFromRoot(ownerDocument).hoistableScripts;
2401 +
2402 + const key = getScriptKey(src);
2403 +
2404 + // Check if this resource already exists
2405 + let resource = scripts.get(key);
2406 + if (resource) {
2407 + // We can early return. The resource exists and there is nothing
2408 + // more to do
2409 + return;
2410 }
2586 - if (options !== undefined && typeof options !== 'object') {
2587 - encountered += ` The \`options\` argument encountered was ${getValueDescriptorExpectingObjectForWarning(
2411 +
2412 + // Attempt to hydrate instance from DOM
2413 + let instance: null | Instance = ownerDocument.querySelector(
2414 + getScriptSelectorFromKey(key),
2415 + );
2416 + if (!instance) {
2417 + // Construct a new instance and insert it
2418 + const scriptProps = Object.assign(
2419 + ({
2420 + src,
2421 + async: true,
2422 + }: ScriptProps),
2423 options,
2589 - )}.`;
2590 - } else if (options && 'as' in options && options.as !== 'script') {
2591 - encountered += ` The \`as\` option encountered was ${getValueDescriptorExpectingEnumForWarning(
2592 - options.as,
2593 - )}.`;
2594 - }
2595 - if (encountered) {
2596 - console.error(
2597 - 'ReactDOM.preinitModule(): Expected up to two arguments, a non-empty `href` string and, optionally, an `options` object with a valid `as` property.%s',
2598 - encountered,
2424 );
2600 - } else {
2601 - const as =
2602 - options && typeof options.as === 'string' ? options.as : 'script';
2603 - switch (as) {
2604 - case 'script': {
2605 - break;
2606 - }
2607 -
2608 - // We have an invalid as type and need to warn
2609 - default: {
2610 - const typeOfAs = getValueDescriptorExpectingEnumForWarning(as);
2611 - console.error(
2612 - 'ReactDOM.preinitModule(): Currently the only supported "as" type for this function is "script"' +
2613 - ' but received "%s" instead. This warning was generated for `href` "%s". In the future other' +
2614 - ' module types will be supported, aligning with the import-attributes proposal. Learn more here:' +
2615 - ' (https://github.com/tc39/proposal-import-attributes)',
2616 - typeOfAs,
2617 - href,
2618 - );
2619 - }
2425 + // Adopt certain preload props
2426 + const preloadProps = preloadPropsMap.get(key);
2427 + if (preloadProps) {
2428 + adoptPreloadPropsForScript(scriptProps, preloadProps);
2429 }
2430 + instance = ownerDocument.createElement('script');
2431 + markNodeAsHoistable(instance);
2432 + setInitialProperties(instance, 'link', scriptProps);
2433 + (ownerDocument.head: any).appendChild(instance);
2434 }
2622 - }
2623 - const ownerDocument = getDocumentForImperativeFloatMethods();
2435
2625 - if (typeof href === 'string' && href) {
2626 - const as =
2627 - options && typeof options.as === 'string' ? options.as : 'script';
2436 + // Construct a Resource and cache it
2437 + resource = {
2438 + type: 'script',
2439 + instance,
2440 + count: 1,
2441 + state: null,
2442 + };
2443 + scripts.set(key, resource);
2444 + return;
2445 + }
2446 +}
2447
2629 - switch (as) {
2630 - case 'script': {
2631 - const src = href;
2632 - const scripts = getResourcesFromRoot(ownerDocument).hoistableScripts;
2448 +function preinitModuleScript(
2449 + src: string,
2450 + options?: ?PreinitModuleScriptOptions,
2451 +) {
2452 + if (!enableFloat) {
2453 + return;
2454 + }
2455 + const ownerDocument = getDocumentForImperativeFloatMethods();
2456
2634 - const key = getScriptKey(src);
2457 + if (src) {
2458 + const scripts = getResourcesFromRoot(ownerDocument).hoistableScripts;
2459
2636 - // Check if this resource already exists
2637 - let resource = scripts.get(key);
2638 - if (resource) {
2639 - // We can early return. The resource exists and there is nothing
2640 - // more to do
2641 - return;
2642 - }
2460 + const key = getScriptKey(src);
2461
2644 - // Attempt to hydrate instance from DOM
2645 - let instance: null | Instance = ownerDocument.querySelector(
2646 - getScriptSelectorFromKey(key),
2647 - );
2648 - if (!instance) {
2649 - // Construct a new instance and insert it
2650 - const scriptProps = modulePropsFromPreinitModuleOptions(src, options);
2651 - // Adopt certain preload props
2652 - const preloadProps = preloadPropsMap.get(key);
2653 - if (preloadProps) {
2654 - adoptPreloadPropsForScript(scriptProps, preloadProps);
2655 - }
2656 - instance = ownerDocument.createElement('script');
2657 - markNodeAsHoistable(instance);
2658 - setInitialProperties(instance, 'link', scriptProps);
2659 - (ownerDocument.head: any).appendChild(instance);
2660 - }
2462 + // Check if this resource already exists
2463 + let resource = scripts.get(key);
2464 + if (resource) {
2465 + // We can early return. The resource exists and there is nothing
2466 + // more to do
2467 + return;
2468 + }
2469
2662 - // Construct a Resource and cache it
2663 - resource = {
2664 - type: 'script',
2665 - instance,
2666 - count: 1,
2667 - state: null,
2668 - };
2669 - scripts.set(key, resource);
2670 - return;
2470 + // Attempt to hydrate instance from DOM
2471 + let instance: null | Instance = ownerDocument.querySelector(
2472 + getScriptSelectorFromKey(key),
2473 + );
2474 + if (!instance) {
2475 + // Construct a new instance and insert it
2476 + const scriptProps = Object.assign(
2477 + ({
2478 + src,
2479 + async: true,
2480 + type: 'module',
2481 + }: ScriptProps),
2482 + options,
2483 + );
2484 + // Adopt certain preload props
2485 + const preloadProps = preloadPropsMap.get(key);
2486 + if (preloadProps) {
2487 + adoptPreloadPropsForScript(scriptProps, preloadProps);
2488 }
2489 + instance = ownerDocument.createElement('script');
2490 + markNodeAsHoistable(instance);
2491 + setInitialProperties(instance, 'link', scriptProps);
2492 + (ownerDocument.head: any).appendChild(instance);
2493 }
2673 - }
2674 -}
2675 -
2676 -function stylesheetPropsFromPreinitOptions(
2677 - href: string,
2678 - precedence: string,
2679 - options: PreinitOptions,
2680 -): StylesheetProps {
2681 - return {
2682 - rel: 'stylesheet',
2683 - href,
2684 - 'data-precedence': precedence,
2685 - crossOrigin: options.crossOrigin,
2686 - integrity: options.integrity,
2687 - fetchPriority: options.fetchPriority,
2688 - };
2689 -}
2690 -
2691 -function scriptPropsFromPreinitOptions(
2692 - src: string,
2693 - options: PreinitOptions,
2694 -): ScriptProps {
2695 - return {
2696 - src,
2697 - async: true,
2698 - crossOrigin: options.crossOrigin,
2699 - integrity: options.integrity,
2700 - nonce: options.nonce,
2701 - fetchPriority: options.fetchPriority,
2702 - };
2703 -}
2494
2705 -function modulePropsFromPreinitModuleOptions(
2706 - src: string,
2707 - options: ?PreinitModuleOptions,
2708 -): ScriptProps {
2709 - return {
2710 - src,
2711 - async: true,
2712 - type: 'module',
2713 - crossOrigin: options ? options.crossOrigin : undefined,
2714 - integrity: options ? options.integrity : undefined,
2715 - };
2495 + // Construct a Resource and cache it
2496 + resource = {
2497 + type: 'script',
2498 + instance,
2499 + count: 1,
2500 + state: null,
2501 + };
2502 + scripts.set(key, resource);
2503 + return;
2504 + }
2505 }
2506
2507 type StyleTagQualifyingProps = {
packages/react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js
+136 -41
@@ -9,12 +9,12 @@
9
10 import type {
11 HostDispatcher,
12 - PrefetchDNSOptions,
13 - PreconnectOptions,
14 - PreloadOptions,
15 - PreloadModuleOptions,
16 - PreinitOptions,
17 - PreinitModuleOptions,
12 + CrossOriginEnum,
13 + PreloadImplOptions,
14 + PreloadModuleImplOptions,
15 + PreinitStyleOptions,
16 + PreinitScriptOptions,
17 + PreinitModuleScriptOptions,
18 } from 'react-dom/src/shared/ReactDOMTypes';
19
20 import {enableFloat} from 'shared/ReactFeatureFlags';
@@ -30,137 +30,232 @@ export const ReactDOMFlightServerDispatcher: HostDispatcher = {
30 preconnect,
31 preload,
32 preloadModule,
33 - preinit,
34 - preinitModule,
33 + preinitStyle,
34 + preinitScript,
35 + preinitModuleScript,
36 };
37
37 -function prefetchDNS(href: string, options?: ?PrefetchDNSOptions) {
38 +function prefetchDNS(href: string) {
39 + if (enableFloat) {
40 + if (typeof href === 'string' && href) {
41 + const request = resolveRequest();
42 + if (request) {
43 + const hints = getHints(request);
44 + const key = 'D|' + href;
45 + if (hints.has(key)) {
46 + // duplicate hint
47 + return;
48 + }
49 + hints.add(key);
50 + emitHint(request, 'D', href);
51 + }
52 + }
53 + }
54 +}
55 +
56 +function preconnect(href: string, crossOrigin?: ?CrossOriginEnum) {
57 if (enableFloat) {
58 if (typeof href === 'string') {
59 const request = resolveRequest();
60 if (request) {
61 const hints = getHints(request);
43 - const key = 'D' + href;
62 +
63 + const key = `C|${crossOrigin == null ? 'null' : crossOrigin}|${href}`;
64 if (hints.has(key)) {
65 // duplicate hint
66 return;
67 }
68 hints.add(key);
49 - if (options) {
50 - emitHint(request, 'D', [href, options]);
69 + if (typeof crossOrigin === 'string') {
70 + emitHint(request, 'C', [href, crossOrigin]);
71 } else {
52 - emitHint(request, 'D', href);
72 + emitHint(request, 'C', href);
73 }
74 }
75 }
76 }
77 }
78
59 -function preconnect(href: string, options?: ?PreconnectOptions) {
79 +function preload(href: string, as: string, options?: ?PreloadImplOptions) {
80 if (enableFloat) {
81 if (typeof href === 'string') {
82 const request = resolveRequest();
83 if (request) {
84 const hints = getHints(request);
65 - const crossOrigin =
66 - options == null || typeof options.crossOrigin !== 'string'
67 - ? null
68 - : options.crossOrigin === 'use-credentials'
69 - ? 'use-credentials'
70 - : '';
71 -
72 - const key = `C${crossOrigin === null ? 'null' : crossOrigin}|${href}`;
85 + let key = 'L';
86 + if (as === 'image' && options) {
87 + key += getImagePreloadKey(
88 + href,
89 + options.imageSrcSet,
90 + options.imageSizes,
91 + );
92 + } else {
93 + key += `[${as}]${href}`;
94 + }
95 if (hints.has(key)) {
96 // duplicate hint
97 return;
98 }
99 hints.add(key);
78 - if (options) {
79 - emitHint(request, 'C', [href, options]);
100 +
101 + const trimmed = trimOptions(options);
102 + if (trimmed) {
103 + emitHint(request, 'L', [href, as, trimmed]);
104 } else {
81 - emitHint(request, 'C', href);
105 + emitHint(request, 'L', [href, as]);
106 }
107 }
108 }
109 }
110 }
111
88 -function preload(href: string, options: PreloadOptions) {
112 +function preloadModule(href: string, options?: ?PreloadModuleImplOptions) {
113 if (enableFloat) {
114 if (typeof href === 'string') {
115 const request = resolveRequest();
116 if (request) {
117 const hints = getHints(request);
94 - const key = 'L' + href;
118 + const key = 'm|' + href;
119 if (hints.has(key)) {
120 // duplicate hint
121 return;
122 }
123 hints.add(key);
100 - emitHint(request, 'L', [href, options]);
124 +
125 + const trimmed = trimOptions(options);
126 + if (trimmed) {
127 + return emitHint(request, 'm', [href, trimmed]);
128 + } else {
129 + return emitHint(request, 'm', href);
130 + }
131 }
132 }
133 }
134 }
135
106 -function preloadModule(href: string, options?: ?PreloadModuleOptions) {
136 +function preinitStyle(
137 + href: string,
138 + precedence: ?string,
139 + options?: ?PreinitStyleOptions,
140 +) {
141 if (enableFloat) {
142 if (typeof href === 'string') {
143 const request = resolveRequest();
144 if (request) {
145 const hints = getHints(request);
112 - const key = 'm' + href;
146 + const key = 'S|' + href;
147 if (hints.has(key)) {
148 // duplicate hint
149 return;
150 }
151 hints.add(key);
118 - if (options) {
119 - emitHint(request, 'm', [href, options]);
152 +
153 + const trimmed = trimOptions(options);
154 + if (trimmed) {
155 + return emitHint(request, 'S', [
156 + href,
157 + typeof precedence === 'string' ? precedence : 0,
158 + trimmed,
159 + ]);
160 + } else if (typeof precedence === 'string') {
161 + return emitHint(request, 'S', [href, precedence]);
162 } else {
121 - emitHint(request, 'm', href);
163 + return emitHint(request, 'S', href);
164 }
165 }
166 }
167 }
168 }
169
128 -function preinit(href: string, options: PreinitOptions) {
170 +function preinitScript(href: string, options?: ?PreinitScriptOptions) {
171 if (enableFloat) {
172 if (typeof href === 'string') {
173 const request = resolveRequest();
174 if (request) {
175 const hints = getHints(request);
134 - const key = 'I' + href;
176 + const key = 'X|' + href;
177 if (hints.has(key)) {
178 // duplicate hint
179 return;
180 }
181 hints.add(key);
140 - emitHint(request, 'I', [href, options]);
182 +
183 + const trimmed = trimOptions(options);
184 + if (trimmed) {
185 + return emitHint(request, 'X', [href, trimmed]);
186 + } else {
187 + return emitHint(request, 'X', href);
188 + }
189 }
190 }
191 }
192 }
193
146 -function preinitModule(href: string, options?: ?PreinitModuleOptions) {
194 +function preinitModuleScript(
195 + href: string,
196 + options?: ?PreinitModuleScriptOptions,
197 +) {
198 if (enableFloat) {
199 if (typeof href === 'string') {
200 const request = resolveRequest();
201 if (request) {
202 const hints = getHints(request);
152 - const key = 'M' + href;
203 + const key = 'M|' + href;
204 if (hints.has(key)) {
205 // duplicate hint
206 return;
207 }
208 hints.add(key);
158 - if (options) {
159 - emitHint(request, 'M', [href, options]);
209 +
210 + const trimmed = trimOptions(options);
211 + if (trimmed) {
212 + return emitHint(request, 'M', [href, trimmed]);
213 } else {
161 - emitHint(request, 'M', href);
214 + return emitHint(request, 'M', href);
215 }
216 }
217 }
218 }
219 }
220 +
221 +// Flight normally encodes undefined as a special character however for directive option
222 +// arguments we don't want to send unnecessary keys and bloat the payload so we create a
223 +// trimmed object which omits any keys with null or undefined values.
224 +// This is only typesafe because these option objects have entirely optional fields where
225 +// null and undefined represent the same thing as no property.
226 +function trimOptions<
227 + T:
228 + | PreloadImplOptions
229 + | PreloadModuleImplOptions
230 + | PreinitStyleOptions
231 + | PreinitScriptOptions
232 + | PreinitModuleScriptOptions,
233 +>(options: ?T): ?T {
234 + if (options == null) return null;
235 + let hasProperties = false;
236 + const trimmed: T = ({}: any);
237 + for (const key in options) {
238 + if (options[key] != null) {
239 + hasProperties = true;
240 + (trimmed: any)[key] = options[key];
241 + }
242 + }
243 + return hasProperties ? trimmed : null;
244 +}
245 +
246 +function getImagePreloadKey(
247 + href: string,
248 + imageSrcSet: ?string,
249 + imageSizes: ?string,
250 +) {
251 + let uniquePart = '';
252 + if (typeof imageSrcSet === 'string' && imageSrcSet !== '') {
253 + uniquePart += '[' + imageSrcSet + ']';
254 + if (typeof imageSizes === 'string') {
255 + uniquePart += '[' + imageSizes + ']';
256 + }
257 + } else {
258 + uniquePart += '[][]' + href;
259 + }
260 + return `[image]${uniquePart}`;
261 +}
packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
+157 -361
@@ -9,12 +9,12 @@
9
10 import type {ReactNodeList, ReactCustomFormAction} from 'shared/ReactTypes';
11 import type {
12 - PrefetchDNSOptions,
13 - PreconnectOptions,
14 - PreloadOptions,
15 - PreloadModuleOptions,
16 - PreinitOptions,
17 - PreinitModuleOptions,
12 + CrossOriginEnum,
13 + PreloadImplOptions,
14 + PreloadModuleImplOptions,
15 + PreinitStyleOptions,
16 + PreinitScriptOptions,
17 + PreinitModuleScriptOptions,
18 ImportMap,
19 } from 'react-dom/src/shared/ReactDOMTypes';
20
@@ -79,10 +79,7 @@ import {
79 formReplaying as formReplayingRuntime,
80 } from './fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings';
81
82 -import {
83 - getValueDescriptorExpectingObjectForWarning,
84 - getValueDescriptorExpectingEnumForWarning,
85 -} from '../shared/ReactDOMResourceValidation';
82 +import {getValueDescriptorExpectingObjectForWarning} from '../shared/ReactDOMResourceValidation';
83
84 import {NotPending} from '../shared/ReactDOMFormActions';
85
@@ -94,8 +91,9 @@ const ReactDOMServerDispatcher = {
91 preconnect,
92 preload,
93 preloadModule,
97 - preinit,
98 - preinitModule,
94 + preinitStyle,
95 + preinitScript,
96 + preinitModuleScript,
97 };
98
99 export function prepareHostDispatcher() {
@@ -5001,7 +4999,7 @@ function getResourceKey(as: string, href: string): string {
4999 return `[${as}]${href}`;
5000 }
5001
5004 -function prefetchDNS(href: string, options?: ?PrefetchDNSOptions) {
5002 +function prefetchDNS(href: string) {
5003 if (!enableFloat) {
5004 return;
5005 }
@@ -5015,29 +5013,6 @@ function prefetchDNS(href: string, options?: ?PrefetchDNSOptions) {
5013 return;
5014 }
5015 const resumableState = getResumableState(request);
5018 - if (__DEV__) {
5019 - if (typeof href !== 'string' || !href) {
5020 - console.error(
5021 - 'ReactDOM.prefetchDNS(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.',
5022 - getValueDescriptorExpectingObjectForWarning(href),
5023 - );
5024 - } else if (options != null) {
5025 - if (
5026 - typeof options === 'object' &&
5027 - options.hasOwnProperty('crossOrigin')
5028 - ) {
5029 - console.error(
5030 - '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`.',
5031 - getValueDescriptorExpectingEnumForWarning(options),
5032 - );
5033 - } else {
5034 - console.error(
5035 - '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`.',
5036 - getValueDescriptorExpectingEnumForWarning(options),
5037 - );
5038 - }
5039 - }
5040 - }
5016
5017 if (typeof href === 'string' && href) {
5018 const key = getResourceKey('prefetchDNS', href);
@@ -5060,7 +5035,7 @@ function prefetchDNS(href: string, options?: ?PrefetchDNSOptions) {
5035 }
5036 }
5037
5063 -function preconnect(href: string, options?: ?PreconnectOptions) {
5038 +function preconnect(href: string, crossOrigin: ?CrossOriginEnum) {
5039 if (!enableFloat) {
5040 return;
5041 }
@@ -5074,35 +5049,10 @@ function preconnect(href: string, options?: ?PreconnectOptions) {
5049 return;
5050 }
5051 const resumableState = getResumableState(request);
5077 - if (__DEV__) {
5078 - if (typeof href !== 'string' || !href) {
5079 - console.error(
5080 - 'ReactDOM.preconnect(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.',
5081 - getValueDescriptorExpectingObjectForWarning(href),
5082 - );
5083 - } else if (options != null && typeof options !== 'object') {
5084 - console.error(
5085 - '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.',
5086 - getValueDescriptorExpectingEnumForWarning(options),
5087 - );
5088 - } else if (options != null && typeof options.crossOrigin !== 'string') {
5089 - console.error(
5090 - '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.',
5091 - getValueDescriptorExpectingObjectForWarning(options.crossOrigin),
5092 - );
5093 - }
5094 - }
5052
5053 if (typeof href === 'string' && href) {
5097 - const crossOrigin =
5098 - options == null || typeof options.crossOrigin !== 'string'
5099 - ? null
5100 - : options.crossOrigin === 'use-credentials'
5101 - ? 'use-credentials'
5102 - : '';
5103 -
5054 const key = `[preconnect][${
5105 - crossOrigin === null ? 'null' : crossOrigin
5055 + typeof crossOrigin === 'string' ? crossOrigin : 'null'
5056 }]${href}`;
5057 let resource = resumableState.preconnectsMap.get(key);
5058 if (!resource) {
@@ -5123,7 +5073,7 @@ function preconnect(href: string, options?: ?PreconnectOptions) {
5073 }
5074 }
5075
5126 -function preload(href: string, options: PreloadOptions) {
5076 +function preload(href: string, as: string, options?: ?PreloadImplOptions) {
5077 if (!enableFloat) {
5078 return;
5079 }
@@ -5137,62 +5087,39 @@ function preload(href: string, options: PreloadOptions) {
5087 return;
5088 }
5089 const resumableState = getResumableState(request);
5140 - if (__DEV__) {
5141 - let encountered = '';
5142 - if (typeof href !== 'string' || !href) {
5143 - encountered += ` The \`href\` argument encountered was ${getValueDescriptorExpectingObjectForWarning(
5144 - href,
5145 - )}.`;
5146 - }
5147 - if (options == null || typeof options !== 'object') {
5148 - encountered += ` The \`options\` argument encountered was ${getValueDescriptorExpectingObjectForWarning(
5149 - options,
5150 - )}.`;
5151 - } else if (typeof options.as !== 'string' || !options.as) {
5152 - encountered += ` The \`as\` option encountered was ${getValueDescriptorExpectingObjectForWarning(
5153 - options.as,
5154 - )}.`;
5155 - }
5156 - if (encountered) {
5157 - console.error(
5158 - 'ReactDOM.preload(): Expected two arguments, a non-empty `href` string and an `options` object with an `as` property valid for a `<link rel="preload" as="..." />` tag.%s',
5159 - encountered,
5160 - );
5161 - }
5162 - }
5163 - if (
5164 - typeof href === 'string' &&
5165 - href &&
5166 - typeof options === 'object' &&
5167 - options !== null &&
5168 - typeof options.as === 'string' &&
5169 - options.as
5170 - ) {
5171 - const as = options.as;
5090 + if (as && href) {
5091 + options = options || {};
5092 let key: string;
5093 if (as === 'image') {
5094 // For image preloads the key contains either the imageSrcSet + imageSizes or the href but not
5095 // both. This is to prevent identical calls with the same srcSet and sizes to be duplicated
5096 // by varying the href. this is an edge case but it is the most correct behavior.
5177 - const {imageSrcSet, imageSizes} = options;
5178 - key = getImagePreloadKey(href, imageSrcSet, imageSizes);
5097 + key = getImagePreloadKey(href, options.imageSrcSet, options.imageSizes);
5098 } else {
5099 key = getResourceKey(as, href);
5100 }
5101 let resource = resumableState.preloadsMap.get(key);
5102 if (!resource) {
5103 + const props = Object.assign(
5104 + ({
5105 + rel: 'preload',
5106 + href: as === 'image' && options.imageSrcSet ? undefined : href,
5107 + as,
5108 + }: PreloadProps),
5109 + options,
5110 + );
5111 resource = {
5112 type: 'preload',
5113 chunks: [],
5114 state: NoState,
5188 - props: preloadPropsFromPreloadOptions(href, as, options),
5115 + props,
5116 };
5117 resumableState.preloadsMap.set(key, resource);
5118 pushLinkImpl(resource.chunks, resource.props);
5119 }
5120 if (as === 'font') {
5121 resumableState.fontPreloads.add(resource);
5195 - } else if (as === 'image' && options.fetchPriority === 'high') {
5122 + } else if (as === 'image' && resource.props.fetchPriority === 'high') {
5123 resumableState.highImagePreloads.add(resource);
5124 } else {
5125 resumableState.bulkPreloads.add(resource);
@@ -5201,7 +5128,10 @@ function preload(href: string, options: PreloadOptions) {
5128 }
5129 }
5130
5204 -function preloadModule(href: string, options?: ?PreloadModuleOptions): void {
5131 +function preloadModule(
5132 + href: string,
5133 + options?: ?PreloadModuleImplOptions,
5134 +): void {
5135 if (!enableFloat) {
5136 return;
5137 }
@@ -5215,40 +5145,24 @@ function preloadModule(href: string, options?: ?PreloadModuleOptions): void {
5145 return;
5146 }
5147 const resumableState = getResumableState(request);
5218 - if (__DEV__) {
5219 - let encountered = '';
5220 - if (typeof href !== 'string' || !href) {
5221 - encountered += ` The \`href\` argument encountered was ${getValueDescriptorExpectingObjectForWarning(
5222 - href,
5223 - )}.`;
5224 - }
5225 - if (options !== undefined && typeof options !== 'object') {
5226 - encountered += ` The \`options\` argument encountered was ${getValueDescriptorExpectingObjectForWarning(
5227 - options,
5228 - )}.`;
5229 - } else if (options && 'as' in options && typeof options.as !== 'string') {
5230 - encountered += ` The \`as\` option encountered was ${getValueDescriptorExpectingObjectForWarning(
5231 - options.as,
5232 - )}.`;
5233 - }
5234 - if (encountered) {
5235 - console.error(
5236 - 'ReactDOM.preloadModule(): Expected two arguments, a non-empty `href` string and, optionally, an `options` object with an `as` property valid for a `<link rel="modulepreload" as="..." />` tag.%s',
5237 - encountered,
5238 - );
5239 - }
5240 - }
5241 - if (typeof href === 'string' && href) {
5148 + if (href) {
5149 const as =
5150 options && typeof options.as === 'string' ? options.as : 'script';
5151 const key = getResourceKey(as, href);
5152 let resource = resumableState.preloadsMap.get(key);
5153 + const props: PreloadModuleProps = Object.assign(
5154 + ({
5155 + rel: 'modulepreload',
5156 + href,
5157 + }: PreloadModuleProps),
5158 + options,
5159 + );
5160 if (!resource) {
5161 resource = {
5162 type: 'preload',
5163 chunks: [],
5164 state: NoState,
5251 - props: preloadModulePropsFromPreloadModuleOptions(href, as, options),
5165 + props,
5166 };
5167 resumableState.preloadsMap.set(key, resource);
5168 pushLinkImpl(resource.chunks, resource.props);
@@ -5258,7 +5172,11 @@ function preloadModule(href: string, options?: ?PreloadModuleOptions): void {
5172 }
5173 }
5174
5261 -function preinit(href: string, options: PreinitOptions): void {
5175 +function preinitStyle(
5176 + href: string,
5177 + precedence: ?string,
5178 + options?: ?PreinitStyleOptions,
5179 +): void {
5180 if (!enableFloat) {
5181 return;
5182 }
@@ -5272,102 +5190,64 @@ function preinit(href: string, options: PreinitOptions): void {
5190 return;
5191 }
5192 const resumableState = getResumableState(request);
5275 - if (__DEV__) {
5276 - if (typeof href !== 'string' || !href) {
5277 - console.error(
5278 - 'ReactDOM.preinit(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.',
5279 - getValueDescriptorExpectingObjectForWarning(href),
5280 - );
5281 - } else if (options == null || typeof options !== 'object') {
5282 - console.error(
5283 - '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.',
5284 - getValueDescriptorExpectingEnumForWarning(options),
5285 - );
5286 - } else if (options.as !== 'style' && options.as !== 'script') {
5287 - console.error(
5288 - '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".',
5289 - getValueDescriptorExpectingEnumForWarning(options.as),
5193 + if (href) {
5194 + const as = 'style';
5195 + const key = getResourceKey(as, href);
5196 + let resource = resumableState.stylesMap.get(key);
5197 + if (!resource) {
5198 + precedence = precedence || 'default';
5199 + let state = NoState;
5200 + const preloadResource = resumableState.preloadsMap.get(key);
5201 + if (preloadResource && preloadResource.state & Flushed) {
5202 + state = PreloadFlushed;
5203 + }
5204 + const props: StylesheetProps = Object.assign(
5205 + ({
5206 + rel: 'stylesheet',
5207 + href,
5208 + 'data-precedence': precedence,
5209 + }: StylesheetProps),
5210 + options,
5211 );
5291 - }
5292 - }
5293 - if (
5294 - typeof href === 'string' &&
5295 - href &&
5296 - typeof options === 'object' &&
5297 - options !== null
5298 - ) {
5299 - const as = options.as;
5300 - switch (as) {
5301 - case 'style': {
5302 - const key = getResourceKey(as, href);
5303 - let resource = resumableState.stylesMap.get(key);
5304 - const precedence = options.precedence || 'default';
5305 - if (!resource) {
5306 - let state = NoState;
5307 - const preloadResource = resumableState.preloadsMap.get(key);
5308 - if (preloadResource && preloadResource.state & Flushed) {
5309 - state = PreloadFlushed;
5310 - }
5311 - resource = {
5312 - type: 'stylesheet',
5313 - chunks: ([]: Array<Chunk | PrecomputedChunk>),
5314 - state,
5315 - props: stylesheetPropsFromPreinitOptions(href, precedence, options),
5316 - };
5317 - resumableState.stylesMap.set(key, resource);
5318 - let precedenceSet = resumableState.precedences.get(precedence);
5319 - if (!precedenceSet) {
5320 - precedenceSet = new Set();
5321 - resumableState.precedences.set(precedence, precedenceSet);
5322 - const emptyStyleResource = {
5323 - type: 'style',
5324 - chunks: ([]: Array<Chunk | PrecomputedChunk>),
5325 - state: NoState,
5326 - props: {
5327 - precedence,
5328 - hrefs: ([]: Array<string>),
5329 - },
5330 - };
5331 - precedenceSet.add(emptyStyleResource);
5332 - if (__DEV__) {
5333 - if (resumableState.stylePrecedences.has(precedence)) {
5334 - console.error(
5335 - 'React constructed an empty style resource when a style resource already exists for this precedence: "%s". This is a bug in React.',
5336 - precedence,
5337 - );
5338 - }
5339 - }
5340 - resumableState.stylePrecedences.set(precedence, emptyStyleResource);
5212 + resource = {
5213 + type: 'stylesheet',
5214 + chunks: ([]: Array<Chunk | PrecomputedChunk>),
5215 + state,
5216 + props,
5217 + };
5218 + resumableState.stylesMap.set(key, resource);
5219 + let precedenceSet = resumableState.precedences.get(precedence);
5220 + if (!precedenceSet) {
5221 + precedenceSet = new Set();
5222 + resumableState.precedences.set(precedence, precedenceSet);
5223 + const emptyStyleResource = {
5224 + type: 'style',
5225 + chunks: ([]: Array<Chunk | PrecomputedChunk>),
5226 + state: NoState,
5227 + props: {
5228 + precedence,
5229 + hrefs: ([]: Array<string>),
5230 + },
5231 + };
5232 + precedenceSet.add(emptyStyleResource);
5233 + if (__DEV__) {
5234 + if (resumableState.stylePrecedences.has(precedence)) {
5235 + console.error(
5236 + 'React constructed an empty style resource when a style resource already exists for this precedence: "%s". This is a bug in React.',
5237 + precedence,
5238 + );
5239 }
5342 - precedenceSet.add(resource);
5343 - flushResources(request);
5240 }
5345 - return;
5346 - }
5347 - case 'script': {
5348 - const src = href;
5349 - const key = getResourceKey(as, src);
5350 - let resource = resumableState.scriptsMap.get(key);
5351 - if (!resource) {
5352 - resource = {
5353 - type: 'script',
5354 - chunks: [],
5355 - state: NoState,
5356 - props: null,
5357 - };
5358 - resumableState.scriptsMap.set(key, resource);
5359 - const resourceProps = scriptPropsFromPreinitOptions(src, options);
5360 - resumableState.scripts.add(resource);
5361 - pushScriptImpl(resource.chunks, resourceProps);
5362 - flushResources(request);
5363 - }
5364 - return;
5241 + resumableState.stylePrecedences.set(precedence, emptyStyleResource);
5242 }
5243 + precedenceSet.add(resource);
5244 + flushResources(request);
5245 }
5246 + return;
5247 }
5248 }
5249
5370 -function preinitModule(href: string, options?: ?PreinitModuleOptions): void {
5250 +function preinitScript(src: string, options?: ?PreinitScriptOptions): void {
5251 if (!enableFloat) {
5252 return;
5253 }
@@ -5381,77 +5261,73 @@ function preinitModule(href: string, options?: ?PreinitModuleOptions): void {
5261 return;
5262 }
5263 const resumableState = getResumableState(request);
5384 - if (__DEV__) {
5385 - let encountered = '';
5386 - if (typeof href !== 'string' || !href) {
5387 - encountered += ` The \`href\` argument encountered was ${getValueDescriptorExpectingObjectForWarning(
5388 - href,
5389 - )}.`;
5390 - }
5391 - if (options !== undefined && typeof options !== 'object') {
5392 - encountered += ` The \`options\` argument encountered was ${getValueDescriptorExpectingObjectForWarning(
5264 + if (src) {
5265 + const key = getResourceKey('script', src);
5266 + let resource = resumableState.scriptsMap.get(key);
5267 + if (!resource) {
5268 + resource = {
5269 + type: 'script',
5270 + chunks: [],
5271 + state: NoState,
5272 + props: null,
5273 + };
5274 + resumableState.scriptsMap.set(key, resource);
5275 + const props: ScriptProps = Object.assign(
5276 + ({
5277 + src,
5278 + async: true,
5279 + }: ScriptProps),
5280 options,
5394 - )}.`;
5395 - } else if (options && 'as' in options && options.as !== 'script') {
5396 - encountered += ` The \`as\` option encountered was ${getValueDescriptorExpectingEnumForWarning(
5397 - options.as,
5398 - )}.`;
5399 - }
5400 - if (encountered) {
5401 - console.error(
5402 - 'ReactDOM.preinitModule(): Expected up to two arguments, a non-empty `href` string and, optionally, an `options` object with a valid `as` property.%s',
5403 - encountered,
5281 );
5405 - } else {
5406 - const as =
5407 - options && typeof options.as === 'string' ? options.as : 'script';
5408 - switch (as) {
5409 - case 'script': {
5410 - break;
5411 - }
5412 -
5413 - // We have an invalid as type and need to warn
5414 - default: {
5415 - const typeOfAs = getValueDescriptorExpectingEnumForWarning(as);
5416 - console.error(
5417 - 'ReactDOM.preinitModule(): Currently the only supported "as" type for this function is "script"' +
5418 - ' but received "%s" instead. This warning was generated for `href` "%s". In the future other' +
5419 - ' module types will be supported, aligning with the import-attributes proposal. Learn more here:' +
5420 - ' (https://github.com/tc39/proposal-import-attributes)',
5421 - typeOfAs,
5422 - href,
5423 - );
5424 - }
5425 - }
5282 + resumableState.scripts.add(resource);
5283 + pushScriptImpl(resource.chunks, props);
5284 + flushResources(request);
5285 }
5286 + return;
5287 }
5428 - if (typeof href === 'string' && href) {
5429 - const as =
5430 - options && typeof options.as === 'string' ? options.as : 'script';
5431 - switch (as) {
5432 - case 'script': {
5433 - const src = href;
5434 - const key = getResourceKey(as, src);
5435 - let resource = resumableState.scriptsMap.get(key);
5436 - if (!resource) {
5437 - resource = {
5438 - type: 'script',
5439 - chunks: [],
5440 - state: NoState,
5441 - props: null,
5442 - };
5443 - resumableState.scriptsMap.set(key, resource);
5444 - const resourceProps = modulePropsFromPreinitModuleOptions(
5445 - src,
5446 - options,
5447 - );
5448 - resumableState.scripts.add(resource);
5449 - pushScriptImpl(resource.chunks, resourceProps);
5450 - flushResources(request);
5451 - }
5452 - return;
5453 - }
5288 +}
5289 +
5290 +function preinitModuleScript(
5291 + src: string,
5292 + options?: ?PreinitModuleScriptOptions,
5293 +): void {
5294 + if (!enableFloat) {
5295 + return;
5296 + }
5297 + const request = resolveRequest();
5298 + if (!request) {
5299 + // In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also
5300 + // possibly get them from the stack if we are not in an async context. Since we were not able to resolve
5301 + // the resources for this call in either case we opt to do nothing. We can consider making this a warning
5302 + // but there may be times where calling a function outside of render is intentional (i.e. to warm up data
5303 + // fetching) and we don't want to warn in those cases.
5304 + return;
5305 + }
5306 + const resumableState = getResumableState(request);
5307 + if (src) {
5308 + const key = getResourceKey('script', src);
5309 + let resource = resumableState.scriptsMap.get(key);
5310 + if (!resource) {
5311 + resource = {
5312 + type: 'script',
5313 + chunks: [],
5314 + state: NoState,
5315 + props: null,
5316 + };
5317 + resumableState.scriptsMap.set(key, resource);
5318 + const props = Object.assign(
5319 + ({
5320 + src,
5321 + type: 'module',
5322 + async: true,
5323 + }: ModuleProps),
5324 + options,
5325 + );
5326 + resumableState.scripts.add(resource);
5327 + pushScriptImpl(resource.chunks, props);
5328 + flushResources(request);
5329 }
5330 + return;
5331 }
5332 }
5333
@@ -5561,44 +5437,6 @@ function internalPreinitScript(
5437 return;
5438 }
5439
5564 -function preloadPropsFromPreloadOptions(
5565 - href: string,
5566 - as: string,
5567 - options: PreloadOptions,
5568 -): PreloadProps {
5569 - return {
5570 - rel: 'preload',
5571 - as,
5572 - // There is a bug in Safari where imageSrcSet is not respected on preload links
5573 - // so we omit the href here if we have imageSrcSet b/c safari will load the wrong image.
5574 - // This harms older browers that do not support imageSrcSet by making their preloads not work
5575 - // but this population is shrinking fast and is already small so we accept this tradeoff.
5576 - href: as === 'image' && options.imageSrcSet ? undefined : href,
5577 - crossOrigin: as === 'font' ? '' : options.crossOrigin,
5578 - integrity: options.integrity,
5579 - type: options.type,
5580 - nonce: options.nonce,
5581 - fetchPriority: options.fetchPriority,
5582 - imageSrcSet: options.imageSrcSet,
5583 - imageSizes: options.imageSizes,
5584 - referrerPolicy: options.referrerPolicy,
5585 - };
5586 -}
5587 -
5588 -function preloadModulePropsFromPreloadModuleOptions(
5589 - href: string,
5590 - as: string,
5591 - options: ?PreloadModuleOptions,
5592 -): PreloadModuleProps {
5593 - return {
5594 - rel: 'modulepreload',
5595 - as: as !== 'script' ? as : undefined,
5596 - href,
5597 - crossOrigin: options ? options.crossOrigin : undefined,
5598 - integrity: options ? options.integrity : undefined,
5599 - };
5600 -}
5601 -
5440 function preloadAsStylePropsFromProps(href: string, props: any): PreloadProps {
5441 return {
5442 rel: 'preload',
@@ -5613,21 +5451,6 @@ function preloadAsStylePropsFromProps(href: string, props: any): PreloadProps {
5451 };
5452 }
5453
5616 -function stylesheetPropsFromPreinitOptions(
5617 - href: string,
5618 - precedence: string,
5619 - options: PreinitOptions,
5620 -): StylesheetProps {
5621 - return {
5622 - rel: 'stylesheet',
5623 - href,
5624 - 'data-precedence': precedence,
5625 - crossOrigin: options.crossOrigin,
5626 - integrity: options.integrity,
5627 - fetchPriority: options.fetchPriority,
5628 - };
5629 -}
5630 -
5454 function stylesheetPropsFromRawProps(rawProps: any): StylesheetProps {
5455 return {
5456 ...rawProps,
@@ -5646,33 +5469,6 @@ function adoptPreloadPropsForStylesheetProps(
5469 resourceProps.integrity = preloadProps.integrity;
5470 }
5471
5649 -function scriptPropsFromPreinitOptions(
5650 - src: string,
5651 - options: PreinitOptions,
5652 -): ScriptProps {
5653 - return {
5654 - src,
5655 - async: true,
5656 - crossOrigin: options.crossOrigin,
5657 - integrity: options.integrity,
5658 - nonce: options.nonce,
5659 - fetchPriority: options.fetchPriority,
5660 - };
5661 -}
5662 -
5663 -function modulePropsFromPreinitModuleOptions(
5664 - src: string,
5665 - options: ?PreinitModuleOptions,
5666 -): ModuleProps {
5667 - return {
5668 - src,
5669 - type: 'module',
5670 - async: true,
5671 - crossOrigin: options ? options.crossOrigin : undefined,
5672 - integrity: options ? options.integrity : undefined,
5673 - };
5674 -}
5675 -
5472 function adoptPreloadPropsForScriptProps(
5473 resourceProps: ScriptProps,
5474 preloadProps: PreloadProps,
packages/react-dom-bindings/src/server/ReactFlightServerConfigDOM.js
+41 -19
@@ -8,12 +8,12 @@
8 */
9
10 import type {
11 - PrefetchDNSOptions,
12 - PreconnectOptions,
13 - PreloadOptions,
14 - PreloadModuleOptions,
15 - PreinitOptions,
16 - PreinitModuleOptions,
11 + CrossOriginEnum,
12 + PreloadImplOptions,
13 + PreloadModuleImplOptions,
14 + PreinitStyleOptions,
15 + PreinitScriptOptions,
16 + PreinitModuleScriptOptions,
17 } from 'react-dom/src/shared/ReactDOMTypes';
18
19 import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
@@ -29,19 +29,41 @@ export function prepareHostDispatcher(): void {
29 // E.g. this can be used to distinguish legacy renderers from this modern one.
30 export const isPrimaryRenderer = true;
31
32 -export type HintModel =
33 - | string
34 - | [
35 - string,
36 - (
37 - | PrefetchDNSOptions
38 - | PreconnectOptions
39 - | PreloadOptions
40 - | PreloadModuleOptions
41 - | PreinitOptions
42 - | PreinitModuleOptions
43 - ),
44 - ];
32 +// We use zero to represent the absence of an explicit precedence because it is
33 +// small, smaller than how we encode undefined, and is unambiguous. We could use
34 +// a different tuple structure to encode this instead but this makes the runtime
35 +// cost cheaper by eliminating a type checks in more positions.
36 +type UnspecifiedPrecedence = 0;
37 +
38 +// prettier-ignore
39 +type TypeMap = {
40 + // prefetchDNS(href)
41 + 'D': /* href */ string,
42 + // preconnect(href, options)
43 + 'C':
44 + | /* href */ string
45 + | [/* href */ string, CrossOriginEnum],
46 + // preconnect(href, options)
47 + 'L':
48 + | [/* href */ string, /* as */ string]
49 + | [/* href */ string, /* as */ string, PreloadImplOptions],
50 + 'm':
51 + | /* href */ string
52 + | [/* href */ string, PreloadModuleImplOptions],
53 + 'S':
54 + | /* href */ string
55 + | [/* href */ string, /* precedence */ string]
56 + | [/* href */ string, /* precedence */ string | UnspecifiedPrecedence, PreinitStyleOptions],
57 + 'X':
58 + | /* href */ string
59 + | [/* href */ string, PreinitScriptOptions],
60 + 'M':
61 + | /* href */ string
62 + | [/* href */ string, PreinitModuleScriptOptions],
63 +}
64 +
65 +export type HintCode = $Keys<TypeMap>;
66 +export type HintModel<T: HintCode> = TypeMap[T];
67
68 export type Hints = Set<string>;
69
packages/react-dom-bindings/src/shared/ReactDOMResourceValidation.js
-40
@@ -62,46 +62,6 @@ function propNamesListJoin(
62 }
63 }
64
65 -export function validatePreinitArguments(href: mixed, options: mixed) {
66 - if (__DEV__) {
67 - if (!href || typeof href !== 'string') {
68 - const typeOfArg = getValueDescriptorExpectingObjectForWarning(href);
69 - console.error(
70 - 'ReactDOM.preinit() expected the first argument to be a string representing an href but found %s instead.',
71 - typeOfArg,
72 - );
73 - } else if (typeof options !== 'object' || options === null) {
74 - const typeOfArg = getValueDescriptorExpectingObjectForWarning(options);
75 - console.error(
76 - 'ReactDOM.preinit() expected the second argument to be an options argument containing at least an "as" property' +
77 - ' specifying the Resource type. It found %s instead. The href for the preload call where this warning originated is "%s".',
78 - typeOfArg,
79 - href,
80 - );
81 - } else {
82 - const as = options.as;
83 - switch (as) {
84 - case 'style':
85 - case 'script': {
86 - break;
87 - }
88 -
89 - // We have an invalid as type and need to warn
90 - default: {
91 - const typeOfAs = getValueDescriptorExpectingEnumForWarning(as);
92 - console.error(
93 - 'ReactDOM.preinit() expected the second argument to be an options argument containing at least an "as" property' +
94 - ' specifying the Resource type. It found %s instead. Currently, valid resource types for for preinit are "style"' +
95 - ' and "script". The href for the preinit call where this warning originated is "%s".',
96 - typeOfAs,
97 - href,
98 - );
99 - }
100 - }
101 - }
102 - }
103 -}
104 -
65 export function getValueDescriptorExpectingObjectForWarning(
66 thing: any,
67 ): string {
packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js
+73 -26
@@ -10,56 +10,103 @@
10 // This client file is in the shared folder because it applies to both SSR and browser contexts.
11 // It is the configuraiton of the FlightClient behavior which can run in either environment.
12
13 -import type {HintModel} from '../server/ReactFlightServerConfigDOM';
13 +import type {HintCode, HintModel} from '../server/ReactFlightServerConfigDOM';
14
15 import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
16 const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher;
17
18 -export function dispatchHint(code: string, model: HintModel): void {
18 +export function dispatchHint<Code: HintCode>(
19 + code: Code,
20 + model: HintModel<Code>,
21 +): void {
22 const dispatcher = ReactDOMCurrentDispatcher.current;
23 if (dispatcher) {
21 - let href, options;
22 - if (typeof model === 'string') {
23 - href = model;
24 - } else {
25 - href = model[0];
26 - options = model[1];
27 - }
24 switch (code) {
25 case 'D': {
30 - // $FlowFixMe[prop-missing] options are not refined to their types by code
31 - dispatcher.prefetchDNS(href, options);
26 + const refined = refineModel(code, model);
27 + const href = refined;
28 + dispatcher.prefetchDNS(href);
29 return;
30 }
31 case 'C': {
35 - // $FlowFixMe[prop-missing] options are not refined to their types by code
36 - dispatcher.preconnect(href, options);
32 + const refined = refineModel(code, model);
33 + if (typeof refined === 'string') {
34 + const href = refined;
35 + dispatcher.preconnect(href);
36 + } else {
37 + const href = refined[0];
38 + const crossOrigin = refined[1];
39 + dispatcher.preconnect(href, crossOrigin);
40 + }
41 return;
42 }
43 case 'L': {
40 - // $FlowFixMe[prop-missing] options are not refined to their types by code
41 - // $FlowFixMe[incompatible-call] options are not refined to their types by code
42 - dispatcher.preload(href, options);
44 + const refined = refineModel(code, model);
45 + const href = refined[0];
46 + const as = refined[1];
47 + if (refined.length === 3) {
48 + const options = refined[2];
49 + dispatcher.preload(href, as, options);
50 + } else {
51 + dispatcher.preload(href, as);
52 + }
53 return;
54 }
55 case 'm': {
46 - // $FlowFixMe[prop-missing] options are not refined to their types by code
47 - // $FlowFixMe[incompatible-call] options are not refined to their types by code
48 - dispatcher.preloadModule(href, options);
56 + const refined = refineModel(code, model);
57 + if (typeof refined === 'string') {
58 + const href = refined;
59 + dispatcher.preloadModule(href);
60 + } else {
61 + const href = refined[0];
62 + const options = refined[1];
63 + dispatcher.preloadModule(href, options);
64 + }
65 + return;
66 + }
67 + case 'S': {
68 + const refined = refineModel(code, model);
69 + if (typeof refined === 'string') {
70 + const href = refined;
71 + dispatcher.preinitStyle(href);
72 + } else {
73 + const href = refined[0];
74 + const precedence = refined[1] === 0 ? undefined : refined[1];
75 + const options = refined.length === 3 ? refined[2] : undefined;
76 + dispatcher.preinitStyle(href, precedence, options);
77 + }
78 return;
79 }
51 - case 'I': {
52 - // $FlowFixMe[prop-missing] options are not refined to their types by code
53 - // $FlowFixMe[incompatible-call] options are not refined to their types by code
54 - dispatcher.preinit(href, options);
80 + case 'X': {
81 + const refined = refineModel(code, model);
82 + if (typeof refined === 'string') {
83 + const href = refined;
84 + dispatcher.preinitScript(href);
85 + } else {
86 + const href = refined[0];
87 + const options = refined[1];
88 + dispatcher.preinitScript(href, options);
89 + }
90 return;
91 }
92 case 'M': {
58 - // $FlowFixMe[prop-missing] options are not refined to their types by code
59 - // $FlowFixMe[incompatible-call] options are not refined to their types by code
60 - dispatcher.preinitModule(href, options);
93 + const refined = refineModel(code, model);
94 + if (typeof refined === 'string') {
95 + const href = refined;
96 + dispatcher.preinitModuleScript(href);
97 + } else {
98 + const href = refined[0];
99 + const options = refined[1];
100 + dispatcher.preinitModuleScript(href, options);
101 + }
102 return;
103 }
104 }
105 }
106 }
107 +
108 +// Flow is having troulbe refining the HintModels so we help it a bit.
109 +// This should be compiled out in the production build.
110 +function refineModel<T>(code: T, model: HintModel<any>): HintModel<T> {
111 + return model;
112 +}
packages/react-dom/src/__tests__/ReactDOMFloat-test.js
-2
@@ -5341,8 +5341,6 @@ body {
5341 ]);
5342 expect(getMeaningfulChildren(document.body)).toEqual(
5343 <div id="container">
5344 - <script type="module" src="1" async="" />
5345 - <script type="module" src="2" async="" />
5344 <div>hello</div>
5345 </div>,
5346 );
packages/react-dom/src/__tests__/react-dom-server-rendering-stub-test.js
+1 -1
@@ -49,7 +49,7 @@ describe('react-dom-server-rendering-stub', () => {
49 }
50 const html = ReactDOMFizzServer.renderToString(<App />);
51 expect(html).toEqual(
52 - '<link rel="stylesheet" href="bar" data-precedence="default"/><script src="barmodule" type="module" async=""></script><link rel="preload" as="style" href="foo"/><link rel="modulepreload" href="foomodule"/><div>foo</div>',
52 + '<link rel="stylesheet" href="bar" data-precedence="default"/><script src="barmodule" type="module" async=""></script><link rel="preload" href="foo" as="style"/><link rel="modulepreload" href="foomodule"/><div>foo</div>',
53 );
54 });
55
packages/react-dom/src/shared/ReactDOMFloat.js
+284 -27
@@ -7,6 +7,7 @@
7 * @flow
8 */
9 import type {
10 + CrossOriginEnum,
11 PreconnectOptions,
12 PreloadOptions,
13 PreloadModuleOptions,
@@ -18,37 +19,64 @@ import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
19 const Dispatcher = ReactDOMSharedInternals.Dispatcher;
20
21 export function prefetchDNS(href: string) {
21 - let passedOptionArg: any;
22 if (__DEV__) {
23 - if (arguments[1] !== undefined) {
24 - passedOptionArg = arguments[1];
25 - }
26 - }
27 - const dispatcher = Dispatcher.current;
28 - if (dispatcher) {
29 - if (__DEV__) {
30 - if (passedOptionArg !== undefined) {
31 - // prefetchDNS will warn if you pass reserved options arg. We pass it along in Dev only to
32 - // elicit the warning. In prod we do not forward since it is not a part of the interface.
33 - // @TODO move all arg validation into this file. It needs to be universal anyway so may as well lock down the interace here and
34 - // let the rest of the codebase trust the types
35 - dispatcher.prefetchDNS(href, passedOptionArg);
23 + if (typeof href !== 'string' || !href) {
24 + console.error(
25 + 'ReactDOM.prefetchDNS(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.',
26 + getValueDescriptorExpectingObjectForWarning(href),
27 + );
28 + } else if (arguments.length > 1) {
29 + const options = arguments[1];
30 + if (
31 + typeof options === 'object' &&
32 + options.hasOwnProperty('crossOrigin')
33 + ) {
34 + console.error(
35 + '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`.',
36 + getValueDescriptorExpectingEnumForWarning(options),
37 + );
38 } else {
37 - dispatcher.prefetchDNS(href);
39 + console.error(
40 + '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`.',
41 + getValueDescriptorExpectingEnumForWarning(options),
42 + );
43 }
39 - } else {
40 - dispatcher.prefetchDNS(href);
44 }
45 }
46 + const dispatcher = Dispatcher.current;
47 + if (dispatcher && typeof href === 'string') {
48 + dispatcher.prefetchDNS(href);
49 + }
50 // We don't error because preconnect needs to be resilient to being called in a variety of scopes
51 // and the runtime may not be capable of responding. The function is optimistic and not critical
52 // so we favor silent bailout over warning or erroring.
53 }
54
55 export function preconnect(href: string, options?: ?PreconnectOptions) {
56 + if (__DEV__) {
57 + if (typeof href !== 'string' || !href) {
58 + console.error(
59 + 'ReactDOM.preconnect(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.',
60 + getValueDescriptorExpectingObjectForWarning(href),
61 + );
62 + } else if (options != null && typeof options !== 'object') {
63 + console.error(
64 + '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.',
65 + getValueDescriptorExpectingEnumForWarning(options),
66 + );
67 + } else if (options != null && typeof options.crossOrigin !== 'string') {
68 + console.error(
69 + '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.',
70 + getValueDescriptorExpectingObjectForWarning(options.crossOrigin),
71 + );
72 + }
73 + }
74 const dispatcher = Dispatcher.current;
50 - if (dispatcher) {
51 - dispatcher.preconnect(href, options);
75 + if (dispatcher && typeof href === 'string') {
76 + const crossOrigin = options
77 + ? getCrossOrigin('preconnect', options.crossOrigin)
78 + : null;
79 + dispatcher.preconnect(href, crossOrigin);
80 }
81 // We don't error because preconnect needs to be resilient to being called in a variety of scopes
82 // 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) {
84 }
85
86 export function preload(href: string, options: PreloadOptions) {
87 + if (__DEV__) {
88 + let encountered = '';
89 + if (typeof href !== 'string' || !href) {
90 + encountered += ` The \`href\` argument encountered was ${getValueDescriptorExpectingObjectForWarning(
91 + href,
92 + )}.`;
93 + }
94 + if (options == null || typeof options !== 'object') {
95 + encountered += ` The \`options\` argument encountered was ${getValueDescriptorExpectingObjectForWarning(
96 + options,
97 + )}.`;
98 + } else if (typeof options.as !== 'string' || !options.as) {
99 + encountered += ` The \`as\` option encountered was ${getValueDescriptorExpectingObjectForWarning(
100 + options.as,
101 + )}.`;
102 + }
103 + if (encountered) {
104 + console.error(
105 + 'ReactDOM.preload(): Expected two arguments, a non-empty `href` string and an `options` object with an `as` property valid for a `<link rel="preload" as="..." />` tag.%s',
106 + encountered,
107 + );
108 + }
109 + }
110 const dispatcher = Dispatcher.current;
60 - if (dispatcher) {
61 - dispatcher.preload(href, options);
111 + if (
112 + dispatcher &&
113 + typeof href === 'string' &&
114 + // We check existence because we cannot enforce this function is actually called with the stated type
115 + typeof options === 'object' &&
116 + options !== null &&
117 + typeof options.as === 'string'
118 + ) {
119 + const as = options.as;
120 + const crossOrigin = getCrossOrigin(as, options.crossOrigin);
121 + dispatcher.preload(href, as, {
122 + crossOrigin,
123 + integrity:
124 + typeof options.integrity === 'string' ? options.integrity : undefined,
125 + nonce: typeof options.nonce === 'string' ? options.nonce : undefined,
126 + type: typeof options.type === 'string' ? options.type : undefined,
127 + fetchPriority:
128 + typeof options.fetchPriority === 'string'
129 + ? options.fetchPriority
130 + : undefined,
131 + referrerPolicy:
132 + typeof options.referrerPolicy === 'string'
133 + ? options.referrerPolicy
134 + : undefined,
135 + imageSrcSet:
136 + typeof options.imageSrcSet === 'string'
137 + ? options.imageSrcSet
138 + : undefined,
139 + imageSizes:
140 + typeof options.imageSizes === 'string' ? options.imageSizes : undefined,
141 + });
142 }
143 // We don't error because preload needs to be resilient to being called in a variety of scopes
144 // 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) {
146 }
147
148 export function preloadModule(href: string, options?: ?PreloadModuleOptions) {
149 + if (__DEV__) {
150 + let encountered = '';
151 + if (typeof href !== 'string' || !href) {
152 + encountered += ` The \`href\` argument encountered was ${getValueDescriptorExpectingObjectForWarning(
153 + href,
154 + )}.`;
155 + }
156 + if (options !== undefined && typeof options !== 'object') {
157 + encountered += ` The \`options\` argument encountered was ${getValueDescriptorExpectingObjectForWarning(
158 + options,
159 + )}.`;
160 + } else if (options && 'as' in options && typeof options.as !== 'string') {
161 + encountered += ` The \`as\` option encountered was ${getValueDescriptorExpectingObjectForWarning(
162 + options.as,
163 + )}.`;
164 + }
165 + if (encountered) {
166 + console.error(
167 + 'ReactDOM.preloadModule(): Expected two arguments, a non-empty `href` string and, optionally, an `options` object with an `as` property valid for a `<link rel="modulepreload" as="..." />` tag.%s',
168 + encountered,
169 + );
170 + }
171 + }
172 const dispatcher = Dispatcher.current;
70 - if (dispatcher) {
71 - dispatcher.preloadModule(href, options);
173 + if (dispatcher && typeof href === 'string') {
174 + if (options) {
175 + const crossOrigin = getCrossOrigin(options.as, options.crossOrigin);
176 + dispatcher.preloadModule(href, {
177 + as:
178 + typeof options.as === 'string' && options.as !== 'script'
179 + ? options.as
180 + : undefined,
181 + crossOrigin,
182 + integrity:
183 + typeof options.integrity === 'string' ? options.integrity : undefined,
184 + });
185 + } else {
186 + dispatcher.preloadModule(href);
187 + }
188 }
189 // We don't error because preload needs to be resilient to being called in a variety of scopes
190 // 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) {
192 }
193
194 export function preinit(href: string, options: PreinitOptions) {
195 + if (__DEV__) {
196 + if (typeof href !== 'string' || !href) {
197 + console.error(
198 + 'ReactDOM.preinit(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.',
199 + getValueDescriptorExpectingObjectForWarning(href),
200 + );
201 + } else if (options == null || typeof options !== 'object') {
202 + console.error(
203 + '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.',
204 + getValueDescriptorExpectingEnumForWarning(options),
205 + );
206 + } else if (options.as !== 'style' && options.as !== 'script') {
207 + console.error(
208 + '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".',
209 + getValueDescriptorExpectingEnumForWarning(options.as),
210 + );
211 + }
212 + }
213 const dispatcher = Dispatcher.current;
80 - if (dispatcher) {
81 - dispatcher.preinit(href, options);
214 + if (
215 + dispatcher &&
216 + typeof href === 'string' &&
217 + options &&
218 + typeof options.as === 'string'
219 + ) {
220 + const as = options.as;
221 + const crossOrigin = getCrossOrigin(as, options.crossOrigin);
222 + const integrity =
223 + typeof options.integrity === 'string' ? options.integrity : undefined;
224 + const fetchPriority =
225 + typeof options.fetchPriority === 'string'
226 + ? options.fetchPriority
227 + : undefined;
228 + if (as === 'style') {
229 + dispatcher.preinitStyle(
230 + href,
231 + typeof options.precedence === 'string' ? options.precedence : undefined,
232 + {
233 + crossOrigin,
234 + integrity,
235 + fetchPriority,
236 + },
237 + );
238 + } else if (as === 'script') {
239 + dispatcher.preinitScript(href, {
240 + crossOrigin,
241 + integrity,
242 + fetchPriority,
243 + nonce: typeof options.nonce === 'string' ? options.nonce : undefined,
244 + });
245 + }
246 }
247 // We don't error because preinit needs to be resilient to being called in a variety of scopes
248 // 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) {
250 }
251
252 export function preinitModule(href: string, options?: ?PreinitModuleOptions) {
253 + if (__DEV__) {
254 + let encountered = '';
255 + if (typeof href !== 'string' || !href) {
256 + encountered += ` The \`href\` argument encountered was ${getValueDescriptorExpectingObjectForWarning(
257 + href,
258 + )}.`;
259 + }
260 + if (options !== undefined && typeof options !== 'object') {
261 + encountered += ` The \`options\` argument encountered was ${getValueDescriptorExpectingObjectForWarning(
262 + options,
263 + )}.`;
264 + } else if (options && 'as' in options && options.as !== 'script') {
265 + encountered += ` The \`as\` option encountered was ${getValueDescriptorExpectingEnumForWarning(
266 + options.as,
267 + )}.`;
268 + }
269 + if (encountered) {
270 + console.error(
271 + 'ReactDOM.preinitModule(): Expected up to two arguments, a non-empty `href` string and, optionally, an `options` object with a valid `as` property.%s',
272 + encountered,
273 + );
274 + } else {
275 + const as =
276 + options && typeof options.as === 'string' ? options.as : 'script';
277 + switch (as) {
278 + case 'script': {
279 + break;
280 + }
281 +
282 + // We have an invalid as type and need to warn
283 + default: {
284 + const typeOfAs = getValueDescriptorExpectingEnumForWarning(as);
285 + console.error(
286 + 'ReactDOM.preinitModule(): Currently the only supported "as" type for this function is "script"' +
287 + ' but received "%s" instead. This warning was generated for `href` "%s". In the future other' +
288 + ' module types will be supported, aligning with the import-attributes proposal. Learn more here:' +
289 + ' (https://github.com/tc39/proposal-import-attributes)',
290 + typeOfAs,
291 + href,
292 + );
293 + }
294 + }
295 + }
296 + }
297 const dispatcher = Dispatcher.current;
90 - if (dispatcher) {
91 - dispatcher.preinitModule(href, options);
298 + if (dispatcher && typeof href === 'string') {
299 + if (
300 + options == null ||
301 + (typeof options === 'object' &&
302 + (options.as == null || options.as === 'script'))
303 + ) {
304 + const crossOrigin = options
305 + ? getCrossOrigin(undefined, options.crossOrigin)
306 + : undefined;
307 + dispatcher.preinitModuleScript(href, {
308 + crossOrigin,
309 + integrity:
310 + options && typeof options.integrity === 'string'
311 + ? options.integrity
312 + : undefined,
313 + });
314 + }
315 }
316 // We don't error because preinit needs to be resilient to being called in a variety of scopes
317 // and the runtime may not be capable of responding. The function is optimistic and not critical
318 // so we favor silent bailout over warning or erroring.
319 }
320 +
321 +function getCrossOrigin(as: ?string, crossOrigin: ?string): ?CrossOriginEnum {
322 + return as === 'font'
323 + ? ''
324 + : typeof crossOrigin === 'string'
325 + ? crossOrigin === 'use-credentials'
326 + ? 'use-credentials'
327 + : ''
328 + : undefined;
329 +}
330 +
331 +function getValueDescriptorExpectingObjectForWarning(thing: any): string {
332 + return thing === null
333 + ? '`null`'
334 + : thing === undefined
335 + ? '`undefined`'
336 + : thing === ''
337 + ? 'an empty string'
338 + : `something with type "${typeof thing}"`;
339 +}
340 +
341 +function getValueDescriptorExpectingEnumForWarning(thing: any): string {
342 + return thing === null
343 + ? '`null`'
344 + : thing === undefined
345 + ? '`undefined`'
346 + : thing === ''
347 + ? 'an empty string'
348 + : typeof thing === 'string'
349 + ? JSON.stringify(thing)
350 + : typeof thing === 'number'
351 + ? '`' + thing + '`'
352 + : `something with type "${typeof thing}"`;
353 +}
packages/react-dom/src/shared/ReactDOMTypes.js
+48 -6
@@ -39,13 +39,55 @@ export type PreinitModuleOptions = {
39 integrity?: string,
40 };
41
42 +export type CrossOriginEnum = '' | 'use-credentials';
43 +export type FetchPriorityEnum = 'high' | 'low' | 'auto';
44 +
45 +export type PreloadImplOptions = {
46 + crossOrigin?: ?CrossOriginEnum,
47 + integrity?: ?string,
48 + nonce?: ?string,
49 + type?: ?string,
50 + fetchPriority?: ?FetchPriorityEnum,
51 + referrerPolicy?: ?string,
52 + imageSrcSet?: ?string,
53 + imageSizes?: ?string,
54 +};
55 +export type PreloadModuleImplOptions = {
56 + as?: ?string,
57 + crossOrigin?: ?CrossOriginEnum,
58 + integrity?: ?string,
59 +};
60 +export type PreinitStyleOptions = {
61 + crossOrigin?: ?string,
62 + integrity?: ?string,
63 + fetchPriority?: ?FetchPriorityEnum,
64 +};
65 +export type PreinitScriptOptions = {
66 + crossOrigin?: ?string,
67 + integrity?: ?string,
68 + fetchPriority?: ?FetchPriorityEnum,
69 + nonce?: ?string,
70 +};
71 +export type PreinitModuleScriptOptions = {
72 + crossOrigin?: ?CrossOriginEnum,
73 + integrity?: string,
74 +};
75 +
76 export type HostDispatcher = {
43 - prefetchDNS: (href: string, options?: ?PrefetchDNSOptions) => void,
44 - preconnect: (href: string, options?: ?PreconnectOptions) => void,
45 - preload: (href: string, options: PreloadOptions) => void,
46 - preloadModule: (href: string, options?: ?PreloadModuleOptions) => void,
47 - preinit: (href: string, options: PreinitOptions) => void,
48 - preinitModule: (href: string, options?: ?PreinitModuleOptions) => void,
77 + prefetchDNS: (href: string) => void,
78 + preconnect: (href: string, crossOrigin?: ?CrossOriginEnum) => void,
79 + preload: (href: string, as: string, options?: ?PreloadImplOptions) => void,
80 + preloadModule: (href: string, options?: ?PreloadModuleImplOptions) => void,
81 + preinitStyle: (
82 + href: string,
83 + precedence: ?string,
84 + options?: ?PreinitStyleOptions,
85 + ) => void,
86 + preinitScript: (src: string, options?: PreinitScriptOptions) => void,
87 + preinitModuleScript: (
88 + src: string,
89 + options?: ?PreinitModuleScriptOptions,
90 + ) => void,
91 };
92
93 export type ImportMap = {
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
+12 -32
@@ -1270,29 +1270,19 @@ describe('ReactFlightDOM', () => {
1270 <link rel="preconnect" href="c2 before" crossorigin="" />
1271 <link rel="preload" as="style" href="l before" />
1272 <link rel="modulepreload" href="lm before" />
1273 - <link rel="modulepreload" href="lm2 before" crossorigin="anonymous" />
1273 + <link rel="modulepreload" href="lm2 before" crossorigin="" />
1274 <script async="" src="i before" />
1275 <script type="module" async="" src="m before" />
1276 - <script
1277 - type="module"
1278 - async=""
1279 - src="m2 before"
1280 - crossorigin="anonymous"
1281 - />
1276 + <script type="module" async="" src="m2 before" crossorigin="" />
1277 <link rel="dns-prefetch" href="d after" />
1278 <link rel="preconnect" href="c after" />
1279 <link rel="preconnect" href="c2 after" crossorigin="" />
1280 <link rel="preload" as="style" href="l after" />
1281 <link rel="modulepreload" href="lm after" />
1287 - <link rel="modulepreload" href="lm2 after" crossorigin="anonymous" />
1282 + <link rel="modulepreload" href="lm2 after" crossorigin="" />
1283 <script async="" src="i after" />
1284 <script type="module" async="" src="m after" />
1290 - <script
1291 - type="module"
1292 - async=""
1293 - src="m2 after"
1294 - crossorigin="anonymous"
1295 - />
1285 + <script type="module" async="" src="m2 after" crossorigin="" />
1286 </head>
1287 <body />
1288 </html>,
@@ -1390,26 +1380,16 @@ describe('ReactFlightDOM', () => {
1380 <link rel="preconnect" href="c2 after" crossorigin="" />
1381 <script async="" src="i before" />
1382 <script type="module" async="" src="m before" />
1393 - <script
1394 - type="module"
1395 - async=""
1396 - src="m2 before"
1397 - crossorigin="anonymous"
1398 - />
1383 + <script type="module" async="" src="m2 before" crossorigin="" />
1384 <script async="" src="i after" />
1385 <script type="module" async="" src="m after" />
1401 - <script
1402 - type="module"
1403 - async=""
1404 - src="m2 after"
1405 - crossorigin="anonymous"
1406 - />
1386 + <script type="module" async="" src="m2 after" crossorigin="" />
1387 <link rel="preload" as="style" href="l before" />
1388 <link rel="modulepreload" href="lm before" />
1409 - <link rel="modulepreload" href="lm2 before" crossorigin="anonymous" />
1389 + <link rel="modulepreload" href="lm2 before" crossorigin="" />
1390 <link rel="preload" as="style" href="l after" />
1391 <link rel="modulepreload" href="lm after" />
1412 - <link rel="modulepreload" href="lm2 after" crossorigin="anonymous" />
1392 + <link rel="modulepreload" href="lm2 after" crossorigin="" />
1393 </head>
1394 <body>
1395 <p>hello world</p>
@@ -1509,12 +1489,12 @@ describe('ReactFlightDOM', () => {
1489 ]);
1490
1491 expect(content1).toEqual(
1512 - '<!DOCTYPE html><html><head><link rel="preload" as="style" href="before1"/>' +
1513 - '<link rel="preload" as="style" href="after1"/></head><body><p>hello world</p></body></html>',
1492 + '<!DOCTYPE html><html><head><link rel="preload" href="before1" as="style"/>' +
1493 + '<link rel="preload" href="after1" as="style"/></head><body><p>hello world</p></body></html>',
1494 );
1495 expect(content2).toEqual(
1516 - '<!DOCTYPE html><html><head><link rel="preload" as="style" href="before2"/>' +
1517 - '<link rel="preload" as="style" href="after2"/></head><body><p>hello world</p></body></html>',
1496 + '<!DOCTYPE html><html><head><link rel="preload" href="before2" as="style"/>' +
1497 + '<link rel="preload" href="after2" as="style"/></head><body><p>hello world</p></body></html>',
1498 );
1499 });
1500
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js
+1 -1
@@ -1107,7 +1107,7 @@ describe('ReactFlightDOMBrowser', () => {
1107 root.render(<App />);
1108 });
1109 expect(document.head.innerHTML).toBe(
1110 - '<link rel="preload" as="style" href="before">',
1110 + '<link rel="preload" href="before" as="style">',
1111 );
1112 expect(container.innerHTML).toBe('<p>hello world</p>');
1113 });
packages/react-server/src/ReactFlightServer.js
+9 -4
@@ -37,6 +37,7 @@ import type {
37 ServerReference,
38 ServerReferenceId,
39 Hints,
40 + HintCode,
41 HintModel,
42 } from './ReactFlightServerConfig';
43 import type {ContextSnapshot} from './ReactFlightNewContext';
@@ -370,10 +371,10 @@ function serializeThenable(request: Request, thenable: Thenable<any>): number {
371 return newTask.id;
372 }
373
373 -export function emitHint(
374 +export function emitHint<Code: HintCode>(
375 request: Request,
375 - code: string,
376 - model: HintModel,
376 + code: Code,
377 + model: HintModel<Code>,
378 ): void {
379 emitHintChunk(request, code, model);
380 enqueueFlush(request);
@@ -1272,7 +1273,11 @@ function emitImportChunk(
1273 request.completedImportChunks.push(processedChunk);
1274 }
1275
1275 -function emitHintChunk(request: Request, code: string, model: HintModel): void {
1276 +function emitHintChunk<Code: HintCode>(
1277 + request: Request,
1278 + code: Code,
1279 + model: HintModel<Code>,
1280 +): void {
1281 const json: string = stringify(model);
1282 const id = request.nextChunkId++;
1283 const row = serializeRowHeader('H' + code, id) + json + '\n';
packages/react-server/src/forks/ReactFlightServerConfig.custom.js
+3 -1
@@ -12,7 +12,9 @@ import type {Request} from 'react-server/src/ReactFlightServer';
12 export * from '../ReactFlightServerConfigBundlerCustom';
13
14 export type Hints = any;
15 -export type HintModel = any;
15 +export type HintCode = any;
16 +// eslint-disable-next-line no-unused-vars
17 +export type HintModel<T: any> = any;
18
19 export const isPrimaryRenderer = false;
20