@alloy-js/core 0.21.0-dev.1 → 0.21.0-dev.13

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 (99) hide show
  1. package/dist/src/binder.d.ts +3 -3
  2. package/dist/src/binder.d.ts.map +1 -1
  3. package/dist/src/binder.js +18 -4
  4. package/dist/src/binder.js.map +1 -1
  5. package/dist/src/components/ReferenceOrContent.d.ts +1 -1
  6. package/dist/src/components/ReferenceOrContent.d.ts.map +1 -1
  7. package/dist/src/index.d.ts +1 -0
  8. package/dist/src/index.d.ts.map +1 -1
  9. package/dist/src/index.js +1 -0
  10. package/dist/src/index.js.map +1 -1
  11. package/dist/src/library-symbol-reference.d.ts +8 -0
  12. package/dist/src/library-symbol-reference.d.ts.map +1 -0
  13. package/dist/src/library-symbol-reference.js +5 -0
  14. package/dist/src/library-symbol-reference.js.map +1 -0
  15. package/dist/src/pretty-string/pretty-string.d.ts +47 -0
  16. package/dist/src/pretty-string/pretty-string.d.ts.map +1 -0
  17. package/dist/src/pretty-string/pretty-string.js +100 -0
  18. package/dist/src/pretty-string/pretty-string.js.map +1 -0
  19. package/dist/src/pretty-string/pretty-string.test.d.ts +2 -0
  20. package/dist/src/pretty-string/pretty-string.test.d.ts.map +1 -0
  21. package/dist/src/pretty-string/pretty-string.test.js +38 -0
  22. package/dist/src/pretty-string/pretty-string.test.js.map +1 -0
  23. package/dist/src/reactivity.d.ts.map +1 -1
  24. package/dist/src/reactivity.js +4 -1
  25. package/dist/src/reactivity.js.map +1 -1
  26. package/dist/src/refkey.d.ts +12 -8
  27. package/dist/src/refkey.d.ts.map +1 -1
  28. package/dist/src/refkey.js +41 -12
  29. package/dist/src/refkey.js.map +1 -1
  30. package/dist/src/render.d.ts.map +1 -1
  31. package/dist/src/render.js +13 -4
  32. package/dist/src/render.js.map +1 -1
  33. package/dist/src/runtime/component.d.ts +22 -2
  34. package/dist/src/runtime/component.d.ts.map +1 -1
  35. package/dist/src/runtime/component.js +18 -0
  36. package/dist/src/runtime/component.js.map +1 -1
  37. package/dist/src/scheduler.d.ts +4 -4
  38. package/dist/src/scheduler.d.ts.map +1 -1
  39. package/dist/src/scheduler.js +10 -5
  40. package/dist/src/scheduler.js.map +1 -1
  41. package/dist/src/symbols/output-scope.js +4 -2
  42. package/dist/src/symbols/output-scope.js.map +1 -1
  43. package/dist/src/symbols/output-symbol.d.ts +9 -0
  44. package/dist/src/symbols/output-symbol.d.ts.map +1 -1
  45. package/dist/src/symbols/output-symbol.js +30 -0
  46. package/dist/src/symbols/output-symbol.js.map +1 -1
  47. package/dist/src/tracer.d.ts.map +1 -1
  48. package/dist/src/tracer.js +4 -4
  49. package/dist/src/tracer.js.map +1 -1
  50. package/dist/src/utils.d.ts +12 -0
  51. package/dist/src/utils.d.ts.map +1 -1
  52. package/dist/src/utils.js +15 -3
  53. package/dist/src/utils.js.map +1 -1
  54. package/dist/test/reactivity/circular-reactives.test.js +18 -0
  55. package/dist/test/reactivity/circular-reactives.test.js.map +1 -1
  56. package/dist/test/refkey.test.js +11 -1
  57. package/dist/test/refkey.test.js.map +1 -1
  58. package/dist/test/rendering/basic.test.js +22 -0
  59. package/dist/test/rendering/basic.test.js.map +1 -1
  60. package/dist/test/symbols/output-scope.test.js +4 -3
  61. package/dist/test/symbols/output-scope.test.js.map +1 -1
  62. package/dist/test/symbols/resolution.test.js +29 -1
  63. package/dist/test/symbols/resolution.test.js.map +1 -1
  64. package/dist/test/utils.test.js +13 -0
  65. package/dist/test/utils.test.js.map +1 -1
  66. package/dist/testing/create-test-wrapper.d.ts +22 -0
  67. package/dist/testing/create-test-wrapper.d.ts.map +1 -0
  68. package/dist/testing/create-test-wrapper.js +60 -0
  69. package/dist/testing/create-test-wrapper.js.map +1 -0
  70. package/dist/testing/index.d.ts +1 -0
  71. package/dist/testing/index.d.ts.map +1 -1
  72. package/dist/testing/index.js +1 -0
  73. package/dist/testing/index.js.map +1 -1
  74. package/dist/tsconfig.tsbuildinfo +1 -1
  75. package/package.json +1 -1
  76. package/src/binder.ts +34 -6
  77. package/src/index.ts +1 -0
  78. package/src/library-symbol-reference.ts +20 -0
  79. package/src/pretty-string/pretty-string.test.ts +47 -0
  80. package/src/pretty-string/pretty-string.ts +130 -0
  81. package/src/reactivity.ts +4 -1
  82. package/src/refkey.ts +68 -26
  83. package/src/render.ts +14 -3
  84. package/src/runtime/component.ts +33 -1
  85. package/src/scheduler.ts +12 -11
  86. package/src/symbols/output-scope.ts +4 -4
  87. package/src/symbols/output-symbol.ts +40 -0
  88. package/src/tracer.ts +5 -8
  89. package/src/utils.tsx +25 -4
  90. package/temp/api.json +854 -92
  91. package/test/reactivity/circular-reactives.test.tsx +20 -0
  92. package/test/refkey.test.ts +12 -1
  93. package/test/rendering/basic.test.tsx +35 -1
  94. package/test/symbols/output-scope.test.ts +4 -4
  95. package/test/symbols/resolution.test.ts +42 -1
  96. package/test/utils.test.tsx +18 -0
  97. package/testing/create-test-wrapper.tsx +70 -0
  98. package/testing/index.ts +1 -0
  99. package/tsconfig.json +1 -0
package/src/binder.ts CHANGED
@@ -3,7 +3,14 @@ import { useBinder } from "./context/binder.js";
3
3
  import { useMemberContext } from "./context/member-scope.js";
4
4
  import { useScope } from "./context/scope.js";
5
5
  import { effect } from "./reactivity.js";
6
- import { isMemberRefkey, refkey, Refkey } from "./refkey.js";
6
+ import {
7
+ isMemberRefkey,
8
+ MemberRefkey,
9
+ refkey,
10
+ Refkey,
11
+ Refkeyable,
12
+ toRefkey,
13
+ } from "./refkey.js";
7
14
  import { OutputScope } from "./symbols/output-scope.js";
8
15
  import { type OutputSymbol } from "./symbols/output-symbol.js";
9
16
  import {
@@ -49,7 +56,7 @@ export interface Binder {
49
56
  * ref is undefined if the symbol has not been created yet.
50
57
  */
51
58
  getSymbolForRefkey<TSymbol extends OutputSymbol>(
52
- refkey: Refkey,
59
+ refkey: Refkeyable,
53
60
  ): Ref<TSymbol | undefined>;
54
61
 
55
62
  /**
@@ -392,8 +399,10 @@ export function createOutputBinder(options: BinderOptions = {}): Binder {
392
399
  }
393
400
 
394
401
  function getSymbolForRefkey<TSymbol extends OutputSymbol = OutputSymbol>(
395
- refkey: Refkey,
402
+ refkeyable: Refkeyable,
396
403
  ) {
404
+ const refkey = toRefkey(refkeyable);
405
+
397
406
  if (waitingDeclarations.has(refkey)) {
398
407
  return waitingDeclarations.get(refkey)! as ShallowRef<TSymbol>;
399
408
  }
@@ -404,7 +413,9 @@ export function createOutputBinder(options: BinderOptions = {}): Binder {
404
413
  const baseSymbolRef: ShallowRef<TSymbol | undefined> =
405
414
  getSymbolForRefkey<TSymbol>(refkey.base);
406
415
  const memberSymbolRef: ShallowRef<TSymbol | undefined> =
407
- getSymbolForRefkey<TSymbol>(refkey.member);
416
+ getMemberSymbolFromMemberRefkey(refkey) as ShallowRef<
417
+ TSymbol | undefined
418
+ >;
408
419
 
409
420
  symbolRef = computed(() => {
410
421
  // even though we don't necessarily need the base symbol to be available
@@ -558,7 +569,7 @@ export function createOutputBinder(options: BinderOptions = {}): Binder {
558
569
  return [
559
570
  ...getMemberPathFromRefkey(refkey.base),
560
571
  {
561
- symbol: getSymbolForRefkey(refkey.member).value!,
572
+ symbol: getMemberSymbolFromMemberRefkey(refkey).value!,
562
573
  isMemberAccess: true,
563
574
  },
564
575
  ];
@@ -572,6 +583,23 @@ export function createOutputBinder(options: BinderOptions = {}): Binder {
572
583
  ];
573
584
  }
574
585
 
586
+ function getMemberSymbolFromMemberRefkey(
587
+ refkey: MemberRefkey,
588
+ ): Ref<OutputSymbol | undefined> {
589
+ if (typeof refkey.member === "string") {
590
+ return computed(() => {
591
+ const baseSymbol = getSymbolForRefkey(refkey.base).value;
592
+ if (!baseSymbol) {
593
+ return undefined;
594
+ }
595
+
596
+ return baseSymbol.resolveMemberByName(refkey.member as string);
597
+ });
598
+ } else {
599
+ return getSymbolForRefkey(refkey.member);
600
+ }
601
+ }
602
+
575
603
  function resolveMember(
576
604
  base: OutputSymbol,
577
605
  member: OutputSymbol,
@@ -692,7 +720,7 @@ export function resolve<
692
720
  * not found. The symbol you're looking for may not have been declared yet. When the symbol
693
721
  * is declared, the ref will be updated with the symbol.
694
722
  */
695
- export function symbolForRefkey(refkey: Refkey) {
723
+ export function symbolForRefkey(refkey: Refkeyable) {
696
724
  const binder = useBinder();
697
725
  if (!binder) {
698
726
  throw new Error("Can't resolve refkey without a binder");
package/src/index.ts CHANGED
@@ -27,6 +27,7 @@ export * from "./code.js";
27
27
  export * from "./components/index.js";
28
28
  export * from "./context.js";
29
29
  export * from "./context/index.js";
30
+ export * from "./library-symbol-reference.js";
30
31
  export * from "./name-policy.js";
31
32
  export * from "./props-combinators.js";
32
33
  export * from "./reactive-union-set.js";
@@ -0,0 +1,20 @@
1
+ import { RefkeyableObject } from "./refkey.js";
2
+ import { OutputSymbol } from "./symbols/output-symbol.js";
3
+
4
+ export const TO_SYMBOL: unique symbol = Symbol(
5
+ "Alloy.RefkeyableObject.TO_SYMBOL",
6
+ );
7
+
8
+ export interface LibrarySymbolReference extends RefkeyableObject {
9
+ [TO_SYMBOL](): OutputSymbol;
10
+ }
11
+
12
+ export function isLibrarySymbolReference(
13
+ value: unknown,
14
+ ): value is LibrarySymbolReference {
15
+ return (
16
+ typeof value === "object" &&
17
+ value !== null &&
18
+ Object.hasOwn(value, TO_SYMBOL)
19
+ );
20
+ }
@@ -0,0 +1,47 @@
1
+ import { expect, it } from "vitest";
2
+ import { pret, PrettyString } from "./pretty-string.js";
3
+
4
+ function expectRender(
5
+ pretty: PrettyString,
6
+ expected: {
7
+ toString: string;
8
+ toAnsi: string;
9
+ },
10
+ ) {
11
+ expect(pretty.toString()).toBe(expected.toString);
12
+ expect(pretty.toAnsi()).toBe(expected.toAnsi);
13
+ }
14
+
15
+ it("interpolate basic string", () => {
16
+ const result = pret`foo ${"bar"} baz`;
17
+ expectRender(result, {
18
+ toString: "foo bar baz",
19
+ toAnsi: "foo bar baz",
20
+ });
21
+ });
22
+
23
+ // cspell:ignore mbar
24
+ it("interpolate pretty segment", () => {
25
+ const result = pret`foo ${pret.red("bar")} baz`;
26
+ expectRender(result, {
27
+ toString: "foo bar baz",
28
+ toAnsi: "foo \x1b[31mbar\x1b[39m baz",
29
+ });
30
+ });
31
+
32
+ it("interpolate another pretty string with more formatting", () => {
33
+ const a = pret`foo ${pret.red("bar")} baz`;
34
+ const result = pret`Hi, ${pret.bgGreen(a)}`;
35
+ expectRender(result, {
36
+ toString: "Hi, foo bar baz",
37
+ toAnsi: "Hi, \x1b[42mfoo \x1b[31mbar\x1b[39m baz\x1b[49m",
38
+ });
39
+ });
40
+
41
+ it("use full rgb", () => {
42
+ const result = pret`foo ${pret.rgb(123, 45, 67, "bar")} baz`;
43
+ expectRender(result, {
44
+ toString: "foo bar baz",
45
+ toAnsi: "foo \x1b[38;2;123;45;67mbar\x1b[39m baz",
46
+ });
47
+ });
@@ -0,0 +1,130 @@
1
+ import { inspect } from "../inspect.js";
2
+
3
+ export function pret(
4
+ strings: TemplateStringsArray,
5
+ ...keys: PrettyStringSegment[]
6
+ ): PrettyString {
7
+ const result: PrettyStringSegment[] = [strings[0]];
8
+ keys.forEach((key, i) => {
9
+ result.push(key);
10
+ result.push(strings[i + 1]);
11
+ });
12
+
13
+ return new PrettyString(result);
14
+ }
15
+
16
+ pret.black = (x: PrettyStringSegment) => new PrettyStringColored(x, "black");
17
+ pret.red = (x: PrettyStringSegment) => new PrettyStringColored(x, "red");
18
+ pret.green = (x: PrettyStringSegment) => new PrettyStringColored(x, "green");
19
+ pret.yellow = (x: PrettyStringSegment) => new PrettyStringColored(x, "yellow");
20
+ pret.blue = (x: PrettyStringSegment) => new PrettyStringColored(x, "blue");
21
+ pret.magenta = (x: PrettyStringSegment) =>
22
+ new PrettyStringColored(x, "magenta");
23
+ pret.cyan = (x: PrettyStringSegment) => new PrettyStringColored(x, "cyan");
24
+ pret.white = (x: PrettyStringSegment) => new PrettyStringColored(x, "white");
25
+ pret.gray = (x: PrettyStringSegment) => new PrettyStringColored(x, "gray");
26
+
27
+ pret.bgBlack = (x: PrettyStringSegment) =>
28
+ new PrettyStringColored(x, "bgBlack");
29
+ pret.bgRed = (x: PrettyStringSegment) => new PrettyStringColored(x, "bgRed");
30
+ pret.bgGreen = (x: PrettyStringSegment) =>
31
+ new PrettyStringColored(x, "bgGreen");
32
+ pret.bgYellow = (x: PrettyStringSegment) =>
33
+ new PrettyStringColored(x, "bgYellow");
34
+ pret.bgBlue = (x: PrettyStringSegment) => new PrettyStringColored(x, "bgBlue");
35
+ pret.bgMagenta = (x: PrettyStringSegment) =>
36
+ new PrettyStringColored(x, "bgMagenta");
37
+ pret.bgCyan = (x: PrettyStringSegment) => new PrettyStringColored(x, "bgCyan");
38
+ pret.bgWhite = (x: PrettyStringSegment) =>
39
+ new PrettyStringColored(x, "bgWhite");
40
+
41
+ pret.rgb = (r: number, g: number, b: number, x: PrettyStringSegment) =>
42
+ new PrettyStringColored(x, { kind: "rgb", r, g, b });
43
+
44
+ const ansiColors = {
45
+ reset: ["\x1b[0m", "\x1b[0m"],
46
+ bold: ["\x1b[1m", "\x1b[22m"],
47
+ dim: ["\x1b[2m", "\x1b[22m"],
48
+ italic: ["\x1b[3m", "\x1b[23m"],
49
+ underline: ["\x1b[4m", "\x1b[24m"],
50
+ inverse: ["\x1b[7m", "\x1b[27m"],
51
+ hidden: ["\x1b[8m", "\x1b[28m"],
52
+ strikethrough: ["\x1b[9m", "\x1b[29m"],
53
+ black: ["\x1b[30m", "\x1b[39m"],
54
+ red: ["\x1b[31m", "\x1b[39m"],
55
+ green: ["\x1b[32m", "\x1b[39m"],
56
+ yellow: ["\x1b[33m", "\x1b[39m"],
57
+ blue: ["\x1b[34m", "\x1b[39m"],
58
+ magenta: ["\x1b[35m", "\x1b[39m"],
59
+ cyan: ["\x1b[36m", "\x1b[39m"],
60
+ white: ["\x1b[37m", "\x1b[39m"],
61
+ gray: ["\x1b[90m", "\x1b[39m"],
62
+ bgBlack: ["\x1b[40m", "\x1b[49m"],
63
+ bgRed: ["\x1b[41m", "\x1b[49m"],
64
+ bgGreen: ["\x1b[42m", "\x1b[49m"],
65
+ bgYellow: ["\x1b[43m", "\x1b[49m"],
66
+ bgBlue: ["\x1b[44m", "\x1b[49m"],
67
+ bgMagenta: ["\x1b[45m", "\x1b[49m"],
68
+ bgCyan: ["\x1b[46m", "\x1b[49m"],
69
+ bgWhite: ["\x1b[47m", "\x1b[49m"],
70
+ } as Record<string, [string, string]>;
71
+
72
+ export type PrettyStringSegment = string | PrettyStringColored | PrettyString;
73
+
74
+ export interface RgbColor {
75
+ kind: "rgb";
76
+ r: number;
77
+ g: number;
78
+ b: number;
79
+ }
80
+
81
+ export type ColorCodes = keyof typeof ansiColors;
82
+ export type Color = ColorCodes | RgbColor;
83
+ export class PrettyStringColored {
84
+ #value: PrettyStringSegment;
85
+ #color: Color;
86
+
87
+ constructor(value: PrettyStringSegment, color: Color) {
88
+ this.#value = value;
89
+ this.#color = color;
90
+ }
91
+
92
+ toString(): string {
93
+ return this.#value.toString();
94
+ }
95
+
96
+ toAnsi(): string {
97
+ if (typeof this.#color === "string") {
98
+ const [start, end] = ansiColors[this.#color];
99
+ return `${start}${typeof this.#value === "string" ? this.#value : this.#value.toAnsi()}${end}`;
100
+ }
101
+ const { r, g, b } = this.#color;
102
+ const start = `\x1b[38;2;${r};${g};${b}m`;
103
+ const end = `\x1b[39m`;
104
+ return `${start}${typeof this.#value === "string" ? this.#value : this.#value.toAnsi()}${end}`;
105
+ }
106
+ }
107
+
108
+ export class PrettyString {
109
+ #segments: PrettyStringSegment[];
110
+
111
+ constructor(segments: PrettyStringSegment[]) {
112
+ this.#segments = segments;
113
+ }
114
+
115
+ toString(): string {
116
+ return this.#segments
117
+ .map((s) => (typeof s === "string" ? s : s.toString()))
118
+ .join("");
119
+ }
120
+
121
+ toAnsi(): string {
122
+ return this.#segments
123
+ .map((s) => (typeof s === "string" ? s : s.toAnsi()))
124
+ .join("");
125
+ }
126
+
127
+ [inspect.custom]() {
128
+ return this.toAnsi();
129
+ }
130
+ }
package/src/reactivity.ts CHANGED
@@ -156,7 +156,10 @@ export function effect<T>(fn: (prev?: T) => T, current?: T) {
156
156
  }
157
157
  },
158
158
  {
159
- scheduler: scheduler(() => runner),
159
+ // allow recursive effects with 32, 1 and 4 are default flags
160
+ // @ts-expect-error flags is a vue internal thing
161
+ flags: 1 | 4 | 32,
162
+ scheduler: scheduler(),
160
163
  onTrack(event) {
161
164
  trace(TracePhase.effect.track, () => {
162
165
  return `tracking ${event.target}, ${String(event.key)}`;
package/src/refkey.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { markRaw } from "@vue/reactivity";
2
+ import { untrack } from "./reactivity.js";
2
3
 
3
4
  const objectIds = new WeakMap<WeakKey, string>();
4
5
  let objId = 0;
@@ -14,15 +15,20 @@ function getObjectKey(value: WeakKey): string {
14
15
  return key;
15
16
  }
16
17
 
17
- const RefkeySym: unique symbol = Symbol();
18
+ export const REFKEYABLE: unique symbol = Symbol("Alloy.REFKEYABLE");
18
19
 
19
- export type RefkeyBase = {
20
- [RefkeySym]: true;
20
+ export type RefkeyableObject = {
21
+ [REFKEYABLE](): Refkey;
21
22
  };
22
23
 
24
+ export function toRefkey(refkey: Refkeyable) {
25
+ return untrack(() => refkey[REFKEYABLE]());
26
+ }
27
+
28
+ export type Refkeyable = RefkeyableObject | Refkey;
23
29
  export type Refkey = SymbolRefkey | MemberRefkey | Namekey;
24
30
 
25
- export interface SymbolRefkey extends RefkeyBase {
31
+ export interface SymbolRefkey extends RefkeyableObject {
26
32
  key: string;
27
33
  }
28
34
 
@@ -37,15 +43,17 @@ export interface Namekey<TOptions extends NamekeyOptions = NamekeyOptions>
37
43
  options: TOptions;
38
44
  }
39
45
 
40
- export interface MemberRefkey extends RefkeyBase {
46
+ export interface MemberRefkey extends RefkeyableObject {
41
47
  base: Refkey;
42
- member: Refkey;
48
+ member: Refkey | string;
43
49
  }
44
50
 
45
51
  function createSymbolRefkey(key: string): SymbolRefkey {
46
52
  const refkey: SymbolRefkey = {
47
53
  key,
48
- [RefkeySym]: true,
54
+ [REFKEYABLE]() {
55
+ return this;
56
+ },
49
57
  };
50
58
 
51
59
  markRaw(refkey);
@@ -53,11 +61,16 @@ function createSymbolRefkey(key: string): SymbolRefkey {
53
61
  return refkey;
54
62
  }
55
63
 
56
- export function isRefkey(value: unknown): value is Refkey {
64
+ export function isRefkeyable(value: unknown): value is RefkeyableObject {
57
65
  return (
58
66
  typeof value === "object" &&
59
67
  value !== null &&
60
- Object.hasOwn(value, RefkeySym)
68
+ Object.hasOwn(value, REFKEYABLE)
69
+ );
70
+ }
71
+ export function isRefkey(value: unknown): value is Refkey {
72
+ return (
73
+ isRefkeyable(value) && (value as RefkeyableObject)[REFKEYABLE]() === value
61
74
  );
62
75
  }
63
76
 
@@ -66,11 +79,7 @@ export function isSymbolRefkey(value: unknown): value is SymbolRefkey {
66
79
  }
67
80
 
68
81
  export function isMemberRefkey(value: unknown): value is MemberRefkey {
69
- return (
70
- isRefkey(value) &&
71
- Object.hasOwn(value, "base") &&
72
- Object.hasOwn(value, "member")
73
- );
82
+ return isRefkey(value) && Object.hasOwn(value, "base");
74
83
  }
75
84
 
76
85
  export function isNamekey(value: unknown): value is Namekey {
@@ -82,9 +91,14 @@ export function isNamekey(value: unknown): value is Namekey {
82
91
  * return the key of that refkey, objects get a unique key for that specific object,
83
92
  * and otherwise the key is based on the value.
84
93
  */
85
- function getKey(value: unknown): SymbolRefkey["key"] {
86
- if (isSymbolRefkey(value)) {
87
- return value.key;
94
+ function getKey(value: unknown): string {
95
+ if (isRefkeyable(value)) {
96
+ const refkey = toRefkey(value);
97
+ if (isSymbolRefkey(refkey)) {
98
+ return refkey.key;
99
+ } else {
100
+ return getObjectKey(value);
101
+ }
88
102
  } else if (typeof value === "object" && value !== null) {
89
103
  return getObjectKey(value);
90
104
  } else {
@@ -142,7 +156,9 @@ export function namekey(name: string, options: NamekeyOptions = {}): Namekey {
142
156
  key: getObjectKey({}),
143
157
  name,
144
158
  options,
145
- [RefkeySym]: true,
159
+ [REFKEYABLE]() {
160
+ return this;
161
+ },
146
162
  };
147
163
  }
148
164
  /**
@@ -168,8 +184,8 @@ export function namekey(name: string, options: NamekeyOptions = {}): Namekey {
168
184
  * `refkey(rk1, rk3)`.
169
185
  */
170
186
  export function memberRefkey(
171
- base: Refkey,
172
- ...members: [Refkey, ...Refkey[]]
187
+ base: Refkeyable,
188
+ ...members: [Refkeyable | string, ...(Refkeyable | string)[]]
173
189
  ): MemberRefkey {
174
190
  if (members.length < 1) {
175
191
  throw new Error("memberRefkey needs at least one member");
@@ -177,18 +193,44 @@ export function memberRefkey(
177
193
 
178
194
  if (members.length === 1) {
179
195
  return {
180
- base,
181
- member: members[0],
182
- [RefkeySym]: true,
196
+ base: toRefkey(base),
197
+ member:
198
+ typeof members[0] === "string" ? members[0] : toRefkey(members[0]),
199
+ [REFKEYABLE]() {
200
+ return this;
201
+ },
183
202
  };
184
203
  }
185
204
 
205
+ const lastMember = members.at(-1)!;
206
+
186
207
  return {
187
208
  base: memberRefkey(
188
209
  base,
189
- ...(members.slice(0, -1) as [Refkey, ...Refkey[]]),
210
+ ...(members.slice(0, -1) as [Refkeyable, ...Refkeyable[]]),
190
211
  ),
191
- member: members.at(-1)!,
192
- [RefkeySym]: true,
212
+ member: typeof lastMember === "string" ? lastMember : toRefkey(lastMember),
213
+ [REFKEYABLE]() {
214
+ return this;
215
+ },
193
216
  };
194
217
  }
218
+
219
+ export function inspectRefkey(refkey: Refkey): string {
220
+ const unwrapped = refkey[REFKEYABLE]();
221
+
222
+ const text =
223
+ isMemberRefkey(unwrapped) ?
224
+ `memberRefkey[${inspectRefkey(unwrapped.base)} -> ${
225
+ typeof unwrapped.member === "string" ?
226
+ unwrapped.member
227
+ : inspectRefkey(unwrapped.member)
228
+ }]`
229
+ : `refkey[${unwrapped.key}]`;
230
+
231
+ return text;
232
+ }
233
+
234
+ export function unresolvedRefkey(refkey: Refkey): string {
235
+ return `<Unresolved Symbol: ${inspectRefkey(refkey)}>`;
236
+ }
package/src/render.ts CHANGED
@@ -14,13 +14,15 @@ import {
14
14
  root,
15
15
  untrack,
16
16
  } from "./reactivity.js";
17
- import { isRefkey } from "./refkey.js";
17
+ import { isRefkeyable, toRefkey } from "./refkey.js";
18
18
  import {
19
19
  Child,
20
20
  Children,
21
21
  Component,
22
22
  isComponentCreator,
23
+ isRenderableObject,
23
24
  Props,
25
+ RENDERABLE,
24
26
  } from "./runtime/component.js";
25
27
  import { IntrinsicElement, isIntrinsicElement } from "./runtime/intrinsic.js";
26
28
  import { flushJobs, flushJobsAsync } from "./scheduler.js";
@@ -479,6 +481,7 @@ function appendChild(node: RenderedTextTree, rawChild: Child) {
479
481
  throw new Error("Unknown intrinsic element");
480
482
  }
481
483
  } else if (isComponentCreator(child)) {
484
+ // todo: remove this effect (only needed for context, not needed for anything else)
482
485
  effect(() => {
483
486
  trace(
484
487
  TracePhase.render.appendChild,
@@ -539,15 +542,19 @@ function normalizeChild(child: Child): NormalizedChildren {
539
542
  return "";
540
543
  } else if (isRef(child)) {
541
544
  return () => child.value as () => Child;
542
- } else if (isRefkey(child)) {
545
+ } else if (isRefkeyable(child)) {
546
+ const refkey = toRefkey(child);
543
547
  return () => {
544
548
  const sfContext = useContext(SourceFileContext);
545
549
  if (!sfContext || !sfContext.reference) {
546
550
  throw new Error("Can only emit references inside of source files");
547
551
  }
548
552
 
549
- return sfContext.reference({ refkey: child });
553
+ return sfContext.reference({ refkey });
550
554
  };
555
+ } else if (isRenderableObject(child)) {
556
+ // For custom renderable objects, we will just normalize them to a bound function.
557
+ return child[RENDERABLE].bind(child);
551
558
  } else if (isCustomContext(child)) {
552
559
  return child;
553
560
  } else if (isIntrinsicElement(child)) {
@@ -573,6 +580,10 @@ function debugPrintChild(child: Children): string {
573
580
  return "$ref";
574
581
  } else if (isIntrinsicElement(child)) {
575
582
  return `<${child.name}>`;
583
+ } else if (isRenderableObject(child)) {
584
+ return `CustomChildElement(${JSON.stringify(child)})`;
585
+ } else if (isRefkeyable(child)) {
586
+ return `refkey`;
576
587
  } else {
577
588
  return JSON.stringify(child);
578
589
  }
@@ -1,9 +1,41 @@
1
1
  import { Ref } from "@vue/reactivity";
2
2
  import { CustomContext } from "../reactivity.js";
3
- import { Refkey } from "../refkey.js";
3
+ import { Refkey, RefkeyableObject } from "../refkey.js";
4
4
  import { IntrinsicElement } from "./intrinsic.js";
5
5
 
6
+ export const RENDERABLE = Symbol.for("Alloy.CustomElement");
7
+
8
+ /**
9
+ * A renderable object is any object that has an `[ay.RENDERABLE]` method that
10
+ * returns children. This is used to allow custom object types to be used as
11
+ * children in Alloy components.
12
+ */
13
+ export interface RenderableObject {
14
+ /**
15
+ * Renders this object to children.
16
+ */
17
+ [RENDERABLE](): Children;
18
+ }
19
+
20
+ /**
21
+ * Returns true if the item is a renderable object, meaning it has an `[ay.RENDERABLE]`
22
+ * method.
23
+ *
24
+ * @param item - The item to check.
25
+ * @returns True if the item is a renderable object.
26
+ */
27
+ export function isRenderableObject(item: unknown): item is RenderableObject {
28
+ return (
29
+ typeof item === "object" &&
30
+ item !== null &&
31
+ RENDERABLE in item &&
32
+ typeof (item as any)[RENDERABLE] === "function"
33
+ );
34
+ }
35
+
6
36
  export type Child =
37
+ | RenderableObject
38
+ | RefkeyableObject
7
39
  | string
8
40
  | boolean
9
41
  | number
package/src/scheduler.ts CHANGED
@@ -1,24 +1,25 @@
1
- import { ReactiveEffectRunner } from "@vue/reactivity";
1
+ import { ReactiveEffect } from "@vue/reactivity";
2
2
 
3
3
  export interface QueueJob {
4
- (): any;
4
+ run(): void;
5
5
  }
6
6
  const immediateQueue = new Set<QueueJob>();
7
7
  const queue = new Set<QueueJob>();
8
8
  const pendingPromises = new Set<Promise<any>>();
9
9
 
10
- export function scheduler(
11
- jobGetter: () => ReactiveEffectRunner,
12
- immediate = false,
13
- ) {
14
- return () => {
15
- queueJob(jobGetter(), immediate);
10
+ export function scheduler(immediate = false) {
11
+ return function (this: ReactiveEffect) {
12
+ queueJob(this, immediate);
16
13
  };
17
14
  }
18
- export function queueJob(job: QueueJob, immediate = false) {
15
+ export function queueJob(job: QueueJob | (() => void), immediate = false) {
19
16
  // if we have an immediate job, we don't need to queue the normal job.
20
17
  // the set is serving an important purpose here in deduping the effects we run
21
18
  // (which in effect coalesces multiple update effects together).
19
+ if (typeof job === "function") {
20
+ job = { run: job };
21
+ }
22
+
22
23
  if (immediate) {
23
24
  immediateQueue.add(job);
24
25
  } else {
@@ -41,7 +42,7 @@ export function flushJobs() {
41
42
  // First, run all synchronous jobs
42
43
  let job;
43
44
  while ((job = takeJob()) !== null) {
44
- job();
45
+ job.run();
45
46
  }
46
47
 
47
48
  // If there are no pending promises, we're done
@@ -58,7 +59,7 @@ export async function flushJobsAsync() {
58
59
  // First, run all synchronous jobs
59
60
  let job;
60
61
  while ((job = takeJob()) !== null) {
61
- job();
62
+ job.run();
62
63
  }
63
64
 
64
65
  // If there are no pending promises, we're done
@@ -256,9 +256,9 @@ export abstract class OutputScope {
256
256
  }
257
257
 
258
258
  toString() {
259
- const ownerSymbol = this.ownerSymbol ? ` for ${this.ownerSymbol}` : "";
260
- return untrack(
261
- () => `${this.constructor.name} ${this.name}[${this.id}]${ownerSymbol}`,
262
- );
259
+ return untrack(() => {
260
+ const ownerSymbol = this.ownerSymbol ? ` for ${this.ownerSymbol}` : "";
261
+ return `${this.constructor.name} ${this.name}[${this.id}]${ownerSymbol}`;
262
+ });
263
263
  }
264
264
  }