@alloy-js/core 0.4.0 → 0.6.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 (228) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/babel.config.cjs +4 -1
  3. package/dist/src/binder.d.ts +20 -13
  4. package/dist/src/binder.d.ts.map +1 -1
  5. package/dist/src/binder.js +33 -15
  6. package/dist/src/binder.js.map +1 -1
  7. package/dist/src/code.d.ts +2 -2
  8. package/dist/src/code.d.ts.map +1 -1
  9. package/dist/src/code.js +4 -4
  10. package/dist/src/code.js.map +1 -1
  11. package/dist/src/components/Block.d.ts +25 -0
  12. package/dist/src/components/Block.d.ts.map +1 -0
  13. package/dist/src/components/Block.js +25 -0
  14. package/dist/src/components/Block.js.map +1 -0
  15. package/dist/src/components/Declaration.d.ts +1 -1
  16. package/dist/src/components/Declaration.d.ts.map +1 -1
  17. package/dist/src/components/Declaration.js +4 -0
  18. package/dist/src/components/Declaration.js.map +1 -1
  19. package/dist/src/components/For.d.ts +44 -0
  20. package/dist/src/components/For.d.ts.map +1 -0
  21. package/dist/src/components/For.js +41 -0
  22. package/dist/src/components/For.js.map +1 -0
  23. package/dist/src/components/Indent.d.ts +6 -10
  24. package/dist/src/components/Indent.d.ts.map +1 -1
  25. package/dist/src/components/Indent.js +7 -18
  26. package/dist/src/components/Indent.js.map +1 -1
  27. package/dist/src/components/List.d.ts +38 -0
  28. package/dist/src/components/List.d.ts.map +1 -0
  29. package/dist/src/components/List.js +40 -0
  30. package/dist/src/components/List.js.map +1 -0
  31. package/dist/src/components/MemberDeclaration.d.ts +1 -1
  32. package/dist/src/components/MemberDeclaration.d.ts.map +1 -1
  33. package/dist/src/components/MemberDeclaration.js.map +1 -1
  34. package/dist/src/components/MemberName.d.ts +1 -1
  35. package/dist/src/components/MemberName.d.ts.map +1 -1
  36. package/dist/src/components/MemberName.js +1 -1
  37. package/dist/src/components/MemberName.js.map +1 -1
  38. package/dist/src/components/MemberScope.d.ts +1 -1
  39. package/dist/src/components/MemberScope.d.ts.map +1 -1
  40. package/dist/src/components/MemberScope.js.map +1 -1
  41. package/dist/src/components/Name.d.ts +1 -1
  42. package/dist/src/components/Name.d.ts.map +1 -1
  43. package/dist/src/components/Name.js +1 -1
  44. package/dist/src/components/Name.js.map +1 -1
  45. package/dist/src/components/Output.d.ts +3 -2
  46. package/dist/src/components/Output.d.ts.map +1 -1
  47. package/dist/src/components/Output.js +12 -2
  48. package/dist/src/components/Output.js.map +1 -1
  49. package/dist/src/components/Scope.d.ts +1 -1
  50. package/dist/src/components/Scope.d.ts.map +1 -1
  51. package/dist/src/components/Scope.js.map +1 -1
  52. package/dist/src/components/Show.d.ts +8 -0
  53. package/dist/src/components/Show.d.ts.map +1 -0
  54. package/dist/src/components/Show.js +4 -0
  55. package/dist/src/components/Show.js.map +1 -0
  56. package/dist/src/components/SourceDirectory.d.ts +2 -2
  57. package/dist/src/components/SourceDirectory.d.ts.map +1 -1
  58. package/dist/src/components/SourceDirectory.js +1 -0
  59. package/dist/src/components/SourceDirectory.js.map +1 -1
  60. package/dist/src/components/SourceFile.d.ts +4 -8
  61. package/dist/src/components/SourceFile.d.ts.map +1 -1
  62. package/dist/src/components/SourceFile.js +6 -13
  63. package/dist/src/components/SourceFile.js.map +1 -1
  64. package/dist/src/components/StatementList.d.ts +9 -0
  65. package/dist/src/components/StatementList.d.ts.map +1 -0
  66. package/dist/src/components/StatementList.js +17 -0
  67. package/dist/src/components/StatementList.js.map +1 -0
  68. package/dist/src/components/Switch.d.ts +41 -0
  69. package/dist/src/components/Switch.d.ts.map +1 -0
  70. package/dist/src/components/Switch.js +41 -0
  71. package/dist/src/components/Switch.js.map +1 -0
  72. package/dist/src/components/Wrap.d.ts +20 -0
  73. package/dist/src/components/Wrap.d.ts.map +1 -0
  74. package/dist/src/components/Wrap.js +15 -0
  75. package/dist/src/components/Wrap.js.map +1 -0
  76. package/dist/src/components/index.d.ts +8 -1
  77. package/dist/src/components/index.d.ts.map +1 -1
  78. package/dist/src/components/index.js +7 -0
  79. package/dist/src/components/index.js.map +1 -1
  80. package/dist/src/components/stc/index.d.ts +89 -18
  81. package/dist/src/components/stc/index.d.ts.map +1 -1
  82. package/dist/src/components/stc/index.js +17 -1
  83. package/dist/src/components/stc/index.js.map +1 -1
  84. package/dist/src/context/index.d.ts +0 -1
  85. package/dist/src/context/index.d.ts.map +1 -1
  86. package/dist/src/context/index.js +0 -1
  87. package/dist/src/context/index.js.map +1 -1
  88. package/dist/src/context/member-declaration.d.ts +1 -0
  89. package/dist/src/context/member-declaration.d.ts.map +1 -1
  90. package/dist/src/context/member-declaration.js +4 -1
  91. package/dist/src/context/member-declaration.js.map +1 -1
  92. package/dist/src/context.d.ts.map +1 -1
  93. package/dist/src/context.js +3 -3
  94. package/dist/src/context.js.map +1 -1
  95. package/dist/src/index.browser.d.ts +3 -0
  96. package/dist/src/index.browser.d.ts.map +1 -0
  97. package/dist/src/index.browser.js +3 -0
  98. package/dist/src/index.browser.js.map +1 -0
  99. package/dist/src/index.d.ts +2 -0
  100. package/dist/src/index.d.ts.map +1 -1
  101. package/dist/src/index.js +2 -0
  102. package/dist/src/index.js.map +1 -1
  103. package/dist/src/jsx-runtime.d.ts +151 -7
  104. package/dist/src/jsx-runtime.d.ts.map +1 -1
  105. package/dist/src/jsx-runtime.js +113 -12
  106. package/dist/src/jsx-runtime.js.map +1 -1
  107. package/dist/src/render.d.ts +107 -132
  108. package/dist/src/render.d.ts.map +1 -1
  109. package/dist/src/render.js +272 -178
  110. package/dist/src/render.js.map +1 -1
  111. package/dist/src/stc.d.ts +14 -0
  112. package/dist/src/stc.d.ts.map +1 -0
  113. package/dist/src/stc.js +52 -0
  114. package/dist/src/stc.js.map +1 -0
  115. package/dist/src/tap.d.ts +19 -0
  116. package/dist/src/tap.d.ts.map +1 -0
  117. package/dist/src/tap.js +39 -0
  118. package/dist/src/tap.js.map +1 -0
  119. package/dist/src/utils.d.ts +22 -15
  120. package/dist/src/utils.d.ts.map +1 -1
  121. package/dist/src/utils.js +95 -59
  122. package/dist/src/utils.js.map +1 -1
  123. package/dist/test/browser-build.test.d.ts +2 -0
  124. package/dist/test/browser-build.test.d.ts.map +1 -0
  125. package/dist/test/components/block.test.d.ts +2 -0
  126. package/dist/test/components/block.test.d.ts.map +1 -0
  127. package/dist/test/components/declaration.test.d.ts +2 -0
  128. package/dist/test/components/declaration.test.d.ts.map +1 -0
  129. package/dist/test/components/list.test.d.ts +2 -0
  130. package/dist/test/components/list.test.d.ts.map +1 -0
  131. package/dist/test/components/wrap.test.d.ts +2 -0
  132. package/dist/test/components/wrap.test.d.ts.map +1 -0
  133. package/dist/test/control-flow/for.test.d.ts +2 -0
  134. package/dist/test/control-flow/for.test.d.ts.map +1 -0
  135. package/dist/test/control-flow/match.test.d.ts +2 -0
  136. package/dist/test/control-flow/match.test.d.ts.map +1 -0
  137. package/dist/test/control-flow/show.test.d.ts +2 -0
  138. package/dist/test/control-flow/show.test.d.ts.map +1 -0
  139. package/dist/test/reactivity/cleanup.test.d.ts +2 -0
  140. package/dist/test/reactivity/cleanup.test.d.ts.map +1 -0
  141. package/dist/test/reactivity/memo.test.d.ts +2 -0
  142. package/dist/test/reactivity/memo.test.d.ts.map +1 -0
  143. package/dist/test/reactivity/untrack.test.d.ts +2 -0
  144. package/dist/test/reactivity/untrack.test.d.ts.map +1 -0
  145. package/dist/test/rendering/formatting.test.d.ts +2 -0
  146. package/dist/test/rendering/formatting.test.d.ts.map +1 -0
  147. package/dist/test/rendering/memoization.test.d.ts +2 -0
  148. package/dist/test/rendering/memoization.test.d.ts.map +1 -0
  149. package/dist/test/split-props.test.d.ts +2 -0
  150. package/dist/test/split-props.test.d.ts.map +1 -0
  151. package/dist/test/stc.test.d.ts.map +1 -1
  152. package/dist/test/utils.test.d.ts.map +1 -1
  153. package/dist/testing/extend-expect.js +4 -4
  154. package/dist/testing/extend-expect.js.map +1 -1
  155. package/dist/testing/render.d.ts +2 -3
  156. package/dist/testing/render.d.ts.map +1 -1
  157. package/dist/testing/render.js +2 -4
  158. package/dist/testing/render.js.map +1 -1
  159. package/dist/tsconfig.tsbuildinfo +1 -1
  160. package/package.json +8 -9
  161. package/src/binder.ts +60 -50
  162. package/src/code.ts +17 -12
  163. package/src/components/Block.tsx +44 -0
  164. package/src/components/Declaration.tsx +10 -4
  165. package/src/components/For.tsx +81 -0
  166. package/src/components/Indent.tsx +20 -27
  167. package/src/components/List.tsx +94 -0
  168. package/src/components/MemberDeclaration.tsx +9 -6
  169. package/src/components/MemberScope.tsx +4 -2
  170. package/src/components/Output.tsx +27 -14
  171. package/src/components/Scope.tsx +4 -2
  172. package/src/components/Show.tsx +11 -0
  173. package/src/components/SourceDirectory.tsx +6 -2
  174. package/src/components/SourceFile.tsx +13 -17
  175. package/src/components/StatementList.tsx +16 -0
  176. package/src/components/Switch.tsx +62 -0
  177. package/src/components/Wrap.tsx +29 -0
  178. package/src/components/index.tsx +8 -1
  179. package/src/components/stc/index.ts +18 -1
  180. package/src/context/index.ts +0 -1
  181. package/src/context/member-declaration.ts +9 -1
  182. package/src/context.ts +2 -3
  183. package/src/index.browser.ts +2 -0
  184. package/src/index.ts +2 -0
  185. package/src/jsx-runtime.ts +265 -23
  186. package/src/render.ts +382 -200
  187. package/src/stc.ts +95 -0
  188. package/src/tap.ts +69 -0
  189. package/src/utils.ts +162 -95
  190. package/temp/api.json +8042 -1886
  191. package/test/browser-build.test.ts +91 -0
  192. package/test/children.test.tsx +8 -10
  193. package/test/components/block.test.tsx +48 -0
  194. package/test/components/declaration.test.tsx +37 -0
  195. package/test/components/list.test.tsx +91 -0
  196. package/test/components/slot.test.tsx +31 -25
  197. package/test/components/source-file.test.tsx +11 -31
  198. package/test/components/wrap.test.tsx +42 -0
  199. package/test/control-flow/for.test.tsx +194 -0
  200. package/test/control-flow/match.test.tsx +49 -0
  201. package/test/control-flow/show.test.tsx +25 -0
  202. package/test/name-policy.test.tsx +5 -5
  203. package/test/reactivity/cleanup.test.tsx +91 -0
  204. package/test/reactivity/memo.test.tsx +17 -0
  205. package/test/reactivity/ref-rendering.test.tsx +3 -8
  206. package/test/reactivity/test.test.tsx +7 -6
  207. package/test/reactivity/untrack.test.ts +33 -0
  208. package/test/rendering/basic.test.tsx +25 -47
  209. package/test/rendering/code.test.tsx +3 -3
  210. package/test/rendering/formatting.test.tsx +487 -0
  211. package/test/rendering/indent.test.tsx +42 -529
  212. package/test/rendering/memoization.test.tsx +30 -0
  213. package/test/split-props.test.ts +87 -0
  214. package/test/stc.test.tsx +29 -8
  215. package/test/symbols.test.ts +132 -3
  216. package/test/utils.test.tsx +129 -20
  217. package/testing/extend-expect.ts +14 -4
  218. package/testing/render.ts +2 -4
  219. package/testing/vitest.d.ts +6 -1
  220. package/vitest.config.ts +1 -1
  221. package/dist/src/context/indent.d.ts +0 -5
  222. package/dist/src/context/indent.d.ts.map +0 -1
  223. package/dist/src/context/indent.js +0 -8
  224. package/dist/src/context/indent.js.map +0 -1
  225. package/dist/test/rendering/linebreaks.test.d.ts +0 -2
  226. package/dist/test/rendering/linebreaks.test.d.ts.map +0 -1
  227. package/src/context/indent.ts +0 -17
  228. package/test/rendering/linebreaks.test.tsx +0 -72
package/src/render.ts CHANGED
@@ -1,15 +1,20 @@
1
1
  import { isRef } from "@vue/reactivity";
2
- import { Indent, IndentState } from "./components/Indent.js";
2
+ import { Doc, doc } from "prettier";
3
+ import prettier from "prettier/doc.js";
3
4
  import { useContext } from "./context.js";
4
- import { IndentContext } from "./context/indent.js";
5
5
  import { SourceFileContext } from "./context/source-file.js";
6
6
  import {
7
7
  Child,
8
8
  Children,
9
9
  Context,
10
+ CustomContext,
10
11
  effect,
11
12
  getContext,
13
+ getElementCache,
14
+ IntrinsicElement,
12
15
  isComponentCreator,
16
+ isCustomContext,
17
+ isIntrinsicElement,
13
18
  popStack,
14
19
  printRenderStack,
15
20
  pushStack,
@@ -17,138 +22,106 @@ import {
17
22
  untrack,
18
23
  } from "./jsx-runtime.js";
19
24
  import { isRefkey } from "./refkey.js";
25
+ const {
26
+ builders: {
27
+ align,
28
+ breakParent,
29
+ dedent,
30
+ dedentToRoot,
31
+ fill,
32
+ group,
33
+ hardline,
34
+ indent,
35
+ line,
36
+ lineSuffix,
37
+ lineSuffixBoundary,
38
+ literalline,
39
+ markAsRoot,
40
+ softline,
41
+ ifBreak,
42
+ },
43
+ } = prettier;
20
44
 
21
45
  /**
22
- * The component tree is constructed as the result of transforming JSX with
23
- * `@alloy-js/babel-preset`. Elements in the component tree (represented by the type
24
- * Children) are three distinct types of things:
46
+ * Turning components into source text involves three different trees produced
47
+ * sequentially:
25
48
  *
26
- * 1. Primitive data types, which are either literal JSX or substitutions
27
- * 2. Components, which are created via `createComponent`
28
- * 3. Memos, which are created via `memo`, and represent substitutions like
29
- * property accesses and function calls that might be reactive.
49
+ * 1. Component tree, built by the nesting of components
50
+ * 2. Rendered text tree, produced by *rendering* the component tree
51
+ * 3. Document tree, produced by *printing* the rendered text tree
30
52
  *
31
- * This tree is then compiled into a render tree, which is a normalized form of
32
- * the component tree. The render tree is constructed by traversing the
33
- * component tree, invoking components, wrapping memos, doing whitespace
34
- * normalization, and other activities. There are four types of nodes in the
35
- * render tree.
53
+ * Finally, the document tree is converted to text via `prettier`. Let's look at
54
+ * each of these trees and the conversions in detail.
36
55
  *
37
- * 1. Strings, which are either literal JSX or substitutions. Other primitive
38
- * types are either converted to the empty string or stringified as
39
- * appropriate.
40
- * 2. Components, which are possibly wrapped if they are indented.
41
- * 3. Memos, which are wrapped in a reactive effect which updates its render
42
- * tree nodes when its value changes.
43
- * 4. Arrays of these things.
56
+ * # Component tree
44
57
  *
45
- * The render tree is whitespace normalized and indentation preserving. When the
46
- * component increases the literal indent level and then embeds a component,
47
- * memo, or array, the contents of that substitution are indented appropriately.
48
- * This is accomplished by wrapping those substitutions in an indent component.
58
+ * The component tree is built by JSX or STC templates. The nodes in this tree
59
+ * are defined by the type `Child` and are one of the following
49
60
  *
50
- * So the high level process for rendering while normalizing whitespace is as
51
- * follows:
61
+ * ## Primitive values
52
62
  *
53
- * 1. For an array of elements in the render tree (which may be a component or
54
- * array of elements):
55
- * 1. Normalize all primitive values other than strings to strings.
56
- * Recursively normalize nested array elements.
57
- * 2. Use the first text node to determine the literal indent level of the
58
- * children. Remove all preceding whitespace - any indent of the first
59
- * line is provided in the text nodes preceding the reference to this
60
- * component. If the first element is not a literal string, then no
61
- * literal indent is applied, and all indentation within the component
62
- * becomes significant.
63
- * 3. For each child of the component, render it:
64
- * 1. If it is a string, reindent it by splitting on lines and replacing
65
- * the detected literal whitespace with the current indent level,
66
- * skipping the first line. If the string ends with a larger literal
67
- * indent than the detected literal indent, then a subsequent child
68
- * will be indented.
69
- * 2. If it's a component, if the next child should be indented, create an
70
- * Indent component and wrap the component's children in it.
71
- * 3. If it's a function, if the next child should be indented, wrap it in
72
- * an indent component. Any elements processed as a result of executing
73
- * the memo are treated as first elements in a child array are with
74
- * respect to establishing literal indent level and whitespace trimming
75
- * behavior.
76
- * 4. If it's an array, if the next child should be indented, create an
77
- * Indent component and wrap it the array in it.
63
+ * Strings in the tree are placed into the rendered text tree as-is. Numbers are
64
+ * converted to strings. Falsy primitive values and booleans are converted to
65
+ * empty strings (and may cause a line break to be ignored, see below).
78
66
  *
79
- * Let's look at a few examples of each of these phases:
67
+ * ## Nullary functions
80
68
  *
81
- * ## Explicit indentation
69
+ * Nullary functions represent computed or reactive values in the component
70
+ * tree, such as expressions placed into a JSX template with curly brackets.
71
+ * Nullary functions return `Children` which are then recursively rendered.
72
+ * Nullary functions are invoked in an effect which will update the rendered
73
+ * text tree when any reactive dependencies change.
82
74
  *
83
- * ### Input
84
- * ```
85
- * <Indent>
86
- * <Foo />
87
- * <Foo />
88
- * </Indent>
89
- * ```
75
+ * ## Component creators
90
76
  *
91
- * ### Compiled tree
92
- * ```
93
- * [
94
- * createComponent(Indent, {
95
- * get children() {
96
- * return [
97
- * "\n ",
98
- * createComponent(Foo, {}),
99
- * "\n ",
100
- * createComponent(Foo, {}),
101
- * "\n"
102
- * ]
103
- * }
104
- * })
105
- * ]
106
- * ```
77
+ * Component creators are a special kind of nullary function which instantiate
78
+ * components in order to get their children which are then recursively
79
+ * rendered. Component creators have some special rendering behavior, such as
80
+ * tracking the stack of rendered components. Like other nullary functions,
81
+ * component creators are invoked in an effect which will update the rendered
82
+ * text tree when any reactive dependencies change.
107
83
  *
108
- * ### Render tree
109
- * ```
110
- * [ // node for Indent
111
- * [ // node for Context Provider
112
- * " ", // indent from the children of Indent
113
- * [ // component for Foo
114
- * "Foo" // result of calling Foo
115
- * ],
116
- * "\n ", // indent and line break from the children of Ident
117
- * [ "Foo" ] // second foo component
118
- * ]
119
- * ]
120
- * ```
121
- * ### Rendered text
122
- * ```
123
- * FooFoo
124
- * ```
84
+ * ## Refs
125
85
  *
126
- * ## Implicit indentation
86
+ * Refs are wrapped in a nullary function and rendered in an effect which will
87
+ * update the rendered text tree when the ref's value changes. This is
88
+ * essentially a syntactic convenience, allowing JSX templates to contain
89
+ * `\{ someRef \}` instead of `\{ someRef.value \}`.
127
90
  *
128
- * ### Input
129
- * ```
130
- * <>
131
- * base
132
- * <Foo /> <Foo />
133
- * </>
134
- * ```
91
+ * ## Refkey
135
92
  *
136
- * ### Render tree
137
- * ```
138
- * [ // node for top-level fragment
139
- * "base\n ", // contents of fragment, including trailing indent
140
- * [ // node for implicitly created Indent component
141
- * [ // node for its context provider [ "Foo" ], // contents of Foo "\n"
142
- * ]
143
- * ```
144
- * ## Rendered text
145
- * ```
146
- * base
147
- * Foo Foo
148
- * ```
93
+ * Refkeys are replaced with a component creator for the Reference component
94
+ * associated with the current source file. This allows creating references by
95
+ * placing them directly in the component tree e.g. `{ someRefkey }`.
96
+ *
97
+ * ## CustomContext
98
+ *
99
+ * CustomContext is a special kind of component which allows rendering children
100
+ * within a custom reactive context. This enables components to manually manage
101
+ * the lifetime of their reactive contexts.
102
+ *
103
+ * ## IntrinsicElement
104
+ *
105
+ * Various intrinsic elements exist to control formatting. These elements
106
+ * provide Print Hooks that are called during Printing.
107
+ *
108
+ * # Rendered Text Tree
109
+ *
110
+ * This tree is a nested array structure containing the rendered output of all
111
+ * the components in the component tree. This structure is updated reactively
112
+ * when reactive dependencies change. The nodes in this tree are predominantly
113
+ * strings, but can also be Print Hooks.
114
+ *
115
+ * This tree is built by the `renderTree` function.
116
+ *
117
+ * # Document Tree
118
+ *
119
+ * This tree is constructed by calling `printTree` on the rendered text tree.
120
+ * The rendered text tree is walked and the appropriate Prettier builders are
121
+ * called to produce a document tree. The result is then passed to Prettier's
122
+ * `printDocToString` function to produce the final source text.
149
123
  */
150
124
 
151
- //
152
125
  export interface OutputDirectory {
153
126
  kind: "directory";
154
127
  path: string;
@@ -162,23 +135,60 @@ export interface OutputFile {
162
135
  filetype: string;
163
136
  }
164
137
 
165
- const nodesToContext = new WeakMap<RenderTextTree, Context>();
138
+ const nodesToContext = new WeakMap<RenderedTextTree, Context>();
166
139
 
167
- export function getContextForRenderNode(node: RenderTextTree) {
140
+ export function getContextForRenderNode(node: RenderedTextTree) {
168
141
  return nodesToContext.get(node);
169
142
  }
170
- export type RenderStructure = {};
171
143
 
172
- export type RenderTextTree = (string | RenderTextTree)[];
144
+ export const printHookTag = Symbol();
145
+
146
+ export interface PrintHook {
147
+ [printHookTag]: true;
148
+ transform?(tree: RenderedTextTree): RenderedTextTree;
149
+ print?(
150
+ tree: RenderedTextTree,
151
+ print: (subtree: RenderedTextTree) => Doc,
152
+ ): Doc;
153
+ subtree: RenderedTextTree;
154
+ }
173
155
 
174
- function traceRender(phase: string, message: string) {
156
+ export function createRenderTreeHook(
157
+ subtree: RenderedTextTree,
158
+ hooks: Omit<PrintHook, typeof printHookTag | "subtree">,
159
+ ): PrintHook {
160
+ return {
161
+ [printHookTag]: true,
162
+ subtree,
163
+ ...hooks,
164
+ };
165
+ }
166
+
167
+ export function isPrintHook(type: unknown): type is PrintHook {
168
+ return typeof type === "object" && type !== null && printHookTag in type;
169
+ }
170
+
171
+ export type RenderedTextTree = (string | RenderedTextTree | PrintHook)[];
172
+
173
+ function traceRender(phase: string, message: () => string) {
175
174
  return false;
176
- // console.log(`[\x1b[34m${phase}\x1b[0m]: ${message}`);
175
+ //console.log(`[\x1b[34m${phase}\x1b[0m]: ${message()}`);
177
176
  }
178
177
 
179
- export function render(children: Children): OutputDirectory {
178
+ export function render(
179
+ children: Children,
180
+ options?: PrintTreeOptions,
181
+ ): OutputDirectory {
180
182
  const tree = renderTree(children);
181
183
  let rootDirectory: OutputDirectory | undefined = undefined;
184
+
185
+ // when passing Output, the first render tree child is the Output component.
186
+ const rootRenderOptions =
187
+ Array.isArray(tree) ?
188
+ (getContextForRenderNode(tree[0] as RenderedTextTree)?.meta
189
+ ?.printOptions ?? {})
190
+ : {};
191
+
182
192
  collectSourceFiles(undefined, tree);
183
193
 
184
194
  if (!rootDirectory) {
@@ -191,7 +201,7 @@ export function render(children: Children): OutputDirectory {
191
201
 
192
202
  function collectSourceFiles(
193
203
  currentDirectory: OutputDirectory | undefined,
194
- root: RenderTextTree,
204
+ root: RenderedTextTree,
195
205
  ) {
196
206
  if (!Array.isArray(root)) {
197
207
  return;
@@ -222,11 +232,25 @@ export function render(children: Children): OutputDirectory {
222
232
  "Source file doesn't have parent directory. Make sure you have used the Output component.",
223
233
  );
224
234
  }
235
+
225
236
  const sourceFile: OutputFile = {
226
237
  kind: "file",
227
238
  path: context.meta?.sourceFile.path,
228
239
  filetype: context.meta?.sourceFile.filetype,
229
- contents: (root as any).flat(Infinity).join(""),
240
+ contents: printTree(root, {
241
+ printWidth:
242
+ options?.printWidth ??
243
+ context.meta?.printOptions?.printWidth ??
244
+ rootRenderOptions.printWidth,
245
+ tabWidth:
246
+ options?.tabWidth ??
247
+ context.meta?.printOptions?.tabWidth ??
248
+ rootRenderOptions.tabWidth,
249
+ useTabs:
250
+ options?.useTabs ??
251
+ context.meta?.printOptions?.useTabs ??
252
+ rootRenderOptions.useTabs,
253
+ }),
230
254
  };
231
255
 
232
256
  currentDirectory.contents.push(sourceFile);
@@ -236,20 +260,17 @@ export function render(children: Children): OutputDirectory {
236
260
 
237
261
  function recurse(cwd: OutputDirectory | undefined) {
238
262
  for (const child of root) {
239
- collectSourceFiles(cwd, child as RenderTextTree);
263
+ collectSourceFiles(cwd, child as RenderedTextTree);
240
264
  }
241
265
  }
242
266
  }
243
267
  }
244
268
 
245
269
  export function renderTree(children: Children) {
246
- const rootElem: RenderTextTree = [];
247
- const state: RenderState = {
248
- newline: false,
249
- };
270
+ const rootElem: RenderedTextTree = [];
250
271
  try {
251
272
  root(() => {
252
- renderWorker(rootElem, children, state);
273
+ renderWorker(rootElem, children);
253
274
  });
254
275
  } catch (e) {
255
276
  printRenderStack();
@@ -259,92 +280,191 @@ export function renderTree(children: Children) {
259
280
  return rootElem;
260
281
  }
261
282
 
262
- interface RenderState {
263
- newline: boolean;
264
- }
265
-
266
- function renderWorker(
267
- node: RenderTextTree,
268
- children: Children,
269
- state: RenderState,
270
- ) {
271
- traceRender("render", dumpChildren(children));
283
+ function renderWorker(node: RenderedTextTree, children: Children) {
284
+ traceRender("render", () => dumpChildren(children));
272
285
 
273
286
  if (Array.isArray(node)) {
274
287
  nodesToContext.set(node, getContext()!);
275
288
  }
276
289
 
277
- const indent = useContext(IndentContext)!;
278
290
  if (Array.isArray(children)) {
279
- for (const child of children) {
280
- appendChild(node, child, indent, state);
291
+ for (const child of (children as any).flat(Infinity)) {
292
+ appendChild(node, child);
281
293
  }
282
294
  } else {
283
- appendChild(node, children, indent, state);
295
+ appendChild(node, children);
284
296
  }
285
297
  }
286
298
 
287
- function appendChild(
288
- node: RenderTextTree,
289
- rawChild: Child,
290
- indentState: IndentState,
291
- state: RenderState,
292
- ) {
293
- traceRender("appendChild", printChild(rawChild));
299
+ function appendChild(node: RenderedTextTree, rawChild: Child) {
300
+ traceRender("appendChild", () => debugPrintChild(rawChild));
294
301
  const child = normalizeChild(rawChild);
295
302
 
296
303
  if (typeof child === "string") {
297
- if (child.match(/\n\s*$/)) {
298
- state.newline = true;
299
- } else {
300
- state.newline = false;
304
+ node.push(child);
305
+ } else {
306
+ const cache = getElementCache();
307
+ if (cache.has(child as any)) {
308
+ traceRender("appendChild:cached", () => debugPrintChild(child));
309
+ node.push(cache.get(child as any)!);
310
+ return;
301
311
  }
302
- const reindented = reindent(child, indentState.indentString);
303
- traceRender("appendChild:string", JSON.stringify(reindented));
304
- node.push(reindented);
305
- } else if (isComponentCreator(child)) {
306
- root(() => {
307
- traceRender("appendChild:component", printChild(child));
308
- if (child.component === Indent && state.newline) {
309
- node.push(indentState.indent);
312
+ if (isCustomContext(child)) {
313
+ traceRender("appendChild:custom-context", () => debugPrintChild(child));
314
+ child.useCustomContext((children) => {
315
+ const newNode: RenderedTextTree = [];
316
+ renderWorker(newNode, children);
317
+ node.push(newNode);
318
+ cache.set(child, newNode);
319
+ });
320
+ } else if (isIntrinsicElement(child)) {
321
+ // don't need a new context here because intrinsics are never reactive
322
+ traceRender("appendChild:intrinsic-element", () =>
323
+ debugPrintChild(child),
324
+ );
325
+ const newNode: RenderedTextTree = [];
326
+
327
+ function formatHookWithChildren(command: (doc: Doc) => Doc) {
328
+ node.push(
329
+ createRenderTreeHook(newNode, {
330
+ print(tree, print) {
331
+ return command(print(tree));
332
+ },
333
+ }),
334
+ );
335
+ renderWorker(newNode, (child as any).props.children);
310
336
  }
311
- const componentRoot: RenderTextTree = [];
312
- pushStack(child.component, child.props);
313
- renderWorker(componentRoot, untrack(child), state);
314
- popStack();
315
- node.push(componentRoot);
316
- traceRender("appendChild:component-done", printChild(child));
317
- }, child);
318
- } else if (typeof child === "function") {
319
- traceRender("appendChild:memo", child.toString());
320
- const index = node.length;
321
- effect((prev: any) => {
322
- traceRender("memoEffect:run", "");
323
- let res = child();
324
- while (typeof res === "function" && !isComponentCreator(res)) {
325
- res = res();
337
+
338
+ function formatHook(command: Doc) {
339
+ return node.push(
340
+ createRenderTreeHook(newNode, {
341
+ print() {
342
+ return command;
343
+ },
344
+ }),
345
+ );
326
346
  }
327
- const newNodes: RenderTextTree = [];
328
- renderWorker(newNodes, res, state);
329
- //node.splice(index, prev ? prev.length : 0, ...newNodes);
330
- node[index] = newNodes;
331
- return newNodes;
332
- });
333
- traceRender("appendChild:memo-done", "");
334
- } else {
335
- traceRender("appendChild:array", dumpChildren(child));
336
- renderWorker(node, child, state);
337
- traceRender("appendChild:array-done", dumpChildren(child));
347
+
348
+ switch (child.name) {
349
+ case "indent":
350
+ return formatHookWithChildren(indent);
351
+ case "fill":
352
+ return formatHookWithChildren(fill as any);
353
+ case "group":
354
+ node.push(
355
+ createRenderTreeHook(newNode, {
356
+ print(tree, print) {
357
+ return group(print(tree), {
358
+ id: child.props.id,
359
+ shouldBreak: child.props.shouldBreak,
360
+ });
361
+ },
362
+ }),
363
+ );
364
+ renderWorker(newNode, child.props.children);
365
+ return;
366
+ case "line":
367
+ case "br":
368
+ return formatHook(line);
369
+ case "hbr":
370
+ case "hardline":
371
+ return formatHook(hardline);
372
+ case "sbr":
373
+ case "softline":
374
+ return formatHook(softline);
375
+ case "literalline":
376
+ case "lbr":
377
+ return formatHook(literalline);
378
+ case "align":
379
+ node.push(
380
+ createRenderTreeHook(newNode, {
381
+ print(tree, print) {
382
+ return align(
383
+ (child.props as any).width ?? (child.props as any).string!,
384
+ print(tree),
385
+ );
386
+ },
387
+ }),
388
+ );
389
+ renderWorker(newNode, (child as any).props.children);
390
+ return;
391
+ case "lineSuffix":
392
+ return formatHookWithChildren(lineSuffix);
393
+ case "lineSuffixBoundary":
394
+ return formatHook(lineSuffixBoundary);
395
+ case "breakParent":
396
+ return formatHook(breakParent);
397
+ case "dedent":
398
+ return formatHookWithChildren(dedent);
399
+ case "dedentToRoot":
400
+ return formatHookWithChildren(dedentToRoot);
401
+ case "markAsRoot":
402
+ return formatHookWithChildren(markAsRoot);
403
+ case "ifBreak":
404
+ node.push(
405
+ createRenderTreeHook(newNode, {
406
+ print(tree, print) {
407
+ return ifBreak(
408
+ print((tree as RenderedTextTree[])[0]),
409
+ print((tree as RenderedTextTree[])[1]),
410
+ );
411
+ },
412
+ }),
413
+ );
414
+ newNode.push([], []);
415
+ renderWorker(
416
+ newNode[0] as RenderedTextTree[],
417
+ (child as any).props.children,
418
+ );
419
+ renderWorker(
420
+ newNode[1] as RenderedTextTree[],
421
+ (child as any).props.flatContents,
422
+ );
423
+ return;
424
+ default:
425
+ throw new Error("Unknown intrinsic element");
426
+ }
427
+ } else if (isComponentCreator(child)) {
428
+ effect(() => {
429
+ traceRender("appendChild:component", () => debugPrintChild(child));
430
+ const componentRoot: RenderedTextTree = [];
431
+ pushStack(child.component, child.props);
432
+ renderWorker(componentRoot, untrack(child));
433
+ popStack();
434
+ node.push(componentRoot);
435
+ cache.set(child, componentRoot);
436
+ traceRender("appendChild:component-done", () => debugPrintChild(child));
437
+ });
438
+ } else if (typeof child === "function") {
439
+ traceRender("appendChild:memo", () => child.toString());
440
+ const index = node.length;
441
+ effect(() => {
442
+ traceRender("memoEffect:run", () => "");
443
+ let res = child();
444
+ while (typeof res === "function" && !isComponentCreator(res)) {
445
+ res = res();
446
+ }
447
+ const newNodes: RenderedTextTree = [];
448
+ renderWorker(newNodes, res);
449
+ node[index] = newNodes;
450
+ cache.set(child, newNodes);
451
+ return newNodes;
452
+ });
453
+ traceRender("appendChild:memo-done", () => "");
454
+ } else {
455
+ throw new Error("Unexpected child type");
456
+ }
338
457
  }
339
458
  }
340
459
 
341
- function reindent(str: string, indent: string) {
342
- const lines = str.split("\n");
343
- return [lines[0], ...lines.slice(1).map((line) => indent + line)].join("\n");
344
- }
345
- type NormalizedChild = string | (() => Child | Children) | NormalizedChild[];
460
+ type NormalizedChildren = NormalizedChild | NormalizedChildren[];
461
+ type NormalizedChild =
462
+ | string
463
+ | (() => Child | Children)
464
+ | CustomContext
465
+ | IntrinsicElement;
346
466
 
347
- function normalizeChild(child: Child): NormalizedChild {
467
+ function normalizeChild(child: Child): NormalizedChildren {
348
468
  if (Array.isArray(child)) {
349
469
  return child.map(normalizeChild);
350
470
  } else if (typeof child === "string" || typeof child === "function") {
@@ -366,24 +486,86 @@ function normalizeChild(child: Child): NormalizedChild {
366
486
 
367
487
  return sfContext.reference({ refkey: child });
368
488
  };
489
+ } else if (isCustomContext(child)) {
490
+ return child;
491
+ } else if (isIntrinsicElement(child)) {
492
+ return child;
369
493
  } else {
370
494
  return String(child);
371
495
  }
372
496
  }
373
497
 
374
- function dumpChildren(children: Child | Children): string {
498
+ function dumpChildren(children: Children): string {
375
499
  if (Array.isArray(children)) {
376
- return `[ ${children.map(printChild).join(", ")} ]`;
500
+ return `[ ${children.map(debugPrintChild).join(", ")} ]`;
377
501
  }
378
- return printChild(children);
502
+ return debugPrintChild(children);
379
503
  }
380
504
 
381
- function printChild(child: Child): string {
505
+ function debugPrintChild(child: Children): string {
382
506
  if (isComponentCreator(child)) {
383
507
  return "<" + child.component.name + ">";
384
508
  } else if (typeof child === "function") {
385
509
  return "$memo";
510
+ } else if (isRef(child)) {
511
+ return "$ref";
386
512
  } else {
387
513
  return JSON.stringify(child);
388
514
  }
389
515
  }
516
+
517
+ export interface PrintTreeOptions {
518
+ /**
519
+ * The number of characters the printer will wrap on. Defaults to 100
520
+ * characters.
521
+ */
522
+ printWidth?: number;
523
+
524
+ /**
525
+ * Whether to use tabs instead of spaces for indentation. Defaults to false.
526
+ */
527
+ useTabs?: boolean;
528
+
529
+ /**
530
+ * The number of spaces to use for indentation. Defaults to 2 spaces.
531
+ */
532
+ tabWidth?: number;
533
+ }
534
+
535
+ const defaultPrintTreeOptions: PrintTreeOptions = {
536
+ printWidth: 80,
537
+ tabWidth: 2,
538
+ };
539
+
540
+ export function printTree(tree: RenderedTextTree, options?: PrintTreeOptions) {
541
+ options = {
542
+ ...defaultPrintTreeOptions,
543
+ ...Object.fromEntries(
544
+ Object.entries(options ?? {}).filter(([_, v]) => v !== undefined),
545
+ ),
546
+ };
547
+
548
+ const d = printTreeWorker(tree);
549
+ return doc.printer.printDocToString(d, options as doc.printer.Options)
550
+ .formatted;
551
+ }
552
+
553
+ function printTreeWorker(tree: RenderedTextTree): Doc {
554
+ const doc: Doc = [];
555
+ for (const node of tree) {
556
+ if (typeof node === "string") {
557
+ const normalizedNode = node
558
+ .split(/\r?\n/)
559
+ .flatMap((line, index, array) =>
560
+ index < array.length - 1 ? [line, hardline] : [line],
561
+ );
562
+ doc.push(normalizedNode);
563
+ } else if (isPrintHook(node)) {
564
+ doc.push(node.print!(node.subtree, printTreeWorker));
565
+ } else {
566
+ doc.push(printTreeWorker(node));
567
+ }
568
+ }
569
+
570
+ return doc;
571
+ }