@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
@@ -0,0 +1,344 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
+ import WebSocket from "ws";
3
+ import {
4
+ createMessageCollector,
5
+ filterRenderTreeMessages,
6
+ type DevtoolsMessage,
7
+ } from "../../testing/devtools-utils.js";
8
+ import { For } from "../components/For.jsx";
9
+ import { Output } from "../components/Output.jsx";
10
+ import { Show } from "../components/Show.jsx";
11
+ import {
12
+ enableDevtools,
13
+ resetDevtoolsServerForTests,
14
+ } from "../devtools/devtools-server.js";
15
+ import { ref } from "../reactivity.js";
16
+ import { renderAsync } from "../render.js";
17
+ import { flushJobsAsync } from "../scheduler.js";
18
+
19
+ /**
20
+ * Build a snapshot of active render tree nodes from a stream of messages.
21
+ * Returns a Map of nodeId → { parentId, kind, name, value }.
22
+ */
23
+ function buildRenderTreeSnapshot(messages: DevtoolsMessage[]) {
24
+ const active = new Map<
25
+ number,
26
+ { parentId: number | null; kind: string; name?: string; value?: string }
27
+ >();
28
+
29
+ for (const msg of messages) {
30
+ if (msg.type === "render:node_added") {
31
+ active.set(msg.id as number, {
32
+ parentId: msg.parent_id as number | null,
33
+ kind: msg.kind as string,
34
+ name: msg.name as string | undefined,
35
+ value: msg.value as string | undefined,
36
+ });
37
+ } else if (msg.type === "render:node_removed") {
38
+ active.delete(msg.id as number);
39
+ } else if (msg.type === "render:reset") {
40
+ active.clear();
41
+ }
42
+ }
43
+
44
+ return active;
45
+ }
46
+
47
+ /**
48
+ * Find orphaned nodes: nodes whose parentId references a node not in the active set.
49
+ */
50
+ function findOrphans(
51
+ active: Map<
52
+ number,
53
+ { parentId: number | null; kind: string; name?: string; value?: string }
54
+ >,
55
+ ) {
56
+ const orphans: Array<{
57
+ id: number;
58
+ parentId: number;
59
+ kind: string;
60
+ name?: string;
61
+ value?: string;
62
+ }> = [];
63
+
64
+ for (const [id, node] of active) {
65
+ if (node.parentId !== null && !active.has(node.parentId)) {
66
+ orphans.push({ id, ...node, parentId: node.parentId! });
67
+ }
68
+ }
69
+
70
+ return orphans;
71
+ }
72
+
73
+ describe("render tree node orphans", () => {
74
+ let socket: WebSocket | undefined;
75
+
76
+ beforeEach(async () => {
77
+ const server = await enableDevtools({ port: 0 });
78
+ socket = new WebSocket(`ws://127.0.0.1:${server.port}`);
79
+ await new Promise<void>((resolve, reject) => {
80
+ socket?.once("open", resolve);
81
+ socket?.once("error", reject);
82
+ });
83
+ });
84
+
85
+ afterEach(async () => {
86
+ if (socket) {
87
+ socket.close();
88
+ socket = undefined;
89
+ }
90
+ await resetDevtoolsServerForTests();
91
+ });
92
+
93
+ it("no orphans after removing items from For component", async () => {
94
+ const items = ref(["a", "b", "c", "d"]);
95
+ const collector = await createMessageCollector(socket!);
96
+
97
+ function ItemView(props: { item: string }) {
98
+ return (
99
+ <>
100
+ <>{props.item}</>
101
+ <> - suffix</>
102
+ </>
103
+ );
104
+ }
105
+
106
+ await renderAsync(
107
+ <Output>
108
+ <For each={items}>{(item) => <ItemView item={item} />}</For>
109
+ </Output>,
110
+ );
111
+
112
+ const renderMessages = await collector.waitForRender();
113
+ const allMessages = [...filterRenderTreeMessages(renderMessages)];
114
+
115
+ items.value = ["a", "d"];
116
+ await flushJobsAsync();
117
+
118
+ const updateMessages = await collector.waitForFlush();
119
+ allMessages.push(...filterRenderTreeMessages(updateMessages));
120
+
121
+ const active = buildRenderTreeSnapshot(allMessages);
122
+ const orphans = findOrphans(active);
123
+
124
+ expect(orphans).toEqual([]);
125
+ collector.stop();
126
+ });
127
+
128
+ it("no orphans after toggling Show component with nested content", async () => {
129
+ const visible = ref(true);
130
+ const collector = await createMessageCollector(socket!);
131
+
132
+ function Inner() {
133
+ return (
134
+ <>
135
+ <>nested fragment</>
136
+ <>more content</>
137
+ </>
138
+ );
139
+ }
140
+
141
+ await renderAsync(
142
+ <Output>
143
+ <Show when={visible.value}>
144
+ <Inner />
145
+ </Show>
146
+ </Output>,
147
+ );
148
+
149
+ const renderMessages = await collector.waitForRender();
150
+ const allMessages = [...filterRenderTreeMessages(renderMessages)];
151
+
152
+ visible.value = false;
153
+ await flushJobsAsync();
154
+ const offMessages = await collector.waitForFlush();
155
+ allMessages.push(...filterRenderTreeMessages(offMessages));
156
+
157
+ visible.value = true;
158
+ await flushJobsAsync();
159
+ const onMessages = await collector.waitForFlush();
160
+ allMessages.push(...filterRenderTreeMessages(onMessages));
161
+
162
+ const active = buildRenderTreeSnapshot(allMessages);
163
+ const orphans = findOrphans(active);
164
+
165
+ expect(orphans).toEqual([]);
166
+ collector.stop();
167
+ });
168
+
169
+ it("no orphans after replacing all items in For component", async () => {
170
+ const items = ref(["x", "y", "z"]);
171
+ const collector = await createMessageCollector(socket!);
172
+
173
+ function Nested(props: { label: string }) {
174
+ return (
175
+ <>
176
+ <>{props.label}</>
177
+ <>
178
+ <>deep nesting</>
179
+ </>
180
+ </>
181
+ );
182
+ }
183
+
184
+ await renderAsync(
185
+ <Output>
186
+ <For each={items}>{(item) => <Nested label={item} />}</For>
187
+ </Output>,
188
+ );
189
+
190
+ const renderMessages = await collector.waitForRender();
191
+ const allMessages = [...filterRenderTreeMessages(renderMessages)];
192
+
193
+ items.value = ["1", "2"];
194
+ await flushJobsAsync();
195
+ const replaceMessages = await collector.waitForFlush();
196
+ allMessages.push(...filterRenderTreeMessages(replaceMessages));
197
+
198
+ const active = buildRenderTreeSnapshot(allMessages);
199
+ const orphans = findOrphans(active);
200
+
201
+ expect(orphans).toEqual([]);
202
+ collector.stop();
203
+ });
204
+
205
+ it("no orphans when For re-renders and some items are kept (cached subtree path)", async () => {
206
+ // When `For` re-renders after items change, items that haven't changed
207
+ // reuse the same CustomContext object (via mapJoin's slot cache). The
208
+ // element cache detects these as cached elements and takes the `isCached`
209
+ // path in recordSubtreeAdded. If the idToNode reverse mapping isn't
210
+ // restored for cached nodes, their children won't be cleaned up on the
211
+ // next re-render, creating orphans.
212
+ const items = ref(["a", "b", "c"]);
213
+ const collector = await createMessageCollector(socket!);
214
+
215
+ function Item(props: { value: string }) {
216
+ return <>item: {props.value}</>;
217
+ }
218
+
219
+ await renderAsync(
220
+ <Output>
221
+ <For each={items}>{(item) => <Item value={item} />}</For>
222
+ </Output>,
223
+ );
224
+
225
+ const renderMessages = await collector.waitForRender();
226
+ const allMessages = [...filterRenderTreeMessages(renderMessages)];
227
+
228
+ // Change items but keep "a" — its CustomContext stays cached
229
+ items.value = ["a", "x"];
230
+ await flushJobsAsync();
231
+ const update1 = await collector.waitForFlush();
232
+ allMessages.push(...filterRenderTreeMessages(update1));
233
+
234
+ // Change again, keeping "a" — cached subtree re-added
235
+ items.value = ["a", "y", "z"];
236
+ await flushJobsAsync();
237
+ const update2 = await collector.waitForFlush();
238
+ allMessages.push(...filterRenderTreeMessages(update2));
239
+
240
+ const active = buildRenderTreeSnapshot(allMessages);
241
+ const orphans = findOrphans(active);
242
+
243
+ expect(orphans).toEqual([]);
244
+
245
+ // No duplicate text nodes under the same parent
246
+ const textByParent = new Map<number, string[]>();
247
+ for (const [_id, node] of active) {
248
+ if (node.kind === "text" && node.value && node.parentId !== null) {
249
+ const list = textByParent.get(node.parentId) ?? [];
250
+ list.push(node.value);
251
+ textByParent.set(node.parentId, list);
252
+ }
253
+ }
254
+ for (const [parentId, values] of textByParent) {
255
+ const dupes = values.filter((v, i) => values.indexOf(v) !== i);
256
+ expect(dupes, `Duplicate text nodes under parent ${parentId}`).toEqual(
257
+ [],
258
+ );
259
+ }
260
+
261
+ collector.stop();
262
+ });
263
+
264
+ it("no orphans when For with separators re-renders and keeps some items", async () => {
265
+ // mapJoin creates separator/joiner slots between items. These separators
266
+ // contain printHook subtrees that are part of the cached element tree.
267
+ // When items change, the cached subtree is re-added but separator fragments
268
+ // from previous renders must be properly cascade-deleted.
269
+ //
270
+ // This test checks the DB directly for orphaned rows.
271
+ const { DatabaseSync } = await import("node:sqlite");
272
+ const os = await import("node:os");
273
+ const path = await import("node:path");
274
+ const fs = await import("node:fs");
275
+ const { initTrace, closeTrace } = await import("../debug/trace-writer.js");
276
+ const { Block } = await import("../components/Block.jsx");
277
+ const { Indent } = await import("../components/Indent.jsx");
278
+
279
+ const tracePath = path.join(
280
+ os.tmpdir(),
281
+ `alloy-orphan-test-${Date.now()}.db`,
282
+ );
283
+ await initTrace(tracePath);
284
+
285
+ const items = ref(["a", "b", "c", "d"]);
286
+ const collector = await createMessageCollector(socket!);
287
+
288
+ // Nested blocks simulate the deeply nested printHook trees
289
+ // found in real emitters (e.g., flight-instructor)
290
+ function Item(props: { value: string }) {
291
+ return (
292
+ <Block>
293
+ <Indent>
294
+ <>label: {props.value}</>
295
+ </Indent>
296
+ </Block>
297
+ );
298
+ }
299
+
300
+ await renderAsync(
301
+ <Output>
302
+ <For each={items} joiner={"\n"}>
303
+ {(item) => <Item value={item} />}
304
+ </For>
305
+ </Output>,
306
+ );
307
+
308
+ await collector.waitForRender();
309
+
310
+ // Change items, keeping "a" — triggers cached subtree path for "a"
311
+ items.value = ["a", "x", "y"];
312
+ await flushJobsAsync();
313
+ await collector.waitForFlush();
314
+
315
+ // Change again
316
+ items.value = ["a", "z"];
317
+ await flushJobsAsync();
318
+ await collector.waitForFlush();
319
+
320
+ // Third change to exercise multiple cached re-add cycles
321
+ items.value = ["a", "w", "v", "u"];
322
+ await flushJobsAsync();
323
+ await collector.waitForFlush();
324
+
325
+ // Check the DB directly for orphaned nodes
326
+ closeTrace();
327
+ const traceDb = new DatabaseSync(tracePath, { readOnly: true });
328
+ const orphans = traceDb
329
+ .prepare(
330
+ `SELECT n.id, n.parent_id, n.kind, n.name
331
+ FROM render_nodes n
332
+ WHERE n.parent_id IS NOT NULL
333
+ AND NOT EXISTS (SELECT 1 FROM render_nodes p WHERE p.id = n.parent_id)`,
334
+ )
335
+ .all();
336
+
337
+ traceDb.close();
338
+ fs.unlinkSync(tracePath);
339
+
340
+ expect(orphans).toEqual([]);
341
+
342
+ collector.stop();
343
+ });
344
+ });
@@ -0,0 +1,357 @@
1
+ import { afterEach, beforeEach, expect, it } from "vitest";
2
+ import WebSocket from "ws";
3
+ import {
4
+ createMessageCollector,
5
+ filterRenderTreeMessages,
6
+ type DevtoolsMessage,
7
+ } from "../../testing/devtools-utils.js";
8
+ import { For } from "../components/For.jsx";
9
+ import { Output } from "../components/Output.jsx";
10
+ import {
11
+ enableDevtools,
12
+ resetDevtoolsServerForTests,
13
+ } from "../devtools/devtools-server.js";
14
+ import { ref } from "../reactivity.js";
15
+ import { renderAsync } from "../render.js";
16
+ import { flushJobsAsync } from "../scheduler.js";
17
+
18
+ let socket: WebSocket | undefined;
19
+
20
+ beforeEach(async () => {
21
+ const server = await enableDevtools({ port: 0 });
22
+ socket = new WebSocket(`ws://127.0.0.1:${server.port}`);
23
+
24
+ await new Promise<void>((resolve, reject) => {
25
+ socket?.once("open", resolve);
26
+ socket?.once("error", reject);
27
+ });
28
+ });
29
+
30
+ afterEach(async () => {
31
+ if (socket) {
32
+ socket.close();
33
+ socket = undefined;
34
+ }
35
+
36
+ await resetDevtoolsServerForTests();
37
+ });
38
+
39
+ it("emits render:complete on successful render", async () => {
40
+ const collector = await createMessageCollector(socket!);
41
+
42
+ await renderAsync(<Output />);
43
+
44
+ const messages = await collector.waitForRender();
45
+
46
+ expect(messages.at(-1)).toMatchObject({ type: "render:complete" });
47
+ collector.stop();
48
+ });
49
+
50
+ it("emits render:error on render failure", async () => {
51
+ function Boom() {
52
+ throw new Error("Boom");
53
+ }
54
+
55
+ const collector = await createMessageCollector(socket!);
56
+
57
+ await expect(
58
+ renderAsync(
59
+ <Output>
60
+ <Boom />
61
+ </Output>,
62
+ ),
63
+ ).rejects.toThrow("Boom");
64
+
65
+ const messages = await collector.waitForRender();
66
+ const renderMessages = messages.filter((m: DevtoolsMessage) =>
67
+ m.type.startsWith("render:"),
68
+ );
69
+ expect(renderMessages.at(-1)).toMatchObject({
70
+ type: "render:error",
71
+ name: expect.any(String),
72
+ message: expect.any(String),
73
+ });
74
+ collector.stop();
75
+ });
76
+
77
+ it("sends render tree messages during render", async () => {
78
+ function Foo() {
79
+ return (
80
+ <>
81
+ Hello
82
+ <br />
83
+ {() => "World"}
84
+ </>
85
+ );
86
+ }
87
+
88
+ const collector = await createMessageCollector(socket!);
89
+
90
+ await renderAsync(
91
+ <Output>
92
+ <Foo />
93
+ </Output>,
94
+ );
95
+
96
+ const messages = await collector.waitForRender();
97
+ const renderMessages = filterRenderTreeMessages(messages);
98
+ collector.stop();
99
+
100
+ expect(renderMessages[0]).toMatchObject({ type: "render:reset" });
101
+
102
+ const nodeAdded = renderMessages.filter(
103
+ (m) => m.type === "render:node_added",
104
+ );
105
+ expect(nodeAdded[0]).toMatchObject({
106
+ type: "render:node_added",
107
+ parent_id: null,
108
+ });
109
+ expect(nodeAdded[1]).toMatchObject({
110
+ type: "render:node_added",
111
+ name: "Output",
112
+ });
113
+ expect(nodeAdded[2]).toMatchObject({
114
+ type: "render:node_added",
115
+ name: "Context Binder",
116
+ });
117
+ expect(nodeAdded[3]).toMatchObject({
118
+ type: "render:node_added",
119
+ });
120
+ expect(nodeAdded[4]).toMatchObject({
121
+ type: "render:node_added",
122
+ name: expect.stringMatching(/^Context FormatOptions/),
123
+ });
124
+ expect(nodeAdded[5]).toMatchObject({
125
+ type: "render:node_added",
126
+ });
127
+ expect(nodeAdded[6]).toMatchObject({
128
+ type: "render:node_added",
129
+ name: "SourceDirectory",
130
+ });
131
+ expect(nodeAdded[7]).toMatchObject({
132
+ type: "render:node_added",
133
+ name: "Context SourceDirectory",
134
+ });
135
+ expect(nodeAdded[8]).toMatchObject({
136
+ type: "render:node_added",
137
+ });
138
+ expect(nodeAdded[9]).toMatchObject({
139
+ type: "render:node_added",
140
+ name: "Foo",
141
+ });
142
+ expect(nodeAdded[10]).toMatchObject({
143
+ type: "render:node_added",
144
+ value: "Hello",
145
+ });
146
+ expect(nodeAdded[11]).toMatchObject({
147
+ type: "render:node_added",
148
+ name: "br",
149
+ });
150
+ expect(nodeAdded[12]).toMatchObject({
151
+ type: "render:node_added",
152
+ });
153
+ expect(nodeAdded[13]).toMatchObject({
154
+ type: "render:node_added",
155
+ value: "World",
156
+ });
157
+ });
158
+
159
+ it("rerenders when devtools requests rerender", async () => {
160
+ let renderCount = 0;
161
+
162
+ function Display() {
163
+ renderCount += 1;
164
+ return "Hi";
165
+ }
166
+
167
+ const collector = await createMessageCollector(socket!);
168
+
169
+ await renderAsync(
170
+ <Output>
171
+ <Display />
172
+ </Output>,
173
+ );
174
+
175
+ const messages = await collector.waitForRender();
176
+ const renderMessages = filterRenderTreeMessages(messages);
177
+ const displayNode = renderMessages.find(
178
+ (message: DevtoolsMessage) =>
179
+ message.type === "render:node_added" && message.name === "Display",
180
+ );
181
+
182
+ expect(renderCount).toBe(1);
183
+ expect(displayNode?.id).toEqual(expect.any(Number));
184
+
185
+ socket!.send(
186
+ JSON.stringify({ type: "render:rerender", id: displayNode!.id }),
187
+ );
188
+
189
+ await collector.waitForFlush();
190
+
191
+ expect(renderCount).toBe(2);
192
+ collector.stop();
193
+ });
194
+
195
+ it("sends render tree messages during render with For component", async () => {
196
+ const collector = await createMessageCollector(socket!);
197
+ function Display(props: any) {
198
+ return <>item {props.item}</>;
199
+ }
200
+ await renderAsync(
201
+ <Output>
202
+ <For each={["a", "b"]}>{(item) => <Display item={item} />}</For>
203
+ </Output>,
204
+ );
205
+
206
+ const messages = await collector.waitForRender();
207
+ const renderMessages = filterRenderTreeMessages(messages);
208
+ collector.stop();
209
+
210
+ expect(renderMessages[0]).toMatchObject({ type: "render:reset" });
211
+ expect(renderMessages).toEqual(
212
+ expect.arrayContaining([
213
+ expect.objectContaining({
214
+ type: "render:node_added",
215
+ name: "For",
216
+ }),
217
+ expect.objectContaining({
218
+ type: "render:node_added",
219
+ value: "a",
220
+ }),
221
+ expect.objectContaining({
222
+ type: "render:node_added",
223
+ value: "b",
224
+ }),
225
+ ]),
226
+ );
227
+ });
228
+
229
+ it("emits nodeUpdated during render for context updates", async () => {
230
+ const collector = await createMessageCollector(socket!);
231
+
232
+ function Counter(props: { value: number }) {
233
+ return `Count: ${props.value}`;
234
+ }
235
+
236
+ await renderAsync(
237
+ <Output>
238
+ <Counter value={1} />
239
+ </Output>,
240
+ );
241
+
242
+ const messages = await collector.waitForRender();
243
+ const renderMessages = filterRenderTreeMessages(messages);
244
+
245
+ // Context updates during the initial render produce render:node_updated messages
246
+ const nodeUpdated = renderMessages.filter(
247
+ (m: DevtoolsMessage) => m.type === "render:node_updated",
248
+ );
249
+
250
+ expect(nodeUpdated.length).toBeGreaterThan(0);
251
+ expect(nodeUpdated[0]).toMatchObject({
252
+ type: "render:node_updated",
253
+ id: expect.any(Number),
254
+ });
255
+ collector.stop();
256
+ });
257
+
258
+ it("tracks render tree nodes during initial render", async () => {
259
+ const items = ref(["a", "b", "c"]);
260
+ const collector = await createMessageCollector(socket!);
261
+
262
+ await renderAsync(
263
+ <Output>
264
+ <For each={items}>{(item) => <>{item}</>}</For>
265
+ </Output>,
266
+ );
267
+
268
+ const messages = await collector.waitForRender();
269
+ const renderMessages = filterRenderTreeMessages(messages);
270
+
271
+ // The initial render should include node_added messages for all items
272
+ const nodeAdded = renderMessages.filter(
273
+ (m: DevtoolsMessage) => m.type === "render:node_added",
274
+ );
275
+ expect(nodeAdded.length).toBeGreaterThan(0);
276
+
277
+ // Verify specific items appear in the tree
278
+ const itemValues = nodeAdded
279
+ .filter(
280
+ (m: DevtoolsMessage) =>
281
+ m.value === "a" || m.value === "b" || m.value === "c",
282
+ )
283
+ .map((m: DevtoolsMessage) => m.value);
284
+ expect(itemValues).toContain("a");
285
+ expect(itemValues).toContain("b");
286
+ expect(itemValues).toContain("c");
287
+ collector.stop();
288
+ });
289
+
290
+ it("emits proper events when items are added/removed in For component", async () => {
291
+ const items = ref(["a", "b"]);
292
+ const collector = await createMessageCollector(socket!);
293
+
294
+ function Display(props: any) {
295
+ return <>item {props.item}</>;
296
+ }
297
+ await renderAsync(
298
+ <Output>
299
+ <For each={items}>{(item) => <Display item={item} />}</For>
300
+ </Output>,
301
+ );
302
+
303
+ const originalMessages = await collector.waitForRender();
304
+
305
+ // Track all nodes that are currently in the tree
306
+ const activeNodes = new Map<
307
+ number,
308
+ { parentId: number | null; kind: string; name?: string }
309
+ >();
310
+
311
+ function processMessages(messages: any[]) {
312
+ for (const msg of messages) {
313
+ if (msg.type === "render:node_added") {
314
+ const nodeId = msg.id;
315
+ const parentId = msg.parent_id;
316
+
317
+ // Root node has null parent, otherwise parent must exist
318
+ if (parentId !== null && !activeNodes.has(parentId)) {
319
+ throw new Error(
320
+ `Node ${nodeId} (${msg.kind}${msg.name ? `: ${msg.name}` : ""}) ` +
321
+ `added with parent ${parentId} but parent is not in active nodes. ` +
322
+ `Active nodes: ${[...activeNodes.keys()].join(", ")}`,
323
+ );
324
+ }
325
+
326
+ activeNodes.set(nodeId, {
327
+ parentId,
328
+ kind: msg.kind,
329
+ name: msg.name,
330
+ });
331
+ } else if (msg.type === "render:node_removed") {
332
+ const nodeId = msg.id;
333
+ if (!activeNodes.has(nodeId)) {
334
+ throw new Error(
335
+ `Node ${nodeId} removed but was not in active nodes. ` +
336
+ `Active nodes: ${[...activeNodes.keys()].join(", ")}`,
337
+ );
338
+ }
339
+ activeNodes.delete(nodeId);
340
+ }
341
+ }
342
+ }
343
+
344
+ // Process initial render
345
+ processMessages(filterRenderTreeMessages(originalMessages));
346
+
347
+ // Mutate the list
348
+ items.value.push("c");
349
+ items.value.unshift("0");
350
+ await flushJobsAsync();
351
+
352
+ const updateMessages = await collector.waitForFlush();
353
+ const updateRenderMessages = filterRenderTreeMessages(updateMessages);
354
+
355
+ // Process update - this will throw if parent invariant is violated
356
+ processMessages(updateRenderMessages);
357
+ });