@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,700 @@
1
+ import { createComponent as _$createComponent } from "@alloy-js/core/jsx-runtime";
2
+ /**
3
+ * Tests that verify the WebSocket message format produced by the server
4
+ * matches what the devtools frontend expects. Each test renders something,
5
+ * collects the raw JSON messages over the WebSocket, and checks the shape.
6
+ *
7
+ * Tests subscribe to specific channels to validate the subscription mechanism
8
+ * and avoid filtering noise from unrelated channels.
9
+ */
10
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
11
+ import WebSocket from "ws";
12
+ import { createMessageCollector } from "../../testing/devtools-utils.js";
13
+ import { Declaration } from "../components/Declaration.js";
14
+ import { For } from "../components/For.js";
15
+ import { Output } from "../components/Output.js";
16
+ import { Scope } from "../components/Scope.js";
17
+ import { SourceFile } from "../components/SourceFile.js";
18
+ import { enableDevtools, resetDevtoolsServerForTests } from "../devtools/devtools-server.js";
19
+ import { effect, ref } from "../reactivity.js";
20
+ import { renderAsync } from "../render.js";
21
+ import { flushJobsAsync } from "../scheduler.js";
22
+ import { debug } from "./index.js";
23
+ let socket;
24
+ let port;
25
+ beforeEach(async () => {
26
+ debug.effect.reset();
27
+ const server = await enableDevtools({
28
+ port: 0
29
+ });
30
+ port = server.port;
31
+ socket = new WebSocket(`ws://127.0.0.1:${port}`);
32
+ await new Promise((resolve, reject) => {
33
+ socket?.once("open", resolve);
34
+ socket?.once("error", reject);
35
+ });
36
+ });
37
+ afterEach(async () => {
38
+ if (socket) {
39
+ socket.close();
40
+ socket = undefined;
41
+ }
42
+ await resetDevtoolsServerForTests();
43
+ debug.effect.reset();
44
+ });
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Subscriptions
48
+ // ---------------------------------------------------------------------------
49
+
50
+ describe("subscriptions", () => {
51
+ it("only receives messages for subscribed channels", async () => {
52
+ // Subscribe to render only — should NOT see effects, refs, etc.
53
+ const collector = await createMessageCollector(socket, ["render"]);
54
+ await renderAsync(_$createComponent(Output, {
55
+ children: "hello"
56
+ }));
57
+ const messages = await collector.waitForRender();
58
+ collector.stop();
59
+ const types = new Set(messages.map(m => m.type));
60
+ // Should have render messages
61
+ expect(types.has("render:node_added")).toBe(true);
62
+ // Should NOT have effect/ref/symbol/scope/edge messages
63
+ expect(types.has("effect:added")).toBe(false);
64
+ expect(types.has("ref:added")).toBe(false);
65
+ expect(types.has("symbol:added")).toBe(false);
66
+ expect(types.has("scope:added")).toBe(false);
67
+ });
68
+ it("receives messages from multiple subscribed channels", async () => {
69
+ const collector = await createMessageCollector(socket, ["render", "effects", "refs"]);
70
+ await renderAsync(_$createComponent(Output, {
71
+ children: "hello"
72
+ }));
73
+ const messages = await collector.waitForRender();
74
+ collector.stop();
75
+ const types = new Set(messages.map(m => m.type));
76
+ expect(types.has("render:node_added")).toBe(true);
77
+ expect(types.has("effect:added")).toBe(true);
78
+ expect(types.has("ref:added")).toBe(true);
79
+ // Not subscribed to these
80
+ expect(types.has("symbol:added")).toBe(false);
81
+ expect(types.has("scope:added")).toBe(false);
82
+ });
83
+ it("lifecycle signals are received regardless of subscription", async () => {
84
+ // Subscribe to nothing useful — lifecycle signals bypass subscriptions
85
+ const collector = await createMessageCollector(socket, ["diagnostics"]);
86
+ await renderAsync(_$createComponent(Output, {}));
87
+ const messages = await collector.waitForRender();
88
+ collector.stop();
89
+ expect(messages.some(m => m.type === "render:complete")).toBe(true);
90
+ });
91
+ });
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // render channel
95
+ // ---------------------------------------------------------------------------
96
+
97
+ describe("render:node_added", () => {
98
+ it("root node has null parent_id and kind 'root'", async () => {
99
+ const collector = await createMessageCollector(socket, ["render"]);
100
+ await renderAsync(_$createComponent(Output, {}));
101
+ const messages = await collector.waitForRender();
102
+ collector.stop();
103
+ const root = messages.find(m => m.type === "render:node_added" && m.kind === "root");
104
+ expect(root).toMatchObject({
105
+ type: "render:node_added",
106
+ id: expect.any(Number),
107
+ parent_id: null,
108
+ kind: "root",
109
+ seq: expect.any(Number)
110
+ });
111
+ });
112
+ it("component node has numeric id, parent_id, and name", async () => {
113
+ const collector = await createMessageCollector(socket, ["render"]);
114
+ await renderAsync(_$createComponent(Output, {}));
115
+ const messages = await collector.waitForRender();
116
+ collector.stop();
117
+ const output = messages.find(m => m.type === "render:node_added" && m.name === "Output");
118
+ expect(output).toMatchObject({
119
+ type: "render:node_added",
120
+ id: expect.any(Number),
121
+ parent_id: expect.any(Number),
122
+ kind: "component",
123
+ name: "Output"
124
+ });
125
+ });
126
+ it("text node has string value", async () => {
127
+ const collector = await createMessageCollector(socket, ["render"]);
128
+ await renderAsync(_$createComponent(Output, {
129
+ children: "hello"
130
+ }));
131
+ const messages = await collector.waitForRender();
132
+ collector.stop();
133
+ const text = messages.find(m => m.type === "render:node_added" && m.kind === "text");
134
+ expect(text).toBeDefined();
135
+ expect(text.value).toBe("hello");
136
+ expect(text.parent_id).toEqual(expect.any(Number));
137
+ });
138
+ it("non-text node has null value (not undefined)", async () => {
139
+ const collector = await createMessageCollector(socket, ["render"]);
140
+ await renderAsync(_$createComponent(Output, {
141
+ children: "hello"
142
+ }));
143
+ const messages = await collector.waitForRender();
144
+ collector.stop();
145
+ const component = messages.find(m => m.type === "render:node_added" && m.kind === "component");
146
+ expect(component.value).toBeNull();
147
+ });
148
+ });
149
+ describe("render:node_removed", () => {
150
+ it("is emitted when a reactive component removes children", async () => {
151
+ const show = ref(true);
152
+ const collector = await createMessageCollector(socket, ["render"]);
153
+ function Conditional() {
154
+ return () => show.value ? "visible" : "";
155
+ }
156
+ await renderAsync(_$createComponent(Output, {
157
+ get children() {
158
+ return _$createComponent(Conditional, {});
159
+ }
160
+ }));
161
+ const renderMessages = await collector.waitForRender();
162
+
163
+ // Verify the text node "visible" was added
164
+ const textAdded = renderMessages.find(m => m.type === "render:node_added" && m.value === "visible");
165
+ expect(textAdded).toBeDefined();
166
+
167
+ // Toggle visibility to trigger removal
168
+ show.value = false;
169
+ await flushJobsAsync();
170
+ const flushMessages = await collector.waitForFlush();
171
+ collector.stop();
172
+ const removed = flushMessages.filter(m => m.type === "render:node_removed");
173
+ expect(removed.length).toBeGreaterThan(0);
174
+ expect(removed[0]).toMatchObject({
175
+ type: "render:node_removed",
176
+ id: expect.any(Number)
177
+ });
178
+ });
179
+ });
180
+ describe("render:node_updated", () => {
181
+ it("emits node_added during reactive list growth", async () => {
182
+ const items = ref(["a"]);
183
+ const collector = await createMessageCollector(socket, ["render"]);
184
+ await renderAsync(_$createComponent(Output, {
185
+ get children() {
186
+ return _$createComponent(For, {
187
+ each: items,
188
+ children: item => [item]
189
+ });
190
+ }
191
+ }));
192
+ await collector.waitForRender();
193
+
194
+ // Add an item to trigger reactive update
195
+ items.value = [...items.value, "b"];
196
+ await flushJobsAsync();
197
+ const flushMessages = await collector.waitForFlush();
198
+ collector.stop();
199
+
200
+ // Reactive updates produce new node_added messages for new content
201
+ const added = flushMessages.filter(m => m.type === "render:node_added");
202
+ expect(added.length).toBeGreaterThan(0);
203
+ // Should contain the new "b" text node
204
+ const bNode = added.find(m => m.value === "b");
205
+ expect(bNode).toBeDefined();
206
+ });
207
+ });
208
+ describe("render:reset", () => {
209
+ it("is sent before node_added messages", async () => {
210
+ const collector = await createMessageCollector(socket, ["render"]);
211
+ await renderAsync(_$createComponent(Output, {}));
212
+ const messages = await collector.waitForRender();
213
+ collector.stop();
214
+ const renderMessages = messages.filter(m => m.type === "render:reset" || m.type === "render:node_added" || m.type === "render:complete");
215
+ expect(renderMessages[0]).toMatchObject({
216
+ type: "render:reset"
217
+ });
218
+ });
219
+ });
220
+ describe("render:complete", () => {
221
+ it("is sent after all node_added messages", async () => {
222
+ const collector = await createMessageCollector(socket, ["render"]);
223
+ await renderAsync(_$createComponent(Output, {}));
224
+ const messages = await collector.waitForRender();
225
+ collector.stop();
226
+ const renderMessages = messages.filter(m => m.type === "render:reset" || m.type === "render:node_added" || m.type === "render:complete");
227
+ const last = renderMessages[renderMessages.length - 1];
228
+ expect(last).toMatchObject({
229
+ type: "render:complete"
230
+ });
231
+ });
232
+ });
233
+
234
+ // ---------------------------------------------------------------------------
235
+ // effects channel
236
+ // ---------------------------------------------------------------------------
237
+
238
+ describe("effect:added", () => {
239
+ it("has id, effect_type, and seq", async () => {
240
+ const collector = await createMessageCollector(socket, ["effects"]);
241
+ await renderAsync(_$createComponent(Output, {
242
+ children: "hello"
243
+ }));
244
+ const messages = await collector.waitForRender();
245
+ collector.stop();
246
+ const effects = messages.filter(m => m.type === "effect:added");
247
+ expect(effects.length).toBeGreaterThan(0);
248
+ const e = effects[0];
249
+ expect(e).toMatchObject({
250
+ type: "effect:added",
251
+ id: expect.any(Number),
252
+ seq: expect.any(Number)
253
+ });
254
+ expect("effect_type" in e).toBe(true);
255
+ });
256
+ });
257
+ describe("effect:updated", () => {
258
+ it("is emitted when an effect's component is set", async () => {
259
+ const collector = await createMessageCollector(socket, ["effects"]);
260
+ await renderAsync(_$createComponent(Output, {
261
+ children: "hello"
262
+ }));
263
+ const messages = await collector.waitForRender();
264
+ collector.stop();
265
+ const updated = messages.filter(m => m.type === "effect:updated");
266
+ // Effects get updated with component info during render
267
+ if (updated.length > 0) {
268
+ expect(updated[0]).toMatchObject({
269
+ type: "effect:updated"
270
+ });
271
+ }
272
+ });
273
+ });
274
+
275
+ // ---------------------------------------------------------------------------
276
+ // refs channel
277
+ // ---------------------------------------------------------------------------
278
+
279
+ describe("ref:added", () => {
280
+ it("has id, kind, and seq", async () => {
281
+ const collector = await createMessageCollector(socket, ["refs"]);
282
+ await renderAsync(_$createComponent(Output, {
283
+ children: "hello"
284
+ }));
285
+ const messages = await collector.waitForRender();
286
+ collector.stop();
287
+ const refs = messages.filter(m => m.type === "ref:added");
288
+ expect(refs.length).toBeGreaterThan(0);
289
+ const r = refs[0];
290
+ expect(r).toMatchObject({
291
+ type: "ref:added",
292
+ id: expect.any(Number),
293
+ seq: expect.any(Number)
294
+ });
295
+ // kind is nullable
296
+ expect("kind" in r).toBe(true);
297
+ });
298
+ });
299
+
300
+ // ---------------------------------------------------------------------------
301
+ // edges channel
302
+ // ---------------------------------------------------------------------------
303
+
304
+ describe("edge:track and edge:trigger", () => {
305
+ it("emits edge messages with effect_id and ref_id", async () => {
306
+ const collector = await createMessageCollector(socket, ["edges"]);
307
+ await renderAsync(_$createComponent(Output, {
308
+ children: "hello"
309
+ }));
310
+ const messages = await collector.waitForRender();
311
+ collector.stop();
312
+ const edges = messages.filter(m => m.type === "edge:track" || m.type === "edge:trigger");
313
+ expect(edges.length).toBeGreaterThan(0);
314
+ const edge = edges[0];
315
+ expect(edge).toMatchObject({
316
+ seq: expect.any(Number),
317
+ effect_id: expect.any(Number)
318
+ });
319
+ // ref_id is present (may be null for some edge types)
320
+ expect("ref_id" in edge).toBe(true);
321
+ });
322
+ });
323
+
324
+ // ---------------------------------------------------------------------------
325
+ // symbols channel
326
+ // ---------------------------------------------------------------------------
327
+
328
+ describe("symbol:added", () => {
329
+ it("has id, name, boolean flags, and seq", async () => {
330
+ const collector = await createMessageCollector(socket, ["symbols"]);
331
+ await renderAsync(_$createComponent(Output, {
332
+ get children() {
333
+ return _$createComponent(Scope, {
334
+ name: "myScope",
335
+ get children() {
336
+ return _$createComponent(Declaration, {
337
+ name: "Foo",
338
+ children: "foo content"
339
+ });
340
+ }
341
+ });
342
+ }
343
+ }));
344
+ const messages = await collector.waitForRender();
345
+ collector.stop();
346
+ const symbols = messages.filter(m => m.type === "symbol:added");
347
+ expect(symbols.length).toBeGreaterThan(0);
348
+ const sym = symbols.find(m => m.name === "Foo");
349
+ expect(sym).toBeDefined();
350
+ expect(sym).toMatchObject({
351
+ type: "symbol:added",
352
+ id: expect.any(Number),
353
+ name: "Foo",
354
+ seq: expect.any(Number)
355
+ });
356
+ // Boolean flags are 0/1 integers
357
+ expect([0, 1]).toContain(sym.is_member);
358
+ expect([0, 1]).toContain(sym.is_transient);
359
+ expect([0, 1]).toContain(sym.is_alias);
360
+ // Nullable fields exist as keys
361
+ expect("scope_id" in sym).toBe(true);
362
+ expect("owner_symbol_id" in sym).toBe(true);
363
+ expect("metadata" in sym).toBe(true);
364
+ });
365
+ });
366
+
367
+ // ---------------------------------------------------------------------------
368
+ // scopes channel
369
+ // ---------------------------------------------------------------------------
370
+
371
+ describe("scope:added", () => {
372
+ it("has id, name, is_member_scope, and seq", async () => {
373
+ const collector = await createMessageCollector(socket, ["scopes"]);
374
+ await renderAsync(_$createComponent(Output, {
375
+ get children() {
376
+ return _$createComponent(Scope, {
377
+ name: "TestScope",
378
+ children: "content"
379
+ });
380
+ }
381
+ }));
382
+ const messages = await collector.waitForRender();
383
+ collector.stop();
384
+ const scopes = messages.filter(m => m.type === "scope:added");
385
+ expect(scopes.length).toBeGreaterThan(0);
386
+ const scope = scopes.find(s => s.name === "TestScope");
387
+ expect(scope).toBeDefined();
388
+ expect(scope).toMatchObject({
389
+ type: "scope:added",
390
+ id: expect.any(Number),
391
+ name: "TestScope",
392
+ seq: expect.any(Number)
393
+ });
394
+ expect([0, 1]).toContain(scope.is_member_scope);
395
+ expect("parent_id" in scope).toBe(true);
396
+ expect("metadata" in scope).toBe(true);
397
+ });
398
+ });
399
+
400
+ // ---------------------------------------------------------------------------
401
+ // files channel
402
+ // ---------------------------------------------------------------------------
403
+
404
+ describe("file:added", () => {
405
+ it("has path, filetype, and seq", async () => {
406
+ const collector = await createMessageCollector(socket, ["files"]);
407
+ await renderAsync(_$createComponent(Output, {
408
+ get children() {
409
+ return _$createComponent(SourceFile, {
410
+ path: "test.ts",
411
+ filetype: "typescript",
412
+ children: "content"
413
+ });
414
+ }
415
+ }));
416
+ const messages = await collector.waitForRender();
417
+ collector.stop();
418
+ const files = messages.filter(m => m.type === "file:added");
419
+ expect(files.length).toBeGreaterThan(0);
420
+ const file = files.find(f => f.path.endsWith("test.ts"));
421
+ expect(file).toBeDefined();
422
+ expect(file).toMatchObject({
423
+ type: "file:added",
424
+ path: expect.stringContaining("test.ts"),
425
+ filetype: "typescript",
426
+ seq: expect.any(Number)
427
+ });
428
+ });
429
+ });
430
+
431
+ // ---------------------------------------------------------------------------
432
+ // directories channel
433
+ // ---------------------------------------------------------------------------
434
+
435
+ describe("directory:added", () => {
436
+ it("has path and seq", async () => {
437
+ const collector = await createMessageCollector(socket, ["directories"]);
438
+ await renderAsync(_$createComponent(Output, {
439
+ children: "hello"
440
+ }));
441
+ const messages = await collector.waitForRender();
442
+ collector.stop();
443
+ const dirs = messages.filter(m => m.type === "directory:added");
444
+ expect(dirs.length).toBeGreaterThan(0);
445
+ expect(dirs[0]).toMatchObject({
446
+ type: "directory:added",
447
+ path: expect.any(String),
448
+ seq: expect.any(Number)
449
+ });
450
+ });
451
+ });
452
+
453
+ // ---------------------------------------------------------------------------
454
+ // scheduler channel
455
+ // ---------------------------------------------------------------------------
456
+
457
+ describe("scheduler:job", () => {
458
+ it("is emitted during synchronous flush", async () => {
459
+ // Scheduler events are only traced during synchronous flushJobs(),
460
+ // not flushJobsAsync(). Subscribe to scheduler + render to see them.
461
+ const collector = await createMessageCollector(socket, ["scheduler", "render"]);
462
+ // A simple render triggers flushJobs internally
463
+ await renderAsync(_$createComponent(Output, {
464
+ children: "hello"
465
+ }));
466
+ const messages = await collector.waitForRender();
467
+ collector.stop();
468
+ const jobs = messages.filter(m => m.type === "scheduler:job");
469
+ // Scheduler events occur during the synchronous flush inside renderAsync
470
+ if (jobs.length > 0) {
471
+ expect(jobs[0]).toMatchObject({
472
+ type: "scheduler:job",
473
+ seq: expect.any(Number)
474
+ });
475
+ expect("event" in jobs[0] || "jobs_run" in jobs[0]).toBe(true);
476
+ }
477
+ });
478
+ });
479
+
480
+ // ---------------------------------------------------------------------------
481
+ // lifecycle channel
482
+ // ---------------------------------------------------------------------------
483
+
484
+ describe("effect:lifecycle", () => {
485
+ it("is emitted for effect lifecycle events", async () => {
486
+ const collector = await createMessageCollector(socket, ["lifecycle"]);
487
+ const r = ref(0);
488
+ effect(() => {
489
+ void r.value;
490
+ });
491
+ await renderAsync(_$createComponent(Output, {
492
+ children: "hello"
493
+ }));
494
+ const messages = await collector.waitForRender();
495
+
496
+ // Trigger a lifecycle event
497
+ r.value = 1;
498
+ await flushJobsAsync();
499
+ const flushMessages = await collector.waitForFlush();
500
+ collector.stop();
501
+ const all = [...messages, ...flushMessages];
502
+ const lifecycle = all.filter(m => m.type === "effect:lifecycle");
503
+ if (lifecycle.length > 0) {
504
+ expect(lifecycle[0]).toMatchObject({
505
+ type: "effect:lifecycle",
506
+ seq: expect.any(Number)
507
+ });
508
+ expect("effect_id" in lifecycle[0]).toBe(true);
509
+ expect("event" in lifecycle[0]).toBe(true);
510
+ }
511
+ });
512
+ });
513
+
514
+ // ---------------------------------------------------------------------------
515
+ // diagnostics channel
516
+ // ---------------------------------------------------------------------------
517
+
518
+ describe("diagnostics:report", () => {
519
+ it("has message, severity, and seq", async () => {
520
+ const {
521
+ insertDiagnostic
522
+ } = await import("./trace-writer.js");
523
+ const collector = await createMessageCollector(socket, ["diagnostics"]);
524
+
525
+ // Insert a diagnostic directly through the trace writer
526
+ insertDiagnostic("test warning", "warning", undefined, undefined, undefined, undefined);
527
+
528
+ // Render to get a completion signal for the collector
529
+ await renderAsync(_$createComponent(Output, {}));
530
+ const messages = await collector.waitForRender();
531
+ collector.stop();
532
+ const diags = messages.filter(m => m.type === "diagnostics:report");
533
+ expect(diags.length).toBeGreaterThan(0);
534
+ expect(diags[0]).toMatchObject({
535
+ type: "diagnostics:report",
536
+ message: "test warning",
537
+ severity: "warning",
538
+ seq: expect.any(Number)
539
+ });
540
+ // Nullable source location fields exist
541
+ expect("source_file" in diags[0]).toBe(true);
542
+ expect("source_line" in diags[0]).toBe(true);
543
+ });
544
+ });
545
+
546
+ // ---------------------------------------------------------------------------
547
+ // errors channel
548
+ // ---------------------------------------------------------------------------
549
+
550
+ describe("render:error", () => {
551
+ it("has name, message, and seq", async () => {
552
+ const {
553
+ insertRenderError
554
+ } = await import("./trace-writer.js");
555
+ const collector = await createMessageCollector(socket, ["errors"]);
556
+
557
+ // Insert an error directly through the trace writer
558
+ insertRenderError("Error", "kaboom", "Error: kaboom\n at ...", undefined);
559
+
560
+ // Render to get a completion signal
561
+ await renderAsync(_$createComponent(Output, {}));
562
+ const messages = await collector.waitForRender();
563
+ collector.stop();
564
+ const errors = messages.filter(m => m.type === "render:error");
565
+ expect(errors.length).toBeGreaterThan(0);
566
+ expect(errors[0]).toMatchObject({
567
+ type: "render:error",
568
+ name: "Error",
569
+ message: "kaboom",
570
+ seq: expect.any(Number)
571
+ });
572
+ expect("stack" in errors[0]).toBe(true);
573
+ expect("component_stack" in errors[0]).toBe(true);
574
+ });
575
+ });
576
+
577
+ // ---------------------------------------------------------------------------
578
+ // flushJobs:complete lifecycle signal
579
+ // ---------------------------------------------------------------------------
580
+
581
+ describe("flushJobs:complete", () => {
582
+ it("is emitted after a reactive flush", async () => {
583
+ const items = ref(["a"]);
584
+ const collector = await createMessageCollector(socket, ["render"]);
585
+ await renderAsync(_$createComponent(Output, {
586
+ get children() {
587
+ return _$createComponent(For, {
588
+ each: items,
589
+ children: item => [item]
590
+ });
591
+ }
592
+ }));
593
+ await collector.waitForRender();
594
+ items.value = ["a", "b"];
595
+ await flushJobsAsync();
596
+ const flushMessages = await collector.waitForFlush();
597
+ collector.stop();
598
+
599
+ // waitForFlush resolves on flushJobs:complete, so if we get here it was received
600
+ expect(flushMessages.some(m => m.type === "flushJobs:complete")).toBe(true);
601
+ });
602
+ });
603
+
604
+ // ---------------------------------------------------------------------------
605
+ // debugger:info (connection handshake)
606
+ // ---------------------------------------------------------------------------
607
+
608
+ describe("debugger:info", () => {
609
+ it("is sent on connection before any other messages", async () => {
610
+ // Close the existing socket so the server accepts a new one
611
+ socket.close();
612
+ await new Promise(resolve => setTimeout(resolve, 100));
613
+ const freshSocket = new WebSocket(`ws://127.0.0.1:${port}`);
614
+ const firstMessage = new Promise((resolve, reject) => {
615
+ const timeout = setTimeout(() => reject(new Error("No debugger:info received")), 3000);
616
+ freshSocket.once("message", data => {
617
+ clearTimeout(timeout);
618
+ resolve(JSON.parse(String(data)));
619
+ });
620
+ });
621
+ await new Promise((resolve, reject) => {
622
+ freshSocket.once("open", resolve);
623
+ freshSocket.once("error", reject);
624
+ });
625
+ const first = await firstMessage;
626
+ freshSocket.close();
627
+ // Replace socket so afterEach doesn't close an already-closed socket
628
+ socket = undefined;
629
+ expect(first).toMatchObject({
630
+ type: "debugger:info",
631
+ version: expect.any(String)
632
+ });
633
+ });
634
+ });
635
+
636
+ // ---------------------------------------------------------------------------
637
+ // null vs undefined contract
638
+ // ---------------------------------------------------------------------------
639
+
640
+ describe("null vs undefined contract", () => {
641
+ it("nullable fields arrive as null (not undefined or missing)", async () => {
642
+ const collector = await createMessageCollector(socket, ["render"]);
643
+ await renderAsync(_$createComponent(Output, {
644
+ children: "hello"
645
+ }));
646
+ const messages = await collector.waitForRender();
647
+ collector.stop();
648
+
649
+ // Root node: parent_id should be explicit null
650
+ const root = messages.find(m => m.type === "render:node_added" && m.kind === "root");
651
+ expect(root).toBeDefined();
652
+ expect(root.parent_id).toBeNull();
653
+ expect("parent_id" in root).toBe(true);
654
+
655
+ // Component node: value should be null, not undefined
656
+ const component = messages.find(m => m.type === "render:node_added" && m.kind === "component");
657
+ expect(component).toBeDefined();
658
+ expect(component.value).toBeNull();
659
+ expect("value" in component).toBe(true);
660
+
661
+ // source_file is nullable
662
+ expect("source_file" in root).toBe(true);
663
+ });
664
+ it("non-null fields have correct types", async () => {
665
+ const collector = await createMessageCollector(socket, ["render"]);
666
+ await renderAsync(_$createComponent(Output, {
667
+ children: "hello"
668
+ }));
669
+ const messages = await collector.waitForRender();
670
+ collector.stop();
671
+ const text = messages.find(m => m.type === "render:node_added" && m.kind === "text");
672
+ expect(text).toBeDefined();
673
+ // value is a string for text nodes
674
+ expect(typeof text.value).toBe("string");
675
+ // id is always a number
676
+ expect(typeof text.id).toBe("number");
677
+ // parent_id is a number for non-root nodes
678
+ expect(typeof text.parent_id).toBe("number");
679
+ });
680
+ });
681
+
682
+ // ---------------------------------------------------------------------------
683
+ // seq ordering
684
+ // ---------------------------------------------------------------------------
685
+
686
+ describe("seq ordering", () => {
687
+ it("messages have monotonically increasing seq", async () => {
688
+ const collector = await createMessageCollector(socket, ["render"]);
689
+ await renderAsync(_$createComponent(Output, {
690
+ children: "hello"
691
+ }));
692
+ const messages = await collector.waitForRender();
693
+ collector.stop();
694
+ const seqs = messages.filter(m => typeof m.seq === "number").map(m => m.seq);
695
+ for (let i = 1; i < seqs.length; i++) {
696
+ expect(seqs[i]).toBeGreaterThan(seqs[i - 1]);
697
+ }
698
+ });
699
+ });
700
+ //# sourceMappingURL=message-format.test.js.map