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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/CHANGELOG.md +0 -22
  2. package/dist/devtools/index.html +68 -0
  3. package/dist/src/binder.d.ts +2 -0
  4. package/dist/src/binder.d.ts.map +1 -1
  5. package/dist/src/binder.js +55 -12
  6. package/dist/src/binder.js.map +1 -1
  7. package/dist/src/components/AppendFile.d.ts.map +1 -1
  8. package/dist/src/components/AppendFile.js +14 -3
  9. package/dist/src/components/AppendFile.js.map +1 -1
  10. package/dist/src/components/Block.js +1 -1
  11. package/dist/src/components/Block.js.map +1 -1
  12. package/dist/src/components/Declaration.d.ts.map +1 -1
  13. package/dist/src/components/Declaration.js +2 -1
  14. package/dist/src/components/Declaration.js.map +1 -1
  15. package/dist/src/components/Scope.d.ts.map +1 -1
  16. package/dist/src/components/Scope.js +4 -1
  17. package/dist/src/components/Scope.js.map +1 -1
  18. package/dist/src/components/TemplateFile.d.ts.map +1 -1
  19. package/dist/src/components/TemplateFile.js +18 -3
  20. package/dist/src/components/TemplateFile.js.map +1 -1
  21. package/dist/src/content-slot.d.ts.map +1 -1
  22. package/dist/src/content-slot.js +6 -5
  23. package/dist/src/content-slot.js.map +1 -1
  24. package/dist/src/context.d.ts.map +1 -1
  25. package/dist/src/context.js +8 -1
  26. package/dist/src/context.js.map +1 -1
  27. package/dist/src/debug/cli.d.ts +6 -0
  28. package/dist/src/debug/cli.d.ts.map +1 -0
  29. package/dist/src/{debug.js → debug/cli.js} +79 -82
  30. package/dist/src/debug/cli.js.map +1 -0
  31. package/dist/src/debug/diagnostics.test.d.ts +2 -0
  32. package/dist/src/debug/diagnostics.test.d.ts.map +1 -0
  33. package/dist/src/debug/diagnostics.test.js +45 -0
  34. package/dist/src/debug/diagnostics.test.js.map +1 -0
  35. package/dist/src/debug/effects.d.ts +69 -0
  36. package/dist/src/debug/effects.d.ts.map +1 -0
  37. package/dist/src/debug/effects.js +228 -0
  38. package/dist/src/debug/effects.js.map +1 -0
  39. package/dist/src/debug/effects.test.d.ts +2 -0
  40. package/dist/src/debug/effects.test.d.ts.map +1 -0
  41. package/dist/src/debug/effects.test.js +86 -0
  42. package/dist/src/debug/effects.test.js.map +1 -0
  43. package/dist/src/debug/files.d.ts +14 -0
  44. package/dist/src/debug/files.d.ts.map +1 -0
  45. package/dist/src/debug/files.js +40 -0
  46. package/dist/src/debug/files.js.map +1 -0
  47. package/dist/src/debug/files.test.d.ts +2 -0
  48. package/dist/src/debug/files.test.d.ts.map +1 -0
  49. package/dist/src/debug/files.test.js +89 -0
  50. package/dist/src/debug/files.test.js.map +1 -0
  51. package/dist/src/debug/index.d.ts +60 -0
  52. package/dist/src/debug/index.d.ts.map +1 -0
  53. package/dist/src/debug/index.js +68 -0
  54. package/dist/src/debug/index.js.map +1 -0
  55. package/dist/src/debug/render.d.ts +57 -0
  56. package/dist/src/debug/render.d.ts.map +1 -0
  57. package/dist/src/debug/render.js +519 -0
  58. package/dist/src/debug/render.js.map +1 -0
  59. package/dist/src/debug/render.test.d.ts +2 -0
  60. package/dist/src/debug/render.test.d.ts.map +1 -0
  61. package/dist/src/debug/render.test.js +328 -0
  62. package/dist/src/debug/render.test.js.map +1 -0
  63. package/dist/src/debug/serialize.d.ts +9 -0
  64. package/dist/src/debug/serialize.d.ts.map +1 -0
  65. package/dist/src/debug/serialize.js +70 -0
  66. package/dist/src/debug/serialize.js.map +1 -0
  67. package/dist/src/debug/symbols.d.ts +9 -0
  68. package/dist/src/debug/symbols.d.ts.map +1 -0
  69. package/dist/src/debug/symbols.js +164 -0
  70. package/dist/src/debug/symbols.js.map +1 -0
  71. package/dist/src/debug/symbols.test.d.ts +2 -0
  72. package/dist/src/debug/symbols.test.d.ts.map +1 -0
  73. package/dist/src/debug/symbols.test.js +104 -0
  74. package/dist/src/debug/symbols.test.js.map +1 -0
  75. package/dist/src/debug/trace.d.ts +342 -0
  76. package/dist/src/debug/trace.d.ts.map +1 -0
  77. package/dist/src/debug/trace.js +443 -0
  78. package/dist/src/debug/trace.js.map +1 -0
  79. package/dist/src/devtools/devtools-protocol.d.ts +232 -0
  80. package/dist/src/devtools/devtools-protocol.d.ts.map +1 -0
  81. package/dist/src/devtools/devtools-protocol.js +2 -0
  82. package/dist/src/devtools/devtools-protocol.js.map +1 -0
  83. package/dist/src/devtools/devtools-server.browser.d.ts +28 -0
  84. package/dist/src/devtools/devtools-server.browser.d.ts.map +1 -0
  85. package/dist/src/devtools/devtools-server.browser.js +36 -0
  86. package/dist/src/devtools/devtools-server.browser.js.map +1 -0
  87. package/dist/src/devtools/devtools-server.d.ts +72 -0
  88. package/dist/src/devtools/devtools-server.d.ts.map +1 -0
  89. package/dist/src/devtools/devtools-server.js +256 -0
  90. package/dist/src/devtools/devtools-server.js.map +1 -0
  91. package/dist/src/devtools/devtools-transport.d.ts +23 -0
  92. package/dist/src/devtools/devtools-transport.d.ts.map +1 -0
  93. package/dist/src/devtools/devtools-transport.js +114 -0
  94. package/dist/src/devtools/devtools-transport.js.map +1 -0
  95. package/dist/src/devtools-entry.browser.d.ts +4 -0
  96. package/dist/src/devtools-entry.browser.d.ts.map +1 -0
  97. package/dist/src/devtools-entry.browser.js +2 -0
  98. package/dist/src/devtools-entry.browser.js.map +1 -0
  99. package/dist/src/devtools-entry.d.ts +4 -0
  100. package/dist/src/devtools-entry.d.ts.map +1 -0
  101. package/dist/src/devtools-entry.js +2 -0
  102. package/dist/src/devtools-entry.js.map +1 -0
  103. package/dist/src/diagnostics.d.ts +34 -0
  104. package/dist/src/diagnostics.d.ts.map +1 -0
  105. package/dist/src/diagnostics.js +89 -0
  106. package/dist/src/diagnostics.js.map +1 -0
  107. package/dist/src/index.d.ts +3 -2
  108. package/dist/src/index.d.ts.map +1 -1
  109. package/dist/src/index.js +3 -2
  110. package/dist/src/index.js.map +1 -1
  111. package/dist/src/print-hook.d.ts +14 -0
  112. package/dist/src/print-hook.d.ts.map +1 -0
  113. package/dist/src/print-hook.js +10 -0
  114. package/dist/src/print-hook.js.map +1 -0
  115. package/dist/src/reactive-union-set.d.ts.map +1 -1
  116. package/dist/src/reactive-union-set.js +15 -0
  117. package/dist/src/reactive-union-set.js.map +1 -1
  118. package/dist/src/reactivity.d.ts +17 -3
  119. package/dist/src/reactivity.d.ts.map +1 -1
  120. package/dist/src/reactivity.js +162 -14
  121. package/dist/src/reactivity.js.map +1 -1
  122. package/dist/src/render-stack.d.ts +17 -1
  123. package/dist/src/render-stack.d.ts.map +1 -1
  124. package/dist/src/render-stack.js +57 -1
  125. package/dist/src/render-stack.js.map +1 -1
  126. package/dist/src/render.d.ts +8 -15
  127. package/dist/src/render.d.ts.map +1 -1
  128. package/dist/src/render.js +362 -103
  129. package/dist/src/render.js.map +1 -1
  130. package/dist/src/resource.d.ts.map +1 -1
  131. package/dist/src/resource.js +5 -0
  132. package/dist/src/resource.js.map +1 -1
  133. package/dist/src/scheduler.d.ts +3 -0
  134. package/dist/src/scheduler.d.ts.map +1 -1
  135. package/dist/src/scheduler.js +45 -2
  136. package/dist/src/scheduler.js.map +1 -1
  137. package/dist/src/symbols/basic-symbol.d.ts.map +1 -1
  138. package/dist/src/symbols/basic-symbol.js +6 -1
  139. package/dist/src/symbols/basic-symbol.js.map +1 -1
  140. package/dist/src/symbols/decl.d.ts.map +1 -1
  141. package/dist/src/symbols/decl.js +5 -1
  142. package/dist/src/symbols/decl.js.map +1 -1
  143. package/dist/src/symbols/output-scope.d.ts +2 -1
  144. package/dist/src/symbols/output-scope.d.ts.map +1 -1
  145. package/dist/src/symbols/output-scope.js +13 -8
  146. package/dist/src/symbols/output-scope.js.map +1 -1
  147. package/dist/src/symbols/output-symbol.d.ts +1 -0
  148. package/dist/src/symbols/output-symbol.d.ts.map +1 -1
  149. package/dist/src/symbols/output-symbol.js +23 -6
  150. package/dist/src/symbols/output-symbol.js.map +1 -1
  151. package/dist/src/symbols/symbol-flow.d.ts.map +1 -1
  152. package/dist/src/symbols/symbol-flow.js +22 -6
  153. package/dist/src/symbols/symbol-flow.js.map +1 -1
  154. package/dist/src/symbols/symbol-slot.d.ts.map +1 -1
  155. package/dist/src/symbols/symbol-slot.js +15 -0
  156. package/dist/src/symbols/symbol-slot.js.map +1 -1
  157. package/dist/src/symbols/symbol-slot.test.d.ts +2 -0
  158. package/dist/src/symbols/symbol-slot.test.d.ts.map +1 -0
  159. package/dist/src/symbols/symbol-slot.test.js +35 -0
  160. package/dist/src/symbols/symbol-slot.test.js.map +1 -0
  161. package/dist/src/symbols/symbol-table.d.ts.map +1 -1
  162. package/dist/src/symbols/symbol-table.js +6 -5
  163. package/dist/src/symbols/symbol-table.js.map +1 -1
  164. package/dist/src/trace.d.ts +2 -0
  165. package/dist/src/trace.d.ts.map +1 -0
  166. package/dist/src/trace.js +2 -0
  167. package/dist/src/trace.js.map +1 -0
  168. package/dist/src/tracer.d.ts +2 -228
  169. package/dist/src/tracer.d.ts.map +1 -1
  170. package/dist/src/tracer.js +5 -298
  171. package/dist/src/tracer.js.map +1 -1
  172. package/dist/src/utils.d.ts.map +1 -1
  173. package/dist/src/utils.js +5 -0
  174. package/dist/src/utils.js.map +1 -1
  175. package/dist/test/components/append-file.test.d.ts.map +1 -1
  176. package/dist/test/components/append-file.test.js +18 -10
  177. package/dist/test/components/append-file.test.js.map +1 -1
  178. package/dist/test/components/template-file.test.d.ts.map +1 -1
  179. package/dist/test/components/template-file.test.js +6 -4
  180. package/dist/test/components/template-file.test.js.map +1 -1
  181. package/dist/test/rendering/basic.test.js +3 -0
  182. package/dist/test/rendering/basic.test.js.map +1 -1
  183. package/dist/test/rendering/print-render-stack.test.d.ts.map +1 -1
  184. package/dist/test/rendering/print-render-stack.test.js +91 -98
  185. package/dist/test/rendering/print-render-stack.test.js.map +1 -1
  186. package/dist/testing/create-test-wrapper.d.ts +1 -1
  187. package/dist/testing/create-test-wrapper.d.ts.map +1 -1
  188. package/dist/testing/create-test-wrapper.js +1 -1
  189. package/dist/testing/create-test-wrapper.js.map +1 -1
  190. package/dist/testing/devtools-utils.d.ts +26 -0
  191. package/dist/testing/devtools-utils.d.ts.map +1 -0
  192. package/dist/testing/devtools-utils.js +140 -0
  193. package/dist/testing/devtools-utils.js.map +1 -0
  194. package/dist/testing/extend-expect.d.ts.map +1 -1
  195. package/dist/testing/extend-expect.js +63 -1
  196. package/dist/testing/extend-expect.js.map +1 -1
  197. package/dist/testing/render.d.ts +2 -2
  198. package/dist/testing/render.d.ts.map +1 -1
  199. package/dist/testing/render.js +2 -2
  200. package/dist/testing/render.js.map +1 -1
  201. package/dist/tsconfig.tsbuildinfo +1 -1
  202. package/package.json +21 -7
  203. package/scripts/copy-devtools-ui.mjs +26 -0
  204. package/src/binder.ts +71 -16
  205. package/src/components/AppendFile.tsx +14 -9
  206. package/src/components/Block.tsx +1 -1
  207. package/src/components/Declaration.tsx +2 -1
  208. package/src/components/Scope.tsx +4 -1
  209. package/src/components/TemplateFile.tsx +18 -9
  210. package/src/content-slot.tsx +6 -6
  211. package/src/context.ts +15 -4
  212. package/src/{debug.ts → debug/cli.ts} +114 -125
  213. package/src/debug/diagnostics.test.tsx +55 -0
  214. package/src/debug/effects.test.tsx +96 -0
  215. package/src/debug/effects.ts +313 -0
  216. package/src/debug/files.test.tsx +96 -0
  217. package/src/debug/files.ts +40 -0
  218. package/src/debug/index.ts +126 -0
  219. package/src/debug/render.test.tsx +379 -0
  220. package/src/debug/render.ts +639 -0
  221. package/src/debug/serialize.ts +85 -0
  222. package/src/debug/symbols.test.tsx +106 -0
  223. package/src/debug/symbols.ts +230 -0
  224. package/src/debug/trace.ts +312 -0
  225. package/src/devtools/devtools-protocol.ts +312 -0
  226. package/src/devtools/devtools-server.browser.ts +71 -0
  227. package/src/devtools/devtools-server.ts +290 -0
  228. package/src/devtools/devtools-transport.ts +154 -0
  229. package/src/devtools-entry.browser.ts +52 -0
  230. package/src/devtools-entry.ts +54 -0
  231. package/src/diagnostics.ts +141 -0
  232. package/src/index.ts +2 -6
  233. package/src/print-hook.ts +22 -0
  234. package/src/reactive-union-set.ts +71 -41
  235. package/src/reactivity.ts +206 -23
  236. package/src/render-stack.ts +68 -1
  237. package/src/render.ts +464 -157
  238. package/src/resource.ts +28 -19
  239. package/src/scheduler.ts +55 -3
  240. package/src/symbols/basic-symbol.ts +6 -1
  241. package/src/symbols/decl.ts +5 -1
  242. package/src/symbols/output-scope.ts +21 -12
  243. package/src/symbols/output-symbol.ts +33 -12
  244. package/src/symbols/symbol-flow.ts +68 -37
  245. package/src/symbols/symbol-slot.test.tsx +41 -0
  246. package/src/symbols/symbol-slot.tsx +47 -20
  247. package/src/symbols/symbol-table.ts +6 -10
  248. package/src/trace.ts +1 -0
  249. package/src/tracer.ts +13 -242
  250. package/src/utils.tsx +22 -13
  251. package/temp/api.json +1675 -162
  252. package/test/components/append-file.test.tsx +36 -29
  253. package/test/components/template-file.test.tsx +11 -11
  254. package/test/rendering/basic.test.tsx +4 -0
  255. package/test/rendering/print-render-stack.test.tsx +52 -43
  256. package/testing/create-test-wrapper.tsx +1 -1
  257. package/testing/devtools-utils.ts +203 -0
  258. package/testing/extend-expect.ts +89 -0
  259. package/testing/render.ts +2 -2
  260. package/testing/vitest.d.ts +9 -0
  261. package/dist/src/debug.d.ts +0 -14
  262. package/dist/src/debug.d.ts.map +0 -1
  263. package/dist/src/debug.js.map +0 -1
@@ -3,7 +3,6 @@ import { tmpdir } from "os";
3
3
  import { join } from "path";
4
4
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
5
  import { AppendFile, AppendRegion } from "../../src/components/AppendFile.jsx";
6
- import { render, renderAsync } from "../../src/render.js";
7
6
  import "../../testing/extend-expect.js";
8
7
  import { d } from "../../testing/render.js";
9
8
 
@@ -34,6 +33,7 @@ describe("AppendFile", () => {
34
33
 
35
34
  await expect(result).toRenderToAsync("Initial content\nNew content");
36
35
  });
36
+
37
37
  it("should append content to end of file when no sigils present with no explicit append region", async () => {
38
38
  // Create initial file content
39
39
  writeFileSync(testFilePath, "Initial content", "utf-8");
@@ -148,30 +148,35 @@ describe("AppendFile", () => {
148
148
  await expect(result).toRenderToAsync("Content\ndefault region");
149
149
  });
150
150
 
151
- it("should throw error when region is missing corresponding AppendRegion", async () => {
151
+ it("should emit diagnostic when region is missing corresponding AppendRegion", async () => {
152
152
  writeFileSync(testFilePath, "content", "utf-8");
153
153
 
154
- expect(() =>
155
- render(
156
- <AppendFile path={testFilePath} regions={["missing"]}>
157
- <AppendRegion id="append">content</AppendRegion>
158
- </AppendFile>,
159
- ),
160
- ).toThrow(
161
- 'Region "missing" specified but no corresponding AppendRegion child found',
162
- );
154
+ await expect(
155
+ <AppendFile path={testFilePath} regions={["missing"]}>
156
+ <AppendRegion id="append">content</AppendRegion>
157
+ </AppendFile>,
158
+ ).toHaveDiagnosticsAsync([
159
+ {
160
+ message:
161
+ 'Region "missing" specified but no corresponding AppendRegion child found',
162
+ severity: "error",
163
+ },
164
+ ]);
163
165
  });
164
166
 
165
- it("should throw error when AppendRegion has neither children nor content", async () => {
167
+ it("should emit diagnostic when AppendRegion has neither children nor content", async () => {
166
168
  writeFileSync(testFilePath, "content", "utf-8");
167
169
 
168
- expect(() =>
169
- render(
170
- <AppendFile path={testFilePath}>
171
- <AppendRegion id="append" />
172
- </AppendFile>,
173
- ),
174
- ).toThrow('AppendRegion "append" must have either children or content');
170
+ await expect(
171
+ <AppendFile path={testFilePath}>
172
+ <AppendRegion id="append" />
173
+ </AppendFile>,
174
+ ).toHaveDiagnosticsAsync([
175
+ {
176
+ message: 'AppendRegion "append" must have either children or content',
177
+ severity: "error",
178
+ },
179
+ ]);
175
180
  });
176
181
 
177
182
  it("should throw error when region has missing start sigil", async () => {
@@ -196,7 +201,7 @@ describe("AppendFile", () => {
196
201
  `);
197
202
  });
198
203
 
199
- it("should throw error when region has missing end sigil", async () => {
204
+ it("should emit diagnostic when region has missing end sigil", async () => {
200
205
  const contentWithOnlyStart = d`
201
206
  Content
202
207
  <!-- alloy-incomplete-start -->
@@ -204,15 +209,17 @@ describe("AppendFile", () => {
204
209
 
205
210
  writeFileSync(testFilePath, contentWithOnlyStart, "utf-8");
206
211
 
207
- await expect(async () =>
208
- renderAsync(
209
- <AppendFile path={testFilePath} regions={["incomplete"]}>
210
- <AppendRegion id="incomplete">content</AppendRegion>
211
- </AppendFile>,
212
- ),
213
- ).rejects.toThrow(
214
- 'Region "incomplete" has start sigil but no corresponding end sigil',
215
- );
212
+ await expect(
213
+ <AppendFile path={testFilePath} regions={["incomplete"]}>
214
+ <AppendRegion id="incomplete">content</AppendRegion>
215
+ </AppendFile>,
216
+ ).toHaveDiagnosticsAsync([
217
+ {
218
+ message:
219
+ 'Region "incomplete" has start sigil but no corresponding end sigil',
220
+ severity: "error",
221
+ },
222
+ ]);
216
223
  });
217
224
 
218
225
  it("should handle complex nested content", async () => {
@@ -6,7 +6,6 @@ import {
6
6
  TemplateFile,
7
7
  TemplateVariable,
8
8
  } from "../../src/components/TemplateFile.jsx";
9
- import { renderAsync } from "../../src/render.js";
10
9
  import "../../testing/extend-expect.js";
11
10
  import { d } from "../../testing/render.js";
12
11
 
@@ -82,21 +81,22 @@ describe("TemplateFile", () => {
82
81
  await expect(result).toRenderToAsync("Hello Bob!");
83
82
  });
84
83
 
85
- it("should throw error for missing template variables", async () => {
84
+ it("should emit diagnostic for missing template variables", async () => {
86
85
  const templatePath = join(tmpdir(), "test-missing-var-template.txt");
87
86
  const templateContent = "Hello {{ name }}! Your age is {{ age }}.";
88
87
  writeFileSync(templatePath, templateContent);
89
88
 
90
89
  await expect(
91
- async () =>
92
- await renderAsync(
93
- <TemplateFile src={templatePath} path="output.txt">
94
- <TemplateVariable name="name" value="Charlie" />
95
- </TemplateFile>,
96
- ),
97
- ).rejects.toThrow(
98
- 'Template variable "age" not found in TemplateVariable children',
99
- );
90
+ <TemplateFile src={templatePath} path="output.txt">
91
+ <TemplateVariable name="name" value="Charlie" />
92
+ </TemplateFile>,
93
+ ).toHaveDiagnosticsAsync([
94
+ {
95
+ message:
96
+ 'Template variable "age" not found in TemplateVariable children',
97
+ severity: "error",
98
+ },
99
+ ]);
100
100
  });
101
101
 
102
102
  it("should handle template with no variables", async () => {
@@ -166,3 +166,7 @@ it("keeps spaces between expressions", () => {
166
166
  </>,
167
167
  ).toRenderTo("a str str getStr getStr c");
168
168
  });
169
+
170
+ it("renders numbers", () => {
171
+ expect(<>200</>).toRenderTo("200");
172
+ });
@@ -1,8 +1,17 @@
1
- import { Output, SourceDirectory, SourceFile } from "@alloy-js/core";
1
+ import {
2
+ Output,
3
+ SourceDirectory,
4
+ SourceFile,
5
+ renderAsync,
6
+ } from "@alloy-js/core";
2
7
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
8
+ import WebSocket from "ws";
3
9
  import { createNamedContext } from "../../src/context.js";
10
+ import {
11
+ enableDevtools,
12
+ resetDevtoolsServerForTests,
13
+ } from "../../src/devtools/devtools-server.js";
4
14
  import { clearRenderStack } from "../../src/render-stack.js";
5
- import { renderTree } from "../../src/render.js";
6
15
  import "../../testing/extend-expect.js";
7
16
 
8
17
  // Strip ANSI escape codes from a string for consistent testing across environments
@@ -21,27 +30,29 @@ function expectErrorContaining(
21
30
  }
22
31
 
23
32
  describe("printRenderStack", () => {
24
- let originalEnv: string | undefined;
25
-
26
- beforeEach(() => {
27
- // Enable debug mode
28
- originalEnv = process.env.ALLOY_DEBUG;
29
- process.env.ALLOY_DEBUG = "1";
33
+ let socket: WebSocket | undefined;
34
+
35
+ beforeEach(async () => {
36
+ const server = await enableDevtools({ port: 0 });
37
+ socket = new WebSocket(`ws://127.0.0.1:${server.port}`);
38
+ await new Promise<void>((resolve, reject) => {
39
+ socket?.once("open", resolve);
40
+ socket?.once("error", reject);
41
+ });
30
42
  });
31
43
 
32
- afterEach(() => {
33
- // Restore environment
34
- if (originalEnv === undefined) {
35
- delete process.env.ALLOY_DEBUG;
36
- } else {
37
- process.env.ALLOY_DEBUG = originalEnv;
44
+ afterEach(async () => {
45
+ if (socket) {
46
+ socket.close();
47
+ socket = undefined;
38
48
  }
49
+ await resetDevtoolsServerForTests();
39
50
 
40
51
  // Clear render stack to prevent state leakage between tests
41
52
  clearRenderStack();
42
53
  });
43
54
 
44
- it("prints the current file when an error occurs", () => {
55
+ it("prints the current file when an error occurs", async () => {
45
56
  const consoleErrorSpy = vi.spyOn(console, "error");
46
57
 
47
58
  function ThrowingComponent() {
@@ -52,15 +63,15 @@ describe("printRenderStack", () => {
52
63
  return <ThrowingComponent />;
53
64
  }
54
65
 
55
- expect(() => {
56
- renderTree(
66
+ await expect(
67
+ renderAsync(
57
68
  <Output>
58
69
  <SourceFile path="test.ts" filetype="typescript">
59
70
  <ParentComponent />
60
71
  </SourceFile>
61
72
  </Output>,
62
- );
63
- }).toThrow("Test error");
73
+ ),
74
+ ).rejects.toThrow("Test error");
64
75
 
65
76
  // Check that console.error was called with file path
66
77
  expectErrorContaining(consoleErrorSpy, "Error rendering in file test.ts");
@@ -70,15 +81,15 @@ describe("printRenderStack", () => {
70
81
  consoleErrorSpy.mockRestore();
71
82
  });
72
83
 
73
- it("prints joined path from nested directories", () => {
84
+ it("prints joined path from nested directories", async () => {
74
85
  const consoleErrorSpy = vi.spyOn(console, "error");
75
86
 
76
87
  function ThrowingComponent() {
77
88
  throw new Error("Nested error");
78
89
  }
79
90
 
80
- expect(() => {
81
- renderTree(
91
+ await expect(
92
+ renderAsync(
82
93
  <Output>
83
94
  <SourceDirectory path="dir1">
84
95
  <SourceDirectory path="dir2">
@@ -88,8 +99,8 @@ describe("printRenderStack", () => {
88
99
  </SourceDirectory>
89
100
  </SourceDirectory>
90
101
  </Output>,
91
- );
92
- }).toThrow("Nested error");
102
+ ),
103
+ ).rejects.toThrow("Nested error");
93
104
 
94
105
  // Should show the joined path of all directories
95
106
  expectErrorContaining(
@@ -100,7 +111,7 @@ describe("printRenderStack", () => {
100
111
  consoleErrorSpy.mockRestore();
101
112
  });
102
113
 
103
- it("works when no file context is present", () => {
114
+ it("works when no file context is present", async () => {
104
115
  const consoleErrorSpy = vi.spyOn(console, "error");
105
116
 
106
117
  function ThrowingComponent() {
@@ -110,13 +121,13 @@ describe("printRenderStack", () => {
110
121
  // Track the number of calls before our test
111
122
  const callsBefore = consoleErrorSpy.mock.calls.length;
112
123
 
113
- expect(() => {
114
- renderTree(
124
+ await expect(
125
+ renderAsync(
115
126
  <Output>
116
127
  <ThrowingComponent />
117
128
  </Output>,
118
- );
119
- }).toThrow("No file context error");
129
+ ),
130
+ ).rejects.toThrow("No file context error");
120
131
 
121
132
  // Get only the calls from THIS test (after callsBefore)
122
133
  const callsFromThisTest = consoleErrorSpy.mock.calls.slice(callsBefore);
@@ -135,7 +146,7 @@ describe("printRenderStack", () => {
135
146
  consoleErrorSpy.mockRestore();
136
147
  });
137
148
 
138
- it("includes component stack with props", () => {
149
+ it("includes component stack with props", async () => {
139
150
  const consoleErrorSpy = vi.spyOn(console, "error");
140
151
 
141
152
  function ThrowingComponent(props: { message: string; count: number }) {
@@ -146,15 +157,15 @@ describe("printRenderStack", () => {
146
157
  return <ThrowingComponent message={props.value} count={42} />;
147
158
  }
148
159
 
149
- expect(() => {
150
- renderTree(
160
+ await expect(
161
+ renderAsync(
151
162
  <Output>
152
163
  <SourceFile path="props-test.ts" filetype="typescript">
153
164
  <WrapperComponent value="test" />
154
165
  </SourceFile>
155
166
  </Output>,
156
- );
157
- }).toThrow("Component error");
167
+ ),
168
+ ).rejects.toThrow("Component error");
158
169
 
159
170
  expectErrorContaining(
160
171
  consoleErrorSpy,
@@ -168,7 +179,7 @@ describe("printRenderStack", () => {
168
179
  consoleErrorSpy.mockRestore();
169
180
  });
170
181
 
171
- it("prints 'Error rendering:' when no file or directory context is present", () => {
182
+ it("prints 'Error rendering:' when no file or directory context is present", async () => {
172
183
  const consoleErrorSpy = vi.spyOn(console, "error");
173
184
 
174
185
  function ThrowingComponent() {
@@ -179,9 +190,7 @@ describe("printRenderStack", () => {
179
190
  const callsBefore = consoleErrorSpy.mock.calls.length;
180
191
 
181
192
  // Don't use Output wrapper to avoid SourceDirectory context
182
- expect(() => {
183
- renderTree(<ThrowingComponent />);
184
- }).toThrow();
193
+ await expect(renderAsync(<ThrowingComponent />)).rejects.toThrow();
185
194
 
186
195
  // Get only the calls from THIS test (after callsBefore)
187
196
  const callsFromThisTest = consoleErrorSpy.mock.calls.slice(callsBefore);
@@ -205,7 +214,7 @@ describe("printRenderStack", () => {
205
214
  consoleErrorSpy.mockRestore();
206
215
  });
207
216
 
208
- it("shows context name for named context providers", () => {
217
+ it("shows context name for named context providers", async () => {
209
218
  const consoleErrorSpy = vi.spyOn(console, "error");
210
219
 
211
220
  const MyContext = createNamedContext<string>("MyContext");
@@ -214,8 +223,8 @@ describe("printRenderStack", () => {
214
223
  throw new Error("Context error");
215
224
  }
216
225
 
217
- expect(() => {
218
- renderTree(
226
+ await expect(
227
+ renderAsync(
219
228
  <Output>
220
229
  <SourceFile path="context-test.ts" filetype="typescript">
221
230
  <MyContext.Provider value="test-value">
@@ -223,8 +232,8 @@ describe("printRenderStack", () => {
223
232
  </MyContext.Provider>
224
233
  </SourceFile>
225
234
  </Output>,
226
- );
227
- }).toThrow("Context error");
235
+ ),
236
+ ).rejects.toThrow("Context error");
228
237
 
229
238
  // Check that the named context provider is shown as a separate component
230
239
  expectErrorContaining(consoleErrorSpy, "at MyContext");
@@ -7,7 +7,7 @@ import {
7
7
  Output,
8
8
  OutputSymbol,
9
9
  shallowReactive,
10
- } from "@alloy-js/core";
10
+ } from "../src/index.js";
11
11
 
12
12
  export interface TestWrapper {
13
13
  Wrapper: (props: { children: Children }) => Children;
@@ -0,0 +1,203 @@
1
+ import WebSocket from "ws";
2
+
3
+ export interface DevtoolsMessage {
4
+ type: string;
5
+ [key: string]: unknown;
6
+ }
7
+
8
+ /**
9
+ * Creates a message collector that accumulates messages and provides utilities
10
+ * for waiting on conditions. Useful for tests with reactive updates.
11
+ */
12
+ export function createMessageCollector(socket: WebSocket) {
13
+ let renderBuffer: DevtoolsMessage[] = [];
14
+ let flushBuffer: DevtoolsMessage[] = [];
15
+ const completedRenderBatches: DevtoolsMessage[][] = [];
16
+ const completedFlushBatches: DevtoolsMessage[][] = [];
17
+ const renderWaiters: Array<{
18
+ resolve: (messages: DevtoolsMessage[]) => void;
19
+ reject: (error: Error) => void;
20
+ timeout: NodeJS.Timeout;
21
+ }> = [];
22
+ const flushWaiters: Array<{
23
+ resolve: (messages: DevtoolsMessage[]) => void;
24
+ reject: (error: Error) => void;
25
+ timeout: NodeJS.Timeout;
26
+ }> = [];
27
+
28
+ const resolveBatch = (
29
+ batch: DevtoolsMessage[],
30
+ waiters: typeof renderWaiters,
31
+ completed: DevtoolsMessage[][],
32
+ ) => {
33
+ if (waiters.length > 0) {
34
+ for (const waiter of waiters.splice(0, waiters.length)) {
35
+ clearTimeout(waiter.timeout);
36
+ waiter.resolve(batch);
37
+ }
38
+ } else {
39
+ completed.push(batch);
40
+ }
41
+ };
42
+
43
+ const purgeMessages = (
44
+ targetBuffer: DevtoolsMessage[],
45
+ targetCompleted: DevtoolsMessage[][],
46
+ batch: DevtoolsMessage[],
47
+ ) => {
48
+ if (batch.length === 0) {
49
+ return {
50
+ buffer: targetBuffer,
51
+ completed: targetCompleted,
52
+ };
53
+ }
54
+
55
+ const toRemove = new Set(batch);
56
+ const buffer = targetBuffer.filter((msg) => !toRemove.has(msg));
57
+ const completed = targetCompleted
58
+ .map((messages) => messages.filter((msg) => !toRemove.has(msg)))
59
+ .filter((messages) => messages.length > 0);
60
+
61
+ return { buffer, completed };
62
+ };
63
+
64
+ const onMessage = (data: WebSocket.RawData) => {
65
+ try {
66
+ const message = JSON.parse(String(data)) as DevtoolsMessage;
67
+ renderBuffer.push(message);
68
+ flushBuffer.push(message);
69
+
70
+ if (
71
+ message.type === "render:complete" ||
72
+ message.type === "render:error"
73
+ ) {
74
+ const batch = renderBuffer;
75
+ renderBuffer = [];
76
+ resolveBatch(batch, renderWaiters, completedRenderBatches);
77
+ }
78
+
79
+ if (message.type === "flushJobs:complete") {
80
+ const batch = flushBuffer;
81
+ flushBuffer = [];
82
+ resolveBatch(batch, flushWaiters, completedFlushBatches);
83
+ }
84
+ } catch {
85
+ // ignore invalid messages
86
+ }
87
+ };
88
+
89
+ socket.on("message", onMessage);
90
+
91
+ return {
92
+ waitForRender(): Promise<DevtoolsMessage[]> {
93
+ if (completedRenderBatches.length > 0) {
94
+ const batch = completedRenderBatches.shift()!;
95
+ const purged = purgeMessages(flushBuffer, completedFlushBatches, batch);
96
+ flushBuffer = purged.buffer;
97
+ completedFlushBatches.length = 0;
98
+ return Promise.resolve(batch);
99
+ }
100
+
101
+ return new Promise((resolve, reject) => {
102
+ const timeout = setTimeout(() => {
103
+ const types = renderBuffer.map((m) => m.type).join(", ");
104
+ reject(
105
+ new Error(
106
+ `Timed out waiting for render completion. Received ${renderBuffer.length} messages${types ? `: ${types}` : "."}`,
107
+ ),
108
+ );
109
+ }, 2000);
110
+
111
+ renderWaiters.push({
112
+ resolve: (messages) => {
113
+ const purged = purgeMessages(
114
+ flushBuffer,
115
+ completedFlushBatches,
116
+ messages,
117
+ );
118
+ flushBuffer = purged.buffer;
119
+ completedFlushBatches.length = 0;
120
+ resolve(messages);
121
+ },
122
+ reject,
123
+ timeout,
124
+ });
125
+ });
126
+ },
127
+ waitForFlush(): Promise<DevtoolsMessage[]> {
128
+ if (completedFlushBatches.length > 0) {
129
+ const batch = completedFlushBatches.shift()!;
130
+ const purged = purgeMessages(
131
+ renderBuffer,
132
+ completedRenderBatches,
133
+ batch,
134
+ );
135
+ renderBuffer = purged.buffer;
136
+ completedRenderBatches.length = 0;
137
+ completedRenderBatches.push(...purged.completed);
138
+ return Promise.resolve(batch);
139
+ }
140
+
141
+ return new Promise((resolve, reject) => {
142
+ const timeout = setTimeout(() => {
143
+ const types = flushBuffer.map((m) => m.type).join(", ");
144
+ reject(
145
+ new Error(
146
+ `Timed out waiting for flushJobs:complete. Received ${flushBuffer.length} messages${types ? `: ${types}` : "."}`,
147
+ ),
148
+ );
149
+ }, 2000);
150
+
151
+ flushWaiters.push({
152
+ resolve: (messages) => {
153
+ const purged = purgeMessages(
154
+ renderBuffer,
155
+ completedRenderBatches,
156
+ messages,
157
+ );
158
+ renderBuffer = purged.buffer;
159
+ completedRenderBatches.length = 0;
160
+ completedRenderBatches.push(...purged.completed);
161
+ resolve(messages);
162
+ },
163
+ reject,
164
+ timeout,
165
+ });
166
+ });
167
+ },
168
+ /**
169
+ * Stop collecting messages
170
+ */
171
+ stop() {
172
+ socket.off("message", onMessage);
173
+ for (const waiter of renderWaiters.splice(0, renderWaiters.length)) {
174
+ clearTimeout(waiter.timeout);
175
+ waiter.reject(new Error("Collector stopped before render completion."));
176
+ }
177
+ for (const waiter of flushWaiters.splice(0, flushWaiters.length)) {
178
+ clearTimeout(waiter.timeout);
179
+ waiter.reject(
180
+ new Error("Collector stopped before flushJobs:complete."),
181
+ );
182
+ }
183
+ },
184
+ };
185
+ }
186
+
187
+ /**
188
+ * Filter messages to only include render tree messages (those starting with "render:")
189
+ */
190
+ export function filterRenderTreeMessages(
191
+ messages: DevtoolsMessage[],
192
+ ): DevtoolsMessage[] {
193
+ return messages.filter((m) => m.type.startsWith("render"));
194
+ }
195
+
196
+ /**
197
+ * Filter messages to only include effect debug messages (those starting with "effect:")
198
+ */
199
+ export function filterEffectsMessages(
200
+ messages: DevtoolsMessage[],
201
+ ): DevtoolsMessage[] {
202
+ return messages.filter((m) => m.type.startsWith("effect:"));
203
+ }