@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
@@ -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", () => {
@@ -242,8 +245,25 @@ describe("instance members", () => {
242
245
  });
243
246
  });
244
247
  describe("instantiating members", () => {
245
- it("instantiates static symbols", () => {
248
+ it("instantiates instance members", () => {
246
249
  const binder = createOutputBinder();
250
+
251
+ /**
252
+ * The following structure would match code like this:
253
+ * ```ts
254
+ * // A class with instance members
255
+ * class Source {
256
+ * instance() {
257
+ * print("instance");
258
+ * }
259
+ * }
260
+ *
261
+ * // Instantiates into t
262
+ * var t = new Source();
263
+ *
264
+ * t.instance();
265
+ * ```
266
+ */
247
267
  const {
248
268
  symbols: {
249
269
  rootSymbol,
@@ -257,7 +277,7 @@ describe("instantiating members", () => {
257
277
  flags: OutputSymbolFlags.InstanceMemberContainer,
258
278
  instanceMembers: {
259
279
  instance: {
260
- flags: OutputSymbolFlags.StaticMember
280
+ flags: OutputSymbolFlags.InstanceMember
261
281
  }
262
282
  }
263
283
  },
@@ -266,12 +286,89 @@ describe("instantiating members", () => {
266
286
  }
267
287
  });
268
288
  binder.instantiateSymbolInto(rootSymbol, instantiation);
269
- expect(instantiation.flags & OutputSymbolFlags.InstanceMemberContainer).toBeTruthy();
270
- expect(instantiation.instanceMemberScope).toBeDefined();
289
+ expect(instantiation.flags & OutputSymbolFlags.StaticMemberContainer).toBeTruthy();
290
+ expect(instantiation.staticMemberScope).toBeDefined();
271
291
  const expectedRefkey = refkey(instantiation.refkeys[0], instance.refkeys[0]);
272
- expect(instantiation.instanceMemberScope.symbolsByRefkey.get(expectedRefkey)).toBeDefined();
292
+ expect(instantiation.staticMemberScope.symbolsByRefkey.get(expectedRefkey)).toBeDefined();
293
+ });
294
+ it("doesn't duplicate symbols", () => {
295
+ const binder = createOutputBinder();
296
+ const {
297
+ symbols: {
298
+ rootSymbol,
299
+ instantiation
300
+ }
301
+ } = createScopeTree(binder, {
302
+ rootScope: {
303
+ symbols: {
304
+ rootSymbol: {
305
+ flags: OutputSymbolFlags.InstanceMemberContainer,
306
+ instanceMembers: {
307
+ instance: {
308
+ flags: OutputSymbolFlags.InstanceMember
309
+ }
310
+ }
311
+ },
312
+ instantiation: {}
313
+ }
314
+ }
315
+ });
316
+ binder.instantiateSymbolInto(rootSymbol, instantiation);
317
+ flushJobs();
318
+ expect(instantiation.staticMemberScope.symbols.size).toBe(1);
319
+ const lateKey = refkey();
320
+ // now add a brand‐new static member to source
321
+ binder.createSymbol({
322
+ name: "lateChild",
323
+ scope: rootSymbol.instanceMemberScope,
324
+ refkey: lateKey,
325
+ flags: OutputSymbolFlags.InstanceMember
326
+ });
327
+ flushJobs();
328
+ expect(rootSymbol.instanceMemberScope.symbols.size).toBe(2);
329
+ expect(instantiation.staticMemberScope.symbols.size).toBe(2);
273
330
  });
274
- it("instantiates static symbols that are added after the instantiation", () => {
331
+ it("should remove members in instance when source deleted them", () => {
332
+ const binder = createOutputBinder();
333
+ const {
334
+ symbols: {
335
+ rootSymbol,
336
+ instantiation
337
+ }
338
+ } = createScopeTree(binder, {
339
+ rootScope: {
340
+ symbols: {
341
+ rootSymbol: {
342
+ flags: OutputSymbolFlags.InstanceMemberContainer,
343
+ instanceMembers: {
344
+ instance: {
345
+ flags: OutputSymbolFlags.InstanceMember
346
+ }
347
+ }
348
+ },
349
+ instantiation: {}
350
+ }
351
+ }
352
+ });
353
+ binder.instantiateSymbolInto(rootSymbol, instantiation);
354
+ expect(instantiation.staticMemberScope.symbols.size).toBe(1);
355
+ const lateKey = refkey();
356
+ // now add a brand‐new static member to source
357
+ binder.createSymbol({
358
+ name: "lateChild",
359
+ scope: rootSymbol.instanceMemberScope,
360
+ refkey: lateKey,
361
+ flags: OutputSymbolFlags.InstanceMember
362
+ });
363
+ flushJobs();
364
+ expect(rootSymbol.instanceMemberScope.symbols.size).toBe(2);
365
+ expect(instantiation.staticMemberScope.symbols.size).toBe(2);
366
+ binder.deleteSymbol(rootSymbol.instanceMemberScope.symbols.values().next().value);
367
+ flushJobs();
368
+ expect(rootSymbol.instanceMemberScope.symbols.size).toBe(1);
369
+ expect(instantiation.staticMemberScope.symbols.size).toBe(1);
370
+ });
371
+ it("instantiates instance members added after the instantiation", () => {
275
372
  const binder = createOutputBinder();
276
373
  const {
277
374
  symbols: {
@@ -286,7 +383,7 @@ describe("instantiating members", () => {
286
383
  flags: OutputSymbolFlags.InstanceMemberContainer,
287
384
  instanceMembers: {
288
385
  instance: {
289
- flags: OutputSymbolFlags.StaticMember
386
+ flags: OutputSymbolFlags.InstanceMember
290
387
  }
291
388
  }
292
389
  },
@@ -295,10 +392,11 @@ describe("instantiating members", () => {
295
392
  }
296
393
  });
297
394
  binder.instantiateSymbolInto(rootSymbol, instantiation);
298
- expect(instantiation.flags & OutputSymbolFlags.InstanceMemberContainer).toBeTruthy();
299
- expect(instantiation.instanceMemberScope).toBeDefined();
395
+ flushJobs();
396
+ expect(instantiation.flags & OutputSymbolFlags.StaticMemberContainer).toBeTruthy();
397
+ expect(instantiation.staticMemberScope).toBeDefined();
300
398
  const expectedRefkey = refkey(instantiation.refkeys[0], instance.refkeys[0]);
301
- expect(instantiation.instanceMemberScope.symbolsByRefkey.get(expectedRefkey)).toBeDefined();
399
+ expect(instantiation.staticMemberScope.symbolsByRefkey.get(expectedRefkey)).toBeDefined();
302
400
  const newInstanceMemberRefkey = refkey();
303
401
  binder.createSymbol({
304
402
  name: "newInstanceMember",
@@ -307,7 +405,279 @@ describe("instantiating members", () => {
307
405
  flags: OutputSymbolFlags.InstanceMember
308
406
  });
309
407
  const newExpectedRefkey = refkey(instantiation.refkeys[0], newInstanceMemberRefkey);
310
- expect(instantiation.instanceMemberScope.symbolsByRefkey.get(newExpectedRefkey)).toBeDefined();
408
+ flushJobs();
409
+ expect(instantiation.staticMemberScope.symbolsByRefkey.get(newExpectedRefkey)).toBeDefined();
410
+ });
411
+ it("instantiates static symbols for a static container source", () => {
412
+ const binder = createOutputBinder();
413
+
414
+ /**
415
+ * The following structure would match code like this:
416
+ * ```ts
417
+ * // A class with instance members
418
+ * class Source {
419
+ * static child() {
420
+ * print("child");
421
+ * }
422
+ * }
423
+ *
424
+ *
425
+ * var printChild = Source.child;
426
+ *
427
+ * printChild();
428
+ * ```
429
+ */
430
+ const {
431
+ symbols: {
432
+ source,
433
+ child,
434
+ target
435
+ }
436
+ } = createScopeTree(binder, {
437
+ root: {
438
+ symbols: {
439
+ source: {
440
+ flags: OutputSymbolFlags.StaticMemberContainer,
441
+ staticMembers: {
442
+ child: {
443
+ flags: OutputSymbolFlags.StaticMember
444
+ }
445
+ }
446
+ },
447
+ target: {}
448
+ }
449
+ }
450
+ });
451
+ binder.instantiateSymbolInto(source, target);
452
+
453
+ // target must now be a StaticMemberContainer too
454
+ expect(target.flags & OutputSymbolFlags.StaticMemberContainer).toBeTruthy();
455
+ expect(target.staticMemberScope).toBeDefined();
456
+ const expectedKey = refkey(target.refkeys[0], child.refkeys[0]);
457
+ expect(target.staticMemberScope.symbolsByRefkey.get(expectedKey)).toBeDefined();
458
+ });
459
+ it("instantiates static symbols added after instantiation", () => {
460
+ const binder = createOutputBinder();
461
+ const lateKey = refkey();
462
+ const {
463
+ symbols: {
464
+ source,
465
+ target
466
+ }
467
+ } = createScopeTree(binder, {
468
+ root: {
469
+ symbols: {
470
+ source: {
471
+ flags: OutputSymbolFlags.StaticMemberContainer
472
+ },
473
+ target: {}
474
+ }
475
+ }
476
+ });
477
+
478
+ // hook up instantiation
479
+ binder.instantiateSymbolInto(source, target);
480
+
481
+ // now add a brand‐new static member to source
482
+ const late = binder.createSymbol({
483
+ name: "lateChild",
484
+ scope: source.staticMemberScope,
485
+ refkey: lateKey,
486
+ flags: OutputSymbolFlags.StaticMember
487
+ });
488
+ flushJobs();
489
+
490
+ // it should *automatically* show up on target.staticMemberScope
491
+ const expectedKey = refkey(target.refkeys[0], late.refkeys[0]);
492
+ expect(target.staticMemberScope.symbolsByRefkey.get(expectedKey)).toBeDefined();
493
+ });
494
+ it("recursively instantiates nested static members", () => {
495
+ const binder = createOutputBinder();
496
+
497
+ /**
498
+ * The following structure would match code like this:
499
+ * ```ts
500
+ * class Source {
501
+ * static Level1 = class {
502
+ * static level2() { print("deep"); }
503
+ * }
504
+ * }
505
+ *
506
+ * var target = Source;
507
+ *
508
+ * target.Level1.level2()
509
+ * ```
510
+ */
511
+ const {
512
+ symbols: {
513
+ source,
514
+ level1,
515
+ level2,
516
+ target
517
+ }
518
+ } = createScopeTree(binder, {
519
+ root: {
520
+ symbols: {
521
+ source: {
522
+ flags: OutputSymbolFlags.StaticMemberContainer,
523
+ staticMembers: {
524
+ level1: {
525
+ flags: OutputSymbolFlags.StaticMember | OutputSymbolFlags.StaticMemberContainer,
526
+ staticMembers: {
527
+ level2: {
528
+ flags: OutputSymbolFlags.StaticMember
529
+ }
530
+ }
531
+ }
532
+ }
533
+ },
534
+ target: {}
535
+ }
536
+ }
537
+ });
538
+ binder.instantiateSymbolInto(source, target);
539
+
540
+ // level1 should appear under target.staticMemberScope
541
+ const key1 = refkey(target.refkeys[0], level1.refkeys[0]);
542
+ const instantiated1 = target.staticMemberScope.symbolsByRefkey.get(key1);
543
+ expect(instantiated1.name).toBe(level1.name);
544
+
545
+ // and level2 should appear under the *child* staticMemberScope of that instantiated level1
546
+ const childScope = instantiated1.staticMemberScope;
547
+ const key2 = refkey(instantiated1.refkeys[0], level2.refkeys[0]);
548
+ expect(childScope.symbolsByRefkey.get(key2)).toBeDefined();
549
+ });
550
+ it("copies both instance *and* static members when source has both flags", () => {
551
+ const binder = createOutputBinder();
552
+
553
+ /**
554
+ * ```ts
555
+ * class Source {
556
+ * instance() { print("inst"); }
557
+ * static s1() { print("static"); }
558
+ * }
559
+ *
560
+ * let t = new Source()
561
+ * t.instance()
562
+ * t.s1()
563
+ * ```
564
+ */
565
+ const {
566
+ symbols: {
567
+ source,
568
+ inst
569
+ }
570
+ } = createScopeTree(binder, {
571
+ root: {
572
+ symbols: {
573
+ source: {
574
+ flags: OutputSymbolFlags.InstanceMemberContainer | OutputSymbolFlags.StaticMemberContainer,
575
+ instanceMembers: {
576
+ i1: {
577
+ flags: OutputSymbolFlags.InstanceMember
578
+ }
579
+ },
580
+ staticMembers: {
581
+ s1: {
582
+ flags: OutputSymbolFlags.StaticMember
583
+ }
584
+ }
585
+ },
586
+ inst: {}
587
+ }
588
+ }
589
+ });
590
+ binder.instantiateSymbolInto(source, inst);
591
+ expect(inst.staticMemberScope).toBeDefined();
592
+ expect([...inst.staticMemberScope.symbols].some(s => s.name === "i1")).toBe(true);
593
+
594
+ // static side
595
+ const symbols = [...source.staticMemberScope.symbols];
596
+ expect(inst.staticMemberScope).toBeDefined();
597
+ const sKey = refkey(inst.refkeys[0], symbols[0].refkeys[0]);
598
+ expect(inst.staticMemberScope.symbolsByRefkey.has(sKey)).toBe(true);
599
+ });
600
+ it("is idempotent, calling twice does not duplicate", () => {
601
+ const binder = createOutputBinder();
602
+ const {
603
+ symbols: {
604
+ source,
605
+ target
606
+ }
607
+ } = createScopeTree(binder, {
608
+ root: {
609
+ symbols: {
610
+ source: {
611
+ flags: OutputSymbolFlags.StaticMemberContainer,
612
+ staticMembers: {
613
+ a: {
614
+ flags: OutputSymbolFlags.StaticMember
615
+ }
616
+ }
617
+ },
618
+ target: {}
619
+ }
620
+ }
621
+ });
622
+ binder.instantiateSymbolInto(source, target);
623
+ flushJobs();
624
+ const initialCount = target.staticMemberScope.symbols.size;
625
+ binder.instantiateSymbolInto(source, target);
626
+ flushJobs();
627
+ expect(target.staticMemberScope.symbols.size).toBe(initialCount);
628
+ });
629
+ it("instantiates static children of instance members under the instance scope", () => {
630
+ const binder = createOutputBinder();
631
+ /**
632
+ * ```ts
633
+ * class Source {
634
+ * instM = class {
635
+ * static deep() { print("deep"); }
636
+ * }
637
+ * }
638
+ *
639
+ * var t = new Source();
640
+ * t.instM.deep();
641
+ * ```
642
+ */
643
+ const {
644
+ symbols: {
645
+ source,
646
+ deep,
647
+ target
648
+ }
649
+ } = createScopeTree(binder, {
650
+ root: {
651
+ symbols: {
652
+ source: {
653
+ flags: OutputSymbolFlags.InstanceMemberContainer,
654
+ instanceMembers: {
655
+ instM: {
656
+ flags: OutputSymbolFlags.InstanceMember | OutputSymbolFlags.StaticMemberContainer,
657
+ staticMembers: {
658
+ deep: {
659
+ flags: OutputSymbolFlags.StaticMember
660
+ }
661
+ }
662
+ }
663
+ }
664
+ },
665
+ target: {}
666
+ }
667
+ }
668
+ });
669
+ binder.instantiateSymbolInto(source, target);
670
+
671
+ // Find the instantiated copy of instM under target.instanceMemberScope
672
+ const instMSym = [...target.staticMemberScope.symbols].find(s => s.name === "instM");
673
+
674
+ // instMSym should have gotten its own staticMemberScope via the StaticMemberContainer flag
675
+ expect(instMSym.staticMemberScope).toBeDefined();
676
+
677
+ // compute the expected key for the deep child:
678
+ // (<target>, <instM>) then (on that) (<deep original>)
679
+ const expectedDeepKey = refkey(instMSym.refkeys[0], deep.refkeys[0]);
680
+ expect(instMSym.staticMemberScope.symbolsByRefkey.has(expectedDeepKey)).toBe(true);
311
681
  });
312
682
  });
313
683
  describe("symbol name resolution", () => {
@@ -439,6 +809,7 @@ describe("refkey resolution", () => {
439
809
  });
440
810
  expect(resolvedSym.value).toBe(undefined);
441
811
  sym.refkeys[0] = key;
812
+ flushJobs();
442
813
  expect(resolvedSym.value?.targetDeclaration).toBe(sym);
443
814
  });
444
815
  });
@@ -453,8 +824,10 @@ describe("Deleting symbols", () => {
453
824
  });
454
825
  expect(resolvedSym.value).toBe(undefined);
455
826
  sym.refkeys[0] = key;
827
+ flushJobs();
456
828
  expect(resolvedSym.value?.targetDeclaration).toBe(sym);
457
829
  binder.deleteSymbol(sym);
830
+ flushJobs();
458
831
  expect(resolvedSym.value).toBe(undefined);
459
832
  });
460
833
  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", () => {