@esportsplus/reactivity 0.33.0 → 0.36.0

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 (45) hide show
  1. package/README.md +9 -7
  2. package/bench/reactive/array.bench.ts +17 -17
  3. package/build/compiler/array.d.ts +2 -2
  4. package/build/compiler/array.js +12 -54
  5. package/build/compiler/constants.d.ts +4 -4
  6. package/build/compiler/constants.js +20 -4
  7. package/build/compiler/index.js +25 -38
  8. package/build/compiler/object.d.ts +2 -2
  9. package/build/compiler/object.js +10 -21
  10. package/build/compiler/plugins/vite.d.ts +4 -2
  11. package/build/compiler/primitives.d.ts +6 -2
  12. package/build/compiler/primitives.js +35 -42
  13. package/build/compiler/types.d.ts +3 -1
  14. package/build/constants.d.ts +2 -2
  15. package/build/constants.js +2 -2
  16. package/build/reactive/array.d.ts +1 -1
  17. package/build/reactive/array.js +26 -15
  18. package/build/reactive/index.js +5 -14
  19. package/build/reactive/object.js +1 -1
  20. package/build/system.d.ts +10 -13
  21. package/build/system.js +105 -83
  22. package/build/types.d.ts +2 -7
  23. package/package.json +5 -5
  24. package/pnpm-workspace.yaml +3 -0
  25. package/src/compiler/array.ts +14 -64
  26. package/src/compiler/constants.ts +21 -5
  27. package/src/compiler/index.ts +39 -70
  28. package/src/compiler/object.ts +12 -29
  29. package/src/compiler/primitives.ts +55 -53
  30. package/src/compiler/types.ts +4 -1
  31. package/src/constants.ts +4 -4
  32. package/src/reactive/array.ts +33 -17
  33. package/src/reactive/index.ts +5 -17
  34. package/src/reactive/object.ts +1 -1
  35. package/src/system.ts +138 -101
  36. package/src/types.ts +2 -9
  37. package/test/async-computed.test.ts +2 -2
  38. package/test/compiler/compiler.test.ts +40 -23
  39. package/test/effects.test.ts +37 -1
  40. package/test/lib/uncaught.ts +26 -0
  41. package/test/reactive/array.test.ts +169 -99
  42. package/test/reactive/nested.test.ts +14 -14
  43. package/test/reactive/objects.test.ts +1 -1
  44. package/test/reactive/reactive.test.ts +49 -0
  45. package/test/system.test.ts +130 -22
package/src/system.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  PACKAGE_NAME,
3
3
  SIGNAL,
4
- STABILIZER_IDLE, STABILIZER_RESCHEDULE, STABILIZER_RUNNING, STABILIZER_SCHEDULED,
4
+ STABILIZER_DEFERRED, STABILIZER_IDLE, STABILIZER_RESCHEDULE, STABILIZER_RUNNING, STABILIZER_SCHEDULED,
5
5
  STATE_CHECK, STATE_COMPUTED, STATE_DIRTY, STATE_EFFECT, STATE_ERROR, STATE_IN_HEAP, STATE_NOTIFY_MASK, STATE_RECOMPUTING
6
6
  } from './constants';
7
7
  import { Computed, ComputedResult, Link, SelectorSignal, Settled, Signal } from './types';
@@ -28,6 +28,8 @@ let asyncMeta = new WeakMap<Computed<unknown>, { factory: Computed<unknown> }>()
28
28
  notified = false,
29
29
  observer: Computed<unknown> | null = null,
30
30
  pendingHead: Signal<unknown> | null = null,
31
+ pulled: Computed<unknown> | null = null,
32
+ puller: Computed<unknown> | null = null,
31
33
  scope: Computed<unknown> | null = null,
32
34
  stabilizer = STABILIZER_IDLE,
33
35
  version = 0,
@@ -35,6 +37,9 @@ let asyncMeta = new WeakMap<Computed<unknown>, { factory: Computed<unknown> }>()
35
37
  writes = 0;
36
38
 
37
39
 
40
+ function noop() {
41
+ }
42
+
38
43
  function walkPop(walk: Walk): Walk | null {
39
44
  let prev = walk.prev;
40
45
 
@@ -129,8 +134,8 @@ function deleteFromHeap<T>(computed: Computed<T>) {
129
134
  computed.prevHeap = computed;
130
135
  }
131
136
 
132
- // Reconstructs main's eager write() fan-out in one batched pass: N writes to one signal
133
- // queue it once, so each subscriber is heap-inserted once. Self-linked nextPending marks the tail.
137
+ // N writes to one signal queue it once, so each subscriber is heap-inserted once. A self-linked
138
+ // nextPending marks the tail.
134
139
  function drainPending() {
135
140
  let node = pendingHead;
136
141
 
@@ -180,6 +185,10 @@ function insertIntoHeap<T>(computed: Computed<T>) {
180
185
  heap.length = Math.max(height + 1, Math.ceil(heap.length * 2));
181
186
  }
182
187
  }
188
+ // heap_i is only non-zero inside a pass: a bucket it already scanned needs another pass
189
+ else if (height < heap_i) {
190
+ stabilizer = STABILIZER_RESCHEDULE;
191
+ }
183
192
  }
184
193
 
185
194
  // https://github.com/stackblitz/alien-signals/blob/v2.0.3/src/system.ts#L52
@@ -305,7 +314,8 @@ function notify<T>(computed: Computed<T>, newState: number) {
305
314
  }
306
315
 
307
316
  // Shared by read()'s tracked pull and peek()'s untracked pull. observer is nulled around update()
308
- // so a recompute triggered here tracks into the node's own scope, never the caller's.
317
+ // so a recompute triggered here tracks into the node's own scope, never the caller's. pulled/puller
318
+ // let propagate() skip re-queueing the caller: it receives the fresh value when this returns.
309
319
  function pull<T>(node: Computed<T>): void {
310
320
  if (!notified) {
311
321
  notified = true;
@@ -319,16 +329,33 @@ function pull<T>(node: Computed<T>): void {
319
329
 
320
330
  let o = observer;
321
331
 
332
+ // Cleared rather than restored: an outer pull then merely fails to skip, which is only a re-run
322
333
  observer = null;
334
+ pulled = node as Computed<unknown>;
335
+ puller = o;
323
336
  update(node);
324
337
  observer = o;
338
+ pulled = null;
339
+ puller = null;
325
340
  }
326
341
 
327
342
  function propagate<T>(computed: Computed<T>) {
328
- for (let c = computed.subs; c; c = c.nextSub) {
343
+ let c = computed.subs;
344
+
345
+ if (c === null) {
346
+ return;
347
+ }
348
+
349
+ let skip = (computed as Computed<unknown>) === pulled ? puller : null;
350
+
351
+ for (; c; c = c.nextSub) {
329
352
  let s = c.sub,
330
353
  state = s.state;
331
354
 
355
+ if (s === skip) {
356
+ continue;
357
+ }
358
+
332
359
  if (state & STATE_CHECK) {
333
360
  s.state = state | STATE_DIRTY;
334
361
  }
@@ -339,14 +366,10 @@ function propagate<T>(computed: Computed<T>) {
339
366
  schedule();
340
367
  }
341
368
 
342
- function recompute<T>(computed: Computed<T>, del: boolean) {
343
- if (del) {
369
+ function recompute<T>(computed: Computed<T>) {
370
+ if (computed.state & STATE_IN_HEAP) {
344
371
  deleteFromHeap(computed);
345
372
  }
346
- else {
347
- computed.nextHeap = undefined;
348
- computed.prevHeap = computed;
349
- }
350
373
 
351
374
  if (computed.cleanup) {
352
375
  // A failing PREVIOUS generation's teardown must not poison this recompute or the stabilize pass
@@ -387,7 +410,8 @@ function recompute<T>(computed: Computed<T>, del: boolean) {
387
410
  // Fresh version so rv/link stamps from this run (incl. nested creations) go stale — false negatives only
388
411
  version++;
389
412
  observer = o;
390
- computed.state = STATE_COMPUTED | flags;
413
+ // fn may have re-queued this node (a tracked read drains pending writes), so heap membership survives
414
+ computed.state = STATE_COMPUTED | flags | (computed.state & STATE_IN_HEAP);
391
415
  // Entry snapshot, not current writes: a node whose fn wrote mid-run must stay gv < writes and validate normally
392
416
  computed.gv = w;
393
417
 
@@ -437,61 +461,62 @@ function recompute<T>(computed: Computed<T>, del: boolean) {
437
461
  propagate(computed);
438
462
  }
439
463
  }
464
+
465
+ // A schedule() requested while depth > 0 is parked as DEFERRED and queued once the outermost
466
+ // recompute returns
467
+ if (!depth && stabilizer === STABILIZER_DEFERRED) {
468
+ stabilizer = STABILIZER_SCHEDULED;
469
+ microtask(stabilize);
470
+ }
440
471
  }
441
472
 
442
473
  function schedule() {
443
- if (stabilizer === STABILIZER_SCHEDULED) {
474
+ if (stabilizer !== STABILIZER_IDLE) {
444
475
  return;
445
476
  }
446
477
 
447
- if (stabilizer === STABILIZER_IDLE && !depth) {
478
+ if (depth) {
479
+ stabilizer = STABILIZER_DEFERRED;
480
+ }
481
+ else {
448
482
  stabilizer = STABILIZER_SCHEDULED;
449
483
  microtask(stabilize);
450
484
  }
451
- else if (stabilizer === STABILIZER_RUNNING) {
452
- stabilizer = STABILIZER_RESCHEDULE;
453
- }
454
485
  }
455
486
 
487
+ // Buckets are popped from the head so a node queued at the current height mid-pass (a write drained
488
+ // by a tracked read) settles in this pass, and the heap stays intact for deleteFromHeap() when a
489
+ // recompute pulls a same-height sibling. A write that lands below heap_i flags RESCHEDULE.
456
490
  function stabilize() {
457
491
  let o = observer;
458
492
 
459
493
  observer = null;
460
- stabilizer = STABILIZER_RUNNING;
461
-
462
- for (heap_i = 0; heap_i <= heap_n; heap_i++) {
463
- // Drain before scanning each height so writes emitted by a lower level's recompute
464
- // land their subscribers in this same pass, matching main's eager same-pass pickup
465
- if (pendingHead !== null) {
466
- drainPending();
467
- }
468
494
 
469
- let computed = heap[heap_i];
495
+ do {
496
+ stabilizer = STABILIZER_RUNNING;
470
497
 
471
- heap[heap_i] = undefined;
498
+ for (heap_i = 0; heap_i <= heap_n; heap_i++) {
499
+ if (pendingHead !== null) {
500
+ drainPending();
501
+ }
472
502
 
473
- while (computed !== undefined) {
474
- let next = computed.nextHeap;
503
+ let computed;
475
504
 
476
- recompute(computed, false);
505
+ while ((computed = heap[heap_i]) !== undefined) {
506
+ recompute(computed);
507
+ }
508
+ }
477
509
 
478
- computed = next;
510
+ while (heap_n > 0 && heap[heap_n] === undefined) {
511
+ heap_n--;
479
512
  }
480
- }
481
513
 
482
- while (heap_n > 0 && heap[heap_n] === undefined) {
483
- heap_n--;
514
+ heap_i = 0;
484
515
  }
516
+ while (pendingHead !== null || stabilizer === STABILIZER_RESCHEDULE);
485
517
 
518
+ stabilizer = STABILIZER_IDLE;
486
519
  observer = o;
487
-
488
- if (stabilizer === STABILIZER_RESCHEDULE) {
489
- stabilizer = STABILIZER_SCHEDULED;
490
- microtask(stabilize);
491
- }
492
- else {
493
- stabilizer = STABILIZER_IDLE;
494
- }
495
520
  }
496
521
 
497
522
  // https://github.com/stackblitz/alien-signals/blob/v2.0.3/src/system.ts#L100
@@ -555,7 +580,7 @@ function update<T>(root: Computed<T>): void {
555
580
  resuming = true;
556
581
  }
557
582
  else if (node.state & STATE_DIRTY) {
558
- recompute(node, true);
583
+ recompute(node);
559
584
  node.state &= ~STATE_NOTIFY_MASK;
560
585
  }
561
586
  else {
@@ -585,7 +610,7 @@ function update<T>(root: Computed<T>): void {
585
610
  }
586
611
 
587
612
  if (node.state & STATE_DIRTY) {
588
- recompute(node, true);
613
+ recompute(node);
589
614
  }
590
615
  else {
591
616
  node.gv = w;
@@ -617,24 +642,33 @@ function makeAsyncComputed<T>(factory: Computed<Promise<T> | AsyncIterable<T> |
617
642
  v = 0;
618
643
 
619
644
  let stop = effect(() => {
620
- let fail = (e: unknown) => {
621
- if (id === v && !(factory.state & (STATE_IN_HEAP | STATE_NOTIFY_MASK))) {
645
+ let id = ++v,
646
+ // A settle is stale once a newer dispatch exists or the factory has a re-run queued. Pending
647
+ // writes are drained first so a write that dirtied the factory but has not been settled yet
648
+ // still counts, whatever the microtask order.
649
+ stale = () => {
650
+ if (pendingHead !== null) {
651
+ drainPending();
652
+ }
653
+
654
+ return id !== v || (factory.state & (STATE_IN_HEAP | STATE_NOTIFY_MASK)) !== 0;
655
+ },
656
+ fail = (e: unknown) => {
657
+ if (!stale()) {
622
658
  write(error, e === undefined ? new Error('reactivity: async computed rejected with undefined') : e);
623
659
  write(pending, false);
624
660
  }
625
661
  },
626
- id = ++v,
627
- // Heap membership (a write's eager insert) marks a pending re-run the notify mask alone misses.
628
662
  result = read(factory);
629
663
 
630
664
  if (isPromise(result)) {
631
- if (id === v && !(factory.state & (STATE_IN_HEAP | STATE_NOTIFY_MASK))) {
665
+ if (!stale()) {
632
666
  write(pending, true);
633
667
  }
634
668
 
635
669
  (result as Promise<T>).then(
636
670
  (value) => {
637
- if (id === v && !(factory.state & (STATE_IN_HEAP | STATE_NOTIFY_MASK))) {
671
+ if (!stale()) {
638
672
  write(error, undefined);
639
673
  write(node, value);
640
674
  write(pending, false);
@@ -651,7 +685,7 @@ function makeAsyncComputed<T>(factory: Computed<Promise<T> | AsyncIterable<T> |
651
685
  });
652
686
 
653
687
  let step = (r: IteratorResult<T>) => {
654
- if (id !== v || (factory.state & (STATE_IN_HEAP | STATE_NOTIFY_MASK))) {
688
+ if (stale()) {
655
689
  return;
656
690
  }
657
691
 
@@ -666,7 +700,7 @@ function makeAsyncComputed<T>(factory: Computed<Promise<T> | AsyncIterable<T> |
666
700
  }
667
701
  };
668
702
 
669
- if (id === v && !(factory.state & (STATE_IN_HEAP | STATE_NOTIFY_MASK))) {
703
+ if (!stale()) {
670
704
  write(pending, true);
671
705
  }
672
706
 
@@ -689,7 +723,7 @@ function makeAsyncComputed<T>(factory: Computed<Promise<T> | AsyncIterable<T> |
689
723
  return read(node);
690
724
  });
691
725
 
692
- (wrapper as Computed<T | undefined> & { pending: Signal<boolean> }).pending = pending;
726
+ wrapper.pending = pending;
693
727
 
694
728
  asyncMeta.set(wrapper as Computed<unknown>, { factory: factory as Computed<unknown> });
695
729
  wrapper.disposal = stop;
@@ -698,38 +732,19 @@ function makeAsyncComputed<T>(factory: Computed<Promise<T> | AsyncIterable<T> |
698
732
  }
699
733
 
700
734
  function makeComputed<T>(fn: Computed<T>['fn'], eager: boolean = false): Computed<T> {
701
- let self: Computed<T> = {
702
- cleanup: null,
703
- deps: null,
704
- depsTail: null,
705
- disposal: null,
706
- equals: null,
707
- error: null,
708
- fn: fn,
709
- gv: 0,
710
- height: 0,
711
- nextHeap: undefined,
712
- prevHeap: null as unknown as Computed<unknown>,
713
- rv: 0,
714
- state: STATE_COMPUTED,
715
- subs: null,
716
- subsTail: null,
717
- value: undefined as T,
718
- };
719
-
720
- self.prevHeap = self;
735
+ let self = makeNode(fn);
721
736
 
722
737
  if (observer) {
723
738
  if (observer.depsTail === null) {
724
739
  self.height = observer.height;
725
- recompute(self, false);
740
+ recompute(self);
726
741
  }
727
742
  else if (eager) {
728
743
  // computed() must know fn's return type to pick sync vs async. This probe runs BEFORE
729
744
  // link() below, so self has no subs yet — recompute's propagate is a no-op and cannot
730
745
  // re-run the parent. Deferring here (as effect() still does) would leave value unset.
731
746
  self.height = observer.height + 1;
732
- recompute(self, false);
747
+ recompute(self);
733
748
  }
734
749
  else {
735
750
  self.height = observer.height + 1;
@@ -741,8 +756,7 @@ function makeComputed<T>(fn: Computed<T>['fn'], eager: boolean = false): Compute
741
756
  onCleanup(() => dispose(self));
742
757
  }
743
758
  else {
744
- recompute(self, false);
745
- root.disposables++;
759
+ recompute(self);
746
760
 
747
761
  if (scope) {
748
762
  onCleanup(() => dispose(self));
@@ -752,6 +766,34 @@ function makeComputed<T>(fn: Computed<T>['fn'], eager: boolean = false): Compute
752
766
  return self;
753
767
  }
754
768
 
769
+ // Every node, including root()'s cleanup-only scope, is built here so dispose() and onCleanup()
770
+ // only ever see one hidden class.
771
+ function makeNode<T>(fn: Computed<T>['fn']): Computed<T> {
772
+ let self: Computed<T> = {
773
+ cleanup: null,
774
+ deps: null,
775
+ depsTail: null,
776
+ disposal: null,
777
+ equals: null,
778
+ error: null,
779
+ fn,
780
+ gv: 0,
781
+ height: 0,
782
+ nextHeap: undefined,
783
+ pending: null,
784
+ prevHeap: null as unknown as Computed<unknown>,
785
+ rv: 0,
786
+ state: STATE_COMPUTED,
787
+ subs: null,
788
+ subsTail: null,
789
+ value: undefined as T,
790
+ };
791
+
792
+ self.prevHeap = self;
793
+
794
+ return self;
795
+ }
796
+
755
797
  // Reuses the same recompute-nesting counter schedule() consults, so writes inside fn defer
756
798
  // scheduling until fn returns; pair with flush() for a synchronous transaction.
757
799
  const batch = <T>(fn: () => T): T => {
@@ -763,8 +805,9 @@ const batch = <T>(fn: () => T): T => {
763
805
  finally {
764
806
  depth--;
765
807
 
766
- if (!depth) {
767
- schedule();
808
+ if (!depth && stabilizer === STABILIZER_DEFERRED) {
809
+ stabilizer = STABILIZER_SCHEDULED;
810
+ microtask(stabilize);
768
811
  }
769
812
  }
770
813
  };
@@ -896,13 +939,9 @@ const effect = <T>(fn: Computed<T>['fn'], apply?: (value: T, prev: T | undefined
896
939
  };
897
940
  };
898
941
 
899
- // RUNNING/RESCHEDULE means a pass is already draining (or a flush is already in this call chain);
900
- // re-entering stabilize here would corrupt heap_i, so this is a deliberate no-op.
901
- // Loops (not a single call): a write during a pass can target a height stabilize()'s current
902
- // pass already scanned past, which only flips stabilizer to RESCHEDULE for the *next* microtask
903
- // rather than draining in-pass — looping here is what actually settles that tail synchronously.
942
+ // A no-op while a pass is running: re-entering stabilize() would corrupt heap_i.
904
943
  const flush = (): void => {
905
- while (stabilizer === STABILIZER_SCHEDULED) {
944
+ if (stabilizer === STABILIZER_SCHEDULED) {
906
945
  stabilize();
907
946
  }
908
947
  };
@@ -958,8 +997,8 @@ const read = <T>(node: Signal<T> | Computed<T>): T => {
958
997
  link(node, observer);
959
998
 
960
999
  if ((node as Computed<unknown>).state & STATE_COMPUTED) {
961
- // Invariant 1: a tracked mid-cycle read must see pending writes drain so the heap
962
- // and this node's notify bits reflect them before the broadcast condition is read
1000
+ // A tracked read must see pending writes: drain so the heap and this node's notify
1001
+ // bits reflect them before the pull gate below is evaluated
963
1002
  if (pendingHead !== null) {
964
1003
  drainPending();
965
1004
  }
@@ -985,7 +1024,6 @@ const read = <T>(node: Signal<T> | Computed<T>): T => {
985
1024
 
986
1025
  const root = <T>(fn: ((dispose: VoidFunction) => T) | (() => T)) => {
987
1026
  let c,
988
- d = root.disposables,
989
1027
  o = observer,
990
1028
  s = scope,
991
1029
  self: Computed<unknown> | null = null,
@@ -993,21 +1031,22 @@ const root = <T>(fn: ((dispose: VoidFunction) => T) | (() => T)) => {
993
1031
  value: T;
994
1032
 
995
1033
  observer = null;
996
- root.disposables = 0;
997
1034
 
998
- if (tracking) {
999
- scope = self = { cleanup: null, state: STATE_COMPUTED } as Computed<unknown>;
1000
- value = (fn as (dispose: VoidFunction) => T)(c = () => dispose(self!));
1035
+ try {
1036
+ if (tracking) {
1037
+ scope = self = makeNode(noop);
1038
+ value = (fn as (dispose: VoidFunction) => T)(c = () => dispose(self!));
1039
+ }
1040
+ else {
1041
+ scope = null;
1042
+ value = (fn as () => T)();
1043
+ }
1001
1044
  }
1002
- else {
1003
- scope = null;
1004
- value = (fn as () => T)();
1045
+ finally {
1046
+ observer = o;
1047
+ scope = s;
1005
1048
  }
1006
1049
 
1007
- observer = o;
1008
- root.disposables = d;
1009
- scope = s;
1010
-
1011
1050
  if (c) {
1012
1051
  onCleanup(c);
1013
1052
  }
@@ -1015,8 +1054,6 @@ const root = <T>(fn: ((dispose: VoidFunction) => T) | (() => T)) => {
1015
1054
  return value;
1016
1055
  };
1017
1056
 
1018
- root.disposables = 0;
1019
-
1020
1057
  const signal = <T>(value: T, equals: ((a: T, b: T) => boolean) | null = null): Signal<T> => {
1021
1058
  return {
1022
1059
  equals: equals as ((a: unknown, b: unknown) => boolean) | null,
package/src/types.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { ts } from '@esportsplus/typescript';
2
1
  import { SIGNAL } from './constants';
3
2
  import { ReactiveArray } from './reactive';
4
3
 
@@ -14,6 +13,7 @@ interface Computed<T> {
14
13
  gv: number;
15
14
  height: number;
16
15
  nextHeap: Computed<unknown> | undefined;
16
+ pending: Signal<boolean> | null;
17
17
  prevHeap: Computed<unknown>;
18
18
  rv: number;
19
19
  state: number;
@@ -73,12 +73,6 @@ type Signal<T> = {
73
73
  value: T;
74
74
  };
75
75
 
76
- interface TransformResult {
77
- changed: boolean;
78
- code: string;
79
- sourceFile: ts.SourceFile;
80
- }
81
-
82
76
 
83
77
  export type {
84
78
  Computed,
@@ -87,6 +81,5 @@ export type {
87
81
  Reactive,
88
82
  SelectorSignal,
89
83
  Settled,
90
- Signal,
91
- TransformResult
84
+ Signal
92
85
  };
@@ -373,11 +373,11 @@ describe('asyncComputed', () => {
373
373
  expect(() => read(node)).toThrow('boom');
374
374
  });
375
375
 
376
- it('sync computed has no pending', () => {
376
+ it('sync computed has a null pending', () => {
377
377
  root(() => {
378
378
  let node = computed(() => 42);
379
379
 
380
- expect((node as { pending?: unknown }).pending).toBeUndefined();
380
+ expect(node.pending).toBeNull();
381
381
  });
382
382
  });
383
383
 
@@ -1,7 +1,8 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import { ts } from '@esportsplus/typescript';
3
+ import { languageService } from '@esportsplus/typescript/compiler';
3
4
  import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
4
- import { NAMESPACE } from '~/compiler/constants';
5
+ import { NAMESPACE, TYPES } from '~/compiler/constants';
5
6
  import type { Bindings } from '~/compiler/types';
6
7
  import array from '~/compiler/array';
7
8
  import object from '~/compiler/object';
@@ -25,26 +26,26 @@ function applyIntents(code: string, sourceFile: ts.SourceFile, intents: Replacem
25
26
  return code;
26
27
  }
27
28
 
28
- function isReactiveCall(node: ts.Node): boolean {
29
+ function isReactiveCall(node: ts.Node): node is ts.CallExpression {
29
30
  return ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'reactive';
30
31
  }
31
32
 
32
33
  function parse(code: string): ts.SourceFile {
33
- return ts.createSourceFile('test.ts', code, ts.ScriptTarget.Latest, true);
34
+ return languageService.parse(process.cwd() + '/test.ts', code);
34
35
  }
35
36
 
36
37
  function transformPrimitives(code: string): { bindings: Bindings; output: string } {
37
38
  let bindings: Bindings = new Map(),
38
39
  sourceFile = parse(code),
39
- intents = primitives(sourceFile, bindings, isReactiveCall);
40
+ { replacements } = primitives(sourceFile, bindings, isReactiveCall);
40
41
 
41
- return { bindings, output: applyIntents(code, sourceFile, intents) };
42
+ return { bindings, output: applyIntents(code, sourceFile, replacements) };
42
43
  }
43
44
 
44
45
  function transformArray(code: string, bindings?: Bindings): { bindings: Bindings; output: string } {
45
46
  let b: Bindings = bindings ?? new Map(),
46
47
  sourceFile = parse(code),
47
- intents = array(sourceFile, b, undefined);
48
+ intents = array(sourceFile, b, isReactiveCall);
48
49
 
49
50
  return { bindings: b, output: applyIntents(code, sourceFile, intents) };
50
51
  }
@@ -52,7 +53,7 @@ function transformArray(code: string, bindings?: Bindings): { bindings: Bindings
52
53
  function transformObject(code: string): { bindings: Bindings; output: string; prepend: string[] } {
53
54
  let bindings: Bindings = new Map(),
54
55
  sourceFile = parse(code),
55
- result = object(sourceFile, bindings, undefined);
56
+ result = object(sourceFile, bindings, isReactiveCall);
56
57
 
57
58
  return {
58
59
  bindings,
@@ -127,15 +128,28 @@ describe('primitives transform', () => {
127
128
  it('tracks bindings for signal type', () => {
128
129
  let { bindings } = transformPrimitives('let x = reactive(0);');
129
130
 
130
- // TYPES.Signal = 3
131
- expect(bindings.get('x')).toBe(3);
131
+ expect(bindings.get('x')).toBe(TYPES.Signal);
132
132
  });
133
133
 
134
134
  it('tracks bindings for computed type', () => {
135
135
  let { bindings } = transformPrimitives('let x = reactive(0); let d = reactive(() => x * 2);');
136
136
 
137
- // TYPES.Computed = 1
138
- expect(bindings.get('d')).toBe(1);
137
+ expect(bindings.get('d')).toBe(TYPES.Computed);
138
+ });
139
+
140
+ it('leaves a same-named plain variable in a sibling function untouched', () => {
141
+ let { output } = transformPrimitives('function a() { let x = reactive(0); return x; }\nfunction b() { let x = 1; return x; }');
142
+
143
+ expect(output).toContain(`function a() { let x = ${NAMESPACE}.signal(0); return ${NAMESPACE}.read(x); }`);
144
+ expect(output).toContain('function b() { let x = 1; return x; }');
145
+ });
146
+
147
+ it('resolves a shadowing inner binding to the innermost declaration', () => {
148
+ let { output } = transformPrimitives('let x = reactive(0); function f() { let x = reactive(() => 1); x = 2; }');
149
+
150
+ expect(output).toContain(`let x = ${NAMESPACE}.signal(0)`);
151
+ expect(output).toContain(`let x = ${NAMESPACE}.computed(() => 1)`);
152
+ expect(output).not.toContain(`${NAMESPACE}.write(x, 2)`);
139
153
  });
140
154
 
141
155
  it('transforms prefix --x in statement', () => {
@@ -205,18 +219,10 @@ describe('object transform', () => {
205
219
  expect(output).toContain('<MyType>');
206
220
  });
207
221
 
208
- it('tracks object binding', () => {
209
- let { bindings } = transformObject('let obj = reactive({ count: 0 });');
210
-
211
- // TYPES.Object = 2
212
- expect(bindings.get('obj')).toBe(2);
213
- });
214
-
215
222
  it('tracks nested array bindings', () => {
216
223
  let { bindings } = transformObject('let obj = reactive({ items: [1, 2, 3] });');
217
224
 
218
- // TYPES.Array = 0
219
- expect(bindings.get('obj.items')).toBe(0);
225
+ expect(bindings.get('obj.items')).toBe(TYPES.Array);
220
226
  });
221
227
  });
222
228
 
@@ -226,7 +232,7 @@ describe('array transform', () => {
226
232
  let { output } = transformArray('let arr = reactive([1, 2, 3]);');
227
233
 
228
234
  expect(output).toContain(`new ${NAMESPACE}.ReactiveArray`);
229
- expect(output).toContain('...[1, 2, 3]');
235
+ expect(output).toContain('([1, 2, 3])');
230
236
  });
231
237
 
232
238
  it('transforms reactive([] as Type[]) to typed ReactiveArray', () => {
@@ -262,6 +268,18 @@ describe('array transform', () => {
262
268
  expect(output).toContain('arr.$length = arr.length + 3');
263
269
  });
264
270
 
271
+ it('transforms every compound operator on arr.length with its own token', () => {
272
+ let bindings: Bindings = new Map();
273
+
274
+ bindings.set('arr', 0);
275
+
276
+ for (let op of ['<<', '>>', '>>>', '??', '||', '&&', '-', '*', '/', '%', '**', '&', '|', '^']) {
277
+ let { output } = transformArray(`arr.length ${op}= 2;`, bindings);
278
+
279
+ expect(output).toContain(`arr.$length = arr.length ${op} 2`);
280
+ }
281
+ });
282
+
265
283
  it('transforms arr[i] = value to arr.$set(i, value)', () => {
266
284
  let bindings: Bindings = new Map();
267
285
 
@@ -275,8 +293,7 @@ describe('array transform', () => {
275
293
  it('tracks reactive array binding from reactive call', () => {
276
294
  let { bindings } = transformArray('let arr = reactive([1, 2, 3]);');
277
295
 
278
- // TYPES.Array = 0
279
- expect(bindings.get('arr')).toBe(0);
296
+ expect(bindings.get('arr')).toBe(TYPES.Array);
280
297
  });
281
298
 
282
299
  it('tracks alias binding from reactive array', () => {