@reckona/mreact-compat 0.0.171 → 0.0.173

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 (58) hide show
  1. package/dist/devtools.js +2 -0
  2. package/dist/devtools.js.map +1 -1
  3. package/dist/dom-host-rules.d.ts.map +1 -1
  4. package/dist/dom-host-rules.js +4 -1
  5. package/dist/dom-host-rules.js.map +1 -1
  6. package/dist/dom-props.js +2 -1
  7. package/dist/dom-props.js.map +1 -1
  8. package/dist/element.d.ts +13 -1
  9. package/dist/element.d.ts.map +1 -1
  10. package/dist/element.js +30 -2
  11. package/dist/element.js.map +1 -1
  12. package/dist/fiber-commit.js +2 -1
  13. package/dist/fiber-commit.js.map +1 -1
  14. package/dist/fiber-host.d.ts +1 -1
  15. package/dist/fiber-host.d.ts.map +1 -1
  16. package/dist/fiber-host.js +1 -1
  17. package/dist/fiber-host.js.map +1 -1
  18. package/dist/fiber.d.ts +2 -1
  19. package/dist/fiber.d.ts.map +1 -1
  20. package/dist/fiber.js +2 -0
  21. package/dist/fiber.js.map +1 -1
  22. package/dist/hooks.d.ts +9 -0
  23. package/dist/hooks.d.ts.map +1 -1
  24. package/dist/hooks.js +127 -44
  25. package/dist/hooks.js.map +1 -1
  26. package/dist/host-reconciler.d.ts +2 -0
  27. package/dist/host-reconciler.d.ts.map +1 -1
  28. package/dist/host-reconciler.js +696 -55
  29. package/dist/host-reconciler.js.map +1 -1
  30. package/dist/jsx-dev-runtime.d.ts +3 -1
  31. package/dist/jsx-dev-runtime.d.ts.map +1 -1
  32. package/dist/jsx-dev-runtime.js +3 -1
  33. package/dist/jsx-dev-runtime.js.map +1 -1
  34. package/dist/jsx-runtime.d.ts +4 -2
  35. package/dist/jsx-runtime.d.ts.map +1 -1
  36. package/dist/jsx-runtime.js +9 -1
  37. package/dist/jsx-runtime.js.map +1 -1
  38. package/dist/reactive-prop-cell.d.ts +13 -0
  39. package/dist/reactive-prop-cell.d.ts.map +1 -0
  40. package/dist/reactive-prop-cell.js +36 -0
  41. package/dist/reactive-prop-cell.js.map +1 -0
  42. package/dist/root.d.ts.map +1 -1
  43. package/dist/root.js +8 -5
  44. package/dist/root.js.map +1 -1
  45. package/package.json +3 -3
  46. package/src/devtools.ts +2 -0
  47. package/src/dom-host-rules.ts +4 -1
  48. package/src/dom-props.ts +2 -1
  49. package/src/element.ts +60 -3
  50. package/src/fiber-commit.ts +3 -1
  51. package/src/fiber-host.ts +2 -0
  52. package/src/fiber.ts +4 -0
  53. package/src/hooks.ts +187 -46
  54. package/src/host-reconciler.ts +1031 -70
  55. package/src/jsx-dev-runtime.ts +4 -0
  56. package/src/jsx-runtime.ts +16 -0
  57. package/src/reactive-prop-cell.ts +67 -0
  58. package/src/root.ts +14 -4
package/dist/hooks.js CHANGED
@@ -1,13 +1,14 @@
1
- import { flushPendingComputed, flushQueuedComputations, } from "@reckona/mreact-reactive-core/internal";
1
+ import { flushPendingComputed, flushQueuedComputations, notifySubscribers, trackSource, } from "@reckona/mreact-reactive-core/internal";
2
2
  import { scheduleCallback } from "./fiber-scheduler.js";
3
3
  import { removeChildIfPresent } from "./dom-children.js";
4
4
  import { isReactCompatContext, readContextValue, useContext, withContextReadObserver, } from "./context.js";
5
- import { REACTIVE_TEXT_BINDING_META } from "./element.js";
5
+ import { REACTIVE_STATE_BINDING_META, REACTIVE_TEXT_BINDING_META, } from "./element.js";
6
6
  import { isThenable } from "./thenable.js";
7
7
  const HOOK_RENDER_STATE_KEY = Symbol.for("modular.react.hook_render_state");
8
8
  const hookRenderState = (globalThis[HOOK_RENDER_STATE_KEY] ??= {
9
9
  currentRuntime: undefined,
10
10
  currentInstance: undefined,
11
+ pendingInstance: undefined,
11
12
  currentCacheScope: undefined,
12
13
  hostCommitDepth: 0,
13
14
  queuedHostCommitRerenders: new Set(),
@@ -156,6 +157,9 @@ export function createRootRuntime(rerender, options = {}) {
156
157
  flushPendingEffects(this.pendingImperativeHandleEffects);
157
158
  this.effectFlushPhase = "layout";
158
159
  const strictLayoutEffects = flushPendingEffects(this.pendingLayoutEffects);
160
+ if (flushHostCommitRerenders()) {
161
+ dedupePendingEffects(this.pendingEffects);
162
+ }
159
163
  this.effectFlushPhase = "normal";
160
164
  const strictEffects = flushPendingEffects(this.pendingEffects);
161
165
  this.effectFlushPhase = undefined;
@@ -258,23 +262,53 @@ export function renderWithProfiler(runtime, path, props, render) {
258
262
  export function renderWithRootRuntime(runtime, path, render, owner) {
259
263
  const previousRuntime = hookRenderState.currentRuntime;
260
264
  const previousInstance = hookRenderState.currentInstance;
261
- let instance = runtime.instances.get(path);
262
- if (instance !== undefined && owner !== undefined && instance.owner !== owner) {
263
- cleanupInstance(instance);
264
- instance = undefined;
265
- }
266
- instance ??= {
267
- owner,
268
- path,
269
- hooks: [],
270
- hookIndex: 0,
271
- dirty: false,
272
- devToolsHookSuppressionDepth: 0,
273
- };
274
- instance.owner = owner;
275
- instance.path = path;
276
- runtime.instances.set(path, instance);
277
- indexInstanceKey(runtime, path);
265
+ const previousPending = hookRenderState.pendingInstance;
266
+ let existing = runtime.instances.get(path);
267
+ if (existing !== undefined && owner !== undefined && existing.owner !== owner) {
268
+ cleanupInstance(existing);
269
+ runtime.instances.delete(path);
270
+ removeInstanceKeyFromIndex(runtime, path);
271
+ existing = undefined;
272
+ }
273
+ // Defer instance materialization: hooks / context reads call
274
+ // materializeInstance() lazily. A component that touches neither never
275
+ // allocates or registers an instance.
276
+ hookRenderState.currentRuntime = runtime;
277
+ hookRenderState.currentInstance = undefined;
278
+ hookRenderState.pendingInstance = { runtime, path, owner, existing };
279
+ try {
280
+ return withContextReadObserver(recordContextDependency, render);
281
+ }
282
+ finally {
283
+ hookRenderState.currentRuntime = previousRuntime;
284
+ hookRenderState.currentInstance = previousInstance;
285
+ hookRenderState.pendingInstance = previousPending;
286
+ }
287
+ }
288
+ // Materialize the deferred instance for the rendering component the first time
289
+ // a hook or context read needs it.
290
+ function materializeInstance() {
291
+ const pending = hookRenderState.pendingInstance;
292
+ if (pending === undefined) {
293
+ throw new Error("Hooks can only be called while rendering.");
294
+ }
295
+ const { runtime, path, owner } = pending;
296
+ let instance = pending.existing;
297
+ if (instance === undefined) {
298
+ instance = {
299
+ owner,
300
+ path,
301
+ hooks: [],
302
+ hookIndex: 0,
303
+ dirty: false,
304
+ devToolsHookSuppressionDepth: 0,
305
+ };
306
+ runtime.instances.set(path, instance);
307
+ indexInstanceKey(runtime, path);
308
+ }
309
+ else {
310
+ instance.owner = owner;
311
+ }
278
312
  runtime.activeInstanceKeys?.add(path);
279
313
  instance.hookIndex = 0;
280
314
  instance.dirty = false;
@@ -289,17 +323,13 @@ export function renderWithRootRuntime(runtime, path, render, owner) {
289
323
  delete instance.devToolsHookTypes;
290
324
  }
291
325
  instance.devToolsHookSuppressionDepth = 0;
292
- hookRenderState.currentRuntime = runtime;
293
326
  hookRenderState.currentInstance = instance;
294
- try {
295
- return withContextReadObserver((context, value) => {
296
- (instance.contextDependencies ??= new Map()).set(context, value);
297
- }, render);
298
- }
299
- finally {
300
- hookRenderState.currentRuntime = previousRuntime;
301
- hookRenderState.currentInstance = previousInstance;
302
- }
327
+ hookRenderState.pendingInstance = undefined;
328
+ return instance;
329
+ }
330
+ function recordContextDependency(context, value) {
331
+ const instance = hookRenderState.currentInstance ?? materializeInstance();
332
+ (instance.contextDependencies ??= new Map()).set(context, value);
303
333
  }
304
334
  export function hasChangedContextDependency(runtime, keys) {
305
335
  for (const key of keys) {
@@ -318,18 +348,22 @@ export function hasChangedContextDependency(runtime, keys) {
318
348
  export function hasContextDependency(runtime, keys) {
319
349
  return keys.some((key) => runtime.instances.get(key)?.contextDependencies !== undefined);
320
350
  }
351
+ // Shared read-only empty result so components with no registered instances
352
+ // (e.g. hookless rows under lazy instance materialization) don't each allocate
353
+ // a fresh array. Callers treat instance-key lists as read-only.
354
+ const EMPTY_INSTANCE_KEYS = [];
321
355
  export function collectRuntimeInstanceKeys(runtime, prefix) {
322
356
  const keys = runtime.instanceKeysByPrefix.get(prefix);
323
357
  if (keys === undefined) {
324
- return [];
358
+ return EMPTY_INSTANCE_KEYS;
325
359
  }
326
- const activeKeys = [];
360
+ let activeKeys;
327
361
  for (const key of keys) {
328
362
  if (runtime.instances.has(key)) {
329
- activeKeys.push(key);
363
+ (activeKeys ??= []).push(key);
330
364
  }
331
365
  }
332
- return activeKeys;
366
+ return activeKeys ?? EMPTY_INSTANCE_KEYS;
333
367
  }
334
368
  export function getDevToolsHookState(runtime, path) {
335
369
  const instance = runtime.instances.get(path);
@@ -480,7 +514,7 @@ export function useState(initial) {
480
514
  if (slot.kind !== "state") {
481
515
  throw new Error("Hook order changed between renders.");
482
516
  }
483
- const setState = (value) => {
517
+ slot.dispatch ??= (value) => {
484
518
  const previousValue = slot.value;
485
519
  const nextValue = typeof value === "function"
486
520
  ? value(slot.value)
@@ -500,7 +534,15 @@ export function useState(initial) {
500
534
  transitionDepth === 0 &&
501
535
  optionsAllowDirectTextBinding(value) &&
502
536
  updateDirectTextBinding(slot.textBinding, nextValue);
503
- if (canUseDirectTextBinding) {
537
+ const canUseDirectStateBinding = hookRenderState.hostCommitDepth === 0 &&
538
+ hookRenderState.currentRuntime !== runtime &&
539
+ hookRenderState.currentInstance !== instance &&
540
+ runtime.effectFlushPhase === undefined &&
541
+ eventBatchDepth === 0 &&
542
+ transitionDepth === 0 &&
543
+ optionsAllowDirectTextBinding(value) &&
544
+ updateDirectStateBinding(slot.stateBinding, nextValue);
545
+ if (canUseDirectTextBinding || canUseDirectStateBinding) {
504
546
  return;
505
547
  }
506
548
  if (hookRenderState.hostCommitDepth > 0) {
@@ -510,12 +552,14 @@ export function useState(initial) {
510
552
  }
511
553
  scheduleInstanceUpdate(runtime, instance, { deferSync: typeof value === "function" });
512
554
  };
555
+ const setState = slot.dispatch;
513
556
  recordDevToolsHook("useState", {
514
557
  kind: "state",
515
558
  value: slot.value,
516
559
  });
517
560
  const result = [slot.value, setState];
518
561
  result[REACTIVE_TEXT_BINDING_META] = getStateTextBinding(slot);
562
+ result[REACTIVE_STATE_BINDING_META] = getStateBinding(slot);
519
563
  return result;
520
564
  }
521
565
  export function subscribeReactiveTextBinding(binding, node) {
@@ -554,6 +598,18 @@ function getStateTextBinding(slot) {
554
598
  slot.textBinding.value = slot.value;
555
599
  return slot.textBinding;
556
600
  }
601
+ function getStateBinding(slot) {
602
+ slot.stateBinding ??= {
603
+ value: slot.value,
604
+ source: { subscribers: null },
605
+ get() {
606
+ trackSource(this.source);
607
+ return this.value;
608
+ },
609
+ };
610
+ slot.stateBinding.value = slot.value;
611
+ return slot.stateBinding;
612
+ }
557
613
  function optionsAllowDirectTextBinding(value) {
558
614
  return typeof value !== "function";
559
615
  }
@@ -577,6 +633,15 @@ function updateDirectTextBinding(binding, value) {
577
633
  binding.value = value;
578
634
  return updated;
579
635
  }
636
+ function updateDirectStateBinding(binding, value) {
637
+ if (binding === undefined || binding.source.subscribers === null) {
638
+ return false;
639
+ }
640
+ binding.value = value;
641
+ notifySubscribers(binding.source);
642
+ flushQueuedComputations();
643
+ return true;
644
+ }
580
645
  function isReactiveTextBinding(value) {
581
646
  return (typeof value === "object" &&
582
647
  value !== null &&
@@ -585,7 +650,8 @@ function isReactiveTextBinding(value) {
585
650
  }
586
651
  /** Stores reducer-managed component state and returns the current state with a dispatch function. */
587
652
  export function useReducer(reducer, initialArg, init) {
588
- const [state, setState] = runWithoutDevToolsHookTracking(() => useState(() => init === undefined ? initialArg : init(initialArg)));
653
+ const stateTuple = runWithoutDevToolsHookTracking(() => useState(() => init === undefined ? initialArg : init(initialArg)));
654
+ const [state, setState] = stateTuple;
589
655
  const reducerRef = runWithoutDevToolsHookTracking(() => useRef(reducer));
590
656
  const stateRef = runWithoutDevToolsHookTracking(() => useRef(state));
591
657
  const dispatchRef = runWithoutDevToolsHookTracking(() => useRef(undefined));
@@ -602,7 +668,12 @@ export function useReducer(reducer, initialArg, init) {
602
668
  kind: "reducer",
603
669
  value: state,
604
670
  });
605
- return [state, dispatchRef.current];
671
+ const result = [state, dispatchRef.current];
672
+ const stateBinding = stateTuple[REACTIVE_STATE_BINDING_META];
673
+ if (stateBinding !== undefined) {
674
+ result[REACTIVE_STATE_BINDING_META] = stateBinding;
675
+ }
676
+ return result;
606
677
  }
607
678
  /** Returns a stable mutable ref object for the component instance. */
608
679
  export function useRef(initial) {
@@ -865,6 +936,7 @@ export function useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
865
936
  if (slot.kind !== "store") {
866
937
  throw new Error("Hook order changed between renders.");
867
938
  }
939
+ slot.getSnapshot = getSnapshot;
868
940
  const isHydrationMount = runtime.idMode === "server" && slot.hasMounted !== true && getServerSnapshot !== undefined;
869
941
  const currentSnapshot = isHydrationMount ? slot.value : getSnapshot();
870
942
  if (!Object.is(slot.value, currentSnapshot)) {
@@ -878,7 +950,7 @@ export function useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
878
950
  if (instance.disposed === true) {
879
951
  return;
880
952
  }
881
- const nextSnapshot = getSnapshot();
953
+ const nextSnapshot = (slot.getSnapshot ?? getSnapshot)();
882
954
  if (!Object.is(slot.value, nextSnapshot)) {
883
955
  if (hookRenderState.hostCommitDepth > 0 && !Object.hasOwn(slot, "hostCommitValue")) {
884
956
  slot.hostCommitValue = slot.value;
@@ -910,7 +982,7 @@ export function useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
910
982
  return () => {
911
983
  unsubscribe();
912
984
  };
913
- }, [subscribe, getSnapshot]));
985
+ }, [subscribe]));
914
986
  recordDevToolsHook("useSyncExternalStore", {
915
987
  kind: "store",
916
988
  value: slot.value,
@@ -1433,8 +1505,9 @@ function flushHostCommitRerenders() {
1433
1505
  if (hostCommitRerenderDepth > 0 ||
1434
1506
  hookRenderState.hostCommitDepth > 0 ||
1435
1507
  hookRenderState.queuedHostCommitRerenders.size === 0) {
1436
- return;
1508
+ return false;
1437
1509
  }
1510
+ let didRerender = false;
1438
1511
  hostCommitRerenderDepth += 1;
1439
1512
  try {
1440
1513
  for (let attempt = 0; attempt < 3 && hookRenderState.queuedHostCommitRerenders.size > 0; attempt += 1) {
@@ -1444,6 +1517,7 @@ function flushHostCommitRerenders() {
1444
1517
  const hasDirtyInstance = Array.from(runtime.instances.values()).some((instance) => instance.dirty);
1445
1518
  clearHostCommitStateBaselines(runtime);
1446
1519
  if (hasDirtyInstance) {
1520
+ didRerender = true;
1447
1521
  runtime.rerender("sync");
1448
1522
  }
1449
1523
  }
@@ -1453,6 +1527,18 @@ function flushHostCommitRerenders() {
1453
1527
  finally {
1454
1528
  hostCommitRerenderDepth -= 1;
1455
1529
  }
1530
+ return didRerender;
1531
+ }
1532
+ function dedupePendingEffects(queue) {
1533
+ if (queue.length < 2) {
1534
+ return;
1535
+ }
1536
+ const latestBySlot = new Map();
1537
+ for (const effect of queue) {
1538
+ latestBySlot.set(effect.slot, effect);
1539
+ }
1540
+ queue.length = 0;
1541
+ queue.push(...latestBySlot.values());
1456
1542
  }
1457
1543
  function flushEffectFlushRerenders() {
1458
1544
  if (effectFlushRerenderDepth > 0 ||
@@ -1616,10 +1702,7 @@ function requireRuntime() {
1616
1702
  return hookRenderState.currentRuntime;
1617
1703
  }
1618
1704
  function requireInstance() {
1619
- if (hookRenderState.currentInstance === undefined) {
1620
- throw new Error("Hooks can only be called while rendering.");
1621
- }
1622
- return hookRenderState.currentInstance;
1705
+ return hookRenderState.currentInstance ?? materializeInstance();
1623
1706
  }
1624
1707
  function areHookInputsEqual(nextDeps, previousDeps) {
1625
1708
  if (nextDeps.length !== previousDeps.length) {