main
js 1,674 lines 47.4 KB
Raw
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 /**
11 * This is a renderer of React that doesn't have a render target output.
12 * It is useful to demonstrate the internals of the reconciler in isolation
13 * and for testing semantics of reconciliation separate from the host
14 * environment.
15 */
16
17 import type {
18 Fiber,
19 TransitionTracingCallbacks,
20 } from 'react-reconciler/src/ReactInternalTypes';
21 import type {UpdateQueue} from 'react-reconciler/src/ReactFiberClassUpdateQueue';
22 import type {ReactNodeList} from 'shared/ReactTypes';
23 import type {RootTag} from 'react-reconciler/src/ReactRootTags';
24 import type {EventPriority} from 'react-reconciler/src/ReactEventPriorities';
25 import type {TransitionTypes} from 'react/src/ReactTransitionType';
26 import typeof * as HostConfig from 'react-reconciler/src/ReactFiberConfig';
27 import typeof * as ReactFiberConfigWithNoMutation from 'react-reconciler/src/ReactFiberConfigWithNoMutation';
28 import typeof * as ReactFiberConfigWithNoViewTransition from 'react-reconciler/src/ReactFiberConfigWithNoViewTransition';
29 import typeof * as ReactFiberConfigWithNoPersistence from 'react-reconciler/src/ReactFiberConfigWithNoPersistence';
30
31 import typeof * as ReconcilerAPI from 'react-reconciler/src/ReactFiberReconciler';
32 import type {
33 Container,
34 HostContext,
35 Instance,
36 PublicInstance,
37 TextInstance,
38 } from './ReactFiberConfigNoop';
39
40 import * as Scheduler from 'scheduler/unstable_mock';
41 import {REACT_FRAGMENT_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
42 import isArray from 'shared/isArray';
43 import {checkPropStringCoercion} from 'shared/CheckStringCoercion';
44 import {
45 NoEventPriority,
46 DiscreteEventPriority,
47 DefaultEventPriority,
48 IdleEventPriority,
49 ConcurrentRoot,
50 LegacyRoot,
51 } from 'react-reconciler/constants';
52 import * as DefaultConfig from './ReactFiberConfigNoop';
53
54 import {disableLegacyMode} from 'shared/ReactFeatureFlags';
55
56 import ReactSharedInternals from 'shared/ReactSharedInternals';
57 import ReactVersion from 'shared/ReactVersion';
58
59 type Props = {
60 prop: any,
61 hidden: boolean,
62 children?: mixed,
63 bottom?: null | number,
64 left?: null | number,
65 right?: null | number,
66 top?: null | number,
67 src?: string,
68 ...
69 };
70 type CreateRootOptions = {
71 unstable_transitionCallbacks?: TransitionTracingCallbacks,
72 onUncaughtError?: (
73 error: mixed,
74 errorInfo: {+componentStack: ?string},
75 ) => void,
76 onCaughtError?: (
77 error: mixed,
78 errorInfo: {
79 +componentStack: ?string,
80 +errorBoundary?: ?component(...props: any),
81 },
82 ) => void,
83 onDefaultTransitionIndicator?: () => void | (() => void),
84 ...
85 };
86 type InstanceMeasurement = null;
87
88 type SuspenseyCommitSubscription = {
89 pendingCount: number,
90 commit: null | (() => void),
91 };
92
93 export opaque type SuspendedState = SuspenseyCommitSubscription;
94
95 export type TransitionStatus = mixed;
96
97 export type FormInstance = Instance;
98
99 export type RunningViewTransition = null;
100
101 export type ViewTransitionInstance = null | {name: string, ...};
102
103 export type GestureTimeline = null;
104
105 const NO_CONTEXT = {};
106 const UPPERCASE_CONTEXT = {};
107 if (__DEV__) {
108 Object.freeze(NO_CONTEXT);
109 }
110
111 function createReactNoop(
112 reconciler: (hostConfig: HostConfig) => ReconcilerAPI,
113 useMutation: boolean,
114 ): any {
115 let instanceCounter = 0;
116 let hostUpdateCounter = 0;
117 let hostCloneCounter = 0;
118
119 function appendChildToContainerOrInstance(
120 parentInstance: Container | Instance,
121 child: Instance | TextInstance,
122 ): void {
123 const prevParent = child.parent;
124
125 if (
126 prevParent !== -1 &&
127 prevParent !==
128 // $FlowFixMe[prop-missing]
129 // $FlowFixMe[incompatible-type]
130 (parentInstance as Instance).id
131 ) {
132 throw new Error('Reparenting is not allowed');
133 }
134
135 child.parent =
136 // $FlowFixMe[prop-missing]
137 // $FlowFixMe[incompatible-type]
138 (parentInstance as Instance).id;
139 const index = parentInstance.children.indexOf(child);
140 if (index !== -1) {
141 parentInstance.children.splice(index, 1);
142 }
143 parentInstance.children.push(child);
144 }
145
146 function appendChildToContainer(
147 parentInstance: Container,
148 child: Instance | TextInstance,
149 ): void {
150 if (typeof parentInstance.rootID !== 'string') {
151 // Some calls to this aren't typesafe.
152 // This helps surface mistakes in tests.
153 throw new Error(
154 'appendChildToContainer() first argument is not a container.',
155 );
156 }
157 appendChildToContainerOrInstance(parentInstance, child);
158 }
159
160 function appendChild(
161 parentInstance: Instance,
162 child: Instance | TextInstance,
163 ): void {
164 if (typeof (parentInstance as any).rootID === 'string') {
165 // Some calls to this aren't typesafe.
166 // This helps surface mistakes in tests.
167 throw new Error('appendChild() first argument is not an instance.');
168 }
169 appendChildToContainerOrInstance(parentInstance, child);
170 }
171
172 function insertInContainerOrInstanceBefore(
173 parentInstance: Container | Instance,
174 child: Instance | TextInstance,
175 beforeChild: Instance | TextInstance,
176 ): void {
177 const index = parentInstance.children.indexOf(child);
178 if (index !== -1) {
179 parentInstance.children.splice(index, 1);
180 }
181 const beforeIndex = parentInstance.children.indexOf(beforeChild);
182 if (beforeIndex === -1) {
183 throw new Error('This child does not exist.');
184 }
185 parentInstance.children.splice(beforeIndex, 0, child);
186 }
187
188 function insertInContainerBefore(
189 parentInstance: Container,
190 child: Instance | TextInstance,
191 beforeChild: Instance | TextInstance,
192 ) {
193 if (typeof parentInstance.rootID !== 'string') {
194 // Some calls to this aren't typesafe.
195 // This helps surface mistakes in tests.
196 throw new Error(
197 'insertInContainerBefore() first argument is not a container.',
198 );
199 }
200 insertInContainerOrInstanceBefore(parentInstance, child, beforeChild);
201 }
202
203 function insertBefore(
204 parentInstance: Instance,
205 child: Instance | TextInstance,
206 beforeChild: Instance | TextInstance,
207 ) {
208 if (typeof (parentInstance as any).rootID === 'string') {
209 // Some calls to this aren't typesafe.
210 // This helps surface mistakes in tests.
211 throw new Error('insertBefore() first argument is not an instance.');
212 }
213 insertInContainerOrInstanceBefore(parentInstance, child, beforeChild);
214 }
215
216 function clearContainer(container: Container): void {
217 container.children.splice(0);
218 }
219
220 function removeChildFromContainerOrInstance(
221 parentInstance: Container | Instance,
222 child: Instance | TextInstance,
223 ): void {
224 const index = parentInstance.children.indexOf(child);
225 if (index === -1) {
226 throw new Error('This child does not exist.');
227 }
228 parentInstance.children.splice(index, 1);
229 }
230
231 function removeChildFromContainer(
232 parentInstance: Container,
233 child: Instance | TextInstance,
234 ): void {
235 if (typeof parentInstance.rootID !== 'string') {
236 // Some calls to this aren't typesafe.
237 // This helps surface mistakes in tests.
238 throw new Error(
239 'removeChildFromContainer() first argument is not a container.',
240 );
241 }
242 removeChildFromContainerOrInstance(parentInstance, child);
243 }
244
245 function removeChild(
246 parentInstance: Instance,
247 child: Instance | TextInstance,
248 ): void {
249 if (typeof (parentInstance as any).rootID === 'string') {
250 // Some calls to this aren't typesafe.
251 // This helps surface mistakes in tests.
252 throw new Error('removeChild() first argument is not an instance.');
253 }
254 removeChildFromContainerOrInstance(parentInstance, child);
255 }
256
257 function cloneInstance(
258 instance: Instance,
259 type: string,
260 oldProps: Props,
261 newProps: Props,
262 keepChildren: boolean,
263 children: ?$ReadOnlyArray<Instance>,
264 ): Instance {
265 if (__DEV__) {
266 checkPropStringCoercion(newProps.children, 'children');
267 }
268 const clone: Instance = {
269 id: instance.id,
270 type: type,
271 parent: instance.parent,
272 children: keepChildren
273 ? instance.children
274 : // $FlowFixMe[incompatible-type] We're not typing immutable instances.
275 (children ?? []),
276 text: shouldSetTextContent(type, newProps)
277 ? computeText((newProps.children as any) + '', instance.context)
278 : null,
279 prop: newProps.prop,
280 hidden: !!newProps.hidden,
281 context: instance.context,
282 };
283
284 if (type === 'suspensey-thing' && typeof newProps.src === 'string') {
285 // $FlowFixMe[prop-missing]
286 clone.src = newProps.src;
287 }
288
289 Object.defineProperty(clone, 'id', {
290 value: clone.id,
291 enumerable: false,
292 });
293 Object.defineProperty(clone, 'parent', {
294 value: clone.parent,
295 enumerable: false,
296 });
297 Object.defineProperty(clone, 'text', {
298 value: clone.text,
299 enumerable: false,
300 });
301 Object.defineProperty(clone, 'context', {
302 value: clone.context,
303 enumerable: false,
304 });
305 hostCloneCounter++;
306 return clone;
307 }
308
309 function shouldSetTextContent(type: string, props: Props): boolean {
310 if (type === 'errorInBeginPhase') {
311 throw new Error('Error in host config.');
312 }
313 return (
314 typeof props.children === 'string' ||
315 typeof props.children === 'number' ||
316 typeof props.children === 'bigint'
317 );
318 }
319
320 function computeText(rawText: string, hostContext: HostContext) {
321 return hostContext === UPPERCASE_CONTEXT ? rawText.toUpperCase() : rawText;
322 }
323
324 type SuspenseyThingRecord = {
325 status: 'pending' | 'fulfilled',
326 subscriptions: Array<SuspenseyCommitSubscription> | null,
327 };
328
329 let suspenseyThingCache: Map<string, SuspenseyThingRecord> | null = null;
330
331 function startSuspendingCommit(): SuspendedState {
332 // Represents a subscription for all the suspensey things that block a
333 // particular commit. Once they've all loaded, the commit phase can proceed.
334 return {
335 pendingCount: 0,
336 commit: null,
337 };
338 }
339
340 function suspendInstance(
341 state: SuspendedState,
342 instance: Instance,
343 type: string,
344 props: Props,
345 ): void {
346 const src = props.src;
347 if (type === 'suspensey-thing' && typeof src === 'string') {
348 // Attach a listener to the suspensey thing and create a subscription
349 // object that uses reference counting to track when all the suspensey
350 // things have loaded.
351 // $FlowFixMe[incompatible-use] Still not nullable
352 const record = suspenseyThingCache.get(src);
353 if (record === undefined) {
354 throw new Error('Could not find record for key.');
355 }
356 if (record.status === 'fulfilled') {
357 // Already loaded.
358 } else if (record.status === 'pending') {
359 state.pendingCount++;
360 // Stash the subscription on the record. In `resolveSuspenseyThing`,
361 // we'll use this fire the commit once all the things have loaded.
362 if (record.subscriptions === null) {
363 record.subscriptions = [];
364 }
365 record.subscriptions.push(state);
366 }
367 } else {
368 throw new Error(
369 'Did not expect this host component to be visited when suspending ' +
370 'the commit. Did you check the SuspendCommit flag?',
371 );
372 }
373 }
374
375 function waitForCommitToBeReady(
376 state: SuspendedState,
377 timeoutOffset: number,
378 ): ((commit: () => void) => () => void) | null {
379 if (state.pendingCount > 0) {
380 return (commit: () => void) => {
381 state.commit = commit;
382 const cancelCommit = () => {
383 state.commit = null;
384 };
385 return cancelCommit;
386 };
387 }
388 return null;
389 }
390
391 const sharedHostConfig: HostConfig = {
392 rendererVersion: ReactVersion,
393 rendererPackageName: 'react-noop',
394
395 ...DefaultConfig,
396
397 extraDevToolsConfig: null,
398
399 getRootHostContext() {
400 return NO_CONTEXT;
401 },
402
403 getChildHostContext(parentHostContext: HostContext, type: string) {
404 if (type === 'offscreen') {
405 return parentHostContext;
406 }
407 if (type === 'uppercase') {
408 return UPPERCASE_CONTEXT;
409 }
410 return NO_CONTEXT;
411 },
412
413 getPublicInstance(instance: Instance): PublicInstance {
414 return instance as any;
415 },
416
417 HostTransitionContext: null,
418
419 createInstance(
420 type: string,
421 props: Props,
422 rootContainerInstance: Container,
423 hostContext: HostContext,
424 internalInstanceHandle: Object,
425 ): Instance {
426 if (type === 'errorInCompletePhase') {
427 throw new Error('Error in host config.');
428 }
429 if (__DEV__) {
430 // The `if` statement here prevents auto-disabling of the safe coercion
431 // ESLint rule, so we must manually disable it below.
432 if (shouldSetTextContent(type, props)) {
433 checkPropStringCoercion(props.children, 'children');
434 }
435 }
436 const inst: Instance = {
437 id: instanceCounter++,
438 type: type,
439 children: [],
440 parent: -1,
441 text: shouldSetTextContent(type, props)
442 ? // eslint-disable-next-line react-internal/safe-string-coercion
443 computeText((props.children as any) + '', hostContext)
444 : null,
445 prop: props.prop,
446 hidden: !!props.hidden,
447 context: hostContext,
448 };
449
450 if (type === 'suspensey-thing' && typeof props.src === 'string') {
451 // $FlowFixMe[prop-missing]
452 inst.src = props.src;
453 }
454
455 // Hide from unit tests
456 Object.defineProperty(inst, 'id', {value: inst.id, enumerable: false});
457 Object.defineProperty(inst, 'parent', {
458 value: inst.parent,
459 enumerable: false,
460 });
461 Object.defineProperty(inst, 'text', {
462 value: inst.text,
463 enumerable: false,
464 });
465 Object.defineProperty(inst, 'context', {
466 value: inst.context,
467 enumerable: false,
468 });
469 // $FlowFixMe[prop-missing]
470 Object.defineProperty(inst, 'fiber', {
471 value: internalInstanceHandle,
472 enumerable: false,
473 });
474 // $FlowFixMe[incompatible-return]
475 return inst;
476 },
477
478 appendInitialChild(
479 parentInstance: Instance,
480 child: Instance | TextInstance,
481 ): void {
482 const prevParent = child.parent;
483 if (prevParent !== -1 && prevParent !== parentInstance.id) {
484 throw new Error('Reparenting is not allowed');
485 }
486 child.parent = parentInstance.id;
487 parentInstance.children.push(child);
488 },
489
490 finalizeInitialChildren(
491 domElement: Instance,
492 type: string,
493 props: Props,
494 ): boolean {
495 return false;
496 },
497
498 shouldSetTextContent,
499
500 createTextInstance(
501 text: string,
502 rootContainerInstance: Container,
503 hostContext: Object,
504 internalInstanceHandle: Object,
505 ): TextInstance {
506 if (hostContext === UPPERCASE_CONTEXT) {
507 text = text.toUpperCase();
508 }
509 const inst = {
510 text: text,
511 id: instanceCounter++,
512 parent: -1,
513 hidden: false,
514 context: hostContext,
515 };
516 // Hide from unit tests
517 Object.defineProperty(inst, 'id', {value: inst.id, enumerable: false});
518 Object.defineProperty(inst, 'parent', {
519 value: inst.parent,
520 enumerable: false,
521 });
522 Object.defineProperty(inst, 'context', {
523 value: inst.context,
524 enumerable: false,
525 });
526 return inst;
527 },
528
529 createFragmentInstance(fragmentFiber: mixed) {
530 return null;
531 },
532
533 updateFragmentInstanceFiber(fragmentFiber: mixed, fragmentInstance: mixed) {
534 // Noop
535 },
536
537 commitNewChildToFragmentInstance(child: mixed, fragmentInstance: mixed) {
538 // Noop
539 },
540
541 deleteChildFromFragmentInstance(child: mixed, fragmentInstance: mixed) {
542 // Noop
543 },
544
545 scheduleTimeout: setTimeout,
546 cancelTimeout: clearTimeout,
547 noTimeout: -1,
548
549 supportsMicrotasks: true,
550 scheduleMicrotask:
551 typeof queueMicrotask === 'function'
552 ? queueMicrotask
553 : typeof Promise !== 'undefined'
554 ? (callback: () => void) =>
555 Promise.resolve(null)
556 .then(callback)
557 .catch(error => {
558 setTimeout(() => {
559 throw error;
560 });
561 })
562 : setTimeout,
563
564 prepareForCommit(): null | Object {
565 return null;
566 },
567
568 resetAfterCommit(): void {},
569
570 setCurrentUpdatePriority,
571 getCurrentUpdatePriority,
572
573 resolveUpdatePriority() {
574 if (currentUpdatePriority !== NoEventPriority) {
575 return currentUpdatePriority;
576 }
577 return currentEventPriority;
578 },
579
580 trackSchedulerEvent(): void {},
581
582 resolveEventType(): null | string {
583 return null;
584 },
585
586 resolveEventTimeStamp(): number {
587 return -1.1;
588 },
589
590 shouldAttemptEagerTransition(): boolean {
591 return false;
592 },
593
594 isPrimaryRenderer: true,
595 warnsIfNotActing: true,
596
597 getInstanceFromNode() {
598 throw new Error('Not yet implemented.');
599 },
600
601 beforeActiveInstanceBlur() {
602 // NO-OP
603 },
604
605 afterActiveInstanceBlur() {
606 // NO-OP
607 },
608
609 preparePortalMount() {
610 // NO-OP
611 },
612
613 detachDeletedInstance() {},
614
615 requestPostPaintCallback(callback: (time: number) => void) {
616 const endTime = Scheduler.unstable_now();
617 callback(endTime);
618 },
619
620 maySuspendCommit(type: string, props: Props): boolean {
621 // Asks whether it's possible for this combination of type and props
622 // to ever need to suspend. This is different from asking whether it's
623 // currently ready because even if it's ready now, it might get purged
624 // from the cache later.
625 return type === 'suspensey-thing' && typeof props.src === 'string';
626 },
627
628 maySuspendCommitOnUpdate(
629 type: string,
630 oldProps: Props,
631 newProps: Props,
632 ): boolean {
633 // Asks whether it's possible for this combination of type and props
634 // to ever need to suspend. This is different from asking whether it's
635 // currently ready because even if it's ready now, it might get purged
636 // from the cache later.
637 return (
638 type === 'suspensey-thing' &&
639 typeof newProps.src === 'string' &&
640 newProps.src !== oldProps.src
641 );
642 },
643
644 maySuspendCommitInSyncRender(type: string, props: Props): boolean {
645 return true;
646 },
647
648 preloadInstance(instance: Instance, type: string, props: Props): boolean {
649 if (type !== 'suspensey-thing' || typeof props.src !== 'string') {
650 throw new Error('Attempted to preload unexpected instance: ' + type);
651 }
652 const src = props.src;
653
654 // In addition to preloading an instance, this method asks whether the
655 // instance is ready to be committed. If it's not, React may yield to the
656 // main thread and ask again. It's possible a load event will fire in
657 // between, in which case we can avoid showing a fallback.
658 if (suspenseyThingCache === null) {
659 suspenseyThingCache = new Map();
660 }
661 const record = suspenseyThingCache.get(src);
662 if (record === undefined) {
663 const newRecord: SuspenseyThingRecord = {
664 status: 'pending',
665 subscriptions: null,
666 };
667 // $FlowFixMe[incompatible-use] Still not nullable
668 suspenseyThingCache.set(src, newRecord);
669 // $FlowFixMe[prop-missing]
670 const onLoadStart = props.onLoadStart;
671 if (typeof onLoadStart === 'function') {
672 onLoadStart();
673 }
674 return false;
675 } else {
676 return record.status === 'fulfilled';
677 }
678 },
679
680 startSuspendingCommit,
681 suspendInstance,
682
683 suspendOnActiveViewTransition(
684 state: SuspendedState,
685 container: Container,
686 ): void {
687 // Not implemented
688 },
689
690 waitForCommitToBeReady,
691
692 getSuspendedCommitReason(
693 state: SuspendedState,
694 rootContainer: Container,
695 ): null | string {
696 return null;
697 },
698
699 NotPendingTransition: null as TransitionStatus,
700
701 resetFormInstance(form: Instance) {},
702
703 bindToConsole(methodName: $FlowFixMe, args: Array<any>, badgeName: string) {
704 // $FlowFixMe[incompatible-type]
705 return Function.prototype.bind.apply(
706 // eslint-disable-next-line react-internal/no-production-logging
707 console[methodName],
708 [console].concat(args),
709 );
710 },
711 };
712
713 const mutationHostConfig: Pick<
714 HostConfig,
715 | $Keys<ReactFiberConfigWithNoMutation>
716 | $Keys<ReactFiberConfigWithNoViewTransition>,
717 > = {
718 supportsMutation: true,
719
720 cloneMutableInstance() {
721 // required for enableGestureTransition
722 throw new Error('Not yet implemented.');
723 },
724
725 cloneMutableTextInstance() {
726 // required for enableGestureTransition
727 throw new Error('Not yet implemented.');
728 },
729
730 commitMount(instance: Instance, type: string, newProps: Props): void {
731 // Noop
732 },
733
734 commitUpdate(
735 instance: Instance,
736 type: string,
737 oldProps: Props,
738 newProps: Props,
739 ): void {
740 // $FlowFixMe[invalid-compare]
741 if (oldProps === null) {
742 throw new Error('Should have old props');
743 }
744 hostUpdateCounter++;
745 instance.prop = newProps.prop;
746 instance.hidden = !!newProps.hidden;
747
748 if (type === 'suspensey-thing' && typeof newProps.src === 'string') {
749 // $FlowFixMe[prop-missing]
750 instance.src = newProps.src;
751 }
752
753 if (shouldSetTextContent(type, newProps)) {
754 if (__DEV__) {
755 checkPropStringCoercion(newProps.children, 'children');
756 }
757 instance.text = computeText(
758 (newProps.children as any) + '',
759 instance.context,
760 );
761 }
762 },
763
764 commitTextUpdate(
765 textInstance: TextInstance,
766 oldText: string,
767 newText: string,
768 ): void {
769 hostUpdateCounter++;
770 textInstance.text = computeText(newText, textInstance.context);
771 },
772
773 appendChild,
774 appendChildToContainer,
775 insertBefore,
776 insertInContainerBefore,
777 removeChild,
778 removeChildFromContainer,
779 clearContainer,
780
781 hideInstance(instance: Instance): void {
782 instance.hidden = true;
783 },
784
785 hideTextInstance(textInstance: TextInstance): void {
786 textInstance.hidden = true;
787 },
788
789 unhideInstance(instance: Instance, props: Props): void {
790 if (!props.hidden) {
791 instance.hidden = false;
792 }
793 },
794
795 unhideTextInstance(textInstance: TextInstance, text: string): void {
796 textInstance.hidden = false;
797 },
798
799 applyViewTransitionName(
800 instance: Instance,
801 name: string,
802 className: ?string,
803 ): void {},
804
805 restoreViewTransitionName(instance: Instance, props: Props): void {},
806
807 cancelViewTransitionName(
808 instance: Instance,
809 name: string,
810 props: Props,
811 ): void {},
812
813 cancelRootViewTransitionName(rootContainer: Container): void {},
814
815 restoreRootViewTransitionName(rootContainer: Container): void {},
816
817 cloneRootViewTransitionContainer(rootContainer: Container): Instance {
818 throw new Error('Not yet implemented.');
819 },
820
821 removeRootViewTransitionClone(
822 rootContainer: Container,
823 clone: Instance,
824 ): void {
825 throw new Error('Not implemented.');
826 },
827
828 measureInstance(instance: Instance): InstanceMeasurement {
829 return null;
830 },
831
832 measureClonedInstance(instance: Instance): InstanceMeasurement {
833 return null;
834 },
835
836 wasInstanceInViewport(measurement: InstanceMeasurement): boolean {
837 return true;
838 },
839
840 hasInstanceChanged(
841 oldMeasurement: InstanceMeasurement,
842 newMeasurement: InstanceMeasurement,
843 ): boolean {
844 return false;
845 },
846
847 hasInstanceAffectedParent(
848 oldMeasurement: InstanceMeasurement,
849 newMeasurement: InstanceMeasurement,
850 ): boolean {
851 return false;
852 },
853
854 startViewTransition(
855 rootContainer: Container,
856 transitionTypes: null | TransitionTypes,
857 mutationCallback: () => void,
858 layoutCallback: () => void,
859 afterMutationCallback: () => void,
860 spawnedWorkCallback: () => void,
861 passiveCallback: () => mixed,
862 errorCallback: mixed => void,
863 blockedCallback: string => void, // Profiling-only
864 finishedAnimation: () => void, // Profiling-only
865 ): null | RunningViewTransition {
866 mutationCallback();
867 layoutCallback();
868 // Skip afterMutationCallback(). We don't need it since we're not animating.
869 spawnedWorkCallback();
870 // Skip passiveCallback(). Spawned work will schedule a task.
871 return null;
872 },
873
874 startGestureTransition(
875 rootContainer: Container,
876 timeline: GestureTimeline,
877 rangeStart: number,
878 rangeEnd: number,
879 transitionTypes: null | TransitionTypes,
880 mutationCallback: () => void,
881 animateCallback: () => void,
882 errorCallback: mixed => void,
883 ): null | RunningViewTransition {
884 mutationCallback();
885 animateCallback();
886 return null;
887 },
888
889 stopViewTransition(transition: RunningViewTransition) {},
890
891 addViewTransitionFinishedListener(
892 transition: RunningViewTransition,
893 callback: () => void,
894 ) {
895 callback();
896 },
897
898 createViewTransitionInstance(name: string): ViewTransitionInstance {
899 return null;
900 },
901
902 getCurrentGestureOffset(provider: GestureTimeline): number {
903 return 0;
904 },
905
906 resetTextContent(instance: Instance): void {
907 instance.text = null;
908 },
909 };
910
911 const persistenceHostConfig: Pick<
912 HostConfig,
913 $Keys<ReactFiberConfigWithNoPersistence>,
914 > = {
915 supportsPersistence: true,
916
917 cloneInstance,
918
919 createContainerChildSet(): Array<Instance | TextInstance> {
920 return [];
921 },
922
923 appendChildToContainerChildSet(
924 childSet: Array<Instance | TextInstance>,
925 child: Instance | TextInstance,
926 ): void {
927 childSet.push(child);
928 },
929
930 finalizeContainerChildren(
931 container: Container,
932 newChildren: Array<Instance | TextInstance>,
933 ): void {
934 container.pendingChildren = newChildren;
935 if (
936 newChildren.length === 1 &&
937 newChildren[0].text === 'Error when completing root'
938 ) {
939 // Trigger an error for testing purposes
940 throw Error('Error when completing root');
941 }
942 },
943
944 replaceContainerChildren(
945 container: Container,
946 newChildren: Array<Instance | TextInstance>,
947 ): void {
948 container.children = newChildren;
949 },
950
951 cloneHiddenInstance(
952 instance: Instance,
953 type: string,
954 props: Props,
955 ): Instance {
956 const clone = cloneInstance(instance, type, props, props, true, null);
957 clone.hidden = true;
958 return clone;
959 },
960
961 cloneHiddenTextInstance(
962 instance: TextInstance,
963 text: string,
964 ): TextInstance {
965 const clone = {
966 text: instance.text,
967 id: instance.id,
968 parent: instance.parent,
969 hidden: true,
970 context: instance.context,
971 };
972 // Hide from unit tests
973 Object.defineProperty(clone, 'id', {
974 value: clone.id,
975 enumerable: false,
976 });
977 Object.defineProperty(clone, 'parent', {
978 value: clone.parent,
979 enumerable: false,
980 });
981 Object.defineProperty(clone, 'context', {
982 value: clone.context,
983 enumerable: false,
984 });
985 return clone;
986 },
987 };
988
989 const hostConfig: HostConfig = useMutation
990 ? {...sharedHostConfig, ...mutationHostConfig}
991 : {...sharedHostConfig, ...persistenceHostConfig};
992
993 const NoopRenderer = reconciler(hostConfig);
994
995 const rootContainers = new Map<string, Container>();
996 const roots = new Map<string, Object>();
997 const DEFAULT_ROOT_ID = '<default>';
998
999 let currentUpdatePriority = NoEventPriority;
1000 function setCurrentUpdatePriority(newPriority: EventPriority): void {
1001 currentUpdatePriority = newPriority;
1002 }
1003
1004 function getCurrentUpdatePriority(): EventPriority {
1005 return currentUpdatePriority;
1006 }
1007
1008 let currentEventPriority = DefaultEventPriority;
1009
1010 function createJSXElementForTestComparison(type: mixed, props: mixed) {
1011 if (__DEV__) {
1012 const element = {
1013 type: type,
1014 $$typeof: REACT_ELEMENT_TYPE,
1015 key: null,
1016 props: props,
1017 // $FlowFixMe[constant-condition]
1018 _owner: null,
1019 // $FlowFixMe[constant-condition]
1020 _store: __DEV__ ? {} : undefined,
1021 };
1022 // $FlowFixMe[prop-missing]
1023 Object.defineProperty(element, 'ref', {
1024 enumerable: false,
1025 value: null,
1026 });
1027 return element;
1028 } else {
1029 return {
1030 $$typeof: REACT_ELEMENT_TYPE,
1031 type: type,
1032 key: null,
1033 ref: null,
1034 props: props,
1035 };
1036 }
1037 }
1038
1039 function childToJSX(
1040 child: null | Instance | TextInstance | Array<Instance | TextInstance>,
1041 text: ?string,
1042 ): mixed {
1043 if (text !== null) {
1044 return text;
1045 }
1046 if (child === null) {
1047 return null;
1048 }
1049 if (typeof child === 'string') {
1050 return child;
1051 }
1052 if (isArray(child)) {
1053 if (child.length === 0) {
1054 return null;
1055 }
1056 if (child.length === 1) {
1057 return childToJSX(child[0], null);
1058 }
1059 const children = child.map(c => childToJSX(c, null));
1060 if (
1061 children.every(
1062 c =>
1063 typeof c === 'string' ||
1064 typeof c === 'number' ||
1065 typeof c === 'bigint',
1066 )
1067 ) {
1068 return children.join('');
1069 }
1070 return children;
1071 }
1072 if (isArray(child.children)) {
1073 // This is an instance.
1074 const instance: Instance = child as any;
1075 const children = childToJSX(instance.children, instance.text);
1076 const props = {prop: instance.prop} as any;
1077 if (instance.hidden) {
1078 props.hidden = true;
1079 }
1080 // $FlowFixMe[prop-missing]
1081 if (instance.src) {
1082 props.src = instance.src;
1083 }
1084 if (children !== null) {
1085 props.children = children;
1086 }
1087 return createJSXElementForTestComparison(instance.type, props);
1088 }
1089 // This is a text instance
1090 const textInstance: TextInstance = child as any;
1091 if (textInstance.hidden) {
1092 return '';
1093 }
1094 return textInstance.text;
1095 }
1096
1097 function getChildren(root: ?(Container | Instance)) {
1098 if (root) {
1099 return root.children;
1100 } else {
1101 return null;
1102 }
1103 }
1104
1105 function getPendingChildren(root: ?(Container | Instance)) {
1106 if (root) {
1107 return root.children;
1108 } else {
1109 return null;
1110 }
1111 }
1112
1113 function getChildrenAsJSX(root: ?(Container | Instance)) {
1114 const children = childToJSX(getChildren(root), null);
1115 if (children === null) {
1116 return null;
1117 }
1118 if (isArray(children)) {
1119 return createJSXElementForTestComparison(REACT_FRAGMENT_TYPE, {children});
1120 }
1121 return children;
1122 }
1123
1124 function getPendingChildrenAsJSX(root: ?(Container | Instance)) {
1125 const children = childToJSX(getChildren(root), null);
1126 if (children === null) {
1127 return null;
1128 }
1129 if (isArray(children)) {
1130 return createJSXElementForTestComparison(REACT_FRAGMENT_TYPE, {children});
1131 }
1132 return children;
1133 }
1134
1135 function flushSync<R>(fn: () => R): ?R {
1136 if (__DEV__) {
1137 if (NoopRenderer.isAlreadyRendering()) {
1138 console.error(
1139 'flushSync was called from inside a lifecycle method. React cannot ' +
1140 'flush when React is already rendering. Consider moving this call to ' +
1141 'a scheduler task or micro task.',
1142 );
1143 }
1144 }
1145 if (disableLegacyMode) {
1146 const previousTransition = ReactSharedInternals.T;
1147 const preivousEventPriority = currentEventPriority;
1148 try {
1149 // $FlowFixMe[constant-condition]
1150 ReactSharedInternals.T = null;
1151 currentEventPriority = DiscreteEventPriority;
1152 // $FlowFixMe[constant-condition]
1153 if (fn) {
1154 return fn();
1155 } else {
1156 return undefined;
1157 }
1158 } finally {
1159 ReactSharedInternals.T = previousTransition;
1160 currentEventPriority = preivousEventPriority;
1161 NoopRenderer.flushSyncWork();
1162 }
1163 } else {
1164 return NoopRenderer.flushSyncFromReconciler(fn);
1165 }
1166 }
1167
1168 function onRecoverableError(error: mixed): void {
1169 // eslint-disable-next-line react-internal/warning-args, react-internal/no-production-logging -- renderer is only used for testing.
1170 console.error(error);
1171 }
1172 function onDefaultTransitionIndicator(): void | (() => void) {}
1173
1174 // $FlowFixMe[recursive-definition]
1175 // $FlowFixMe[definition-cycle]
1176 let idCounter = 0;
1177
1178 // $FlowFixMe[definition-cycle]
1179 // $FlowFixMe[recursive-definition]
1180 const ReactNoop = {
1181 _Scheduler: Scheduler,
1182
1183 getChildren(rootID: string = DEFAULT_ROOT_ID) {
1184 throw new Error(
1185 'No longer supported due to bad performance when used with `expect()`. ' +
1186 'Use `ReactNoop.getChildrenAsJSX()` instead or, if you really need to, `dangerouslyGetChildren` after you carefully considered the warning in its JSDOC.',
1187 );
1188 },
1189
1190 getPendingChildren(rootID: string = DEFAULT_ROOT_ID) {
1191 throw new Error(
1192 'No longer supported due to bad performance when used with `expect()`. ' +
1193 'Use `ReactNoop.getPendingChildrenAsJSX()` instead or, if you really need to, `dangerouslyGetPendingChildren` after you carefully considered the warning in its JSDOC.',
1194 );
1195 },
1196
1197 /**
1198 * Prefer using `getChildrenAsJSX`.
1199 * Using the returned children in `.toEqual` has very poor performance on mismatch due to deep equality checking of fiber structures.
1200 * Make sure you deeply remove enumerable properties before passing it to `.toEqual`, or, better, use `getChildrenAsJSX` or `toMatchRenderedOutput`.
1201 */
1202 dangerouslyGetChildren(rootID: string = DEFAULT_ROOT_ID) {
1203 const container = rootContainers.get(rootID);
1204 return getChildren(container);
1205 },
1206
1207 /**
1208 * Prefer using `getPendingChildrenAsJSX`.
1209 * Using the returned children in `.toEqual` has very poor performance on mismatch due to deep equality checking of fiber structures.
1210 * Make sure you deeply remove enumerable properties before passing it to `.toEqual`, or, better, use `getChildrenAsJSX` or `toMatchRenderedOutput`.
1211 */
1212 dangerouslyGetPendingChildren(rootID: string = DEFAULT_ROOT_ID) {
1213 const container = rootContainers.get(rootID);
1214 return getPendingChildren(container);
1215 },
1216
1217 getOrCreateRootContainer(
1218 rootID: string = DEFAULT_ROOT_ID,
1219 tag: RootTag,
1220 ): Container {
1221 let root = roots.get(rootID);
1222 if (!root) {
1223 const container: Container = {
1224 rootID: rootID,
1225 pendingChildren: [],
1226 children: [],
1227 };
1228 // $FlowFixMe[incompatible-call]
1229 rootContainers.set(rootID, container);
1230 root = NoopRenderer.createContainer(
1231 // $FlowFixMe[incompatible-call] -- Discovered when typechecking noop-renderer
1232 container,
1233 // $FlowFixMe[incompatible-type]
1234 tag,
1235 null,
1236 // $FlowFixMe[incompatible-type] -- Discovered when typechecking noop-renderer
1237 null,
1238 false,
1239 '',
1240 NoopRenderer.defaultOnUncaughtError,
1241 NoopRenderer.defaultOnCaughtError,
1242 onRecoverableError,
1243 onDefaultTransitionIndicator,
1244 null,
1245 );
1246 roots.set(rootID, root);
1247 }
1248 return root.current.stateNode.containerInfo;
1249 },
1250
1251 // TODO: Replace ReactNoop.render with createRoot + root.render
1252 createRoot(options?: CreateRootOptions) {
1253 const container: Container = {
1254 rootID: '' + idCounter++,
1255 pendingChildren: [],
1256 children: [],
1257 };
1258 const fiberRoot = NoopRenderer.createContainer(
1259 // $FlowFixMe[incompatible-call]
1260 container,
1261 // $FlowFixMe[incompatible-type]
1262 ConcurrentRoot,
1263 null,
1264 // $FlowFixMe[incompatible-type]
1265 null,
1266 false,
1267 '',
1268 options && options.onUncaughtError
1269 ? options.onUncaughtError
1270 : NoopRenderer.defaultOnUncaughtError,
1271 options && options.onCaughtError
1272 ? options.onCaughtError
1273 : NoopRenderer.defaultOnCaughtError,
1274 onRecoverableError,
1275 options && options.onDefaultTransitionIndicator
1276 ? options.onDefaultTransitionIndicator
1277 : onDefaultTransitionIndicator,
1278 options && options.unstable_transitionCallbacks
1279 ? options.unstable_transitionCallbacks
1280 : null,
1281 );
1282 return {
1283 _Scheduler: Scheduler,
1284 render(children: ReactNodeList) {
1285 NoopRenderer.updateContainer(children, fiberRoot, null, null);
1286 },
1287 getChildren() {
1288 return getChildren(container);
1289 },
1290 getChildrenAsJSX() {
1291 return getChildrenAsJSX(container);
1292 },
1293 };
1294 },
1295
1296 createLegacyRoot() {
1297 if (disableLegacyMode) {
1298 throw new Error('createLegacyRoot: Unsupported Legacy Mode API.');
1299 }
1300
1301 const container: Container = {
1302 rootID: '' + idCounter++,
1303 pendingChildren: [],
1304 children: [],
1305 };
1306 const fiberRoot = NoopRenderer.createContainer(
1307 // $FlowFixMe[incompatible-call] -- TODO: Discovered when typechecking noop-renderer
1308 container,
1309 LegacyRoot,
1310 null,
1311 false,
1312 null,
1313 '',
1314 NoopRenderer.defaultOnUncaughtError,
1315 NoopRenderer.defaultOnCaughtError,
1316 onRecoverableError,
1317 onDefaultTransitionIndicator,
1318 null,
1319 );
1320 return {
1321 _Scheduler: Scheduler,
1322 render(children: ReactNodeList) {
1323 NoopRenderer.updateContainer(children, fiberRoot, null, null);
1324 },
1325 getChildren() {
1326 return getChildren(container);
1327 },
1328 getChildrenAsJSX() {
1329 return getChildrenAsJSX(container);
1330 },
1331 legacy: true,
1332 };
1333 },
1334
1335 getChildrenAsJSX(rootID: string = DEFAULT_ROOT_ID) {
1336 const container = rootContainers.get(rootID);
1337 return getChildrenAsJSX(container);
1338 },
1339
1340 getPendingChildrenAsJSX(rootID: string = DEFAULT_ROOT_ID) {
1341 const container = rootContainers.get(rootID);
1342 return getPendingChildrenAsJSX(container);
1343 },
1344
1345 getSuspenseyThingStatus(src: string): string | null {
1346 if (suspenseyThingCache === null) {
1347 return null;
1348 } else {
1349 const record = suspenseyThingCache.get(src);
1350 // $FlowFixMe[prop-missing]
1351 return record === undefined ? null : record.status;
1352 }
1353 },
1354
1355 resolveSuspenseyThing(key: string): void {
1356 if (suspenseyThingCache === null) {
1357 suspenseyThingCache = new Map();
1358 }
1359 const record = suspenseyThingCache.get(key);
1360 if (record === undefined) {
1361 const newRecord: SuspenseyThingRecord = {
1362 status: 'fulfilled',
1363 subscriptions: null,
1364 };
1365 // $FlowFixMe[incompatible-use] still non-nullable
1366 suspenseyThingCache.set(key, newRecord);
1367 } else {
1368 if (record.status === 'pending') {
1369 record.status = 'fulfilled';
1370 const subscriptions = record.subscriptions;
1371 if (subscriptions !== null) {
1372 record.subscriptions = null;
1373 for (let i = 0; i < subscriptions.length; i++) {
1374 const subscription = subscriptions[i];
1375 subscription.pendingCount--;
1376 if (subscription.pendingCount === 0) {
1377 const commit = subscription.commit;
1378 subscription.commit = null;
1379 if (commit === null) {
1380 throw new Error(
1381 'Expected commit to be a function. This is a bug in React.',
1382 );
1383 }
1384 commit();
1385 }
1386 }
1387 }
1388 }
1389 }
1390 },
1391
1392 resetSuspenseyThingCache() {
1393 suspenseyThingCache = null;
1394 },
1395
1396 createPortal(
1397 children: ReactNodeList,
1398 container: Container,
1399 key: ?string = null,
1400 ) {
1401 return NoopRenderer.createPortal(children, container, null, key);
1402 },
1403
1404 // Shortcut for testing a single root
1405 render(element: React$Element<any>, callback: ?Function): void {
1406 ReactNoop.renderToRootWithID(element, DEFAULT_ROOT_ID, callback);
1407 },
1408
1409 renderLegacySyncRoot(element: React$Element<any>, callback: ?Function) {
1410 if (disableLegacyMode) {
1411 throw new Error('createLegacyRoot: Unsupported Legacy Mode API.');
1412 }
1413 // $FlowFixMe[incompatible-type]
1414 const rootID = DEFAULT_ROOT_ID;
1415 const container = ReactNoop.getOrCreateRootContainer(rootID, LegacyRoot);
1416 const root = roots.get(container.rootID);
1417 // $FlowFixMe[incompatible-type]
1418 NoopRenderer.updateContainer(element, root, null, callback);
1419 },
1420
1421 renderToRootWithID(
1422 element: React$Element<any>,
1423 rootID: string,
1424 callback: ?Function,
1425 ) {
1426 const container = ReactNoop.getOrCreateRootContainer(
1427 rootID,
1428 // $FlowFixMe[incompatible-type]
1429 ConcurrentRoot,
1430 );
1431 const root = roots.get(container.rootID);
1432 // $FlowFixMe[incompatible-type]
1433 NoopRenderer.updateContainer(element, root, null, callback);
1434 },
1435
1436 unmountRootWithID(rootID: string) {
1437 const root = roots.get(rootID);
1438 if (root) {
1439 NoopRenderer.updateContainer(null, root, null, () => {
1440 roots.delete(rootID);
1441 rootContainers.delete(rootID);
1442 });
1443 }
1444 },
1445
1446 findInstance(
1447 componentOrElement: Element | ?component(...props: any),
1448 ): null | Instance | TextInstance {
1449 if (componentOrElement == null) {
1450 return null;
1451 }
1452 // Unsound duck typing.
1453 const component = componentOrElement as any;
1454 if (typeof component.id === 'number') {
1455 return component;
1456 }
1457 if (__DEV__) {
1458 return NoopRenderer.findHostInstanceWithWarning(
1459 component,
1460 'findInstance',
1461 );
1462 }
1463 return NoopRenderer.findHostInstance(component);
1464 },
1465
1466 flushNextYield(): Array<mixed> {
1467 Scheduler.unstable_flushNumberOfYields(1);
1468 return Scheduler.unstable_clearLog();
1469 },
1470
1471 startTrackingHostCounters(): void {
1472 hostUpdateCounter = 0;
1473 hostCloneCounter = 0;
1474 },
1475
1476 stopTrackingHostCounters():
1477 | {
1478 hostUpdateCounter: number,
1479 }
1480 | {
1481 hostCloneCounter: number,
1482 } {
1483 const result = useMutation
1484 ? {
1485 hostUpdateCounter,
1486 }
1487 : {
1488 hostCloneCounter,
1489 };
1490 hostUpdateCounter = 0;
1491 hostCloneCounter = 0;
1492
1493 return result;
1494 },
1495
1496 expire: Scheduler.unstable_advanceTime,
1497
1498 flushExpired(): Array<mixed> {
1499 return Scheduler.unstable_flushExpired();
1500 },
1501
1502 unstable_runWithPriority: function runWithPriority<T>(
1503 priority: EventPriority,
1504 fn: () => T,
1505 ): T {
1506 const previousPriority = getCurrentUpdatePriority();
1507 try {
1508 setCurrentUpdatePriority(priority);
1509 return fn();
1510 } finally {
1511 setCurrentUpdatePriority(previousPriority);
1512 }
1513 },
1514
1515 batchedUpdates: NoopRenderer.batchedUpdates,
1516
1517 deferredUpdates: NoopRenderer.deferredUpdates,
1518
1519 discreteUpdates: NoopRenderer.discreteUpdates,
1520
1521 idleUpdates<T>(fn: () => T): void {
1522 const prevEventPriority = currentEventPriority;
1523 currentEventPriority = IdleEventPriority;
1524 try {
1525 fn();
1526 } finally {
1527 currentEventPriority = prevEventPriority;
1528 }
1529 },
1530
1531 flushSync,
1532 flushPassiveEffects: NoopRenderer.flushPassiveEffects,
1533
1534 // Logs the current state of the tree.
1535 dumpTree(rootID: string = DEFAULT_ROOT_ID) {
1536 const root = roots.get(rootID);
1537 const rootContainer = rootContainers.get(rootID);
1538 if (!root || !rootContainer) {
1539 // eslint-disable-next-line react-internal/no-production-logging
1540 console.log('Nothing rendered yet.');
1541 return;
1542 }
1543
1544 const bufferedLog: string[] = [];
1545 function log(...args: string[]) {
1546 bufferedLog.push(...args, '\n');
1547 }
1548
1549 function logHostInstances(
1550 children: Array<Instance | TextInstance>,
1551 depth: number,
1552 ) {
1553 for (let i = 0; i < children.length; i++) {
1554 const child = children[i];
1555 const indent = ' '.repeat(depth);
1556 if (typeof child.text === 'string') {
1557 log(indent + '- ' + child.text);
1558 } else {
1559 // $FlowFixMe[unsafe-addition]
1560 log(indent + '- ' + child.type + '#' + child.id);
1561
1562 // $FlowFixMe[incompatible-type]
1563 logHostInstances(
1564 // $FlowFixMe[incompatible-type]
1565 child.children,
1566 depth + 1,
1567 );
1568 }
1569 }
1570 }
1571 function logContainer(container: Container, depth: number) {
1572 log(' '.repeat(depth) + '- [root#' + container.rootID + ']');
1573 logHostInstances(container.children, depth + 1);
1574 }
1575
1576 function logUpdateQueue(updateQueue: UpdateQueue<mixed>, depth: number) {
1577 log(' '.repeat(depth + 1) + 'QUEUED UPDATES');
1578 const first = updateQueue.firstBaseUpdate;
1579 const update = first;
1580 if (update !== null) {
1581 do {
1582 // $FlowFixMe[unsafe-addition]
1583 // $FlowFixMe[prop-missing]
1584 log(
1585 ' '.repeat(depth + 1) + '~',
1586 // $FlowFixMe[invalid-compare]
1587 // $FlowFixMe[prop-missing]
1588 // $FlowFixMe[unsafe-addition]
1589 '[' + update.expirationTime + ']',
1590 );
1591 // $FlowFixMe[invalid-compare]
1592 } while (update !== null);
1593 }
1594
1595 const lastPending = updateQueue.shared.pending;
1596 if (lastPending !== null) {
1597 const firstPending = lastPending.next;
1598 const pendingUpdate = firstPending;
1599 if (pendingUpdate !== null) {
1600 // $FlowFixMe[unsafe-addition]
1601 // $FlowFixMe[prop-missing]
1602 do {
1603 log(
1604 // $FlowFixMe[invalid-compare]
1605 ' '.repeat(depth + 1) + '~',
1606 // $FlowFixMe[prop-missing]
1607 // $FlowFixMe[unsafe-addition]
1608 '[' + pendingUpdate.expirationTime + ']',
1609 );
1610 // $FlowFixMe[invalid-compare]
1611 } while (pendingUpdate !== null && pendingUpdate !== firstPending);
1612 }
1613 }
1614 }
1615
1616 function logFiber(fiber: Fiber, depth: number) {
1617 log(
1618 ' '.repeat(depth) +
1619 '- ' +
1620 // need to explicitly coerce Symbol to a string
1621 (fiber.type ? fiber.type.name || fiber.type.toString() : '[root]'),
1622 // $FlowFixMe[unsafe-addition]
1623 '[' +
1624 // $FlowFixMe[prop-missing]
1625 fiber.childExpirationTime +
1626 (fiber.pendingProps ? '*' : '') +
1627 // $FlowFixMe[incompatible-type]
1628 ']',
1629 );
1630 if (fiber.updateQueue) {
1631 logUpdateQueue(
1632 // $FlowFixMe[incompatible-type]
1633 fiber.updateQueue,
1634 depth,
1635 );
1636 }
1637 // const childInProgress = fiber.progressedChild;
1638 // if (childInProgress && childInProgress !== fiber.child) {
1639 // log(
1640 // ' '.repeat(depth + 1) + 'IN PROGRESS: ' + fiber.pendingWorkPriority,
1641 // );
1642 // logFiber(childInProgress, depth + 1);
1643 // if (fiber.child) {
1644 // log(' '.repeat(depth + 1) + 'CURRENT');
1645 // }
1646 // } else if (fiber.child && fiber.updateQueue) {
1647 // log(' '.repeat(depth + 1) + 'CHILDREN');
1648 // }
1649 if (fiber.child) {
1650 logFiber(fiber.child, depth + 1);
1651 }
1652 if (fiber.sibling) {
1653 logFiber(fiber.sibling, depth);
1654 }
1655 }
1656
1657 log('HOST INSTANCES:');
1658 logContainer(rootContainer, 0);
1659 log('FIBERS:');
1660 logFiber(root.current, 0);
1661
1662 // eslint-disable-next-line react-internal/no-production-logging
1663 console.log(...bufferedLog);
1664 },
1665
1666 getRoot(rootID: string = DEFAULT_ROOT_ID) {
1667 return roots.get(rootID);
1668 },
1669 };
1670
1671 return ReactNoop;
1672 }
1673
1674 export default createReactNoop;