@alloy-js/core 0.23.0-dev.1 → 0.23.0-dev.8

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 (263) hide show
  1. package/CHANGELOG.md +0 -22
  2. package/dist/devtools/index.html +68 -0
  3. package/dist/src/binder.d.ts +2 -0
  4. package/dist/src/binder.d.ts.map +1 -1
  5. package/dist/src/binder.js +55 -12
  6. package/dist/src/binder.js.map +1 -1
  7. package/dist/src/components/AppendFile.d.ts.map +1 -1
  8. package/dist/src/components/AppendFile.js +14 -3
  9. package/dist/src/components/AppendFile.js.map +1 -1
  10. package/dist/src/components/Block.js +1 -1
  11. package/dist/src/components/Block.js.map +1 -1
  12. package/dist/src/components/Declaration.d.ts.map +1 -1
  13. package/dist/src/components/Declaration.js +2 -1
  14. package/dist/src/components/Declaration.js.map +1 -1
  15. package/dist/src/components/Scope.d.ts.map +1 -1
  16. package/dist/src/components/Scope.js +4 -1
  17. package/dist/src/components/Scope.js.map +1 -1
  18. package/dist/src/components/TemplateFile.d.ts.map +1 -1
  19. package/dist/src/components/TemplateFile.js +18 -3
  20. package/dist/src/components/TemplateFile.js.map +1 -1
  21. package/dist/src/content-slot.d.ts.map +1 -1
  22. package/dist/src/content-slot.js +6 -5
  23. package/dist/src/content-slot.js.map +1 -1
  24. package/dist/src/context.d.ts.map +1 -1
  25. package/dist/src/context.js +8 -1
  26. package/dist/src/context.js.map +1 -1
  27. package/dist/src/debug/cli.d.ts +6 -0
  28. package/dist/src/debug/cli.d.ts.map +1 -0
  29. package/dist/src/{debug.js → debug/cli.js} +79 -82
  30. package/dist/src/debug/cli.js.map +1 -0
  31. package/dist/src/debug/diagnostics.test.d.ts +2 -0
  32. package/dist/src/debug/diagnostics.test.d.ts.map +1 -0
  33. package/dist/src/debug/diagnostics.test.js +45 -0
  34. package/dist/src/debug/diagnostics.test.js.map +1 -0
  35. package/dist/src/debug/effects.d.ts +69 -0
  36. package/dist/src/debug/effects.d.ts.map +1 -0
  37. package/dist/src/debug/effects.js +228 -0
  38. package/dist/src/debug/effects.js.map +1 -0
  39. package/dist/src/debug/effects.test.d.ts +2 -0
  40. package/dist/src/debug/effects.test.d.ts.map +1 -0
  41. package/dist/src/debug/effects.test.js +86 -0
  42. package/dist/src/debug/effects.test.js.map +1 -0
  43. package/dist/src/debug/files.d.ts +14 -0
  44. package/dist/src/debug/files.d.ts.map +1 -0
  45. package/dist/src/debug/files.js +40 -0
  46. package/dist/src/debug/files.js.map +1 -0
  47. package/dist/src/debug/files.test.d.ts +2 -0
  48. package/dist/src/debug/files.test.d.ts.map +1 -0
  49. package/dist/src/debug/files.test.js +89 -0
  50. package/dist/src/debug/files.test.js.map +1 -0
  51. package/dist/src/debug/index.d.ts +60 -0
  52. package/dist/src/debug/index.d.ts.map +1 -0
  53. package/dist/src/debug/index.js +68 -0
  54. package/dist/src/debug/index.js.map +1 -0
  55. package/dist/src/debug/render.d.ts +57 -0
  56. package/dist/src/debug/render.d.ts.map +1 -0
  57. package/dist/src/debug/render.js +519 -0
  58. package/dist/src/debug/render.js.map +1 -0
  59. package/dist/src/debug/render.test.d.ts +2 -0
  60. package/dist/src/debug/render.test.d.ts.map +1 -0
  61. package/dist/src/debug/render.test.js +328 -0
  62. package/dist/src/debug/render.test.js.map +1 -0
  63. package/dist/src/debug/serialize.d.ts +9 -0
  64. package/dist/src/debug/serialize.d.ts.map +1 -0
  65. package/dist/src/debug/serialize.js +70 -0
  66. package/dist/src/debug/serialize.js.map +1 -0
  67. package/dist/src/debug/symbols.d.ts +9 -0
  68. package/dist/src/debug/symbols.d.ts.map +1 -0
  69. package/dist/src/debug/symbols.js +164 -0
  70. package/dist/src/debug/symbols.js.map +1 -0
  71. package/dist/src/debug/symbols.test.d.ts +2 -0
  72. package/dist/src/debug/symbols.test.d.ts.map +1 -0
  73. package/dist/src/debug/symbols.test.js +104 -0
  74. package/dist/src/debug/symbols.test.js.map +1 -0
  75. package/dist/src/debug/trace.d.ts +342 -0
  76. package/dist/src/debug/trace.d.ts.map +1 -0
  77. package/dist/src/debug/trace.js +443 -0
  78. package/dist/src/debug/trace.js.map +1 -0
  79. package/dist/src/devtools/devtools-protocol.d.ts +232 -0
  80. package/dist/src/devtools/devtools-protocol.d.ts.map +1 -0
  81. package/dist/src/devtools/devtools-protocol.js +2 -0
  82. package/dist/src/devtools/devtools-protocol.js.map +1 -0
  83. package/dist/src/devtools/devtools-server.browser.d.ts +28 -0
  84. package/dist/src/devtools/devtools-server.browser.d.ts.map +1 -0
  85. package/dist/src/devtools/devtools-server.browser.js +36 -0
  86. package/dist/src/devtools/devtools-server.browser.js.map +1 -0
  87. package/dist/src/devtools/devtools-server.d.ts +72 -0
  88. package/dist/src/devtools/devtools-server.d.ts.map +1 -0
  89. package/dist/src/devtools/devtools-server.js +256 -0
  90. package/dist/src/devtools/devtools-server.js.map +1 -0
  91. package/dist/src/devtools/devtools-transport.d.ts +23 -0
  92. package/dist/src/devtools/devtools-transport.d.ts.map +1 -0
  93. package/dist/src/devtools/devtools-transport.js +114 -0
  94. package/dist/src/devtools/devtools-transport.js.map +1 -0
  95. package/dist/src/devtools-entry.browser.d.ts +4 -0
  96. package/dist/src/devtools-entry.browser.d.ts.map +1 -0
  97. package/dist/src/devtools-entry.browser.js +2 -0
  98. package/dist/src/devtools-entry.browser.js.map +1 -0
  99. package/dist/src/devtools-entry.d.ts +4 -0
  100. package/dist/src/devtools-entry.d.ts.map +1 -0
  101. package/dist/src/devtools-entry.js +2 -0
  102. package/dist/src/devtools-entry.js.map +1 -0
  103. package/dist/src/diagnostics.d.ts +34 -0
  104. package/dist/src/diagnostics.d.ts.map +1 -0
  105. package/dist/src/diagnostics.js +89 -0
  106. package/dist/src/diagnostics.js.map +1 -0
  107. package/dist/src/index.d.ts +3 -2
  108. package/dist/src/index.d.ts.map +1 -1
  109. package/dist/src/index.js +3 -2
  110. package/dist/src/index.js.map +1 -1
  111. package/dist/src/print-hook.d.ts +14 -0
  112. package/dist/src/print-hook.d.ts.map +1 -0
  113. package/dist/src/print-hook.js +10 -0
  114. package/dist/src/print-hook.js.map +1 -0
  115. package/dist/src/reactive-union-set.d.ts.map +1 -1
  116. package/dist/src/reactive-union-set.js +15 -0
  117. package/dist/src/reactive-union-set.js.map +1 -1
  118. package/dist/src/reactivity.d.ts +17 -3
  119. package/dist/src/reactivity.d.ts.map +1 -1
  120. package/dist/src/reactivity.js +162 -14
  121. package/dist/src/reactivity.js.map +1 -1
  122. package/dist/src/render-stack.d.ts +17 -1
  123. package/dist/src/render-stack.d.ts.map +1 -1
  124. package/dist/src/render-stack.js +57 -1
  125. package/dist/src/render-stack.js.map +1 -1
  126. package/dist/src/render.d.ts +8 -15
  127. package/dist/src/render.d.ts.map +1 -1
  128. package/dist/src/render.js +362 -103
  129. package/dist/src/render.js.map +1 -1
  130. package/dist/src/resource.d.ts.map +1 -1
  131. package/dist/src/resource.js +5 -0
  132. package/dist/src/resource.js.map +1 -1
  133. package/dist/src/scheduler.d.ts +3 -0
  134. package/dist/src/scheduler.d.ts.map +1 -1
  135. package/dist/src/scheduler.js +45 -2
  136. package/dist/src/scheduler.js.map +1 -1
  137. package/dist/src/symbols/basic-symbol.d.ts.map +1 -1
  138. package/dist/src/symbols/basic-symbol.js +6 -1
  139. package/dist/src/symbols/basic-symbol.js.map +1 -1
  140. package/dist/src/symbols/decl.d.ts.map +1 -1
  141. package/dist/src/symbols/decl.js +5 -1
  142. package/dist/src/symbols/decl.js.map +1 -1
  143. package/dist/src/symbols/output-scope.d.ts +2 -1
  144. package/dist/src/symbols/output-scope.d.ts.map +1 -1
  145. package/dist/src/symbols/output-scope.js +13 -8
  146. package/dist/src/symbols/output-scope.js.map +1 -1
  147. package/dist/src/symbols/output-symbol.d.ts +1 -0
  148. package/dist/src/symbols/output-symbol.d.ts.map +1 -1
  149. package/dist/src/symbols/output-symbol.js +23 -6
  150. package/dist/src/symbols/output-symbol.js.map +1 -1
  151. package/dist/src/symbols/symbol-flow.d.ts.map +1 -1
  152. package/dist/src/symbols/symbol-flow.js +22 -6
  153. package/dist/src/symbols/symbol-flow.js.map +1 -1
  154. package/dist/src/symbols/symbol-slot.d.ts.map +1 -1
  155. package/dist/src/symbols/symbol-slot.js +15 -0
  156. package/dist/src/symbols/symbol-slot.js.map +1 -1
  157. package/dist/src/symbols/symbol-slot.test.d.ts +2 -0
  158. package/dist/src/symbols/symbol-slot.test.d.ts.map +1 -0
  159. package/dist/src/symbols/symbol-slot.test.js +35 -0
  160. package/dist/src/symbols/symbol-slot.test.js.map +1 -0
  161. package/dist/src/symbols/symbol-table.d.ts.map +1 -1
  162. package/dist/src/symbols/symbol-table.js +6 -5
  163. package/dist/src/symbols/symbol-table.js.map +1 -1
  164. package/dist/src/trace.d.ts +2 -0
  165. package/dist/src/trace.d.ts.map +1 -0
  166. package/dist/src/trace.js +2 -0
  167. package/dist/src/trace.js.map +1 -0
  168. package/dist/src/tracer.d.ts +2 -228
  169. package/dist/src/tracer.d.ts.map +1 -1
  170. package/dist/src/tracer.js +5 -298
  171. package/dist/src/tracer.js.map +1 -1
  172. package/dist/src/utils.d.ts.map +1 -1
  173. package/dist/src/utils.js +5 -0
  174. package/dist/src/utils.js.map +1 -1
  175. package/dist/test/components/append-file.test.d.ts.map +1 -1
  176. package/dist/test/components/append-file.test.js +18 -10
  177. package/dist/test/components/append-file.test.js.map +1 -1
  178. package/dist/test/components/template-file.test.d.ts.map +1 -1
  179. package/dist/test/components/template-file.test.js +6 -4
  180. package/dist/test/components/template-file.test.js.map +1 -1
  181. package/dist/test/rendering/basic.test.js +3 -0
  182. package/dist/test/rendering/basic.test.js.map +1 -1
  183. package/dist/test/rendering/print-render-stack.test.d.ts.map +1 -1
  184. package/dist/test/rendering/print-render-stack.test.js +91 -98
  185. package/dist/test/rendering/print-render-stack.test.js.map +1 -1
  186. package/dist/testing/create-test-wrapper.d.ts +1 -1
  187. package/dist/testing/create-test-wrapper.d.ts.map +1 -1
  188. package/dist/testing/create-test-wrapper.js +1 -1
  189. package/dist/testing/create-test-wrapper.js.map +1 -1
  190. package/dist/testing/devtools-utils.d.ts +26 -0
  191. package/dist/testing/devtools-utils.d.ts.map +1 -0
  192. package/dist/testing/devtools-utils.js +140 -0
  193. package/dist/testing/devtools-utils.js.map +1 -0
  194. package/dist/testing/extend-expect.d.ts.map +1 -1
  195. package/dist/testing/extend-expect.js +63 -1
  196. package/dist/testing/extend-expect.js.map +1 -1
  197. package/dist/testing/render.d.ts +2 -2
  198. package/dist/testing/render.d.ts.map +1 -1
  199. package/dist/testing/render.js +2 -2
  200. package/dist/testing/render.js.map +1 -1
  201. package/dist/tsconfig.tsbuildinfo +1 -1
  202. package/package.json +21 -7
  203. package/scripts/copy-devtools-ui.mjs +26 -0
  204. package/src/binder.ts +71 -16
  205. package/src/components/AppendFile.tsx +14 -9
  206. package/src/components/Block.tsx +1 -1
  207. package/src/components/Declaration.tsx +2 -1
  208. package/src/components/Scope.tsx +4 -1
  209. package/src/components/TemplateFile.tsx +18 -9
  210. package/src/content-slot.tsx +6 -6
  211. package/src/context.ts +15 -4
  212. package/src/{debug.ts → debug/cli.ts} +114 -125
  213. package/src/debug/diagnostics.test.tsx +55 -0
  214. package/src/debug/effects.test.tsx +96 -0
  215. package/src/debug/effects.ts +313 -0
  216. package/src/debug/files.test.tsx +96 -0
  217. package/src/debug/files.ts +40 -0
  218. package/src/debug/index.ts +126 -0
  219. package/src/debug/render.test.tsx +379 -0
  220. package/src/debug/render.ts +639 -0
  221. package/src/debug/serialize.ts +85 -0
  222. package/src/debug/symbols.test.tsx +106 -0
  223. package/src/debug/symbols.ts +230 -0
  224. package/src/debug/trace.ts +312 -0
  225. package/src/devtools/devtools-protocol.ts +312 -0
  226. package/src/devtools/devtools-server.browser.ts +71 -0
  227. package/src/devtools/devtools-server.ts +290 -0
  228. package/src/devtools/devtools-transport.ts +154 -0
  229. package/src/devtools-entry.browser.ts +52 -0
  230. package/src/devtools-entry.ts +54 -0
  231. package/src/diagnostics.ts +141 -0
  232. package/src/index.ts +2 -6
  233. package/src/print-hook.ts +22 -0
  234. package/src/reactive-union-set.ts +71 -41
  235. package/src/reactivity.ts +206 -23
  236. package/src/render-stack.ts +68 -1
  237. package/src/render.ts +464 -157
  238. package/src/resource.ts +28 -19
  239. package/src/scheduler.ts +55 -3
  240. package/src/symbols/basic-symbol.ts +6 -1
  241. package/src/symbols/decl.ts +5 -1
  242. package/src/symbols/output-scope.ts +21 -12
  243. package/src/symbols/output-symbol.ts +33 -12
  244. package/src/symbols/symbol-flow.ts +68 -37
  245. package/src/symbols/symbol-slot.test.tsx +41 -0
  246. package/src/symbols/symbol-slot.tsx +47 -20
  247. package/src/symbols/symbol-table.ts +6 -10
  248. package/src/trace.ts +1 -0
  249. package/src/tracer.ts +13 -242
  250. package/src/utils.tsx +22 -13
  251. package/temp/api.json +1675 -162
  252. package/test/components/append-file.test.tsx +36 -29
  253. package/test/components/template-file.test.tsx +11 -11
  254. package/test/rendering/basic.test.tsx +4 -0
  255. package/test/rendering/print-render-stack.test.tsx +52 -43
  256. package/testing/create-test-wrapper.tsx +1 -1
  257. package/testing/devtools-utils.ts +203 -0
  258. package/testing/extend-expect.ts +89 -0
  259. package/testing/render.ts +2 -2
  260. package/testing/vitest.d.ts +9 -0
  261. package/dist/src/debug.d.ts +0 -14
  262. package/dist/src/debug.d.ts.map +0 -1
  263. package/dist/src/debug.js.map +0 -1
package/src/render.ts CHANGED
@@ -1,8 +1,27 @@
1
- import { isRef, ref } from "@vue/reactivity";
1
+ import { isRef } 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";
5
5
  import { SourceFileContext } from "./context/source-file.js";
6
+ import {
7
+ debug,
8
+ getRenderNodeId,
9
+ isDevtoolsEnabled,
10
+ type RenderTreeNodeInfo,
11
+ } from "./debug/index.js";
12
+ import { broadcastDevtoolsMessage } from "./devtools/devtools-server.js";
13
+ import {
14
+ attachDiagnosticsCollector,
15
+ DiagnosticsCollector,
16
+ emitDiagnostic,
17
+ reportDiagnostics,
18
+ } from "./diagnostics.js";
19
+ import {
20
+ isPrintHook,
21
+ printHookTag,
22
+ type PrintHook,
23
+ type RenderedTextTree,
24
+ } from "./print-hook.js";
6
25
  import {
7
26
  Context,
8
27
  CustomContext,
@@ -10,11 +29,18 @@ import {
10
29
  getContext,
11
30
  getElementCache,
12
31
  isCustomContext,
32
+ onCleanup,
33
+ ref,
13
34
  root,
14
35
  untrack,
15
36
  } from "./reactivity.js";
16
37
  import { isRefkeyable, toRefkey } from "./refkey.js";
17
- import { popStack, printRenderStack, pushStack } from "./render-stack.js";
38
+ import {
39
+ getRenderStackSnapshot,
40
+ popStack,
41
+ printRenderStack,
42
+ pushStack,
43
+ } from "./render-stack.js";
18
44
  import {
19
45
  Child,
20
46
  Children,
@@ -23,8 +49,91 @@ import {
23
49
  RENDERABLE,
24
50
  } from "./runtime/component.js";
25
51
  import { IntrinsicElement, isIntrinsicElement } from "./runtime/intrinsic.js";
26
- import { flushJobs, flushJobsAsync } from "./scheduler.js";
27
- import { trace, TracePhase } from "./tracer.js";
52
+ import { flushJobs, flushJobsAsync, waitForSignal } from "./scheduler.js";
53
+
54
+ const notifiedErrors = new WeakSet<object>();
55
+ let lastRenderError: {
56
+ error: { name: string; message: string; stack?: string };
57
+ componentStack: Array<{
58
+ name: string;
59
+ props?: Record<string, unknown> | undefined;
60
+ propsSerialized?: string;
61
+ renderNodeId?: number;
62
+ source?: RenderTreeNodeInfo["source"];
63
+ }>;
64
+ } | null = null;
65
+
66
+ function normalizeRenderError(error: unknown): {
67
+ name: string;
68
+ message: string;
69
+ stack?: string;
70
+ } {
71
+ if (error instanceof Error) {
72
+ return {
73
+ name: error.name || error.constructor?.name || "Error",
74
+ message: error.message || "",
75
+ stack: error.stack,
76
+ };
77
+ }
78
+ if (error && typeof error === "object") {
79
+ const anyError = error as {
80
+ name?: string;
81
+ message?: string;
82
+ stack?: string;
83
+ };
84
+ return {
85
+ name: anyError.name || "Error",
86
+ message: anyError.message || String(error),
87
+ stack: anyError.stack,
88
+ };
89
+ }
90
+ return {
91
+ name: "Error",
92
+ message: String(error),
93
+ };
94
+ }
95
+
96
+ function notifyRenderError(error: unknown) {
97
+ if (error && typeof error === "object") {
98
+ if (notifiedErrors.has(error)) return;
99
+ notifiedErrors.add(error);
100
+ }
101
+ if (lastRenderError) return;
102
+
103
+ const { name, message, stack } = normalizeRenderError(error);
104
+ const componentStack = getRenderStackSnapshot().map((entry) => {
105
+ const renderNode = entry.context?.meta?.renderNode as
106
+ | RenderedTextTree
107
+ | undefined;
108
+ const renderNodeId = renderNode ? getRenderNodeId(renderNode) : undefined;
109
+ return {
110
+ name: entry.displayName,
111
+ props: entry.props as Record<string, unknown> | undefined,
112
+ renderNodeId,
113
+ source: entry.source,
114
+ };
115
+ });
116
+
117
+ // Output to console
118
+ printRenderStack(error);
119
+
120
+ // Send to devtools if enabled
121
+ debug.render.error({ name, message, stack }, componentStack);
122
+
123
+ // Store for diagnostics
124
+ lastRenderError = { error: { name, message, stack }, componentStack };
125
+ const lastEntry = componentStack.at(-1);
126
+ emitDiagnostic({
127
+ severity: "error",
128
+ message: `${name}: ${message}`,
129
+ source: lastEntry?.source,
130
+ });
131
+ }
132
+
133
+ function reportLastRenderError() {
134
+ // Error already reported in notifyRenderError via debug.renderError
135
+ lastRenderError = null;
136
+ }
28
137
 
29
138
  const {
30
139
  builders: {
@@ -150,23 +259,36 @@ export interface ContentOutputFile extends OutputFileBase {
150
259
  export type OutputFile = ContentOutputFile | CopyOutputFile;
151
260
 
152
261
  const nodesToContext = new WeakMap<RenderedTextTree, Context>();
262
+ const diagnosticsByTree = new WeakMap<RenderedTextTree, DiagnosticsCollector>();
153
263
 
154
264
  export function getContextForRenderNode(node: RenderedTextTree) {
155
265
  return nodesToContext.get(node);
156
266
  }
157
267
 
158
- export const printHookTag = Symbol();
268
+ export function getDiagnosticsForTree(tree: RenderedTextTree) {
269
+ return diagnosticsByTree.get(tree)?.getDiagnostics() ?? [];
270
+ }
159
271
 
160
- export interface PrintHook {
161
- [printHookTag]: true;
162
- transform?(tree: RenderedTextTree): RenderedTextTree;
163
- print?(
164
- tree: RenderedTextTree,
165
- print: (subtree: RenderedTextTree) => Doc,
166
- ): Doc;
167
- subtree: RenderedTextTree;
272
+ function reportDiagnosticsForTree(tree: RenderedTextTree) {
273
+ const diagnostics = diagnosticsByTree.get(tree);
274
+ if (!diagnostics) return;
275
+ const entries = diagnostics.getDiagnostics();
276
+ if (entries.length === 0) return;
277
+ reportDiagnostics(diagnostics);
278
+ void broadcastDevtoolsMessage({
279
+ type: "diagnostics:report",
280
+ diagnostics: entries,
281
+ });
168
282
  }
169
283
 
284
+ // Re-export from print-hook.ts to maintain backwards compatibility
285
+ export {
286
+ isPrintHook,
287
+ printHookTag,
288
+ type PrintHook,
289
+ type RenderedTextTree,
290
+ } from "./print-hook.js";
291
+
170
292
  export function createRenderTreeHook(
171
293
  subtree: RenderedTextTree,
172
294
  hooks: Omit<PrintHook, typeof printHookTag | "subtree">,
@@ -178,12 +300,6 @@ export function createRenderTreeHook(
178
300
  };
179
301
  }
180
302
 
181
- export function isPrintHook(type: unknown): type is PrintHook {
182
- return typeof type === "object" && type !== null && printHookTag in type;
183
- }
184
-
185
- export type RenderedTextTree = (string | RenderedTextTree | PrintHook)[];
186
-
187
303
  /**
188
304
  * Render a component tree to source directories and files. Will ensure that
189
305
  * all non-async scheduled jobs are completed before returning. If async jobs
@@ -196,7 +312,14 @@ export function render(
196
312
  ): OutputDirectory {
197
313
  const tree = renderTree(children);
198
314
  flushJobs();
199
- return sourceFilesForTree(tree, options);
315
+ const output = sourceFilesForTree(tree, options);
316
+ reportDiagnosticsForTree(tree);
317
+ reportLastRenderError();
318
+ debug.render.complete();
319
+ if (isDevtoolsEnabled()) {
320
+ void waitForSignal();
321
+ }
322
+ return output;
200
323
  }
201
324
 
202
325
  /**
@@ -207,23 +330,16 @@ export async function renderAsync(
207
330
  children: Children,
208
331
  options?: PrintTreeOptions,
209
332
  ): Promise<OutputDirectory> {
333
+ await debug.prepare();
210
334
  const tree = renderTree(children);
211
- return sourceFilesForTreeAsync(tree, options);
212
- }
213
-
214
- /**
215
- * Convert a rendered text tree to source directories and files. Will ensure that
216
- * all scheduled jobs are completed, including async ones.
217
- */
218
- export async function sourceFilesForTreeAsync(
219
- tree: RenderedTextTree,
220
- options?: PrintTreeOptions,
221
- ) {
222
- // if we await here, we ensure all reactive updates are flushed.
223
- // sourceFilesForTree will flush again, but won't find anything, because tree
224
- // printing won't schedule anything.
335
+ // Ensure all reactive updates are flushed before printing.
225
336
  await flushJobsAsync();
226
- return sourceFilesForTree(tree, options);
337
+ const output = sourceFilesForTree(tree, options);
338
+ reportDiagnosticsForTree(tree);
339
+ reportLastRenderError();
340
+ debug.render.complete();
341
+
342
+ return output;
227
343
  }
228
344
 
229
345
  /**
@@ -239,9 +355,12 @@ export function sourceFilesForTree(
239
355
  collectSourceFiles(undefined, tree);
240
356
 
241
357
  if (!rootDirectory) {
242
- throw new Error(
243
- "No root directory found. Make sure you are using the Output component.",
244
- );
358
+ emitDiagnostic({
359
+ severity: "error",
360
+ message:
361
+ "No root directory found. Make sure you are using the output component.",
362
+ });
363
+ return { kind: "directory", path: "", contents: [] };
245
364
  }
246
365
 
247
366
  return rootDirectory;
@@ -325,25 +444,35 @@ export function sourceFilesForTree(
325
444
  }
326
445
  export function renderTree(children: Children) {
327
446
  const rootElem: RenderedTextTree = [];
447
+ const diagnostics = new DiagnosticsCollector();
448
+ lastRenderError = null;
449
+ debug.effect.reset();
450
+ debug.symbols.reset();
451
+ debug.files.reset();
452
+ debug.render.initialize(rootElem);
328
453
  try {
329
454
  root(() => {
455
+ attachDiagnosticsCollector(diagnostics);
330
456
  renderWorker(rootElem, children);
331
457
  });
332
458
  } catch (e) {
333
- printRenderStack();
459
+ notifyRenderError(e);
460
+ reportLastRenderError();
334
461
  throw e;
335
462
  }
336
463
 
464
+ diagnosticsByTree.set(rootElem, diagnostics);
465
+
337
466
  return rootElem;
338
467
  }
339
468
 
340
469
  function renderWorker(node: RenderedTextTree, children: Children) {
470
+ if (lastRenderError) return;
341
471
  if (!getContext()) {
342
472
  throw new Error(
343
473
  "Cannot render without a context. Make sure you are using the Output component.",
344
474
  );
345
475
  }
346
- trace(TracePhase.render.worker, () => dumpChildren(children));
347
476
 
348
477
  if (Array.isArray(node)) {
349
478
  nodesToContext.set(node, getContext()!);
@@ -352,6 +481,7 @@ function renderWorker(node: RenderedTextTree, children: Children) {
352
481
  if (Array.isArray(children)) {
353
482
  for (const child of (children as any).flat(Infinity)) {
354
483
  appendChild(node, child);
484
+ if (lastRenderError) break;
355
485
  }
356
486
  } else {
357
487
  appendChild(node, children);
@@ -384,6 +514,10 @@ export function notifyContentState() {
384
514
  break;
385
515
  }
386
516
  current.childrenWithContent--;
517
+ if (current.childrenWithContent > 0) {
518
+ // This isn't the last content so we have no work to do
519
+ break;
520
+ }
387
521
  if (current.isEmpty) {
388
522
  current.isEmpty.value = true;
389
523
  }
@@ -419,95 +553,120 @@ export function notifyContentState() {
419
553
  }
420
554
 
421
555
  function appendChild(node: RenderedTextTree, rawChild: Child) {
422
- trace(TracePhase.render.appendChild, () => debugPrintChild(rawChild));
556
+ if (lastRenderError) return;
423
557
  const child = normalizeChild(rawChild);
424
558
 
425
559
  if (typeof child === "string") {
426
560
  if (child !== "") {
427
561
  contentAdded();
562
+ debug.render.appendTextNode(node, node.length, child);
428
563
  }
429
564
  node.push(child);
430
565
  } else {
431
566
  const cache = getElementCache();
432
567
  if (cache.has(child as any)) {
433
- trace(
434
- TracePhase.render.appendChild,
435
- () => "Cached: " + debugPrintChild(child),
436
- );
437
- node.push(cache.get(child as any)!);
568
+ const cachedNode = cache.get(child as any)!;
569
+ // recordSubtreeAdded detects cached nodes automatically and re-adds their children
570
+ if (isCustomContext(child)) {
571
+ debug.render.appendCustomContext(node, cachedNode);
572
+ } else {
573
+ debug.render.appendFragmentChild(node, cachedNode);
574
+ }
575
+ node.push(cachedNode);
438
576
  return;
439
577
  }
440
578
  if (isCustomContext(child)) {
441
- trace(
442
- TracePhase.render.appendChild,
443
- () => "CustomContext: " + debugPrintChild(child),
444
- );
579
+ const newNode: RenderedTextTree = [];
580
+ debug.render.appendCustomContext(node, newNode);
445
581
  child.useCustomContext((children) => {
446
- const newNode: RenderedTextTree = [];
447
582
  renderWorker(newNode, children);
448
583
  node.push(newNode);
449
584
  cache.set(child, newNode);
450
585
  notifyContentState();
586
+ notifyFileUpdateForNode(node);
451
587
  });
452
588
  } else if (isIntrinsicElement(child)) {
453
- trace(
454
- TracePhase.render.appendChild,
455
- () => "IntrinsicElement: " + debugPrintChild(child),
456
- );
457
589
  // don't need a new context here because intrinsics are never reactive
590
+ const intrinsic = child as IntrinsicElement;
458
591
  const newNode: RenderedTextTree = [];
459
592
 
460
593
  function formatHookWithChildren(command: (doc: Doc) => Doc) {
461
- node.push(
462
- createRenderTreeHook(newNode, {
463
- print(tree, print) {
464
- return command(print(tree));
465
- },
466
- }),
594
+ const hook = createRenderTreeHook(newNode, {
595
+ print(tree, print) {
596
+ return command(print(tree));
597
+ },
598
+ });
599
+ debug.render.appendPrintHook(
600
+ node,
601
+ node.length,
602
+ hook,
603
+ intrinsic.name,
604
+ newNode,
467
605
  );
606
+ node.push(hook);
468
607
  renderWorker(newNode, (child as any).props.children);
608
+ notifyFileUpdateForNode(node);
469
609
  }
470
610
 
471
611
  function formatHook(command: Doc) {
472
- return node.push(
473
- createRenderTreeHook(newNode, {
474
- print() {
475
- return command;
476
- },
477
- }),
478
- );
612
+ const hook = createRenderTreeHook(newNode, {
613
+ print() {
614
+ return command;
615
+ },
616
+ });
617
+ debug.render.appendPrintHook(node, node.length, hook, intrinsic.name);
618
+ node.push(hook);
619
+ return hook;
479
620
  }
480
621
 
481
622
  switch (child.name) {
482
623
  case "indent":
483
624
  return formatHookWithChildren(indent);
484
625
  case "indentIfBreak":
485
- node.push(
486
- createRenderTreeHook(newNode, {
626
+ {
627
+ const hook = createRenderTreeHook(newNode, {
487
628
  print(tree, print) {
488
629
  return indentIfBreak(print(tree), {
489
630
  groupId: child.props.groupId,
490
631
  negate: child.props.negate,
491
632
  });
492
633
  },
493
- }),
494
- );
634
+ });
635
+ debug.render.appendPrintHook(
636
+ node,
637
+ node.length,
638
+ hook,
639
+ intrinsic.name,
640
+ newNode,
641
+ );
642
+ node.push(hook);
643
+ }
495
644
  renderWorker(newNode, child.props.children);
645
+ notifyFileUpdateForNode(node);
496
646
  return;
497
647
  case "fill":
498
648
  return formatHookWithChildren(fill as any);
499
649
  case "group":
500
- node.push(
501
- createRenderTreeHook(newNode, {
650
+ {
651
+ const hook = createRenderTreeHook(newNode, {
502
652
  print(tree, print) {
503
653
  return group(print(tree), {
504
654
  id: child.props.id,
505
655
  shouldBreak: child.props.shouldBreak,
506
656
  });
507
657
  },
508
- }),
509
- );
658
+ });
659
+ debug.render.appendPrintHook(
660
+ node,
661
+ node.length,
662
+ hook,
663
+ intrinsic.name,
664
+ newNode,
665
+ );
666
+ node.push(hook);
667
+ }
510
668
  renderWorker(newNode, child.props.children);
669
+ notifyFileUpdateForNode(node);
511
670
  return;
512
671
  case "line":
513
672
  case "br":
@@ -522,17 +681,26 @@ function appendChild(node: RenderedTextTree, rawChild: Child) {
522
681
  case "lbr":
523
682
  return formatHook(literalline);
524
683
  case "align":
525
- node.push(
526
- createRenderTreeHook(newNode, {
684
+ {
685
+ const hook = createRenderTreeHook(newNode, {
527
686
  print(tree, print) {
528
687
  return align(
529
688
  (child.props as any).width ?? (child.props as any).string!,
530
689
  print(tree),
531
690
  );
532
691
  },
533
- }),
534
- );
692
+ });
693
+ debug.render.appendPrintHook(
694
+ node,
695
+ node.length,
696
+ hook,
697
+ intrinsic.name,
698
+ newNode,
699
+ );
700
+ node.push(hook);
701
+ }
535
702
  renderWorker(newNode, (child as any).props.children);
703
+ notifyFileUpdateForNode(node);
536
704
  return;
537
705
  case "lineSuffix":
538
706
  return formatHookWithChildren(lineSuffix);
@@ -547,17 +715,33 @@ function appendChild(node: RenderedTextTree, rawChild: Child) {
547
715
  case "markAsRoot":
548
716
  return formatHookWithChildren(markAsRoot);
549
717
  case "ifBreak":
550
- node.push(
551
- createRenderTreeHook(newNode, {
718
+ {
719
+ const hook = createRenderTreeHook(newNode, {
552
720
  print(tree, print) {
553
721
  return ifBreak(
554
722
  print((tree as RenderedTextTree[])[0]),
555
723
  print((tree as RenderedTextTree[])[1]),
556
724
  );
557
725
  },
558
- }),
559
- );
726
+ });
727
+ debug.render.appendPrintHook(
728
+ node,
729
+ node.length,
730
+ hook,
731
+ intrinsic.name,
732
+ newNode,
733
+ );
734
+ node.push(hook);
735
+ }
560
736
  newNode.push([], []);
737
+ debug.render.appendFragmentChild(
738
+ newNode,
739
+ newNode[0] as RenderedTextTree,
740
+ );
741
+ debug.render.appendFragmentChild(
742
+ newNode,
743
+ newNode[1] as RenderedTextTree,
744
+ );
561
745
  renderWorker(
562
746
  newNode[0] as RenderedTextTree[],
563
747
  (child as any).props.children,
@@ -566,66 +750,206 @@ function appendChild(node: RenderedTextTree, rawChild: Child) {
566
750
  newNode[1] as RenderedTextTree[],
567
751
  (child as any).props.flatContents,
568
752
  );
753
+ notifyFileUpdateForNode(node);
569
754
  return;
570
755
  default:
571
756
  throw new Error("Unknown intrinsic element");
572
757
  }
573
758
  } else if (isComponentCreator(child)) {
759
+ const index = node.length;
760
+ const rerenderToken = ref(0);
761
+ const breakNext = ref(false);
574
762
  // todo: remove this effect (only needed for context, not needed for anything else)
575
- effect(() => {
576
- trace(
577
- TracePhase.render.appendChild,
578
- () => "Component: " + debugPrintChild(child),
579
- );
580
- const context = getContext();
581
- context!.childrenWithContent = 0;
582
- context!.isEmpty ??= ref(true);
583
-
584
- if (context) context.componentOwner = child;
585
- const componentRoot: RenderedTextTree = [];
586
-
587
- pushStack(child.component, child.props, child.source);
588
- renderWorker(componentRoot, untrack(child));
589
- popStack();
590
- node.push(componentRoot);
591
- cache.set(child, componentRoot);
592
- notifyContentState();
593
- trace(
594
- TracePhase.render.appendChild,
595
- () =>
596
- "Component done: " +
597
- debugPrintChild(child) +
598
- ", empty: " +
599
- context!.isEmpty!.value,
600
- );
601
- });
763
+ effect(
764
+ () => {
765
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
766
+ rerenderToken.value;
767
+ const context = getContext();
768
+ context!.childrenWithContent = 0;
769
+ context!.isEmpty ??= ref(true);
770
+
771
+ if (context) context.componentOwner = child;
772
+ const existing = node[index];
773
+ const componentRoot: RenderedTextTree =
774
+ Array.isArray(existing) ? existing : [];
775
+ context!.meta ??= {};
776
+ context!.meta.renderNode = componentRoot;
777
+ const propsSource = (child.props ?? undefined) as
778
+ | Record<string, unknown>
779
+ | undefined;
780
+ const debugSession = debug.render.beginComponent({
781
+ parent: node,
782
+ index,
783
+ node: componentRoot,
784
+ component: child,
785
+ propsSource,
786
+ source: child.source,
787
+ isExisting: Array.isArray(existing),
788
+ actions: {
789
+ rerender: () => {
790
+ lastRenderError = null;
791
+ rerenderToken.value++;
792
+ },
793
+ rerenderAndBreak: () => {
794
+ lastRenderError = null;
795
+ breakNext.value = true;
796
+ rerenderToken.value++;
797
+ },
798
+ },
799
+ });
800
+ if (Array.isArray(existing)) {
801
+ componentRoot.length = 0;
802
+ }
803
+
804
+ pushStack(child.component, child.props, child.source);
805
+ let renderFailed = false;
806
+ let childResult: Children | undefined;
807
+ try {
808
+ childResult = untrack(() => {
809
+ const shouldBreak = breakNext.value;
810
+ if (shouldBreak) {
811
+ breakNext.value = false;
812
+ // eslint-disable-next-line no-debugger
813
+ debugger;
814
+ }
815
+ return child();
816
+ });
817
+ } catch (error) {
818
+ notifyRenderError(error);
819
+ renderFailed = true;
820
+ throw error;
821
+ }
822
+ try {
823
+ if (context?.meta?.directory) {
824
+ debugSession.recordDirectory(context.meta.directory.path);
825
+ }
826
+ if (context?.meta?.sourceFile) {
827
+ context.meta.renderNode = componentRoot;
828
+ debugSession.recordFile(
829
+ context.meta.sourceFile.path,
830
+ context.meta.sourceFile.filetype,
831
+ );
832
+ context.meta.sourceFileReady = false;
833
+ }
834
+ if (!renderFailed) {
835
+ renderWorker(componentRoot, childResult);
836
+ }
837
+ } finally {
838
+ popStack();
839
+ }
840
+ if (renderFailed) {
841
+ node[index] = componentRoot;
842
+ cache.set(child, componentRoot);
843
+ notifyFileUpdateForNode(node);
844
+ notifyContentState();
845
+ onCleanup(() => debugSession.dispose());
846
+ return;
847
+ }
848
+ if (context?.meta?.sourceFile) {
849
+ context.meta.sourceFileReady = true;
850
+ notifyFileUpdateForNode(componentRoot);
851
+ }
852
+ node[index] = componentRoot;
853
+ cache.set(child, componentRoot);
854
+ notifyContentState();
855
+ onCleanup(() => debugSession.dispose());
856
+ },
857
+ undefined,
858
+ {
859
+ debug: {
860
+ name: `render:${child.component.name || "Anonymous"}`,
861
+ type: "render",
862
+ },
863
+ },
864
+ );
602
865
  } else if (typeof child === "function") {
603
- trace(TracePhase.render.appendChild, () => "Memo: " + child.toString());
604
866
  const index = node.length;
605
- effect(() => {
606
- trace(TracePhase.render.renderEffect, () => "");
607
- let res = child();
608
- while (typeof res === "function" && !isComponentCreator(res)) {
609
- res = res();
610
- }
611
- const context = getContext();
612
- context!.childrenWithContent = 0;
613
- context!.isEmpty ??= ref(true);
614
-
615
- const newNodes: RenderedTextTree = [];
616
- renderWorker(newNodes, res);
617
- node[index] = newNodes;
618
- cache.set(child, newNodes);
619
-
620
- notifyContentState();
621
- return newNodes;
622
- });
867
+ effect(
868
+ () => {
869
+ let res: Child | Children | undefined;
870
+ let renderFailed = false;
871
+ try {
872
+ res = child();
873
+ while (typeof res === "function" && !isComponentCreator(res)) {
874
+ res = res();
875
+ }
876
+ } catch (error) {
877
+ notifyRenderError(error);
878
+ renderFailed = true;
879
+ throw error;
880
+ }
881
+ const context = getContext();
882
+ context!.childrenWithContent = 0;
883
+ context!.isEmpty ??= ref(true);
884
+
885
+ const existing = node[index];
886
+ const memoNode: RenderedTextTree =
887
+ Array.isArray(existing) ? existing : [];
888
+
889
+ debug.render.prepareMemoNode(node, memoNode, Array.isArray(existing));
890
+ if (Array.isArray(existing)) {
891
+ memoNode.length = 0;
892
+ }
893
+
894
+ if (!renderFailed) {
895
+ renderWorker(memoNode, res);
896
+ }
897
+ node[index] = memoNode;
898
+ cache.set(child, memoNode);
899
+ notifyFileUpdateForNode(node);
900
+ notifyContentState();
901
+ return memoNode;
902
+ },
903
+ undefined,
904
+ {
905
+ debug: {
906
+ name: `render:memo:${child.name || "anonymous"}`,
907
+ type: "render",
908
+ },
909
+ },
910
+ );
623
911
  } else {
624
912
  throw new Error("Unexpected child type");
625
913
  }
626
914
  }
627
915
  }
628
916
 
917
+ function findSourceFileContext(node: RenderedTextTree) {
918
+ let context: Context | null | undefined =
919
+ getContextForRenderNode(node) ?? null;
920
+ while (context) {
921
+ if (context.meta?.sourceFile) return context;
922
+ context = context.owner;
923
+ }
924
+ return undefined;
925
+ }
926
+
927
+ function notifyFileUpdateForNode(node: RenderedTextTree) {
928
+ // Only do the expensive printTree when devtools are actually enabled
929
+ if (!isDevtoolsEnabled()) return;
930
+ const context = findSourceFileContext(node);
931
+ if (!context?.meta?.sourceFile) return;
932
+ if (context.meta.sourceFileReady === false) return;
933
+ const sourceFile = context.meta.sourceFile;
934
+ const renderNode: RenderedTextTree =
935
+ (context.meta.renderNode as RenderedTextTree | undefined) ?? node;
936
+ // Pass noFlush here since it flushes jobs and can re-enter rendering
937
+ // during effect setup, triggering premature cleanup.
938
+ const contents = printTree(renderNode, {
939
+ printWidth: context.meta?.printOptions?.printWidth,
940
+ tabWidth: context.meta?.printOptions?.tabWidth,
941
+ useTabs: context.meta?.printOptions?.useTabs,
942
+ insertFinalNewLine: context.meta?.printOptions?.insertFinalNewLine ?? true,
943
+ noFlush: true,
944
+ });
945
+
946
+ debug.files.updated({
947
+ path: sourceFile.path,
948
+ filetype: sourceFile.filetype,
949
+ contents,
950
+ });
951
+ }
952
+
629
953
  type NormalizedChildren = NormalizedChild | NormalizedChildren[];
630
954
  type NormalizedChild =
631
955
  | string
@@ -668,31 +992,6 @@ function normalizeChild(child: Child): NormalizedChildren {
668
992
  }
669
993
  }
670
994
 
671
- function dumpChildren(children: Children): string {
672
- if (Array.isArray(children)) {
673
- return `[ ${children.map(debugPrintChild).join(", ")} ]`;
674
- }
675
- return debugPrintChild(children);
676
- }
677
-
678
- function debugPrintChild(child: Children): string {
679
- if (isComponentCreator(child)) {
680
- return "<" + child.component.name + ">";
681
- } else if (typeof child === "function") {
682
- return "$memo";
683
- } else if (isRef(child)) {
684
- return "$ref";
685
- } else if (isIntrinsicElement(child)) {
686
- return `<${child.name}>`;
687
- } else if (isRenderableObject(child)) {
688
- return `CustomChildElement(${JSON.stringify(child)})`;
689
- } else if (isRefkeyable(child)) {
690
- return `refkey`;
691
- } else {
692
- return JSON.stringify(child);
693
- }
694
- }
695
-
696
995
  export interface PrintTreeOptions {
697
996
  /**
698
997
  * The number of characters the printer will wrap on. Defaults to 100
@@ -715,6 +1014,12 @@ export interface PrintTreeOptions {
715
1014
  * @default true
716
1015
  */
717
1016
  insertFinalNewLine?: boolean;
1017
+
1018
+ /**
1019
+ * Skip flushing scheduled jobs before printing.
1020
+ * @default false
1021
+ */
1022
+ noFlush?: boolean;
718
1023
  }
719
1024
 
720
1025
  const defaultPrintTreeOptions: PrintTreeOptions = {
@@ -734,8 +1039,10 @@ export function printTree(tree: RenderedTextTree, options?: PrintTreeOptions) {
734
1039
  ),
735
1040
  };
736
1041
 
737
- // make sure queue is empty
738
- flushJobs();
1042
+ if (!options.noFlush) {
1043
+ // make sure queue is empty
1044
+ flushJobs();
1045
+ }
739
1046
 
740
1047
  const d = printTreeWorker(tree);
741
1048
  const result = doc.printer.printDocToString(