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

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