@samitouri / QOS-React-2 / commits / 73b6435ca4

[Float][Fiber] Implement waitForCommitToBeReady for stylesheet resources (#26450)

Before a commit is finished if any new stylesheet resources are going to mount and we are capable of delaying the commit we will do the following 1. Wait for all preloads for newly created stylesheet resources to load 2. Once all preloads are finished we insert the stylesheet instances for these resources and wait for them all to load 3. Once all stylesheets have loaded we complete the commit In this PR I also removed the synchronous loadingstate tracking in the fizz runtime. It was not necessary to support the implementation on not used by the fizz runtime itself. It makes the inline script slightly smaller In this PR I also integrated ReactDOMFloatClient with ReactDOMHostConfig. It leads to better code factoring, something I already did on the server a while back. To make the diff a little easier to follow i make these changes in a single commit so you can look at the change after that commit if helpful There is a 500ms timeout which will finish the commit even if all suspended host instances have not finished loading yet At the moment error and load events are treated the same and we're really tracking whether the host instance is finished attempting to load.

Josh Story committed Mar 24, 2023 at 19:17 UTC 73b6435ca4e0c3ae3aac8126509a82420a84f0d7
16 files changed +2354 -1423
packages/react-dom-bindings/src/client/ReactDOMComponentTree.js
+2 -1
@@ -7,7 +7,6 @@
7 * @flow
8 */
9
10 -import type {HoistableRoot, RootResources} from './ReactDOMFloatClient';
10 import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
11 import type {ReactScopeInstance} from 'shared/ReactTypes';
12 import type {
@@ -20,6 +19,8 @@ import type {
19 Instance,
20 SuspenseInstance,
21 Props,
22 + HoistableRoot,
23 + RootResources,
24 } from './ReactDOMHostConfig';
25
26 import {
packages/react-dom-bindings/src/client/ReactDOMFloatClient.js deleted
-1106
@@ -1,1106 +0,0 @@
1 -/**
2 - * Copyright (c) Meta Platforms, Inc. and affiliates.
3 - *
4 - * This source code is licensed under the MIT license found in the
5 - * LICENSE file in the root directory of this source tree.
6 - *
7 - * @flow
8 - */
9 -
10 -import type {Instance, Container} from './ReactDOMHostConfig';
11 -
12 -import {getCurrentRootHostContainer} from 'react-reconciler/src/ReactFiberHostContext';
13 -
14 -import hasOwnProperty from 'shared/hasOwnProperty';
15 -import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals.js';
16 -const {Dispatcher} = ReactDOMSharedInternals;
17 -import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
18 -
19 -import {DOCUMENT_NODE} from './HTMLNodeType';
20 -import {SVG_NAMESPACE} from './DOMNamespaces';
21 -import {
22 - validatePreloadArguments,
23 - validatePreinitArguments,
24 - getValueDescriptorExpectingObjectForWarning,
25 - getValueDescriptorExpectingEnumForWarning,
26 -} from '../shared/ReactDOMResourceValidation';
27 -
28 -import {setInitialProperties} from './ReactDOMComponent';
29 -import {
30 - precacheFiberNode,
31 - getResourcesFromRoot,
32 - isOwnedInstance,
33 - markNodeAsHoistable,
34 -} from './ReactDOMComponentTree';
35 -
36 -// The resource types we support. currently they match the form for the as argument.
37 -// In the future this may need to change, especially when modules / scripts are supported
38 -type ResourceType = 'style' | 'font' | 'script';
39 -
40 -type HoistableTagType = 'link' | 'meta' | 'title';
41 -type TResource<T: 'stylesheet' | 'style' | 'script' | 'void'> = {
42 - type: T,
43 - instance: null | Instance,
44 - count: number,
45 -};
46 -type StylesheetResource = TResource<'stylesheet'>;
47 -type StyleTagResource = TResource<'style'>;
48 -type StyleResource = StyleTagResource | StylesheetResource;
49 -type ScriptResource = TResource<'script'>;
50 -type VoidResource = TResource<'void'>;
51 -type Resource = StyleResource | ScriptResource | VoidResource;
52 -
53 -type StyleTagProps = {
54 - 'data-href': string,
55 - 'data-precedence': string,
56 - [string]: mixed,
57 -};
58 -type StylesheetProps = {
59 - rel: 'stylesheet',
60 - href: string,
61 - 'data-precedence': string,
62 - [string]: mixed,
63 -};
64 -
65 -type ScriptProps = {
66 - src: string,
67 - async: true,
68 - [string]: mixed,
69 -};
70 -
71 -type PreloadProps = {
72 - rel: 'preload',
73 - href: string,
74 - [string]: mixed,
75 -};
76 -
77 -export type RootResources = {
78 - hoistableStyles: Map<string, StyleResource>,
79 - hoistableScripts: Map<string, ScriptResource>,
80 -};
81 -
82 -// It is valid to preload even when we aren't actively rendering. For cases where Float functions are
83 -// called when there is no rendering we track the last used document. It is not safe to insert
84 -// arbitrary resources into the lastCurrentDocument b/c it may not actually be the document
85 -// that the resource is meant to apply too (for example stylesheets or scripts). This is only
86 -// appropriate for resources that don't really have a strict tie to the document itself for example
87 -// preloads
88 -let lastCurrentDocument: ?Document = null;
89 -
90 -let previousDispatcher = null;
91 -export function prepareToRenderResources(rootContainer: Container) {
92 - const rootNode = getHoistableRoot(rootContainer);
93 - lastCurrentDocument = getDocumentFromRoot(rootNode);
94 -
95 - previousDispatcher = Dispatcher.current;
96 - Dispatcher.current = ReactDOMClientDispatcher;
97 -}
98 -
99 -export function cleanupAfterRenderResources() {
100 - Dispatcher.current = previousDispatcher;
101 - previousDispatcher = null;
102 -}
103 -
104 -export function prepareToCommitHoistables() {
105 - tagCaches = null;
106 -}
107 -
108 -// We want this to be the default dispatcher on ReactDOMSharedInternals but we don't want to mutate
109 -// internals in Module scope. Instead we export it and Internals will import it. There is already a cycle
110 -// from Internals -> ReactDOM -> FloatClient -> Internals so this doesn't introduce a new one.
111 -export const ReactDOMClientDispatcher = {
112 - prefetchDNS,
113 - preconnect,
114 - preload,
115 - preinit,
116 -};
117 -
118 -export type HoistableRoot = Document | ShadowRoot;
119 -
120 -// global collections of Resources
121 -const preloadPropsMap: Map<string, PreloadProps> = new Map();
122 -const preconnectsSet: Set<string> = new Set();
123 -
124 -// getRootNode is missing from IE and old jsdom versions
125 -export function getHoistableRoot(container: Container): HoistableRoot {
126 - // $FlowFixMe[method-unbinding]
127 - return typeof container.getRootNode === 'function'
128 - ? /* $FlowFixMe[incompatible-return] Flow types this as returning a `Node`,
129 - * but it's either a `Document` or `ShadowRoot`. */
130 - container.getRootNode()
131 - : container.ownerDocument;
132 -}
133 -
134 -function getCurrentResourceRoot(): null | HoistableRoot {
135 - const currentContainer = getCurrentRootHostContainer();
136 - return currentContainer ? getHoistableRoot(currentContainer) : null;
137 -}
138 -
139 -// Preloads are somewhat special. Even if we don't have the Document
140 -// used by the root that is rendering a component trying to insert a preload
141 -// we can still seed the file cache by doing the preload on any document we have
142 -// access to. We prefer the currentDocument if it exists, we also prefer the
143 -// lastCurrentDocument if that exists. As a fallback we will use the window.document
144 -// if available.
145 -function getDocumentForPreloads(): ?Document {
146 - const root = getCurrentResourceRoot();
147 - if (root) {
148 - return root.ownerDocument || root;
149 - } else {
150 - try {
151 - return lastCurrentDocument || window.document;
152 - } catch (error) {
153 - return null;
154 - }
155 - }
156 -}
157 -
158 -function getDocumentFromRoot(root: HoistableRoot): Document {
159 - return root.ownerDocument || root;
160 -}
161 -
162 -function preconnectAs(
163 - rel: 'preconnect' | 'dns-prefetch',
164 - crossOrigin: null | '' | 'use-credentials',
165 - href: string,
166 -) {
167 - const ownerDocument = getDocumentForPreloads();
168 - if (typeof href === 'string' && href && ownerDocument) {
169 - const limitedEscapedHref =
170 - escapeSelectorAttributeValueInsideDoubleQuotes(href);
171 - let key = `link[rel="${rel}"][href="${limitedEscapedHref}"]`;
172 - if (typeof crossOrigin === 'string') {
173 - key += `[crossorigin="${crossOrigin}"]`;
174 - }
175 - if (!preconnectsSet.has(key)) {
176 - preconnectsSet.add(key);
177 -
178 - const preconnectProps = {rel, crossOrigin, href};
179 - if (null === ownerDocument.querySelector(key)) {
180 - const preloadInstance = ownerDocument.createElement('link');
181 - setInitialProperties(preloadInstance, 'link', preconnectProps);
182 - markNodeAsHoistable(preloadInstance);
183 - (ownerDocument.head: any).appendChild(preloadInstance);
184 - }
185 - }
186 - }
187 -}
188 -
189 -// --------------------------------------
190 -// ReactDOM.prefetchDNS
191 -// --------------------------------------
192 -function prefetchDNS(href: string, options?: mixed) {
193 - if (__DEV__) {
194 - if (typeof href !== 'string' || !href) {
195 - console.error(
196 - 'ReactDOM.prefetchDNS(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.',
197 - getValueDescriptorExpectingObjectForWarning(href),
198 - );
199 - } else if (options != null) {
200 - if (
201 - typeof options === 'object' &&
202 - hasOwnProperty.call(options, 'crossOrigin')
203 - ) {
204 - console.error(
205 - '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`.',
206 - getValueDescriptorExpectingEnumForWarning(options),
207 - );
208 - } else {
209 - console.error(
210 - '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`.',
211 - getValueDescriptorExpectingEnumForWarning(options),
212 - );
213 - }
214 - }
215 - }
216 - preconnectAs('dns-prefetch', null, href);
217 -}
218 -
219 -// --------------------------------------
220 -// ReactDOM.preconnect
221 -// --------------------------------------
222 -function preconnect(href: string, options?: {crossOrigin?: string}) {
223 - if (__DEV__) {
224 - if (typeof href !== 'string' || !href) {
225 - console.error(
226 - 'ReactDOM.preconnect(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.',
227 - getValueDescriptorExpectingObjectForWarning(href),
228 - );
229 - } else if (options != null && typeof options !== 'object') {
230 - console.error(
231 - '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.',
232 - getValueDescriptorExpectingEnumForWarning(options),
233 - );
234 - } else if (options != null && typeof options.crossOrigin !== 'string') {
235 - console.error(
236 - '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.',
237 - getValueDescriptorExpectingObjectForWarning(options.crossOrigin),
238 - );
239 - }
240 - }
241 - const crossOrigin =
242 - options == null || typeof options.crossOrigin !== 'string'
243 - ? null
244 - : options.crossOrigin === 'use-credentials'
245 - ? 'use-credentials'
246 - : '';
247 - preconnectAs('preconnect', crossOrigin, href);
248 -}
249 -
250 -// --------------------------------------
251 -// ReactDOM.preload
252 -// --------------------------------------
253 -type PreloadAs = ResourceType;
254 -type PreloadOptions = {
255 - as: PreloadAs,
256 - crossOrigin?: string,
257 - integrity?: string,
258 - type?: string,
259 -};
260 -function preload(href: string, options: PreloadOptions) {
261 - if (__DEV__) {
262 - validatePreloadArguments(href, options);
263 - }
264 - const ownerDocument = getDocumentForPreloads();
265 - if (
266 - typeof href === 'string' &&
267 - href &&
268 - typeof options === 'object' &&
269 - options !== null &&
270 - ownerDocument
271 - ) {
272 - const as = options.as;
273 - const limitedEscapedHref =
274 - escapeSelectorAttributeValueInsideDoubleQuotes(href);
275 - const preloadKey = `link[rel="preload"][as="${as}"][href="${limitedEscapedHref}"]`;
276 - let key = preloadKey;
277 - switch (as) {
278 - case 'style':
279 - key = getStyleKey(href);
280 - break;
281 - case 'script':
282 - key = getScriptKey(href);
283 - break;
284 - }
285 - if (!preloadPropsMap.has(key)) {
286 - const preloadProps = preloadPropsFromPreloadOptions(href, as, options);
287 - preloadPropsMap.set(key, preloadProps);
288 -
289 - if (null === ownerDocument.querySelector(preloadKey)) {
290 - const preloadInstance = ownerDocument.createElement('link');
291 - setInitialProperties(preloadInstance, 'link', preloadProps);
292 - markNodeAsHoistable(preloadInstance);
293 - (ownerDocument.head: any).appendChild(preloadInstance);
294 - }
295 - }
296 - }
297 -}
298 -
299 -function preloadPropsFromPreloadOptions(
300 - href: string,
301 - as: ResourceType,
302 - options: PreloadOptions,
303 -): PreloadProps {
304 - return {
305 - href,
306 - rel: 'preload',
307 - as,
308 - crossOrigin: as === 'font' ? '' : options.crossOrigin,
309 - integrity: options.integrity,
310 - type: options.type,
311 - };
312 -}
313 -
314 -// --------------------------------------
315 -// ReactDOM.preinit
316 -// --------------------------------------
317 -
318 -type PreinitAs = 'style' | 'script';
319 -type PreinitOptions = {
320 - as: PreinitAs,
321 - precedence?: string,
322 - crossOrigin?: string,
323 - integrity?: string,
324 -};
325 -function preinit(href: string, options: PreinitOptions) {
326 - if (__DEV__) {
327 - validatePreinitArguments(href, options);
328 - }
329 -
330 - if (
331 - typeof href === 'string' &&
332 - href &&
333 - typeof options === 'object' &&
334 - options !== null
335 - ) {
336 - const resourceRoot = getCurrentResourceRoot();
337 - const as = options.as;
338 - if (!resourceRoot) {
339 - if (as === 'style' || as === 'script') {
340 - // We are going to emit a preload as a best effort fallback since this preinit
341 - // was called outside of a render. Given the passive nature of this fallback
342 - // we do not warn in dev when props disagree if there happens to already be a
343 - // matching preload with this href
344 - const preloadDocument = getDocumentForPreloads();
345 - if (preloadDocument) {
346 - const limitedEscapedHref =
347 - escapeSelectorAttributeValueInsideDoubleQuotes(href);
348 - const preloadKey = `link[rel="preload"][as="${as}"][href="${limitedEscapedHref}"]`;
349 - let key = preloadKey;
350 - switch (as) {
351 - case 'style':
352 - key = getStyleKey(href);
353 - break;
354 - case 'script':
355 - key = getScriptKey(href);
356 - break;
357 - }
358 - if (!preloadPropsMap.has(key)) {
359 - const preloadProps = preloadPropsFromPreinitOptions(
360 - href,
361 - as,
362 - options,
363 - );
364 - preloadPropsMap.set(key, preloadProps);
365 -
366 - if (null === preloadDocument.querySelector(preloadKey)) {
367 - const preloadInstance = preloadDocument.createElement('link');
368 - setInitialProperties(preloadInstance, 'link', preloadProps);
369 - markNodeAsHoistable(preloadInstance);
370 - (preloadDocument.head: any).appendChild(preloadInstance);
371 - }
372 - }
373 - }
374 - }
375 - return;
376 - }
377 -
378 - switch (as) {
379 - case 'style': {
380 - const styles = getResourcesFromRoot(resourceRoot).hoistableStyles;
381 -
382 - const key = getStyleKey(href);
383 - const precedence = options.precedence || 'default';
384 -
385 - // Check if this resource already exists
386 - let resource = styles.get(key);
387 - if (resource) {
388 - // We can early return. The resource exists and there is nothing
389 - // more to do
390 - return;
391 - }
392 -
393 - // Attempt to hydrate instance from DOM
394 - let instance: null | Instance = resourceRoot.querySelector(
395 - getStylesheetSelectorFromKey(key),
396 - );
397 - if (!instance) {
398 - // Construct a new instance and insert it
399 - const stylesheetProps = stylesheetPropsFromPreinitOptions(
400 - href,
401 - precedence,
402 - options,
403 - );
404 - const preloadProps = preloadPropsMap.get(key);
405 - if (preloadProps) {
406 - adoptPreloadPropsForStylesheet(stylesheetProps, preloadProps);
407 - }
408 - const ownerDocument = getDocumentFromRoot(resourceRoot);
409 - instance = ownerDocument.createElement('link');
410 - markNodeAsHoistable(instance);
411 - setInitialProperties(instance, 'link', stylesheetProps);
412 - insertStylesheet(instance, precedence, resourceRoot);
413 - }
414 -
415 - // Construct a Resource and cache it
416 - resource = {
417 - type: 'stylesheet',
418 - instance,
419 - count: 1,
420 - };
421 - styles.set(key, resource);
422 - return;
423 - }
424 - case 'script': {
425 - const src = href;
426 - const scripts = getResourcesFromRoot(resourceRoot).hoistableScripts;
427 -
428 - const key = getScriptKey(src);
429 -
430 - // Check if this resource already exists
431 - let resource = scripts.get(key);
432 - if (resource) {
433 - // We can early return. The resource exists and there is nothing
434 - // more to do
435 - return;
436 - }
437 -
438 - // Attempt to hydrate instance from DOM
439 - let instance: null | Instance = resourceRoot.querySelector(
440 - getScriptSelectorFromKey(key),
441 - );
442 - if (!instance) {
443 - // Construct a new instance and insert it
444 - const scriptProps = scriptPropsFromPreinitOptions(src, options);
445 - // Adopt certain preload props
446 - const preloadProps = preloadPropsMap.get(key);
447 - if (preloadProps) {
448 - adoptPreloadPropsForScript(scriptProps, preloadProps);
449 - }
450 - const ownerDocument = getDocumentFromRoot(resourceRoot);
451 - instance = ownerDocument.createElement('script');
452 - markNodeAsHoistable(instance);
453 - setInitialProperties(instance, 'link', scriptProps);
454 - (ownerDocument.head: any).appendChild(instance);
455 - }
456 -
457 - // Construct a Resource and cache it
458 - resource = {
459 - type: 'script',
460 - instance,
461 - count: 1,
462 - };
463 - scripts.set(key, resource);
464 - return;
465 - }
466 - }
467 - }
468 -}
469 -
470 -function preloadPropsFromPreinitOptions(
471 - href: string,
472 - as: ResourceType,
473 - options: PreinitOptions,
474 -): PreloadProps {
475 - return {
476 - href,
477 - rel: 'preload',
478 - as,
479 - crossOrigin: as === 'font' ? '' : options.crossOrigin,
480 - integrity: options.integrity,
481 - };
482 -}
483 -
484 -function stylesheetPropsFromPreinitOptions(
485 - href: string,
486 - precedence: string,
487 - options: PreinitOptions,
488 -): StylesheetProps {
489 - return {
490 - rel: 'stylesheet',
491 - href,
492 - 'data-precedence': precedence,
493 - crossOrigin: options.crossOrigin,
494 - };
495 -}
496 -
497 -function scriptPropsFromPreinitOptions(
498 - src: string,
499 - options: PreinitOptions,
500 -): ScriptProps {
501 - return {
502 - src,
503 - async: true,
504 - crossOrigin: options.crossOrigin,
505 - integrity: options.integrity,
506 - };
507 -}
508 -
509 -// --------------------------------------
510 -// Resources from render
511 -// --------------------------------------
512 -
513 -type StyleTagQualifyingProps = {
514 - href: string,
515 - precedence: string,
516 - [string]: mixed,
517 -};
518 -
519 -type StylesheetQualifyingProps = {
520 - rel: 'stylesheet',
521 - href: string,
522 - precedence: string,
523 - [string]: mixed,
524 -};
525 -
526 -// This function is called in begin work and we should always have a currentDocument set
527 -export function getResource(
528 - type: string,
529 - currentProps: any,
530 - pendingProps: any,
531 -): null | Resource {
532 - const resourceRoot = getCurrentResourceRoot();
533 - if (!resourceRoot) {
534 - throw new Error(
535 - '"resourceRoot" was expected to exist. This is a bug in React.',
536 - );
537 - }
538 - switch (type) {
539 - case 'meta':
540 - case 'title': {
541 - return null;
542 - }
543 - case 'style': {
544 - if (
545 - typeof pendingProps.precedence === 'string' &&
546 - typeof pendingProps.href === 'string'
547 - ) {
548 - const key = getStyleKey(pendingProps.href);
549 - const styles = getResourcesFromRoot(resourceRoot).hoistableStyles;
550 - let resource = styles.get(key);
551 - if (!resource) {
552 - resource = {
553 - type: 'style',
554 - instance: null,
555 - count: 0,
556 - };
557 - styles.set(key, resource);
558 - }
559 - return resource;
560 - }
561 - return {
562 - type: 'void',
563 - instance: null,
564 - count: 0,
565 - };
566 - }
567 - case 'link': {
568 - if (
569 - pendingProps.rel === 'stylesheet' &&
570 - typeof pendingProps.href === 'string' &&
571 - typeof pendingProps.precedence === 'string'
572 - ) {
573 - const qualifiedProps: StylesheetQualifyingProps = pendingProps;
574 - const key = getStyleKey(qualifiedProps.href);
575 -
576 - const styles = getResourcesFromRoot(resourceRoot).hoistableStyles;
577 -
578 - let resource = styles.get(key);
579 - if (!resource) {
580 - // We asserted this above but Flow can't figure out that the type satisfies
581 - const ownerDocument = getDocumentFromRoot(resourceRoot);
582 - resource = {
583 - type: 'stylesheet',
584 - instance: null,
585 - count: 0,
586 - };
587 - styles.set(key, resource);
588 - if (!preloadPropsMap.has(key)) {
589 - preloadStylesheet(
590 - ownerDocument,
591 - key,
592 - preloadPropsFromStylesheet(qualifiedProps),
593 - );
594 - }
595 - }
596 - return resource;
597 - }
598 - return null;
599 - }
600 - case 'script': {
601 - if (typeof pendingProps.src === 'string' && pendingProps.async === true) {
602 - const scriptProps: ScriptProps = pendingProps;
603 - const key = getScriptKey(scriptProps.src);
604 - const scripts = getResourcesFromRoot(resourceRoot).hoistableScripts;
605 -
606 - let resource = scripts.get(key);
607 - if (!resource) {
608 - resource = {
609 - type: 'script',
610 - instance: null,
611 - count: 0,
612 - };
613 - scripts.set(key, resource);
614 - }
615 - return resource;
616 - }
617 - return {
618 - type: 'void',
619 - instance: null,
620 - count: 0,
621 - };
622 - }
623 - default: {
624 - throw new Error(
625 - `getResource encountered a type it did not expect: "${type}". this is a bug in React.`,
626 - );
627 - }
628 - }
629 -}
630 -
631 -function styleTagPropsFromRawProps(
632 - rawProps: StyleTagQualifyingProps,
633 -): StyleTagProps {
634 - return {
635 - ...rawProps,
636 - 'data-href': rawProps.href,
637 - 'data-precedence': rawProps.precedence,
638 - href: null,
639 - precedence: null,
640 - };
641 -}
642 -
643 -function getStyleKey(href: string) {
644 - const limitedEscapedHref =
645 - escapeSelectorAttributeValueInsideDoubleQuotes(href);
646 - return `href~="${limitedEscapedHref}"`;
647 -}
648 -
649 -function getStyleTagSelectorFromKey(key: string) {
650 - return `style[data-${key}]`;
651 -}
652 -
653 -function getStylesheetSelectorFromKey(key: string) {
654 - return `link[rel="stylesheet"][${key}]`;
655 -}
656 -
657 -function getPreloadStylesheetSelectorFromKey(key: string) {
658 - return `link[rel="preload"][as="style"][${key}]`;
659 -}
660 -
661 -function stylesheetPropsFromRawProps(
662 - rawProps: StylesheetQualifyingProps,
663 -): StylesheetProps {
664 - return {
665 - ...rawProps,
666 - 'data-precedence': rawProps.precedence,
667 - precedence: null,
668 - };
669 -}
670 -
671 -function preloadStylesheet(
672 - ownerDocument: Document,
673 - key: string,
674 - preloadProps: PreloadProps,
675 -) {
676 - preloadPropsMap.set(key, preloadProps);
677 -
678 - if (!ownerDocument.querySelector(getStylesheetSelectorFromKey(key))) {
679 - // There is no matching stylesheet instance in the Document.
680 - // We will insert a preload now to kick off loading because
681 - // we expect this stylesheet to commit
682 - if (
683 - null ===
684 - ownerDocument.querySelector(getPreloadStylesheetSelectorFromKey(key))
685 - ) {
686 - const preloadInstance = ownerDocument.createElement('link');
687 - setInitialProperties(preloadInstance, 'link', preloadProps);
688 - markNodeAsHoistable(preloadInstance);
689 - (ownerDocument.head: any).appendChild(preloadInstance);
690 - }
691 - }
692 -}
693 -
694 -function preloadPropsFromStylesheet(
695 - props: StylesheetQualifyingProps,
696 -): PreloadProps {
697 - return {
698 - rel: 'preload',
699 - as: 'style',
700 - href: props.href,
701 - crossOrigin: props.crossOrigin,
702 - integrity: props.integrity,
703 - media: props.media,
704 - hrefLang: props.hrefLang,
705 - referrerPolicy: props.referrerPolicy,
706 - };
707 -}
708 -
709 -function getScriptKey(src: string): string {
710 - const limitedEscapedSrc = escapeSelectorAttributeValueInsideDoubleQuotes(src);
711 - return `[src="${limitedEscapedSrc}"]`;
712 -}
713 -
714 -function getScriptSelectorFromKey(key: string): string {
715 - return 'script[async]' + key;
716 -}
717 -
718 -// --------------------------------------
719 -// Hoistable Resource Reconciliation
720 -// --------------------------------------
721 -
722 -export function acquireResource(
723 - hoistableRoot: HoistableRoot,
724 - resource: Resource,
725 - props: any,
726 -): null | Instance {
727 - resource.count++;
728 - if (resource.instance === null) {
729 - switch (resource.type) {
730 - case 'style': {
731 - const qualifiedProps: StyleTagQualifyingProps = props;
732 - const key = getStyleKey(qualifiedProps.href);
733 -
734 - // Attempt to hydrate instance from DOM
735 - let instance: null | Instance = hoistableRoot.querySelector(
736 - getStyleTagSelectorFromKey(key),
737 - );
738 - if (instance) {
739 - resource.instance = instance;
740 - markNodeAsHoistable(instance);
741 - return instance;
742 - }
743 -
744 - const styleProps = styleTagPropsFromRawProps(props);
745 - const ownerDocument = getDocumentFromRoot(hoistableRoot);
746 - instance = ownerDocument.createElement('style');
747 -
748 - markNodeAsHoistable(instance);
749 - setInitialProperties(instance, 'style', styleProps);
750 - insertStylesheet(instance, qualifiedProps.precedence, hoistableRoot);
751 - resource.instance = instance;
752 -
753 - return instance;
754 - }
755 - case 'stylesheet': {
756 - // This typing is enforce by `getResource`. If we change the logic
757 - // there for what qualifies as a stylesheet resource we need to ensure
758 - // this cast still makes sense;
759 - const qualifiedProps: StylesheetQualifyingProps = props;
760 - const key = getStyleKey(qualifiedProps.href);
761 -
762 - // Attempt to hydrate instance from DOM
763 - let instance: null | Instance = hoistableRoot.querySelector(
764 - getStylesheetSelectorFromKey(key),
765 - );
766 - if (instance) {
767 - resource.instance = instance;
768 - markNodeAsHoistable(instance);
769 - return instance;
770 - }
771 -
772 - const stylesheetProps = stylesheetPropsFromRawProps(props);
773 - const preloadProps = preloadPropsMap.get(key);
774 - if (preloadProps) {
775 - adoptPreloadPropsForStylesheet(stylesheetProps, preloadProps);
776 - }
777 -
778 - // Construct and insert a new instance
779 - const ownerDocument = getDocumentFromRoot(hoistableRoot);
780 - instance = ownerDocument.createElement('link');
781 - markNodeAsHoistable(instance);
782 - const linkInstance: HTMLLinkElement = (instance: any);
783 - (linkInstance: any)._p = new Promise((resolve, reject) => {
784 - linkInstance.onload = resolve;
785 - linkInstance.onerror = reject;
786 - }).then(
787 - () => ((linkInstance: any)._p.s = 'l'),
788 - () => ((linkInstance: any)._p.s = 'e'),
789 - );
790 - setInitialProperties(instance, 'link', stylesheetProps);
791 - insertStylesheet(instance, qualifiedProps.precedence, hoistableRoot);
792 - resource.instance = instance;
793 -
794 - return instance;
795 - }
796 - case 'script': {
797 - // This typing is enforce by `getResource`. If we change the logic
798 - // there for what qualifies as a stylesheet resource we need to ensure
799 - // this cast still makes sense;
800 - const borrowedScriptProps: ScriptProps = props;
801 - const key = getScriptKey(borrowedScriptProps.src);
802 -
803 - // Attempt to hydrate instance from DOM
804 - let instance: null | Instance = hoistableRoot.querySelector(
805 - getScriptSelectorFromKey(key),
806 - );
807 - if (instance) {
808 - resource.instance = instance;
809 - markNodeAsHoistable(instance);
810 - return instance;
811 - }
812 -
813 - let scriptProps = borrowedScriptProps;
814 - const preloadProps = preloadPropsMap.get(key);
815 - if (preloadProps) {
816 - scriptProps = {...borrowedScriptProps};
817 - adoptPreloadPropsForScript(scriptProps, preloadProps);
818 - }
819 -
820 - // Construct and insert a new instance
821 - const ownerDocument = getDocumentFromRoot(hoistableRoot);
822 - instance = ownerDocument.createElement('script');
823 - markNodeAsHoistable(instance);
824 - setInitialProperties(instance, 'link', scriptProps);
825 - (ownerDocument.head: any).appendChild(instance);
826 - resource.instance = instance;
827 -
828 - return instance;
829 - }
830 - case 'void': {
831 - return null;
832 - }
833 - default: {
834 - throw new Error(
835 - `acquireResource encountered a resource type it did not expect: "${resource.type}". this is a bug in React.`,
836 - );
837 - }
838 - }
839 - }
840 - return resource.instance;
841 -}
842 -
843 -export function releaseResource(resource: Resource): void {
844 - resource.count--;
845 -}
846 -
847 -function insertStylesheet(
848 - instance: Instance,
849 - precedence: string,
850 - root: HoistableRoot,
851 -): void {
852 - const nodes = root.querySelectorAll(
853 - 'link[rel="stylesheet"][data-precedence],style[data-precedence]',
854 - );
855 - const last = nodes.length ? nodes[nodes.length - 1] : null;
856 - let prior = last;
857 - for (let i = 0; i < nodes.length; i++) {
858 - const node = nodes[i];
859 - const nodePrecedence = node.dataset.precedence;
860 - if (nodePrecedence === precedence) {
861 - prior = node;
862 - } else if (prior !== last) {
863 - break;
864 - }
865 - }
866 - if (prior) {
867 - // We get the prior from the document so we know it is in the tree.
868 - // We also know that links can't be the topmost Node so the parentNode
869 - // must exist.
870 - ((prior.parentNode: any): Node).insertBefore(instance, prior.nextSibling);
871 - } else {
872 - const parent =
873 - root.nodeType === DOCUMENT_NODE
874 - ? ((((root: any): Document).head: any): Element)
875 - : ((root: any): ShadowRoot);
876 - parent.insertBefore(instance, parent.firstChild);
877 - }
878 -}
879 -
880 -function adoptPreloadPropsForStylesheet(
881 - stylesheetProps: StylesheetProps,
882 - preloadProps: PreloadProps,
883 -): void {
884 - if (stylesheetProps.crossOrigin == null)
885 - stylesheetProps.crossOrigin = preloadProps.crossOrigin;
886 - if (stylesheetProps.referrerPolicy == null)
887 - stylesheetProps.referrerPolicy = preloadProps.referrerPolicy;
888 - if (stylesheetProps.title == null) stylesheetProps.title = preloadProps.title;
889 -}
890 -
891 -function adoptPreloadPropsForScript(
892 - scriptProps: ScriptProps,
893 - preloadProps: PreloadProps,
894 -): void {
895 - if (scriptProps.crossOrigin == null)
896 - scriptProps.crossOrigin = preloadProps.crossOrigin;
897 - if (scriptProps.referrerPolicy == null)
898 - scriptProps.referrerPolicy = preloadProps.referrerPolicy;
899 - if (scriptProps.integrity == null)
900 - scriptProps.referrerPolicy = preloadProps.integrity;
901 -}
902 -
903 -// --------------------------------------
904 -// Hoistable Element Reconciliation
905 -// --------------------------------------
906 -
907 -type KeyedTagCache = Map<string, Array<Element>>;
908 -type DocumentTagCaches = Map<Document, KeyedTagCache>;
909 -let tagCaches: null | DocumentTagCaches = null;
910 -
911 -export function hydrateHoistable(
912 - hoistableRoot: HoistableRoot,
913 - type: HoistableTagType,
914 - props: any,
915 - internalInstanceHandle: Object,
916 -): Instance {
917 - const ownerDocument = getDocumentFromRoot(hoistableRoot);
918 -
919 - let instance: ?Instance = null;
920 - getInstance: switch (type) {
921 - case 'title': {
922 - instance = ownerDocument.getElementsByTagName('title')[0];
923 - if (
924 - !instance ||
925 - isOwnedInstance(instance) ||
926 - instance.namespaceURI === SVG_NAMESPACE ||
927 - instance.hasAttribute('itemprop')
928 - ) {
929 - instance = ownerDocument.createElement(type);
930 - (ownerDocument.head: any).insertBefore(
931 - instance,
932 - ownerDocument.querySelector('head > title'),
933 - );
934 - }
935 - setInitialProperties(instance, type, props);
936 - precacheFiberNode(internalInstanceHandle, instance);
937 - markNodeAsHoistable(instance);
938 - return instance;
939 - }
940 - case 'link': {
941 - const cache = getHydratableHoistableCache('link', 'href', ownerDocument);
942 - const key = type + (props.href || '');
943 - const maybeNodes = cache.get(key);
944 - if (maybeNodes) {
945 - const nodes = maybeNodes;
946 - for (let i = 0; i < nodes.length; i++) {
947 - const node = nodes[i];
948 - if (
949 - node.getAttribute('href') !==
950 - (props.href == null ? null : props.href) ||
951 - node.getAttribute('rel') !==
952 - (props.rel == null ? null : props.rel) ||
953 - node.getAttribute('title') !==
954 - (props.title == null ? null : props.title) ||
955 - node.getAttribute('crossorigin') !==
956 - (props.crossOrigin == null ? null : props.crossOrigin)
957 - ) {
958 - // mismatch, try the next node;
959 - continue;
960 - }
961 - instance = node;
962 - nodes.splice(i, 1);
963 - break getInstance;
964 - }
965 - }
966 - instance = ownerDocument.createElement(type);
967 - setInitialProperties(instance, type, props);
968 - (ownerDocument.head: any).appendChild(instance);
969 - break;
970 - }
971 - case 'meta': {
972 - const cache = getHydratableHoistableCache(
973 - 'meta',
974 - 'content',
975 - ownerDocument,
976 - );
977 - const key = type + (props.content || '');
978 - const maybeNodes = cache.get(key);
979 - if (maybeNodes) {
980 - const nodes = maybeNodes;
981 - for (let i = 0; i < nodes.length; i++) {
982 - const node = nodes[i];
983 -
984 - // We coerce content to string because it is the most likely one to
985 - // use a `toString` capable value. For the rest we just do identity match
986 - // passing non-strings here is not really valid anyway.
987 - if (__DEV__) {
988 - checkAttributeStringCoercion(props.content, 'content');
989 - }
990 - if (
991 - node.getAttribute('content') !==
992 - (props.content == null ? null : '' + props.content) ||
993 - node.getAttribute('name') !==
994 - (props.name == null ? null : props.name) ||
995 - node.getAttribute('property') !==
996 - (props.property == null ? null : props.property) ||
997 - node.getAttribute('http-equiv') !==
998 - (props.httpEquiv == null ? null : props.httpEquiv) ||
999 - node.getAttribute('charset') !==
1000 - (props.charSet == null ? null : props.charSet)
1001 - ) {
1002 - // mismatch, try the next node;
1003 - continue;
1004 - }
1005 - instance = node;
1006 - nodes.splice(i, 1);
1007 - break getInstance;
1008 - }
1009 - }
1010 - instance = ownerDocument.createElement(type);
1011 - setInitialProperties(instance, type, props);
1012 - (ownerDocument.head: any).appendChild(instance);
1013 - break;
1014 - }
1015 - default:
1016 - throw new Error(
1017 - `getNodesForType encountered a type it did not expect: "${type}". This is a bug in React.`,
1018 - );
1019 - }
1020 -
1021 - // This node is a match
1022 - precacheFiberNode(internalInstanceHandle, instance);
1023 - markNodeAsHoistable(instance);
1024 - return instance;
1025 -}
1026 -
1027 -function getHydratableHoistableCache(
1028 - type: HoistableTagType,
1029 - keyAttribute: string,
1030 - ownerDocument: Document,
1031 -): KeyedTagCache {
1032 - let cache: KeyedTagCache;
1033 - let caches: DocumentTagCaches;
1034 - if (tagCaches === null) {
1035 - cache = new Map();
1036 - caches = tagCaches = new Map();
1037 - caches.set(ownerDocument, cache);
1038 - } else {
1039 - caches = tagCaches;
1040 - const maybeCache = caches.get(ownerDocument);
1041 - if (!maybeCache) {
1042 - cache = new Map();
1043 - caches.set(ownerDocument, cache);
1044 - } else {
1045 - cache = maybeCache;
1046 - }
1047 - }
1048 -
1049 - if (cache.has(type)) {
1050 - // We use type as a special key that signals that this cache has been seeded for this type
1051 - return cache;
1052 - }
1053 -
1054 - // Mark this cache as seeded for this type
1055 - cache.set(type, (null: any));
1056 -
1057 - const nodes = ownerDocument.getElementsByTagName(type);
1058 - for (let i = 0; i < nodes.length; i++) {
1059 - const node = nodes[i];
1060 - if (
1061 - !isOwnedInstance(node) &&
1062 - (type !== 'link' || node.getAttribute('rel') !== 'stylesheet') &&
1063 - node.namespaceURI !== SVG_NAMESPACE
1064 - ) {
1065 - const nodeKey = node.getAttribute(keyAttribute) || '';
1066 - const key = type + nodeKey;
1067 - const existing = cache.get(key);
1068 - if (existing) {
1069 - existing.push(node);
1070 - } else {
1071 - cache.set(key, [node]);
1072 - }
1073 - }
1074 - }
1075 -
1076 - return cache;
1077 -}
1078 -
1079 -export function mountHoistable(
1080 - hoistableRoot: HoistableRoot,
1081 - type: HoistableTagType,
1082 - instance: Instance,
1083 -): void {
1084 - const ownerDocument = getDocumentFromRoot(hoistableRoot);
1085 - (ownerDocument.head: any).insertBefore(
1086 - instance,
1087 - type === 'title' ? ownerDocument.querySelector('head > title') : null,
1088 - );
1089 -}
1090 -
1091 -export function unmountHoistable(instance: Instance): void {
1092 - (instance.parentNode: any).removeChild(instance);
1093 -}
1094 -
1095 -// When passing user input into querySelector(All) the embedded string must not alter
1096 -// the semantics of the query. This escape function is safe to use when we know the
1097 -// provided value is going to be wrapped in double quotes as part of an attribute selector
1098 -// Do not use it anywhere else
1099 -// we escape double quotes and backslashes
1100 -const escapeSelectorAttributeValueInsideDoubleQuotesRegex = /[\n\"\\]/g;
1101 -function escapeSelectorAttributeValueInsideDoubleQuotes(value: string): string {
1102 - return value.replace(
1103 - escapeSelectorAttributeValueInsideDoubleQuotesRegex,
1104 - ch => '\\' + ch.charCodeAt(0).toString(16),
1105 - );
1106 -}
packages/react-dom-bindings/src/client/ReactDOMHostConfig.js
+1598 -232
@@ -18,6 +18,16 @@ import type {
18 import type {ReactScopeInstance} from 'shared/ReactTypes';
19 import type {AncestorInfoDev} from './validateDOMNesting';
20
21 +import {getCurrentRootHostContainer} from 'react-reconciler/src/ReactFiberHostContext';
22 +import {DefaultEventPriority} from 'react-reconciler/src/ReactEventPriorities';
23 +// TODO: Remove this deep import when we delete the legacy root API
24 +import {ConcurrentMode, NoMode} from 'react-reconciler/src/ReactTypeOfMode';
25 +
26 +import hasOwnProperty from 'shared/hasOwnProperty';
27 +import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
28 +import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals.js';
29 +const {Dispatcher} = ReactDOMSharedInternals;
30 +
31 import {
32 precacheFiberNode,
33 updateFiberProps,
@@ -26,8 +36,10 @@ import {
36 getInstanceFromNode as getInstanceFromNodeDOMTree,
37 isContainerMarkedAsRoot,
38 detachDeletedInstance,
39 + getResourcesFromRoot,
40 isMarkedHoistable,
41 markNodeAsHoistable,
42 + isOwnedInstance,
43 } from './ReactDOMComponentTree';
44 export {detachDeletedInstance};
45 import {hasRole} from './DOMAccessibilityRoles';
@@ -86,17 +98,13 @@ import {
98 HostSingleton,
99 } from 'react-reconciler/src/ReactWorkTags';
100 import {listenToAllSupportedEvents} from '../events/DOMPluginEventSystem';
89 -
90 -import {DefaultEventPriority} from 'react-reconciler/src/ReactEventPriorities';
91 -
92 -// TODO: Remove this deep import when we delete the legacy root API
93 -import {ConcurrentMode, NoMode} from 'react-reconciler/src/ReactTypeOfMode';
94 -
101 import {
96 - prepareToRenderResources,
97 - cleanupAfterRenderResources,
98 -} from './ReactDOMFloatClient';
99 -import {validateLinkPropsForStyleResource} from '../shared/ReactDOMResourceValidation';
102 + validatePreloadArguments,
103 + validatePreinitArguments,
104 + validateLinkPropsForStyleResource,
105 + getValueDescriptorExpectingObjectForWarning,
106 + getValueDescriptorExpectingEnumForWarning,
107 +} from '../shared/ReactDOMResourceValidation';
108
109 export type Type = string;
110 export type Props = {
@@ -138,7 +146,6 @@ export type Container =
146 | interface extends DocumentFragment {_reactRootContainer?: FiberRoot};
147 export type Instance = Element;
148 export type TextInstance = Text;
141 -export type {HoistableRoot} from './ReactDOMFloatClient';
149 export interface SuspenseInstance extends Comment {
150 _reactRetry?: () => void;
151 }
@@ -1613,227 +1620,6 @@ export function requestPostPaintCallback(callback: (time: number) => void) {
1620 });
1621 }
1622
1616 -export function maySuspendCommit(type: Type, props: Props): boolean {
1617 - return false;
1618 -}
1619 -
1620 -export function preloadInstance(type: Type, props: Props): boolean {
1621 - // Return true to indicate it's already loaded
1622 - return true;
1623 -}
1624 -
1625 -export function startSuspendingCommit(): void {}
1626 -
1627 -export function suspendInstance(type: Type, props: Props): void {}
1628 -
1629 -export function waitForCommitToBeReady(): null {
1630 - return null;
1631 -}
1632 -
1633 -// -------------------
1634 -// Resources
1635 -// -------------------
1636 -
1637 -export const supportsResources = true;
1638 -
1639 -export function isHostHoistableType(
1640 - type: string,
1641 - props: RawProps,
1642 - hostContext: HostContext,
1643 -): boolean {
1644 - let outsideHostContainerContext: boolean;
1645 - let namespace: HostContextProd;
1646 - if (__DEV__) {
1647 - const hostContextDev: HostContextDev = (hostContext: any);
1648 - // We can only render resources when we are not within the host container context
1649 - outsideHostContainerContext =
1650 - !hostContextDev.ancestorInfo.containerTagInScope;
1651 - namespace = hostContextDev.namespace;
1652 - } else {
1653 - const hostContextProd: HostContextProd = (hostContext: any);
1654 - namespace = hostContextProd;
1655 - }
1656 -
1657 - // Global opt out of hoisting for anything in SVG Namespace or anything with an itemProp inside an itemScope
1658 - if (namespace === SVG_NAMESPACE || props.itemProp != null) {
1659 - if (__DEV__) {
1660 - if (
1661 - outsideHostContainerContext &&
1662 - props.itemProp != null &&
1663 - (type === 'meta' ||
1664 - type === 'title' ||
1665 - type === 'style' ||
1666 - type === 'link' ||
1667 - type === 'script')
1668 - ) {
1669 - console.error(
1670 - 'Cannot render a <%s> outside the main document if it has an `itemProp` prop. `itemProp` suggests the tag belongs to an' +
1671 - ' `itemScope` which can appear anywhere in the DOM. If you were intending for React to hoist this <%s> remove the `itemProp` prop.' +
1672 - ' Otherwise, try moving this tag into the <head> or <body> of the Document.',
1673 - type,
1674 - type,
1675 - );
1676 - }
1677 - }
1678 - return false;
1679 - }
1680 -
1681 - switch (type) {
1682 - case 'meta':
1683 - case 'title': {
1684 - return true;
1685 - }
1686 - case 'style': {
1687 - if (
1688 - typeof props.precedence !== 'string' ||
1689 - typeof props.href !== 'string' ||
1690 - props.href === ''
1691 - ) {
1692 - if (__DEV__) {
1693 - if (outsideHostContainerContext) {
1694 - console.error(
1695 - 'Cannot render a <style> outside the main document without knowing its precedence and a unique href key.' +
1696 - ' React can hoist and deduplicate <style> tags if you provide a `precedence` prop along with an `href` prop that' +
1697 - ' does not conflic with the `href` values used in any other hoisted <style> or <link rel="stylesheet" ...> tags. ' +
1698 - ' Note that hoisting <style> tags is considered an advanced feature that most will not use directly.' +
1699 - ' Consider moving the <style> tag to the <head> or consider adding a `precedence="default"` and `href="some unique resource identifier"`, or move the <style>' +
1700 - ' to the <style> tag.',
1701 - );
1702 - }
1703 - }
1704 - return false;
1705 - }
1706 - return true;
1707 - }
1708 - case 'link': {
1709 - if (
1710 - typeof props.rel !== 'string' ||
1711 - typeof props.href !== 'string' ||
1712 - props.href === '' ||
1713 - props.onLoad ||
1714 - props.onError
1715 - ) {
1716 - if (__DEV__) {
1717 - if (
1718 - props.rel === 'stylesheet' &&
1719 - typeof props.precedence === 'string'
1720 - ) {
1721 - validateLinkPropsForStyleResource(props);
1722 - }
1723 - if (outsideHostContainerContext) {
1724 - if (
1725 - typeof props.rel !== 'string' ||
1726 - typeof props.href !== 'string' ||
1727 - props.href === ''
1728 - ) {
1729 - console.error(
1730 - 'Cannot render a <link> outside the main document without a `rel` and `href` prop.' +
1731 - ' Try adding a `rel` and/or `href` prop to this <link> or moving the link into the <head> tag',
1732 - );
1733 - } else if (props.onError || props.onLoad) {
1734 - console.error(
1735 - 'Cannot render a <link> with onLoad or onError listeners outside the main document.' +
1736 - ' Try removing onLoad={...} and onError={...} or moving it into the root <head> tag or' +
1737 - ' somewhere in the <body>.',
1738 - );
1739 - }
1740 - }
1741 - }
1742 - return false;
1743 - }
1744 - switch (props.rel) {
1745 - case 'stylesheet': {
1746 - const {precedence, disabled} = props;
1747 - if (__DEV__) {
1748 - if (typeof precedence !== 'string') {
1749 - if (outsideHostContainerContext) {
1750 - console.error(
1751 - 'Cannot render a <link rel="stylesheet" /> outside the main document without knowing its precedence.' +
1752 - ' Consider adding precedence="default" or moving it into the root <head> tag.',
1753 - );
1754 - }
1755 - }
1756 - }
1757 - return typeof precedence === 'string' && disabled == null;
1758 - }
1759 - default: {
1760 - return true;
1761 - }
1762 - }
1763 - }
1764 - case 'script': {
1765 - if (
1766 - props.async !== true ||
1767 - props.onLoad ||
1768 - props.onError ||
1769 - typeof props.src !== 'string' ||
1770 - !props.src
1771 - ) {
1772 - if (__DEV__) {
1773 - if (outsideHostContainerContext) {
1774 - if (props.async !== true) {
1775 - console.error(
1776 - 'Cannot render a sync or defer <script> outside the main document without knowing its order.' +
1777 - ' Try adding async="" or moving it into the root <head> tag.',
1778 - );
1779 - } else if (props.onLoad || props.onError) {
1780 - console.error(
1781 - 'Cannot render a <script> with onLoad or onError listeners outside the main document.' +
1782 - ' Try removing onLoad={...} and onError={...} or moving it into the root <head> tag or' +
1783 - ' somewhere in the <body>.',
1784 - );
1785 - } else {
1786 - console.error(
1787 - 'Cannot render a <script> outside the main document without `async={true}` and a non-empty `src` prop.' +
1788 - ' Ensure there is a valid `src` and either make the script async or move it into the root <head> tag or' +
1789 - ' somewhere in the <body>.',
1790 - );
1791 - }
1792 - }
1793 - }
1794 - return false;
1795 - }
1796 - return true;
1797 - }
1798 - case 'noscript':
1799 - case 'template': {
1800 - if (__DEV__) {
1801 - if (outsideHostContainerContext) {
1802 - console.error(
1803 - 'Cannot render <%s> outside the main document. Try moving it into the root <head> tag.',
1804 - type,
1805 - );
1806 - }
1807 - }
1808 - return false;
1809 - }
1810 - }
1811 - return false;
1812 -}
1813 -
1814 -export function prepareRendererToRender(rootContainer: Container) {
1815 - if (enableFloat) {
1816 - prepareToRenderResources(rootContainer);
1817 - }
1818 -}
1819 -
1820 -export function resetRendererAfterRender() {
1821 - if (enableFloat) {
1822 - cleanupAfterRenderResources();
1823 - }
1824 -}
1825 -
1826 -export {
1827 - getHoistableRoot,
1828 - getResource,
1829 - acquireResource,
1830 - releaseResource,
1831 - hydrateHoistable,
1832 - mountHoistable,
1833 - unmountHoistable,
1834 - prepareToCommitHoistables,
1835 -} from './ReactDOMFloatClient';
1836 -
1623 // -------------------
1624 // Singletons
1625 // -------------------
@@ -1976,3 +1762,1583 @@ export function clearSingleton(instance: Instance): void {
1762 }
1763 return;
1764 }
1765 +
1766 +// -------------------
1767 +// Resources
1768 +// -------------------
1769 +
1770 +export const supportsResources = true;
1771 +
1772 +// The resource types we support. currently they match the form for the as argument.
1773 +// In the future this may need to change, especially when modules / scripts are supported
1774 +type ResourceType = 'style' | 'font' | 'script';
1775 +
1776 +type HoistableTagType = 'link' | 'meta' | 'title';
1777 +type TResource<
1778 + T: 'stylesheet' | 'style' | 'script' | 'void',
1779 + S: null | {...},
1780 +> = {
1781 + type: T,
1782 + instance: null | Instance,
1783 + count: number,
1784 + state: S,
1785 +};
1786 +type StylesheetResource = TResource<'stylesheet', StylesheetState>;
1787 +type StyleTagResource = TResource<'style', null>;
1788 +type StyleResource = StyleTagResource | StylesheetResource;
1789 +type ScriptResource = TResource<'script', null>;
1790 +type VoidResource = TResource<'void', null>;
1791 +type Resource = StyleResource | ScriptResource | VoidResource;
1792 +
1793 +type LoadingState = number;
1794 +const NotLoaded = /* */ 0b000;
1795 +const Loaded = /* */ 0b001;
1796 +const Errored = /* */ 0b010;
1797 +const Settled = /* */ 0b011;
1798 +const Inserted = /* */ 0b100;
1799 +
1800 +type StylesheetState = {
1801 + loading: LoadingState,
1802 + preload: null | HTMLLinkElement,
1803 +};
1804 +
1805 +type StyleTagProps = {
1806 + 'data-href': string,
1807 + 'data-precedence': string,
1808 + [string]: mixed,
1809 +};
1810 +type StylesheetProps = {
1811 + rel: 'stylesheet',
1812 + href: string,
1813 + 'data-precedence': string,
1814 + [string]: mixed,
1815 +};
1816 +
1817 +type ScriptProps = {
1818 + src: string,
1819 + async: true,
1820 + [string]: mixed,
1821 +};
1822 +
1823 +type PreloadProps = {
1824 + rel: 'preload',
1825 + href: string,
1826 + [string]: mixed,
1827 +};
1828 +
1829 +export type RootResources = {
1830 + hoistableStyles: Map<string, StyleResource>,
1831 + hoistableScripts: Map<string, ScriptResource>,
1832 +};
1833 +
1834 +export function prepareToCommitHoistables() {
1835 + tagCaches = null;
1836 +}
1837 +
1838 +// It is valid to preload even when we aren't actively rendering. For cases where Float functions are
1839 +// called when there is no rendering we track the last used document. It is not safe to insert
1840 +// arbitrary resources into the lastCurrentDocument b/c it may not actually be the document
1841 +// that the resource is meant to apply too (for example stylesheets or scripts). This is only
1842 +// appropriate for resources that don't really have a strict tie to the document itself for example
1843 +// preloads
1844 +let lastCurrentDocument: ?Document = null;
1845 +let previousDispatcher = null;
1846 +export function prepareRendererToRender(rootContainer: Container) {
1847 + if (enableFloat) {
1848 + const rootNode = getHoistableRoot(rootContainer);
1849 + lastCurrentDocument = getDocumentFromRoot(rootNode);
1850 +
1851 + previousDispatcher = Dispatcher.current;
1852 + Dispatcher.current = ReactDOMClientDispatcher;
1853 + }
1854 +}
1855 +
1856 +export function resetRendererAfterRender() {
1857 + if (enableFloat) {
1858 + Dispatcher.current = previousDispatcher;
1859 + previousDispatcher = null;
1860 + }
1861 +}
1862 +
1863 +// global collections of Resources
1864 +const preloadPropsMap: Map<string, PreloadProps> = new Map();
1865 +const preconnectsSet: Set<string> = new Set();
1866 +
1867 +export type HoistableRoot = Document | ShadowRoot;
1868 +
1869 +// getRootNode is missing from IE and old jsdom versions
1870 +export function getHoistableRoot(container: Container): HoistableRoot {
1871 + // $FlowFixMe[method-unbinding]
1872 + return typeof container.getRootNode === 'function'
1873 + ? /* $FlowFixMe[incompatible-return] Flow types this as returning a `Node`,
1874 + * but it's either a `Document` or `ShadowRoot`. */
1875 + container.getRootNode()
1876 + : container.ownerDocument;
1877 +}
1878 +
1879 +function getCurrentResourceRoot(): null | HoistableRoot {
1880 + const currentContainer = getCurrentRootHostContainer();
1881 + return currentContainer ? getHoistableRoot(currentContainer) : null;
1882 +}
1883 +
1884 +// Preloads are somewhat special. Even if we don't have the Document
1885 +// used by the root that is rendering a component trying to insert a preload
1886 +// we can still seed the file cache by doing the preload on any document we have
1887 +// access to. We prefer the currentDocument if it exists, we also prefer the
1888 +// lastCurrentDocument if that exists. As a fallback we will use the window.document
1889 +// if available.
1890 +function getDocumentForPreloads(): ?Document {
1891 + const root = getCurrentResourceRoot();
1892 + if (root) {
1893 + return root.ownerDocument || root;
1894 + } else {
1895 + try {
1896 + return lastCurrentDocument || window.document;
1897 + } catch (error) {
1898 + return null;
1899 + }
1900 + }
1901 +}
1902 +
1903 +function getDocumentFromRoot(root: HoistableRoot): Document {
1904 + return root.ownerDocument || root;
1905 +}
1906 +
1907 +// We want this to be the default dispatcher on ReactDOMSharedInternals but we don't want to mutate
1908 +// internals in Module scope. Instead we export it and Internals will import it. There is already a cycle
1909 +// from Internals -> ReactDOM -> HostConfig -> Internals so this doesn't introduce a new one.
1910 +export const ReactDOMClientDispatcher = {
1911 + prefetchDNS,
1912 + preconnect,
1913 + preload,
1914 + preinit,
1915 +};
1916 +
1917 +function preconnectAs(
1918 + rel: 'preconnect' | 'dns-prefetch',
1919 + crossOrigin: null | '' | 'use-credentials',
1920 + href: string,
1921 +) {
1922 + const ownerDocument = getDocumentForPreloads();
1923 + if (typeof href === 'string' && href && ownerDocument) {
1924 + const limitedEscapedHref =
1925 + escapeSelectorAttributeValueInsideDoubleQuotes(href);
1926 + let key = `link[rel="${rel}"][href="${limitedEscapedHref}"]`;
1927 + if (typeof crossOrigin === 'string') {
1928 + key += `[crossorigin="${crossOrigin}"]`;
1929 + }
1930 + if (!preconnectsSet.has(key)) {
1931 + preconnectsSet.add(key);
1932 +
1933 + const preconnectProps = {rel, crossOrigin, href};
1934 + if (null === ownerDocument.querySelector(key)) {
1935 + const instance = ownerDocument.createElement('link');
1936 + setInitialProperties(instance, 'link', preconnectProps);
1937 + markNodeAsHoistable(instance);
1938 + (ownerDocument.head: any).appendChild(instance);
1939 + }
1940 + }
1941 + }
1942 +}
1943 +
1944 +function prefetchDNS(href: string, options?: mixed) {
1945 + if (__DEV__) {
1946 + if (typeof href !== 'string' || !href) {
1947 + console.error(
1948 + 'ReactDOM.prefetchDNS(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.',
1949 + getValueDescriptorExpectingObjectForWarning(href),
1950 + );
1951 + } else if (options != null) {
1952 + if (
1953 + typeof options === 'object' &&
1954 + hasOwnProperty.call(options, 'crossOrigin')
1955 + ) {
1956 + console.error(
1957 + '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`.',
1958 + getValueDescriptorExpectingEnumForWarning(options),
1959 + );
1960 + } else {
1961 + console.error(
1962 + '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`.',
1963 + getValueDescriptorExpectingEnumForWarning(options),
1964 + );
1965 + }
1966 + }
1967 + }
1968 + preconnectAs('dns-prefetch', null, href);
1969 +}
1970 +
1971 +function preconnect(href: string, options?: {crossOrigin?: string}) {
1972 + if (__DEV__) {
1973 + if (typeof href !== 'string' || !href) {
1974 + console.error(
1975 + 'ReactDOM.preconnect(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.',
1976 + getValueDescriptorExpectingObjectForWarning(href),
1977 + );
1978 + } else if (options != null && typeof options !== 'object') {
1979 + console.error(
1980 + '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.',
1981 + getValueDescriptorExpectingEnumForWarning(options),
1982 + );
1983 + } else if (options != null && typeof options.crossOrigin !== 'string') {
1984 + console.error(
1985 + '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.',
1986 + getValueDescriptorExpectingObjectForWarning(options.crossOrigin),
1987 + );
1988 + }
1989 + }
1990 + const crossOrigin =
1991 + options == null || typeof options.crossOrigin !== 'string'
1992 + ? null
1993 + : options.crossOrigin === 'use-credentials'
1994 + ? 'use-credentials'
1995 + : '';
1996 + preconnectAs('preconnect', crossOrigin, href);
1997 +}
1998 +
1999 +type PreloadAs = ResourceType;
2000 +type PreloadOptions = {
2001 + as: PreloadAs,
2002 + crossOrigin?: string,
2003 + integrity?: string,
2004 + type?: string,
2005 +};
2006 +function preload(href: string, options: PreloadOptions) {
2007 + if (__DEV__) {
2008 + validatePreloadArguments(href, options);
2009 + }
2010 + const ownerDocument = getDocumentForPreloads();
2011 + if (
2012 + typeof href === 'string' &&
2013 + href &&
2014 + typeof options === 'object' &&
2015 + options !== null &&
2016 + ownerDocument
2017 + ) {
2018 + const as = options.as;
2019 + const limitedEscapedHref =
2020 + escapeSelectorAttributeValueInsideDoubleQuotes(href);
2021 + const preloadKey = `link[rel="preload"][as="${as}"][href="${limitedEscapedHref}"]`;
2022 + let key = preloadKey;
2023 + switch (as) {
2024 + case 'style':
2025 + key = getStyleKey(href);
2026 + break;
2027 + case 'script':
2028 + key = getScriptKey(href);
2029 + break;
2030 + }
2031 + if (!preloadPropsMap.has(key)) {
2032 + const preloadProps = preloadPropsFromPreloadOptions(href, as, options);
2033 + preloadPropsMap.set(key, preloadProps);
2034 +
2035 + if (null === ownerDocument.querySelector(preloadKey)) {
2036 + const instance = ownerDocument.createElement('link');
2037 + setInitialProperties(instance, 'link', preloadProps);
2038 + markNodeAsHoistable(instance);
2039 + (ownerDocument.head: any).appendChild(instance);
2040 + }
2041 + }
2042 + }
2043 +}
2044 +
2045 +function preloadPropsFromPreloadOptions(
2046 + href: string,
2047 + as: ResourceType,
2048 + options: PreloadOptions,
2049 +): PreloadProps {
2050 + return {
2051 + href,
2052 + rel: 'preload',
2053 + as,
2054 + crossOrigin: as === 'font' ? '' : options.crossOrigin,
2055 + integrity: options.integrity,
2056 + type: options.type,
2057 + };
2058 +}
2059 +
2060 +type PreinitAs = 'style' | 'script';
2061 +type PreinitOptions = {
2062 + as: PreinitAs,
2063 + precedence?: string,
2064 + crossOrigin?: string,
2065 + integrity?: string,
2066 +};
2067 +function preinit(href: string, options: PreinitOptions) {
2068 + if (__DEV__) {
2069 + validatePreinitArguments(href, options);
2070 + }
2071 +
2072 + if (
2073 + typeof href === 'string' &&
2074 + href &&
2075 + typeof options === 'object' &&
2076 + options !== null
2077 + ) {
2078 + const resourceRoot = getCurrentResourceRoot();
2079 + const as = options.as;
2080 + if (!resourceRoot) {
2081 + if (as === 'style' || as === 'script') {
2082 + // We are going to emit a preload as a best effort fallback since this preinit
2083 + // was called outside of a render. Given the passive nature of this fallback
2084 + // we do not warn in dev when props disagree if there happens to already be a
2085 + // matching preload with this href
2086 + const preloadDocument = getDocumentForPreloads();
2087 + if (preloadDocument) {
2088 + const limitedEscapedHref =
2089 + escapeSelectorAttributeValueInsideDoubleQuotes(href);
2090 + const preloadKey = `link[rel="preload"][as="${as}"][href="${limitedEscapedHref}"]`;
2091 + let key = preloadKey;
2092 + switch (as) {
2093 + case 'style':
2094 + key = getStyleKey(href);
2095 + break;
2096 + case 'script':
2097 + key = getScriptKey(href);
2098 + break;
2099 + }
2100 + if (!preloadPropsMap.has(key)) {
2101 + const preloadProps = preloadPropsFromPreinitOptions(
2102 + href,
2103 + as,
2104 + options,
2105 + );
2106 + preloadPropsMap.set(key, preloadProps);
2107 +
2108 + if (null === preloadDocument.querySelector(preloadKey)) {
2109 + const instance = preloadDocument.createElement('link');
2110 + setInitialProperties(instance, 'link', preloadProps);
2111 + markNodeAsHoistable(instance);
2112 + (preloadDocument.head: any).appendChild(instance);
2113 + }
2114 + }
2115 + }
2116 + }
2117 + return;
2118 + }
2119 +
2120 + switch (as) {
2121 + case 'style': {
2122 + const styles = getResourcesFromRoot(resourceRoot).hoistableStyles;
2123 +
2124 + const key = getStyleKey(href);
2125 + const precedence = options.precedence || 'default';
2126 +
2127 + // Check if this resource already exists
2128 + let resource = styles.get(key);
2129 + if (resource) {
2130 + // We can early return. The resource exists and there is nothing
2131 + // more to do
2132 + return;
2133 + }
2134 +
2135 + const state = {
2136 + loading: NotLoaded,
2137 + preload: null,
2138 + };
2139 +
2140 + // Attempt to hydrate instance from DOM
2141 + let instance: null | Instance = resourceRoot.querySelector(
2142 + getStylesheetSelectorFromKey(key),
2143 + );
2144 + if (instance) {
2145 + state.loading = Loaded;
2146 + } else {
2147 + // Construct a new instance and insert it
2148 + const stylesheetProps = stylesheetPropsFromPreinitOptions(
2149 + href,
2150 + precedence,
2151 + options,
2152 + );
2153 + const preloadProps = preloadPropsMap.get(key);
2154 + if (preloadProps) {
2155 + adoptPreloadPropsForStylesheet(stylesheetProps, preloadProps);
2156 + }
2157 + const ownerDocument = getDocumentFromRoot(resourceRoot);
2158 + const link = (instance = ownerDocument.createElement('link'));
2159 + markNodeAsHoistable(link);
2160 + setInitialProperties(link, 'link', stylesheetProps);
2161 +
2162 + (link: any)._p = new Promise((resolve, reject) => {
2163 + link.onload = resolve;
2164 + link.onerror = reject;
2165 + });
2166 + link.addEventListener('load', () => {
2167 + state.loading |= Loaded;
2168 + });
2169 + link.addEventListener('error', () => {
2170 + state.loading |= Errored;
2171 + });
2172 +
2173 + insertStylesheet(instance, precedence, resourceRoot);
2174 + }
2175 +
2176 + // Construct a Resource and cache it
2177 + resource = {
2178 + type: 'stylesheet',
2179 + instance,
2180 + count: 1,
2181 + state,
2182 + };
2183 + styles.set(key, resource);
2184 + return;
2185 + }
2186 + case 'script': {
2187 + const src = href;
2188 + const scripts = getResourcesFromRoot(resourceRoot).hoistableScripts;
2189 +
2190 + const key = getScriptKey(src);
2191 +
2192 + // Check if this resource already exists
2193 + let resource = scripts.get(key);
2194 + if (resource) {
2195 + // We can early return. The resource exists and there is nothing
2196 + // more to do
2197 + return;
2198 + }
2199 +
2200 + // Attempt to hydrate instance from DOM
2201 + let instance: null | Instance = resourceRoot.querySelector(
2202 + getScriptSelectorFromKey(key),
2203 + );
2204 + if (!instance) {
2205 + // Construct a new instance and insert it
2206 + const scriptProps = scriptPropsFromPreinitOptions(src, options);
2207 + // Adopt certain preload props
2208 + const preloadProps = preloadPropsMap.get(key);
2209 + if (preloadProps) {
2210 + adoptPreloadPropsForScript(scriptProps, preloadProps);
2211 + }
2212 + const ownerDocument = getDocumentFromRoot(resourceRoot);
2213 + instance = ownerDocument.createElement('script');
2214 + markNodeAsHoistable(instance);
2215 + setInitialProperties(instance, 'link', scriptProps);
2216 + (ownerDocument.head: any).appendChild(instance);
2217 + }
2218 +
2219 + // Construct a Resource and cache it
2220 + resource = {
2221 + type: 'script',
2222 + instance,
2223 + count: 1,
2224 + state: null,
2225 + };
2226 + scripts.set(key, resource);
2227 + return;
2228 + }
2229 + }
2230 + }
2231 +}
2232 +
2233 +function preloadPropsFromPreinitOptions(
2234 + href: string,
2235 + as: ResourceType,
2236 + options: PreinitOptions,
2237 +): PreloadProps {
2238 + return {
2239 + href,
2240 + rel: 'preload',
2241 + as,
2242 + crossOrigin: as === 'font' ? '' : options.crossOrigin,
2243 + integrity: options.integrity,
2244 + };
2245 +}
2246 +
2247 +function stylesheetPropsFromPreinitOptions(
2248 + href: string,
2249 + precedence: string,
2250 + options: PreinitOptions,
2251 +): StylesheetProps {
2252 + return {
2253 + rel: 'stylesheet',
2254 + href,
2255 + 'data-precedence': precedence,
2256 + crossOrigin: options.crossOrigin,
2257 + };
2258 +}
2259 +
2260 +function scriptPropsFromPreinitOptions(
2261 + src: string,
2262 + options: PreinitOptions,
2263 +): ScriptProps {
2264 + return {
2265 + src,
2266 + async: true,
2267 + crossOrigin: options.crossOrigin,
2268 + integrity: options.integrity,
2269 + };
2270 +}
2271 +
2272 +type StyleTagQualifyingProps = {
2273 + href: string,
2274 + precedence: string,
2275 + [string]: mixed,
2276 +};
2277 +
2278 +type StylesheetQualifyingProps = {
2279 + rel: 'stylesheet',
2280 + href: string,
2281 + precedence: string,
2282 + [string]: mixed,
2283 +};
2284 +
2285 +// This function is called in begin work and we should always have a currentDocument set
2286 +export function getResource(
2287 + type: string,
2288 + currentProps: any,
2289 + pendingProps: any,
2290 +): null | Resource {
2291 + const resourceRoot = getCurrentResourceRoot();
2292 + if (!resourceRoot) {
2293 + throw new Error(
2294 + '"resourceRoot" was expected to exist. This is a bug in React.',
2295 + );
2296 + }
2297 + switch (type) {
2298 + case 'meta':
2299 + case 'title': {
2300 + return null;
2301 + }
2302 + case 'style': {
2303 + if (
2304 + typeof pendingProps.precedence === 'string' &&
2305 + typeof pendingProps.href === 'string'
2306 + ) {
2307 + const key = getStyleKey(pendingProps.href);
2308 + const styles = getResourcesFromRoot(resourceRoot).hoistableStyles;
2309 + let resource = styles.get(key);
2310 + if (!resource) {
2311 + resource = {
2312 + type: 'style',
2313 + instance: null,
2314 + count: 0,
2315 + state: null,
2316 + };
2317 + styles.set(key, resource);
2318 + }
2319 + return resource;
2320 + }
2321 + return {
2322 + type: 'void',
2323 + instance: null,
2324 + count: 0,
2325 + state: null,
2326 + };
2327 + }
2328 + case 'link': {
2329 + if (
2330 + pendingProps.rel === 'stylesheet' &&
2331 + typeof pendingProps.href === 'string' &&
2332 + typeof pendingProps.precedence === 'string'
2333 + ) {
2334 + const qualifiedProps: StylesheetQualifyingProps = pendingProps;
2335 + const key = getStyleKey(qualifiedProps.href);
2336 +
2337 + const styles = getResourcesFromRoot(resourceRoot).hoistableStyles;
2338 +
2339 + let resource = styles.get(key);
2340 + if (!resource) {
2341 + // We asserted this above but Flow can't figure out that the type satisfies
2342 + const ownerDocument = getDocumentFromRoot(resourceRoot);
2343 + resource = {
2344 + type: 'stylesheet',
2345 + instance: null,
2346 + count: 0,
2347 + state: {
2348 + loading: NotLoaded,
2349 + preload: null,
2350 + },
2351 + };
2352 + styles.set(key, resource);
2353 + if (!preloadPropsMap.has(key)) {
2354 + preloadStylesheet(
2355 + ownerDocument,
2356 + key,
2357 + preloadPropsFromStylesheet(qualifiedProps),
2358 + resource.state,
2359 + );
2360 + }
2361 + }
2362 + return resource;
2363 + }
2364 + return null;
2365 + }
2366 + case 'script': {
2367 + if (typeof pendingProps.src === 'string' && pendingProps.async === true) {
2368 + const scriptProps: ScriptProps = pendingProps;
2369 + const key = getScriptKey(scriptProps.src);
2370 + const scripts = getResourcesFromRoot(resourceRoot).hoistableScripts;
2371 +
2372 + let resource = scripts.get(key);
2373 + if (!resource) {
2374 + resource = {
2375 + type: 'script',
2376 + instance: null,
2377 + count: 0,
2378 + state: null,
2379 + };
2380 + scripts.set(key, resource);
2381 + }
2382 + return resource;
2383 + }
2384 + return {
2385 + type: 'void',
2386 + instance: null,
2387 + count: 0,
2388 + state: null,
2389 + };
2390 + }
2391 + default: {
2392 + throw new Error(
2393 + `getResource encountered a type it did not expect: "${type}". this is a bug in React.`,
2394 + );
2395 + }
2396 + }
2397 +}
2398 +
2399 +function styleTagPropsFromRawProps(
2400 + rawProps: StyleTagQualifyingProps,
2401 +): StyleTagProps {
2402 + return {
2403 + ...rawProps,
2404 + 'data-href': rawProps.href,
2405 + 'data-precedence': rawProps.precedence,
2406 + href: null,
2407 + precedence: null,
2408 + };
2409 +}
2410 +
2411 +function getStyleKey(href: string) {
2412 + const limitedEscapedHref =
2413 + escapeSelectorAttributeValueInsideDoubleQuotes(href);
2414 + return `href~="${limitedEscapedHref}"`;
2415 +}
2416 +
2417 +function getStyleTagSelectorFromKey(key: string) {
2418 + return `style[data-${key}]`;
2419 +}
2420 +
2421 +function getStylesheetSelectorFromKey(key: string) {
2422 + return `link[rel="stylesheet"][${key}]`;
2423 +}
2424 +
2425 +function getPreloadStylesheetSelectorFromKey(key: string) {
2426 + return `link[rel="preload"][as="style"][${key}]`;
2427 +}
2428 +
2429 +function stylesheetPropsFromRawProps(
2430 + rawProps: StylesheetQualifyingProps,
2431 +): StylesheetProps {
2432 + return {
2433 + ...rawProps,
2434 + 'data-precedence': rawProps.precedence,
2435 + precedence: null,
2436 + };
2437 +}
2438 +function preloadStylesheet(
2439 + ownerDocument: Document,
2440 + key: string,
2441 + preloadProps: PreloadProps,
2442 + state: StylesheetState,
2443 +) {
2444 + preloadPropsMap.set(key, preloadProps);
2445 +
2446 + if (!ownerDocument.querySelector(getStylesheetSelectorFromKey(key))) {
2447 + // There is no matching stylesheet instance in the Document.
2448 + // We will insert a preload now to kick off loading because
2449 + // we expect this stylesheet to commit
2450 + const preloadEl = ownerDocument.querySelector(
2451 + getPreloadStylesheetSelectorFromKey(key),
2452 + );
2453 + if (preloadEl) {
2454 + // If we find a preload already it was SSR'd and we won't have an actual
2455 + // loading state to track. For now we will just assume it is loaded
2456 + state.loading = Loaded;
2457 + } else {
2458 + const instance = ownerDocument.createElement('link');
2459 + state.preload = instance;
2460 + instance.addEventListener('load', () => (state.loading |= Loaded));
2461 + instance.addEventListener('error', () => (state.loading |= Errored));
2462 + setInitialProperties(instance, 'link', preloadProps);
2463 + markNodeAsHoistable(instance);
2464 + (ownerDocument.head: any).appendChild(instance);
2465 + }
2466 + }
2467 +}
2468 +
2469 +function preloadPropsFromStylesheet(
2470 + props: StylesheetQualifyingProps,
2471 +): PreloadProps {
2472 + return {
2473 + rel: 'preload',
2474 + as: 'style',
2475 + href: props.href,
2476 + crossOrigin: props.crossOrigin,
2477 + integrity: props.integrity,
2478 + media: props.media,
2479 + hrefLang: props.hrefLang,
2480 + referrerPolicy: props.referrerPolicy,
2481 + };
2482 +}
2483 +
2484 +function getScriptKey(src: string): string {
2485 + const limitedEscapedSrc = escapeSelectorAttributeValueInsideDoubleQuotes(src);
2486 + return `[src="${limitedEscapedSrc}"]`;
2487 +}
2488 +
2489 +function getScriptSelectorFromKey(key: string): string {
2490 + return 'script[async]' + key;
2491 +}
2492 +
2493 +export function acquireResource(
2494 + hoistableRoot: HoistableRoot,
2495 + resource: Resource,
2496 + props: any,
2497 +): null | Instance {
2498 + resource.count++;
2499 + if (resource.instance === null) {
2500 + switch (resource.type) {
2501 + case 'style': {
2502 + const qualifiedProps: StyleTagQualifyingProps = props;
2503 + const key = getStyleKey(qualifiedProps.href);
2504 +
2505 + // Attempt to hydrate instance from DOM
2506 + let instance: null | Instance = hoistableRoot.querySelector(
2507 + getStyleTagSelectorFromKey(key),
2508 + );
2509 + if (instance) {
2510 + resource.instance = instance;
2511 + markNodeAsHoistable(instance);
2512 + return instance;
2513 + }
2514 +
2515 + const styleProps = styleTagPropsFromRawProps(props);
2516 + const ownerDocument = getDocumentFromRoot(hoistableRoot);
2517 + instance = ownerDocument.createElement('style');
2518 +
2519 + markNodeAsHoistable(instance);
2520 + setInitialProperties(instance, 'style', styleProps);
2521 + insertStylesheet(instance, qualifiedProps.precedence, hoistableRoot);
2522 + resource.instance = instance;
2523 +
2524 + return instance;
2525 + }
2526 + case 'stylesheet': {
2527 + // This typing is enforce by `getResource`. If we change the logic
2528 + // there for what qualifies as a stylesheet resource we need to ensure
2529 + // this cast still makes sense;
2530 + const qualifiedProps: StylesheetQualifyingProps = props;
2531 + const key = getStyleKey(qualifiedProps.href);
2532 +
2533 + // Attempt to hydrate instance from DOM
2534 + let instance: null | Instance = hoistableRoot.querySelector(
2535 + getStylesheetSelectorFromKey(key),
2536 + );
2537 + if (instance) {
2538 + resource.instance = instance;
2539 + markNodeAsHoistable(instance);
2540 + return instance;
2541 + }
2542 +
2543 + const stylesheetProps = stylesheetPropsFromRawProps(props);
2544 + const preloadProps = preloadPropsMap.get(key);
2545 + if (preloadProps) {
2546 + adoptPreloadPropsForStylesheet(stylesheetProps, preloadProps);
2547 + }
2548 +
2549 + // Construct and insert a new instance
2550 + const ownerDocument = getDocumentFromRoot(hoistableRoot);
2551 + instance = ownerDocument.createElement('link');
2552 + markNodeAsHoistable(instance);
2553 + const linkInstance: HTMLLinkElement = (instance: any);
2554 + (linkInstance: any)._p = new Promise((resolve, reject) => {
2555 + linkInstance.onload = resolve;
2556 + linkInstance.onerror = reject;
2557 + });
2558 + setInitialProperties(instance, 'link', stylesheetProps);
2559 + insertStylesheet(instance, qualifiedProps.precedence, hoistableRoot);
2560 + resource.instance = instance;
2561 +
2562 + return instance;
2563 + }
2564 + case 'script': {
2565 + // This typing is enforce by `getResource`. If we change the logic
2566 + // there for what qualifies as a stylesheet resource we need to ensure
2567 + // this cast still makes sense;
2568 + const borrowedScriptProps: ScriptProps = props;
2569 + const key = getScriptKey(borrowedScriptProps.src);
2570 +
2571 + // Attempt to hydrate instance from DOM
2572 + let instance: null | Instance = hoistableRoot.querySelector(
2573 + getScriptSelectorFromKey(key),
2574 + );
2575 + if (instance) {
2576 + resource.instance = instance;
2577 + markNodeAsHoistable(instance);
2578 + return instance;
2579 + }
2580 +
2581 + let scriptProps = borrowedScriptProps;
2582 + const preloadProps = preloadPropsMap.get(key);
2583 + if (preloadProps) {
2584 + scriptProps = {...borrowedScriptProps};
2585 + adoptPreloadPropsForScript(scriptProps, preloadProps);
2586 + }
2587 +
2588 + // Construct and insert a new instance
2589 + const ownerDocument = getDocumentFromRoot(hoistableRoot);
2590 + instance = ownerDocument.createElement('script');
2591 + markNodeAsHoistable(instance);
2592 + setInitialProperties(instance, 'link', scriptProps);
2593 + (ownerDocument.head: any).appendChild(instance);
2594 + resource.instance = instance;
2595 +
2596 + return instance;
2597 + }
2598 + case 'void': {
2599 + return null;
2600 + }
2601 + default: {
2602 + throw new Error(
2603 + `acquireResource encountered a resource type it did not expect: "${resource.type}". this is a bug in React.`,
2604 + );
2605 + }
2606 + }
2607 + }
2608 + return resource.instance;
2609 +}
2610 +
2611 +export function releaseResource(resource: Resource): void {
2612 + resource.count--;
2613 +}
2614 +
2615 +function insertStylesheet(
2616 + instance: HTMLElement,
2617 + precedence: string,
2618 + root: HoistableRoot,
2619 +): void {
2620 + const nodes = root.querySelectorAll(
2621 + 'link[rel="stylesheet"][data-precedence],style[data-precedence]',
2622 + );
2623 + const last = nodes.length ? nodes[nodes.length - 1] : null;
2624 + let prior = last;
2625 + for (let i = 0; i < nodes.length; i++) {
2626 + const node = nodes[i];
2627 + const nodePrecedence = node.dataset.precedence;
2628 + if (nodePrecedence === precedence) {
2629 + prior = node;
2630 + } else if (prior !== last) {
2631 + break;
2632 + }
2633 + }
2634 +
2635 + if (prior) {
2636 + // We get the prior from the document so we know it is in the tree.
2637 + // We also know that links can't be the topmost Node so the parentNode
2638 + // must exist.
2639 + ((prior.parentNode: any): Node).insertBefore(instance, prior.nextSibling);
2640 + } else {
2641 + const parent =
2642 + root.nodeType === DOCUMENT_NODE
2643 + ? ((((root: any): Document).head: any): Element)
2644 + : ((root: any): ShadowRoot);
2645 + parent.insertBefore(instance, parent.firstChild);
2646 + }
2647 +}
2648 +
2649 +function adoptPreloadPropsForStylesheet(
2650 + stylesheetProps: StylesheetProps,
2651 + preloadProps: PreloadProps,
2652 +): void {
2653 + if (stylesheetProps.crossOrigin == null)
2654 + stylesheetProps.crossOrigin = preloadProps.crossOrigin;
2655 + if (stylesheetProps.referrerPolicy == null)
2656 + stylesheetProps.referrerPolicy = preloadProps.referrerPolicy;
2657 + if (stylesheetProps.title == null) stylesheetProps.title = preloadProps.title;
2658 +}
2659 +
2660 +function adoptPreloadPropsForScript(
2661 + scriptProps: ScriptProps,
2662 + preloadProps: PreloadProps,
2663 +): void {
2664 + if (scriptProps.crossOrigin == null)
2665 + scriptProps.crossOrigin = preloadProps.crossOrigin;
2666 + if (scriptProps.referrerPolicy == null)
2667 + scriptProps.referrerPolicy = preloadProps.referrerPolicy;
2668 + if (scriptProps.integrity == null)
2669 + scriptProps.referrerPolicy = preloadProps.integrity;
2670 +}
2671 +
2672 +type KeyedTagCache = Map<string, Array<Element>>;
2673 +type DocumentTagCaches = Map<Document, KeyedTagCache>;
2674 +let tagCaches: null | DocumentTagCaches = null;
2675 +
2676 +export function hydrateHoistable(
2677 + hoistableRoot: HoistableRoot,
2678 + type: HoistableTagType,
2679 + props: any,
2680 + internalInstanceHandle: Object,
2681 +): Instance {
2682 + const ownerDocument = getDocumentFromRoot(hoistableRoot);
2683 +
2684 + let instance: ?Instance = null;
2685 + getInstance: switch (type) {
2686 + case 'title': {
2687 + instance = ownerDocument.getElementsByTagName('title')[0];
2688 + if (
2689 + !instance ||
2690 + isOwnedInstance(instance) ||
2691 + instance.namespaceURI === SVG_NAMESPACE ||
2692 + instance.hasAttribute('itemprop')
2693 + ) {
2694 + instance = ownerDocument.createElement(type);
2695 + (ownerDocument.head: any).insertBefore(
2696 + instance,
2697 + ownerDocument.querySelector('head > title'),
2698 + );
2699 + }
2700 + setInitialProperties(instance, type, props);
2701 + precacheFiberNode(internalInstanceHandle, instance);
2702 + markNodeAsHoistable(instance);
2703 + return instance;
2704 + }
2705 + case 'link': {
2706 + const cache = getHydratableHoistableCache('link', 'href', ownerDocument);
2707 + const key = type + (props.href || '');
2708 + const maybeNodes = cache.get(key);
2709 + if (maybeNodes) {
2710 + const nodes = maybeNodes;
2711 + for (let i = 0; i < nodes.length; i++) {
2712 + const node = nodes[i];
2713 + if (
2714 + node.getAttribute('href') !==
2715 + (props.href == null ? null : props.href) ||
2716 + node.getAttribute('rel') !==
2717 + (props.rel == null ? null : props.rel) ||
2718 + node.getAttribute('title') !==
2719 + (props.title == null ? null : props.title) ||
2720 + node.getAttribute('crossorigin') !==
2721 + (props.crossOrigin == null ? null : props.crossOrigin)
2722 + ) {
2723 + // mismatch, try the next node;
2724 + continue;
2725 + }
2726 + instance = node;
2727 + nodes.splice(i, 1);
2728 + break getInstance;
2729 + }
2730 + }
2731 + instance = ownerDocument.createElement(type);
2732 + setInitialProperties(instance, type, props);
2733 + (ownerDocument.head: any).appendChild(instance);
2734 + break;
2735 + }
2736 + case 'meta': {
2737 + const cache = getHydratableHoistableCache(
2738 + 'meta',
2739 + 'content',
2740 + ownerDocument,
2741 + );
2742 + const key = type + (props.content || '');
2743 + const maybeNodes = cache.get(key);
2744 + if (maybeNodes) {
2745 + const nodes = maybeNodes;
2746 + for (let i = 0; i < nodes.length; i++) {
2747 + const node = nodes[i];
2748 +
2749 + // We coerce content to string because it is the most likely one to
2750 + // use a `toString` capable value. For the rest we just do identity match
2751 + // passing non-strings here is not really valid anyway.
2752 + if (__DEV__) {
2753 + checkAttributeStringCoercion(props.content, 'content');
2754 + }
2755 + if (
2756 + node.getAttribute('content') !==
2757 + (props.content == null ? null : '' + props.content) ||
2758 + node.getAttribute('name') !==
2759 + (props.name == null ? null : props.name) ||
2760 + node.getAttribute('property') !==
2761 + (props.property == null ? null : props.property) ||
2762 + node.getAttribute('http-equiv') !==
2763 + (props.httpEquiv == null ? null : props.httpEquiv) ||
2764 + node.getAttribute('charset') !==
2765 + (props.charSet == null ? null : props.charSet)
2766 + ) {
2767 + // mismatch, try the next node;
2768 + continue;
2769 + }
2770 + instance = node;
2771 + nodes.splice(i, 1);
2772 + break getInstance;
2773 + }
2774 + }
2775 + instance = ownerDocument.createElement(type);
2776 + setInitialProperties(instance, type, props);
2777 + (ownerDocument.head: any).appendChild(instance);
2778 + break;
2779 + }
2780 + default:
2781 + throw new Error(
2782 + `getNodesForType encountered a type it did not expect: "${type}". This is a bug in React.`,
2783 + );
2784 + }
2785 +
2786 + // This node is a match
2787 + precacheFiberNode(internalInstanceHandle, instance);
2788 + markNodeAsHoistable(instance);
2789 + return instance;
2790 +}
2791 +
2792 +function getHydratableHoistableCache(
2793 + type: HoistableTagType,
2794 + keyAttribute: string,
2795 + ownerDocument: Document,
2796 +): KeyedTagCache {
2797 + let cache: KeyedTagCache;
2798 + let caches: DocumentTagCaches;
2799 + if (tagCaches === null) {
2800 + cache = new Map();
2801 + caches = tagCaches = new Map();
2802 + caches.set(ownerDocument, cache);
2803 + } else {
2804 + caches = tagCaches;
2805 + const maybeCache = caches.get(ownerDocument);
2806 + if (!maybeCache) {
2807 + cache = new Map();
2808 + caches.set(ownerDocument, cache);
2809 + } else {
2810 + cache = maybeCache;
2811 + }
2812 + }
2813 +
2814 + if (cache.has(type)) {
2815 + // We use type as a special key that signals that this cache has been seeded for this type
2816 + return cache;
2817 + }
2818 +
2819 + // Mark this cache as seeded for this type
2820 + cache.set(type, (null: any));
2821 +
2822 + const nodes = ownerDocument.getElementsByTagName(type);
2823 + for (let i = 0; i < nodes.length; i++) {
2824 + const node = nodes[i];
2825 + if (
2826 + !isOwnedInstance(node) &&
2827 + (type !== 'link' || node.getAttribute('rel') !== 'stylesheet') &&
2828 + node.namespaceURI !== SVG_NAMESPACE
2829 + ) {
2830 + const nodeKey = node.getAttribute(keyAttribute) || '';
2831 + const key = type + nodeKey;
2832 + const existing = cache.get(key);
2833 + if (existing) {
2834 + existing.push(node);
2835 + } else {
2836 + cache.set(key, [node]);
2837 + }
2838 + }
2839 + }
2840 +
2841 + return cache;
2842 +}
2843 +
2844 +export function mountHoistable(
2845 + hoistableRoot: HoistableRoot,
2846 + type: HoistableTagType,
2847 + instance: Instance,
2848 +): void {
2849 + const ownerDocument = getDocumentFromRoot(hoistableRoot);
2850 + (ownerDocument.head: any).insertBefore(
2851 + instance,
2852 + type === 'title' ? ownerDocument.querySelector('head > title') : null,
2853 + );
2854 +}
2855 +
2856 +export function unmountHoistable(instance: Instance): void {
2857 + (instance.parentNode: any).removeChild(instance);
2858 +}
2859 +
2860 +// When passing user input into querySelector(All) the embedded string must not alter
2861 +// the semantics of the query. This escape function is safe to use when we know the
2862 +// provided value is going to be wrapped in double quotes as part of an attribute selector
2863 +// Do not use it anywhere else
2864 +// we escape double quotes and backslashes
2865 +const escapeSelectorAttributeValueInsideDoubleQuotesRegex = /[\n\"\\]/g;
2866 +function escapeSelectorAttributeValueInsideDoubleQuotes(value: string): string {
2867 + return value.replace(
2868 + escapeSelectorAttributeValueInsideDoubleQuotesRegex,
2869 + ch => '\\' + ch.charCodeAt(0).toString(16),
2870 + );
2871 +}
2872 +
2873 +export function isHostHoistableType(
2874 + type: string,
2875 + props: RawProps,
2876 + hostContext: HostContext,
2877 +): boolean {
2878 + let outsideHostContainerContext: boolean;
2879 + let namespace: HostContextProd;
2880 + if (__DEV__) {
2881 + const hostContextDev: HostContextDev = (hostContext: any);
2882 + // We can only render resources when we are not within the host container context
2883 + outsideHostContainerContext =
2884 + !hostContextDev.ancestorInfo.containerTagInScope;
2885 + namespace = hostContextDev.namespace;
2886 + } else {
2887 + const hostContextProd: HostContextProd = (hostContext: any);
2888 + namespace = hostContextProd;
2889 + }
2890 +
2891 + // Global opt out of hoisting for anything in SVG Namespace or anything with an itemProp inside an itemScope
2892 + if (namespace === SVG_NAMESPACE || props.itemProp != null) {
2893 + if (__DEV__) {
2894 + if (
2895 + outsideHostContainerContext &&
2896 + props.itemProp != null &&
2897 + (type === 'meta' ||
2898 + type === 'title' ||
2899 + type === 'style' ||
2900 + type === 'link' ||
2901 + type === 'script')
2902 + ) {
2903 + console.error(
2904 + 'Cannot render a <%s> outside the main document if it has an `itemProp` prop. `itemProp` suggests the tag belongs to an' +
2905 + ' `itemScope` which can appear anywhere in the DOM. If you were intending for React to hoist this <%s> remove the `itemProp` prop.' +
2906 + ' Otherwise, try moving this tag into the <head> or <body> of the Document.',
2907 + type,
2908 + type,
2909 + );
2910 + }
2911 + }
2912 + return false;
2913 + }
2914 +
2915 + switch (type) {
2916 + case 'meta':
2917 + case 'title': {
2918 + return true;
2919 + }
2920 + case 'style': {
2921 + if (
2922 + typeof props.precedence !== 'string' ||
2923 + typeof props.href !== 'string' ||
2924 + props.href === ''
2925 + ) {
2926 + if (__DEV__) {
2927 + if (outsideHostContainerContext) {
2928 + console.error(
2929 + 'Cannot render a <style> outside the main document without knowing its precedence and a unique href key.' +
2930 + ' React can hoist and deduplicate <style> tags if you provide a `precedence` prop along with an `href` prop that' +
2931 + ' does not conflic with the `href` values used in any other hoisted <style> or <link rel="stylesheet" ...> tags. ' +
2932 + ' Note that hoisting <style> tags is considered an advanced feature that most will not use directly.' +
2933 + ' Consider moving the <style> tag to the <head> or consider adding a `precedence="default"` and `href="some unique resource identifier"`, or move the <style>' +
2934 + ' to the <style> tag.',
2935 + );
2936 + }
2937 + }
2938 + return false;
2939 + }
2940 + return true;
2941 + }
2942 + case 'link': {
2943 + if (
2944 + typeof props.rel !== 'string' ||
2945 + typeof props.href !== 'string' ||
2946 + props.href === '' ||
2947 + props.onLoad ||
2948 + props.onError
2949 + ) {
2950 + if (__DEV__) {
2951 + if (
2952 + props.rel === 'stylesheet' &&
2953 + typeof props.precedence === 'string'
2954 + ) {
2955 + validateLinkPropsForStyleResource(props);
2956 + }
2957 + if (outsideHostContainerContext) {
2958 + if (
2959 + typeof props.rel !== 'string' ||
2960 + typeof props.href !== 'string' ||
2961 + props.href === ''
2962 + ) {
2963 + console.error(
2964 + 'Cannot render a <link> outside the main document without a `rel` and `href` prop.' +
2965 + ' Try adding a `rel` and/or `href` prop to this <link> or moving the link into the <head> tag',
2966 + );
2967 + } else if (props.onError || props.onLoad) {
2968 + console.error(
2969 + 'Cannot render a <link> with onLoad or onError listeners outside the main document.' +
2970 + ' Try removing onLoad={...} and onError={...} or moving it into the root <head> tag or' +
2971 + ' somewhere in the <body>.',
2972 + );
2973 + }
2974 + }
2975 + }
2976 + return false;
2977 + }
2978 + switch (props.rel) {
2979 + case 'stylesheet': {
2980 + const {precedence, disabled} = props;
2981 + if (__DEV__) {
2982 + if (typeof precedence !== 'string') {
2983 + if (outsideHostContainerContext) {
2984 + console.error(
2985 + 'Cannot render a <link rel="stylesheet" /> outside the main document without knowing its precedence.' +
2986 + ' Consider adding precedence="default" or moving it into the root <head> tag.',
2987 + );
2988 + }
2989 + }
2990 + }
2991 + return typeof precedence === 'string' && disabled == null;
2992 + }
2993 + default: {
2994 + return true;
2995 + }
2996 + }
2997 + }
2998 + case 'script': {
2999 + if (
3000 + props.async !== true ||
3001 + props.onLoad ||
3002 + props.onError ||
3003 + typeof props.src !== 'string' ||
3004 + !props.src
3005 + ) {
3006 + if (__DEV__) {
3007 + if (outsideHostContainerContext) {
3008 + if (props.async !== true) {
3009 + console.error(
3010 + 'Cannot render a sync or defer <script> outside the main document without knowing its order.' +
3011 + ' Try adding async="" or moving it into the root <head> tag.',
3012 + );
3013 + } else if (props.onLoad || props.onError) {
3014 + console.error(
3015 + 'Cannot render a <script> with onLoad or onError listeners outside the main document.' +
3016 + ' Try removing onLoad={...} and onError={...} or moving it into the root <head> tag or' +
3017 + ' somewhere in the <body>.',
3018 + );
3019 + } else {
3020 + console.error(
3021 + 'Cannot render a <script> outside the main document without `async={true}` and a non-empty `src` prop.' +
3022 + ' Ensure there is a valid `src` and either make the script async or move it into the root <head> tag or' +
3023 + ' somewhere in the <body>.',
3024 + );
3025 + }
3026 + }
3027 + }
3028 + return false;
3029 + }
3030 + return true;
3031 + }
3032 + case 'noscript':
3033 + case 'template': {
3034 + if (__DEV__) {
3035 + if (outsideHostContainerContext) {
3036 + console.error(
3037 + 'Cannot render <%s> outside the main document. Try moving it into the root <head> tag.',
3038 + type,
3039 + );
3040 + }
3041 + }
3042 + return false;
3043 + }
3044 + }
3045 + return false;
3046 +}
3047 +
3048 +export function maySuspendCommit(type: Type, props: Props): boolean {
3049 + return false;
3050 +}
3051 +
3052 +export function mayResourceSuspendCommit(resource: Resource): boolean {
3053 + return (
3054 + resource.type === 'stylesheet' &&
3055 + (resource.state.loading & Inserted) === NotLoaded
3056 + );
3057 +}
3058 +
3059 +export function preloadInstance(type: Type, props: Props): boolean {
3060 + // Return true to indicate it's already loaded
3061 + return true;
3062 +}
3063 +
3064 +export function preloadResource(resource: Resource): boolean {
3065 + if (
3066 + resource.type === 'stylesheet' &&
3067 + (resource.state.loading & Settled) === NotLoaded
3068 + ) {
3069 + // we have not finished loading the underlying stylesheet yet.
3070 + return false;
3071 + }
3072 + // Return true to indicate it's already loaded
3073 + return true;
3074 +}
3075 +
3076 +type SuspendedState = {
3077 + stylesheets: null | Map<StylesheetResource, HoistableRoot>,
3078 + count: number,
3079 + unsuspend: null | (() => void),
3080 +};
3081 +let suspendedState: null | SuspendedState = null;
3082 +
3083 +// We use a noop function when we begin suspending because if possible we want the
3084 +// waitfor step to finish synchronously. If it doesn't we'll return a function to
3085 +// provide the actual unsuspend function and that will get completed when the count
3086 +// hits zero or it will get cancelled if the root starts new work.
3087 +function noop() {}
3088 +
3089 +export function startSuspendingCommit(): void {
3090 + suspendedState = {
3091 + stylesheets: null,
3092 + count: 0,
3093 + unsuspend: noop,
3094 + };
3095 +}
3096 +
3097 +export function suspendInstance(type: Type, props: Props): void {
3098 + return;
3099 +}
3100 +
3101 +export function suspendResource(
3102 + hoistableRoot: HoistableRoot,
3103 + resource: Resource,
3104 + props: any,
3105 +): void {
3106 + if (suspendedState === null) {
3107 + throw new Error(
3108 + 'Internal React Error: suspendedState null when it was expected to exists. Please report this as a React bug.',
3109 + );
3110 + }
3111 + const state = suspendedState;
3112 + if (resource.type === 'stylesheet') {
3113 + if (typeof props.media === 'string') {
3114 + // If we don't currently match media we avoid suspending on this resource
3115 + // and let it insert on the mutation path
3116 + if (matchMedia(props.media).matches === false) {
3117 + return;
3118 + }
3119 + }
3120 + if (resource.instance === null) {
3121 + const qualifiedProps: StylesheetQualifyingProps = props;
3122 + const key = getStyleKey(qualifiedProps.href);
3123 +
3124 + // Attempt to hydrate instance from DOM
3125 + let instance: null | Instance = hoistableRoot.querySelector(
3126 + getStylesheetSelectorFromKey(key),
3127 + );
3128 + if (instance) {
3129 + // If this instance has a loading state it came from the Fizz runtime.
3130 + // If there is not loading state it is assumed to have been server rendered
3131 + // as part of the preamble and therefore synchronously loaded. It could have
3132 + // errored however which we still do not yet have a means to detect. For now
3133 + // we assume it is loaded.
3134 + const maybeLoadingState: ?Promise<mixed> = (instance: any)._p;
3135 + if (
3136 + maybeLoadingState !== null &&
3137 + typeof maybeLoadingState === 'object' &&
3138 + // $FlowFixMe[method-unbinding]
3139 + typeof maybeLoadingState.then === 'function'
3140 + ) {
3141 + const loadingState = maybeLoadingState;
3142 + state.count++;
3143 + const ping = onUnsuspend.bind(state);
3144 + loadingState.then(ping, ping);
3145 + }
3146 + resource.state.loading |= Inserted;
3147 + resource.instance = instance;
3148 + markNodeAsHoistable(instance);
3149 + return;
3150 + }
3151 +
3152 + const ownerDocument = getDocumentFromRoot(hoistableRoot);
3153 +
3154 + const stylesheetProps = stylesheetPropsFromRawProps(props);
3155 + const preloadProps = preloadPropsMap.get(key);
3156 + if (preloadProps) {
3157 + adoptPreloadPropsForStylesheet(stylesheetProps, preloadProps);
3158 + }
3159 +
3160 + // Construct and insert a new instance
3161 + instance = ownerDocument.createElement('link');
3162 + markNodeAsHoistable(instance);
3163 + const linkInstance: HTMLLinkElement = (instance: any);
3164 + // This Promise is a loading state used by the Fizz runtime. We need this incase there is a race
3165 + // between this resource being rendered on the client and being rendered with a late completed boundary.
3166 + (linkInstance: any)._p = new Promise((resolve, reject) => {
3167 + linkInstance.onload = resolve;
3168 + linkInstance.onerror = reject;
3169 + });
3170 + setInitialProperties(instance, 'link', stylesheetProps);
3171 + resource.instance = instance;
3172 + }
3173 +
3174 + if (state.stylesheets === null) {
3175 + state.stylesheets = new Map();
3176 + }
3177 + state.stylesheets.set(resource, hoistableRoot);
3178 +
3179 + const preloadEl = resource.state.preload;
3180 + if (preloadEl && (resource.state.loading & Settled) === NotLoaded) {
3181 + state.count++;
3182 + const ping = onUnsuspend.bind(state);
3183 + preloadEl.addEventListener('load', ping);
3184 + preloadEl.addEventListener('error', ping);
3185 + }
3186 + }
3187 +}
3188 +
3189 +export function waitForCommitToBeReady(): null | (Function => Function) {
3190 + if (suspendedState === null) {
3191 + throw new Error(
3192 + 'Internal React Error: suspendedState null when it was expected to exists. Please report this as a React bug.',
3193 + );
3194 + }
3195 +
3196 + const state = suspendedState;
3197 +
3198 + if (state.stylesheets && state.count === 0) {
3199 + // We are not currently blocked but we have not inserted all stylesheets.
3200 + // If this insertion happens and loads or errors synchronously then we can
3201 + // avoid suspending the commit. To do this we check the count again immediately after
3202 + insertSuspendedStylesheets(state, state.stylesheets);
3203 + }
3204 +
3205 + // We need to check the count again because the inserted stylesheets may have led to new
3206 + // tasks to wait on.
3207 + if (state.count > 0) {
3208 + return commit => {
3209 + unsuspendAfterTimeout(state);
3210 + state.unsuspend = commit;
3211 +
3212 + return () => (state.unsuspend = null);
3213 + };
3214 + }
3215 + return null;
3216 +}
3217 +
3218 +function unsuspendAfterTimeout(state: SuspendedState) {
3219 + setTimeout(() => {
3220 + if (state.stylesheets) {
3221 + insertSuspendedStylesheets(state, state.stylesheets);
3222 + }
3223 + if (state.unsuspend) {
3224 + const unsuspend = state.unsuspend;
3225 + state.unsuspend = null;
3226 + unsuspend();
3227 + }
3228 + }, 500);
3229 +}
3230 +
3231 +function onUnsuspend(this: SuspendedState) {
3232 + this.count--;
3233 + if (this.count === 0) {
3234 + if (this.stylesheets) {
3235 + // If we haven't actually inserted the stylesheets yet we need to do so now before starting the commit.
3236 + // The reason we do this after everything else has finished is because we want to have all the stylesheets
3237 + // load synchronously right before mutating. Ideally the new styles will cause a single recalc only on the
3238 + // new tree. When we filled up stylesheets we only inlcuded stylesheets with matching media attributes so we
3239 + // wait for them to load before actually continuing. We expect this to increase the count above zero
3240 + insertSuspendedStylesheets(this, this.stylesheets);
3241 + } else if (this.unsuspend) {
3242 + const unsuspend = this.unsuspend;
3243 + this.unsuspend = null;
3244 + unsuspend();
3245 + }
3246 + }
3247 +}
3248 +
3249 +// This is typecast to non-null because it will always be set before read.
3250 +// it is important that this not be used except when the stack guarantees it exists.
3251 +// Currentlyt his is only during insertSuspendedStylesheet.
3252 +let precedencesByRoot: Map<HoistableRoot, Map<string, Instance>> = (null: any);
3253 +
3254 +function insertSuspendedStylesheets(
3255 + state: SuspendedState,
3256 + resources: Map<StylesheetResource, HoistableRoot>,
3257 +): void {
3258 + // We need to clear this out so we don't try to reinsert after the stylesheets have loaded
3259 + state.stylesheets = null;
3260 +
3261 + if (state.unsuspend === null) {
3262 + // The suspended commit was cancelled. We don't need to insert any stylesheets.
3263 + return;
3264 + }
3265 +
3266 + // Temporarily increment count. we don't want any synchronously loaded stylesheets to try to unsuspend
3267 + // before we finish inserting all stylesheets.
3268 + state.count++;
3269 +
3270 + precedencesByRoot = new Map();
3271 + resources.forEach(insertStylesheetIntoRoot, state);
3272 + precedencesByRoot = (null: any);
3273 +
3274 + // We can remove our temporary count and if we're still at zero we can unsuspend.
3275 + // If we are in the synchronous phase before deciding if the commit should suspend and this
3276 + // ends up hitting the unsuspend path it will just invoke the noop unsuspend.
3277 + onUnsuspend.call(state);
3278 +}
3279 +
3280 +function insertStylesheetIntoRoot(
3281 + this: SuspendedState,
3282 + root: HoistableRoot,
3283 + resource: StylesheetResource,
3284 + map: Map<StylesheetResource, HoistableRoot>,
3285 +) {
3286 + if (resource.state.loading & Inserted) {
3287 + // This resource was inserted by another root committing. we don't need to insert it again
3288 + return;
3289 + }
3290 +
3291 + let last;
3292 + let precedences = precedencesByRoot.get(root);
3293 + if (!precedences) {
3294 + precedences = new Map();
3295 + precedencesByRoot.set(root, precedences);
3296 + const nodes = root.querySelectorAll(
3297 + 'link[data-precedence],style[data-precedence]',
3298 + );
3299 + for (let i = 0; i < nodes.length; i++) {
3300 + const node = nodes[i];
3301 + if (
3302 + node.nodeName === 'link' ||
3303 + // We omit style tags with media="not all" because they are not in the right position
3304 + // and will be hoisted by the Fizz runtime imminently.
3305 + node.getAttribute('media') !== 'not all'
3306 + ) {
3307 + precedences.set('p' + node.dataset.precedence, node);
3308 + last = node;
3309 + }
3310 + }
3311 + if (last) {
3312 + precedences.set('last', last);
3313 + }
3314 + } else {
3315 + last = precedences.get('last');
3316 + }
3317 +
3318 + // We only call this after we have constructed an instance so we assume it here
3319 + const instance: HTMLLinkElement = (resource.instance: any);
3320 + // We will always have a precedence for stylesheet instances
3321 + const precedence: string = (instance.getAttribute('data-precedence'): any);
3322 +
3323 + const prior = precedences.get('p' + precedence) || last;
3324 + if (prior === last) {
3325 + precedences.set('last', instance);
3326 + }
3327 + precedences.set(precedence, instance);
3328 +
3329 + this.count++;
3330 + const onComplete = onUnsuspend.bind(this);
3331 + instance.addEventListener('load', onComplete);
3332 + instance.addEventListener('error', onComplete);
3333 +
3334 + if (prior) {
3335 + (prior.parentNode: any).insertBefore(instance, prior.nextSibling);
3336 + } else {
3337 + const parent =
3338 + root.nodeType === DOCUMENT_NODE
3339 + ? ((((root: any): Document).head: any): Element)
3340 + : ((root: any): ShadowRoot);
3341 + parent.insertBefore(instance, parent.firstChild);
3342 + }
3343 + resource.state.loading |= Inserted;
3344 +}
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetExternalRuntime.js
+3 -13
@@ -6,8 +6,6 @@ import {
6 clientRenderBoundary,
7 completeBoundary,
8 completeSegment,
9 - LOADED,
10 - ERRORED,
9 } from './ReactDOMFizzInstructionSetShared';
10
11 export {clientRenderBoundary, completeBoundary, completeSegment};
@@ -46,10 +44,6 @@ export function completeBoundaryWithStyles(
44 const dependencies = [];
45 let href, precedence, attr, loadingState, resourceEl, media;
46
49 - function setStatus(s) {
50 - this['s'] = s;
51 - }
52 -
47 // Sheets Mode
48 let sheetMode = true;
49 while (true) {
@@ -84,14 +78,10 @@ export function completeBoundaryWithStyles(
78 while ((attr = stylesheetDescriptor[j++])) {
79 resourceEl.setAttribute(attr, stylesheetDescriptor[j++]);
80 }
87 - loadingState = resourceEl['_p'] = new Promise((re, rj) => {
88 - resourceEl.onload = re;
89 - resourceEl.onerror = rj;
81 + loadingState = resourceEl['_p'] = new Promise((resolve, reject) => {
82 + resourceEl.onload = resolve;
83 + resourceEl.onerror = reject;
84 });
91 - loadingState.then(
92 - setStatus.bind(loadingState, LOADED),
93 - setStatus.bind(loadingState, ERRORED),
94 - );
85 // Save this resource element so we can bailout if it is used again
86 resourceMap.set(href, resourceEl);
87 }
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js
+1 -1
@@ -6,6 +6,6 @@ export const clientRenderBoundary =
6 export const completeBoundary =
7 '$RC=function(b,c,e){c=document.getElementById(c);c.parentNode.removeChild(c);var a=document.getElementById(b);if(a){b=a.previousSibling;if(e)b.data="$!",a.setAttribute("data-dgst",e);else{e=b.parentNode;a=b.nextSibling;var f=0;do{if(a&&8===a.nodeType){var d=a.data;if("/$"===d)if(0===f)break;else f--;else"$"!==d&&"$?"!==d&&"$!"!==d||f++}d=a.nextSibling;e.removeChild(a);a=d}while(a);for(;c.firstChild;)e.insertBefore(c.firstChild,a);b.data="$"}b._reactRetry&&b._reactRetry()}};';
8 export const completeBoundaryWithStyles =
9 - '$RM=new Map;\n$RR=function(t,u,y){function v(n){this.s=n}for(var w=$RC,p=$RM,q=new Map,r=document,g,b,h=r.querySelectorAll("link[data-precedence],style[data-precedence]"),x=[],k=0;b=h[k++];)"not all"===b.getAttribute("media")?x.push(b):("LINK"===b.tagName&&p.set(b.getAttribute("href"),b),q.set(b.dataset.precedence,g=b));b=0;h=[];var l,a;for(k=!0;;){if(k){var f=y[b++];if(!f){k=!1;b=0;continue}var c=!1,m=0;var e=f[m++];if(a=p.get(e)){var d=a._p;c=!0}else{a=r.createElement("link");a.href=e;a.rel=\n"stylesheet";for(a.dataset.precedence=l=f[m++];d=f[m++];)a.setAttribute(d,f[m++]);d=a._p=new Promise(function(n,z){a.onload=n;a.onerror=z});d.then(v.bind(d,"l"),v.bind(d,"e"));p.set(e,a)}e=a.getAttribute("media");!d||"l"===d.s||e&&!matchMedia(e).matches||h.push(d);if(c)continue}else{a=x[b++];if(!a)break;l=a.getAttribute("data-precedence");a.removeAttribute("media")}c=q.get(l)||g;c===g&&(g=a);q.set(l,a);c?c.parentNode.insertBefore(a,c.nextSibling):(c=r.head,c.insertBefore(a,c.firstChild))}Promise.all(h).then(w.bind(null,\nt,u,""),w.bind(null,t,u,"Resource failed to load"))};';
9 + '$RM=new Map;\n$RR=function(r,t,w){for(var u=$RC,n=$RM,p=new Map,q=document,g,b,h=q.querySelectorAll("link[data-precedence],style[data-precedence]"),v=[],k=0;b=h[k++];)"not all"===b.getAttribute("media")?v.push(b):("LINK"===b.tagName&&n.set(b.getAttribute("href"),b),p.set(b.dataset.precedence,g=b));b=0;h=[];var l,a;for(k=!0;;){if(k){var f=w[b++];if(!f){k=!1;b=0;continue}var c=!1,m=0;var d=f[m++];if(a=n.get(d)){var e=a._p;c=!0}else{a=q.createElement("link");a.href=d;a.rel="stylesheet";for(a.dataset.precedence=\nl=f[m++];e=f[m++];)a.setAttribute(e,f[m++]);e=a._p=new Promise(function(x,y){a.onload=x;a.onerror=y});n.set(d,a)}d=a.getAttribute("media");!e||"l"===e.s||d&&!matchMedia(d).matches||h.push(e);if(c)continue}else{a=v[b++];if(!a)break;l=a.getAttribute("data-precedence");a.removeAttribute("media")}c=p.get(l)||g;c===g&&(g=a);p.set(l,a);c?c.parentNode.insertBefore(a,c.nextSibling):(c=q.head,c.insertBefore(a,c.firstChild))}Promise.all(h).then(u.bind(null,r,t,""),u.bind(null,r,t,"Resource failed to load"))};';
10 export const completeSegment =
11 '$RS=function(a,b){a=document.getElementById(a);b=document.getElementById(b);for(a.parentNode.removeChild(a);a.firstChild;)b.parentNode.insertBefore(a.firstChild,b);b.parentNode.removeChild(b)};';
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineSource.js
+3 -13
@@ -8,8 +8,6 @@ import {
8 clientRenderBoundary,
9 completeBoundary,
10 completeSegment,
11 - LOADED,
12 - ERRORED,
11 } from './ReactDOMFizzInstructionSetShared';
12
13 export {clientRenderBoundary, completeBoundary, completeSegment};
@@ -49,10 +47,6 @@ export function completeBoundaryWithStyles(
47 const dependencies = [];
48 let href, precedence, attr, loadingState, resourceEl, media;
49
52 - function setStatus(s) {
53 - this['s'] = s;
54 - }
55 -
50 // Sheets Mode
51 let sheetMode = true;
52 while (true) {
@@ -87,14 +81,10 @@ export function completeBoundaryWithStyles(
81 while ((attr = stylesheetDescriptor[j++])) {
82 resourceEl.setAttribute(attr, stylesheetDescriptor[j++]);
83 }
90 - loadingState = resourceEl['_p'] = new Promise((re, rj) => {
91 - resourceEl.onload = re;
92 - resourceEl.onerror = rj;
84 + loadingState = resourceEl['_p'] = new Promise((resolve, reject) => {
85 + resourceEl.onload = resolve;
86 + resourceEl.onerror = reject;
87 });
94 - loadingState.then(
95 - setStatus.bind(loadingState, LOADED),
96 - setStatus.bind(loadingState, ERRORED),
97 - );
88 // Save this resource element so we can bailout if it is used again
89 resourceMap.set(href, resourceEl);
90 }
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetShared.js
-2
@@ -8,8 +8,6 @@ export const SUSPENSE_START_DATA = '$';
8 export const SUSPENSE_END_DATA = '/$';
9 export const SUSPENSE_PENDING_START_DATA = '$?';
10 export const SUSPENSE_FALLBACK_START_DATA = '$!';
11 -export const LOADED = 'l';
12 -export const ERRORED = 'e';
11
12 // TODO: Symbols that are referenced outside this module use dynamic accessor
13 // notation instead of dot notation to prevent Closure's advanced compilation
packages/react-dom/src/__tests__/ReactDOMFloat-test.js
+625 -10
@@ -23,6 +23,7 @@ let ReactDOMClient;
23 let ReactDOMFizzServer;
24 let Suspense;
25 let textCache;
26 +let loadCache;
27 let window;
28 let document;
29 let writable;
@@ -34,6 +35,8 @@ let fatalError = undefined;
35 let renderOptions;
36 let waitForAll;
37 let waitForThrow;
38 +let assertLog;
39 +let Scheduler;
40
41 function resetJSDOM(markup) {
42 // Test Environment
@@ -62,12 +65,15 @@ describe('ReactDOMFloat', () => {
65 ReactDOMFizzServer = require('react-dom/server');
66 Stream = require('stream');
67 Suspense = React.Suspense;
68 + Scheduler = require('scheduler/unstable_mock');
69
70 const InternalTestUtils = require('internal-test-utils');
71 waitForAll = InternalTestUtils.waitForAll;
72 waitForThrow = InternalTestUtils.waitForThrow;
73 + assertLog = InternalTestUtils.assertLog;
74
75 textCache = new Map();
76 + loadCache = new Set();
77
78 resetJSDOM('<!DOCTYPE html><html><head></head><body><div id="container">');
79 container = document.getElementById('container');
@@ -259,6 +265,54 @@ describe('ReactDOMFloat', () => {
265 );
266 }
267
268 + function loadPreloads(hrefs) {
269 + const event = new window.Event('load');
270 + const nodes = document.querySelectorAll('link[rel="preload"]');
271 + resolveLoadables(hrefs, nodes, event, href =>
272 + Scheduler.log('load preload: ' + href),
273 + );
274 + }
275 +
276 + function errorPreloads(hrefs) {
277 + const event = new window.Event('error');
278 + const nodes = document.querySelectorAll('link[rel="preload"]');
279 + resolveLoadables(hrefs, nodes, event, href =>
280 + Scheduler.log('error preload: ' + href),
281 + );
282 + }
283 +
284 + function loadStylesheets(hrefs) {
285 + const event = new window.Event('load');
286 + const nodes = document.querySelectorAll('link[rel="stylesheet"]');
287 + resolveLoadables(hrefs, nodes, event, href =>
288 + Scheduler.log('load stylesheet: ' + href),
289 + );
290 + }
291 +
292 + function errorStylesheets(hrefs) {
293 + const event = new window.Event('error');
294 + const nodes = document.querySelectorAll('link[rel="stylesheet"]');
295 + resolveLoadables(hrefs, nodes, event, href => {
296 + Scheduler.log('error stylesheet: ' + href);
297 + });
298 + }
299 +
300 + function resolveLoadables(hrefs, nodes, event, onLoad) {
301 + const hrefSet = hrefs ? new Set(hrefs) : null;
302 + for (let i = 0; i < nodes.length; i++) {
303 + const node = nodes[i];
304 + if (loadCache.has(node)) {
305 + continue;
306 + }
307 + const href = node.getAttribute('href');
308 + if (!hrefSet || hrefSet.has(href)) {
309 + loadCache.add(node);
310 + onLoad(href);
311 + node.dispatchEvent(event);
312 + }
313 + }
314 + }
315 +
316 // @gate enableFloat
317 it('can render resources before singletons', async () => {
318 const root = ReactDOMClient.createRoot(document);
@@ -1092,14 +1146,10 @@ body {
1146 </html>,
1147 );
1148
1095 - await act(() => {
1096 - const barLink = document.querySelector(
1097 - 'link[rel="stylesheet"][href="bar"]',
1098 - );
1099 - const event = document.createEvent('Events');
1100 - event.initEvent('error', true, true);
1101 - barLink.dispatchEvent(event);
1102 - });
1149 + errorStylesheets(['bar']);
1150 + assertLog(['error stylesheet: bar']);
1151 +
1152 + await waitForAll([]);
1153
1154 const boundaryTemplateInstance = document.getElementById('B:0');
1155 const suspenseInstance = boundaryTemplateInstance.previousSibling;
@@ -1131,6 +1181,13 @@ body {
1181 },
1182 });
1183 await waitForAll([]);
1184 + // When binding a stylesheet that was SSR'd in a boundary reveal there is a loadingState promise
1185 + // We need to use that promise to resolve the suspended commit because we don't know if the load or error
1186 + // events have already fired. This requires the load to be awaited for the commit to have a chance to flush
1187 + // We could change this by tracking the loadingState's fulfilled status directly on the loadingState similar
1188 + // to thenables however this slightly increases the fizz runtime code size.
1189 + await loadStylesheets();
1190 + assertLog(['load stylesheet: foo']);
1191 expect(getMeaningfulChildren(document)).toEqual(
1192 <html>
1193 <head>
@@ -1667,7 +1724,7 @@ body {
1724 <html>
1725 <head />
1726 <body>
1670 - <div>Hello</div>
1727 + <div>Goodbye</div>
1728 <link rel="stylesheet" href="baz" precedence="one" />
1729 </body>
1730 </html>,
@@ -1682,7 +1739,7 @@ body {
1739 <link rel="preload" as="style" href="baz" />
1740 </head>
1741 <body>
1685 - <div>Hello</div>
1742 + <div>Goodbye</div>
1743 </body>
1744 </html>,
1745 );
@@ -2676,6 +2733,564 @@ body {
2733 );
2734 });
2735
2736 + it('can delay commit until css resources load', async () => {
2737 + const root = ReactDOMClient.createRoot(container);
2738 + expect(getMeaningfulChildren(container)).toBe(undefined);
2739 + React.startTransition(() => {
2740 + root.render(
2741 + <>
2742 + <link rel="stylesheet" href="foo" precedence="default" />
2743 + <div>hello</div>
2744 + </>,
2745 + );
2746 + });
2747 + await waitForAll([]);
2748 + expect(getMeaningfulChildren(container)).toBe(undefined);
2749 + expect(getMeaningfulChildren(document.head)).toEqual(
2750 + <link rel="preload" as="style" href="foo" />,
2751 + );
2752 +
2753 + loadPreloads();
2754 + assertLog(['load preload: foo']);
2755 +
2756 + // We expect that the stylesheet is inserted now but the commit has not happened yet.
2757 + expect(getMeaningfulChildren(container)).toBe(undefined);
2758 + expect(getMeaningfulChildren(document.head)).toEqual([
2759 + <link rel="stylesheet" href="foo" data-precedence="default" />,
2760 + <link rel="preload" as="style" href="foo" />,
2761 + ]);
2762 +
2763 + loadStylesheets();
2764 + assertLog(['load stylesheet: foo']);
2765 +
2766 + // We expect that the commit finishes synchronously after the stylesheet loads.
2767 + expect(getMeaningfulChildren(container)).toEqual(<div>hello</div>);
2768 + expect(getMeaningfulChildren(document.head)).toEqual([
2769 + <link rel="stylesheet" href="foo" data-precedence="default" />,
2770 + <link rel="preload" as="style" href="foo" />,
2771 + ]);
2772 + });
2773 +
2774 + xit('can delay commit until css resources error', async () => {
2775 + // TODO: This test fails and crashes jest. need to figure out why before unskipping.
2776 + const root = ReactDOMClient.createRoot(container);
2777 + expect(getMeaningfulChildren(container)).toBe(undefined);
2778 + React.startTransition(() => {
2779 + root.render(
2780 + <>
2781 + <link rel="stylesheet" href="foo" precedence="default" />
2782 + <link rel="stylesheet" href="bar" precedence="default" />
2783 + <div>hello</div>
2784 + </>,
2785 + );
2786 + });
2787 + await waitForAll([]);
2788 + expect(getMeaningfulChildren(container)).toBe(undefined);
2789 + expect(getMeaningfulChildren(document.head)).toEqual([
2790 + <link rel="preload" as="style" href="foo" />,
2791 + <link rel="preload" as="style" href="bar" />,
2792 + ]);
2793 +
2794 + loadPreloads(['foo']);
2795 + errorPreloads(['bar']);
2796 + assertLog(['load preload: foo', 'error preload: bar']);
2797 +
2798 + // We expect that the stylesheet is inserted now but the commit has not happened yet.
2799 + expect(getMeaningfulChildren(container)).toBe(undefined);
2800 + expect(getMeaningfulChildren(document.head)).toEqual([
2801 + <link rel="stylesheet" href="foo" data-precedence="default" />,
2802 + <link rel="stylesheet" href="bar" data-precedence="default" />,
2803 + <link rel="preload" as="style" href="foo" />,
2804 + <link rel="preload" as="style" href="bar" />,
2805 + ]);
2806 +
2807 + // Try just this and crash all of Jest
2808 + errorStylesheets(['bar']);
2809 +
2810 + // // Try this and it fails the test when it shouldn't
2811 + // await act(() => {
2812 + // errorStylesheets(['bar']);
2813 + // });
2814 +
2815 + // // Try this there is nothing throwing here which is not really surprising since
2816 + // // the error is bubbling up through some kind of unhandled promise rejection thingy but
2817 + // // still I thought it was worth confirming
2818 + // try {
2819 + // await act(() => {
2820 + // errorStylesheets(['bar']);
2821 + // });
2822 + // } catch (e) {
2823 + // console.log(e);
2824 + // }
2825 +
2826 + loadStylesheets(['foo']);
2827 + assertLog(['load stylesheet: foo', 'error stylesheet: bar']);
2828 +
2829 + // We expect that the commit finishes synchronously after the stylesheet loads.
2830 + expect(getMeaningfulChildren(container)).toEqual(<div>hello</div>);
2831 + expect(getMeaningfulChildren(document.head)).toEqual([
2832 + <link rel="stylesheet" href="foo" data-precedence="default" />,
2833 + <link rel="stylesheet" href="bar" data-precedence="default" />,
2834 + <link rel="preload" as="style" href="foo" />,
2835 + <link rel="preload" as="style" href="bar" />,
2836 + ]);
2837 + });
2838 +
2839 + it('assumes stylesheets that load in the shell loaded already', async () => {
2840 + await actIntoEmptyDocument(() => {
2841 + renderToPipeableStream(
2842 + <html>
2843 + <body>
2844 + <link rel="stylesheet" href="foo" precedence="default" />
2845 + hello
2846 + </body>
2847 + </html>,
2848 + ).pipe(writable);
2849 + });
2850 +
2851 + let root;
2852 + React.startTransition(() => {
2853 + root = ReactDOMClient.hydrateRoot(
2854 + document,
2855 + <html>
2856 + <body>
2857 + <link rel="stylesheet" href="foo" precedence="default" />
2858 + hello
2859 + </body>
2860 + </html>,
2861 + );
2862 + });
2863 + await waitForAll([]);
2864 + expect(getMeaningfulChildren(document)).toEqual(
2865 + <html>
2866 + <head>
2867 + <link rel="stylesheet" href="foo" data-precedence="default" />
2868 + </head>
2869 + <body>hello</body>
2870 + </html>,
2871 + );
2872 +
2873 + React.startTransition(() => {
2874 + root.render(
2875 + <html>
2876 + <body>
2877 + <link rel="stylesheet" href="foo" precedence="default" />
2878 + hello2
2879 + </body>
2880 + </html>,
2881 + );
2882 + });
2883 + await waitForAll([]);
2884 + expect(getMeaningfulChildren(document)).toEqual(
2885 + <html>
2886 + <head>
2887 + <link rel="stylesheet" href="foo" data-precedence="default" />
2888 + </head>
2889 + <body>hello2</body>
2890 + </html>,
2891 + );
2892 +
2893 + React.startTransition(() => {
2894 + root.render(
2895 + <html>
2896 + <body>
2897 + <link rel="stylesheet" href="foo" precedence="default" />
2898 + hello3
2899 + <link rel="stylesheet" href="bar" precedence="default" />
2900 + </body>
2901 + </html>,
2902 + );
2903 + });
2904 + await waitForAll([]);
2905 + expect(getMeaningfulChildren(document)).toEqual(
2906 + <html>
2907 + <head>
2908 + <link rel="stylesheet" href="foo" data-precedence="default" />
2909 + <link rel="preload" href="bar" as="style" />
2910 + </head>
2911 + <body>hello2</body>
2912 + </html>,
2913 + );
2914 +
2915 + loadPreloads();
2916 + assertLog(['load preload: bar']);
2917 + expect(getMeaningfulChildren(document)).toEqual(
2918 + <html>
2919 + <head>
2920 + <link rel="stylesheet" href="foo" data-precedence="default" />
2921 + <link rel="stylesheet" href="bar" data-precedence="default" />
2922 + <link rel="preload" href="bar" as="style" />
2923 + </head>
2924 + <body>hello2</body>
2925 + </html>,
2926 + );
2927 +
2928 + loadStylesheets(['bar']);
2929 + assertLog(['load stylesheet: bar']);
2930 + expect(getMeaningfulChildren(document)).toEqual(
2931 + <html>
2932 + <head>
2933 + <link rel="stylesheet" href="foo" data-precedence="default" />
2934 + <link rel="stylesheet" href="bar" data-precedence="default" />
2935 + <link rel="preload" href="bar" as="style" />
2936 + </head>
2937 + <body>hello3</body>
2938 + </html>,
2939 + );
2940 + });
2941 +
2942 + // @gate TODO
2943 + it('can interrupt a suspended commit with a new update', async () => {
2944 + function App({children}) {
2945 + return (
2946 + <html>
2947 + <body>{children}</body>
2948 + </html>
2949 + );
2950 + }
2951 + const root = ReactDOMClient.createRoot(document);
2952 + root.render(<App />);
2953 + await waitForAll([]);
2954 +
2955 + React.startTransition(() => {
2956 + root.render(
2957 + <App>
2958 + hello
2959 + <link rel="stylesheet" href="foo" precedence="default" />
2960 + </App>,
2961 + );
2962 + });
2963 + await waitForAll([]);
2964 + expect(getMeaningfulChildren(document)).toEqual(
2965 + <html>
2966 + <head>
2967 + <link rel="preload" href="foo" as="style" />
2968 + </head>
2969 + <body />
2970 + </html>,
2971 + );
2972 +
2973 + root.render(
2974 + <App>
2975 + hello2
2976 + {null}
2977 + <link rel="stylesheet" href="bar" precedence="default" />
2978 + </App>,
2979 + );
2980 + await waitForAll([]);
2981 + expect(getMeaningfulChildren(document)).toEqual(
2982 + <html>
2983 + <head>
2984 + <link rel="stylesheet" href="bar" data-precedence="default" />
2985 + <link rel="preload" href="foo" as="style" />
2986 + <link rel="preload" href="bar" as="style" />
2987 + </head>
2988 + <body>hello2</body>
2989 + </html>,
2990 + );
2991 +
2992 + // Even though foo was preloaded we don't see the stylesheet insert because the commit was cancelled.
2993 + // If we do a followup render that tries to recommit that resource it will insert right away because
2994 + // the preload is already loaded
2995 + loadPreloads(['foo']);
2996 + assertLog(['load preload: foo']);
2997 + expect(getMeaningfulChildren(document)).toEqual(
2998 + <html>
2999 + <head>
3000 + <link rel="stylesheet" href="bar" data-precedence="default" />
3001 + <link rel="preload" href="foo" as="style" />
3002 + <link rel="preload" href="bar" as="style" />
3003 + </head>
3004 + <body>hello2</body>
3005 + </html>,
3006 + );
3007 +
3008 + React.startTransition(() => {
3009 + root.render(
3010 + <App>
3011 + hello3
3012 + <link rel="stylesheet" href="foo" precedence="default" />
3013 + <link rel="stylesheet" href="bar" precedence="default" />
3014 + </App>,
3015 + );
3016 + });
3017 + await waitForAll([]);
3018 + expect(getMeaningfulChildren(document)).toEqual(
3019 + <html>
3020 + <head>
3021 + <link rel="stylesheet" href="bar" data-precedence="default" />
3022 + <link rel="stylesheet" href="foo" data-precedence="default" />
3023 + <link rel="preload" href="foo" as="style" />
3024 + <link rel="preload" href="bar" as="style" />
3025 + </head>
3026 + <body>hello2</body>
3027 + </html>,
3028 + );
3029 +
3030 + loadStylesheets(['foo']);
3031 + assertLog(['load stylesheet: foo']);
3032 + expect(getMeaningfulChildren(document)).toEqual(
3033 + <html>
3034 + <head>
3035 + <link rel="stylesheet" href="bar" data-precedence="default" />
3036 + <link rel="stylesheet" href="foo" data-precedence="default" />
3037 + <link rel="preload" href="foo" as="style" />
3038 + <link rel="preload" href="bar" as="style" />
3039 + </head>
3040 + <body>hello3</body>
3041 + </html>,
3042 + );
3043 + });
3044 +
3045 + it('can suspend commits on more than one root for the same resource at the same time', async () => {
3046 + document.body.innerHTML = '';
3047 + const container1 = document.createElement('div');
3048 + const container2 = document.createElement('div');
3049 + document.body.appendChild(container1);
3050 + document.body.appendChild(container2);
3051 +
3052 + const root1 = ReactDOMClient.createRoot(container1);
3053 + const root2 = ReactDOMClient.createRoot(container2);
3054 +
3055 + React.startTransition(() => {
3056 + root1.render(
3057 + <div>
3058 + one
3059 + <link rel="stylesheet" href="foo" precedence="default" />
3060 + <link rel="stylesheet" href="one" precedence="default" />
3061 + </div>,
3062 + );
3063 + });
3064 + await waitForAll([]);
3065 + React.startTransition(() => {
3066 + root2.render(
3067 + <div>
3068 + two
3069 + <link rel="stylesheet" href="foo" precedence="default" />
3070 + <link rel="stylesheet" href="two" precedence="default" />
3071 + </div>,
3072 + );
3073 + });
3074 + await waitForAll([]);
3075 +
3076 + expect(getMeaningfulChildren(document)).toEqual(
3077 + <html>
3078 + <head>
3079 + <link rel="preload" href="foo" as="style" />
3080 + <link rel="preload" href="one" as="style" />
3081 + <link rel="preload" href="two" as="style" />
3082 + </head>
3083 + <body>
3084 + <div />
3085 + <div />
3086 + </body>
3087 + </html>,
3088 + );
3089 +
3090 + loadPreloads(['foo', 'two']);
3091 + assertLog(['load preload: foo', 'load preload: two']);
3092 + expect(getMeaningfulChildren(document)).toEqual(
3093 + <html>
3094 + <head>
3095 + <link rel="stylesheet" href="foo" data-precedence="default" />
3096 + <link rel="stylesheet" href="two" data-precedence="default" />
3097 + <link rel="preload" href="foo" as="style" />
3098 + <link rel="preload" href="one" as="style" />
3099 + <link rel="preload" href="two" as="style" />
3100 + </head>
3101 + <body>
3102 + <div />
3103 + <div />
3104 + </body>
3105 + </html>,
3106 + );
3107 +
3108 + loadStylesheets(['foo', 'two']);
3109 + assertLog(['load stylesheet: foo', 'load stylesheet: two']);
3110 + expect(getMeaningfulChildren(document)).toEqual(
3111 + <html>
3112 + <head>
3113 + <link rel="stylesheet" href="foo" data-precedence="default" />
3114 + <link rel="stylesheet" href="two" data-precedence="default" />
3115 + <link rel="preload" href="foo" as="style" />
3116 + <link rel="preload" href="one" as="style" />
3117 + <link rel="preload" href="two" as="style" />
3118 + </head>
3119 + <body>
3120 + <div />
3121 + <div>
3122 + <div>two</div>
3123 + </div>
3124 + </body>
3125 + </html>,
3126 + );
3127 +
3128 + loadPreloads();
3129 + loadStylesheets();
3130 + assertLog(['load preload: one', 'load stylesheet: one']);
3131 + expect(getMeaningfulChildren(document)).toEqual(
3132 + <html>
3133 + <head>
3134 + <link rel="stylesheet" href="foo" data-precedence="default" />
3135 + <link rel="stylesheet" href="two" data-precedence="default" />
3136 + <link rel="stylesheet" href="one" data-precedence="default" />
3137 + <link rel="preload" href="foo" as="style" />
3138 + <link rel="preload" href="one" as="style" />
3139 + <link rel="preload" href="two" as="style" />
3140 + </head>
3141 + <body>
3142 + <div>
3143 + <div>one</div>
3144 + </div>
3145 + <div>
3146 + <div>two</div>
3147 + </div>
3148 + </body>
3149 + </html>,
3150 + );
3151 + });
3152 +
3153 + it('can unsuspend after a timeout even if some assets never load', async () => {
3154 + function App({children}) {
3155 + return (
3156 + <html>
3157 + <body>{children}</body>
3158 + </html>
3159 + );
3160 + }
3161 + const root = ReactDOMClient.createRoot(document);
3162 + root.render(<App />);
3163 + React.startTransition(() => {
3164 + root.render(
3165 + <App>
3166 + hello
3167 + <link rel="stylesheet" href="foo" precedence="default" />
3168 + </App>,
3169 + );
3170 + });
3171 + await waitForAll([]);
3172 + expect(getMeaningfulChildren(document)).toEqual(
3173 + <html>
3174 + <head>
3175 + <link rel="preload" href="foo" as="style" />
3176 + </head>
3177 + <body />
3178 + </html>,
3179 + );
3180 +
3181 + jest.advanceTimersByTime(1000);
3182 + expect(getMeaningfulChildren(document)).toEqual(
3183 + <html>
3184 + <head>
3185 + <link rel="stylesheet" href="foo" data-precedence="default" />
3186 + <link rel="preload" href="foo" as="style" />
3187 + </head>
3188 + <body>hello</body>
3189 + </html>,
3190 + );
3191 +
3192 + // We will load these after the commit finishes to ensure nothing errors and nothing new inserts
3193 + loadPreloads(['foo']);
3194 + loadStylesheets(['foo']);
3195 + expect(getMeaningfulChildren(document)).toEqual(
3196 + <html>
3197 + <head>
3198 + <link rel="stylesheet" href="foo" data-precedence="default" />
3199 + <link rel="preload" href="foo" as="style" />
3200 + </head>
3201 + <body>hello</body>
3202 + </html>,
3203 + );
3204 + });
3205 +
3206 + it('can start a new suspended commit after a previous one finishes', async () => {
3207 + function App({children}) {
3208 + return (
3209 + <html>
3210 + <body>{children}</body>
3211 + </html>
3212 + );
3213 + }
3214 + const root = ReactDOMClient.createRoot(document);
3215 + root.render(<App />);
3216 + React.startTransition(() => {
3217 + root.render(
3218 + <App>
3219 + hello
3220 + <link rel="stylesheet" href="foo" precedence="default" />
3221 + </App>,
3222 + );
3223 + });
3224 + await waitForAll([]);
3225 + expect(getMeaningfulChildren(document)).toEqual(
3226 + <html>
3227 + <head>
3228 + <link rel="preload" href="foo" as="style" />
3229 + </head>
3230 + <body />
3231 + </html>,
3232 + );
3233 +
3234 + React.startTransition(() => {
3235 + root.render(
3236 + <App>
3237 + hello2
3238 + {null}
3239 + <link rel="stylesheet" href="bar" precedence="default" />
3240 + </App>,
3241 + );
3242 + });
3243 + await waitForAll([]);
3244 + expect(getMeaningfulChildren(document)).toEqual(
3245 + <html>
3246 + <head>
3247 + <link rel="preload" href="foo" as="style" />
3248 + </head>
3249 + <body />
3250 + </html>,
3251 + );
3252 +
3253 + loadPreloads();
3254 + loadStylesheets();
3255 + assertLog(['load preload: foo', 'load stylesheet: foo']);
3256 + expect(getMeaningfulChildren(document)).toEqual(
3257 + <html>
3258 + <head>
3259 + <link rel="stylesheet" href="foo" data-precedence="default" />
3260 + <link rel="preload" href="foo" as="style" />
3261 + </head>
3262 + <body>hello</body>
3263 + </html>,
3264 + );
3265 +
3266 + // The second update should process now
3267 + await waitForAll([]);
3268 + expect(getMeaningfulChildren(document)).toEqual(
3269 + <html>
3270 + <head>
3271 + <link rel="stylesheet" href="foo" data-precedence="default" />
3272 + <link rel="preload" href="foo" as="style" />
3273 + <link rel="preload" href="bar" as="style" />
3274 + </head>
3275 + <body>hello</body>
3276 + </html>,
3277 + );
3278 + loadPreloads();
3279 + loadStylesheets();
3280 + assertLog(['load preload: bar', 'load stylesheet: bar']);
3281 + expect(getMeaningfulChildren(document)).toEqual(
3282 + <html>
3283 + <head>
3284 + <link rel="stylesheet" href="foo" data-precedence="default" />
3285 + <link rel="stylesheet" href="bar" data-precedence="default" />
3286 + <link rel="preload" href="foo" as="style" />
3287 + <link rel="preload" href="bar" as="style" />
3288 + </head>
3289 + <body>hello2</body>
3290 + </html>,
3291 + );
3292 + });
3293 +
3294 describe('ReactDOM.prefetchDNS(href)', () => {
3295 it('creates a dns-prefetch resource when called', async () => {
3296 function App({url}) {
packages/react-dom/src/client/ReactDOMRoot.js
+1 -1
@@ -15,7 +15,7 @@ import type {
15
16 import ReactDOMSharedInternals from '../ReactDOMSharedInternals';
17 const {Dispatcher} = ReactDOMSharedInternals;
18 -import {ReactDOMClientDispatcher} from 'react-dom-bindings/src/client/ReactDOMFloatClient';
18 +import {ReactDOMClientDispatcher} from 'react-dom-bindings/src/client/ReactDOMHostConfig';
19 import {queueExplicitHydrationTarget} from 'react-dom-bindings/src/events/ReactDOMEventReplaying';
20 import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
21 import {
packages/react-noop-renderer/src/createReactNoop.js
+19
@@ -578,6 +578,12 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
578 return type === 'suspensey-thing' && typeof props.src === 'string';
579 },
580
581 + mayResourceSuspendCommit(resource: mixed): boolean {
582 + throw new Error(
583 + 'Resources are not implemented for React Noop yet. This method should not be called',
584 + );
585 + },
586 +
587 preloadInstance(type: string, props: Props): boolean {
588 if (type !== 'suspensey-thing' || typeof props.src !== 'string') {
589 throw new Error('Attempted to preload unexpected instance: ' + type);
@@ -608,8 +614,21 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
614 }
615 },
616
617 + preloadResource(resource: mixed): boolean {
618 + throw new Error(
619 + 'Resources are not implemented for React Noop yet. This method should not be called',
620 + );
621 + },
622 +
623 startSuspendingCommit,
624 suspendInstance,
625 +
626 + suspendResource(resource: mixed): void {
627 + throw new Error(
628 + 'Resources are not implemented for React Noop yet. This method should not be called',
629 + );
630 + },
631 +
632 waitForCommitToBeReady,
633
634 prepareRendererToRender() {},
packages/react-reconciler/src/ReactFiberCommitWork.js
+54 -12
@@ -160,6 +160,7 @@ import {
160 unmountHoistable,
161 prepareToCommitHoistables,
162 suspendInstance,
163 + suspendResource,
164 } from './ReactFiberHostConfig';
165 import {
166 captureCommitPhaseError,
@@ -4064,23 +4065,64 @@ export function commitPassiveUnmountEffects(finishedWork: Fiber): void {
4065 resetCurrentDebugFiberInDEV();
4066 }
4067
4067 -export function recursivelyAccumulateSuspenseyCommit(parentFiber: Fiber): void {
4068 +export function accumulateSuspenseyCommit(finishedWork: Fiber): void {
4069 + accumulateSuspenseyCommitOnFiber(finishedWork);
4070 +}
4071 +
4072 +function recursivelyAccumulateSuspenseyCommit(parentFiber: Fiber): void {
4073 if (parentFiber.subtreeFlags & SuspenseyCommit) {
4074 let child = parentFiber.child;
4075 while (child !== null) {
4071 - recursivelyAccumulateSuspenseyCommit(child);
4072 - switch (child.tag) {
4073 - case HostComponent:
4074 - case HostHoistable: {
4075 - if (child.flags & SuspenseyCommit) {
4076 - const type = child.type;
4077 - const props = child.memoizedProps;
4078 - suspendInstance(type, props);
4079 - }
4080 - break;
4076 + accumulateSuspenseyCommitOnFiber(child);
4077 + child = child.sibling;
4078 + }
4079 + }
4080 +}
4081 +
4082 +function accumulateSuspenseyCommitOnFiber(fiber: Fiber) {
4083 + switch (fiber.tag) {
4084 + case HostHoistable: {
4085 + recursivelyAccumulateSuspenseyCommit(fiber);
4086 + if (fiber.flags & SuspenseyCommit) {
4087 + if (fiber.memoizedState !== null) {
4088 + suspendResource(
4089 + // This should always be set by visiting HostRoot first
4090 + (currentHoistableRoot: any),
4091 + fiber.memoizedState,
4092 + fiber.memoizedProps,
4093 + );
4094 + } else {
4095 + const type = fiber.type;
4096 + const props = fiber.memoizedProps;
4097 + suspendInstance(type, props);
4098 }
4099 }
4083 - child = child.sibling;
4100 + break;
4101 + }
4102 + case HostComponent: {
4103 + recursivelyAccumulateSuspenseyCommit(fiber);
4104 + if (fiber.flags & SuspenseyCommit) {
4105 + const type = fiber.type;
4106 + const props = fiber.memoizedProps;
4107 + suspendInstance(type, props);
4108 + }
4109 + break;
4110 + }
4111 + case HostRoot:
4112 + case HostPortal: {
4113 + if (enableFloat && supportsResources) {
4114 + const previousHoistableRoot = currentHoistableRoot;
4115 + const container: Container = fiber.stateNode.containerInfo;
4116 + currentHoistableRoot = getHoistableRoot(container);
4117 +
4118 + recursivelyAccumulateSuspenseyCommit(fiber);
4119 + currentHoistableRoot = previousHoistableRoot;
4120 + break;
4121 + }
4122 + }
4123 + // eslint-disable-next-line-no-fallthrough
4124 + default: {
4125 + recursivelyAccumulateSuspenseyCommit(fiber);
4126 }
4127 }
4128 }
packages/react-reconciler/src/ReactFiberCompleteWork.js
+38 -29
@@ -110,6 +110,7 @@ import {
110 preparePortalMount,
111 prepareScopeUpdate,
112 maySuspendCommit,
113 + mayResourceSuspendCommit,
114 preloadInstance,
115 } from './ReactFiberHostConfig';
116 import {
@@ -520,24 +521,7 @@ function preloadInstanceAndSuspendIfNeeded(
521 props: Props,
522 renderLanes: Lanes,
523 ) {
523 - // Ask the renderer if this instance should suspend the commit.
524 - if (!maySuspendCommit(type, props)) {
525 - // If this flag was set previously, we can remove it. The flag represents
526 - // whether this particular set of props might ever need to suspend. The
527 - // safest thing to do is for maySuspendCommit to always return true, but
528 - // if the renderer is reasonably confident that the underlying resource
529 - // won't be evicted, it can return false as a performance optimization.
530 - workInProgress.flags &= ~SuspenseyCommit;
531 - return;
532 - }
533 -
534 - // Mark this fiber with a flag. We use this right before the commit phase to
535 - // find all the fibers that might need to suspend the commit. In the future
536 - // we'll also use it when revealing a hidden tree. It gets set even if we
537 - // don't end up suspending this particular commit, because if this tree ever
538 - // becomes hidden, we might want to suspend before revealing it again.
524 workInProgress.flags |= SuspenseyCommit;
540 -
525 // Check if we're rendering at a "non-urgent" priority. This is the same
526 // check that `useDeferredValue` does to determine whether it needs to
527 // defer. This is partly for gradual adoption purposes (i.e. shouldn't start
@@ -1036,6 +1020,10 @@ function completeWork(
1020 markRef(workInProgress);
1021 }
1022
1023 + let maySuspend = false;
1024 +
1025 + // @TODO refactor this block to create the instance here in complete phase if we
1026 + // are not hydrating.
1027 if (
1028 // We are mounting and must Update this Hoistable in this commit
1029 current === null ||
@@ -1043,8 +1031,20 @@ function completeWork(
1031 // and require an update
1032 current.memoizedState !== workInProgress.memoizedState
1033 ) {
1034 + if (workInProgress.memoizedState !== null) {
1035 + maySuspend = mayResourceSuspendCommit(workInProgress.memoizedState);
1036 + } else {
1037 + maySuspend = maySuspendCommit(
1038 + workInProgress.type,
1039 + workInProgress.pendingProps,
1040 + );
1041 + }
1042 markUpdate(workInProgress);
1043 } else if (workInProgress.memoizedState === null) {
1044 + maySuspend = maySuspendCommit(
1045 + workInProgress.type,
1046 + workInProgress.pendingProps,
1047 + );
1048 // We may have props to update on the Hoistable instance. We use the
1049 // updateHostComponent path becuase it produces the update queue
1050 // we need for Hoistables
@@ -1062,12 +1062,16 @@ function completeWork(
1062 // throw to suspend, and if the resource immediately loads, the work loop
1063 // will resume rendering as if the work-in-progress completed. So it must
1064 // fully complete.
1065 - preloadInstanceAndSuspendIfNeeded(
1066 - workInProgress,
1067 - workInProgress.type,
1068 - workInProgress.pendingProps,
1069 - renderLanes,
1070 - );
1065 + if (maySuspend) {
1066 + preloadInstanceAndSuspendIfNeeded(
1067 + workInProgress,
1068 + workInProgress.type,
1069 + workInProgress.pendingProps,
1070 + renderLanes,
1071 + );
1072 + } else {
1073 + workInProgress.flags &= ~SuspenseyCommit;
1074 + }
1075 return null;
1076 }
1077 }
@@ -1137,6 +1141,7 @@ function completeWork(
1141 case HostComponent: {
1142 popHostContext(workInProgress);
1143 const type = workInProgress.type;
1144 + const maySuspend = maySuspendCommit(type, newProps);
1145 if (current !== null && workInProgress.stateNode != null) {
1146 updateHostComponent(
1147 current,
@@ -1217,12 +1222,16 @@ function completeWork(
1222 // throw to suspend, and if the resource immediately loads, the work loop
1223 // will resume rendering as if the work-in-progress completed. So it must
1224 // fully complete.
1220 - preloadInstanceAndSuspendIfNeeded(
1221 - workInProgress,
1222 - type,
1223 - newProps,
1224 - renderLanes,
1225 - );
1225 + if (maySuspend) {
1226 + preloadInstanceAndSuspendIfNeeded(
1227 + workInProgress,
1228 + type,
1229 + newProps,
1230 + renderLanes,
1231 + );
1232 + } else {
1233 + workInProgress.flags &= ~SuspenseyCommit;
1234 + }
1235 return null;
1236 }
1237 case HostText: {
packages/react-reconciler/src/ReactFiberHostConfigWithNoResources.js
+3
@@ -32,3 +32,6 @@ export const mountHoistable = shim;
32 export const unmountHoistable = shim;
33 export const createHoistableInstance = shim;
34 export const prepareToCommitHoistables = shim;
35 +export const mayResourceSuspendCommit = shim;
36 +export const preloadResource = shim;
37 +export const suspendResource = shim;
packages/react-reconciler/src/ReactFiberWorkLoop.js
+2 -2
@@ -209,7 +209,7 @@ import {
209 invokePassiveEffectMountInDEV,
210 invokeLayoutEffectUnmountInDEV,
211 invokePassiveEffectUnmountInDEV,
212 - recursivelyAccumulateSuspenseyCommit,
212 + accumulateSuspenseyCommit,
213 } from './ReactFiberCommitWork';
214 import {enqueueUpdate} from './ReactFiberClassUpdateQueue';
215 import {resetContextDependencies} from './ReactFiberNewContext';
@@ -1444,7 +1444,7 @@ function commitRootWhenReady(
1444 // the suspensey resources. The renderer is responsible for accumulating
1445 // all the load events. This all happens in a single synchronous
1446 // transaction, so it track state in its own module scope.
1447 - recursivelyAccumulateSuspenseyCommit(finishedWork);
1447 + accumulateSuspenseyCommit(finishedWork);
1448 // At the end, ask the renderer if it's ready to commit, or if we should
1449 // suspend. If it's not ready, it will return a callback to subscribe to
1450 // a ready event.
packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
+3
@@ -220,6 +220,9 @@ export const unmountHoistable = $$$hostConfig.unmountHoistable;
220 export const createHoistableInstance = $$$hostConfig.createHoistableInstance;
221 export const prepareToCommitHoistables =
222 $$$hostConfig.prepareToCommitHoistables;
223 +export const mayResourceSuspendCommit = $$$hostConfig.mayResourceSuspendCommit;
224 +export const preloadResource = $$$hostConfig.preloadResource;
225 +export const suspendResource = $$$hostConfig.suspendResource;
226
227 // -------------------
228 // Singletons
scripts/error-codes/codes.json
+2 -1
@@ -459,5 +459,6 @@
459 "471": "BigInt (%s) is not yet supported as an argument to a Server Function.",
460 "472": "Type %s is not supported as an argument to a Server Function.",
461 "473": "React doesn't accept base64 encoded file uploads because we don't except form data passed from a browser to ever encode data that way. If that's the wrong assumption, we can easily fix it.",
462 - "474": "Suspense Exception: This is not a real error, and should not leak into userspace. If you're seeing this, it's likely a bug in React."
462 + "474": "Suspense Exception: This is not a real error, and should not leak into userspace. If you're seeing this, it's likely a bug in React.",
463 + "475": "Internal React Error: suspendedState null when it was expected to exists. Please report this as a React bug."
464 }