@reckona/mreact-compat 0.0.138 → 0.0.140

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/dom-props.d.ts.map +1 -1
  2. package/dist/dom-props.js +111 -44
  3. package/dist/dom-props.js.map +1 -1
  4. package/dist/element.d.ts +3 -0
  5. package/dist/element.d.ts.map +1 -1
  6. package/dist/element.js +32 -2
  7. package/dist/element.js.map +1 -1
  8. package/dist/event-listeners.d.ts +2 -4
  9. package/dist/event-listeners.d.ts.map +1 -1
  10. package/dist/event-listeners.js +2 -1
  11. package/dist/event-listeners.js.map +1 -1
  12. package/dist/fiber-child.d.ts.map +1 -1
  13. package/dist/fiber-child.js +44 -0
  14. package/dist/fiber-child.js.map +1 -1
  15. package/dist/fiber-commit.d.ts.map +1 -1
  16. package/dist/fiber-commit.js +70 -0
  17. package/dist/fiber-commit.js.map +1 -1
  18. package/dist/hooks.d.ts +4 -2
  19. package/dist/hooks.d.ts.map +1 -1
  20. package/dist/hooks.js +64 -6
  21. package/dist/hooks.js.map +1 -1
  22. package/dist/host-reconciler.d.ts.map +1 -1
  23. package/dist/host-reconciler.js +2 -2
  24. package/dist/host-reconciler.js.map +1 -1
  25. package/dist/jsx-runtime.d.ts.map +1 -1
  26. package/dist/jsx-runtime.js +2 -11
  27. package/dist/jsx-runtime.js.map +1 -1
  28. package/dist/reconciler.d.ts.map +1 -1
  29. package/dist/reconciler.js +2 -2
  30. package/dist/reconciler.js.map +1 -1
  31. package/dist/server-render.d.ts.map +1 -1
  32. package/dist/server-render.js +29 -4
  33. package/dist/server-render.js.map +1 -1
  34. package/dist/url-safety.d.ts +1 -1
  35. package/dist/url-safety.d.ts.map +1 -1
  36. package/dist/url-safety.js +1 -1
  37. package/dist/url-safety.js.map +1 -1
  38. package/package.json +3 -3
  39. package/src/dom-props.ts +189 -49
  40. package/src/element.ts +45 -1
  41. package/src/event-listeners.ts +4 -5
  42. package/src/fiber-child.ts +67 -0
  43. package/src/fiber-commit.ts +92 -0
  44. package/src/hooks.ts +88 -8
  45. package/src/host-reconciler.ts +2 -3
  46. package/src/jsx-runtime.ts +2 -18
  47. package/src/reconciler.ts +2 -3
  48. package/src/server-render.ts +43 -4
  49. package/src/url-safety.ts +1 -0
package/src/dom-props.ts CHANGED
@@ -1,17 +1,16 @@
1
1
  import {
2
2
  getAppliedProps,
3
3
  setAppliedProps,
4
- type AppliedEventListener,
5
- type AppliedProps,
6
4
  ensureDelegatedEventListener,
7
5
  toEventNames,
8
6
  } from "./host-event-binder.js";
9
7
  import { HOST_OWN_PROPS_META } from "./element.js";
10
8
  import { reportRecoverable, type RenderOptions } from "./hydration.js";
11
- import type { SyntheticEvent } from "./event-types.js";
12
9
  import {
13
10
  isDangerousHtmlAttribute,
14
11
  isDangerousHtmlOptIn,
12
+ isSrcsetAttribute,
13
+ isUnsafeMetaRefreshContent,
15
14
  isUnsafeUrlAttribute,
16
15
  isUrlAttribute,
17
16
  } from "./url-safety.js";
@@ -29,28 +28,30 @@ export function applyProps(
29
28
  ): void {
30
29
  const preserveHydrationAttributes = options.preserveHydrationAttributes === true;
31
30
  const previous = getAppliedProps(element);
31
+ const nextProps = sanitizeMetaRefreshElementProps(element, props);
32
32
 
33
33
  if (previous === undefined && !preserveHydrationAttributes) {
34
- if (applyInitialRowProps(element, props)) {
35
- setAppliedProps(element, { props });
34
+ if (applyInitialRowProps(element, nextProps)) {
35
+ setAppliedProps(element, {
36
+ props: nextProps,
37
+ });
36
38
  return;
37
39
  }
38
40
 
41
+ applyInitialProps(element, nextProps, path, options);
39
42
  setAppliedProps(element, {
40
- props,
41
- ...applyInitialProps(element, props, path, options),
43
+ props: nextProps,
42
44
  });
43
45
  return;
44
46
  }
45
47
 
46
48
  const previousProps = previous?.props ?? {};
47
- let listeners = previous?.listeners;
48
- const previousAttributeNames = collectAttributeNames(previousProps);
49
- const nextAttributeNames = collectAttributeNames(props);
49
+ const previousAttributeNames = previous?.attributeNames ?? collectAttributeNames(previousProps);
50
+ const nextAttributeNames = collectAttributeNames(nextProps);
50
51
 
51
52
  if (!preserveHydrationAttributes) {
52
53
  for (const attributeName of previousAttributeNames) {
53
- if (!nextAttributeNames.has(attributeName)) {
54
+ if (!nextAttributeNames.includes(attributeName)) {
54
55
  if (attributeName === "style") {
55
56
  removePreviousStyle(element, previousProps.style, path, options);
56
57
  continue;
@@ -69,22 +70,18 @@ export function applyProps(
69
70
  }
70
71
  }
71
72
 
72
- if (listeners !== undefined) {
73
- for (const [name, appliedListener] of listeners) {
74
- const nextValue = props[name];
75
-
76
- if (nextValue !== appliedListener.handler) {
77
- listeners.delete(name);
78
- }
73
+ for (const name in nextProps) {
74
+ if (!Object.prototype.hasOwnProperty.call(nextProps, name)) {
75
+ continue;
79
76
  }
80
- }
81
77
 
82
- for (const [name, value] of Object.entries(props)) {
78
+ const value = nextProps[name];
79
+
83
80
  if (name === "children" || name === "ref" || name === "key") {
84
81
  continue;
85
82
  }
86
83
 
87
- if (applyFormValueProp(element, name, value, path, options)) {
84
+ if (isFormValuePropName(name) && applyFormValueProp(element, name, value, path, options)) {
88
85
  continue;
89
86
  }
90
87
 
@@ -103,17 +100,18 @@ export function applyProps(
103
100
  continue;
104
101
  }
105
102
 
106
- if (/^on[A-Z]/.test(name) && typeof value === "function") {
107
- if (listeners?.get(name)?.handler === value) {
103
+ if (isReactEventHandlerPropName(name) && typeof value === "function") {
104
+ if (previousProps[name] === value) {
108
105
  continue;
109
106
  }
110
107
 
111
- const handler = value as (event: SyntheticEvent) => void;
112
108
  for (const eventName of toEventNames(name)) {
113
109
  ensureDelegatedEventListener(options.eventRoot ?? element, eventName);
114
110
  }
115
- listeners ??= new Map<string, AppliedEventListener>();
116
- listeners.set(name, { handler });
111
+ continue;
112
+ }
113
+
114
+ if (isEventLikePropName(name)) {
117
115
  continue;
118
116
  }
119
117
 
@@ -163,8 +161,8 @@ export function applyProps(
163
161
  }
164
162
 
165
163
  setAppliedProps(element, {
166
- props,
167
- ...(listeners === undefined ? {} : { listeners }),
164
+ attributeNames: nextAttributeNames,
165
+ props: nextProps,
168
166
  });
169
167
  }
170
168
 
@@ -173,8 +171,8 @@ function applyInitialProps(
173
171
  props: Record<string, unknown>,
174
172
  path: string,
175
173
  options: RenderOptions,
176
- ): Pick<AppliedProps, "listeners"> | {} {
177
- let listeners: Map<string, AppliedEventListener> | undefined;
174
+ ): void {
175
+ const useAttributeFastPath = options.hydration === undefined;
178
176
 
179
177
  for (const name in props) {
180
178
  if (!Object.prototype.hasOwnProperty.call(props, name)) {
@@ -187,7 +185,7 @@ function applyInitialProps(
187
185
  continue;
188
186
  }
189
187
 
190
- if (applyFormValueProp(element, name, value, path, options)) {
188
+ if (isFormValuePropName(name) && applyFormValueProp(element, name, value, path, options)) {
191
189
  continue;
192
190
  }
193
191
 
@@ -196,12 +194,26 @@ function applyInitialProps(
196
194
  }
197
195
 
198
196
  if (name === "className") {
199
- applyAttribute(element, "class", value, path, options);
197
+ applyInitialOrHydrationAttribute(
198
+ element,
199
+ "class",
200
+ value,
201
+ path,
202
+ options,
203
+ useAttributeFastPath,
204
+ );
200
205
  continue;
201
206
  }
202
207
 
203
208
  if (name === "htmlFor") {
204
- applyAttribute(element, "for", value, path, options);
209
+ applyInitialOrHydrationAttribute(
210
+ element,
211
+ "for",
212
+ value,
213
+ path,
214
+ options,
215
+ useAttributeFastPath,
216
+ );
205
217
  continue;
206
218
  }
207
219
 
@@ -210,25 +222,40 @@ function applyInitialProps(
210
222
  continue;
211
223
  }
212
224
 
213
- if (/^on[A-Z]/.test(name) && typeof value === "function") {
214
- const handler = value as (event: SyntheticEvent) => void;
225
+ if (isReactEventHandlerPropName(name) && typeof value === "function") {
215
226
  for (const eventName of toEventNames(name)) {
216
227
  ensureDelegatedEventListener(options.eventRoot ?? element, eventName);
217
228
  }
218
- listeners ??= new Map<string, AppliedEventListener>();
219
- listeners.set(name, { handler });
229
+ continue;
230
+ }
231
+
232
+ if (isEventLikePropName(name)) {
220
233
  continue;
221
234
  }
222
235
 
223
236
  const attributeName = toDomAttributeName(name);
224
237
 
225
238
  if (typeof value === "boolean" && isBooleanishStringAttribute(attributeName)) {
226
- applyAttribute(element, attributeName, value ? "true" : "false", path, options);
239
+ applyInitialOrHydrationAttribute(
240
+ element,
241
+ attributeName,
242
+ value ? "true" : "false",
243
+ path,
244
+ options,
245
+ useAttributeFastPath,
246
+ );
227
247
  continue;
228
248
  }
229
249
 
230
250
  if (typeof value === "boolean" && isDataAttribute(attributeName)) {
231
- applyAttribute(element, attributeName, value ? "true" : "false", path, options);
251
+ applyInitialOrHydrationAttribute(
252
+ element,
253
+ attributeName,
254
+ value ? "true" : "false",
255
+ path,
256
+ options,
257
+ useAttributeFastPath,
258
+ );
232
259
  continue;
233
260
  }
234
261
 
@@ -251,10 +278,16 @@ function applyInitialProps(
251
278
  continue;
252
279
  }
253
280
 
254
- applyAttribute(element, attributeName, value, path, options);
281
+ applyInitialOrHydrationAttribute(
282
+ element,
283
+ attributeName,
284
+ value,
285
+ path,
286
+ options,
287
+ useAttributeFastPath,
288
+ );
255
289
  }
256
290
 
257
- return listeners === undefined ? {} : { listeners };
258
291
  }
259
292
 
260
293
  function applyInitialRowProps(
@@ -338,6 +371,13 @@ function applyAttribute(
338
371
  return;
339
372
  }
340
373
 
374
+ if (isEventLikePropName(name)) {
375
+ if (element.hasAttribute(name) && !preserveHydrationAttributes) {
376
+ element.removeAttribute(name);
377
+ }
378
+ return;
379
+ }
380
+
341
381
  if (value === null || value === undefined || value === false) {
342
382
  if (element.hasAttribute(name) && !preserveHydrationAttributes) {
343
383
  reportRecoverable(
@@ -363,7 +403,10 @@ function applyAttribute(
363
403
  // value is unsafe we treat it as if it were null -- remove the
364
404
  // existing attribute, log a recoverable mismatch, and stop. This
365
405
  // matches react-dom's sanitizeURL posture.
366
- if (isUrlAttribute(name) && isUnsafeUrlAttribute(name, stringValue)) {
406
+ if (
407
+ (isUrlAttribute(name) || isSrcsetAttribute(name)) &&
408
+ isUnsafeUrlAttribute(name, stringValue)
409
+ ) {
367
410
  if (element.hasAttribute(name) && !preserveHydrationAttributes) {
368
411
  reportRecoverable(
369
412
  options,
@@ -400,6 +443,49 @@ function applyAttribute(
400
443
  element.setAttribute(name, stringValue);
401
444
  }
402
445
 
446
+ function applyInitialAttribute(
447
+ element: Element,
448
+ name: string,
449
+ value: unknown,
450
+ ): void {
451
+ if (isDangerousHtmlAttribute(name) && !isDangerousHtmlOptIn(value)) {
452
+ return;
453
+ }
454
+
455
+ if (isEventLikePropName(name) || value === null || value === undefined || value === false) {
456
+ return;
457
+ }
458
+
459
+ const stringValue = isDangerousHtmlOptIn(value)
460
+ ? (value as { __html: string }).__html
461
+ : String(value);
462
+
463
+ if (
464
+ (isUrlAttribute(name) || isSrcsetAttribute(name)) &&
465
+ isUnsafeUrlAttribute(name, stringValue)
466
+ ) {
467
+ return;
468
+ }
469
+
470
+ element.setAttribute(name, stringValue);
471
+ }
472
+
473
+ function applyInitialOrHydrationAttribute(
474
+ element: Element,
475
+ name: string,
476
+ value: unknown,
477
+ path: string,
478
+ options: RenderOptions,
479
+ useFastPath: boolean,
480
+ ): void {
481
+ if (useFastPath) {
482
+ applyInitialAttribute(element, name, value);
483
+ return;
484
+ }
485
+
486
+ applyAttribute(element, name, value, path, options);
487
+ }
488
+
403
489
  function applyFormValueProp(
404
490
  element: Element,
405
491
  name: string,
@@ -474,6 +560,15 @@ function applyFormValueProp(
474
560
  return false;
475
561
  }
476
562
 
563
+ function isFormValuePropName(name: string): boolean {
564
+ return (
565
+ name === "value" ||
566
+ name === "defaultValue" ||
567
+ name === "checked" ||
568
+ name === "defaultChecked"
569
+ );
570
+ }
571
+
477
572
  function applyStyle(
478
573
  element: HostElement,
479
574
  previousStyle: unknown,
@@ -530,15 +625,21 @@ function removePreviousStyle(
530
625
  }
531
626
  }
532
627
 
533
- function collectAttributeNames(props: Record<string, unknown>): Set<string> {
534
- const names = new Set<string>();
628
+ function collectAttributeNames(props: Record<string, unknown>): string[] {
629
+ const names: string[] = [];
630
+
631
+ for (const name in props) {
632
+ if (!Object.prototype.hasOwnProperty.call(props, name)) {
633
+ continue;
634
+ }
635
+
636
+ const value = props[name];
535
637
 
536
- for (const [name, value] of Object.entries(props)) {
537
638
  if (
538
639
  name === "children" ||
539
640
  name === "ref" ||
540
641
  name === "key" ||
541
- /^on[A-Z]/.test(name) ||
642
+ isEventLikePropName(name) ||
542
643
  value === null ||
543
644
  value === undefined
544
645
  ) {
@@ -556,21 +657,60 @@ function collectAttributeNames(props: Record<string, unknown>): Set<string> {
556
657
  }
557
658
 
558
659
  if (name === "defaultValue") {
559
- names.add("value");
660
+ pushUniqueAttributeName(names, "value");
560
661
  continue;
561
662
  }
562
663
 
563
664
  if (name === "defaultChecked") {
564
- names.add("checked");
665
+ pushUniqueAttributeName(names, "checked");
565
666
  continue;
566
667
  }
567
668
 
568
- names.add(attributeName);
669
+ pushUniqueAttributeName(names, attributeName);
569
670
  }
570
671
 
571
672
  return names;
572
673
  }
573
674
 
675
+ function pushUniqueAttributeName(names: string[], name: string): void {
676
+ if (!names.includes(name)) {
677
+ names.push(name);
678
+ }
679
+ }
680
+
681
+ function isReactEventHandlerPropName(name: string): boolean {
682
+ const third = name.charCodeAt(2);
683
+ return name.charCodeAt(0) === 111 && name.charCodeAt(1) === 110 && third >= 65 && third <= 90;
684
+ }
685
+
686
+ function isEventLikePropName(name: string): boolean {
687
+ const first = name.charCodeAt(0);
688
+ const second = name.charCodeAt(1);
689
+ return (first === 111 || first === 79) && (second === 110 || second === 78);
690
+ }
691
+
692
+ function sanitizeMetaRefreshElementProps(
693
+ element: Element,
694
+ props: Record<string, unknown>,
695
+ ): Record<string, unknown> {
696
+ if (element.tagName.toLowerCase() !== "meta") {
697
+ return props;
698
+ }
699
+
700
+ const httpEquiv = props["http-equiv"] ?? props.httpEquiv ?? element.getAttribute("http-equiv");
701
+ const content = props.content;
702
+ if (typeof httpEquiv !== "string" || typeof content !== "string") {
703
+ return props;
704
+ }
705
+ if (!isUnsafeMetaRefreshContent(httpEquiv, content)) {
706
+ return props;
707
+ }
708
+
709
+ const sanitized = { ...props };
710
+ delete sanitized.content;
711
+ return sanitized;
712
+ }
713
+
574
714
  function isBooleanishStringAttribute(name: string): boolean {
575
715
  const attributeName = toDomAttributeName(name).toLowerCase();
576
716
  return attributeName.startsWith("aria-") || BOOLEANISH_STRING_ATTRIBUTES.has(attributeName);
package/src/element.ts CHANGED
@@ -120,6 +120,43 @@ export function createElement<P extends Record<string, unknown>>(
120
120
  };
121
121
  }
122
122
 
123
+ export function createElementFromJsxConfig<P extends Record<string, unknown>>(
124
+ type: ElementType<P>,
125
+ config: (P & ReactReservedProps & { children?: ReactCompatNode }) | null,
126
+ keyArgument?: unknown,
127
+ ): ReactCompatElement<P> {
128
+ const normalizedType =
129
+ typeof type === "object" && type !== null ? normalizeElementType(type) : type;
130
+ const key = keyArgument !== undefined
131
+ ? String(keyArgument)
132
+ : config?.key === undefined ? null : String(config.key);
133
+ const ref = config?.ref ?? null;
134
+ const hasChildren = config !== null && config !== undefined && hasOwnProperty.call(config, "children");
135
+ const children = config?.children;
136
+ const copiedProps = copyElementProps(config, undefined, true);
137
+ const props = (typeof normalizedType === "string"
138
+ ? copiedProps
139
+ : applyDefaultProps(normalizedType, copiedProps)) as P & {
140
+ children?: ReactCompatNode;
141
+ };
142
+
143
+ if (hasChildren) {
144
+ props.children = children;
145
+ }
146
+
147
+ if (typeof normalizedType === "string") {
148
+ setHostOwnPropsMeta(props);
149
+ }
150
+
151
+ return {
152
+ $$typeof: REACT_COMPAT_ELEMENT_TYPE,
153
+ type: normalizedType as ElementType<P>,
154
+ key,
155
+ ref,
156
+ props,
157
+ };
158
+ }
159
+
123
160
  export function isReactCompatElement(
124
161
  value: unknown,
125
162
  ): value is ReactCompatElement {
@@ -232,6 +269,7 @@ export function cloneElement<P extends Record<string, unknown>>(
232
269
  function copyElementProps(
233
270
  source: Record<string, unknown> | null | undefined,
234
271
  base?: Record<string, unknown>,
272
+ omitChildren = false,
235
273
  ): Record<string, unknown> {
236
274
  const props: Record<string, unknown> = base === undefined ? {} : { ...base };
237
275
 
@@ -244,7 +282,13 @@ function copyElementProps(
244
282
  continue;
245
283
  }
246
284
 
247
- if (name !== "key" && name !== "ref" && name !== "__self" && name !== "__source") {
285
+ if (
286
+ name !== "key" &&
287
+ name !== "ref" &&
288
+ name !== "__self" &&
289
+ name !== "__source" &&
290
+ (!omitChildren || name !== "children")
291
+ ) {
248
292
  props[name] = source[name];
249
293
  }
250
294
  }
@@ -1,13 +1,11 @@
1
1
  import type { SyntheticEvent } from "./event-types.js";
2
2
 
3
3
  export interface AppliedProps {
4
+ attributeNames?: string[];
4
5
  props: Record<string, unknown>;
5
- listeners?: Map<string, AppliedEventListener>;
6
6
  }
7
7
 
8
- export interface AppliedEventListener {
9
- handler: (event: SyntheticEvent) => void;
10
- }
8
+ export type AppliedEventListener = (event: SyntheticEvent) => void;
11
9
 
12
10
  const appliedProps = new WeakMap<Element, AppliedProps>();
13
11
 
@@ -23,5 +21,6 @@ export function getAppliedEventHandler(
23
21
  element: Element,
24
22
  name: string,
25
23
  ): ((event: SyntheticEvent) => void) | undefined {
26
- return appliedProps.get(element)?.listeners?.get(name)?.handler;
24
+ const handler = appliedProps.get(element)?.props[name];
25
+ return typeof handler === "function" ? (handler as (event: SyntheticEvent) => void) : undefined;
27
26
  }
@@ -27,6 +27,16 @@ export function reconcileChildFibers(
27
27
  newChildren: ReactCompatNode,
28
28
  ): Fiber | undefined {
29
29
  const children = normalizeChildren(newChildren);
30
+ const orderedKeyedChildren = reconcileSameKeyOrderChildren(
31
+ parent,
32
+ currentFirstChild,
33
+ children,
34
+ );
35
+
36
+ if (orderedKeyedChildren !== undefined) {
37
+ return orderedKeyedChildren;
38
+ }
39
+
30
40
  const keyed = collectKeyedChildren(currentFirstChild);
31
41
  const oldIndexes = collectChildIndexes(currentFirstChild);
32
42
  const used = new Set<Fiber>();
@@ -86,6 +96,63 @@ export function reconcileChildFibers(
86
96
  return first;
87
97
  }
88
98
 
99
+ function reconcileSameKeyOrderChildren(
100
+ parent: Fiber,
101
+ currentFirstChild: Fiber | undefined,
102
+ children: readonly ReactCompatNode[],
103
+ ): Fiber | undefined {
104
+ if (currentFirstChild === undefined || children.length === 0) {
105
+ return undefined;
106
+ }
107
+
108
+ let cursor: Fiber | undefined = currentFirstChild;
109
+
110
+ for (const child of children) {
111
+ const key = getNodeKey(child);
112
+
113
+ if (
114
+ key === undefined ||
115
+ cursor === undefined ||
116
+ cursor.key !== key ||
117
+ !isReactCompatElement(child) ||
118
+ !canReuseElementFiber(cursor, child)
119
+ ) {
120
+ return undefined;
121
+ }
122
+
123
+ cursor = cursor.sibling;
124
+ }
125
+
126
+ if (cursor !== undefined) {
127
+ return undefined;
128
+ }
129
+
130
+ cursor = currentFirstChild;
131
+ let first: Fiber | undefined;
132
+ let previous: Fiber | undefined;
133
+
134
+ for (const child of children) {
135
+ const key = getNodeKey(child) as string;
136
+ const fiber = reconcileSingleChild(parent, cursor, child, key) as Fiber;
137
+
138
+ fiber.lanes |= parent.lanes;
139
+ fiber.return = parent;
140
+ fiber.sibling = undefined;
141
+
142
+ if (first === undefined) {
143
+ first = fiber;
144
+ } else if (previous !== undefined) {
145
+ previous.sibling = fiber;
146
+ }
147
+
148
+ previous = fiber;
149
+ cursor = cursor?.sibling;
150
+ }
151
+
152
+ parent.child = first;
153
+ return first;
154
+ }
155
+
89
156
  function reconcileSingleChild(
90
157
  parent: Fiber,
91
158
  current: Fiber | undefined,
@@ -29,6 +29,9 @@ export function commitFiberRoot(
29
29
  });
30
30
  }
31
31
  commitHostFiberRoot(root, finishedWork, options);
32
+ if (hasRemovedAlternateChildren(finishedWork)) {
33
+ detachRemovedAlternateChildren(finishedWork);
34
+ }
32
35
  root.current = finishedWork;
33
36
  root.current.stateNode = root;
34
37
  markRootFinished(root, finishedWork.lanes);
@@ -44,6 +47,95 @@ export function detachFiberRefs(fiber: Fiber): void {
44
47
  }
45
48
  }
46
49
 
50
+ function detachRemovedAlternateChildren(fiber: Fiber | undefined): void {
51
+ let cursor: Fiber | undefined = fiber;
52
+
53
+ while (cursor !== undefined) {
54
+ const retained = collectRetainedAlternateChildren(cursor.child);
55
+ let alternateChild = cursor.alternate?.child;
56
+
57
+ while (alternateChild !== undefined) {
58
+ const nextAlternateChild = alternateChild.sibling;
59
+
60
+ if (!retained.has(alternateChild)) {
61
+ detachFiberSubtree(alternateChild, retained);
62
+ }
63
+
64
+ alternateChild = nextAlternateChild;
65
+ }
66
+
67
+ if (cursor.deletions !== undefined) {
68
+ for (const deleted of cursor.deletions) {
69
+ detachFiberSubtree(deleted, retained);
70
+ }
71
+ cursor.deletions = undefined;
72
+ }
73
+
74
+ detachRemovedAlternateChildren(cursor.child);
75
+ cursor = cursor.sibling;
76
+ }
77
+ }
78
+
79
+ function hasRemovedAlternateChildren(fiber: Fiber): boolean {
80
+ return (
81
+ fiber.childListChanged ||
82
+ fiber.subtreeChildListChanged ||
83
+ fiber.deletions !== undefined
84
+ );
85
+ }
86
+
87
+ function collectRetainedAlternateChildren(fiber: Fiber | undefined): Set<Fiber> {
88
+ const retained = new Set<Fiber>();
89
+ let cursor = fiber;
90
+
91
+ while (cursor !== undefined) {
92
+ retained.add(cursor);
93
+
94
+ if (cursor.alternate !== undefined) {
95
+ retained.add(cursor.alternate);
96
+ }
97
+
98
+ cursor = cursor.sibling;
99
+ }
100
+
101
+ return retained;
102
+ }
103
+
104
+ function detachFiberSubtree(fiber: Fiber, preserve: ReadonlySet<Fiber>): void {
105
+ const stack = [fiber];
106
+ const seen = new Set<Fiber>();
107
+
108
+ while (stack.length > 0) {
109
+ const current = stack.pop();
110
+
111
+ if (current === undefined || seen.has(current) || preserve.has(current)) {
112
+ continue;
113
+ }
114
+
115
+ seen.add(current);
116
+
117
+ let child = current.child;
118
+ while (child !== undefined) {
119
+ stack.push(child);
120
+ child = child.sibling;
121
+ }
122
+
123
+ if (current.alternate !== undefined) {
124
+ stack.push(current.alternate);
125
+ }
126
+
127
+ current.return = undefined;
128
+ current.child = undefined;
129
+ current.sibling = undefined;
130
+ current.alternate = undefined;
131
+ current.pendingProps = undefined;
132
+ current.memoizedProps = undefined;
133
+ current.memoizedState = undefined;
134
+ current.stateNode = undefined;
135
+ current.deletions = undefined;
136
+ }
137
+ }
138
+
47
139
  function cleanupDeletedRefs(previous: Fiber, next: Fiber): void {
48
140
  const nextRefs = new Set<unknown>();
49
141