@samitouri / QOS-React-2 / commits / e505316920

[Float][Flight][Fizz][Fiber] Implement `preloadModule` and `preinitModule` (#27220)

Stacked on #27224 ### Implements `ReactDOM.preloadModule()` `preloadModule` is a function to preload modules of various types. Similar to `preload` this is useful when you expect to use a Resource soon but can't render that resource directly. At the moment the only sensible module to preload is script modules along with some other `as` variants such as `as="serviceworker"`. In the future when there is some notion of how to preload style module script or json module scripts this API will be extended to support those as well. ##### Arguments 1. `href: string` -> the href or src value you want to preload. 2. `options?: {...}` -> 2.1. `options.as?: string` -> the `as` property the modulepreload link should render with. If not provided it will be omitted which will cause the modulepreload to be treated like a script module 2.2. `options.crossOrigin?: string` -> modules always load with CORS but you can provide `use-credentials` if you want to change the default behavior 2.3. `options.integrity?: string` -> an integrity hash for subresource integrity APIs ##### Rendering each preloaded module will emit a `<link rel="modulepreload" href="..." />` if `as` is specified and is something other than `"script"` the as attribute will also be included if crossOrigin or integrity as specified their attributes will also be included During SSR these script tags will be emitted before content. If we have not yet flushed the document head they will be emitted there after things that block paint such as font preloads, img preloads, and stylesheets. On the client these link tags will be appended to the document.head. ### Implements `ReactDOM.preinitModule()` `preinitModule` is a function to loading module scripts before they are required. It has the same use cases as `preinit`. During SSR you would use this to tell the browsers to start fetching code that will be used without having to wait for bootstrapping to initiate module fetches. ON the client you would use this to start fetching a module script early for an anticipated navigation or other event that is likely to depend on this module script. the `as` property for Float methods drew inspiration from the `as` attribute of the `<link rel="preload" ... >` tag but it is used as a sort of tag for the kind of thing being targetted by Float methods. For `preinitModule` we currently only support `as: "script"` and this is also the assumed default type so you current never need to specify this `as` value. In the future `preinitModule` will support additional module script types such as `style` or `json`. The support of these types will correspond to [Module Import Attributes](https://github.com/tc39/proposal-import-attributes). ##### Arguments 1. `href: string` -> the href or src value you want to preinitialize 2. `options?: {...}` -> 2.1 `options.as?: string` -> only supports `script` and this is the default behavior. Until we support import attributes such as `json` and `style` there will not be much reason to provide an `as` option. 2.2. `options.crossOrigin?: string`: modules always load with CORS but you can provide `use-credentials` if you want to change the default behavior 2.3 `options.integrity?: string` -> an integrity hash for subresource integrity APIs ##### Rendering each preinitialized `script` module will emit a `<script type="module" async="" src"...">` During SSR these will appear behind display blocking resources such as font preloads, img preloads, and stylesheets. In the browser these will be appende to the head. Note that for other `as` types the rendered output will be slightly different. `<script type="module">import "..." with {type: "json" }</script>`. Since this tag is an inline script variants of React that do not use inline scripts will simply omit these preinitialization tags from the SSR output. This is not implemented in this PR but will appear in a future update.

Josh Story committed Aug 17, 2023 at 10:30 UTC e50531692010bbda2a4627b07c7c810c3770a52a
15 files changed +943 -18
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+221 -3
@@ -23,7 +23,9 @@ import type {
23 PrefetchDNSOptions,
24 PreconnectOptions,
25 PreloadOptions,
26 + PreloadModuleOptions,
27 PreinitOptions,
28 + PreinitModuleOptions,
29 } from 'react-dom/src/shared/ReactDOMTypes';
30
31 import {NotPending} from 'react-dom-bindings/src/shared/ReactDOMFormActions';
@@ -2018,6 +2020,11 @@ type PreloadProps = {
2020 href: ?string,
2021 [string]: mixed,
2022 };
2023 +type PreloadModuleProps = {
2024 + rel: 'modulepreload',
2025 + href: string,
2026 + [string]: mixed,
2027 +};
2028
2029 export type RootResources = {
2030 hoistableStyles: Map<string, StyleResource>,
@@ -2029,7 +2036,8 @@ export function prepareToCommitHoistables() {
2036 }
2037
2038 // global collections of Resources
2032 -const preloadPropsMap: Map<string, PreloadProps> = new Map();
2039 +const preloadPropsMap: Map<string, PreloadProps | PreloadModuleProps> =
2040 + new Map();
2041 const preconnectsSet: Set<string> = new Set();
2042
2043 export type HoistableRoot = Document | ShadowRoot;
@@ -2060,7 +2068,9 @@ export const ReactDOMClientDispatcher: HostDispatcher = {
2068 prefetchDNS,
2069 preconnect,
2070 preload,
2071 + preloadModule,
2072 preinit,
2073 + preinitModule,
2074 };
2075
2076 // We expect this to get inlined. It is a function mostly to communicate the special nature of
@@ -2264,6 +2274,86 @@ function preload(href: string, options: PreloadOptions) {
2274 }
2275 }
2276
2277 +function preloadModule(href: string, options?: ?PreloadModuleOptions) {
2278 + if (!enableFloat) {
2279 + return;
2280 + }
2281 + if (__DEV__) {
2282 + let encountered = '';
2283 + if (typeof href !== 'string' || !href) {
2284 + encountered += ` The \`href\` argument encountered was ${getValueDescriptorExpectingObjectForWarning(
2285 + href,
2286 + )}.`;
2287 + }
2288 + if (options !== undefined && typeof options !== 'object') {
2289 + encountered += ` The \`options\` argument encountered was ${getValueDescriptorExpectingObjectForWarning(
2290 + options,
2291 + )}.`;
2292 + } else if (options && 'as' in options && typeof options.as !== 'string') {
2293 + encountered += ` The \`as\` option encountered was ${getValueDescriptorExpectingObjectForWarning(
2294 + options.as,
2295 + )}.`;
2296 + }
2297 + if (encountered) {
2298 + console.error(
2299 + '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',
2300 + encountered,
2301 + );
2302 + }
2303 + }
2304 + const ownerDocument = getDocumentForImperativeFloatMethods();
2305 + if (typeof href === 'string' && href) {
2306 + const as =
2307 + options && typeof options.as === 'string' ? options.as : 'script';
2308 + const preloadSelector = `link[rel="modulepreload"][as="${escapeSelectorAttributeValueInsideDoubleQuotes(
2309 + as,
2310 + )}"][href="${escapeSelectorAttributeValueInsideDoubleQuotes(href)}"]`;
2311 + // Some preloads are keyed under their selector. This happens when the preload is for
2312 + // an arbitrary type. Other preloads are keyed under the resource key they represent a preload for.
2313 + // Here we figure out which key to use to determine if we have a preload already.
2314 + let key = preloadSelector;
2315 + switch (as) {
2316 + case 'audioworklet':
2317 + case 'paintworklet':
2318 + case 'serviceworker':
2319 + case 'sharedworker':
2320 + case 'worker':
2321 + case 'script': {
2322 + key = getScriptKey(href);
2323 + break;
2324 + }
2325 + }
2326 +
2327 + if (!preloadPropsMap.has(key)) {
2328 + const preloadProps = preloadModulePropsFromPreloadModuleOptions(
2329 + href,
2330 + as,
2331 + options,
2332 + );
2333 + preloadPropsMap.set(key, preloadProps);
2334 +
2335 + if (null === ownerDocument.querySelector(preloadSelector)) {
2336 + switch (as) {
2337 + case 'audioworklet':
2338 + case 'paintworklet':
2339 + case 'serviceworker':
2340 + case 'sharedworker':
2341 + case 'worker':
2342 + case 'script': {
2343 + if (ownerDocument.querySelector(getScriptSelectorFromKey(key))) {
2344 + return;
2345 + }
2346 + }
2347 + }
2348 + const instance = ownerDocument.createElement('link');
2349 + setInitialProperties(instance, 'link', preloadProps);
2350 + markNodeAsHoistable(instance);
2351 + (ownerDocument.head: any).appendChild(instance);
2352 + }
2353 + }
2354 + }
2355 +}
2356 +
2357 function preloadPropsFromPreloadOptions(
2358 href: string,
2359 as: string,
@@ -2288,6 +2378,20 @@ function preloadPropsFromPreloadOptions(
2378 };
2379 }
2380
2381 +function preloadModulePropsFromPreloadModuleOptions(
2382 + href: string,
2383 + as: string,
2384 + options: ?PreloadModuleOptions,
2385 +): PreloadModuleProps {
2386 + return {
2387 + rel: 'modulepreload',
2388 + as: as !== 'script' ? as : undefined,
2389 + href,
2390 + crossOrigin: options ? options.crossOrigin : undefined,
2391 + integrity: options ? options.integrity : undefined,
2392 + };
2393 +}
2394 +
2395 function preinit(href: string, options: PreinitOptions) {
2396 if (!enableFloat) {
2397 return;
@@ -2417,6 +2521,107 @@ function preinit(href: string, options: PreinitOptions) {
2521 }
2522 }
2523
2524 +function preinitModule(href: string, options?: ?PreinitModuleOptions) {
2525 + if (!enableFloat) {
2526 + return;
2527 + }
2528 + if (__DEV__) {
2529 + let encountered = '';
2530 + if (typeof href !== 'string' || !href) {
2531 + encountered += ` The \`href\` argument encountered was ${getValueDescriptorExpectingObjectForWarning(
2532 + href,
2533 + )}.`;
2534 + }
2535 + if (options !== undefined && typeof options !== 'object') {
2536 + encountered += ` The \`options\` argument encountered was ${getValueDescriptorExpectingObjectForWarning(
2537 + options,
2538 + )}.`;
2539 + } else if (options && 'as' in options && options.as !== 'script') {
2540 + encountered += ` The \`as\` option encountered was ${getValueDescriptorExpectingEnumForWarning(
2541 + options.as,
2542 + )}.`;
2543 + }
2544 + if (encountered) {
2545 + console.error(
2546 + 'ReactDOM.preinitModule(): Expected up to two arguments, a non-empty `href` string and, optionally, an `options` object with a valid `as` property.%s',
2547 + encountered,
2548 + );
2549 + } else {
2550 + const as =
2551 + options && typeof options.as === 'string' ? options.as : 'script';
2552 + switch (as) {
2553 + case 'script': {
2554 + break;
2555 + }
2556 +
2557 + // We have an invalid as type and need to warn
2558 + default: {
2559 + const typeOfAs = getValueDescriptorExpectingEnumForWarning(as);
2560 + console.error(
2561 + 'ReactDOM.preinitModule(): Currently the only supported "as" type for this function is "script"' +
2562 + ' but received "%s" instead. This warning was generated for `href` "%s". In the future other' +
2563 + ' module types will be supported, aligning with the import-attributes proposal. Learn more here:' +
2564 + ' (https://github.com/tc39/proposal-import-attributes)',
2565 + typeOfAs,
2566 + href,
2567 + );
2568 + }
2569 + }
2570 + }
2571 + }
2572 + const ownerDocument = getDocumentForImperativeFloatMethods();
2573 +
2574 + if (typeof href === 'string' && href) {
2575 + const as =
2576 + options && typeof options.as === 'string' ? options.as : 'script';
2577 +
2578 + switch (as) {
2579 + case 'script': {
2580 + const src = href;
2581 + const scripts = getResourcesFromRoot(ownerDocument).hoistableScripts;
2582 +
2583 + const key = getScriptKey(src);
2584 +
2585 + // Check if this resource already exists
2586 + let resource = scripts.get(key);
2587 + if (resource) {
2588 + // We can early return. The resource exists and there is nothing
2589 + // more to do
2590 + return;
2591 + }
2592 +
2593 + // Attempt to hydrate instance from DOM
2594 + let instance: null | Instance = ownerDocument.querySelector(
2595 + getScriptSelectorFromKey(key),
2596 + );
2597 + if (!instance) {
2598 + // Construct a new instance and insert it
2599 + const scriptProps = modulePropsFromPreinitModuleOptions(src, options);
2600 + // Adopt certain preload props
2601 + const preloadProps = preloadPropsMap.get(key);
2602 + if (preloadProps) {
2603 + adoptPreloadPropsForScript(scriptProps, preloadProps);
2604 + }
2605 + instance = ownerDocument.createElement('script');
2606 + markNodeAsHoistable(instance);
2607 + setInitialProperties(instance, 'link', scriptProps);
2608 + (ownerDocument.head: any).appendChild(instance);
2609 + }
2610 +
2611 + // Construct a Resource and cache it
2612 + resource = {
2613 + type: 'script',
2614 + instance,
2615 + count: 1,
2616 + state: null,
2617 + };
2618 + scripts.set(key, resource);
2619 + return;
2620 + }
2621 + }
2622 + }
2623 +}
2624 +
2625 function stylesheetPropsFromPreinitOptions(
2626 href: string,
2627 precedence: string,
@@ -2446,6 +2651,19 @@ function scriptPropsFromPreinitOptions(
2651 };
2652 }
2653
2654 +function modulePropsFromPreinitModuleOptions(
2655 + src: string,
2656 + options: ?PreinitModuleOptions,
2657 +): ScriptProps {
2658 + return {
2659 + src,
2660 + async: true,
2661 + type: 'module',
2662 + crossOrigin: options ? options.crossOrigin : undefined,
2663 + integrity: options ? options.integrity : undefined,
2664 + };
2665 +}
2666 +
2667 type StyleTagQualifyingProps = {
2668 href: string,
2669 precedence: string,
@@ -2854,7 +3072,7 @@ function insertStylesheet(
3072
3073 function adoptPreloadPropsForStylesheet(
3074 stylesheetProps: StylesheetProps,
2857 - preloadProps: PreloadProps,
3075 + preloadProps: PreloadProps | PreloadModuleProps,
3076 ): void {
3077 if (stylesheetProps.crossOrigin == null)
3078 stylesheetProps.crossOrigin = preloadProps.crossOrigin;
@@ -2865,7 +3083,7 @@ function adoptPreloadPropsForStylesheet(
3083
3084 function adoptPreloadPropsForScript(
3085 scriptProps: ScriptProps,
2868 - preloadProps: PreloadProps,
3086 + preloadProps: PreloadProps | PreloadModuleProps,
3087 ): void {
3088 if (scriptProps.crossOrigin == null)
3089 scriptProps.crossOrigin = preloadProps.crossOrigin;
packages/react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js
+48
@@ -12,7 +12,9 @@ import type {
12 PrefetchDNSOptions,
13 PreconnectOptions,
14 PreloadOptions,
15 + PreloadModuleOptions,
16 PreinitOptions,
17 + PreinitModuleOptions,
18 } from 'react-dom/src/shared/ReactDOMTypes';
19
20 import {enableFloat} from 'shared/ReactFeatureFlags';
@@ -27,7 +29,9 @@ export const ReactDOMFlightServerDispatcher: HostDispatcher = {
29 prefetchDNS,
30 preconnect,
31 preload,
32 + preloadModule,
33 preinit,
34 + preinitModule,
35 };
36
37 function prefetchDNS(href: string, options?: ?PrefetchDNSOptions) {
@@ -99,6 +103,28 @@ function preload(href: string, options: PreloadOptions) {
103 }
104 }
105
106 +function preloadModule(href: string, options?: ?PreloadModuleOptions) {
107 + if (enableFloat) {
108 + if (typeof href === 'string') {
109 + const request = resolveRequest();
110 + if (request) {
111 + const hints = getHints(request);
112 + const key = 'm' + href;
113 + if (hints.has(key)) {
114 + // duplicate hint
115 + return;
116 + }
117 + hints.add(key);
118 + if (options) {
119 + emitHint(request, 'm', [href, options]);
120 + } else {
121 + emitHint(request, 'm', href);
122 + }
123 + }
124 + }
125 + }
126 +}
127 +
128 function preinit(href: string, options: PreinitOptions) {
129 if (enableFloat) {
130 if (typeof href === 'string') {
@@ -116,3 +142,25 @@ function preinit(href: string, options: PreinitOptions) {
142 }
143 }
144 }
145 +
146 +function preinitModule(href: string, options?: ?PreinitModuleOptions) {
147 + if (enableFloat) {
148 + if (typeof href === 'string') {
149 + const request = resolveRequest();
150 + if (request) {
151 + const hints = getHints(request);
152 + const key = 'M' + href;
153 + if (hints.has(key)) {
154 + // duplicate hint
155 + return;
156 + }
157 + hints.add(key);
158 + if (options) {
159 + emitHint(request, 'M', [href, options]);
160 + } else {
161 + emitHint(request, 'M', href);
162 + }
163 + }
164 + }
165 + }
166 +}
packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
+182
@@ -12,7 +12,9 @@ import type {
12 PrefetchDNSOptions,
13 PreconnectOptions,
14 PreloadOptions,
15 + PreloadModuleOptions,
16 PreinitOptions,
17 + PreinitModuleOptions,
18 } from 'react-dom/src/shared/ReactDOMTypes';
19
20 import {
@@ -90,7 +92,9 @@ const ReactDOMServerDispatcher = {
92 prefetchDNS,
93 preconnect,
94 preload,
95 + preloadModule,
96 preinit,
97 + preinitModule,
98 };
99
100 export function prepareHostDispatcher() {
@@ -4802,6 +4806,12 @@ type ScriptProps = {
4806 src: string,
4807 [string]: mixed,
4808 };
4809 +type ModuleProps = {
4810 + async: true,
4811 + src: string,
4812 + type: 'module',
4813 + [string]: mixed,
4814 +};
4815 type ScriptResource = TResource<'script', null>;
4816
4817 type Resource =
@@ -5075,6 +5085,63 @@ function preload(href: string, options: PreloadOptions) {
5085 }
5086 }
5087
5088 +function preloadModule(href: string, options?: ?PreloadModuleOptions): void {
5089 + if (!enableFloat) {
5090 + return;
5091 + }
5092 + const request = resolveRequest();
5093 + if (!request) {
5094 + // In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also
5095 + // possibly get them from the stack if we are not in an async context. Since we were not able to resolve
5096 + // the resources for this call in either case we opt to do nothing. We can consider making this a warning
5097 + // but there may be times where calling a function outside of render is intentional (i.e. to warm up data
5098 + // fetching) and we don't want to warn in those cases.
5099 + return;
5100 + }
5101 + const resources = getResources(request);
5102 + if (__DEV__) {
5103 + let encountered = '';
5104 + if (typeof href !== 'string' || !href) {
5105 + encountered += ` The \`href\` argument encountered was ${getValueDescriptorExpectingObjectForWarning(
5106 + href,
5107 + )}.`;
5108 + }
5109 + if (options !== undefined && typeof options !== 'object') {
5110 + encountered += ` The \`options\` argument encountered was ${getValueDescriptorExpectingObjectForWarning(
5111 + options,
5112 + )}.`;
5113 + } else if (options && 'as' in options && typeof options.as !== 'string') {
5114 + encountered += ` The \`as\` option encountered was ${getValueDescriptorExpectingObjectForWarning(
5115 + options.as,
5116 + )}.`;
5117 + }
5118 + if (encountered) {
5119 + console.error(
5120 + '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',
5121 + encountered,
5122 + );
5123 + }
5124 + }
5125 + if (typeof href === 'string' && href) {
5126 + const as =
5127 + options && typeof options.as === 'string' ? options.as : 'script';
5128 + const key = getResourceKey(as, href);
5129 + let resource = resources.preloadsMap.get(key);
5130 + if (!resource) {
5131 + resource = {
5132 + type: 'preload',
5133 + chunks: [],
5134 + state: NoState,
5135 + props: preloadModulePropsFromPreloadModuleOptions(href, as, options),
5136 + };
5137 + resources.preloadsMap.set(key, resource);
5138 + pushLinkImpl(resource.chunks, resource.props);
5139 + }
5140 + resources.bulkPreloads.add(resource);
5141 + flushResources(request);
5142 + }
5143 +}
5144 +
5145 function preinit(href: string, options: PreinitOptions): void {
5146 if (!enableFloat) {
5147 return;
@@ -5184,6 +5251,94 @@ function preinit(href: string, options: PreinitOptions): void {
5251 }
5252 }
5253
5254 +function preinitModule(href: string, options?: ?PreinitModuleOptions): void {
5255 + if (!enableFloat) {
5256 + return;
5257 + }
5258 + const request = resolveRequest();
5259 + if (!request) {
5260 + // In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also
5261 + // possibly get them from the stack if we are not in an async context. Since we were not able to resolve
5262 + // the resources for this call in either case we opt to do nothing. We can consider making this a warning
5263 + // but there may be times where calling a function outside of render is intentional (i.e. to warm up data
5264 + // fetching) and we don't want to warn in those cases.
5265 + return;
5266 + }
5267 + const resources = getResources(request);
5268 + if (__DEV__) {
5269 + let encountered = '';
5270 + if (typeof href !== 'string' || !href) {
5271 + encountered += ` The \`href\` argument encountered was ${getValueDescriptorExpectingObjectForWarning(
5272 + href,
5273 + )}.`;
5274 + }
5275 + if (options !== undefined && typeof options !== 'object') {
5276 + encountered += ` The \`options\` argument encountered was ${getValueDescriptorExpectingObjectForWarning(
5277 + options,
5278 + )}.`;
5279 + } else if (options && 'as' in options && options.as !== 'script') {
5280 + encountered += ` The \`as\` option encountered was ${getValueDescriptorExpectingEnumForWarning(
5281 + options.as,
5282 + )}.`;
5283 + }
5284 + if (encountered) {
5285 + console.error(
5286 + 'ReactDOM.preinitModule(): Expected up to two arguments, a non-empty `href` string and, optionally, an `options` object with a valid `as` property.%s',
5287 + encountered,
5288 + );
5289 + } else {
5290 + const as =
5291 + options && typeof options.as === 'string' ? options.as : 'script';
5292 + switch (as) {
5293 + case 'script': {
5294 + break;
5295 + }
5296 +
5297 + // We have an invalid as type and need to warn
5298 + default: {
5299 + const typeOfAs = getValueDescriptorExpectingEnumForWarning(as);
5300 + console.error(
5301 + 'ReactDOM.preinitModule(): Currently the only supported "as" type for this function is "script"' +
5302 + ' but received "%s" instead. This warning was generated for `href` "%s". In the future other' +
5303 + ' module types will be supported, aligning with the import-attributes proposal. Learn more here:' +
5304 + ' (https://github.com/tc39/proposal-import-attributes)',
5305 + typeOfAs,
5306 + href,
5307 + );
5308 + }
5309 + }
5310 + }
5311 + }
5312 + if (typeof href === 'string' && href) {
5313 + const as =
5314 + options && typeof options.as === 'string' ? options.as : 'script';
5315 + switch (as) {
5316 + case 'script': {
5317 + const src = href;
5318 + const key = getResourceKey(as, src);
5319 + let resource = resources.scriptsMap.get(key);
5320 + if (!resource) {
5321 + resource = {
5322 + type: 'script',
5323 + chunks: [],
5324 + state: NoState,
5325 + props: null,
5326 + };
5327 + resources.scriptsMap.set(key, resource);
5328 + const resourceProps = modulePropsFromPreinitModuleOptions(
5329 + src,
5330 + options,
5331 + );
5332 + resources.scripts.add(resource);
5333 + pushScriptImpl(resource.chunks, resourceProps);
5334 + flushResources(request);
5335 + }
5336 + return;
5337 + }
5338 + }
5339 + }
5340 +}
5341 +
5342 // This function is only safe to call at Request start time since it assumes
5343 // that each script has not already been preloaded. If we find a need to preload
5344 // scripts at any other point in time we will need to check whether the preload
@@ -5314,6 +5469,20 @@ function preloadPropsFromPreloadOptions(
5469 };
5470 }
5471
5472 +function preloadModulePropsFromPreloadModuleOptions(
5473 + href: string,
5474 + as: string,
5475 + options: ?PreloadModuleOptions,
5476 +): PreloadModuleProps {
5477 + return {
5478 + rel: 'modulepreload',
5479 + as: as !== 'script' ? as : undefined,
5480 + href,
5481 + crossOrigin: options ? options.crossOrigin : undefined,
5482 + integrity: options ? options.integrity : undefined,
5483 + };
5484 +}
5485 +
5486 function preloadAsStylePropsFromProps(href: string, props: any): PreloadProps {
5487 return {
5488 rel: 'preload',
@@ -5375,6 +5544,19 @@ function scriptPropsFromPreinitOptions(
5544 };
5545 }
5546
5547 +function modulePropsFromPreinitModuleOptions(
5548 + src: string,
5549 + options: ?PreinitModuleOptions,
5550 +): ModuleProps {
5551 + return {
5552 + src,
5553 + type: 'module',
5554 + async: true,
5555 + crossOrigin: options ? options.crossOrigin : undefined,
5556 + integrity: options ? options.integrity : undefined,
5557 + };
5558 +}
5559 +
5560 function adoptPreloadPropsForScriptProps(
5561 resourceProps: ScriptProps,
5562 preloadProps: PreloadProps,
packages/react-dom-bindings/src/server/ReactFlightServerConfigDOM.js
+10 -1
@@ -11,7 +11,9 @@ import type {
11 PrefetchDNSOptions,
12 PreconnectOptions,
13 PreloadOptions,
14 + PreloadModuleOptions,
15 PreinitOptions,
16 + PreinitModuleOptions,
17 } from 'react-dom/src/shared/ReactDOMTypes';
18
19 import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
@@ -31,7 +33,14 @@ export type HintModel =
33 | string
34 | [
35 string,
34 - PrefetchDNSOptions | PreconnectOptions | PreloadOptions | PreinitOptions,
36 + (
37 + | PrefetchDNSOptions
38 + | PreconnectOptions
39 + | PreloadOptions
40 + | PreloadModuleOptions
41 + | PreinitOptions
42 + | PreinitModuleOptions
43 + ),
44 ];
45
46 export type Hints = Set<string>;
packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js
+12
@@ -42,12 +42,24 @@ export function dispatchHint(code: string, model: HintModel): void {
42 dispatcher.preload(href, options);
43 return;
44 }
45 + 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);
49 + return;
50 + }
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);
55 return;
56 }
57 + 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);
61 + return;
62 + }
63 }
64 }
65 }
packages/react-dom/index.classic.fb.js
+2
@@ -35,7 +35,9 @@ export {
35 prefetchDNS,
36 preconnect,
37 preload,
38 + preloadModule,
39 preinit,
40 + preinitModule,
41 version,
42 } from './src/client/ReactDOM';
43
packages/react-dom/index.experimental.js
+2
@@ -24,6 +24,8 @@ export {
24 prefetchDNS,
25 preconnect,
26 preload,
27 + preloadModule,
28 preinit,
29 + preinitModule,
30 version,
31 } from './src/client/ReactDOM';
packages/react-dom/index.js
+2
@@ -27,6 +27,8 @@ export {
27 prefetchDNS,
28 preconnect,
29 preload,
30 + preloadModule,
31 preinit,
32 + preinitModule,
33 version,
34 } from './src/client/ReactDOM';
packages/react-dom/index.modern.fb.js
+2
@@ -20,6 +20,8 @@ export {
20 prefetchDNS,
21 preconnect,
22 preload,
23 + preloadModule,
24 preinit,
25 + preinitModule,
26 version,
27 } from './src/client/ReactDOM';
packages/react-dom/index.stable.js
+2
@@ -22,6 +22,8 @@ export {
22 prefetchDNS,
23 preconnect,
24 preload,
25 + preloadModule,
26 preinit,
27 + preinitModule,
28 version,
29 } from './src/client/ReactDOM';
packages/react-dom/src/__tests__/ReactDOMFloat-test.js
+263
@@ -4442,6 +4442,138 @@ body {
4442 });
4443 });
4444
4445 + describe('ReactDOM.preloadModule(href, options)', () => {
4446 + it('preloads scripts as modules', async () => {
4447 + function App({ssr}) {
4448 + const prefix = ssr ? 'ssr ' : 'browser ';
4449 + ReactDOM.preloadModule(prefix + 'plain');
4450 + ReactDOM.preloadModule(prefix + 'default', {as: 'script'});
4451 + ReactDOM.preloadModule(prefix + 'cors', {
4452 + crossOrigin: 'use-credentials',
4453 + });
4454 + ReactDOM.preloadModule(prefix + 'integrity', {integrity: 'some hash'});
4455 + ReactDOM.preloadModule(prefix + 'serviceworker', {as: 'serviceworker'});
4456 + return <div>hello</div>;
4457 + }
4458 + await act(() => {
4459 + renderToPipeableStream(<App ssr={true} />).pipe(writable);
4460 + });
4461 + expect(getMeaningfulChildren(document.body)).toEqual(
4462 + <div id="container">
4463 + <link rel="modulepreload" href="ssr plain" />
4464 + <link rel="modulepreload" href="ssr default" />
4465 + <link
4466 + rel="modulepreload"
4467 + href="ssr cors"
4468 + crossorigin="use-credentials"
4469 + />
4470 + <link
4471 + rel="modulepreload"
4472 + href="ssr integrity"
4473 + integrity="some hash"
4474 + />
4475 + <link
4476 + rel="modulepreload"
4477 + href="ssr serviceworker"
4478 + as="serviceworker"
4479 + />
4480 + <div>hello</div>
4481 + </div>,
4482 + );
4483 +
4484 + ReactDOMClient.hydrateRoot(container, <App />);
4485 + await waitForAll([]);
4486 + expect(getMeaningfulChildren(document)).toEqual(
4487 + <html>
4488 + <head>
4489 + <link rel="modulepreload" href="browser plain" />
4490 + <link rel="modulepreload" href="browser default" />
4491 + <link
4492 + rel="modulepreload"
4493 + href="browser cors"
4494 + crossorigin="use-credentials"
4495 + />
4496 + <link
4497 + rel="modulepreload"
4498 + href="browser integrity"
4499 + integrity="some hash"
4500 + />
4501 + <link
4502 + rel="modulepreload"
4503 + href="browser serviceworker"
4504 + as="serviceworker"
4505 + />
4506 + </head>
4507 + <body>
4508 + <div id="container">
4509 + <link rel="modulepreload" href="ssr plain" />
4510 + <link rel="modulepreload" href="ssr default" />
4511 + <link
4512 + rel="modulepreload"
4513 + href="ssr cors"
4514 + crossorigin="use-credentials"
4515 + />
4516 + <link
4517 + rel="modulepreload"
4518 + href="ssr integrity"
4519 + integrity="some hash"
4520 + />
4521 + <link
4522 + rel="modulepreload"
4523 + href="ssr serviceworker"
4524 + as="serviceworker"
4525 + />
4526 + <div>hello</div>
4527 + </div>
4528 + </body>
4529 + </html>,
4530 + );
4531 + });
4532 +
4533 + it('warns if you provide invalid arguments', async () => {
4534 + function App() {
4535 + ReactDOM.preloadModule();
4536 + ReactDOM.preloadModule(() => {});
4537 + ReactDOM.preloadModule('');
4538 + ReactDOM.preloadModule('1', true);
4539 + ReactDOM.preloadModule('2', {as: true});
4540 + return <div>hello</div>;
4541 + }
4542 + await expect(async () => {
4543 + await act(() => {
4544 + renderToPipeableStream(<App />).pipe(writable);
4545 + });
4546 + }).toErrorDev([
4547 + '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. The `href` argument encountered was `undefined`',
4548 + '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. The `href` argument encountered was something with type "function"',
4549 + '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. The `href` argument encountered was an empty string',
4550 + '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. The `options` argument encountered was something with type "boolean"',
4551 + '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. The `as` option encountered was something with type "boolean"',
4552 + ]);
4553 + expect(getMeaningfulChildren(document.body)).toEqual(
4554 + <div id="container">
4555 + <link rel="modulepreload" href="1" />
4556 + <link rel="modulepreload" href="2" />
4557 + <div>hello</div>
4558 + </div>,
4559 + );
4560 +
4561 + const root = ReactDOMClient.createRoot(
4562 + document.getElementById('container'),
4563 + );
4564 + root.render(<App />);
4565 + await expect(async () => {
4566 + await waitForAll([]);
4567 + }).toErrorDev([
4568 + '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. The `href` argument encountered was `undefined`',
4569 + '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. The `href` argument encountered was something with type "function"',
4570 + '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. The `href` argument encountered was an empty string',
4571 + '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. The `options` argument encountered was something with type "boolean"',
4572 + '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. The `as` option encountered was something with type "boolean"',
4573 + ]);
4574 + });
4575 + });
4576 +
4577 describe('ReactDOM.preinit(href, { as: ... })', () => {
4578 // @gate enableFloat
4579 it('creates a stylesheet resource when ReactDOM.preinit(..., {as: "style" }) is called', async () => {
@@ -4980,6 +5112,137 @@ body {
5112 });
5113 });
5114
5115 + describe('ReactDOM.preinitModule(href, options)', () => {
5116 + it('creates a script module resources', async () => {
5117 + function App({ssr}) {
5118 + const prefix = ssr ? 'ssr ' : 'browser ';
5119 + ReactDOM.preinitModule(prefix + 'plain');
5120 + ReactDOM.preinitModule(prefix + 'default', {as: 'script'});
5121 + ReactDOM.preinitModule(prefix + 'cors', {
5122 + crossOrigin: 'use-credentials',
5123 + });
5124 + ReactDOM.preinitModule(prefix + 'integrity', {integrity: 'some hash'});
5125 + ReactDOM.preinitModule(prefix + 'warning', {as: 'style'});
5126 + return <div>hello</div>;
5127 + }
5128 + await expect(async () => {
5129 + await act(() => {
5130 + renderToPipeableStream(<App ssr={true} />).pipe(writable);
5131 + });
5132 + }).toErrorDev([
5133 + 'ReactDOM.preinitModule(): Expected up to two arguments, a non-empty `href` string and, optionally, an `options` object with a valid `as` property. The `as` option encountered was "style"',
5134 + ]);
5135 + expect(getMeaningfulChildren(document.body)).toEqual(
5136 + <div id="container">
5137 + <script type="module" src="ssr plain" async="" />
5138 + <script type="module" src="ssr default" async="" />
5139 + <script
5140 + type="module"
5141 + src="ssr cors"
5142 + crossorigin="use-credentials"
5143 + async=""
5144 + />
5145 + <script
5146 + type="module"
5147 + src="ssr integrity"
5148 + integrity="some hash"
5149 + async=""
5150 + />
5151 + <div>hello</div>
5152 + </div>,
5153 + );
5154 +
5155 + ReactDOMClient.hydrateRoot(container, <App />);
5156 + await expect(async () => {
5157 + await waitForAll([]);
5158 + }).toErrorDev([
5159 + 'ReactDOM.preinitModule(): Expected up to two arguments, a non-empty `href` string and, optionally, an `options` object with a valid `as` property. The `as` option encountered was "style"',
5160 + ]);
5161 + expect(getMeaningfulChildren(document)).toEqual(
5162 + <html>
5163 + <head>
5164 + <script type="module" src="browser plain" async="" />
5165 + <script type="module" src="browser default" async="" />
5166 + <script
5167 + type="module"
5168 + src="browser cors"
5169 + crossorigin="use-credentials"
5170 + async=""
5171 + />
5172 + <script
5173 + type="module"
5174 + src="browser integrity"
5175 + integrity="some hash"
5176 + async=""
5177 + />
5178 + </head>
5179 + <body>
5180 + <div id="container">
5181 + <script type="module" src="ssr plain" async="" />
5182 + <script type="module" src="ssr default" async="" />
5183 + <script
5184 + type="module"
5185 + src="ssr cors"
5186 + crossorigin="use-credentials"
5187 + async=""
5188 + />
5189 + <script
5190 + type="module"
5191 + src="ssr integrity"
5192 + integrity="some hash"
5193 + async=""
5194 + />
5195 + <div>hello</div>
5196 + </div>
5197 + </body>
5198 + </html>,
5199 + );
5200 + });
5201 +
5202 + it('warns if you provide invalid arguments', async () => {
5203 + function App() {
5204 + ReactDOM.preinitModule();
5205 + ReactDOM.preinitModule(() => {});
5206 + ReactDOM.preinitModule('');
5207 + ReactDOM.preinitModule('1', true);
5208 + ReactDOM.preinitModule('2', {as: true});
5209 + return <div>hello</div>;
5210 + }
5211 + await expect(async () => {
5212 + await act(() => {
5213 + renderToPipeableStream(<App />).pipe(writable);
5214 + });
5215 + }).toErrorDev([
5216 + 'ReactDOM.preinitModule(): Expected up to two arguments, a non-empty `href` string and, optionally, an `options` object with a valid `as` property. The `href` argument encountered was `undefined`',
5217 + 'ReactDOM.preinitModule(): Expected up to two arguments, a non-empty `href` string and, optionally, an `options` object with a valid `as` property. The `href` argument encountered was something with type "function"',
5218 + 'ReactDOM.preinitModule(): Expected up to two arguments, a non-empty `href` string and, optionally, an `options` object with a valid `as` property. The `href` argument encountered was an empty string',
5219 + 'ReactDOM.preinitModule(): Expected up to two arguments, a non-empty `href` string and, optionally, an `options` object with a valid `as` property. The `options` argument encountered was something with type "boolean"',
5220 + 'ReactDOM.preinitModule(): Expected up to two arguments, a non-empty `href` string and, optionally, an `options` object with a valid `as` property. The `as` option encountered was something with type "boolean"',
5221 + ]);
5222 + expect(getMeaningfulChildren(document.body)).toEqual(
5223 + <div id="container">
5224 + <script type="module" src="1" async="" />
5225 + <script type="module" src="2" async="" />
5226 + <div>hello</div>
5227 + </div>,
5228 + );
5229 +
5230 + const root = ReactDOMClient.createRoot(
5231 + document.getElementById('container'),
5232 + );
5233 + root.render(<App />);
5234 + await expect(async () => {
5235 + await waitForAll([]);
5236 + }).toErrorDev([
5237 + 'ReactDOM.preinitModule(): Expected up to two arguments, a non-empty `href` string and, optionally, an `options` object with a valid `as` property. The `href` argument encountered was `undefined`',
5238 + 'ReactDOM.preinitModule(): Expected up to two arguments, a non-empty `href` string and, optionally, an `options` object with a valid `as` property. The `href` argument encountered was something with type "function"',
5239 + 'ReactDOM.preinitModule(): Expected up to two arguments, a non-empty `href` string and, optionally, an `options` object with a valid `as` property. The `href` argument encountered was an empty string',
5240 + 'ReactDOM.preinitModule(): Expected up to two arguments, a non-empty `href` string and, optionally, an `options` object with a valid `as` property. The `options` argument encountered was something with type "boolean"',
5241 + 'ReactDOM.preinitModule(): Expected up to two arguments, a non-empty `href` string and, optionally, an `options` object with a valid `as` property. The `as` option encountered was something with type "boolean"',
5242 + ]);
5243 + });
5244 + });
5245 +
5246 describe('Stylesheet Resources', () => {
5247 // @gate enableFloat
5248 it('treats link rel stylesheet elements as a stylesheet resource when it includes a precedence when server rendering', async () => {
packages/react-dom/src/client/ReactDOM.js
+2
@@ -59,7 +59,9 @@ export {
59 prefetchDNS,
60 preconnect,
61 preload,
62 + preloadModule,
63 preinit,
64 + preinitModule,
65 } from '../shared/ReactDOMFloat';
66 export {useFormStatus} from 'react-dom-bindings/src/shared/ReactDOMFormActions';
67
packages/react-dom/src/shared/ReactDOMFloat.js
+22
@@ -9,7 +9,9 @@
9 import type {
10 PreconnectOptions,
11 PreloadOptions,
12 + PreloadModuleOptions,
13 PreinitOptions,
14 + PreinitModuleOptions,
15 } from './ReactDOMTypes';
16
17 import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
@@ -63,6 +65,16 @@ export function preload(href: string, options: PreloadOptions) {
65 // so we favor silent bailout over warning or erroring.
66 }
67
68 +export function preloadModule(href: string, options?: ?PreloadModuleOptions) {
69 + const dispatcher = Dispatcher.current;
70 + if (dispatcher) {
71 + dispatcher.preloadModule(href, options);
72 + }
73 + // We don't error because preload needs to be resilient to being called in a variety of scopes
74 + // and the runtime may not be capable of responding. The function is optimistic and not critical
75 + // so we favor silent bailout over warning or erroring.
76 +}
77 +
78 export function preinit(href: string, options: PreinitOptions) {
79 const dispatcher = Dispatcher.current;
80 if (dispatcher) {
@@ -72,3 +84,13 @@ export function preinit(href: string, options: PreinitOptions) {
84 // and the runtime may not be capable of responding. The function is optimistic and not critical
85 // so we favor silent bailout over warning or erroring.
86 }
87 +
88 +export function preinitModule(href: string, options?: ?PreinitModuleOptions) {
89 + const dispatcher = Dispatcher.current;
90 + if (dispatcher) {
91 + dispatcher.preinitModule(href, options);
92 + }
93 + // We don't error because preinit needs to be resilient to being called in a variety of scopes
94 + // and the runtime may not be capable of responding. The function is optimistic and not critical
95 + // so we favor silent bailout over warning or erroring.
96 +}
packages/react-dom/src/shared/ReactDOMTypes.js
+13 -1
@@ -20,6 +20,11 @@ export type PreloadOptions = {
20 imageSizes?: string,
21 referrerPolicy?: string,
22 };
23 +export type PreloadModuleOptions = {
24 + as?: string,
25 + crossOrigin?: string,
26 + integrity?: string,
27 +};
28 export type PreinitOptions = {
29 as: string,
30 precedence?: string,
@@ -28,10 +33,17 @@ export type PreinitOptions = {
33 nonce?: string,
34 fetchPriority?: 'high' | 'low' | 'auto',
35 };
36 +export type PreinitModuleOptions = {
37 + as?: string,
38 + crossOrigin?: string,
39 + integrity?: string,
40 +};
41
42 export type HostDispatcher = {
43 prefetchDNS: (href: string, options?: ?PrefetchDNSOptions) => void,
34 - preconnect: (href: string, options: ?PreconnectOptions) => 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,
49 };
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
+160 -13
@@ -12,6 +12,7 @@
12 // Polyfills for test environment
13 global.ReadableStream =
14 require('web-streams-polyfill/ponyfill/es6').ReadableStream;
15 +global.TextEncoder = require('util').TextEncoder;
16 global.TextDecoder = require('util').TextDecoder;
17
18 // Don't wait before processing work on the server.
@@ -32,11 +33,14 @@ let ReactServerDOMClient;
33 let ReactDOMFizzServer;
34 let Suspense;
35 let ErrorBoundary;
36 +let JSDOM;
37
38 describe('ReactFlightDOM', () => {
39 beforeEach(() => {
40 jest.resetModules();
41
42 + JSDOM = require('jsdom').JSDOM;
43 +
44 // Simulate the condition resolution
45 jest.mock('react-server-dom-webpack/server', () =>
46 require('react-server-dom-webpack/server.node.unbundled'),
@@ -99,6 +103,51 @@ describe('ReactFlightDOM', () => {
103 throw theInfinitePromise;
104 }
105
106 + function getMeaningfulChildren(element) {
107 + const children = [];
108 + let node = element.firstChild;
109 + while (node) {
110 + if (node.nodeType === 1) {
111 + if (
112 + // some tags are ambiguous and might be hidden because they look like non-meaningful children
113 + // so we have a global override where if this data attribute is included we also include the node
114 + node.hasAttribute('data-meaningful') ||
115 + (node.tagName === 'SCRIPT' &&
116 + node.hasAttribute('src') &&
117 + node.hasAttribute('async')) ||
118 + (node.tagName !== 'SCRIPT' &&
119 + node.tagName !== 'TEMPLATE' &&
120 + node.tagName !== 'template' &&
121 + !node.hasAttribute('hidden') &&
122 + !node.hasAttribute('aria-hidden'))
123 + ) {
124 + const props = {};
125 + const attributes = node.attributes;
126 + for (let i = 0; i < attributes.length; i++) {
127 + if (
128 + attributes[i].name === 'id' &&
129 + attributes[i].value.includes(':')
130 + ) {
131 + // We assume this is a React added ID that's a non-visual implementation detail.
132 + continue;
133 + }
134 + props[attributes[i].name] = attributes[i].value;
135 + }
136 + props.children = getMeaningfulChildren(node);
137 + children.push(React.createElement(node.tagName.toLowerCase(), props));
138 + }
139 + } else if (node.nodeType === 3) {
140 + children.push(node.data);
141 + }
142 + node = node.nextSibling;
143 + }
144 + return children.length === 0
145 + ? undefined
146 + : children.length === 1
147 + ? children[0]
148 + : children;
149 + }
150 +
151 it('should resolve HTML using Node streams', async () => {
152 function Text({children}) {
153 return <span>{children}</span>;
@@ -1148,7 +1197,7 @@ describe('ReactFlightDOM', () => {
1197 expect(reportedErrors).toEqual([theError]);
1198 });
1199
1151 - it('should support ReactDOM.preload when rendering in Fiber', async () => {
1200 + it('should support float methods when rendering in Fiber', async () => {
1201 function Component() {
1202 return <p>hello world</p>;
1203 }
@@ -1156,9 +1205,25 @@ describe('ReactFlightDOM', () => {
1205 const ClientComponent = clientExports(Component);
1206
1207 async function ServerComponent() {
1159 - ReactDOM.preload('before', {as: 'style'});
1208 + ReactDOM.prefetchDNS('d before');
1209 + ReactDOM.preconnect('c before');
1210 + ReactDOM.preconnect('c2 before', {crossOrigin: 'anonymous'});
1211 + ReactDOM.preload('l before', {as: 'style'});
1212 + ReactDOM.preloadModule('lm before');
1213 + ReactDOM.preloadModule('lm2 before', {crossOrigin: 'anonymous'});
1214 + ReactDOM.preinit('i before', {as: 'script'});
1215 + ReactDOM.preinitModule('m before');
1216 + ReactDOM.preinitModule('m2 before', {crossOrigin: 'anonymous'});
1217 await 1;
1161 - ReactDOM.preload('after', {as: 'style'});
1218 + ReactDOM.prefetchDNS('d after');
1219 + ReactDOM.preconnect('c after');
1220 + ReactDOM.preconnect('c2 after', {crossOrigin: 'anonymous'});
1221 + ReactDOM.preload('l after', {as: 'style'});
1222 + ReactDOM.preloadModule('lm after');
1223 + ReactDOM.preloadModule('lm2 after', {crossOrigin: 'anonymous'});
1224 + ReactDOM.preinit('i after', {as: 'script'});
1225 + ReactDOM.preinitModule('m after');
1226 + ReactDOM.preinitModule('m2 after', {crossOrigin: 'anonymous'});
1227 return <ClientComponent />;
1228 }
1229
@@ -1196,14 +1261,46 @@ describe('ReactFlightDOM', () => {
1261 await act(() => {
1262 root.render(<App />);
1263 });
1199 - expect(document.head.innerHTML).toBe(
1200 - '<link rel="preload" as="style" href="before">' +
1201 - '<link rel="preload" as="style" href="after">',
1264 +
1265 + expect(getMeaningfulChildren(document)).toEqual(
1266 + <html>
1267 + <head>
1268 + <link rel="dns-prefetch" href="d before" />
1269 + <link rel="preconnect" href="c before" />
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" />
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 + />
1282 + <link rel="dns-prefetch" href="d after" />
1283 + <link rel="preconnect" href="c after" />
1284 + <link rel="preconnect" href="c2 after" crossorigin="" />
1285 + <link rel="preload" as="style" href="l after" />
1286 + <link rel="modulepreload" href="lm after" />
1287 + <link rel="modulepreload" href="lm2 after" crossorigin="anonymous" />
1288 + <script async="" src="i after" />
1289 + <script type="module" async="" src="m after" />
1290 + <script
1291 + type="module"
1292 + async=""
1293 + src="m2 after"
1294 + crossorigin="anonymous"
1295 + />
1296 + </head>
1297 + <body />
1298 + </html>,
1299 );
1203 - expect(container.innerHTML).toBe('<p>hello world</p>');
1300 + expect(getMeaningfulChildren(container)).toEqual(<p>hello world</p>);
1301 });
1302
1206 - it('should support ReactDOM.preload when rendering in Fizz', async () => {
1303 + it('should support float methods when rendering in Fizz', async () => {
1304 function Component() {
1305 return <p>hello world</p>;
1306 }
@@ -1211,9 +1308,25 @@ describe('ReactFlightDOM', () => {
1308 const ClientComponent = clientExports(Component);
1309
1310 async function ServerComponent() {
1214 - ReactDOM.preload('before', {as: 'style'});
1311 + ReactDOM.prefetchDNS('d before');
1312 + ReactDOM.preconnect('c before');
1313 + ReactDOM.preconnect('c2 before', {crossOrigin: 'anonymous'});
1314 + ReactDOM.preload('l before', {as: 'style'});
1315 + ReactDOM.preloadModule('lm before');
1316 + ReactDOM.preloadModule('lm2 before', {crossOrigin: 'anonymous'});
1317 + ReactDOM.preinit('i before', {as: 'script'});
1318 + ReactDOM.preinitModule('m before');
1319 + ReactDOM.preinitModule('m2 before', {crossOrigin: 'anonymous'});
1320 await 1;
1216 - ReactDOM.preload('after', {as: 'style'});
1321 + ReactDOM.prefetchDNS('d after');
1322 + ReactDOM.preconnect('c after');
1323 + ReactDOM.preconnect('c2 after', {crossOrigin: 'anonymous'});
1324 + ReactDOM.preload('l after', {as: 'style'});
1325 + ReactDOM.preloadModule('lm after');
1326 + ReactDOM.preloadModule('lm2 after', {crossOrigin: 'anonymous'});
1327 + ReactDOM.preinit('i after', {as: 'script'});
1328 + ReactDOM.preinitModule('m after');
1329 + ReactDOM.preinitModule('m2 after', {crossOrigin: 'anonymous'});
1330 return <ClientComponent />;
1331 }
1332
@@ -1265,9 +1378,43 @@ describe('ReactFlightDOM', () => {
1378 content += decoder.decode(value, {stream: true});
1379 }
1380
1268 - expect(content).toEqual(
1269 - '<!DOCTYPE html><html><head><link rel="preload" as="style" href="before"/>' +
1270 - '<link rel="preload" as="style" href="after"/></head><body><p>hello world</p></body></html>',
1381 + const doc = new JSDOM(content).window.document;
1382 + expect(getMeaningfulChildren(doc)).toEqual(
1383 + <html>
1384 + <head>
1385 + <link rel="dns-prefetch" href="d before" />
1386 + <link rel="preconnect" href="c before" />
1387 + <link rel="preconnect" href="c2 before" crossorigin="" />
1388 + <link rel="dns-prefetch" href="d after" />
1389 + <link rel="preconnect" href="c after" />
1390 + <link rel="preconnect" href="c2 after" crossorigin="" />
1391 + <script async="" src="i before" />
1392 + <script type="module" async="" src="m before" />
1393 + <script
1394 + type="module"
1395 + async=""
1396 + src="m2 before"
1397 + crossorigin="anonymous"
1398 + />
1399 + <script async="" src="i after" />
1400 + <script type="module" async="" src="m after" />
1401 + <script
1402 + type="module"
1403 + async=""
1404 + src="m2 after"
1405 + crossorigin="anonymous"
1406 + />
1407 + <link rel="preload" as="style" href="l before" />
1408 + <link rel="modulepreload" href="lm before" />
1409 + <link rel="modulepreload" href="lm2 before" crossorigin="anonymous" />
1410 + <link rel="preload" as="style" href="l after" />
1411 + <link rel="modulepreload" href="lm after" />
1412 + <link rel="modulepreload" href="lm2 after" crossorigin="anonymous" />
1413 + </head>
1414 + <body>
1415 + <p>hello world</p>
1416 + </body>
1417 + </html>,
1418 );
1419 });
1420