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