@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.
- package/dist/dom-props.d.ts.map +1 -1
- package/dist/dom-props.js +111 -44
- package/dist/dom-props.js.map +1 -1
- package/dist/element.d.ts +3 -0
- package/dist/element.d.ts.map +1 -1
- package/dist/element.js +32 -2
- package/dist/element.js.map +1 -1
- package/dist/event-listeners.d.ts +2 -4
- package/dist/event-listeners.d.ts.map +1 -1
- package/dist/event-listeners.js +2 -1
- package/dist/event-listeners.js.map +1 -1
- package/dist/fiber-child.d.ts.map +1 -1
- package/dist/fiber-child.js +44 -0
- package/dist/fiber-child.js.map +1 -1
- package/dist/fiber-commit.d.ts.map +1 -1
- package/dist/fiber-commit.js +70 -0
- package/dist/fiber-commit.js.map +1 -1
- package/dist/hooks.d.ts +4 -2
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +64 -6
- package/dist/hooks.js.map +1 -1
- package/dist/host-reconciler.d.ts.map +1 -1
- package/dist/host-reconciler.js +2 -2
- package/dist/host-reconciler.js.map +1 -1
- package/dist/jsx-runtime.d.ts.map +1 -1
- package/dist/jsx-runtime.js +2 -11
- package/dist/jsx-runtime.js.map +1 -1
- package/dist/reconciler.d.ts.map +1 -1
- package/dist/reconciler.js +2 -2
- package/dist/reconciler.js.map +1 -1
- package/dist/server-render.d.ts.map +1 -1
- package/dist/server-render.js +29 -4
- package/dist/server-render.js.map +1 -1
- package/dist/url-safety.d.ts +1 -1
- package/dist/url-safety.d.ts.map +1 -1
- package/dist/url-safety.js +1 -1
- package/dist/url-safety.js.map +1 -1
- package/package.json +3 -3
- package/src/dom-props.ts +189 -49
- package/src/element.ts +45 -1
- package/src/event-listeners.ts +4 -5
- package/src/fiber-child.ts +67 -0
- package/src/fiber-commit.ts +92 -0
- package/src/hooks.ts +88 -8
- package/src/host-reconciler.ts +2 -3
- package/src/jsx-runtime.ts +2 -18
- package/src/reconciler.ts +2 -3
- package/src/server-render.ts +43 -4
- 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,
|
|
35
|
-
setAppliedProps(element, {
|
|
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
|
-
|
|
48
|
-
const
|
|
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.
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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 (
|
|
107
|
-
if (
|
|
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
|
-
|
|
116
|
-
|
|
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
|
-
|
|
167
|
-
|
|
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
|
-
):
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
219
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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>):
|
|
534
|
-
const names =
|
|
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
|
-
|
|
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
|
|
660
|
+
pushUniqueAttributeName(names, "value");
|
|
560
661
|
continue;
|
|
561
662
|
}
|
|
562
663
|
|
|
563
664
|
if (name === "defaultChecked") {
|
|
564
|
-
names
|
|
665
|
+
pushUniqueAttributeName(names, "checked");
|
|
565
666
|
continue;
|
|
566
667
|
}
|
|
567
668
|
|
|
568
|
-
names
|
|
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 (
|
|
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
|
}
|
package/src/event-listeners.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
24
|
+
const handler = appliedProps.get(element)?.props[name];
|
|
25
|
+
return typeof handler === "function" ? (handler as (event: SyntheticEvent) => void) : undefined;
|
|
27
26
|
}
|
package/src/fiber-child.ts
CHANGED
|
@@ -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,
|
package/src/fiber-commit.ts
CHANGED
|
@@ -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
|
|