@alloy-js/core 0.11.0 → 0.13.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 (40) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/src/binder.d.ts.map +1 -1
  3. package/dist/src/binder.js +79 -22
  4. package/dist/src/components/For.d.ts +1 -1
  5. package/dist/src/components/For.d.ts.map +1 -1
  6. package/dist/src/jsx-runtime.d.ts.map +1 -1
  7. package/dist/src/jsx-runtime.js +9 -3
  8. package/dist/src/render.d.ts.map +1 -1
  9. package/dist/src/render.js +5 -0
  10. package/dist/src/scheduler.d.ts +8 -0
  11. package/dist/src/scheduler.d.ts.map +1 -0
  12. package/dist/src/scheduler.js +17 -0
  13. package/dist/test/components/declaration.test.js +2 -0
  14. package/dist/test/control-flow/for.test.js +36 -2
  15. package/dist/test/reactivity/circular-reactives.test.d.ts +2 -0
  16. package/dist/test/reactivity/circular-reactives.test.d.ts.map +1 -0
  17. package/dist/test/reactivity/circular-reactives.test.js +31 -0
  18. package/dist/test/reactivity/cleanup.test.js +5 -0
  19. package/dist/test/reactivity/untrack.test.js +3 -0
  20. package/dist/test/rendering/memoization.test.js +2 -0
  21. package/dist/test/symbols.test.js +391 -11
  22. package/dist/test/utils.test.d.ts.map +1 -1
  23. package/dist/test/utils.test.js +2 -0
  24. package/dist/tsconfig.tsbuildinfo +1 -1
  25. package/package.json +3 -3
  26. package/src/binder.ts +116 -22
  27. package/src/components/For.tsx +4 -4
  28. package/src/jsx-runtime.ts +22 -12
  29. package/src/render.ts +5 -0
  30. package/src/scheduler.ts +24 -0
  31. package/temp/api.json +8 -8
  32. package/test/components/declaration.test.tsx +2 -0
  33. package/test/components/list.test.tsx +0 -1
  34. package/test/control-flow/for.test.tsx +34 -4
  35. package/test/reactivity/circular-reactives.test.tsx +32 -0
  36. package/test/reactivity/cleanup.test.tsx +5 -0
  37. package/test/reactivity/untrack.test.ts +3 -0
  38. package/test/rendering/memoization.test.tsx +2 -0
  39. package/test/symbols.test.ts +401 -13
  40. package/test/utils.test.tsx +2 -0
@@ -8,6 +8,7 @@ import {
8
8
  OutputSymbolFlags,
9
9
  } from "../src/binder.js";
10
10
  import { Refkey, refkey } from "../src/refkey.js";
11
+ import { flushJobs } from "../src/scheduler.js";
11
12
 
12
13
  it("works", () => {
13
14
  const binder = createOutputBinder();
@@ -22,10 +23,11 @@ it("works", () => {
22
23
  scope,
23
24
  });
24
25
 
26
+ flushJobs();
25
27
  expect([...scope.getSymbolNames()]).toEqual(["sym"]);
26
28
 
27
29
  symbol.name = "bar";
28
-
30
+ flushJobs();
29
31
  expect([...scope.getSymbolNames()]).toEqual(["bar"]);
30
32
  });
31
33
 
@@ -47,7 +49,16 @@ it("resolves symbol conflicts", () => {
47
49
  scope,
48
50
  });
49
51
 
52
+ const s3 = binder.createSymbol({
53
+ name: "sym",
54
+ scope,
55
+ });
56
+
57
+ flushJobs();
58
+
59
+ expect(_s1.name).toEqual("sym");
50
60
  expect(s2.name).toEqual("sym_2");
61
+ expect(s3.name).toEqual("sym_3");
51
62
  });
52
63
 
53
64
  type ScopeRecords = Record<string, ScopeDescriptor>;
@@ -299,8 +310,25 @@ describe("instance members", () => {
299
310
  });
300
311
 
301
312
  describe("instantiating members", () => {
302
- it("instantiates static symbols", () => {
313
+ it("instantiates instance members", () => {
303
314
  const binder = createOutputBinder();
315
+
316
+ /**
317
+ * The following structure would match code like this:
318
+ * ```ts
319
+ * // A class with instance members
320
+ * class Source {
321
+ * instance() {
322
+ * print("instance");
323
+ * }
324
+ * }
325
+ *
326
+ * // Instantiates into t
327
+ * var t = new Source();
328
+ *
329
+ * t.instance();
330
+ * ```
331
+ */
304
332
  const {
305
333
  symbols: { rootSymbol, instance, instantiation },
306
334
  } = createScopeTree(binder, {
@@ -310,7 +338,7 @@ describe("instantiating members", () => {
310
338
  flags: OutputSymbolFlags.InstanceMemberContainer,
311
339
  instanceMembers: {
312
340
  instance: {
313
- flags: OutputSymbolFlags.StaticMember,
341
+ flags: OutputSymbolFlags.InstanceMember,
314
342
  },
315
343
  },
316
344
  },
@@ -321,20 +349,107 @@ describe("instantiating members", () => {
321
349
 
322
350
  binder.instantiateSymbolInto(rootSymbol, instantiation);
323
351
  expect(
324
- instantiation.flags & OutputSymbolFlags.InstanceMemberContainer,
352
+ instantiation.flags & OutputSymbolFlags.StaticMemberContainer,
325
353
  ).toBeTruthy();
326
- expect(instantiation.instanceMemberScope).toBeDefined();
354
+ expect(instantiation.staticMemberScope).toBeDefined();
327
355
  const expectedRefkey = refkey(
328
356
  instantiation.refkeys[0],
329
357
  instance.refkeys[0],
330
358
  );
331
359
  expect(
332
- instantiation.instanceMemberScope!.symbolsByRefkey.get(expectedRefkey),
360
+ instantiation.staticMemberScope!.symbolsByRefkey.get(expectedRefkey),
333
361
  ).toBeDefined();
334
362
  });
335
363
 
336
- it("instantiates static symbols that are added after the instantiation", () => {
364
+ it("doesn't duplicate symbols", () => {
365
+ const binder = createOutputBinder();
366
+
367
+ const {
368
+ symbols: { rootSymbol, instantiation },
369
+ } = createScopeTree(binder, {
370
+ rootScope: {
371
+ symbols: {
372
+ rootSymbol: {
373
+ flags: OutputSymbolFlags.InstanceMemberContainer,
374
+ instanceMembers: {
375
+ instance: {
376
+ flags: OutputSymbolFlags.InstanceMember,
377
+ },
378
+ },
379
+ },
380
+ instantiation: {},
381
+ },
382
+ },
383
+ });
384
+
385
+ binder.instantiateSymbolInto(rootSymbol, instantiation);
386
+ flushJobs();
387
+ expect(instantiation.staticMemberScope!.symbols.size).toBe(1);
388
+
389
+ const lateKey = refkey();
390
+ // now add a brand‐new static member to source
391
+ binder.createSymbol({
392
+ name: "lateChild",
393
+ scope: rootSymbol.instanceMemberScope!,
394
+ refkey: lateKey,
395
+ flags: OutputSymbolFlags.InstanceMember,
396
+ });
397
+
398
+ flushJobs();
399
+
400
+ expect(rootSymbol.instanceMemberScope!.symbols.size).toBe(2);
401
+ expect(instantiation.staticMemberScope!.symbols.size).toBe(2);
402
+ });
403
+
404
+ it("should remove members in instance when source deleted them", () => {
405
+ const binder = createOutputBinder();
406
+
407
+ const {
408
+ symbols: { rootSymbol, instantiation },
409
+ } = createScopeTree(binder, {
410
+ rootScope: {
411
+ symbols: {
412
+ rootSymbol: {
413
+ flags: OutputSymbolFlags.InstanceMemberContainer,
414
+ instanceMembers: {
415
+ instance: {
416
+ flags: OutputSymbolFlags.InstanceMember,
417
+ },
418
+ },
419
+ },
420
+ instantiation: {},
421
+ },
422
+ },
423
+ });
424
+
425
+ binder.instantiateSymbolInto(rootSymbol, instantiation);
426
+ expect(instantiation.staticMemberScope!.symbols.size).toBe(1);
427
+
428
+ const lateKey = refkey();
429
+ // now add a brand‐new static member to source
430
+ binder.createSymbol({
431
+ name: "lateChild",
432
+ scope: rootSymbol.instanceMemberScope!,
433
+ refkey: lateKey,
434
+ flags: OutputSymbolFlags.InstanceMember,
435
+ });
436
+
437
+ flushJobs();
438
+
439
+ expect(rootSymbol.instanceMemberScope!.symbols.size).toBe(2);
440
+ expect(instantiation.staticMemberScope!.symbols.size).toBe(2);
441
+
442
+ binder.deleteSymbol(
443
+ rootSymbol.instanceMemberScope!.symbols.values().next().value!,
444
+ );
445
+ flushJobs();
446
+ expect(rootSymbol.instanceMemberScope!.symbols.size).toBe(1);
447
+ expect(instantiation.staticMemberScope!.symbols.size).toBe(1);
448
+ });
449
+
450
+ it("instantiates instance members added after the instantiation", () => {
337
451
  const binder = createOutputBinder();
452
+
338
453
  const {
339
454
  symbols: { rootSymbol, instance, instantiation },
340
455
  } = createScopeTree(binder, {
@@ -344,7 +459,7 @@ describe("instantiating members", () => {
344
459
  flags: OutputSymbolFlags.InstanceMemberContainer,
345
460
  instanceMembers: {
346
461
  instance: {
347
- flags: OutputSymbolFlags.StaticMember,
462
+ flags: OutputSymbolFlags.InstanceMember,
348
463
  },
349
464
  },
350
465
  },
@@ -354,16 +469,17 @@ describe("instantiating members", () => {
354
469
  });
355
470
 
356
471
  binder.instantiateSymbolInto(rootSymbol, instantiation);
472
+ flushJobs();
357
473
  expect(
358
- instantiation.flags & OutputSymbolFlags.InstanceMemberContainer,
474
+ instantiation.flags & OutputSymbolFlags.StaticMemberContainer,
359
475
  ).toBeTruthy();
360
- expect(instantiation.instanceMemberScope).toBeDefined();
476
+ expect(instantiation.staticMemberScope).toBeDefined();
361
477
  const expectedRefkey = refkey(
362
478
  instantiation.refkeys[0],
363
479
  instance.refkeys[0],
364
480
  );
365
481
  expect(
366
- instantiation.instanceMemberScope!.symbolsByRefkey.get(expectedRefkey),
482
+ instantiation.staticMemberScope!.symbolsByRefkey.get(expectedRefkey),
367
483
  ).toBeDefined();
368
484
 
369
485
  const newInstanceMemberRefkey = refkey();
@@ -377,10 +493,280 @@ describe("instantiating members", () => {
377
493
  instantiation.refkeys[0],
378
494
  newInstanceMemberRefkey,
379
495
  );
496
+ flushJobs();
497
+ expect(
498
+ instantiation.staticMemberScope!.symbolsByRefkey.get(newExpectedRefkey),
499
+ ).toBeDefined();
500
+ });
501
+
502
+ it("instantiates static symbols for a static container source", () => {
503
+ const binder = createOutputBinder();
504
+
505
+ /**
506
+ * The following structure would match code like this:
507
+ * ```ts
508
+ * // A class with instance members
509
+ * class Source {
510
+ * static child() {
511
+ * print("child");
512
+ * }
513
+ * }
514
+ *
515
+ *
516
+ * var printChild = Source.child;
517
+ *
518
+ * printChild();
519
+ * ```
520
+ */
521
+ const {
522
+ symbols: { source, child, target },
523
+ } = createScopeTree(binder, {
524
+ root: {
525
+ symbols: {
526
+ source: {
527
+ flags: OutputSymbolFlags.StaticMemberContainer,
528
+ staticMembers: {
529
+ child: { flags: OutputSymbolFlags.StaticMember },
530
+ },
531
+ },
532
+ target: {},
533
+ },
534
+ },
535
+ });
536
+
537
+ binder.instantiateSymbolInto(source, target);
538
+
539
+ // target must now be a StaticMemberContainer too
540
+ expect(target.flags & OutputSymbolFlags.StaticMemberContainer).toBeTruthy();
541
+ expect(target.staticMemberScope).toBeDefined();
542
+
543
+ const expectedKey = refkey(target.refkeys[0], child.refkeys[0]);
544
+ expect(
545
+ target.staticMemberScope!.symbolsByRefkey.get(expectedKey),
546
+ ).toBeDefined();
547
+ });
548
+
549
+ it("instantiates static symbols added after instantiation", () => {
550
+ const binder = createOutputBinder();
551
+ const lateKey = refkey();
552
+
553
+ const {
554
+ symbols: { source, target },
555
+ } = createScopeTree(binder, {
556
+ root: {
557
+ symbols: {
558
+ source: {
559
+ flags: OutputSymbolFlags.StaticMemberContainer,
560
+ },
561
+ target: {},
562
+ },
563
+ },
564
+ });
565
+
566
+ // hook up instantiation
567
+ binder.instantiateSymbolInto(source, target);
568
+
569
+ // now add a brand‐new static member to source
570
+ const late = binder.createSymbol({
571
+ name: "lateChild",
572
+ scope: source.staticMemberScope!,
573
+ refkey: lateKey,
574
+ flags: OutputSymbolFlags.StaticMember,
575
+ });
576
+
577
+ flushJobs();
578
+
579
+ // it should *automatically* show up on target.staticMemberScope
580
+ const expectedKey = refkey(target.refkeys[0], late.refkeys[0]);
380
581
  expect(
381
- instantiation.instanceMemberScope!.symbolsByRefkey.get(newExpectedRefkey),
582
+ target.staticMemberScope!.symbolsByRefkey.get(expectedKey),
382
583
  ).toBeDefined();
383
584
  });
585
+
586
+ it("recursively instantiates nested static members", () => {
587
+ const binder = createOutputBinder();
588
+
589
+ /**
590
+ * The following structure would match code like this:
591
+ * ```ts
592
+ * class Source {
593
+ * static Level1 = class {
594
+ * static level2() { print("deep"); }
595
+ * }
596
+ * }
597
+ *
598
+ * var target = Source;
599
+ *
600
+ * target.Level1.level2()
601
+ * ```
602
+ */
603
+ const {
604
+ symbols: { source, level1, level2, target },
605
+ } = createScopeTree(binder, {
606
+ root: {
607
+ symbols: {
608
+ source: {
609
+ flags: OutputSymbolFlags.StaticMemberContainer,
610
+ staticMembers: {
611
+ level1: {
612
+ flags:
613
+ OutputSymbolFlags.StaticMember |
614
+ OutputSymbolFlags.StaticMemberContainer,
615
+ staticMembers: {
616
+ level2: { flags: OutputSymbolFlags.StaticMember },
617
+ },
618
+ },
619
+ },
620
+ },
621
+ target: {},
622
+ },
623
+ },
624
+ });
625
+
626
+ binder.instantiateSymbolInto(source, target);
627
+
628
+ // level1 should appear under target.staticMemberScope
629
+ const key1 = refkey(target.refkeys[0], level1.refkeys[0]);
630
+ const instantiated1 = target.staticMemberScope!.symbolsByRefkey.get(key1)!;
631
+ expect(instantiated1.name).toBe(level1.name);
632
+
633
+ // and level2 should appear under the *child* staticMemberScope of that instantiated level1
634
+ const childScope = instantiated1.staticMemberScope!;
635
+ const key2 = refkey(instantiated1.refkeys[0], level2.refkeys[0]);
636
+ expect(childScope.symbolsByRefkey.get(key2)).toBeDefined();
637
+ });
638
+
639
+ it("copies both instance *and* static members when source has both flags", () => {
640
+ const binder = createOutputBinder();
641
+
642
+ /**
643
+ * ```ts
644
+ * class Source {
645
+ * instance() { print("inst"); }
646
+ * static s1() { print("static"); }
647
+ * }
648
+ *
649
+ * let t = new Source()
650
+ * t.instance()
651
+ * t.s1()
652
+ * ```
653
+ */
654
+ const {
655
+ symbols: { source, inst },
656
+ } = createScopeTree(binder, {
657
+ root: {
658
+ symbols: {
659
+ source: {
660
+ flags:
661
+ OutputSymbolFlags.InstanceMemberContainer |
662
+ OutputSymbolFlags.StaticMemberContainer,
663
+ instanceMembers: {
664
+ i1: { flags: OutputSymbolFlags.InstanceMember },
665
+ },
666
+ staticMembers: {
667
+ s1: { flags: OutputSymbolFlags.StaticMember },
668
+ },
669
+ },
670
+ inst: {},
671
+ },
672
+ },
673
+ });
674
+
675
+ binder.instantiateSymbolInto(source, inst);
676
+
677
+ expect(inst.staticMemberScope).toBeDefined();
678
+ expect(
679
+ [...inst.staticMemberScope!.symbols].some((s) => s.name === "i1"),
680
+ ).toBe(true);
681
+
682
+ // static side
683
+ const symbols = [...source.staticMemberScope!.symbols];
684
+ expect(inst.staticMemberScope).toBeDefined();
685
+ const sKey = refkey(inst.refkeys[0], symbols[0].refkeys[0]);
686
+ expect(inst.staticMemberScope!.symbolsByRefkey.has(sKey)).toBe(true);
687
+ });
688
+
689
+ it("is idempotent, calling twice does not duplicate", () => {
690
+ const binder = createOutputBinder();
691
+ const {
692
+ symbols: { source, target },
693
+ } = createScopeTree(binder, {
694
+ root: {
695
+ symbols: {
696
+ source: {
697
+ flags: OutputSymbolFlags.StaticMemberContainer,
698
+ staticMembers: {
699
+ a: { flags: OutputSymbolFlags.StaticMember },
700
+ },
701
+ },
702
+ target: {},
703
+ },
704
+ },
705
+ });
706
+
707
+ binder.instantiateSymbolInto(source, target);
708
+ flushJobs();
709
+ const initialCount = target.staticMemberScope!.symbols.size;
710
+ binder.instantiateSymbolInto(source, target);
711
+ flushJobs();
712
+ expect(target.staticMemberScope!.symbols.size).toBe(initialCount);
713
+ });
714
+
715
+ it("instantiates static children of instance members under the instance scope", () => {
716
+ const binder = createOutputBinder();
717
+ /**
718
+ * ```ts
719
+ * class Source {
720
+ * instM = class {
721
+ * static deep() { print("deep"); }
722
+ * }
723
+ * }
724
+ *
725
+ * var t = new Source();
726
+ * t.instM.deep();
727
+ * ```
728
+ */
729
+ const {
730
+ symbols: { source, deep, target },
731
+ } = createScopeTree(binder, {
732
+ root: {
733
+ symbols: {
734
+ source: {
735
+ flags: OutputSymbolFlags.InstanceMemberContainer,
736
+ instanceMembers: {
737
+ instM: {
738
+ flags:
739
+ OutputSymbolFlags.InstanceMember |
740
+ OutputSymbolFlags.StaticMemberContainer,
741
+ staticMembers: {
742
+ deep: { flags: OutputSymbolFlags.StaticMember },
743
+ },
744
+ },
745
+ },
746
+ },
747
+ target: {},
748
+ },
749
+ },
750
+ });
751
+
752
+ binder.instantiateSymbolInto(source, target);
753
+
754
+ // Find the instantiated copy of instM under target.instanceMemberScope
755
+ const instMSym = [...target.staticMemberScope!.symbols].find(
756
+ (s) => s.name === "instM",
757
+ )!;
758
+
759
+ // instMSym should have gotten its own staticMemberScope via the StaticMemberContainer flag
760
+ expect(instMSym.staticMemberScope).toBeDefined();
761
+
762
+ // compute the expected key for the deep child:
763
+ // (<target>, <instM>) then (on that) (<deep original>)
764
+ const expectedDeepKey = refkey(instMSym.refkeys[0], deep.refkeys[0]);
765
+
766
+ expect(
767
+ instMSym.staticMemberScope!.symbolsByRefkey.has(expectedDeepKey),
768
+ ).toBe(true);
769
+ });
384
770
  });
385
771
 
386
772
  describe("symbol name resolution", () => {
@@ -540,6 +926,7 @@ describe("refkey resolution", () => {
540
926
  expect(resolvedSym.value).toBe(undefined);
541
927
 
542
928
  sym.refkeys[0] = key;
929
+ flushJobs();
543
930
  expect(resolvedSym.value?.targetDeclaration).toBe(sym);
544
931
  });
545
932
  });
@@ -563,10 +950,11 @@ describe("Deleting symbols", () => {
563
950
  expect(resolvedSym.value).toBe(undefined);
564
951
 
565
952
  sym.refkeys[0] = key;
953
+ flushJobs();
566
954
  expect(resolvedSym.value?.targetDeclaration).toBe(sym);
567
955
 
568
956
  binder.deleteSymbol(sym);
569
-
957
+ flushJobs();
570
958
  expect(resolvedSym.value).toBe(undefined);
571
959
  });
572
960
 
@@ -2,6 +2,7 @@ import { Children } from "@alloy-js/core/jsx-runtime";
2
2
  import { computed, ref, triggerRef } from "@vue/reactivity";
3
3
  import { describe, expect, it } from "vitest";
4
4
  import { renderTree } from "../src/render.js";
5
+ import { flushJobs } from "../src/scheduler.js";
5
6
  import { children, join, mapJoin } from "../src/utils.js";
6
7
  import "../testing/extend-expect.js";
7
8
 
@@ -93,6 +94,7 @@ describe("mapJoin", () => {
93
94
  expect(callCount).toBe(2);
94
95
  arr.value.push(3);
95
96
  triggerRef(arr);
97
+ flushJobs();
96
98
  expect(callCount).toBe(3);
97
99
  });
98
100
  it("can map a joiner", () => {