@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
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import { createOutputBinder, OutputScopeFlags, OutputSymbolFlags } from "../src/binder.js";
3
3
  import { refkey } from "../src/refkey.js";
4
+ import { flushJobs } from "../src/scheduler.js";
4
5
  it("works", () => {
5
6
  const binder = createOutputBinder();
6
7
  const scope = binder.createScope({
@@ -12,8 +13,10 @@ it("works", () => {
12
13
  name: "sym",
13
14
  scope
14
15
  });
16
+ flushJobs();
15
17
  expect([...scope.getSymbolNames()]).toEqual(["sym"]);
16
18
  symbol.name = "bar";
19
+ flushJobs();
17
20
  expect([...scope.getSymbolNames()]).toEqual(["bar"]);
18
21
  });
19
22
  it("resolves symbol conflicts", () => {
@@ -31,7 +34,14 @@ it("resolves symbol conflicts", () => {
31
34
  name: "sym",
32
35
  scope
33
36
  });
37
+ const s3 = binder.createSymbol({
38
+ name: "sym",
39
+ scope
40
+ });
41
+ flushJobs();
42
+ expect(_s1.name).toEqual("sym");
34
43
  expect(s2.name).toEqual("sym_2");
44
+ expect(s3.name).toEqual("sym_3");
35
45
  });
36
46
  function createScopeTree(binder, tree) {
37
47
  const createdItems = {
@@ -242,8 +252,25 @@ describe("instance members", () => {
242
252
  });
243
253
  });
244
254
  describe("instantiating members", () => {
245
- it("instantiates static symbols", () => {
255
+ it("instantiates instance members", () => {
246
256
  const binder = createOutputBinder();
257
+
258
+ /**
259
+ * The following structure would match code like this:
260
+ * ```ts
261
+ * // A class with instance members
262
+ * class Source {
263
+ * instance() {
264
+ * print("instance");
265
+ * }
266
+ * }
267
+ *
268
+ * // Instantiates into t
269
+ * var t = new Source();
270
+ *
271
+ * t.instance();
272
+ * ```
273
+ */
247
274
  const {
248
275
  symbols: {
249
276
  rootSymbol,
@@ -257,7 +284,7 @@ describe("instantiating members", () => {
257
284
  flags: OutputSymbolFlags.InstanceMemberContainer,
258
285
  instanceMembers: {
259
286
  instance: {
260
- flags: OutputSymbolFlags.StaticMember
287
+ flags: OutputSymbolFlags.InstanceMember
261
288
  }
262
289
  }
263
290
  },
@@ -266,12 +293,89 @@ describe("instantiating members", () => {
266
293
  }
267
294
  });
268
295
  binder.instantiateSymbolInto(rootSymbol, instantiation);
269
- expect(instantiation.flags & OutputSymbolFlags.InstanceMemberContainer).toBeTruthy();
270
- expect(instantiation.instanceMemberScope).toBeDefined();
296
+ expect(instantiation.flags & OutputSymbolFlags.StaticMemberContainer).toBeTruthy();
297
+ expect(instantiation.staticMemberScope).toBeDefined();
271
298
  const expectedRefkey = refkey(instantiation.refkeys[0], instance.refkeys[0]);
272
- expect(instantiation.instanceMemberScope.symbolsByRefkey.get(expectedRefkey)).toBeDefined();
299
+ expect(instantiation.staticMemberScope.symbolsByRefkey.get(expectedRefkey)).toBeDefined();
300
+ });
301
+ it("doesn't duplicate symbols", () => {
302
+ const binder = createOutputBinder();
303
+ const {
304
+ symbols: {
305
+ rootSymbol,
306
+ instantiation
307
+ }
308
+ } = createScopeTree(binder, {
309
+ rootScope: {
310
+ symbols: {
311
+ rootSymbol: {
312
+ flags: OutputSymbolFlags.InstanceMemberContainer,
313
+ instanceMembers: {
314
+ instance: {
315
+ flags: OutputSymbolFlags.InstanceMember
316
+ }
317
+ }
318
+ },
319
+ instantiation: {}
320
+ }
321
+ }
322
+ });
323
+ binder.instantiateSymbolInto(rootSymbol, instantiation);
324
+ flushJobs();
325
+ expect(instantiation.staticMemberScope.symbols.size).toBe(1);
326
+ const lateKey = refkey();
327
+ // now add a brand‐new static member to source
328
+ binder.createSymbol({
329
+ name: "lateChild",
330
+ scope: rootSymbol.instanceMemberScope,
331
+ refkey: lateKey,
332
+ flags: OutputSymbolFlags.InstanceMember
333
+ });
334
+ flushJobs();
335
+ expect(rootSymbol.instanceMemberScope.symbols.size).toBe(2);
336
+ expect(instantiation.staticMemberScope.symbols.size).toBe(2);
337
+ });
338
+ it("should remove members in instance when source deleted them", () => {
339
+ const binder = createOutputBinder();
340
+ const {
341
+ symbols: {
342
+ rootSymbol,
343
+ instantiation
344
+ }
345
+ } = createScopeTree(binder, {
346
+ rootScope: {
347
+ symbols: {
348
+ rootSymbol: {
349
+ flags: OutputSymbolFlags.InstanceMemberContainer,
350
+ instanceMembers: {
351
+ instance: {
352
+ flags: OutputSymbolFlags.InstanceMember
353
+ }
354
+ }
355
+ },
356
+ instantiation: {}
357
+ }
358
+ }
359
+ });
360
+ binder.instantiateSymbolInto(rootSymbol, instantiation);
361
+ expect(instantiation.staticMemberScope.symbols.size).toBe(1);
362
+ const lateKey = refkey();
363
+ // now add a brand‐new static member to source
364
+ binder.createSymbol({
365
+ name: "lateChild",
366
+ scope: rootSymbol.instanceMemberScope,
367
+ refkey: lateKey,
368
+ flags: OutputSymbolFlags.InstanceMember
369
+ });
370
+ flushJobs();
371
+ expect(rootSymbol.instanceMemberScope.symbols.size).toBe(2);
372
+ expect(instantiation.staticMemberScope.symbols.size).toBe(2);
373
+ binder.deleteSymbol(rootSymbol.instanceMemberScope.symbols.values().next().value);
374
+ flushJobs();
375
+ expect(rootSymbol.instanceMemberScope.symbols.size).toBe(1);
376
+ expect(instantiation.staticMemberScope.symbols.size).toBe(1);
273
377
  });
274
- it("instantiates static symbols that are added after the instantiation", () => {
378
+ it("instantiates instance members added after the instantiation", () => {
275
379
  const binder = createOutputBinder();
276
380
  const {
277
381
  symbols: {
@@ -286,7 +390,7 @@ describe("instantiating members", () => {
286
390
  flags: OutputSymbolFlags.InstanceMemberContainer,
287
391
  instanceMembers: {
288
392
  instance: {
289
- flags: OutputSymbolFlags.StaticMember
393
+ flags: OutputSymbolFlags.InstanceMember
290
394
  }
291
395
  }
292
396
  },
@@ -295,10 +399,11 @@ describe("instantiating members", () => {
295
399
  }
296
400
  });
297
401
  binder.instantiateSymbolInto(rootSymbol, instantiation);
298
- expect(instantiation.flags & OutputSymbolFlags.InstanceMemberContainer).toBeTruthy();
299
- expect(instantiation.instanceMemberScope).toBeDefined();
402
+ flushJobs();
403
+ expect(instantiation.flags & OutputSymbolFlags.StaticMemberContainer).toBeTruthy();
404
+ expect(instantiation.staticMemberScope).toBeDefined();
300
405
  const expectedRefkey = refkey(instantiation.refkeys[0], instance.refkeys[0]);
301
- expect(instantiation.instanceMemberScope.symbolsByRefkey.get(expectedRefkey)).toBeDefined();
406
+ expect(instantiation.staticMemberScope.symbolsByRefkey.get(expectedRefkey)).toBeDefined();
302
407
  const newInstanceMemberRefkey = refkey();
303
408
  binder.createSymbol({
304
409
  name: "newInstanceMember",
@@ -307,7 +412,279 @@ describe("instantiating members", () => {
307
412
  flags: OutputSymbolFlags.InstanceMember
308
413
  });
309
414
  const newExpectedRefkey = refkey(instantiation.refkeys[0], newInstanceMemberRefkey);
310
- expect(instantiation.instanceMemberScope.symbolsByRefkey.get(newExpectedRefkey)).toBeDefined();
415
+ flushJobs();
416
+ expect(instantiation.staticMemberScope.symbolsByRefkey.get(newExpectedRefkey)).toBeDefined();
417
+ });
418
+ it("instantiates static symbols for a static container source", () => {
419
+ const binder = createOutputBinder();
420
+
421
+ /**
422
+ * The following structure would match code like this:
423
+ * ```ts
424
+ * // A class with instance members
425
+ * class Source {
426
+ * static child() {
427
+ * print("child");
428
+ * }
429
+ * }
430
+ *
431
+ *
432
+ * var printChild = Source.child;
433
+ *
434
+ * printChild();
435
+ * ```
436
+ */
437
+ const {
438
+ symbols: {
439
+ source,
440
+ child,
441
+ target
442
+ }
443
+ } = createScopeTree(binder, {
444
+ root: {
445
+ symbols: {
446
+ source: {
447
+ flags: OutputSymbolFlags.StaticMemberContainer,
448
+ staticMembers: {
449
+ child: {
450
+ flags: OutputSymbolFlags.StaticMember
451
+ }
452
+ }
453
+ },
454
+ target: {}
455
+ }
456
+ }
457
+ });
458
+ binder.instantiateSymbolInto(source, target);
459
+
460
+ // target must now be a StaticMemberContainer too
461
+ expect(target.flags & OutputSymbolFlags.StaticMemberContainer).toBeTruthy();
462
+ expect(target.staticMemberScope).toBeDefined();
463
+ const expectedKey = refkey(target.refkeys[0], child.refkeys[0]);
464
+ expect(target.staticMemberScope.symbolsByRefkey.get(expectedKey)).toBeDefined();
465
+ });
466
+ it("instantiates static symbols added after instantiation", () => {
467
+ const binder = createOutputBinder();
468
+ const lateKey = refkey();
469
+ const {
470
+ symbols: {
471
+ source,
472
+ target
473
+ }
474
+ } = createScopeTree(binder, {
475
+ root: {
476
+ symbols: {
477
+ source: {
478
+ flags: OutputSymbolFlags.StaticMemberContainer
479
+ },
480
+ target: {}
481
+ }
482
+ }
483
+ });
484
+
485
+ // hook up instantiation
486
+ binder.instantiateSymbolInto(source, target);
487
+
488
+ // now add a brand‐new static member to source
489
+ const late = binder.createSymbol({
490
+ name: "lateChild",
491
+ scope: source.staticMemberScope,
492
+ refkey: lateKey,
493
+ flags: OutputSymbolFlags.StaticMember
494
+ });
495
+ flushJobs();
496
+
497
+ // it should *automatically* show up on target.staticMemberScope
498
+ const expectedKey = refkey(target.refkeys[0], late.refkeys[0]);
499
+ expect(target.staticMemberScope.symbolsByRefkey.get(expectedKey)).toBeDefined();
500
+ });
501
+ it("recursively instantiates nested static members", () => {
502
+ const binder = createOutputBinder();
503
+
504
+ /**
505
+ * The following structure would match code like this:
506
+ * ```ts
507
+ * class Source {
508
+ * static Level1 = class {
509
+ * static level2() { print("deep"); }
510
+ * }
511
+ * }
512
+ *
513
+ * var target = Source;
514
+ *
515
+ * target.Level1.level2()
516
+ * ```
517
+ */
518
+ const {
519
+ symbols: {
520
+ source,
521
+ level1,
522
+ level2,
523
+ target
524
+ }
525
+ } = createScopeTree(binder, {
526
+ root: {
527
+ symbols: {
528
+ source: {
529
+ flags: OutputSymbolFlags.StaticMemberContainer,
530
+ staticMembers: {
531
+ level1: {
532
+ flags: OutputSymbolFlags.StaticMember | OutputSymbolFlags.StaticMemberContainer,
533
+ staticMembers: {
534
+ level2: {
535
+ flags: OutputSymbolFlags.StaticMember
536
+ }
537
+ }
538
+ }
539
+ }
540
+ },
541
+ target: {}
542
+ }
543
+ }
544
+ });
545
+ binder.instantiateSymbolInto(source, target);
546
+
547
+ // level1 should appear under target.staticMemberScope
548
+ const key1 = refkey(target.refkeys[0], level1.refkeys[0]);
549
+ const instantiated1 = target.staticMemberScope.symbolsByRefkey.get(key1);
550
+ expect(instantiated1.name).toBe(level1.name);
551
+
552
+ // and level2 should appear under the *child* staticMemberScope of that instantiated level1
553
+ const childScope = instantiated1.staticMemberScope;
554
+ const key2 = refkey(instantiated1.refkeys[0], level2.refkeys[0]);
555
+ expect(childScope.symbolsByRefkey.get(key2)).toBeDefined();
556
+ });
557
+ it("copies both instance *and* static members when source has both flags", () => {
558
+ const binder = createOutputBinder();
559
+
560
+ /**
561
+ * ```ts
562
+ * class Source {
563
+ * instance() { print("inst"); }
564
+ * static s1() { print("static"); }
565
+ * }
566
+ *
567
+ * let t = new Source()
568
+ * t.instance()
569
+ * t.s1()
570
+ * ```
571
+ */
572
+ const {
573
+ symbols: {
574
+ source,
575
+ inst
576
+ }
577
+ } = createScopeTree(binder, {
578
+ root: {
579
+ symbols: {
580
+ source: {
581
+ flags: OutputSymbolFlags.InstanceMemberContainer | OutputSymbolFlags.StaticMemberContainer,
582
+ instanceMembers: {
583
+ i1: {
584
+ flags: OutputSymbolFlags.InstanceMember
585
+ }
586
+ },
587
+ staticMembers: {
588
+ s1: {
589
+ flags: OutputSymbolFlags.StaticMember
590
+ }
591
+ }
592
+ },
593
+ inst: {}
594
+ }
595
+ }
596
+ });
597
+ binder.instantiateSymbolInto(source, inst);
598
+ expect(inst.staticMemberScope).toBeDefined();
599
+ expect([...inst.staticMemberScope.symbols].some(s => s.name === "i1")).toBe(true);
600
+
601
+ // static side
602
+ const symbols = [...source.staticMemberScope.symbols];
603
+ expect(inst.staticMemberScope).toBeDefined();
604
+ const sKey = refkey(inst.refkeys[0], symbols[0].refkeys[0]);
605
+ expect(inst.staticMemberScope.symbolsByRefkey.has(sKey)).toBe(true);
606
+ });
607
+ it("is idempotent, calling twice does not duplicate", () => {
608
+ const binder = createOutputBinder();
609
+ const {
610
+ symbols: {
611
+ source,
612
+ target
613
+ }
614
+ } = createScopeTree(binder, {
615
+ root: {
616
+ symbols: {
617
+ source: {
618
+ flags: OutputSymbolFlags.StaticMemberContainer,
619
+ staticMembers: {
620
+ a: {
621
+ flags: OutputSymbolFlags.StaticMember
622
+ }
623
+ }
624
+ },
625
+ target: {}
626
+ }
627
+ }
628
+ });
629
+ binder.instantiateSymbolInto(source, target);
630
+ flushJobs();
631
+ const initialCount = target.staticMemberScope.symbols.size;
632
+ binder.instantiateSymbolInto(source, target);
633
+ flushJobs();
634
+ expect(target.staticMemberScope.symbols.size).toBe(initialCount);
635
+ });
636
+ it("instantiates static children of instance members under the instance scope", () => {
637
+ const binder = createOutputBinder();
638
+ /**
639
+ * ```ts
640
+ * class Source {
641
+ * instM = class {
642
+ * static deep() { print("deep"); }
643
+ * }
644
+ * }
645
+ *
646
+ * var t = new Source();
647
+ * t.instM.deep();
648
+ * ```
649
+ */
650
+ const {
651
+ symbols: {
652
+ source,
653
+ deep,
654
+ target
655
+ }
656
+ } = createScopeTree(binder, {
657
+ root: {
658
+ symbols: {
659
+ source: {
660
+ flags: OutputSymbolFlags.InstanceMemberContainer,
661
+ instanceMembers: {
662
+ instM: {
663
+ flags: OutputSymbolFlags.InstanceMember | OutputSymbolFlags.StaticMemberContainer,
664
+ staticMembers: {
665
+ deep: {
666
+ flags: OutputSymbolFlags.StaticMember
667
+ }
668
+ }
669
+ }
670
+ }
671
+ },
672
+ target: {}
673
+ }
674
+ }
675
+ });
676
+ binder.instantiateSymbolInto(source, target);
677
+
678
+ // Find the instantiated copy of instM under target.instanceMemberScope
679
+ const instMSym = [...target.staticMemberScope.symbols].find(s => s.name === "instM");
680
+
681
+ // instMSym should have gotten its own staticMemberScope via the StaticMemberContainer flag
682
+ expect(instMSym.staticMemberScope).toBeDefined();
683
+
684
+ // compute the expected key for the deep child:
685
+ // (<target>, <instM>) then (on that) (<deep original>)
686
+ const expectedDeepKey = refkey(instMSym.refkeys[0], deep.refkeys[0]);
687
+ expect(instMSym.staticMemberScope.symbolsByRefkey.has(expectedDeepKey)).toBe(true);
311
688
  });
312
689
  });
313
690
  describe("symbol name resolution", () => {
@@ -439,6 +816,7 @@ describe("refkey resolution", () => {
439
816
  });
440
817
  expect(resolvedSym.value).toBe(undefined);
441
818
  sym.refkeys[0] = key;
819
+ flushJobs();
442
820
  expect(resolvedSym.value?.targetDeclaration).toBe(sym);
443
821
  });
444
822
  });
@@ -453,8 +831,10 @@ describe("Deleting symbols", () => {
453
831
  });
454
832
  expect(resolvedSym.value).toBe(undefined);
455
833
  sym.refkeys[0] = key;
834
+ flushJobs();
456
835
  expect(resolvedSym.value?.targetDeclaration).toBe(sym);
457
836
  binder.deleteSymbol(sym);
837
+ flushJobs();
458
838
  expect(resolvedSym.value).toBe(undefined);
459
839
  });
460
840
  it("removes from parent scopes", () => {
@@ -1 +1 @@
1
- {"version":3,"file":"utils.test.d.ts","sourceRoot":"","sources":["../../test/utils.test.tsx"],"names":[],"mappings":"AAKA,OAAO,6BAA6B,CAAC"}
1
+ {"version":3,"file":"utils.test.d.ts","sourceRoot":"","sources":["../../test/utils.test.tsx"],"names":[],"mappings":"AAMA,OAAO,6BAA6B,CAAC"}
@@ -2,6 +2,7 @@ import { memo as _$memo, createComponent as _$createComponent } from "@alloy-js/
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
  describe("mapJoin", () => {
@@ -66,6 +67,7 @@ describe("mapJoin", () => {
66
67
  expect(callCount).toBe(2);
67
68
  arr.value.push(3);
68
69
  triggerRef(arr);
70
+ flushJobs();
69
71
  expect(callCount).toBe(3);
70
72
  });
71
73
  it("can map a joiner", () => {