@alloy-js/core 0.23.0-dev.0 → 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 (269) 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} +78 -84
  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 +29 -0
  123. package/dist/src/render-stack.d.ts.map +1 -0
  124. package/dist/src/render-stack.js +247 -0
  125. package/dist/src/render-stack.js.map +1 -0
  126. package/dist/src/render.d.ts +9 -19
  127. package/dist/src/render.d.ts.map +1 -1
  128. package/dist/src/render.js +363 -153
  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/runtime/component.d.ts +7 -1
  134. package/dist/src/runtime/component.d.ts.map +1 -1
  135. package/dist/src/runtime/component.js +4 -1
  136. package/dist/src/runtime/component.js.map +1 -1
  137. package/dist/src/scheduler.d.ts +3 -0
  138. package/dist/src/scheduler.d.ts.map +1 -1
  139. package/dist/src/scheduler.js +45 -2
  140. package/dist/src/scheduler.js.map +1 -1
  141. package/dist/src/symbols/basic-symbol.d.ts.map +1 -1
  142. package/dist/src/symbols/basic-symbol.js +6 -1
  143. package/dist/src/symbols/basic-symbol.js.map +1 -1
  144. package/dist/src/symbols/decl.d.ts.map +1 -1
  145. package/dist/src/symbols/decl.js +5 -1
  146. package/dist/src/symbols/decl.js.map +1 -1
  147. package/dist/src/symbols/output-scope.d.ts +2 -1
  148. package/dist/src/symbols/output-scope.d.ts.map +1 -1
  149. package/dist/src/symbols/output-scope.js +13 -8
  150. package/dist/src/symbols/output-scope.js.map +1 -1
  151. package/dist/src/symbols/output-symbol.d.ts +1 -0
  152. package/dist/src/symbols/output-symbol.d.ts.map +1 -1
  153. package/dist/src/symbols/output-symbol.js +23 -6
  154. package/dist/src/symbols/output-symbol.js.map +1 -1
  155. package/dist/src/symbols/symbol-flow.d.ts.map +1 -1
  156. package/dist/src/symbols/symbol-flow.js +22 -6
  157. package/dist/src/symbols/symbol-flow.js.map +1 -1
  158. package/dist/src/symbols/symbol-slot.d.ts.map +1 -1
  159. package/dist/src/symbols/symbol-slot.js +15 -0
  160. package/dist/src/symbols/symbol-slot.js.map +1 -1
  161. package/dist/src/symbols/symbol-slot.test.d.ts +2 -0
  162. package/dist/src/symbols/symbol-slot.test.d.ts.map +1 -0
  163. package/dist/src/symbols/symbol-slot.test.js +35 -0
  164. package/dist/src/symbols/symbol-slot.test.js.map +1 -0
  165. package/dist/src/symbols/symbol-table.d.ts.map +1 -1
  166. package/dist/src/symbols/symbol-table.js +6 -5
  167. package/dist/src/symbols/symbol-table.js.map +1 -1
  168. package/dist/src/trace.d.ts +2 -0
  169. package/dist/src/trace.d.ts.map +1 -0
  170. package/dist/src/trace.js +2 -0
  171. package/dist/src/trace.js.map +1 -0
  172. package/dist/src/tracer.d.ts +2 -228
  173. package/dist/src/tracer.d.ts.map +1 -1
  174. package/dist/src/tracer.js +5 -298
  175. package/dist/src/tracer.js.map +1 -1
  176. package/dist/src/utils.d.ts.map +1 -1
  177. package/dist/src/utils.js +5 -0
  178. package/dist/src/utils.js.map +1 -1
  179. package/dist/test/components/append-file.test.d.ts.map +1 -1
  180. package/dist/test/components/append-file.test.js +18 -10
  181. package/dist/test/components/append-file.test.js.map +1 -1
  182. package/dist/test/components/template-file.test.d.ts.map +1 -1
  183. package/dist/test/components/template-file.test.js +6 -4
  184. package/dist/test/components/template-file.test.js.map +1 -1
  185. package/dist/test/rendering/basic.test.js +3 -0
  186. package/dist/test/rendering/basic.test.js.map +1 -1
  187. package/dist/test/rendering/print-render-stack.test.d.ts +2 -0
  188. package/dist/test/rendering/print-render-stack.test.d.ts.map +1 -0
  189. package/dist/test/rendering/print-render-stack.test.js +207 -0
  190. package/dist/test/rendering/print-render-stack.test.js.map +1 -0
  191. package/dist/testing/create-test-wrapper.d.ts +1 -1
  192. package/dist/testing/create-test-wrapper.d.ts.map +1 -1
  193. package/dist/testing/create-test-wrapper.js +1 -1
  194. package/dist/testing/create-test-wrapper.js.map +1 -1
  195. package/dist/testing/devtools-utils.d.ts +26 -0
  196. package/dist/testing/devtools-utils.d.ts.map +1 -0
  197. package/dist/testing/devtools-utils.js +140 -0
  198. package/dist/testing/devtools-utils.js.map +1 -0
  199. package/dist/testing/extend-expect.d.ts.map +1 -1
  200. package/dist/testing/extend-expect.js +63 -1
  201. package/dist/testing/extend-expect.js.map +1 -1
  202. package/dist/testing/render.d.ts +2 -2
  203. package/dist/testing/render.d.ts.map +1 -1
  204. package/dist/testing/render.js +2 -2
  205. package/dist/testing/render.js.map +1 -1
  206. package/dist/tsconfig.tsbuildinfo +1 -1
  207. package/package.json +21 -7
  208. package/scripts/copy-devtools-ui.mjs +26 -0
  209. package/src/binder.ts +71 -16
  210. package/src/components/AppendFile.tsx +14 -9
  211. package/src/components/Block.tsx +1 -1
  212. package/src/components/Declaration.tsx +2 -1
  213. package/src/components/Scope.tsx +4 -1
  214. package/src/components/TemplateFile.tsx +18 -9
  215. package/src/content-slot.tsx +6 -6
  216. package/src/context.ts +15 -4
  217. package/src/{debug.ts → debug/cli.ts} +112 -127
  218. package/src/debug/diagnostics.test.tsx +55 -0
  219. package/src/debug/effects.test.tsx +96 -0
  220. package/src/debug/effects.ts +313 -0
  221. package/src/debug/files.test.tsx +96 -0
  222. package/src/debug/files.ts +40 -0
  223. package/src/debug/index.ts +126 -0
  224. package/src/debug/render.test.tsx +379 -0
  225. package/src/debug/render.ts +639 -0
  226. package/src/debug/serialize.ts +85 -0
  227. package/src/debug/symbols.test.tsx +106 -0
  228. package/src/debug/symbols.ts +230 -0
  229. package/src/debug/trace.ts +312 -0
  230. package/src/devtools/devtools-protocol.ts +312 -0
  231. package/src/devtools/devtools-server.browser.ts +71 -0
  232. package/src/devtools/devtools-server.ts +290 -0
  233. package/src/devtools/devtools-transport.ts +154 -0
  234. package/src/devtools-entry.browser.ts +52 -0
  235. package/src/devtools-entry.ts +54 -0
  236. package/src/diagnostics.ts +141 -0
  237. package/src/index.ts +2 -6
  238. package/src/print-hook.ts +22 -0
  239. package/src/reactive-union-set.ts +71 -41
  240. package/src/reactivity.ts +206 -23
  241. package/src/render-stack.ts +289 -0
  242. package/src/render.ts +464 -212
  243. package/src/resource.ts +28 -19
  244. package/src/runtime/component.ts +11 -0
  245. package/src/scheduler.ts +55 -3
  246. package/src/symbols/basic-symbol.ts +6 -1
  247. package/src/symbols/decl.ts +5 -1
  248. package/src/symbols/output-scope.ts +21 -12
  249. package/src/symbols/output-symbol.ts +33 -12
  250. package/src/symbols/symbol-flow.ts +68 -37
  251. package/src/symbols/symbol-slot.test.tsx +41 -0
  252. package/src/symbols/symbol-slot.tsx +47 -20
  253. package/src/symbols/symbol-table.ts +6 -10
  254. package/src/trace.ts +1 -0
  255. package/src/tracer.ts +13 -242
  256. package/src/utils.tsx +22 -13
  257. package/temp/api.json +1811 -277
  258. package/test/components/append-file.test.tsx +36 -29
  259. package/test/components/template-file.test.tsx +11 -11
  260. package/test/rendering/basic.test.tsx +4 -0
  261. package/test/rendering/print-render-stack.test.tsx +244 -0
  262. package/testing/create-test-wrapper.tsx +1 -1
  263. package/testing/devtools-utils.ts +203 -0
  264. package/testing/extend-expect.ts +89 -0
  265. package/testing/render.ts +2 -2
  266. package/testing/vitest.d.ts +9 -0
  267. package/dist/src/debug.d.ts +0 -15
  268. package/dist/src/debug.d.ts.map +0 -1
  269. package/dist/src/debug.js.map +0 -1
package/src/render.ts CHANGED
@@ -1,9 +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 { shouldDebug } from "./debug.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";
7
25
  import {
8
26
  Context,
9
27
  CustomContext,
@@ -11,22 +29,111 @@ import {
11
29
  getContext,
12
30
  getElementCache,
13
31
  isCustomContext,
32
+ onCleanup,
33
+ ref,
14
34
  root,
15
35
  untrack,
16
36
  } from "./reactivity.js";
17
37
  import { isRefkeyable, toRefkey } from "./refkey.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,
21
- Component,
22
47
  isComponentCreator,
23
48
  isRenderableObject,
24
- Props,
25
49
  RENDERABLE,
26
50
  } from "./runtime/component.js";
27
51
  import { IntrinsicElement, isIntrinsicElement } from "./runtime/intrinsic.js";
28
- import { flushJobs, flushJobsAsync } from "./scheduler.js";
29
- 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
+ }
30
137
 
31
138
  const {
32
139
  builders: {
@@ -152,23 +259,36 @@ export interface ContentOutputFile extends OutputFileBase {
152
259
  export type OutputFile = ContentOutputFile | CopyOutputFile;
153
260
 
154
261
  const nodesToContext = new WeakMap<RenderedTextTree, Context>();
262
+ const diagnosticsByTree = new WeakMap<RenderedTextTree, DiagnosticsCollector>();
155
263
 
156
264
  export function getContextForRenderNode(node: RenderedTextTree) {
157
265
  return nodesToContext.get(node);
158
266
  }
159
267
 
160
- export const printHookTag = Symbol();
268
+ export function getDiagnosticsForTree(tree: RenderedTextTree) {
269
+ return diagnosticsByTree.get(tree)?.getDiagnostics() ?? [];
270
+ }
161
271
 
162
- export interface PrintHook {
163
- [printHookTag]: true;
164
- transform?(tree: RenderedTextTree): RenderedTextTree;
165
- print?(
166
- tree: RenderedTextTree,
167
- print: (subtree: RenderedTextTree) => Doc,
168
- ): Doc;
169
- 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
+ });
170
282
  }
171
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
+
172
292
  export function createRenderTreeHook(
173
293
  subtree: RenderedTextTree,
174
294
  hooks: Omit<PrintHook, typeof printHookTag | "subtree">,
@@ -180,12 +300,6 @@ export function createRenderTreeHook(
180
300
  };
181
301
  }
182
302
 
183
- export function isPrintHook(type: unknown): type is PrintHook {
184
- return typeof type === "object" && type !== null && printHookTag in type;
185
- }
186
-
187
- export type RenderedTextTree = (string | RenderedTextTree | PrintHook)[];
188
-
189
303
  /**
190
304
  * Render a component tree to source directories and files. Will ensure that
191
305
  * all non-async scheduled jobs are completed before returning. If async jobs
@@ -198,7 +312,14 @@ export function render(
198
312
  ): OutputDirectory {
199
313
  const tree = renderTree(children);
200
314
  flushJobs();
201
- 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;
202
323
  }
203
324
 
204
325
  /**
@@ -209,23 +330,16 @@ export async function renderAsync(
209
330
  children: Children,
210
331
  options?: PrintTreeOptions,
211
332
  ): Promise<OutputDirectory> {
333
+ await debug.prepare();
212
334
  const tree = renderTree(children);
213
- return sourceFilesForTreeAsync(tree, options);
214
- }
215
-
216
- /**
217
- * Convert a rendered text tree to source directories and files. Will ensure that
218
- * all scheduled jobs are completed, including async ones.
219
- */
220
- export async function sourceFilesForTreeAsync(
221
- tree: RenderedTextTree,
222
- options?: PrintTreeOptions,
223
- ) {
224
- // if we await here, we ensure all reactive updates are flushed.
225
- // sourceFilesForTree will flush again, but won't find anything, because tree
226
- // printing won't schedule anything.
335
+ // Ensure all reactive updates are flushed before printing.
227
336
  await flushJobsAsync();
228
- return sourceFilesForTree(tree, options);
337
+ const output = sourceFilesForTree(tree, options);
338
+ reportDiagnosticsForTree(tree);
339
+ reportLastRenderError();
340
+ debug.render.complete();
341
+
342
+ return output;
229
343
  }
230
344
 
231
345
  /**
@@ -241,9 +355,12 @@ export function sourceFilesForTree(
241
355
  collectSourceFiles(undefined, tree);
242
356
 
243
357
  if (!rootDirectory) {
244
- throw new Error(
245
- "No root directory found. Make sure you are using the Output component.",
246
- );
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: [] };
247
364
  }
248
365
 
249
366
  return rootDirectory;
@@ -327,25 +444,35 @@ export function sourceFilesForTree(
327
444
  }
328
445
  export function renderTree(children: Children) {
329
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);
330
453
  try {
331
454
  root(() => {
455
+ attachDiagnosticsCollector(diagnostics);
332
456
  renderWorker(rootElem, children);
333
457
  });
334
458
  } catch (e) {
335
- printRenderStack();
459
+ notifyRenderError(e);
460
+ reportLastRenderError();
336
461
  throw e;
337
462
  }
338
463
 
464
+ diagnosticsByTree.set(rootElem, diagnostics);
465
+
339
466
  return rootElem;
340
467
  }
341
468
 
342
469
  function renderWorker(node: RenderedTextTree, children: Children) {
470
+ if (lastRenderError) return;
343
471
  if (!getContext()) {
344
472
  throw new Error(
345
473
  "Cannot render without a context. Make sure you are using the Output component.",
346
474
  );
347
475
  }
348
- trace(TracePhase.render.worker, () => dumpChildren(children));
349
476
 
350
477
  if (Array.isArray(node)) {
351
478
  nodesToContext.set(node, getContext()!);
@@ -354,6 +481,7 @@ function renderWorker(node: RenderedTextTree, children: Children) {
354
481
  if (Array.isArray(children)) {
355
482
  for (const child of (children as any).flat(Infinity)) {
356
483
  appendChild(node, child);
484
+ if (lastRenderError) break;
357
485
  }
358
486
  } else {
359
487
  appendChild(node, children);
@@ -386,6 +514,10 @@ export function notifyContentState() {
386
514
  break;
387
515
  }
388
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
+ }
389
521
  if (current.isEmpty) {
390
522
  current.isEmpty.value = true;
391
523
  }
@@ -421,95 +553,120 @@ export function notifyContentState() {
421
553
  }
422
554
 
423
555
  function appendChild(node: RenderedTextTree, rawChild: Child) {
424
- trace(TracePhase.render.appendChild, () => debugPrintChild(rawChild));
556
+ if (lastRenderError) return;
425
557
  const child = normalizeChild(rawChild);
426
558
 
427
559
  if (typeof child === "string") {
428
560
  if (child !== "") {
429
561
  contentAdded();
562
+ debug.render.appendTextNode(node, node.length, child);
430
563
  }
431
564
  node.push(child);
432
565
  } else {
433
566
  const cache = getElementCache();
434
567
  if (cache.has(child as any)) {
435
- trace(
436
- TracePhase.render.appendChild,
437
- () => "Cached: " + debugPrintChild(child),
438
- );
439
- 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);
440
576
  return;
441
577
  }
442
578
  if (isCustomContext(child)) {
443
- trace(
444
- TracePhase.render.appendChild,
445
- () => "CustomContext: " + debugPrintChild(child),
446
- );
579
+ const newNode: RenderedTextTree = [];
580
+ debug.render.appendCustomContext(node, newNode);
447
581
  child.useCustomContext((children) => {
448
- const newNode: RenderedTextTree = [];
449
582
  renderWorker(newNode, children);
450
583
  node.push(newNode);
451
584
  cache.set(child, newNode);
452
585
  notifyContentState();
586
+ notifyFileUpdateForNode(node);
453
587
  });
454
588
  } else if (isIntrinsicElement(child)) {
455
- trace(
456
- TracePhase.render.appendChild,
457
- () => "IntrinsicElement: " + debugPrintChild(child),
458
- );
459
589
  // don't need a new context here because intrinsics are never reactive
590
+ const intrinsic = child as IntrinsicElement;
460
591
  const newNode: RenderedTextTree = [];
461
592
 
462
593
  function formatHookWithChildren(command: (doc: Doc) => Doc) {
463
- node.push(
464
- createRenderTreeHook(newNode, {
465
- print(tree, print) {
466
- return command(print(tree));
467
- },
468
- }),
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,
469
605
  );
606
+ node.push(hook);
470
607
  renderWorker(newNode, (child as any).props.children);
608
+ notifyFileUpdateForNode(node);
471
609
  }
472
610
 
473
611
  function formatHook(command: Doc) {
474
- return node.push(
475
- createRenderTreeHook(newNode, {
476
- print() {
477
- return command;
478
- },
479
- }),
480
- );
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;
481
620
  }
482
621
 
483
622
  switch (child.name) {
484
623
  case "indent":
485
624
  return formatHookWithChildren(indent);
486
625
  case "indentIfBreak":
487
- node.push(
488
- createRenderTreeHook(newNode, {
626
+ {
627
+ const hook = createRenderTreeHook(newNode, {
489
628
  print(tree, print) {
490
629
  return indentIfBreak(print(tree), {
491
630
  groupId: child.props.groupId,
492
631
  negate: child.props.negate,
493
632
  });
494
633
  },
495
- }),
496
- );
634
+ });
635
+ debug.render.appendPrintHook(
636
+ node,
637
+ node.length,
638
+ hook,
639
+ intrinsic.name,
640
+ newNode,
641
+ );
642
+ node.push(hook);
643
+ }
497
644
  renderWorker(newNode, child.props.children);
645
+ notifyFileUpdateForNode(node);
498
646
  return;
499
647
  case "fill":
500
648
  return formatHookWithChildren(fill as any);
501
649
  case "group":
502
- node.push(
503
- createRenderTreeHook(newNode, {
650
+ {
651
+ const hook = createRenderTreeHook(newNode, {
504
652
  print(tree, print) {
505
653
  return group(print(tree), {
506
654
  id: child.props.id,
507
655
  shouldBreak: child.props.shouldBreak,
508
656
  });
509
657
  },
510
- }),
511
- );
658
+ });
659
+ debug.render.appendPrintHook(
660
+ node,
661
+ node.length,
662
+ hook,
663
+ intrinsic.name,
664
+ newNode,
665
+ );
666
+ node.push(hook);
667
+ }
512
668
  renderWorker(newNode, child.props.children);
669
+ notifyFileUpdateForNode(node);
513
670
  return;
514
671
  case "line":
515
672
  case "br":
@@ -524,17 +681,26 @@ function appendChild(node: RenderedTextTree, rawChild: Child) {
524
681
  case "lbr":
525
682
  return formatHook(literalline);
526
683
  case "align":
527
- node.push(
528
- createRenderTreeHook(newNode, {
684
+ {
685
+ const hook = createRenderTreeHook(newNode, {
529
686
  print(tree, print) {
530
687
  return align(
531
688
  (child.props as any).width ?? (child.props as any).string!,
532
689
  print(tree),
533
690
  );
534
691
  },
535
- }),
536
- );
692
+ });
693
+ debug.render.appendPrintHook(
694
+ node,
695
+ node.length,
696
+ hook,
697
+ intrinsic.name,
698
+ newNode,
699
+ );
700
+ node.push(hook);
701
+ }
537
702
  renderWorker(newNode, (child as any).props.children);
703
+ notifyFileUpdateForNode(node);
538
704
  return;
539
705
  case "lineSuffix":
540
706
  return formatHookWithChildren(lineSuffix);
@@ -549,17 +715,33 @@ function appendChild(node: RenderedTextTree, rawChild: Child) {
549
715
  case "markAsRoot":
550
716
  return formatHookWithChildren(markAsRoot);
551
717
  case "ifBreak":
552
- node.push(
553
- createRenderTreeHook(newNode, {
718
+ {
719
+ const hook = createRenderTreeHook(newNode, {
554
720
  print(tree, print) {
555
721
  return ifBreak(
556
722
  print((tree as RenderedTextTree[])[0]),
557
723
  print((tree as RenderedTextTree[])[1]),
558
724
  );
559
725
  },
560
- }),
561
- );
726
+ });
727
+ debug.render.appendPrintHook(
728
+ node,
729
+ node.length,
730
+ hook,
731
+ intrinsic.name,
732
+ newNode,
733
+ );
734
+ node.push(hook);
735
+ }
562
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
+ );
563
745
  renderWorker(
564
746
  newNode[0] as RenderedTextTree[],
565
747
  (child as any).props.children,
@@ -568,65 +750,206 @@ function appendChild(node: RenderedTextTree, rawChild: Child) {
568
750
  newNode[1] as RenderedTextTree[],
569
751
  (child as any).props.flatContents,
570
752
  );
753
+ notifyFileUpdateForNode(node);
571
754
  return;
572
755
  default:
573
756
  throw new Error("Unknown intrinsic element");
574
757
  }
575
758
  } else if (isComponentCreator(child)) {
759
+ const index = node.length;
760
+ const rerenderToken = ref(0);
761
+ const breakNext = ref(false);
576
762
  // todo: remove this effect (only needed for context, not needed for anything else)
577
- effect(() => {
578
- trace(
579
- TracePhase.render.appendChild,
580
- () => "Component: " + debugPrintChild(child),
581
- );
582
- const context = getContext();
583
- context!.childrenWithContent = 0;
584
- context!.isEmpty ??= ref(true);
585
-
586
- if (context) context.componentOwner = child;
587
- const componentRoot: RenderedTextTree = [];
588
- pushStack(child.component, child.props);
589
- renderWorker(componentRoot, untrack(child));
590
- popStack();
591
- node.push(componentRoot);
592
- cache.set(child, componentRoot);
593
- notifyContentState();
594
- trace(
595
- TracePhase.render.appendChild,
596
- () =>
597
- "Component done: " +
598
- debugPrintChild(child) +
599
- ", empty: " +
600
- context!.isEmpty!.value,
601
- );
602
- });
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
+ );
603
865
  } else if (typeof child === "function") {
604
- trace(TracePhase.render.appendChild, () => "Memo: " + child.toString());
605
866
  const index = node.length;
606
- effect(() => {
607
- trace(TracePhase.render.renderEffect, () => "");
608
- let res = child();
609
- while (typeof res === "function" && !isComponentCreator(res)) {
610
- res = res();
611
- }
612
- const context = getContext();
613
- context!.childrenWithContent = 0;
614
- context!.isEmpty ??= ref(true);
615
-
616
- const newNodes: RenderedTextTree = [];
617
- renderWorker(newNodes, res);
618
- node[index] = newNodes;
619
- cache.set(child, newNodes);
620
-
621
- notifyContentState();
622
- return newNodes;
623
- });
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
+ );
624
911
  } else {
625
912
  throw new Error("Unexpected child type");
626
913
  }
627
914
  }
628
915
  }
629
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
+
630
953
  type NormalizedChildren = NormalizedChild | NormalizedChildren[];
631
954
  type NormalizedChild =
632
955
  | string
@@ -669,31 +992,6 @@ function normalizeChild(child: Child): NormalizedChildren {
669
992
  }
670
993
  }
671
994
 
672
- function dumpChildren(children: Children): string {
673
- if (Array.isArray(children)) {
674
- return `[ ${children.map(debugPrintChild).join(", ")} ]`;
675
- }
676
- return debugPrintChild(children);
677
- }
678
-
679
- function debugPrintChild(child: Children): string {
680
- if (isComponentCreator(child)) {
681
- return "<" + child.component.name + ">";
682
- } else if (typeof child === "function") {
683
- return "$memo";
684
- } else if (isRef(child)) {
685
- return "$ref";
686
- } else if (isIntrinsicElement(child)) {
687
- return `<${child.name}>`;
688
- } else if (isRenderableObject(child)) {
689
- return `CustomChildElement(${JSON.stringify(child)})`;
690
- } else if (isRefkeyable(child)) {
691
- return `refkey`;
692
- } else {
693
- return JSON.stringify(child);
694
- }
695
- }
696
-
697
995
  export interface PrintTreeOptions {
698
996
  /**
699
997
  * The number of characters the printer will wrap on. Defaults to 100
@@ -716,6 +1014,12 @@ export interface PrintTreeOptions {
716
1014
  * @default true
717
1015
  */
718
1016
  insertFinalNewLine?: boolean;
1017
+
1018
+ /**
1019
+ * Skip flushing scheduled jobs before printing.
1020
+ * @default false
1021
+ */
1022
+ noFlush?: boolean;
719
1023
  }
720
1024
 
721
1025
  const defaultPrintTreeOptions: PrintTreeOptions = {
@@ -735,8 +1039,10 @@ export function printTree(tree: RenderedTextTree, options?: PrintTreeOptions) {
735
1039
  ),
736
1040
  };
737
1041
 
738
- // make sure queue is empty
739
- flushJobs();
1042
+ if (!options.noFlush) {
1043
+ // make sure queue is empty
1044
+ flushJobs();
1045
+ }
740
1046
 
741
1047
  const d = printTreeWorker(tree);
742
1048
  const result = doc.printer.printDocToString(
@@ -768,57 +1074,3 @@ function printTreeWorker(tree: RenderedTextTree): Doc {
768
1074
 
769
1075
  return doc;
770
1076
  }
771
- // debugging utilities
772
- const renderStack: {
773
- component: Component<any>;
774
- props: Props;
775
- }[] = [];
776
-
777
- export function pushStack(component: Component<any>, props: Props) {
778
- if (!shouldDebug()) return;
779
- renderStack.push({ component, props });
780
- }
781
-
782
- export function popStack() {
783
- if (!shouldDebug()) return;
784
- renderStack.pop();
785
- }
786
-
787
- export function printRenderStack() {
788
- if (!shouldDebug()) return;
789
-
790
- // eslint-disable-next-line no-console
791
- console.error("Error rendering:");
792
- for (let i = renderStack.length - 1; i >= 0; i--) {
793
- const { component, props } = renderStack[i];
794
- // eslint-disable-next-line no-console
795
- console.error(` at ${component.name}(${inspectProps(props)})`);
796
- }
797
- }
798
-
799
- function inspectProps(props: Props) {
800
- return JSON.stringify(
801
- Object.fromEntries(
802
- Object.entries(props).map(([key, value]) => {
803
- let safeValue;
804
- switch (typeof value) {
805
- case "string":
806
- case "number":
807
- case "boolean":
808
- safeValue = value;
809
- break;
810
- case "undefined":
811
- safeValue = "undefined";
812
- break;
813
- case "object":
814
- safeValue = value ? "{...}" : null;
815
- break;
816
- case "function":
817
- safeValue = "function";
818
- break;
819
- }
820
- return [key, safeValue];
821
- }),
822
- ),
823
- );
824
- }