@alloy-js/core 0.11.0 → 0.12.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 +15 -0
  2. package/dist/src/binder.d.ts.map +1 -1
  3. package/dist/src/binder.js +67 -17
  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 +384 -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 +2 -2
  26. package/src/binder.ts +100 -17
  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 +392 -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
 
@@ -299,8 +301,25 @@ describe("instance members", () => {
299
301
  });
300
302
 
301
303
  describe("instantiating members", () => {
302
- it("instantiates static symbols", () => {
304
+ it("instantiates instance members", () => {
303
305
  const binder = createOutputBinder();
306
+
307
+ /**
308
+ * The following structure would match code like this:
309
+ * ```ts
310
+ * // A class with instance members
311
+ * class Source {
312
+ * instance() {
313
+ * print("instance");
314
+ * }
315
+ * }
316
+ *
317
+ * // Instantiates into t
318
+ * var t = new Source();
319
+ *
320
+ * t.instance();
321
+ * ```
322
+ */
304
323
  const {
305
324
  symbols: { rootSymbol, instance, instantiation },
306
325
  } = createScopeTree(binder, {
@@ -310,7 +329,7 @@ describe("instantiating members", () => {
310
329
  flags: OutputSymbolFlags.InstanceMemberContainer,
311
330
  instanceMembers: {
312
331
  instance: {
313
- flags: OutputSymbolFlags.StaticMember,
332
+ flags: OutputSymbolFlags.InstanceMember,
314
333
  },
315
334
  },
316
335
  },
@@ -321,20 +340,107 @@ describe("instantiating members", () => {
321
340
 
322
341
  binder.instantiateSymbolInto(rootSymbol, instantiation);
323
342
  expect(
324
- instantiation.flags & OutputSymbolFlags.InstanceMemberContainer,
343
+ instantiation.flags & OutputSymbolFlags.StaticMemberContainer,
325
344
  ).toBeTruthy();
326
- expect(instantiation.instanceMemberScope).toBeDefined();
345
+ expect(instantiation.staticMemberScope).toBeDefined();
327
346
  const expectedRefkey = refkey(
328
347
  instantiation.refkeys[0],
329
348
  instance.refkeys[0],
330
349
  );
331
350
  expect(
332
- instantiation.instanceMemberScope!.symbolsByRefkey.get(expectedRefkey),
351
+ instantiation.staticMemberScope!.symbolsByRefkey.get(expectedRefkey),
333
352
  ).toBeDefined();
334
353
  });
335
354
 
336
- it("instantiates static symbols that are added after the instantiation", () => {
355
+ it("doesn't duplicate symbols", () => {
356
+ const binder = createOutputBinder();
357
+
358
+ const {
359
+ symbols: { rootSymbol, instantiation },
360
+ } = createScopeTree(binder, {
361
+ rootScope: {
362
+ symbols: {
363
+ rootSymbol: {
364
+ flags: OutputSymbolFlags.InstanceMemberContainer,
365
+ instanceMembers: {
366
+ instance: {
367
+ flags: OutputSymbolFlags.InstanceMember,
368
+ },
369
+ },
370
+ },
371
+ instantiation: {},
372
+ },
373
+ },
374
+ });
375
+
376
+ binder.instantiateSymbolInto(rootSymbol, instantiation);
377
+ flushJobs();
378
+ expect(instantiation.staticMemberScope!.symbols.size).toBe(1);
379
+
380
+ const lateKey = refkey();
381
+ // now add a brand‐new static member to source
382
+ binder.createSymbol({
383
+ name: "lateChild",
384
+ scope: rootSymbol.instanceMemberScope!,
385
+ refkey: lateKey,
386
+ flags: OutputSymbolFlags.InstanceMember,
387
+ });
388
+
389
+ flushJobs();
390
+
391
+ expect(rootSymbol.instanceMemberScope!.symbols.size).toBe(2);
392
+ expect(instantiation.staticMemberScope!.symbols.size).toBe(2);
393
+ });
394
+
395
+ it("should remove members in instance when source deleted them", () => {
396
+ const binder = createOutputBinder();
397
+
398
+ const {
399
+ symbols: { rootSymbol, instantiation },
400
+ } = createScopeTree(binder, {
401
+ rootScope: {
402
+ symbols: {
403
+ rootSymbol: {
404
+ flags: OutputSymbolFlags.InstanceMemberContainer,
405
+ instanceMembers: {
406
+ instance: {
407
+ flags: OutputSymbolFlags.InstanceMember,
408
+ },
409
+ },
410
+ },
411
+ instantiation: {},
412
+ },
413
+ },
414
+ });
415
+
416
+ binder.instantiateSymbolInto(rootSymbol, instantiation);
417
+ expect(instantiation.staticMemberScope!.symbols.size).toBe(1);
418
+
419
+ const lateKey = refkey();
420
+ // now add a brand‐new static member to source
421
+ binder.createSymbol({
422
+ name: "lateChild",
423
+ scope: rootSymbol.instanceMemberScope!,
424
+ refkey: lateKey,
425
+ flags: OutputSymbolFlags.InstanceMember,
426
+ });
427
+
428
+ flushJobs();
429
+
430
+ expect(rootSymbol.instanceMemberScope!.symbols.size).toBe(2);
431
+ expect(instantiation.staticMemberScope!.symbols.size).toBe(2);
432
+
433
+ binder.deleteSymbol(
434
+ rootSymbol.instanceMemberScope!.symbols.values().next().value!,
435
+ );
436
+ flushJobs();
437
+ expect(rootSymbol.instanceMemberScope!.symbols.size).toBe(1);
438
+ expect(instantiation.staticMemberScope!.symbols.size).toBe(1);
439
+ });
440
+
441
+ it("instantiates instance members added after the instantiation", () => {
337
442
  const binder = createOutputBinder();
443
+
338
444
  const {
339
445
  symbols: { rootSymbol, instance, instantiation },
340
446
  } = createScopeTree(binder, {
@@ -344,7 +450,7 @@ describe("instantiating members", () => {
344
450
  flags: OutputSymbolFlags.InstanceMemberContainer,
345
451
  instanceMembers: {
346
452
  instance: {
347
- flags: OutputSymbolFlags.StaticMember,
453
+ flags: OutputSymbolFlags.InstanceMember,
348
454
  },
349
455
  },
350
456
  },
@@ -354,16 +460,17 @@ describe("instantiating members", () => {
354
460
  });
355
461
 
356
462
  binder.instantiateSymbolInto(rootSymbol, instantiation);
463
+ flushJobs();
357
464
  expect(
358
- instantiation.flags & OutputSymbolFlags.InstanceMemberContainer,
465
+ instantiation.flags & OutputSymbolFlags.StaticMemberContainer,
359
466
  ).toBeTruthy();
360
- expect(instantiation.instanceMemberScope).toBeDefined();
467
+ expect(instantiation.staticMemberScope).toBeDefined();
361
468
  const expectedRefkey = refkey(
362
469
  instantiation.refkeys[0],
363
470
  instance.refkeys[0],
364
471
  );
365
472
  expect(
366
- instantiation.instanceMemberScope!.symbolsByRefkey.get(expectedRefkey),
473
+ instantiation.staticMemberScope!.symbolsByRefkey.get(expectedRefkey),
367
474
  ).toBeDefined();
368
475
 
369
476
  const newInstanceMemberRefkey = refkey();
@@ -377,10 +484,280 @@ describe("instantiating members", () => {
377
484
  instantiation.refkeys[0],
378
485
  newInstanceMemberRefkey,
379
486
  );
487
+ flushJobs();
488
+ expect(
489
+ instantiation.staticMemberScope!.symbolsByRefkey.get(newExpectedRefkey),
490
+ ).toBeDefined();
491
+ });
492
+
493
+ it("instantiates static symbols for a static container source", () => {
494
+ const binder = createOutputBinder();
495
+
496
+ /**
497
+ * The following structure would match code like this:
498
+ * ```ts
499
+ * // A class with instance members
500
+ * class Source {
501
+ * static child() {
502
+ * print("child");
503
+ * }
504
+ * }
505
+ *
506
+ *
507
+ * var printChild = Source.child;
508
+ *
509
+ * printChild();
510
+ * ```
511
+ */
512
+ const {
513
+ symbols: { source, child, target },
514
+ } = createScopeTree(binder, {
515
+ root: {
516
+ symbols: {
517
+ source: {
518
+ flags: OutputSymbolFlags.StaticMemberContainer,
519
+ staticMembers: {
520
+ child: { flags: OutputSymbolFlags.StaticMember },
521
+ },
522
+ },
523
+ target: {},
524
+ },
525
+ },
526
+ });
527
+
528
+ binder.instantiateSymbolInto(source, target);
529
+
530
+ // target must now be a StaticMemberContainer too
531
+ expect(target.flags & OutputSymbolFlags.StaticMemberContainer).toBeTruthy();
532
+ expect(target.staticMemberScope).toBeDefined();
533
+
534
+ const expectedKey = refkey(target.refkeys[0], child.refkeys[0]);
380
535
  expect(
381
- instantiation.instanceMemberScope!.symbolsByRefkey.get(newExpectedRefkey),
536
+ target.staticMemberScope!.symbolsByRefkey.get(expectedKey),
382
537
  ).toBeDefined();
383
538
  });
539
+
540
+ it("instantiates static symbols added after instantiation", () => {
541
+ const binder = createOutputBinder();
542
+ const lateKey = refkey();
543
+
544
+ const {
545
+ symbols: { source, target },
546
+ } = createScopeTree(binder, {
547
+ root: {
548
+ symbols: {
549
+ source: {
550
+ flags: OutputSymbolFlags.StaticMemberContainer,
551
+ },
552
+ target: {},
553
+ },
554
+ },
555
+ });
556
+
557
+ // hook up instantiation
558
+ binder.instantiateSymbolInto(source, target);
559
+
560
+ // now add a brand‐new static member to source
561
+ const late = binder.createSymbol({
562
+ name: "lateChild",
563
+ scope: source.staticMemberScope!,
564
+ refkey: lateKey,
565
+ flags: OutputSymbolFlags.StaticMember,
566
+ });
567
+
568
+ flushJobs();
569
+
570
+ // it should *automatically* show up on target.staticMemberScope
571
+ const expectedKey = refkey(target.refkeys[0], late.refkeys[0]);
572
+ expect(
573
+ target.staticMemberScope!.symbolsByRefkey.get(expectedKey),
574
+ ).toBeDefined();
575
+ });
576
+
577
+ it("recursively instantiates nested static members", () => {
578
+ const binder = createOutputBinder();
579
+
580
+ /**
581
+ * The following structure would match code like this:
582
+ * ```ts
583
+ * class Source {
584
+ * static Level1 = class {
585
+ * static level2() { print("deep"); }
586
+ * }
587
+ * }
588
+ *
589
+ * var target = Source;
590
+ *
591
+ * target.Level1.level2()
592
+ * ```
593
+ */
594
+ const {
595
+ symbols: { source, level1, level2, target },
596
+ } = createScopeTree(binder, {
597
+ root: {
598
+ symbols: {
599
+ source: {
600
+ flags: OutputSymbolFlags.StaticMemberContainer,
601
+ staticMembers: {
602
+ level1: {
603
+ flags:
604
+ OutputSymbolFlags.StaticMember |
605
+ OutputSymbolFlags.StaticMemberContainer,
606
+ staticMembers: {
607
+ level2: { flags: OutputSymbolFlags.StaticMember },
608
+ },
609
+ },
610
+ },
611
+ },
612
+ target: {},
613
+ },
614
+ },
615
+ });
616
+
617
+ binder.instantiateSymbolInto(source, target);
618
+
619
+ // level1 should appear under target.staticMemberScope
620
+ const key1 = refkey(target.refkeys[0], level1.refkeys[0]);
621
+ const instantiated1 = target.staticMemberScope!.symbolsByRefkey.get(key1)!;
622
+ expect(instantiated1.name).toBe(level1.name);
623
+
624
+ // and level2 should appear under the *child* staticMemberScope of that instantiated level1
625
+ const childScope = instantiated1.staticMemberScope!;
626
+ const key2 = refkey(instantiated1.refkeys[0], level2.refkeys[0]);
627
+ expect(childScope.symbolsByRefkey.get(key2)).toBeDefined();
628
+ });
629
+
630
+ it("copies both instance *and* static members when source has both flags", () => {
631
+ const binder = createOutputBinder();
632
+
633
+ /**
634
+ * ```ts
635
+ * class Source {
636
+ * instance() { print("inst"); }
637
+ * static s1() { print("static"); }
638
+ * }
639
+ *
640
+ * let t = new Source()
641
+ * t.instance()
642
+ * t.s1()
643
+ * ```
644
+ */
645
+ const {
646
+ symbols: { source, inst },
647
+ } = createScopeTree(binder, {
648
+ root: {
649
+ symbols: {
650
+ source: {
651
+ flags:
652
+ OutputSymbolFlags.InstanceMemberContainer |
653
+ OutputSymbolFlags.StaticMemberContainer,
654
+ instanceMembers: {
655
+ i1: { flags: OutputSymbolFlags.InstanceMember },
656
+ },
657
+ staticMembers: {
658
+ s1: { flags: OutputSymbolFlags.StaticMember },
659
+ },
660
+ },
661
+ inst: {},
662
+ },
663
+ },
664
+ });
665
+
666
+ binder.instantiateSymbolInto(source, inst);
667
+
668
+ expect(inst.staticMemberScope).toBeDefined();
669
+ expect(
670
+ [...inst.staticMemberScope!.symbols].some((s) => s.name === "i1"),
671
+ ).toBe(true);
672
+
673
+ // static side
674
+ const symbols = [...source.staticMemberScope!.symbols];
675
+ expect(inst.staticMemberScope).toBeDefined();
676
+ const sKey = refkey(inst.refkeys[0], symbols[0].refkeys[0]);
677
+ expect(inst.staticMemberScope!.symbolsByRefkey.has(sKey)).toBe(true);
678
+ });
679
+
680
+ it("is idempotent, calling twice does not duplicate", () => {
681
+ const binder = createOutputBinder();
682
+ const {
683
+ symbols: { source, target },
684
+ } = createScopeTree(binder, {
685
+ root: {
686
+ symbols: {
687
+ source: {
688
+ flags: OutputSymbolFlags.StaticMemberContainer,
689
+ staticMembers: {
690
+ a: { flags: OutputSymbolFlags.StaticMember },
691
+ },
692
+ },
693
+ target: {},
694
+ },
695
+ },
696
+ });
697
+
698
+ binder.instantiateSymbolInto(source, target);
699
+ flushJobs();
700
+ const initialCount = target.staticMemberScope!.symbols.size;
701
+ binder.instantiateSymbolInto(source, target);
702
+ flushJobs();
703
+ expect(target.staticMemberScope!.symbols.size).toBe(initialCount);
704
+ });
705
+
706
+ it("instantiates static children of instance members under the instance scope", () => {
707
+ const binder = createOutputBinder();
708
+ /**
709
+ * ```ts
710
+ * class Source {
711
+ * instM = class {
712
+ * static deep() { print("deep"); }
713
+ * }
714
+ * }
715
+ *
716
+ * var t = new Source();
717
+ * t.instM.deep();
718
+ * ```
719
+ */
720
+ const {
721
+ symbols: { source, deep, target },
722
+ } = createScopeTree(binder, {
723
+ root: {
724
+ symbols: {
725
+ source: {
726
+ flags: OutputSymbolFlags.InstanceMemberContainer,
727
+ instanceMembers: {
728
+ instM: {
729
+ flags:
730
+ OutputSymbolFlags.InstanceMember |
731
+ OutputSymbolFlags.StaticMemberContainer,
732
+ staticMembers: {
733
+ deep: { flags: OutputSymbolFlags.StaticMember },
734
+ },
735
+ },
736
+ },
737
+ },
738
+ target: {},
739
+ },
740
+ },
741
+ });
742
+
743
+ binder.instantiateSymbolInto(source, target);
744
+
745
+ // Find the instantiated copy of instM under target.instanceMemberScope
746
+ const instMSym = [...target.staticMemberScope!.symbols].find(
747
+ (s) => s.name === "instM",
748
+ )!;
749
+
750
+ // instMSym should have gotten its own staticMemberScope via the StaticMemberContainer flag
751
+ expect(instMSym.staticMemberScope).toBeDefined();
752
+
753
+ // compute the expected key for the deep child:
754
+ // (<target>, <instM>) then (on that) (<deep original>)
755
+ const expectedDeepKey = refkey(instMSym.refkeys[0], deep.refkeys[0]);
756
+
757
+ expect(
758
+ instMSym.staticMemberScope!.symbolsByRefkey.has(expectedDeepKey),
759
+ ).toBe(true);
760
+ });
384
761
  });
385
762
 
386
763
  describe("symbol name resolution", () => {
@@ -540,6 +917,7 @@ describe("refkey resolution", () => {
540
917
  expect(resolvedSym.value).toBe(undefined);
541
918
 
542
919
  sym.refkeys[0] = key;
920
+ flushJobs();
543
921
  expect(resolvedSym.value?.targetDeclaration).toBe(sym);
544
922
  });
545
923
  });
@@ -563,10 +941,11 @@ describe("Deleting symbols", () => {
563
941
  expect(resolvedSym.value).toBe(undefined);
564
942
 
565
943
  sym.refkeys[0] = key;
944
+ flushJobs();
566
945
  expect(resolvedSym.value?.targetDeclaration).toBe(sym);
567
946
 
568
947
  binder.deleteSymbol(sym);
569
-
948
+ flushJobs();
570
949
  expect(resolvedSym.value).toBe(undefined);
571
950
  });
572
951
 
@@ -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", () => {