@alloy-js/core 0.22.0-dev.1 → 0.22.0-dev.3

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 (65) hide show
  1. package/dist/src/components/Block.d.ts.map +1 -1
  2. package/dist/src/components/Block.js +24 -7
  3. package/dist/src/components/Block.js.map +1 -1
  4. package/dist/src/components/Indent.d.ts.map +1 -1
  5. package/dist/src/components/Indent.js +2 -1
  6. package/dist/src/components/Indent.js.map +1 -1
  7. package/dist/src/components/Prose.d.ts.map +1 -1
  8. package/dist/src/components/Prose.js +2 -1
  9. package/dist/src/components/Prose.js.map +1 -1
  10. package/dist/src/content-slot.d.ts +51 -0
  11. package/dist/src/content-slot.d.ts.map +1 -0
  12. package/dist/src/content-slot.js +69 -0
  13. package/dist/src/content-slot.js.map +1 -0
  14. package/dist/src/content-slot.test.d.ts +2 -0
  15. package/dist/src/content-slot.test.d.ts.map +1 -0
  16. package/dist/src/content-slot.test.js +57 -0
  17. package/dist/src/content-slot.test.js.map +1 -0
  18. package/dist/src/index.d.ts +1 -0
  19. package/dist/src/index.d.ts.map +1 -1
  20. package/dist/src/index.js +1 -0
  21. package/dist/src/index.js.map +1 -1
  22. package/dist/src/reactivity.d.ts +15 -1
  23. package/dist/src/reactivity.d.ts.map +1 -1
  24. package/dist/src/reactivity.js +20 -8
  25. package/dist/src/reactivity.js.map +1 -1
  26. package/dist/src/render.d.ts +1 -0
  27. package/dist/src/render.d.ts.map +1 -1
  28. package/dist/src/render.js +66 -2
  29. package/dist/src/render.js.map +1 -1
  30. package/dist/src/symbols/decl.d.ts +8 -0
  31. package/dist/src/symbols/decl.d.ts.map +1 -0
  32. package/dist/src/symbols/decl.js +22 -0
  33. package/dist/src/symbols/decl.js.map +1 -0
  34. package/dist/src/symbols/index.d.ts +1 -0
  35. package/dist/src/symbols/index.d.ts.map +1 -1
  36. package/dist/src/symbols/index.js +1 -0
  37. package/dist/src/symbols/index.js.map +1 -1
  38. package/dist/src/utils.d.ts.map +1 -1
  39. package/dist/src/utils.js +212 -15
  40. package/dist/src/utils.js.map +1 -1
  41. package/dist/test/components/block.test.d.ts.map +1 -1
  42. package/dist/test/components/block.test.js +18 -1
  43. package/dist/test/components/block.test.js.map +1 -1
  44. package/dist/test/components/list.test.d.ts.map +1 -1
  45. package/dist/test/components/list.test.js +80 -1
  46. package/dist/test/components/list.test.js.map +1 -1
  47. package/dist/test/control-flow/for.test.js +32 -2
  48. package/dist/test/control-flow/for.test.js.map +1 -1
  49. package/dist/tsconfig.tsbuildinfo +1 -1
  50. package/package.json +1 -1
  51. package/src/components/Block.tsx +18 -6
  52. package/src/components/Indent.tsx +4 -2
  53. package/src/components/Prose.tsx +2 -1
  54. package/src/content-slot.test.tsx +65 -0
  55. package/src/content-slot.tsx +91 -0
  56. package/src/index.ts +1 -0
  57. package/src/reactivity.ts +38 -5
  58. package/src/render.ts +80 -3
  59. package/src/symbols/decl.ts +25 -0
  60. package/src/symbols/index.ts +1 -0
  61. package/src/utils.tsx +240 -16
  62. package/temp/api.json +469 -0
  63. package/test/components/block.test.tsx +21 -1
  64. package/test/components/list.test.tsx +76 -1
  65. package/test/control-flow/for.test.tsx +43 -2
@@ -1,6 +1,6 @@
1
1
  import { computed } from "@vue/reactivity";
2
+ import { createContentSlot } from "../content-slot.jsx";
2
3
  import type { Children } from "../runtime/component.js";
3
- import { childrenArray } from "../utils.jsx";
4
4
  import { Indent } from "./Indent.jsx";
5
5
 
6
6
  export interface BlockProps {
@@ -33,17 +33,29 @@ export interface BlockProps {
33
33
  * added after the block, which defaults to `"}"`.
34
34
  */
35
35
  export function Block(props: BlockProps) {
36
- const childCount = computed(() => childrenArray(() => props.children).length);
36
+ const ContentSlot = createContentSlot();
37
+ const leadingNewline = computed(() => {
38
+ if (!props.newline) return false;
39
+
40
+ // When inline, we want a newline only when content breaks, otherwise a nothing..
41
+ if (props.inline) return <sbr />;
42
+
43
+ // When not inline, we want a hardline when the slot has contents otherwise a space.
44
+ if (ContentSlot.hasContent) return <hbr />;
45
+
46
+ return " ";
47
+ });
37
48
  return (
38
49
  <group>
39
- {props.newline && (props.inline ? <softline /> : <br />)}
50
+ {leadingNewline}
40
51
  {props.opener ?? "{"}
41
52
  <Indent
42
- line={props.inline && childCount.value > 0}
43
- softline={childCount.value === 0}
53
+ hardline={!props.inline && ContentSlot.hasContent}
54
+ line={props.inline && ContentSlot.hasContent}
55
+ softline={!props.inline || ContentSlot.isEmpty}
44
56
  trailingBreak
45
57
  >
46
- {props.children}
58
+ <ContentSlot>{props.children}</ContentSlot>
47
59
  </Indent>
48
60
  {props.closer ?? "}"}
49
61
  </group>
@@ -1,3 +1,4 @@
1
+ import { computed } from "@vue/reactivity";
1
2
  import type { Children } from "../runtime/component.js";
2
3
 
3
4
  export interface IndentProps {
@@ -40,12 +41,13 @@ export interface IndentProps {
40
41
  * break suitable for typical blocks of statements but can be configured.
41
42
  */
42
43
  export function Indent(props: IndentProps) {
43
- const breakElem =
44
+ const breakElem = computed(() =>
44
45
  props.nobreak ? ""
45
46
  : props.hardline ? <hbr />
46
47
  : props.softline ? <sbr />
47
48
  : props.line ? <br />
48
- : <hbr />;
49
+ : <hbr />,
50
+ );
49
51
 
50
52
  return (
51
53
  <>
@@ -1,5 +1,6 @@
1
- import { childrenArray, computed } from "@alloy-js/core";
1
+ import { computed } from "@vue/reactivity";
2
2
  import type { Children } from "../runtime/component.js";
3
+ import { childrenArray } from "../utils.jsx";
3
4
 
4
5
  export interface Prose {
5
6
  children: Children;
@@ -0,0 +1,65 @@
1
+ import { ref } from "@vue/reactivity";
2
+ import { expect, it } from "vitest";
3
+ import "../testing/extend-expect.js";
4
+ import { Show } from "./components/Show.jsx";
5
+ import { createContentSlot } from "./content-slot.jsx";
6
+ import { printTree, renderTree } from "./render.js";
7
+
8
+ it("knows when its empty", () => {
9
+ const ContentSlot = createContentSlot();
10
+
11
+ expect(
12
+ <>
13
+ {ContentSlot.hasContent && "{"}
14
+ <ContentSlot>hi</ContentSlot>
15
+ {ContentSlot.hasContent && "}"}
16
+ </>,
17
+ ).toRenderTo(`
18
+ {hi}
19
+ `);
20
+
21
+ expect(
22
+ <>
23
+ {ContentSlot.hasContent && "{"}
24
+ <ContentSlot>{false}</ContentSlot>
25
+ {ContentSlot.hasContent && "}"}
26
+ </>,
27
+ ).toRenderTo(``);
28
+ });
29
+
30
+ it("is reactive", () => {
31
+ const ContentSlot = createContentSlot();
32
+ const showContent = ref(false);
33
+
34
+ const tree = renderTree(
35
+ <>
36
+ {ContentSlot.isEmpty && "It's empty!"}
37
+ <ContentSlot>
38
+ <Show when={showContent.value}>Content!</Show>
39
+ </ContentSlot>
40
+ </>,
41
+ );
42
+
43
+ expect(printTree(tree)).toBe(`It's empty!`);
44
+ showContent.value = true;
45
+ expect(printTree(tree)).toBe(`Content!`);
46
+ });
47
+
48
+ it("works with WhenEmpty and WhenHasContent", () => {
49
+ const ContentSlot = createContentSlot();
50
+ const showContent = ref(false);
51
+
52
+ const tree = renderTree(
53
+ <>
54
+ <ContentSlot.WhenEmpty>It's empty!</ContentSlot.WhenEmpty>
55
+ <ContentSlot.WhenHasContent>Has content!</ContentSlot.WhenHasContent>
56
+ <ContentSlot>
57
+ <Show when={showContent.value}>Content!</Show>
58
+ </ContentSlot>
59
+ </>,
60
+ );
61
+
62
+ expect(printTree(tree)).toBe(`It's empty!`);
63
+ showContent.value = true;
64
+ expect(printTree(tree)).toBe(`Has content!Content!`);
65
+ });
@@ -0,0 +1,91 @@
1
+ import { effect, Ref, shallowRef } from "@vue/reactivity";
2
+ import { Show } from "./components/Show.jsx";
3
+ import { getContext } from "./reactivity.js";
4
+ import { Children, Component } from "./runtime/component.js";
5
+
6
+ export interface ContentSlot {
7
+ (props: { children: Children }): Children;
8
+ /**
9
+ * A ref indicating whether the slot is currently empty.
10
+ */
11
+ ref: Ref<boolean>;
12
+
13
+ /**
14
+ * Whether the slot is currently empty.
15
+ */
16
+ isEmpty: boolean;
17
+
18
+ /**
19
+ * Whether the slot has any content.
20
+ */
21
+ hasContent: boolean;
22
+
23
+ /**
24
+ * A component that will render its contents when the content slot is empty.
25
+ */
26
+ WhenEmpty: Component<{}>;
27
+
28
+ /**
29
+ * A component that will render its contents when the content slot has content.
30
+ */
31
+ WhenHasContent: Component<{}>;
32
+ }
33
+
34
+ /**
35
+ * Create a component which tracks whether any content is placed inside of it.
36
+ * The component exposes a ref `isEmpty` which indicates whether the slot is
37
+ * empty, as well as convenience accessors `isEmpty` and `hasContent`.
38
+ * Additionally, it provides two sub-components, `WhenEmpty` and
39
+ * `WhenHasContent`, which render their contents conditionally based on whether
40
+ * the slot is empty.
41
+ *
42
+ * @example
43
+ *
44
+ * ```tsx
45
+ * const ContentSlot = createContentSlot();
46
+ *
47
+ * <>
48
+ * <ContentSlot.WhenEmpty>The slot is empty!</ContentSlot.WhenEmpty>
49
+ * <ContentSlot.WhenHasContent>The slot has content!</ContentSlot.WhenHasContent>
50
+ * <ContentSlot>
51
+ * {someCondition && "Here is some content!"}
52
+ * </ContentSlot>
53
+ * </>
54
+ * ```
55
+ */
56
+ export function createContentSlot(): ContentSlot {
57
+ const isEmpty = shallowRef<boolean>(false);
58
+
59
+ function ContentSlot(props: { children: Children }) {
60
+ const context = getContext()!;
61
+ effect(() => {
62
+ isEmpty.value = context.isEmpty!.value;
63
+ });
64
+
65
+ return props.children;
66
+ }
67
+ ContentSlot.ref = isEmpty;
68
+ ContentSlot.WhenEmpty = function WhenEmpty(props: { children: Children }) {
69
+ return <Show when={isEmpty.value}>{props.children}</Show>;
70
+ };
71
+
72
+ ContentSlot.WhenHasContent = function WhenHasContent(props: {
73
+ children: Children;
74
+ }) {
75
+ return <Show when={!isEmpty.value}>{props.children}</Show>;
76
+ };
77
+
78
+ Object.defineProperty(ContentSlot, "isEmpty", {
79
+ get() {
80
+ return isEmpty.value;
81
+ },
82
+ });
83
+
84
+ Object.defineProperty(ContentSlot, "hasContent", {
85
+ get() {
86
+ return !isEmpty.value;
87
+ },
88
+ });
89
+
90
+ return ContentSlot as any;
91
+ }
package/src/index.ts CHANGED
@@ -25,6 +25,7 @@ export {
25
25
  export * from "./binder.js";
26
26
  export * from "./code.js";
27
27
  export * from "./components/index.js";
28
+ export * from "./content-slot.js";
28
29
  export * from "./context.js";
29
30
  export * from "./context/index.js";
30
31
  export * from "./library-symbol-reference.js";
package/src/reactivity.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import {
2
+ isRef,
2
3
  pauseTracking,
3
4
  ReactiveEffectRunner,
5
+ ref,
6
+ Ref,
4
7
  resetTracking,
5
8
  ShallowReactive,
6
9
  shallowRef,
@@ -66,6 +69,22 @@ export interface Context {
66
69
  * The symbol that this component has taken.
67
70
  */
68
71
  takenSymbols?: ShallowReactive<Set<OutputSymbol>>;
72
+
73
+ /**
74
+ * The number of child nodes that have content. When zero, this component is
75
+ * semantically empty.
76
+ */
77
+ childrenWithContent: number;
78
+
79
+ /**
80
+ * A ref that indicates whether the component is empty.
81
+ */
82
+ isEmpty?: Ref<boolean>;
83
+
84
+ /**
85
+ * Whether this context is a root context
86
+ */
87
+ isRoot: boolean;
69
88
  }
70
89
 
71
90
  let globalContext: Context | null = null;
@@ -86,6 +105,9 @@ export function root<T>(fn: (d: Disposable) => T, options?: RootOptions): T {
86
105
  elementCache: new Map(),
87
106
  takesSymbols: false,
88
107
  takenSymbols: undefined,
108
+ childrenWithContent: 0,
109
+ isEmpty: ref(true),
110
+ isRoot: true,
89
111
  };
90
112
 
91
113
  globalContext = context;
@@ -131,6 +153,8 @@ export function effect<T>(fn: (prev?: T) => T, current?: T) {
131
153
  elementCache: new Map(),
132
154
  takesSymbols: false,
133
155
  takenSymbols: undefined,
156
+ childrenWithContent: 0,
157
+ isRoot: false,
134
158
  };
135
159
 
136
160
  const cleanupFn = (final: boolean) => {
@@ -162,19 +186,16 @@ export function effect<T>(fn: (prev?: T) => T, current?: T) {
162
186
  scheduler: scheduler(),
163
187
  onTrack(event) {
164
188
  trace(TracePhase.effect.track, () => {
165
- return `tracking ${event.target}, ${String(event.key)}`;
189
+ return `tracking ${isRef(event.target) ? `Ref:${refId(event.target)}` : event.target}, ${String(event.key)}`;
166
190
  });
167
191
  },
168
192
  onTrigger(event) {
169
193
  trace(TracePhase.effect.trigger, () => {
170
- return `triggering ${event.target}, ${String(event.key)}`;
194
+ return `triggering ${isRef(event.target) ? `Ref:${refId(event.target)}` : event.target}, ${String(event.key)}`;
171
195
  });
172
196
  },
173
197
  },
174
198
  );
175
-
176
- // allow recursive effects (recursive option does nothing, possible bug)
177
- (runner as any).effect.flags |= 1 << 5;
178
199
  }
179
200
 
180
201
  /**
@@ -231,3 +252,15 @@ export function isCustomContext(child: Children): child is CustomContext {
231
252
  Object.hasOwn(child, CUSTOM_CONTEXT_SYM)
232
253
  );
233
254
  }
255
+
256
+ const seenRefs = new WeakMap<Ref<unknown>, number>();
257
+ let refIdCounter = 1;
258
+
259
+ export function refId(ref: Ref<unknown>): number {
260
+ let id = seenRefs.get(ref);
261
+ if (id === undefined) {
262
+ id = refIdCounter++;
263
+ seenRefs.set(ref, id);
264
+ }
265
+ return id;
266
+ }
package/src/render.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { isRef } from "@vue/reactivity";
1
+ import { isRef, ref } from "@vue/reactivity";
2
2
  import { Doc, doc } from "prettier";
3
3
  import prettier from "prettier/doc.js";
4
4
  import { useContext } from "./context.js";
@@ -332,11 +332,74 @@ function renderWorker(node: RenderedTextTree, children: Children) {
332
332
  }
333
333
  }
334
334
 
335
+ function contentAdded() {
336
+ const context: Context = getContext()!;
337
+ context.childrenWithContent++;
338
+ }
339
+
340
+ export function notifyContentState() {
341
+ untrack(() => {
342
+ const startContext = getContext()!;
343
+
344
+ if (startContext.childrenWithContent === 0) {
345
+ if (startContext.isEmpty!.value === true) {
346
+ // it was already empty, no work to do.
347
+ return;
348
+ }
349
+
350
+ if (startContext.isEmpty) {
351
+ startContext.isEmpty.value = true;
352
+ }
353
+
354
+ // otherwise we need to decrement the content counts up the tree.
355
+ let current = startContext.owner;
356
+ while (current) {
357
+ if (current.childrenWithContent === 0) {
358
+ break;
359
+ }
360
+ current.childrenWithContent--;
361
+ if (current.isEmpty) {
362
+ current.isEmpty.value = true;
363
+ }
364
+ current = current.owner;
365
+ }
366
+ } else {
367
+ if (startContext.isEmpty!.value === false) {
368
+ // it was already non-empty, no work to do.
369
+ return;
370
+ }
371
+
372
+ if (startContext.isEmpty && startContext.isEmpty.value) {
373
+ startContext.isEmpty.value = false;
374
+ }
375
+
376
+ // otherwise we need to increment the content counts up the tree.
377
+ let current = startContext.owner;
378
+ while (current) {
379
+ current.childrenWithContent++;
380
+ if (current.childrenWithContent > 1) {
381
+ // This isn't the first content so we have no work to do
382
+ break;
383
+ }
384
+
385
+ if (current.isEmpty && current.isEmpty.value) {
386
+ current.isEmpty.value = false;
387
+ }
388
+
389
+ current = current.owner;
390
+ }
391
+ }
392
+ });
393
+ }
394
+
335
395
  function appendChild(node: RenderedTextTree, rawChild: Child) {
336
396
  trace(TracePhase.render.appendChild, () => debugPrintChild(rawChild));
337
397
  const child = normalizeChild(rawChild);
338
398
 
339
399
  if (typeof child === "string") {
400
+ if (child !== "") {
401
+ contentAdded();
402
+ }
340
403
  node.push(child);
341
404
  } else {
342
405
  const cache = getElementCache();
@@ -358,6 +421,7 @@ function appendChild(node: RenderedTextTree, rawChild: Child) {
358
421
  renderWorker(newNode, children);
359
422
  node.push(newNode);
360
423
  cache.set(child, newNode);
424
+ notifyContentState();
361
425
  });
362
426
  } else if (isIntrinsicElement(child)) {
363
427
  trace(
@@ -488,6 +552,9 @@ function appendChild(node: RenderedTextTree, rawChild: Child) {
488
552
  () => "Component: " + debugPrintChild(child),
489
553
  );
490
554
  const context = getContext();
555
+ context!.childrenWithContent = 0;
556
+ context!.isEmpty ??= ref(true);
557
+
491
558
  if (context) context.componentOwner = child;
492
559
  const componentRoot: RenderedTextTree = [];
493
560
  pushStack(child.component, child.props);
@@ -495,10 +562,14 @@ function appendChild(node: RenderedTextTree, rawChild: Child) {
495
562
  popStack();
496
563
  node.push(componentRoot);
497
564
  cache.set(child, componentRoot);
498
-
565
+ notifyContentState();
499
566
  trace(
500
567
  TracePhase.render.appendChild,
501
- () => "Component done: " + debugPrintChild(child),
568
+ () =>
569
+ "Component done: " +
570
+ debugPrintChild(child) +
571
+ ", empty: " +
572
+ context!.isEmpty!.value,
502
573
  );
503
574
  });
504
575
  } else if (typeof child === "function") {
@@ -510,10 +581,16 @@ function appendChild(node: RenderedTextTree, rawChild: Child) {
510
581
  while (typeof res === "function" && !isComponentCreator(res)) {
511
582
  res = res();
512
583
  }
584
+ const context = getContext();
585
+ context!.childrenWithContent = 0;
586
+ context!.isEmpty ??= ref(true);
587
+
513
588
  const newNodes: RenderedTextTree = [];
514
589
  renderWorker(newNodes, res);
515
590
  node[index] = newNodes;
516
591
  cache.set(child, newNodes);
592
+
593
+ notifyContentState();
517
594
  return newNodes;
518
595
  });
519
596
  } else {
@@ -0,0 +1,25 @@
1
+ import { toRef } from "@vue/reactivity";
2
+ import { useScope } from "../context/scope.js";
3
+ import { Namekey } from "../refkey.js";
4
+ import { createComponent } from "../runtime/component.js";
5
+ import { BasicScope } from "./basic-scope.js";
6
+ import { BasicSymbol } from "./basic-symbol.js";
7
+
8
+ /**
9
+ * Create a declaration in the current scope with the given namekey. Only works
10
+ * with basic scopes. Import `decl` from a specific language library for
11
+ * declaring language-specific symbols.
12
+ */
13
+ export function decl(namekey: Namekey) {
14
+ return createComponent(() => {
15
+ const currentScope = useScope();
16
+ if (!(currentScope instanceof BasicScope)) {
17
+ throw new Error(
18
+ `Cannot declare symbol in non-basic scope: ${currentScope.constructor.name}. Use a language-specific 'decl' function instead.`,
19
+ );
20
+ }
21
+ const symbol = new BasicSymbol(namekey, currentScope.symbols);
22
+
23
+ return toRef(symbol, "name");
24
+ }, {});
25
+ }
@@ -1,5 +1,6 @@
1
1
  export * from "./basic-scope.js";
2
2
  export * from "./basic-symbol.js";
3
+ export * from "./decl.js";
3
4
  export * from "./output-scope.js";
4
5
  export * from "./output-space.js";
5
6
  export * from "./output-symbol.js";