@reckona/mreact-compat 0.0.139 → 0.0.141

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reckona/mreact-compat",
3
- "version": "0.0.139",
3
+ "version": "0.0.141",
4
4
  "description": "React-compatible runtime implementation for mreact.",
5
5
  "keywords": [
6
6
  "compatibility",
@@ -69,7 +69,7 @@
69
69
  "access": "public"
70
70
  },
71
71
  "dependencies": {
72
- "@reckona/mreact-reactive-core": "0.0.139",
73
- "@reckona/mreact-shared": "0.0.139"
72
+ "@reckona/mreact-reactive-core": "0.0.141",
73
+ "@reckona/mreact-shared": "0.0.141"
74
74
  }
75
75
  }
package/src/dom-props.ts CHANGED
@@ -1,14 +1,10 @@
1
1
  import {
2
2
  getAppliedProps,
3
3
  setAppliedProps,
4
- type AppliedEventListener,
5
- type AppliedProps,
6
- ensureDelegatedEventListener,
7
- toEventNames,
4
+ ensureDelegatedEventListenersForProp,
8
5
  } from "./host-event-binder.js";
9
6
  import { HOST_OWN_PROPS_META } from "./element.js";
10
7
  import { reportRecoverable, type RenderOptions } from "./hydration.js";
11
- import type { SyntheticEvent } from "./event-types.js";
12
8
  import {
13
9
  isDangerousHtmlAttribute,
14
10
  isDangerousHtmlOptIn,
@@ -36,23 +32,19 @@ export function applyProps(
36
32
  if (previous === undefined && !preserveHydrationAttributes) {
37
33
  if (applyInitialRowProps(element, nextProps)) {
38
34
  setAppliedProps(element, {
39
- attributeNames: collectAttributeNames(nextProps),
40
35
  props: nextProps,
41
36
  });
42
37
  return;
43
38
  }
44
39
 
45
- const attributeNames = collectAttributeNames(nextProps);
40
+ applyInitialProps(element, nextProps, path, options);
46
41
  setAppliedProps(element, {
47
- attributeNames,
48
42
  props: nextProps,
49
- ...applyInitialProps(element, nextProps, path, options),
50
43
  });
51
44
  return;
52
45
  }
53
46
 
54
47
  const previousProps = previous?.props ?? {};
55
- let listeners = previous?.listeners;
56
48
  const previousAttributeNames = previous?.attributeNames ?? collectAttributeNames(previousProps);
57
49
  const nextAttributeNames = collectAttributeNames(nextProps);
58
50
 
@@ -77,16 +69,6 @@ export function applyProps(
77
69
  }
78
70
  }
79
71
 
80
- if (listeners !== undefined) {
81
- for (const [name, appliedListener] of listeners) {
82
- const nextValue = nextProps[name];
83
-
84
- if (nextValue !== appliedListener.handler) {
85
- listeners.delete(name);
86
- }
87
- }
88
- }
89
-
90
72
  for (const name in nextProps) {
91
73
  if (!Object.prototype.hasOwnProperty.call(nextProps, name)) {
92
74
  continue;
@@ -98,7 +80,7 @@ export function applyProps(
98
80
  continue;
99
81
  }
100
82
 
101
- if (applyFormValueProp(element, name, value, path, options)) {
83
+ if (isFormValuePropName(name) && applyFormValueProp(element, name, value, path, options)) {
102
84
  continue;
103
85
  }
104
86
 
@@ -117,21 +99,16 @@ export function applyProps(
117
99
  continue;
118
100
  }
119
101
 
120
- if (/^on[A-Z]/.test(name) && typeof value === "function") {
121
- if (listeners?.get(name)?.handler === value) {
102
+ if (isReactEventHandlerPropName(name) && typeof value === "function") {
103
+ if (previousProps[name] === value) {
122
104
  continue;
123
105
  }
124
106
 
125
- const handler = value as (event: SyntheticEvent) => void;
126
- for (const eventName of toEventNames(name)) {
127
- ensureDelegatedEventListener(options.eventRoot ?? element, eventName);
128
- }
129
- listeners ??= new Map<string, AppliedEventListener>();
130
- listeners.set(name, { handler });
107
+ ensureDelegatedEventListenersForProp(options.eventRoot ?? element, name);
131
108
  continue;
132
109
  }
133
110
 
134
- if (/^on/i.test(name)) {
111
+ if (isEventLikePropName(name)) {
135
112
  continue;
136
113
  }
137
114
 
@@ -183,7 +160,6 @@ export function applyProps(
183
160
  setAppliedProps(element, {
184
161
  attributeNames: nextAttributeNames,
185
162
  props: nextProps,
186
- ...(listeners === undefined ? {} : { listeners }),
187
163
  });
188
164
  }
189
165
 
@@ -192,8 +168,8 @@ function applyInitialProps(
192
168
  props: Record<string, unknown>,
193
169
  path: string,
194
170
  options: RenderOptions,
195
- ): Pick<AppliedProps, "listeners"> | {} {
196
- let listeners: Map<string, AppliedEventListener> | undefined;
171
+ ): void {
172
+ const useAttributeFastPath = options.hydration === undefined;
197
173
 
198
174
  for (const name in props) {
199
175
  if (!Object.prototype.hasOwnProperty.call(props, name)) {
@@ -206,7 +182,7 @@ function applyInitialProps(
206
182
  continue;
207
183
  }
208
184
 
209
- if (applyFormValueProp(element, name, value, path, options)) {
185
+ if (isFormValuePropName(name) && applyFormValueProp(element, name, value, path, options)) {
210
186
  continue;
211
187
  }
212
188
 
@@ -215,12 +191,26 @@ function applyInitialProps(
215
191
  }
216
192
 
217
193
  if (name === "className") {
218
- applyAttribute(element, "class", value, path, options);
194
+ applyInitialOrHydrationAttribute(
195
+ element,
196
+ "class",
197
+ value,
198
+ path,
199
+ options,
200
+ useAttributeFastPath,
201
+ );
219
202
  continue;
220
203
  }
221
204
 
222
205
  if (name === "htmlFor") {
223
- applyAttribute(element, "for", value, path, options);
206
+ applyInitialOrHydrationAttribute(
207
+ element,
208
+ "for",
209
+ value,
210
+ path,
211
+ options,
212
+ useAttributeFastPath,
213
+ );
224
214
  continue;
225
215
  }
226
216
 
@@ -229,29 +219,38 @@ function applyInitialProps(
229
219
  continue;
230
220
  }
231
221
 
232
- if (/^on[A-Z]/.test(name) && typeof value === "function") {
233
- const handler = value as (event: SyntheticEvent) => void;
234
- for (const eventName of toEventNames(name)) {
235
- ensureDelegatedEventListener(options.eventRoot ?? element, eventName);
236
- }
237
- listeners ??= new Map<string, AppliedEventListener>();
238
- listeners.set(name, { handler });
222
+ if (isReactEventHandlerPropName(name) && typeof value === "function") {
223
+ ensureDelegatedEventListenersForProp(options.eventRoot ?? element, name);
239
224
  continue;
240
225
  }
241
226
 
242
- if (/^on/i.test(name)) {
227
+ if (isEventLikePropName(name)) {
243
228
  continue;
244
229
  }
245
230
 
246
231
  const attributeName = toDomAttributeName(name);
247
232
 
248
233
  if (typeof value === "boolean" && isBooleanishStringAttribute(attributeName)) {
249
- applyAttribute(element, attributeName, value ? "true" : "false", path, options);
234
+ applyInitialOrHydrationAttribute(
235
+ element,
236
+ attributeName,
237
+ value ? "true" : "false",
238
+ path,
239
+ options,
240
+ useAttributeFastPath,
241
+ );
250
242
  continue;
251
243
  }
252
244
 
253
245
  if (typeof value === "boolean" && isDataAttribute(attributeName)) {
254
- applyAttribute(element, attributeName, value ? "true" : "false", path, options);
246
+ applyInitialOrHydrationAttribute(
247
+ element,
248
+ attributeName,
249
+ value ? "true" : "false",
250
+ path,
251
+ options,
252
+ useAttributeFastPath,
253
+ );
255
254
  continue;
256
255
  }
257
256
 
@@ -274,10 +273,16 @@ function applyInitialProps(
274
273
  continue;
275
274
  }
276
275
 
277
- applyAttribute(element, attributeName, value, path, options);
276
+ applyInitialOrHydrationAttribute(
277
+ element,
278
+ attributeName,
279
+ value,
280
+ path,
281
+ options,
282
+ useAttributeFastPath,
283
+ );
278
284
  }
279
285
 
280
- return listeners === undefined ? {} : { listeners };
281
286
  }
282
287
 
283
288
  function applyInitialRowProps(
@@ -361,7 +366,7 @@ function applyAttribute(
361
366
  return;
362
367
  }
363
368
 
364
- if (/^on/i.test(name)) {
369
+ if (isEventLikePropName(name)) {
365
370
  if (element.hasAttribute(name) && !preserveHydrationAttributes) {
366
371
  element.removeAttribute(name);
367
372
  }
@@ -433,6 +438,49 @@ function applyAttribute(
433
438
  element.setAttribute(name, stringValue);
434
439
  }
435
440
 
441
+ function applyInitialAttribute(
442
+ element: Element,
443
+ name: string,
444
+ value: unknown,
445
+ ): void {
446
+ if (isDangerousHtmlAttribute(name) && !isDangerousHtmlOptIn(value)) {
447
+ return;
448
+ }
449
+
450
+ if (isEventLikePropName(name) || value === null || value === undefined || value === false) {
451
+ return;
452
+ }
453
+
454
+ const stringValue = isDangerousHtmlOptIn(value)
455
+ ? (value as { __html: string }).__html
456
+ : String(value);
457
+
458
+ if (
459
+ (isUrlAttribute(name) || isSrcsetAttribute(name)) &&
460
+ isUnsafeUrlAttribute(name, stringValue)
461
+ ) {
462
+ return;
463
+ }
464
+
465
+ element.setAttribute(name, stringValue);
466
+ }
467
+
468
+ function applyInitialOrHydrationAttribute(
469
+ element: Element,
470
+ name: string,
471
+ value: unknown,
472
+ path: string,
473
+ options: RenderOptions,
474
+ useFastPath: boolean,
475
+ ): void {
476
+ if (useFastPath) {
477
+ applyInitialAttribute(element, name, value);
478
+ return;
479
+ }
480
+
481
+ applyAttribute(element, name, value, path, options);
482
+ }
483
+
436
484
  function applyFormValueProp(
437
485
  element: Element,
438
486
  name: string,
@@ -507,6 +555,15 @@ function applyFormValueProp(
507
555
  return false;
508
556
  }
509
557
 
558
+ function isFormValuePropName(name: string): boolean {
559
+ return (
560
+ name === "value" ||
561
+ name === "defaultValue" ||
562
+ name === "checked" ||
563
+ name === "defaultChecked"
564
+ );
565
+ }
566
+
510
567
  function applyStyle(
511
568
  element: HostElement,
512
569
  previousStyle: unknown,
@@ -577,7 +634,7 @@ function collectAttributeNames(props: Record<string, unknown>): string[] {
577
634
  name === "children" ||
578
635
  name === "ref" ||
579
636
  name === "key" ||
580
- /^on/i.test(name) ||
637
+ isEventLikePropName(name) ||
581
638
  value === null ||
582
639
  value === undefined
583
640
  ) {
@@ -616,11 +673,22 @@ function pushUniqueAttributeName(names: string[], name: string): void {
616
673
  }
617
674
  }
618
675
 
676
+ function isReactEventHandlerPropName(name: string): boolean {
677
+ const third = name.charCodeAt(2);
678
+ return name.charCodeAt(0) === 111 && name.charCodeAt(1) === 110 && third >= 65 && third <= 90;
679
+ }
680
+
681
+ function isEventLikePropName(name: string): boolean {
682
+ const first = name.charCodeAt(0);
683
+ const second = name.charCodeAt(1);
684
+ return (first === 111 || first === 79) && (second === 110 || second === 78);
685
+ }
686
+
619
687
  function sanitizeMetaRefreshElementProps(
620
688
  element: Element,
621
689
  props: Record<string, unknown>,
622
690
  ): Record<string, unknown> {
623
- if (element.tagName.toLowerCase() !== "meta") {
691
+ if (element.tagName !== "META" && element.tagName !== "meta") {
624
692
  return props;
625
693
  }
626
694
 
package/src/element.ts CHANGED
@@ -12,6 +12,9 @@ export const SuspenseList = Symbol.for("react.suspense_list");
12
12
  export const Activity = Symbol.for("react.activity");
13
13
  export const Profiler = Symbol.for("react.profiler");
14
14
  export const HOST_OWN_PROPS_META = Symbol.for("modular.react.host_own_props_meta");
15
+ export const HOST_CHILDREN_ONLY_PROPS_META = Symbol.for(
16
+ "modular.react.host_children_only_props_meta",
17
+ );
15
18
  const hasOwnProperty = Object.prototype.hasOwnProperty;
16
19
 
17
20
  export interface ReactCompatProviderType {
@@ -271,12 +274,25 @@ function copyElementProps(
271
274
  base?: Record<string, unknown>,
272
275
  omitChildren = false,
273
276
  ): Record<string, unknown> {
274
- const props: Record<string, unknown> = base === undefined ? {} : { ...base };
277
+ const props: Record<string, unknown> = {};
278
+
279
+ if (base !== undefined) {
280
+ copyOwnStringElementProps(base, props, omitChildren);
281
+ }
275
282
 
276
283
  if (source === null || source === undefined) {
277
284
  return props;
278
285
  }
279
286
 
287
+ copyOwnStringElementProps(source, props, omitChildren);
288
+ return props;
289
+ }
290
+
291
+ function copyOwnStringElementProps(
292
+ source: Record<string, unknown>,
293
+ target: Record<string, unknown>,
294
+ omitChildren: boolean,
295
+ ): void {
280
296
  for (const name in source) {
281
297
  if (!hasOwnProperty.call(source, name)) {
282
298
  continue;
@@ -289,11 +305,9 @@ function copyElementProps(
289
305
  name !== "__source" &&
290
306
  (!omitChildren || name !== "children")
291
307
  ) {
292
- props[name] = source[name];
308
+ target[name] = source[name];
293
309
  }
294
310
  }
295
-
296
- return props;
297
311
  }
298
312
 
299
313
  function normalizeElementType<P>(type: ElementType<P>): ElementType<P> {
@@ -351,6 +365,11 @@ function setHostOwnPropsMeta(props: Record<string, unknown>): void {
351
365
  const dataKey = props["data-key"];
352
366
 
353
367
  if (typeof dataKey !== "number" || !Number.isSafeInteger(dataKey) || dataKey < 0) {
368
+ if (hostPropsAreChildrenOnly(props)) {
369
+ (props as { [HOST_CHILDREN_ONLY_PROPS_META]?: true })[
370
+ HOST_CHILDREN_ONLY_PROPS_META
371
+ ] = true;
372
+ }
354
373
  return;
355
374
  }
356
375
 
@@ -402,6 +421,16 @@ function setHostOwnPropsMeta(props: Record<string, unknown>): void {
402
421
  dataKey * 4 + selectedState;
403
422
  }
404
423
 
424
+ function hostPropsAreChildrenOnly(props: Record<string, unknown>): boolean {
425
+ for (const name in props) {
426
+ if (hasOwnProperty.call(props, name) && name !== "children") {
427
+ return false;
428
+ }
429
+ }
430
+
431
+ return true;
432
+ }
433
+
405
434
  export const isValidElement = isReactCompatElement;
406
435
 
407
436
  export const Children = {
@@ -3,12 +3,9 @@ import type { SyntheticEvent } from "./event-types.js";
3
3
  export interface AppliedProps {
4
4
  attributeNames?: string[];
5
5
  props: Record<string, unknown>;
6
- listeners?: Map<string, AppliedEventListener>;
7
6
  }
8
7
 
9
- export interface AppliedEventListener {
10
- handler: (event: SyntheticEvent) => void;
11
- }
8
+ export type AppliedEventListener = (event: SyntheticEvent) => void;
12
9
 
13
10
  const appliedProps = new WeakMap<Element, AppliedProps>();
14
11
 
@@ -24,5 +21,6 @@ export function getAppliedEventHandler(
24
21
  element: Element,
25
22
  name: string,
26
23
  ): ((event: SyntheticEvent) => void) | undefined {
27
- 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;
28
26
  }
package/src/events.ts CHANGED
@@ -90,12 +90,109 @@ const nativeEventToReactProps = new Map<string, string[]>([
90
90
  ]);
91
91
 
92
92
  export function toEventNames(propName: string): string[] {
93
+ const eventNames: string[] = [];
94
+ forEachEventName(propName, (eventName) => {
95
+ eventNames.push(eventName);
96
+ });
97
+ return eventNames;
98
+ }
99
+
100
+ export function forEachEventName(
101
+ propName: string,
102
+ callback: (eventName: string) => void,
103
+ ): void {
104
+ const directEventName = directNativeEventName(propName);
105
+
106
+ if (directEventName !== undefined) {
107
+ callback(directEventName);
108
+ return;
109
+ }
110
+
111
+ const basePropName = toBaseEventPropName(propName);
112
+ const mappedEventNames = reactPropToNativeEvent.get(basePropName);
113
+
114
+ if (mappedEventNames === undefined) {
115
+ callback(basePropName.slice(2).toLowerCase());
116
+ return;
117
+ }
118
+
119
+ for (let index = 0; index < mappedEventNames.length; index += 1) {
120
+ callback(mappedEventNames[index]!);
121
+ }
122
+ }
123
+
124
+ export function ensureDelegatedEventListenersForProp(
125
+ root: Element,
126
+ propName: string,
127
+ ): void {
128
+ const directEventName = directNativeEventName(propName);
129
+
130
+ if (directEventName !== undefined) {
131
+ ensureDelegatedEventListener(root, directEventName);
132
+ return;
133
+ }
134
+
135
+ const basePropName = toBaseEventPropName(propName);
136
+ const mappedEventNames = reactPropToNativeEvent.get(basePropName);
137
+
138
+ if (mappedEventNames === undefined) {
139
+ ensureDelegatedEventListener(root, basePropName.slice(2).toLowerCase());
140
+ return;
141
+ }
142
+
143
+ for (let index = 0; index < mappedEventNames.length; index += 1) {
144
+ ensureDelegatedEventListener(root, mappedEventNames[index]!);
145
+ }
146
+ }
147
+
148
+ function directNativeEventName(propName: string): string | undefined {
149
+ switch (propName) {
150
+ case "onClick":
151
+ case "onClickCapture":
152
+ return "click";
153
+ case "onInput":
154
+ case "onInputCapture":
155
+ return "input";
156
+ case "onKeyDown":
157
+ case "onKeyDownCapture":
158
+ return "keydown";
159
+ case "onKeyUp":
160
+ case "onKeyUpCapture":
161
+ return "keyup";
162
+ case "onMouseDown":
163
+ case "onMouseDownCapture":
164
+ return "mousedown";
165
+ case "onMouseMove":
166
+ case "onMouseMoveCapture":
167
+ return "mousemove";
168
+ case "onMouseOut":
169
+ case "onMouseOutCapture":
170
+ return "mouseout";
171
+ case "onMouseOver":
172
+ case "onMouseOverCapture":
173
+ return "mouseover";
174
+ case "onMouseUp":
175
+ case "onMouseUpCapture":
176
+ return "mouseup";
177
+ case "onScroll":
178
+ case "onScrollCapture":
179
+ return "scroll";
180
+ case "onSubmit":
181
+ case "onSubmitCapture":
182
+ return "submit";
183
+ case "onWheel":
184
+ case "onWheelCapture":
185
+ return "wheel";
186
+ default:
187
+ return undefined;
188
+ }
189
+ }
190
+
191
+ function toBaseEventPropName(propName: string): string {
93
192
  const basePropName = propName.endsWith("Capture")
94
193
  ? propName.slice(0, -"Capture".length)
95
194
  : propName;
96
- return reactPropToNativeEvent.get(basePropName) ?? [
97
- basePropName.slice(2).toLowerCase(),
98
- ];
195
+ return basePropName;
99
196
  }
100
197
 
101
198
  export function toEventPropNames(eventName: string): string[] {
@@ -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,