Split ReactNoop into normal and persistent exports (#12793)
* Copy-paste ReactNoop into ReactNoopPersistent * Split ReactNoop into normal and persistent exports * ReactNoopShared -> createReactNoop
Dan Abramov committed
May 14, 2018 at 13:57 UTC
0470854f5522dde521d46049bcac894b2e86d280
8 files changed
+671
-590
packages/react-noop-renderer/npm/persistent.js
new
+7
@@ -0,0 +1,7 @@
1
+'use strict';
2
+
3
+if (process.env.NODE_ENV === 'production') {
4
+ module.exports = require('./cjs/react-noop-renderer-persistent.production.min.js');
5
+} else {
6
+ module.exports = require('./cjs/react-noop-renderer-persistent.development.js');
7
+}
packages/react-noop-renderer/package.json
+1
@@ -20,6 +20,7 @@
20
"LICENSE",
21
"README.md",
22
"index.js",
23
+ "persistent.js",
24
"cjs/"
25
]
26
}
packages/react-noop-renderer/persistent.js
new
+18
@@ -0,0 +1,18 @@
1
+/**
2
+ * Copyright (c) 2013-present, Facebook, Inc.
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
+'use strict';
11
+
12
+const ReactNoopPersistent = require('./src/ReactNoopPersistent');
13
+
14
+// TODO: decide on the top-level export form.
15
+// This is hacky but makes it work with both Rollup and Jest.
16
+module.exports = ReactNoopPersistent.default
17
+ ? ReactNoopPersistent.default
18
+ : ReactNoopPersistent;
packages/react-noop-renderer/src/ReactNoop.js
+4
-575
@@ -14,581 +14,10 @@
14
* environment.
15
*/
16
17
-import type {Fiber} from 'react-reconciler/src/ReactFiber';
18
-import type {UpdateQueue} from 'react-reconciler/src/ReactUpdateQueue';
19
-import type {ReactNodeList} from 'shared/ReactTypes';
20
-import ReactFiberReconciler from 'react-reconciler';
21
-import {enablePersistentReconciler} from 'shared/ReactFeatureFlags';
22
-import * as ReactPortal from 'shared/ReactPortal';
23
-import emptyObject from 'fbjs/lib/emptyObject';
24
-import expect from 'expect';
17
+import createReactNoop from './createReactNoop';
18
26
-const UPDATE_SIGNAL = {};
27
-
28
-let scheduledCallback = null;
29
-
30
-type Container = {rootID: string, children: Array<Instance | TextInstance>};
31
-type Props = {prop: any, hidden?: boolean};
32
-type Instance = {|
33
- type: string,
34
- id: number,
35
- children: Array<Instance | TextInstance>,
36
- prop: any,
37
-|};
38
-type TextInstance = {|text: string, id: number|};
39
-
40
-let instanceCounter = 0;
41
-let failInBeginPhase = false;
42
-
43
-function appendChild(
44
- parentInstance: Instance | Container,
45
- child: Instance | TextInstance,
46
-): void {
47
- const index = parentInstance.children.indexOf(child);
48
- if (index !== -1) {
49
- parentInstance.children.splice(index, 1);
50
- }
51
- parentInstance.children.push(child);
52
-}
53
-
54
-function insertBefore(
55
- parentInstance: Instance | Container,
56
- child: Instance | TextInstance,
57
- beforeChild: Instance | TextInstance,
58
-): void {
59
- const index = parentInstance.children.indexOf(child);
60
- if (index !== -1) {
61
- parentInstance.children.splice(index, 1);
62
- }
63
- const beforeIndex = parentInstance.children.indexOf(beforeChild);
64
- if (beforeIndex === -1) {
65
- throw new Error('This child does not exist.');
66
- }
67
- parentInstance.children.splice(beforeIndex, 0, child);
68
-}
69
-
70
-function removeChild(
71
- parentInstance: Instance | Container,
72
- child: Instance | TextInstance,
73
-): void {
74
- const index = parentInstance.children.indexOf(child);
75
- if (index === -1) {
76
- throw new Error('This child does not exist.');
77
- }
78
- parentInstance.children.splice(index, 1);
79
-}
80
-
81
-let elapsedTimeInMs = 0;
82
-
83
-let SharedHostConfig = {
84
- getRootHostContext() {
85
- if (failInBeginPhase) {
86
- throw new Error('Error in host config.');
87
- }
88
- return emptyObject;
89
- },
90
-
91
- getChildHostContext() {
92
- return emptyObject;
93
- },
94
-
95
- getPublicInstance(instance) {
96
- return instance;
97
- },
98
-
99
- createInstance(type: string, props: Props): Instance {
100
- const inst = {
101
- id: instanceCounter++,
102
- type: type,
103
- children: [],
104
- prop: props.prop,
105
- };
106
- // Hide from unit tests
107
- Object.defineProperty(inst, 'id', {value: inst.id, enumerable: false});
108
- return inst;
109
- },
110
-
111
- appendInitialChild(
112
- parentInstance: Instance,
113
- child: Instance | TextInstance,
114
- ): void {
115
- parentInstance.children.push(child);
116
- },
117
-
118
- finalizeInitialChildren(
119
- domElement: Instance,
120
- type: string,
121
- props: Props,
122
- ): boolean {
123
- return false;
124
- },
125
-
126
- prepareUpdate(
127
- instance: Instance,
128
- type: string,
129
- oldProps: Props,
130
- newProps: Props,
131
- ): null | {} {
132
- if (oldProps === null) {
133
- throw new Error('Should have old props');
134
- }
135
- if (newProps === null) {
136
- throw new Error('Should have new props');
137
- }
138
- return UPDATE_SIGNAL;
139
- },
140
-
141
- shouldSetTextContent(type: string, props: Props): boolean {
142
- return (
143
- typeof props.children === 'string' || typeof props.children === 'number'
144
- );
145
- },
146
-
147
- shouldDeprioritizeSubtree(type: string, props: Props): boolean {
148
- return !!props.hidden;
149
- },
150
-
151
- createTextInstance(
152
- text: string,
153
- rootContainerInstance: Container,
154
- hostContext: Object,
155
- internalInstanceHandle: Object,
156
- ): TextInstance {
157
- const inst = {text: text, id: instanceCounter++};
158
- // Hide from unit tests
159
- Object.defineProperty(inst, 'id', {value: inst.id, enumerable: false});
160
- return inst;
161
- },
162
-
163
- scheduleDeferredCallback(callback) {
164
- if (scheduledCallback) {
165
- throw new Error(
166
- 'Scheduling a callback twice is excessive. Instead, keep track of ' +
167
- 'whether the callback has already been scheduled.',
168
- );
169
- }
170
- scheduledCallback = callback;
171
- return 0;
172
- },
173
-
174
- cancelDeferredCallback() {
175
- if (scheduledCallback === null) {
176
- throw new Error('No callback is scheduled.');
177
- }
178
- scheduledCallback = null;
179
- },
180
-
181
- prepareForCommit(): void {},
182
-
183
- resetAfterCommit(): void {},
184
-
185
- now(): number {
186
- return elapsedTimeInMs;
187
- },
188
-
189
- isPrimaryRenderer: true,
190
-};
191
-
192
-const NoopRenderer = ReactFiberReconciler({
193
- ...SharedHostConfig,
194
- mutation: {
195
- commitMount(instance: Instance, type: string, newProps: Props): void {
196
- // Noop
197
- },
198
-
199
- commitUpdate(
200
- instance: Instance,
201
- updatePayload: Object,
202
- type: string,
203
- oldProps: Props,
204
- newProps: Props,
205
- ): void {
206
- if (oldProps === null) {
207
- throw new Error('Should have old props');
208
- }
209
- instance.prop = newProps.prop;
210
- },
211
-
212
- commitTextUpdate(
213
- textInstance: TextInstance,
214
- oldText: string,
215
- newText: string,
216
- ): void {
217
- textInstance.text = newText;
218
- },
219
-
220
- appendChild: appendChild,
221
- appendChildToContainer: appendChild,
222
- insertBefore: insertBefore,
223
- insertInContainerBefore: insertBefore,
224
- removeChild: removeChild,
225
- removeChildFromContainer: removeChild,
226
-
227
- resetTextContent(instance: Instance): void {},
228
- },
229
-});
230
-
231
-const PersistentNoopRenderer = enablePersistentReconciler
232
- ? ReactFiberReconciler({
233
- ...SharedHostConfig,
234
- persistence: {
235
- cloneInstance(
236
- instance: Instance,
237
- updatePayload: null | Object,
238
- type: string,
239
- oldProps: Props,
240
- newProps: Props,
241
- internalInstanceHandle: Object,
242
- keepChildren: boolean,
243
- recyclableInstance: null | Instance,
244
- ): Instance {
245
- const clone = {
246
- id: instance.id,
247
- type: type,
248
- children: keepChildren ? instance.children : [],
249
- prop: newProps.prop,
250
- };
251
- Object.defineProperty(clone, 'id', {
252
- value: clone.id,
253
- enumerable: false,
254
- });
255
- return clone;
256
- },
257
-
258
- createContainerChildSet(
259
- container: Container,
260
- ): Array<Instance | TextInstance> {
261
- return [];
262
- },
263
-
264
- appendChildToContainerChildSet(
265
- childSet: Array<Instance | TextInstance>,
266
- child: Instance | TextInstance,
267
- ): void {
268
- childSet.push(child);
269
- },
270
-
271
- finalizeContainerChildren(
272
- container: Container,
273
- newChildren: Array<Instance | TextInstance>,
274
- ): void {},
275
-
276
- replaceContainerChildren(
277
- container: Container,
278
- newChildren: Array<Instance | TextInstance>,
279
- ): void {
280
- container.children = newChildren;
281
- },
282
- },
283
- })
284
- : null;
285
-
286
-const rootContainers = new Map();
287
-const roots = new Map();
288
-const persistentRoots = new Map();
289
-const DEFAULT_ROOT_ID = '<default>';
290
-
291
-let yieldedValues = null;
292
-
293
-let unitsRemaining;
294
-
295
-function* flushUnitsOfWork(n: number): Generator<Array<mixed>, void, void> {
296
- let didStop = false;
297
- while (!didStop && scheduledCallback !== null) {
298
- let cb = scheduledCallback;
299
- scheduledCallback = null;
300
- unitsRemaining = n;
301
- cb({
302
- timeRemaining() {
303
- if (yieldedValues !== null) {
304
- return 0;
305
- }
306
- if (unitsRemaining-- > 0) {
307
- return 999;
308
- }
309
- didStop = true;
310
- return 0;
311
- },
312
- // React's scheduler has its own way of keeping track of expired
313
- // work and doesn't read this, so don't bother setting it to the
314
- // correct value.
315
- didTimeout: false,
316
- });
317
-
318
- if (yieldedValues !== null) {
319
- const values = yieldedValues;
320
- yieldedValues = null;
321
- yield values;
322
- }
323
- }
324
-}
325
-
326
-const ReactNoop = {
327
- getChildren(rootID: string = DEFAULT_ROOT_ID) {
328
- const container = rootContainers.get(rootID);
329
- if (container) {
330
- return container.children;
331
- } else {
332
- return null;
333
- }
334
- },
335
-
336
- createPortal(
337
- children: ReactNodeList,
338
- container: Container,
339
- key: ?string = null,
340
- ) {
341
- return ReactPortal.createPortal(children, container, null, key);
342
- },
343
-
344
- // Shortcut for testing a single root
345
- render(element: React$Element<any>, callback: ?Function) {
346
- ReactNoop.renderToRootWithID(element, DEFAULT_ROOT_ID, callback);
347
- },
348
-
349
- renderToRootWithID(
350
- element: React$Element<any>,
351
- rootID: string,
352
- callback: ?Function,
353
- ) {
354
- let root = roots.get(rootID);
355
- if (!root) {
356
- const container = {rootID: rootID, children: []};
357
- rootContainers.set(rootID, container);
358
- root = NoopRenderer.createContainer(container, true, false);
359
- roots.set(rootID, root);
360
- }
361
- NoopRenderer.updateContainer(element, root, null, callback);
362
- },
363
-
364
- renderToPersistentRootWithID(
365
- element: React$Element<any>,
366
- rootID: string,
367
- callback: ?Function,
368
- ) {
369
- if (PersistentNoopRenderer === null) {
370
- throw new Error(
371
- 'Enable ReactFeatureFlags.enablePersistentReconciler to use it in tests.',
372
- );
373
- }
374
- let root = persistentRoots.get(rootID);
375
- if (!root) {
376
- const container = {rootID: rootID, children: []};
377
- rootContainers.set(rootID, container);
378
- root = PersistentNoopRenderer.createContainer(container, true, false);
379
- persistentRoots.set(rootID, root);
380
- }
381
- PersistentNoopRenderer.updateContainer(element, root, null, callback);
382
- },
383
-
384
- unmountRootWithID(rootID: string) {
385
- const root = roots.get(rootID);
386
- if (root) {
387
- NoopRenderer.updateContainer(null, root, null, () => {
388
- roots.delete(rootID);
389
- rootContainers.delete(rootID);
390
- });
391
- }
392
- },
393
-
394
- findInstance(
395
- componentOrElement: Element | ?React$Component<any, any>,
396
- ): null | Instance | TextInstance {
397
- if (componentOrElement == null) {
398
- return null;
399
- }
400
- // Unsound duck typing.
401
- const component = (componentOrElement: any);
402
- if (typeof component.id === 'number') {
403
- return component;
404
- }
405
- return NoopRenderer.findHostInstance(component);
406
- },
407
-
408
- flushDeferredPri(timeout: number = Infinity): Array<mixed> {
409
- // The legacy version of this function decremented the timeout before
410
- // returning the new time.
411
- // TODO: Convert tests to use flushUnitsOfWork or flushAndYield instead.
412
- const n = timeout / 5 - 1;
413
-
414
- let values = [];
415
- // eslint-disable-next-line no-for-of-loops/no-for-of-loops
416
- for (const value of flushUnitsOfWork(n)) {
417
- values.push(...value);
418
- }
419
- return values;
420
- },
421
-
422
- flush(): Array<mixed> {
423
- return ReactNoop.flushUnitsOfWork(Infinity);
424
- },
425
-
426
- flushAndYield(
427
- unitsOfWork: number = Infinity,
428
- ): Generator<Array<mixed>, void, void> {
429
- return flushUnitsOfWork(unitsOfWork);
430
- },
431
-
432
- flushUnitsOfWork(n: number): Array<mixed> {
433
- let values = yieldedValues || [];
434
- yieldedValues = null;
435
- // eslint-disable-next-line no-for-of-loops/no-for-of-loops
436
- for (const value of flushUnitsOfWork(n)) {
437
- values.push(...value);
438
- }
439
- return values;
440
- },
441
-
442
- flushThrough(expected: Array<mixed>): void {
443
- let actual = [];
444
- if (expected.length !== 0) {
445
- // eslint-disable-next-line no-for-of-loops/no-for-of-loops
446
- for (const value of flushUnitsOfWork(Infinity)) {
447
- actual.push(...value);
448
- if (actual.length >= expected.length) {
449
- break;
450
- }
451
- }
452
- }
453
- expect(actual).toEqual(expected);
454
- },
455
-
456
- expire(ms: number): void {
457
- elapsedTimeInMs += ms;
458
- },
459
-
460
- flushExpired(): Array<mixed> {
461
- return ReactNoop.flushUnitsOfWork(0);
462
- },
463
-
464
- yield(value: mixed) {
465
- if (yieldedValues === null) {
466
- yieldedValues = [value];
467
- } else {
468
- yieldedValues.push(value);
469
- }
470
- },
471
-
472
- clearYields() {
473
- const values = yieldedValues;
474
- yieldedValues = null;
475
- return values;
476
- },
477
-
478
- hasScheduledCallback() {
479
- return !!scheduledCallback;
480
- },
481
-
482
- batchedUpdates: NoopRenderer.batchedUpdates,
483
-
484
- deferredUpdates: NoopRenderer.deferredUpdates,
485
-
486
- unbatchedUpdates: NoopRenderer.unbatchedUpdates,
487
-
488
- interactiveUpdates: NoopRenderer.interactiveUpdates,
489
-
490
- flushSync(fn: () => mixed) {
491
- yieldedValues = [];
492
- NoopRenderer.flushSync(fn);
493
- return yieldedValues;
494
- },
495
-
496
- // Logs the current state of the tree.
497
- dumpTree(rootID: string = DEFAULT_ROOT_ID) {
498
- const root = roots.get(rootID);
499
- const rootContainer = rootContainers.get(rootID);
500
- if (!root || !rootContainer) {
501
- console.log('Nothing rendered yet.');
502
- return;
503
- }
504
-
505
- let bufferedLog = [];
506
- function log(...args) {
507
- bufferedLog.push(...args, '\n');
508
- }
509
-
510
- function logHostInstances(children: Array<Instance | TextInstance>, depth) {
511
- for (let i = 0; i < children.length; i++) {
512
- const child = children[i];
513
- const indent = ' '.repeat(depth);
514
- if (typeof child.text === 'string') {
515
- log(indent + '- ' + child.text);
516
- } else {
517
- // $FlowFixMe - The child should've been refined now.
518
- log(indent + '- ' + child.type + '#' + child.id);
519
- // $FlowFixMe - The child should've been refined now.
520
- logHostInstances(child.children, depth + 1);
521
- }
522
- }
523
- }
524
- function logContainer(container: Container, depth) {
525
- log(' '.repeat(depth) + '- [root#' + container.rootID + ']');
526
- logHostInstances(container.children, depth + 1);
527
- }
528
-
529
- function logUpdateQueue(updateQueue: UpdateQueue<mixed>, depth) {
530
- log(' '.repeat(depth + 1) + 'QUEUED UPDATES');
531
- const firstUpdate = updateQueue.firstUpdate;
532
- if (!firstUpdate) {
533
- return;
534
- }
535
-
536
- log(' '.repeat(depth + 1) + '~', '[' + firstUpdate.expirationTime + ']');
537
- while (firstUpdate.next) {
538
- log(
539
- ' '.repeat(depth + 1) + '~',
540
- '[' + firstUpdate.expirationTime + ']',
541
- );
542
- }
543
- }
544
-
545
- function logFiber(fiber: Fiber, depth) {
546
- log(
547
- ' '.repeat(depth) +
548
- '- ' +
549
- // need to explicitly coerce Symbol to a string
550
- (fiber.type ? fiber.type.name || fiber.type.toString() : '[root]'),
551
- '[' + fiber.expirationTime + (fiber.pendingProps ? '*' : '') + ']',
552
- );
553
- if (fiber.updateQueue) {
554
- logUpdateQueue(fiber.updateQueue, depth);
555
- }
556
- // const childInProgress = fiber.progressedChild;
557
- // if (childInProgress && childInProgress !== fiber.child) {
558
- // log(
559
- // ' '.repeat(depth + 1) + 'IN PROGRESS: ' + fiber.pendingWorkPriority,
560
- // );
561
- // logFiber(childInProgress, depth + 1);
562
- // if (fiber.child) {
563
- // log(' '.repeat(depth + 1) + 'CURRENT');
564
- // }
565
- // } else if (fiber.child && fiber.updateQueue) {
566
- // log(' '.repeat(depth + 1) + 'CHILDREN');
567
- // }
568
- if (fiber.child) {
569
- logFiber(fiber.child, depth + 1);
570
- }
571
- if (fiber.sibling) {
572
- logFiber(fiber.sibling, depth);
573
- }
574
- }
575
-
576
- log('HOST INSTANCES:');
577
- logContainer(rootContainer, 0);
578
- log('FIBERS:');
579
- logFiber(root.current, 0);
580
-
581
- console.log(...bufferedLog);
582
- },
583
-
584
- simulateErrorInHostConfig(fn: () => void) {
585
- failInBeginPhase = true;
586
- try {
587
- fn();
588
- } finally {
589
- failInBeginPhase = false;
590
- }
591
- },
592
-};
19
+const ReactNoop = createReactNoop(
20
+ true, // useMutation
21
+);
22
23
export default ReactNoop;
packages/react-noop-renderer/src/ReactNoopPersistent.js
new
+23
@@ -0,0 +1,23 @@
1
+/**
2
+ * Copyright (c) 2013-present, Facebook, Inc.
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 createReactNoop from './createReactNoop';
18
+
19
+const ReactNoopPersistent = createReactNoop(
20
+ false, // useMutation
21
+);
22
+
23
+export default ReactNoopPersistent;
packages/react-noop-renderer/src/createReactNoop.js
new
+582
@@ -0,0 +1,582 @@
1
+/**
2
+ * Copyright (c) 2013-present, Facebook, Inc.
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 {Fiber} from 'react-reconciler/src/ReactFiber';
18
+import type {UpdateQueue} from 'react-reconciler/src/ReactUpdateQueue';
19
+import type {ReactNodeList} from 'shared/ReactTypes';
20
+
21
+import ReactFiberReconciler from 'react-reconciler';
22
+import * as ReactPortal from 'shared/ReactPortal';
23
+import emptyObject from 'fbjs/lib/emptyObject';
24
+import expect from 'expect';
25
+
26
+type Container = {rootID: string, children: Array<Instance | TextInstance>};
27
+type Props = {prop: any, hidden?: boolean};
28
+type Instance = {|
29
+ type: string,
30
+ id: number,
31
+ children: Array<Instance | TextInstance>,
32
+ prop: any,
33
+|};
34
+type TextInstance = {|text: string, id: number|};
35
+
36
+function createReactNoop(useMutation: boolean) {
37
+ const UPDATE_SIGNAL = {};
38
+ let scheduledCallback = null;
39
+
40
+ let instanceCounter = 0;
41
+ let failInBeginPhase = false;
42
+
43
+ function appendChild(
44
+ parentInstance: Instance | Container,
45
+ child: Instance | TextInstance,
46
+ ): void {
47
+ const index = parentInstance.children.indexOf(child);
48
+ if (index !== -1) {
49
+ parentInstance.children.splice(index, 1);
50
+ }
51
+ parentInstance.children.push(child);
52
+ }
53
+
54
+ function insertBefore(
55
+ parentInstance: Instance | Container,
56
+ child: Instance | TextInstance,
57
+ beforeChild: Instance | TextInstance,
58
+ ): void {
59
+ const index = parentInstance.children.indexOf(child);
60
+ if (index !== -1) {
61
+ parentInstance.children.splice(index, 1);
62
+ }
63
+ const beforeIndex = parentInstance.children.indexOf(beforeChild);
64
+ if (beforeIndex === -1) {
65
+ throw new Error('This child does not exist.');
66
+ }
67
+ parentInstance.children.splice(beforeIndex, 0, child);
68
+ }
69
+
70
+ function removeChild(
71
+ parentInstance: Instance | Container,
72
+ child: Instance | TextInstance,
73
+ ): void {
74
+ const index = parentInstance.children.indexOf(child);
75
+ if (index === -1) {
76
+ throw new Error('This child does not exist.');
77
+ }
78
+ parentInstance.children.splice(index, 1);
79
+ }
80
+
81
+ let elapsedTimeInMs = 0;
82
+
83
+ const sharedHostConfig = {
84
+ getRootHostContext() {
85
+ if (failInBeginPhase) {
86
+ throw new Error('Error in host config.');
87
+ }
88
+ return emptyObject;
89
+ },
90
+
91
+ getChildHostContext() {
92
+ return emptyObject;
93
+ },
94
+
95
+ getPublicInstance(instance) {
96
+ return instance;
97
+ },
98
+
99
+ createInstance(type: string, props: Props): Instance {
100
+ const inst = {
101
+ id: instanceCounter++,
102
+ type: type,
103
+ children: [],
104
+ prop: props.prop,
105
+ };
106
+ // Hide from unit tests
107
+ Object.defineProperty(inst, 'id', {value: inst.id, enumerable: false});
108
+ return inst;
109
+ },
110
+
111
+ appendInitialChild(
112
+ parentInstance: Instance,
113
+ child: Instance | TextInstance,
114
+ ): void {
115
+ parentInstance.children.push(child);
116
+ },
117
+
118
+ finalizeInitialChildren(
119
+ domElement: Instance,
120
+ type: string,
121
+ props: Props,
122
+ ): boolean {
123
+ return false;
124
+ },
125
+
126
+ prepareUpdate(
127
+ instance: Instance,
128
+ type: string,
129
+ oldProps: Props,
130
+ newProps: Props,
131
+ ): null | {} {
132
+ if (oldProps === null) {
133
+ throw new Error('Should have old props');
134
+ }
135
+ if (newProps === null) {
136
+ throw new Error('Should have new props');
137
+ }
138
+ return UPDATE_SIGNAL;
139
+ },
140
+
141
+ shouldSetTextContent(type: string, props: Props): boolean {
142
+ return (
143
+ typeof props.children === 'string' || typeof props.children === 'number'
144
+ );
145
+ },
146
+
147
+ shouldDeprioritizeSubtree(type: string, props: Props): boolean {
148
+ return !!props.hidden;
149
+ },
150
+
151
+ createTextInstance(
152
+ text: string,
153
+ rootContainerInstance: Container,
154
+ hostContext: Object,
155
+ internalInstanceHandle: Object,
156
+ ): TextInstance {
157
+ const inst = {text: text, id: instanceCounter++};
158
+ // Hide from unit tests
159
+ Object.defineProperty(inst, 'id', {value: inst.id, enumerable: false});
160
+ return inst;
161
+ },
162
+
163
+ scheduleDeferredCallback(callback) {
164
+ if (scheduledCallback) {
165
+ throw new Error(
166
+ 'Scheduling a callback twice is excessive. Instead, keep track of ' +
167
+ 'whether the callback has already been scheduled.',
168
+ );
169
+ }
170
+ scheduledCallback = callback;
171
+ return 0;
172
+ },
173
+
174
+ cancelDeferredCallback() {
175
+ if (scheduledCallback === null) {
176
+ throw new Error('No callback is scheduled.');
177
+ }
178
+ scheduledCallback = null;
179
+ },
180
+
181
+ prepareForCommit(): void {},
182
+
183
+ resetAfterCommit(): void {},
184
+
185
+ now(): number {
186
+ return elapsedTimeInMs;
187
+ },
188
+
189
+ isPrimaryRenderer: true,
190
+ };
191
+
192
+ const hostConfig = useMutation
193
+ ? {
194
+ ...sharedHostConfig,
195
+ mutation: {
196
+ commitMount(instance: Instance, type: string, newProps: Props): void {
197
+ // Noop
198
+ },
199
+
200
+ commitUpdate(
201
+ instance: Instance,
202
+ updatePayload: Object,
203
+ type: string,
204
+ oldProps: Props,
205
+ newProps: Props,
206
+ ): void {
207
+ if (oldProps === null) {
208
+ throw new Error('Should have old props');
209
+ }
210
+ instance.prop = newProps.prop;
211
+ },
212
+
213
+ commitTextUpdate(
214
+ textInstance: TextInstance,
215
+ oldText: string,
216
+ newText: string,
217
+ ): void {
218
+ textInstance.text = newText;
219
+ },
220
+
221
+ appendChild: appendChild,
222
+ appendChildToContainer: appendChild,
223
+ insertBefore: insertBefore,
224
+ insertInContainerBefore: insertBefore,
225
+ removeChild: removeChild,
226
+ removeChildFromContainer: removeChild,
227
+
228
+ resetTextContent(instance: Instance): void {},
229
+ },
230
+ }
231
+ : {
232
+ ...sharedHostConfig,
233
+ persistence: {
234
+ cloneInstance(
235
+ instance: Instance,
236
+ updatePayload: null | Object,
237
+ type: string,
238
+ oldProps: Props,
239
+ newProps: Props,
240
+ internalInstanceHandle: Object,
241
+ keepChildren: boolean,
242
+ recyclableInstance: null | Instance,
243
+ ): Instance {
244
+ const clone = {
245
+ id: instance.id,
246
+ type: type,
247
+ children: keepChildren ? instance.children : [],
248
+ prop: newProps.prop,
249
+ };
250
+ Object.defineProperty(clone, 'id', {
251
+ value: clone.id,
252
+ enumerable: false,
253
+ });
254
+ return clone;
255
+ },
256
+
257
+ createContainerChildSet(
258
+ container: Container,
259
+ ): Array<Instance | TextInstance> {
260
+ return [];
261
+ },
262
+
263
+ appendChildToContainerChildSet(
264
+ childSet: Array<Instance | TextInstance>,
265
+ child: Instance | TextInstance,
266
+ ): void {
267
+ childSet.push(child);
268
+ },
269
+
270
+ finalizeContainerChildren(
271
+ container: Container,
272
+ newChildren: Array<Instance | TextInstance>,
273
+ ): void {},
274
+
275
+ replaceContainerChildren(
276
+ container: Container,
277
+ newChildren: Array<Instance | TextInstance>,
278
+ ): void {
279
+ container.children = newChildren;
280
+ },
281
+ },
282
+ };
283
+
284
+ const NoopRenderer = ReactFiberReconciler(hostConfig);
285
+
286
+ const rootContainers = new Map();
287
+ const roots = new Map();
288
+ const DEFAULT_ROOT_ID = '<default>';
289
+
290
+ let yieldedValues = null;
291
+
292
+ let unitsRemaining;
293
+
294
+ function* flushUnitsOfWork(n: number): Generator<Array<mixed>, void, void> {
295
+ let didStop = false;
296
+ while (!didStop && scheduledCallback !== null) {
297
+ let cb = scheduledCallback;
298
+ scheduledCallback = null;
299
+ unitsRemaining = n;
300
+ cb({
301
+ timeRemaining() {
302
+ if (yieldedValues !== null) {
303
+ return 0;
304
+ }
305
+ if (unitsRemaining-- > 0) {
306
+ return 999;
307
+ }
308
+ didStop = true;
309
+ return 0;
310
+ },
311
+ // React's scheduler has its own way of keeping track of expired
312
+ // work and doesn't read this, so don't bother setting it to the
313
+ // correct value.
314
+ didTimeout: false,
315
+ });
316
+
317
+ if (yieldedValues !== null) {
318
+ const values = yieldedValues;
319
+ yieldedValues = null;
320
+ yield values;
321
+ }
322
+ }
323
+ }
324
+
325
+ const ReactNoop = {
326
+ getChildren(rootID: string = DEFAULT_ROOT_ID) {
327
+ const container = rootContainers.get(rootID);
328
+ if (container) {
329
+ return container.children;
330
+ } else {
331
+ return null;
332
+ }
333
+ },
334
+
335
+ createPortal(
336
+ children: ReactNodeList,
337
+ container: Container,
338
+ key: ?string = null,
339
+ ) {
340
+ return ReactPortal.createPortal(children, container, null, key);
341
+ },
342
+
343
+ // Shortcut for testing a single root
344
+ render(element: React$Element<any>, callback: ?Function) {
345
+ ReactNoop.renderToRootWithID(element, DEFAULT_ROOT_ID, callback);
346
+ },
347
+
348
+ renderToRootWithID(
349
+ element: React$Element<any>,
350
+ rootID: string,
351
+ callback: ?Function,
352
+ ) {
353
+ let root = roots.get(rootID);
354
+ if (!root) {
355
+ const container = {rootID: rootID, children: []};
356
+ rootContainers.set(rootID, container);
357
+ root = NoopRenderer.createContainer(container, true, false);
358
+ roots.set(rootID, root);
359
+ }
360
+ NoopRenderer.updateContainer(element, root, null, callback);
361
+ },
362
+
363
+ unmountRootWithID(rootID: string) {
364
+ const root = roots.get(rootID);
365
+ if (root) {
366
+ NoopRenderer.updateContainer(null, root, null, () => {
367
+ roots.delete(rootID);
368
+ rootContainers.delete(rootID);
369
+ });
370
+ }
371
+ },
372
+
373
+ findInstance(
374
+ componentOrElement: Element | ?React$Component<any, any>,
375
+ ): null | Instance | TextInstance {
376
+ if (componentOrElement == null) {
377
+ return null;
378
+ }
379
+ // Unsound duck typing.
380
+ const component = (componentOrElement: any);
381
+ if (typeof component.id === 'number') {
382
+ return component;
383
+ }
384
+ return NoopRenderer.findHostInstance(component);
385
+ },
386
+
387
+ flushDeferredPri(timeout: number = Infinity): Array<mixed> {
388
+ // The legacy version of this function decremented the timeout before
389
+ // returning the new time.
390
+ // TODO: Convert tests to use flushUnitsOfWork or flushAndYield instead.
391
+ const n = timeout / 5 - 1;
392
+
393
+ let values = [];
394
+ // eslint-disable-next-line no-for-of-loops/no-for-of-loops
395
+ for (const value of flushUnitsOfWork(n)) {
396
+ values.push(...value);
397
+ }
398
+ return values;
399
+ },
400
+
401
+ flush(): Array<mixed> {
402
+ return ReactNoop.flushUnitsOfWork(Infinity);
403
+ },
404
+
405
+ flushAndYield(
406
+ unitsOfWork: number = Infinity,
407
+ ): Generator<Array<mixed>, void, void> {
408
+ return flushUnitsOfWork(unitsOfWork);
409
+ },
410
+
411
+ flushUnitsOfWork(n: number): Array<mixed> {
412
+ let values = yieldedValues || [];
413
+ yieldedValues = null;
414
+ // eslint-disable-next-line no-for-of-loops/no-for-of-loops
415
+ for (const value of flushUnitsOfWork(n)) {
416
+ values.push(...value);
417
+ }
418
+ return values;
419
+ },
420
+
421
+ flushThrough(expected: Array<mixed>): void {
422
+ let actual = [];
423
+ if (expected.length !== 0) {
424
+ // eslint-disable-next-line no-for-of-loops/no-for-of-loops
425
+ for (const value of flushUnitsOfWork(Infinity)) {
426
+ actual.push(...value);
427
+ if (actual.length >= expected.length) {
428
+ break;
429
+ }
430
+ }
431
+ }
432
+ expect(actual).toEqual(expected);
433
+ },
434
+
435
+ expire(ms: number): void {
436
+ elapsedTimeInMs += ms;
437
+ },
438
+
439
+ flushExpired(): Array<mixed> {
440
+ return ReactNoop.flushUnitsOfWork(0);
441
+ },
442
+
443
+ yield(value: mixed) {
444
+ if (yieldedValues === null) {
445
+ yieldedValues = [value];
446
+ } else {
447
+ yieldedValues.push(value);
448
+ }
449
+ },
450
+
451
+ clearYields() {
452
+ const values = yieldedValues;
453
+ yieldedValues = null;
454
+ return values;
455
+ },
456
+
457
+ hasScheduledCallback() {
458
+ return !!scheduledCallback;
459
+ },
460
+
461
+ batchedUpdates: NoopRenderer.batchedUpdates,
462
+
463
+ deferredUpdates: NoopRenderer.deferredUpdates,
464
+
465
+ unbatchedUpdates: NoopRenderer.unbatchedUpdates,
466
+
467
+ interactiveUpdates: NoopRenderer.interactiveUpdates,
468
+
469
+ flushSync(fn: () => mixed) {
470
+ yieldedValues = [];
471
+ NoopRenderer.flushSync(fn);
472
+ return yieldedValues;
473
+ },
474
+
475
+ // Logs the current state of the tree.
476
+ dumpTree(rootID: string = DEFAULT_ROOT_ID) {
477
+ const root = roots.get(rootID);
478
+ const rootContainer = rootContainers.get(rootID);
479
+ if (!root || !rootContainer) {
480
+ console.log('Nothing rendered yet.');
481
+ return;
482
+ }
483
+
484
+ let bufferedLog = [];
485
+ function log(...args) {
486
+ bufferedLog.push(...args, '\n');
487
+ }
488
+
489
+ function logHostInstances(
490
+ children: Array<Instance | TextInstance>,
491
+ depth,
492
+ ) {
493
+ for (let i = 0; i < children.length; i++) {
494
+ const child = children[i];
495
+ const indent = ' '.repeat(depth);
496
+ if (typeof child.text === 'string') {
497
+ log(indent + '- ' + child.text);
498
+ } else {
499
+ // $FlowFixMe - The child should've been refined now.
500
+ log(indent + '- ' + child.type + '#' + child.id);
501
+ // $FlowFixMe - The child should've been refined now.
502
+ logHostInstances(child.children, depth + 1);
503
+ }
504
+ }
505
+ }
506
+ function logContainer(container: Container, depth) {
507
+ log(' '.repeat(depth) + '- [root#' + container.rootID + ']');
508
+ logHostInstances(container.children, depth + 1);
509
+ }
510
+
511
+ function logUpdateQueue(updateQueue: UpdateQueue<mixed>, depth) {
512
+ log(' '.repeat(depth + 1) + 'QUEUED UPDATES');
513
+ const firstUpdate = updateQueue.firstUpdate;
514
+ if (!firstUpdate) {
515
+ return;
516
+ }
517
+
518
+ log(
519
+ ' '.repeat(depth + 1) + '~',
520
+ '[' + firstUpdate.expirationTime + ']',
521
+ );
522
+ while (firstUpdate.next) {
523
+ log(
524
+ ' '.repeat(depth + 1) + '~',
525
+ '[' + firstUpdate.expirationTime + ']',
526
+ );
527
+ }
528
+ }
529
+
530
+ function logFiber(fiber: Fiber, depth) {
531
+ log(
532
+ ' '.repeat(depth) +
533
+ '- ' +
534
+ // need to explicitly coerce Symbol to a string
535
+ (fiber.type ? fiber.type.name || fiber.type.toString() : '[root]'),
536
+ '[' + fiber.expirationTime + (fiber.pendingProps ? '*' : '') + ']',
537
+ );
538
+ if (fiber.updateQueue) {
539
+ logUpdateQueue(fiber.updateQueue, depth);
540
+ }
541
+ // const childInProgress = fiber.progressedChild;
542
+ // if (childInProgress && childInProgress !== fiber.child) {
543
+ // log(
544
+ // ' '.repeat(depth + 1) + 'IN PROGRESS: ' + fiber.pendingWorkPriority,
545
+ // );
546
+ // logFiber(childInProgress, depth + 1);
547
+ // if (fiber.child) {
548
+ // log(' '.repeat(depth + 1) + 'CURRENT');
549
+ // }
550
+ // } else if (fiber.child && fiber.updateQueue) {
551
+ // log(' '.repeat(depth + 1) + 'CHILDREN');
552
+ // }
553
+ if (fiber.child) {
554
+ logFiber(fiber.child, depth + 1);
555
+ }
556
+ if (fiber.sibling) {
557
+ logFiber(fiber.sibling, depth);
558
+ }
559
+ }
560
+
561
+ log('HOST INSTANCES:');
562
+ logContainer(rootContainer, 0);
563
+ log('FIBERS:');
564
+ logFiber(root.current, 0);
565
+
566
+ console.log(...bufferedLog);
567
+ },
568
+
569
+ simulateErrorInHostConfig(fn: () => void) {
570
+ failInBeginPhase = true;
571
+ try {
572
+ fn();
573
+ } finally {
574
+ failInBeginPhase = false;
575
+ }
576
+ },
577
+ };
578
+
579
+ return ReactNoop;
580
+}
581
+
582
+export default createReactNoop;
packages/react-reconciler/src/__tests__/ReactPersistent-test.internal.js
+13
-15
@@ -11,7 +11,7 @@
11
'use strict';
12
13
let React;
14
-let ReactNoop;
14
+let ReactNoopPersistent;
15
let ReactPortal;
16
17
describe('ReactPersistent', () => {
@@ -24,14 +24,12 @@ describe('ReactPersistent', () => {
24
ReactFeatureFlags.enableNoopReconciler = false;
25
26
React = require('react');
27
- ReactNoop = require('react-noop-renderer');
27
+ ReactNoopPersistent = require('react-noop-renderer/persistent');
28
ReactPortal = require('shared/ReactPortal');
29
});
30
31
- const DEFAULT_ROOT_ID = 'persistent-test';
32
-
31
function render(element) {
34
- ReactNoop.renderToPersistentRootWithID(element, DEFAULT_ROOT_ID);
32
+ ReactNoopPersistent.render(element);
33
}
34
35
function div(...children) {
@@ -44,7 +42,7 @@ describe('ReactPersistent', () => {
42
}
43
44
function getChildren() {
47
- return ReactNoop.getChildren(DEFAULT_ROOT_ID);
45
+ return ReactNoopPersistent.getChildren();
46
}
47
48
it('can update child nodes of a host instance', () => {
@@ -62,12 +60,12 @@ describe('ReactPersistent', () => {
60
}
61
62
render(<Foo text="Hello" />);
65
- ReactNoop.flush();
63
+ ReactNoopPersistent.flush();
64
const originalChildren = getChildren();
65
expect(originalChildren).toEqual([div(span())]);
66
67
render(<Foo text="World" />);
70
- ReactNoop.flush();
68
+ ReactNoopPersistent.flush();
69
const newChildren = getChildren();
70
expect(newChildren).toEqual([div(span(), span())]);
71
@@ -96,12 +94,12 @@ describe('ReactPersistent', () => {
94
}
95
96
render(<Foo text="Hello" />);
99
- ReactNoop.flush();
97
+ ReactNoopPersistent.flush();
98
const originalChildren = getChildren();
99
expect(originalChildren).toEqual([div(span('Hello'))]);
100
101
render(<Foo text="World" />);
104
- ReactNoop.flush();
102
+ ReactNoopPersistent.flush();
103
const newChildren = getChildren();
104
expect(newChildren).toEqual([div(span('Hello'), span('World'))]);
105
@@ -122,12 +120,12 @@ describe('ReactPersistent', () => {
120
}
121
122
render(<Foo text="Hello" />);
125
- ReactNoop.flush();
123
+ ReactNoopPersistent.flush();
124
const originalChildren = getChildren();
125
expect(originalChildren).toEqual([div('Hello', span())]);
126
127
render(<Foo text="World" />);
130
- ReactNoop.flush();
128
+ ReactNoopPersistent.flush();
129
const newChildren = getChildren();
130
expect(newChildren).toEqual([div('World', span())]);
131
@@ -167,7 +165,7 @@ describe('ReactPersistent', () => {
165
{ReactPortal.createPortal(<Child />, portalContainer, null)}
166
</Parent>,
167
);
170
- ReactNoop.flush();
168
+ ReactNoopPersistent.flush();
169
170
expect(emptyPortalChildSet).toEqual([]);
171
@@ -185,7 +183,7 @@ describe('ReactPersistent', () => {
183
)}
184
</Parent>,
185
);
188
- ReactNoop.flush();
186
+ ReactNoopPersistent.flush();
187
188
const newChildren = getChildren();
189
expect(newChildren).toEqual([div()]);
@@ -202,7 +200,7 @@ describe('ReactPersistent', () => {
200
201
// Deleting the Portal, should clear its children
202
render(<Parent />);
205
- ReactNoop.flush();
203
+ ReactNoopPersistent.flush();
204
205
const clearedPortalChildren = portalContainer.children;
206
expect(clearedPortalChildren).toEqual([]);
scripts/rollup/bundles.js
+23
@@ -275,6 +275,29 @@ const bundles = [
275
}),
276
},
277
278
+ /******* React Noop Persistent Renderer (used for tests) *******/
279
+ {
280
+ label: 'noop-persistent',
281
+ bundleTypes: [NODE_DEV, NODE_PROD],
282
+ moduleType: RENDERER,
283
+ entry: 'react-noop-renderer/persistent',
284
+ global: 'ReactNoopRendererPersistent',
285
+ externals: ['react', 'expect'],
286
+ // React Noop uses generators. However GCC currently
287
+ // breaks when we attempt to use them in the output.
288
+ // So we precompile them with regenerator, and include
289
+ // it as a runtime dependency of React Noop. In practice
290
+ // this isn't an issue because React Noop is only used
291
+ // in our tests. We wouldn't want to do this for any
292
+ // public package though.
293
+ babel: opts =>
294
+ Object.assign({}, opts, {
295
+ plugins: opts.plugins.concat([
296
+ require.resolve('babel-plugin-transform-regenerator'),
297
+ ]),
298
+ }),
299
+ },
300
+
301
/******* React Reconciler *******/
302
{
303
label: 'react-reconciler',